Navigation and Networking in SwiftUI
#SwiftUI #Networking_in_SwiftUI #RESTAPI #Navigation_in_SwiftUI

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.

No alt text provided for this image

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.

No alt text provided for this image

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!

References:

Hi Can you explain how to create a advance class network? like this : https://github.com/luqigit/AppRTCMacSwiftUIDemo

回复

要查看或添加评论,请登录

Dheeraj Bhavsar的更多文章

  • Fall in ?? with Recursion

    Fall in ?? with Recursion

    Hello everyone, in your journey of programming you would have used recursion at least once. Today, I want to introduce…

  • Generic Codables - Swift

    Generic Codables - Swift

    Hello everyone, I guess you all have faced this problem which I came across in the past few days and didn't found any…

  • Tap Me!

    Tap Me!

    Level: Beginner-Intermediate Hello guys, today we are going to create a reactive app, and you are going to learn a new…

  • Hello Swift UI vs Hello Flutter

    Hello Swift UI vs Hello Flutter

    Level: Beginner In the previous article, We've discussed Swift UI and we are going to compare Swift UI with Flutter…

    1 条评论
  • Hello Swift UI

    Hello Swift UI

    Level: Beginner Hello everyone, today we are going to create an application based on Swift UI. As a programmer, we all…

    1 条评论

社区洞察

其他会员也浏览了