UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Day 52 Cupcake Challenge Question 3

Forums > 100 Days of SwiftUI

I finished Day 52 but I'm not sure if this is correct.

https://www.hackingwithswift.com/books/ios-swiftui/cupcake-corner-wrap-up

  1. Our address fields are currently considered valid if they contain anything, even if it’s just only whitespace. Improve the validation to make sure a string of pure whitespace is invalid.

  2. If our call to placeOrder() fails – for example if there is no internet connection – show an informative alert for the user. To test this, try commenting out the request.httpMethod = "POST" line in your code, which should force the request to fail.

3. For a more challenging task, try updating the Order class so it saves data such as the user's delivery address to UserDefaults. This takes a little thinking, because @AppStorage won't work here, and you'll find getters and settings cause problems with Codable support. Can you find a middle ground?

I'm confused what was expected here in point number 3 bolded above, so I simply converted the address into a struct and used JSONEncoder and Decoder and to test it out, navigated to a new form after the order was placed to show address details, is this correct? If not, what is expected?

Order class

//
//  Order.swift
//  CupcakeCorner
//
//

import Foundation

struct UserAddress: Codable {
    var name = ""
    var streetAddress = ""
    var city = ""
    var zip = ""
}

@Observable
class Order: Codable {

    enum CodingKeys: String, CodingKey {
        case _type = "type"
        case _quantity = "quantity"
        case _specialRequestEnabled = "specialRequestEnabled"
        case _extraFrosting = "extraFrosting"
        case _addSprinkles = "addSprinkles"
        case _userAddress = "userAddress"
    }

    static let types = ["Vanilla", "Strawberry", "Chocolate", "Rainbow"]

    var type = 0
    var quantity = 3

    var specialRequestEnabled = false {
        didSet {
            if specialRequestEnabled == false {
                extraFrosting = false
                addSprinkles = false
            }
        }
    }

    var extraFrosting = false
    var addSprinkles = false        

    var userAddress: UserAddress {
        didSet {
            if let encoded = try? JSONEncoder().encode(userAddress) {
                UserDefaults.standard.set(encoded, forKey: "UserAddress")
            }
        }
    }

    init() {
        if let address = UserDefaults.standard.data(forKey: "UserAddress") {
            if let decodedItems = try? JSONDecoder().decode(UserAddress.self, from: address) {
                userAddress = decodedItems
                return
            }
        }  
       userAddress = UserAddress()
    }

    var hasValidAddress: Bool {

        if userAddress.name.isEmpty || userAddress.streetAddress.isEmpty || userAddress.city.isEmpty || userAddress.zip.isEmpty {
            return false
        }

        if userAddress.name.containsOnlyWhitespace() || userAddress.streetAddress.containsOnlyWhitespace() || userAddress.city.containsOnlyWhitespace() || userAddress.zip.containsOnlyWhitespace() {
            return false
        }

        return true
    }

    var cost: Decimal {
        //$2 per cake
        var cost = Decimal(quantity * 2)

        //complicated cakes cost more
        cost += Decimal(type) / 2

        // $1/cake for extra frosting        
        if extraFrosting {
            cost += Decimal(quantity)
        }
        if addSprinkles { 
            // $0.50/cake for extra sprinkles
            cost += Decimal(quantity) / 2
        }

        return cost
    }
}

extension String {
    func containsOnlyWhitespace() -> Bool {
        return self.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
    }
}

Updated CheckoutView

//
//  CheckoutView.swift
//  CupcakeCorner
//
//

import SwiftUI

struct CheckoutView: View {
    var order: Order

    @State private var alertTitle = ""
    @State private var alertMessage = ""
    @State private var showingAlert = false

    @State private var showAddressLink = false

    var body: some View {
        ScrollView {
            VStack {
                AsyncImage(url: URL(string: "https://hws.dev/img/cupcakes@3x.jpg"),
                           scale: 3) { image in
                    image
                        .resizable()
                        .scaledToFit()
                } placeholder: {
                    ProgressView()
                }
                .frame(height: 233)

                Text("Your total cost is \(order.cost, format: .currency(code: "USD"))")
                    .font(.title)

                Button("Place Order") {
                    Task {
                        await placeOrder(for: order)
                    }
                }
                    .padding()
            }

            if showAddressLink {
                NavigationLink("Order details", destination: CustomerAddressView(order: order))
            }
        }
        .navigationTitle("Check out")
        .navigationBarTitleDisplayMode(.inline)
        .scrollBounceBehavior(.basedOnSize)
        .alert(alertTitle,isPresented: $showingAlert) {
            Button("OK") {
                showAddressLink.toggle()
            }
        } message: {
            Text(alertMessage)
        }
    }

