100 Days of SwiftUI Day 52

100 Days of SwiftUI – Day 52

We’ve arrived at day 52 of the 100 Days of SwiftUI! Yesterday, we finished the implementation of the Cupcake Corner app. It turned out great! Today, we’re wrapping up this project as we look to complete 3 new challenges. Let’s dive in!

Cupcake Corner SwiftUI challenge #1

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

extension String {
    var isReallyEmpty: Bool {
        self.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
    }
}
    var hasValidAddress: Bool { // Used to validate that none of the address fields are empty
        if name.isReallyEmpty || streetAddress.isReallyEmpty || city.isReallyEmpty || zip.isReallyEmpty {
            return false
        }
        return true
    }

Cupcake Corner SwiftUI challenge #2

The second challenge: 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.

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


import SwiftUI

struct CheckoutView: View {
    
    @ObservedObject var order: Order
    
    @State private var confirmationMessage = ""
    @State private var showingConfirmation = false
    @State private var errorMessage = ""
    @State private var showingError = false
    
    var body: some View {
        ScrollView {
            VStack {
                // Load an image from the internet
                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 is \(order.cost, format: .currency(code: "USD"))")
                    .font(.title)
                
                Button("Place Order") {
                    Task { // a button does not support an async function by default. A task + await resolves this
                        await placeOrder()
                    }
                }
                    .padding()
            }
        }
        .navigationTitle("Checkout")
        .navigationBarTitleDisplayMode(.inline)
        .alert("Thank you!", isPresented: $showingConfirmation) {
            Button("OK") {}
        } message: {
            Text(confirmationMessage)
        }
        .alert("Oops", isPresented: $showingError) {
            Button("OK") {}
        } message: {
            Text(errorMessage)
        }
    }
    
    func placeOrder() async { // Just like when downloading, we need to use async when uploading
        guard let encoded = try? JSONEncoder().encode(order) else { // try to encode our order and if it fails for whatever reason, print and end the function
            print("Failed to encode order")
            return
        }
        
        let url = URL(string: "https://reqres.in/api/cupcakes")! // The exclamation mark is to indicate that we should not get back an optional in case the URL is wrong.
        var request = URLRequest(url: url)
        request.setValue("application/json", forHTTPHeaderField: "Content-Type") // we are writing JSON f
        // request.httpMethod = "POST" // POST is for writing data to a server. GET is to read data.
        
        do {
            let (data, _) = try await URLSession.shared.upload(for: request, from: encoded)
            let decodedOrder = try JSONDecoder().decode(Order.self, from: data)
            confirmationMessage = "Your order for \(decodedOrder.quantity) x \(Order.types[decodedOrder.type].lowercased()) cupcakes is on its way!"
            showingConfirmation = true
        } catch {
            errorMessage = "Sorry, checkout failed. \n\nMessage: \(error.localizedDescription)"
            showingError = true
        }
    }
}

struct CheckoutView_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            CheckoutView(order: Order())
        }
    }
}

Cupcake Corner SwiftUI challenge #3

The third and final challenge is to see if we can convert our data model from a class to a struct, then create an ObservableObject class wrapper around it that gets passed around. This will result in your class having one @Published property, which is the data struct inside it, and should make supporting Codable on the struct much easier.

In order to solve this challenge, Paul uses one of Swift’s most advanced features called Dynamic Member Lookup. This lets us resolve properties at run time.

//
//  SharedOrder.swift
//  CupcakeCorner
//


import SwiftUI

@dynamicMemberLookup // This attribute allows us to dynamically handle access properties that don't directly exist. Yes, it's a bit confusing.

class SharedOrder: ObservableObject {
    static let types = ["Vanilla", "Strawberry", "Chocolate", "Rainbow"]
    @Published var data = Order() // Creates an instance of our order struct

    subscript<T>(dynamicMember keyPath: KeyPath<Order, T>) -> T { // This adds a custom subscript using a dynamic member keyPath.
        data[keyPath: keyPath]
    }
    
    subscript<T>(dynamicMember keyPath: WritableKeyPath<Order, T>) -> T {
        get {
            data[keyPath: keyPath]
        }
        
        set {
            data[keyPath: keyPath] = newValue
        }
    }

}

struct Order: Codable { // We now use a struct that automatically conforms to Codable, meaning we don't need the initializer or hand-coded JSONEncoder and Decoder anymore.
    
    enum CodingKeys: CodingKey {
        case type, quantity, extraFrosting, addSprinkles, name, streetAddress, city, zip
    }
    
    var type = 0 // Vanilla by default, as it's the first entry in the types array
    var quantity = 3 // Default quantity that is ordered
    
    var specialRequestEnabled = false { // Has the user requested a special request? False by default.
        didSet {
            if specialRequestEnabled == false { // If specialRequestEnabled is false, the two extra options are false by default as well
                extraFrosting = false
                addSprinkles = false
            }
        }
    }
    
    var extraFrosting = false // Has the user requested extra frosting? False by default.
    var addSprinkles = false // Has the user requested sprinkles? False by default.
    
    // Delivery details
    
    var name = ""
    var streetAddress = ""
    var city = ""
    var zip = ""
    
    var hasValidAddress: Bool { // Used to validate that none of the address fields are empty
        if name.isReallyEmpty || streetAddress.isReallyEmpty || city.isReallyEmpty || zip.isReallyEmpty {
            return false
        }
        return true
    }
    
    // Product cost
    
    var cost: Double {
        
        // 2$ per cake
        var cost = Double(quantity) * 2
        
        // complicated cakes cost more
        cost += (Double(type) / 2)
        
        // 1$ per cake for extra frosting
        
        if extraFrosting {
            cost += Double(quantity)
        }
        
        // $0.50 per cake for sprinkles
        if addSprinkles {
            cost += Double(quantity) / 2
        }
        
        return cost
        
        // Side note: A Double is not ideal for currency, but we have not yet learned better ways in this point in time (Day 50 of the 100 Days of SwiftUI). We will in the future, though!
    }
}

Wrap up

And that’s it for day 52! Tomorrow, we’ll dive into the 11th project. High time to recharge then. See you tomorrow!

Darryl

Hi! My name is Darryl and this is my personal blog where I write about my journey as I learn programming! You'll also find articles about other things that interest me including games, tech and anime.

Post navigation

Leave a Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

100 Days of SwiftUI – Day 98

100 Days of SwiftUI – Day 4 – Type Annotations

100 Days of SwiftUI – Day 57

100 Days of SwiftUI – Day 48