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