100 Days of SwiftUI Day 49

100 Days of SwiftUI – Day 49

We’ve arrived at day 49 of the 100 Days of SwiftUI! After two consolidation days, it’s time to get back into learning mode. We’ll be starting our 10th project today and it’s called Cupcake Corner. It’s going to be a multiscreen app that allows users to order cupcakes. Sounds delicious!

Adding Codable conformance for @Published properties

If all the properties of a type already conform to Codable, then the type itself can also conform to Codable. However, this does not work with property wrappers like @Published. As a result, we have to make our property conform to Codable ourselves.

First, we need to tell Swift what properties we want to conform to Codable. Then we need to tell Swift how to encode and decode these properties.

//
//  ContentView.swift
//  CupcakeCorner
//

import SwiftUI

class User: ObservableObject, Codable {
    enum CodingKeys: CodingKey {
        case name
    }
    @Published var name = "Paul Hudson" // @Published announces changes to other views
    
    required init(from decoder: Decoder) throws { // The decoder contains all our data. We need to figure out how to read it, it's not a JSON decoder, it's a general decoder. We can read values from it. Required means that you must use it when creating a subclass of this class.
        let container = try decoder.container(keyedBy: CodingKeys.self) // We ask our decoder for a container that contains the keys to our CodingKeys enum. It will expect to find a name, because that is the only case in our enum.
        name = try container.decode(String.self, forKey: .name) // It will look for the key .name in the enum. If it finds it, it will be put into the property called name.
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name) // write our name to the key name. It sounds repetitive and it'll take some getting used to, but it works!
    }
}

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Sending and receiving Codable data with URLSession and SwiftUI

iOS gives us built int tools for getting data from the internet into our application. If we combine that with Codable support, it becomes possible to convert Swift objects to JSON for sending and receive back JSON to decode into Swift objects.

To showcase this, we use our very first API, Apple’s iTunes API, to send us all the songs by Taylor Swift. Then, use a JSONDecoder to convert the rules into an array of results. We will run into two new keywords: async and await.

//
//  ContentView.swift
//  CupcakeCorner
//

import SwiftUI

struct Response: Codable {
    var results: [Result]
}

struct Result: Codable {
    var trackId: Int
    var trackName: String
    var collectionName: String
}

struct ContentView: View {
    @State private var results = [Result]()
    var body: some View {
        List(results, id: \.trackId) { item in
            VStack(alignment: .leading) {
                Text(item.trackName)
                    .font(.headline)
                Text(item.collectionName)
            }
        }
        .task {
            await loadData() // await tells Swift that the function might "go to sleep", meaning it might take a while to complete
        }
    }
    
    func loadData() async { // async allows the function to wait until the necessary data from the internet is loaded until it's run. Meanwhile, other aspects of the app will still work. Users won't have to wait until it's completed.
        guard let url = URL(string: "https://itunes.apple.com/search?term=taylor+swift&entity=song") else {
            print("Invalid URL") // The URL is defined in Apple's iTunes search API.
            return
        }
        
        do {
            let (data,_) = try await URLSession.shared.data(from: url) // The data from URL method contains a data instance of the data it found at the url. You must a try await as it may throw an error
            if let decodedResponse = try?  JSONDecoder().decode(Response.self, from: data) {
                results = decodedResponse.results
            }
        } catch {
            print("Invalid data")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Loading an image from a remote server

SwiftUI allows us to load an image from a remote server, using an AsyncImage view.

One of the most interesting things about an AsyncImage is that you can’t directly apply modifiers to it, because Swift does not know what image will be downloaded from the internet until the app is run.

When we work around this, we tell Swift that an image is coming in and what modifiers we want to apply. Until that is ready, display a placeholder to the user.

//
//  ContentView.swift
//  CupcakeCorner


import SwiftUI

struct ContentView: View {
    var body: some View {
        AsyncImage(url: URL(string: "https://www.hackingwithswift.com/samples/img/logo.png")) { image in // loads and image from a url
            image
                .resizable()
                .scaledToFit()
        } placeholder: { // this is displayed until the image from the url is ready
            ProgressView() // this will display a loading spinner to show that the image is loading
        }
        .frame(width: 200, height: 200)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

An AsyncImage has three phases. It can be successful, meaning the image was loaded as expected, failure, which means the image doesn’t exist or could not be loaded, or in progress: meaning the image is being loaded and we don’t know whether or not it’ll be successful or not.

If an image fails to load, we can control what needs to be done instead of showing the image we intended.

//
//  ContentView.swift
//  CupcakeCorner


import SwiftUI

struct ContentView: View {
    var body: some View {
        AsyncImage(url: URL(string: "https://www.hackingwithswift.com/samples/img/logo.png")) { phase in
            if let image = phase.image { // if the image is succesfully loaded, then...
                image
                    .resizable()
                    .scaledToFit()
            } else if phase.error != nil { // if there's an error, then...
                Text("There was an error loading the image.")
            } else { // if the image is still loading, then...
                ProgressView()
            }
        }
        .frame(width: 200, height: 200)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Validating and disabling forms

The Form view let’s us store user input in a fast and convenient way. However, we have not yet validated user input yet. We haven’t checked if the user input is correct. Thankfully, SwiftUI features a modifier called .disabled. Whenever it’s active, the view it’s attached to won’t respond to user input.

//
//  ContentView.swift
//  CupcakeCorner


import SwiftUI

struct ContentView: View {
    @State private var username =  ""
    @State private var email = ""
    
    var body: some View {
        VStack {
            Form {
                Section {
                    TextField("Username", text: $username)
                    TextField("Email", text: $email)
                }
                
                Section {
                    Button("Create account") {
                        print("Creating account...")
                    }
                }
                .disabled(disabledForm)
            }
        }
    }
    var disabledForm: Bool {
        username.count < 5 || email.count < 5
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Wrap up

And that’s it for day 49! We’ve learned a lot of interesting new features and functionality and I look forward to implementing them in our Cupcake Corner app. We’ll continue tomorrow, so stay tuned!

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 53

100 Days of SwiftUI – Day 47

100 Days of SwiftUI – Day 95

100 Days of SwiftUI – Day 29 – Word Scramble