I finished Day 52 but I'm not sure if this is correct.
https://www.hackingwithswift.com/books/ios-swiftui/cupcake-corner-wrap-up
-
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.
-
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)
}