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