    func placeOrder(for _order: Order) async {
        guard let encoded = try? JSONEncoder().encode(order) else {
            print("Failed to encode order")
            return
        }

        let url = URL(string: "https://reqres.in/api/cupcakes")!
        var request = URLRequest(url: url)
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpMethod = "POST"

        do {
            let(data, _) = try await URLSession.shared.upload(for: request, from: encoded)

            let decodedOrder = try JSONDecoder().decode(Order.self, from: data)
            alertTitle = "Thank you!"
            alertMessage = "Your order for \(decodedOrder.quantity)x \(Order.types[decodedOrder.type].lowercased()) cupcakes is on the way!"
            showingAlert = true

        } catch {
            print("Check out failed \(error.localizedDescription)")
            alertTitle = "Network error"
            alertMessage = "Oops, order failed to send! Please try again."
            showingAlert = true
        }
    }
}

#Preview {
    CheckoutView(order: Order())
}

New view to show address details

//
//  CustomerAddressView.swift
//  CupcakeCorner
//
//

import SwiftUI

struct CustomerAddressView: View {
    var order: Order
    var body: some View {
        NavigationStack {
            Form {
                VStack(alignment: .leading, spacing: 10) {
                    Text("Name: \(order.userAddress.name)")
                        .font(.title2)
                    Text("Street Address: \(order.userAddress.streetAddress)")
                        .font(.title2)
                    Text("City: \(order.userAddress.city)")
                        .font(.title2)
                    Text("Zip: \(order.userAddress.zip)")
                        .font(.title2)
                }
                .navigationTitle("User details")
            }
        } 
    }
}

#Preview {
    var address = UserAddress(name: "Tom", streetAddress: "123 cupcake street", city: "London", zip : String(123))
    var order = Order()
    order.userAddress = address
    return CustomerAddressView(order: order)
}

2      

I think I solved it! But if you think I have not, please feel free to leave suggestions :)

It's not the cleanest code and definitely not something we should use for UserDefaults but somehow had to make it work as this challenge was bugging me.

1) Abstract address into a Struct 2) Perform some decoding/encoding in Order class using a key with a unique name of 'UUID + UserAddress' for each order's address 3) Create a new Addresses class and retrieve all UUIDs from a list of UUID keys 4) Iterate through each ID in the list to retrieve each unique key name of the address and store it in a UserAddress Array 5) Present a new list of addresses view to retrieve and display the saved address( for testing purposes)

Here's the code for anyone curious:

Order Class

import Foundation

struct UserAddress: Codable, Identifiable {
    var id = UUID()
    var name = ""
    var streetAddress = ""
    var city = ""
    var zip = ""
}

@Observable
class Addresses: Codable, RandomAccessCollection {
    var listOfAddressIds = [UUID]()
    var addresses = [UserAddress]()

    var startIndex: Int { listOfAddressIds.startIndex }
       var endIndex: Int { listOfAddressIds.endIndex }

       subscript(position: Int) -> UserAddress {
           return addresses[position]
       }

    init() {
        if let savedItems = UserDefaults.standard.data(forKey: "addressIDs") {
            if let decodedIds = try? JSONDecoder().decode([UUID].self, from: savedItems) {
                listOfAddressIds = decodedIds
            } else {
                listOfAddressIds = []
            }
        }        

        for id in listOfAddressIds {
            if let savedItem = UserDefaults.standard.data(forKey: "UserAddress \(id)") {
                if let decodedAddress = try? JSONDecoder().decode(UserAddress.self, from: savedItem) {
                    addresses.append(decodedAddress)
                } else {
                    addresses = []
                }
            } 
        }
    }    
}

@Observable
class Order: Codable {

