Navigation and Networking in SwiftUI
Level: Intermediate-Advanced
Hello everyone, today we are going to learn the basics of navigation, networking and image caching in "SwiftUI". Before onboarding, I am assuming that you all know the basics of URLSession, NSCache and Data Modelling along with the Codable Protocols.
Before getting started, this is the API which we are going to use in this project. It consists of a list of Users with first name, last name, email and avatar.
Okay, so head over to the Xcode 11 and create a brand new project. I'll be naming it Navigation and Networking. However, you can name it to whatever you want. As usual, select SwiftUI as the User Interface.
Now before getting on to our Content View, let's first set up our response model.
Create a new swift file "UsersResponse.swift" and paste the following code.
import Foundation // MARK: - UsersResponse struct UsersResponse: Codable { let page, perPage, total, totalPages: Int let users: [User] enum CodingKeys: String, CodingKey { case page case perPage = "per_page" case total case totalPages = "total_pages" case users = "data" } } // MARK: - User struct User: Codable, Identifiable { let id: Int let email, firstName, lastName: String let avatar: String enum CodingKeys: String, CodingKey { case id, email case firstName = "first_name" case lastName = "last_name" case avatar } }
Since we are dealing with an API, we will need to have a Network Manager which will be in charge of all the calls over the network.
For that, create a new file and name it "NetworkManager.swift" and paste the following code.
import Foundation class NetworkManager: ObservableObject { @Published var usersResponse: UsersResponse? @Published var isLoading = false init() { getData() } func getData() { guard let url = URL(string: "https://reqres.in/api/users") else { return } self.isLoading = true URLSession.shared.dataTask(with: url) { (data, response, error) in guard let data ?= data else { return } let usersResponse = try! JSONDecoder().decode(UsersResponse.self, from: data) DispatchQueue.main.async { self.usersResponse = usersResponse self.isLoading = false dump(usersResponse) } }.resume() } }
You can see that the Network Manager is confirming to the Observable Protocol. Now you'll be questioning, "What does it imply?" Well, in the declarative syntax world of programming (whether it is SwiftUI or Flutter), state management is the most challenging thing to do.
Let's first understand what is State Management.
Suppose we have a component "Text" (UILabel in UIKit) and it is holding a property "data" (text in UIKit) which is associated with a variable "firstName" of type String. What we want is to change the component's data whenever there is a change in the variable "firstName".
If you've read my previous article you would be questioning, "Then why did we used "@State" property wrapper in the first place. The "@State" var was only being observed in the "Component" it was declared whereas, the Observable objects are observed throughout the Components tree. Another question right? Refer to the diagram below to understand the Component Tree.
This is just the Content View's component tree of the Tap Me! App. When we create new Views, the object needs to be observed there too. This is the place where Observable Objects comes into action.
The objects are observed even when they are passed as a value for the other component (Remember Single Source of Truth. refer: https://www.dhirubhai.net/pulse/tap-me-dheeraj-bhavsar/). All the properties that have to be observed, should have @Published Property Wrapper before them.
Create a var "usersResponse" and instantiate it with a dummy value. Also, create another var "isLoading" and assign false. This is going to keep track of the data being loaded.
import Foundation class NetworkManager: ObservableObject { @Published var usersResponse: UsersResponse? @Published var isLoading = false
}
Create a function "getData" which will get the data from the API. This is the code snippet of the complete function.
func getData() { guard let url = URL(string: "https://reqres.in/api/users") else { return } self.isLoading = true URLSession.shared.dataTask(with: url) { (data, response, error) in guard let data ?= data else { return } let usersResponse = try! JSONDecoder().decode(UsersResponse.self, from: data) DispatchQueue.main.async { self.usersResponse = usersResponse self.isLoading = false dump(usersResponse) } }.resume() }
Call getData function in the init method of our NetworkManager class. Your final NetworkManager should look like this.
import Foundation class NetworkManager: ObservableObject { @Published var usersResponse: UsersResponse? @Published var isLoading = false init() { getData() } func getData() { guard let url = URL(string: "https://reqres.in/api/users") else { return } self.isLoading = true URLSession.shared.dataTask(with: url) { (data, response, error) in guard let data ?= data else { return } let usersResponse = try! JSONDecoder().decode(UsersResponse.self, from: data) DispatchQueue.main.async { self.usersResponse = usersResponse self.isLoading = false dump(usersResponse) } }.resume() } }
After the networking and data modelling, create a custom component which is responsible for the Network image.
Before that, we will need some sort of download manager for the image itself. Create a new swift file and name it "ImageLoader.swift"
import Foundation let cache = NSCache<NSString,NSData>() class ImageLoader: ObservableObject { @Published var data ?= Data() init(imageUrl: String) { guard let url = URL(string: imageUrl) else { return } if let cachedData = cache.object(forKey: NSString(string: imageUrl)) { self.data = Data(referencing: cachedData) } else { URLSession.shared.dataTask(with: url) { (data, response, error) in DispatchQueue.main.async { guard let data ?= data else { return } self.data = data cache.setObject(NSData(data: data), forKey: NSString(string: imageUrl)) } }.resume() } } }
You can see that this is somewhat similar to the NetworkManager which we have created earlier. There's also a cache instance which is global which will hold the images temporarily.
Note:- It is not a good practice to declare global variables. This is just for the tutorial purpose. Don't use this kind of caching structure in the production environment.
We have created our image downloader, now let's create a new SwiftUI file "NetworkImage.swift".
The objects which conform to the Observable Protocol needs to have @ObservedObject Property Wrapper before them to observe changes in the published properties. This is the snippet of the NetworkImage component.
import SwiftUI struct NetworkImage: View { @ObservedObject var imageLoader: ImageLoader init(imageUrl: String) { imageLoader = ImageLoader(imageUrl: imageUrl) } var body: some View { (imageLoader.data.count == 0) ? Image("placeholder") .resizable() .scaledToFill() .aspectRatio(1, contentMode: .fit) .clipped() : Image(uiImage: UIImage(data: imageLoader.data)!) .resizable() .scaledToFill() .aspectRatio(1, contentMode: .fit) .clipped() } }
Note:- The placeholder image can be found in the GitHub repository.
We have done making the skeleton of our app, now let's jump to the UI part. Open up the ContentView.swift file.
Refactor and rename the ContentView with UsersListView so that it makes sense. Create an object of the network manager with an @ObservedObject Property Wrapper before it.
@ObservedObject var networkManager = NetworkManager()
The code for the body is given below. Don't worry about the UserRow, we will be creating it soon.
import SwiftUI struct UsersListView: View { @ObservedObject var networkManager = NetworkManager() var body: some View { NavigationView { Group { if networkManager.isLoading { Text("Loading") } else { List (networkManager.usersResponse!.users, id: \.id) { user in UserRow(user: user) } } } .navigationBarTitle(Text("Users")) } } }
Let's first talk about the List initializer. It takes two arguments out of which the first one is the list of objects and the other is the "id". The "id" is the one which is used to identify each element in the list.
Note:- The "id" argument can be eliminated by confirming to the Identifiable protocol (Will discuss this part in the upcoming articles.).
The rest of the code is understandable if you've read my other articles, and if you haven't check them out first as they will give a brief introduction to SwiftUI.
After that, create a new SwiftUI file and name it "UserRow.swift". This is a simple Row component which will consist of the user's avatar, name and email address.
Since we want to navigate to the UserDetailsView as well, we will enclose the view inside a "NavigationLink" which allows us to navigate.
The NavigationLink initializer takes the destination argument which is nothing but the view to which we want to navigate. "No more didSelect??". This is the code snippet for the "UserRow.swift" file.
import SwiftUI struct UserRow: View { let user: User var body: some View { NavigationLink(destination: UserDetailsView(user: user)) { HStack { NetworkImage(imageUrl: user.avatar) .clipShape(Circle()) .frame(width: 50, height: 50) VStack (alignment: .leading) { Text(user.firstName + " " + user.lastName) .font(.headline) Text(user.email) .font(.subheadline) } } } } }
We are almost done, let's quickly create a new SwiftUI file "UserDetailsView.swift". The code below is a very simplistic code showing the User's Avatar, Name and Email as details.
import SwiftUI struct UserDetailsView: View { let user: User var body: some View { VStack { NetworkImage(imageUrl: user.avatar) Text(user.firstName + " " + user.lastName) .font(.title) Text(user.email) .font(.headline) } .navigationBarTitle(Text(user.firstName)) } }
Note:- Get all the assets from the GitHub repository to avoid any crashes.
Let's quickly run the project and see what we've got.
I hope you all have enjoyed this article and got to learn a lot of cool features of SwiftUI. If you want an article on the @EnvironmentObject (The dark mode in iOS 13 apps works with the help of Environment Object) please comment below and let me know, we'll build an app using environment objects and get to know them better.
Thank you all. Have a great day!
iOS developer
3 年Hi Can you explain how to create a advance class network? like this : https://github.com/luqigit/AppRTCMacSwiftUIDemo