100 Days of SwiftUI Day 51

100 Days of SwiftUI – Day 51

It’s day 51 of the 100 Days of SwiftUI! Yesterday, we reached the halfway point of this course and finished the first half of our Cupcake Corner app. Today, we’re finishing the app by adding and learning some new functionality like sending and receiving data over the internet. Let’s dive in!

Encoding an ObservableObject class in SwiftUI

By using @Published properties in our Order class, we lost automatic Codable conformance. Swift does not understand how to encode or decode @Published properties. This is a problem, because we need to submit the order to an internet server, which means the data we send has to be JSON. To fix this, we need to manually add Codable conformance ourselves.

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


import SwiftUI

class Order: ObservableObject, Codable {
    
    enum CodingKeys: CodingKey {
        case type, quantity, extraFrosting, addSprinkles, name, streetAddress, city, zip
    }
    
    
    
    // Order details
    static let types = ["Vanilla", "Strawberry", "Chocolate", "Rainbow"]
    
    @Published var type = 0 // Vanilla by default, as it's the first entry in the types array
    @Published var quantity = 3 // Default quantity that is ordered
    
    @Published 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
            }
        }
    }
    
    @Published var extraFrosting = false // Has the user requested extra frosting? False by default.
    @Published var addSprinkles = false // Has the user requested sprinkles? False by default.
    
    // Delivery details
    
    @Published var name = ""
    @Published var streetAddress = ""
    @Published var city = ""
    @Published var zip = ""
    
    var hasValidAddress: Bool { // Used to validate that none of the address fields are empty
        if name.isEmpty || streetAddress.isEmpty || city.isEmpty || zip.isEmpty {
            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!
    }
    
    init() {} // Empty initalizer so we can create an instance of the order class without any data
    
    func encode (to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        try container.encode(type, forKey: .type)
        try container.encode(quantity, forKey: .quantity)
        
        try container.encode(extraFrosting, forKey: .extraFrosting)
        try container.encode(addSprinkles, forKey: .addSprinkles)
        
        try container.encode(name, forKey: .name)
        try container.encode(streetAddress, forKey: .streetAddress)
        try container.encode(city, forKey: .city)
        try container.encode(zip, forKey: .zip)
    }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        type = try container.decode(Int.self, forKey: .type)
        quantity = try container.decode(Int.self, forKey: .quantity)
        
        extraFrosting = try container.decode(Bool.self, forKey: .extraFrosting)
        addSprinkles = try container.decode(Bool.self, forKey: .addSprinkles)
        
        name = try container.decode(String.self, forKey: .name)
        streetAddress = try container.decode(String.self, forKey: .streetAddress)
        city = try container.decode(String.self, forKey: .city)
        zip = try container.decode(String.self, forKey: .zip)
    }
}

Sending and receiving orders over the internet in SwiftUI

iOS comes with functionality built in to handle networking (uploading and downloading data). In order to send and receive data for our app, we would need a webserver, which we don’t have. Luckily, there is a server called reqres which we can use. It will send back the data we write to it, which is helpful for testing purposes.

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


import SwiftUI

struct CheckoutView: View {
    
    @ObservedObject var order: Order
    
    @State private var confirmationMessage = ""
    @State private var showingConfirmation = 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)
        }
    }
    
    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 {
            print("Checkout failed.")
        }
    }
}

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

Cupcake Corner app screenshots

Wrap up

And that’s it for day 51 and our 10th project! We learned lots of new and advanced techniques during this project and I very much look forward to implementing them in other projects in the future. For now though, it’s time to rest up and recharge for tomorrow. Until then!

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 56

100 Days of SwiftUI – Day 50

100 Days of SwiftUI – Day 39 – Moonshot

100 Days of SwiftUI – Day 75