    enum CodingKeys: String, CodingKey {
        case _type = "type"
        case _quantity = "quantity"
        case _specialRequestEnabled = "specialRequestEnabled"
        case _extraFrosting = "extraFrosting"
        case _addSprinkles = "addSprinkles"
        case _userAddress = "userAddress"
        case _id = "id"
    }

    static let types = ["Vanilla", "Strawberry", "Chocolate", "Rainbow"]

    var type = 0
    var quantity = 3

    var specialRequestEnabled = false {
        didSet {
            if specialRequestEnabled == false {
                extraFrosting = false
                addSprinkles = false
            }
        }
    }

    var extraFrosting = false
    var addSprinkles = false        

    var userAddress: UserAddress {
        didSet {
            let key = "UserAddress \(id)"
            if let encoded = try? JSONEncoder().encode(userAddress) {
                UserDefaults.standard.set(encoded, forKey: key)
            }
        }
    }

    var id: UUID
    var addressIDs = [UUID]() {
        didSet {
            if let encoded = try? JSONEncoder().encode(addressIDs) {
                UserDefaults.standard.set(encoded, forKey: "addressIDs")
            }
        }
    }

    init() {

        if let savedItems = UserDefaults.standard.data(forKey: "addressIDs") {
            if let decodedItems = try? JSONDecoder().decode([UUID].self, from: savedItems) {
                addressIDs = decodedItems
            } else {
                addressIDs = [] 
            }
        }      

        type = 0
        quantity = 3
        specialRequestEnabled = false
        extraFrosting = false
        addSprinkles = false
        userAddress = UserAddress()
        id = UUID()
        addressIDs.append(id)
    }

    var hasValidAddress: Bool {

        if userAddress.name.isEmpty || userAddress.streetAddress.isEmpty || userAddress.city.isEmpty || userAddress.zip.isEmpty {
            return false
        }

        if userAddress.name.containsOnlyWhitespace() || userAddress.streetAddress.containsOnlyWhitespace() || userAddress.city.containsOnlyWhitespace() || userAddress.zip.containsOnlyWhitespace() {
            return false
        }

        return true
    }

    var cost: Decimal {
        //$2 per cake
        var cost = Decimal(quantity * 2)

        //complicated cakes cost more
        cost += Decimal(type) / 2

        // $1/cake for extra frosting        
        if extraFrosting {
            cost += Decimal(quantity)
        }
        if addSprinkles { 
            // $0.50/cake for extra sprinkles
            cost += Decimal(quantity) / 2
        }

        return cost
    }
}

extension String {
    func containsOnlyWhitespace() -> Bool {
        return self.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
    }
}

New CustomerAddressView to see list of saved addresses

import SwiftUI

struct CustomerAddressView: View {
    @State private var addresses = Addresses()
    var body: some View {
        NavigationStack {
            VStack {
                List(addresses) { address in
                    VStack(alignment: .leading) {
                        Text(address.name)
                        Text(address.streetAddress)
                        Text(address.city)
                        Text(address.zip)
                    }
                }
            }
        } 
    }
}

#Preview {
    CustomerAddressView()
}

2      

@Blyte  

I know this is a few months old, but putting this here in case someone else stumbles upon this thread.

In the order class I used UserDefaults to read the values and used didSet to set the values, although I'm not sure if it's the most optimal solution. It seems to be working fine though.

var name = UserDefaults.standard.string(forKey: "name") ?? "" {
      didSet {
          UserDefaults.standard.setValue(name, forKey: "name")
      }
  }

  var streetAddress = UserDefaults.standard.string(forKey: "streetAddress") ?? "" {
      didSet {
          UserDefaults.standard.setValue(streetAddress, forKey: "streetAddress")
      }
  }

  var city = UserDefaults.standard.string(forKey: "city") ?? "" {
      didSet {
          UserDefaults.standard.setValue(city, forKey: "city")
      }
  }

  var zip = UserDefaults.standard.string(forKey: "zip") ?? "" {
      didSet {
          UserDefaults.standard.setValue(zip, forKey: "zip")
      }
  }

2      

TAKE YOUR SKILLS TO THE NEXT LEVEL If you like Hacking with Swift, you'll love Hacking with Swift+ – it's my premium service where you can learn advanced Swift and SwiftUI, functional programming, algorithms, and more. Plus it comes with stacks of benefits, including monthly live streams, downloadable projects, a 20% discount on all books, and free gifts!

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.