BLACK FRIDAY: Save 50% on all my Swift books and bundles! >>

SOLVED: Correction or Guidance

Forums > SwiftUI

Hii to all programmers,

I'm new to the whole concept of network calls, and I have some issues with my code that I would like to resolve, but don't know how.

The whole app works (https://github.com/Tim-engineer/WBPO-SwiftUI):

But I have problem with creating the following button to have it's own value for each user, I haven't figured how can I do this almost whole day, so I'm asking if somebody can help me and teach me what can I do different.

import SwiftUI

struct ListOfUsersView: View {

    @StateObject var viewModel = UserViewModel()

    @State private var isFollowed = false

    var body: some View {

        NavigationStack {
            List(viewModel.users, id: \.id) { user in
                NavigationLink(value: user) {
                    UserRow(user: user, isFollowed: $isFollowed)
                }
            }
            .listStyle(.grouped)
            .navigationTitle("Users")
            .navigationDestination(for: User.self) { user in
                UserDetail(user: user, isFollowed: $isFollowed)
            }
        }
        .alert(item: $viewModel.alertItem) { alertItem in
            Alert(title: alertItem.title, message: alertItem.message, dismissButton: alertItem.dismissButton)
        }
        .task {
            viewModel.getUsers()
        }
    }
}

#Preview {
    ListOfUsersView()
}

   

Your logic should revolve on the idea of User having property isFollowed, as this is the state of truth for each user. You cannot add this property directly to JSON received as it will not be decoded and end up with invalid data. You may think to create a separate object that copies a user received and then adds property isFollowed which later can be used to follow or unfollow. But with current structure of JSON, it's not really so easy. If JSON have have a user with such property then it is much easier.

1      

In the example below I will use more fresh @Observable macros instead of "old" ObservableObject protocol to avoid some complexities with view udpates.

  1. Model update
struct User: Codable, Hashable {
    let id: Int
    let email: String
    let firstName: String
    let lastName: String
    let avatar: String

}

struct UserResponse: Codable {
    let data: [User]
}

@Observable
final class UserObject: Hashable, Identifiable {
    let id: Int
    let email: String
    let firstName: String
    let lastName: String
    let avatar: String

    var isFollowed: Bool

    init(id: Int, email: String, firstName: String, lastName: String, avatar: String, isFollowed: Bool = false) {
        self.id = id
        self.email = email
        self.firstName = firstName
        self.lastName = lastName
        self.avatar = avatar
        self.isFollowed = isFollowed
    }

    // Needs to conform to Hashable
    func hash(into hasher: inout Hasher) {
        hasher.combine(email)
    }

    // Needs to conform to Equatable
    static func == (lhs: UserObject, rhs: UserObject) -> Bool {
        lhs.id == rhs.id
    }
}
  1. UserRow update
struct UserRow: View {
    let user: UserObject

    var body: some View {
        HStack(alignment: .top) {
            AsyncImage(url: URL(string: user.avatar)) { image in
                image
                    .resizable()
                    .scaledToFit()
                    .clipShape(Circle())
            } placeholder: {
                Circle()
                    .foregroundStyle(.ultraThinMaterial)
            }
            .frame(width: 100, height: 100)

            VStack(alignment: .leading, spacing: 16) {
                HStack {
                    Text("\(user.firstName) \(user.lastName)")
                        .font(.title3)
                        .fontWeight(.bold)
                    Spacer()
                    Button {
                        user.isFollowed.toggle()
                    } label: {
                        Text(user.isFollowed ? "Unfollow" : "Follow")
                    }
                    .buttonStyle(.borderless)
                }
                VStack(alignment: .leading) {
                    Text("Email Adress:")
                    Text(user.email)
                        .foregroundStyle(.blue)
                }

            }
            .padding(.vertical)
            .padding(.leading)
        }
    }
}
  1. UserDetail udpate
struct UserDetail: View {
    let user: UserObject

    var body: some View {
        VStack {
            AsyncImage(url: URL(string: user.avatar)) { image in
                image
                    .resizable()
                    .scaledToFill()
            } placeholder: {
                Rectangle()
                    .foregroundStyle(.black)
            }
            .frame(height: 300)
            .ignoresSafeArea()

            VStack(spacing: 44) {
                Text("\(user.firstName) \(user.lastName)")
                    .font(.system(size: 40))
                    .fontWeight(.heavy)
                Text(user.email)
                    .foregroundStyle(.blue)
                Button {
                    user.isFollowed.toggle()
                } label: {
                    Text(user.isFollowed ? "Unfollow" : "Follow")
                }
                .buttonStyle(.bordered)
                .tint(.blue)
                .padding(.top, 24)
            }
            Spacer()
        }
        .navigationBarTitleDisplayMode(.inline)
    }
}
  1. UserViewModel udpate

    @Observable
    final class UserViewModel {
    var users: [UserObject] = []
    var alertItem: AlertItem?
    
    func getUsers() async throws -> [User] {
        let endpoint = "https://reqres.in/api/users?page=1&per_page=15"
    
        guard let url = URL(string: endpoint) else {
            throw ReqResError.invalidURL
        }
    
        let (data, response) = try await URLSession.shared.data(from: url)
    
        guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
            throw ReqResError.invalidResponse
        }
    
        do {
            let decoder = JSONDecoder()
            decoder.keyDecodingStrategy = .convertFromSnakeCase
            let userResponse = try decoder.decode(UserResponse.self, from: data)
            return userResponse.data
        } catch {
            throw ReqResError.invalidData
        }
    }
    
    func getUsers() {
        Task {
            do {
                let receivedUsers = try await getUsers()
                users = convertUsers(receivedUsers)
    
            } catch ReqResError.invalidURL {
                print("Invalid URL")
            } catch ReqResError.invalidResponse {
                print("Invalid Response")
            } catch ReqResError.invalidData {
                print("Invalid Data")
            } catch {
                print("Unexpected Error")
            }
        }
    }
    
    // Let's convert those JSON objects to the Object we can use with isFollowed property
    func convertUsers(_ anArray: [User]) -> [UserObject] {
        var newArray = [UserObject]()
        anArray.forEach { user in
            let newUserObject = UserObject(
                id: user.id,
                email: user.email,
                firstName: user.firstName,
                lastName: user.lastName,
                avatar: user.avatar
            )
            newArray.append(newUserObject)
        }
        return newArray
    }
    }
  2. ListOfUsersView udpate

struct ListOfUsersView: View {
    @State var viewModel = UserViewModel()

    var body: some View {
        NavigationStack {
            List(viewModel.users, id: \.id) { user in
                NavigationLink(value: user) {
                    UserRow(user: user)
                }
            }
            .listStyle(.grouped)
            .navigationTitle("Users")
            .navigationDestination(for: UserObject.self) { user in
                UserDetail(user: user)
            }
        }
        .alert(item: $viewModel.alertItem) { alertItem in
            Alert(title: alertItem.title, message: alertItem.message, dismissButton: alertItem.dismissButton)
        }
        .task {
            viewModel.getUsers()
        }
    }
}

1      

Thank youuu soo much I really appretiate your work. It's littlebit too complicated for beginners to add isFollowed and combine it with network call. As you said it would be much easier if it was in the API.

I've learned a lot from you thank you. :)

   

Save 50% in my WWDC sale.

SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.