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!
100 Days of SwiftUI – Day 52