Search code examples
swiftuiswiftui-list

The user is not presented on the List after click on add user button


I'm trying to redo UIKit codes in SwiftUI. I implemented a list of users with an add users button. But I have a bug that when I click on the add users button, nothing happens, the user is not shown in the List.

import SwiftUI

struct User {
    let id: Int
    var name: String
}

struct UserListView: View {
    @State private var users: [User] = []
    @State private var selectedUser: User?

    var body: some View {
        List(users, id: \.id) { user in
            Text(user.name)
                .onTapGesture {
                    selectedUser = user
                }
        }
    }

    func addUser(_ user: User) {
        users.append(user)
    }
}

struct UserDetailView: View {
    @Binding var user: User?

    var body: some View {
        if let user = user {
            Text("User Details: \(user.name)")
        } else {
            Text("No user selected")
        }
    }
}

struct ContentView: View {
    @State private var selectedUser: User?
    @State private var userList = UserListView()

    var body: some View {
        VStack {
            userList
            UserDetailView(user: $selectedUser)
            Button("Add User") {
                let newUser = User(id: Int.random(in: 1...1000), name: "New User")
                userList.addUser(newUser)
            }
        }
    }
}

#Preview {
    ContentView()
{

The code is running but I have this bug. Any tip?


Solution

  • In addition to fixing the code, it is possible to improve it:

    Remove direct coupling: UserListView directly manages the state of users, which makes it difficult to share this state with other views.

    Fix communication between views: The update of selectedUser is not propagated efficiently between views.

    Bonus: While we're at it, let's use a little SOLID: Violation of the single responsibility principle: UserListView is managing both the display and the data logic.

    With the Mediator Design Pattern we can solve this:

    Implement another view UserMediator that acts as an intermediary between the views and the data.

    import SwiftUI
    
    struct User: Identifiable {
        let id: Int
        var name: String
    }
    
    // Mediator (ViewModel)
    
    class UserMediator: ObservableObject {
        @Published var users: [User] = []
        @Published var selectedUser: User?
    
        func addUser(_ user: User) {
            users.append(user)
        }
    
        func selectUser(_ user: User) {
            selectedUser = user
        }
    }
    
    struct UserListView: View {
        @ObservedObject var mediator: UserMediator
    
        var body: some View {
            List(mediator.users) { user in
                Text(user.name)
                    .onTapGesture {
                        mediator.selectUser(user)
                    }
            }
        }
    }
    
    struct UserDetailView: View {
        @ObservedObject var mediator: UserMediator
    
        var body: some View {
            if let user = mediator.selectedUser {
                Text("User Details: \(user.name)")
            } else {
                Text("No user selected")
            }
        }
    }
    
    struct ContentView: View {
        @StateObject private var mediator = UserMediator()
    
        var body: some View {
            VStack {
                UserListView(mediator: mediator)
                UserDetailView(mediator: mediator)
                Button("Add User") {
                    let newUser = User(id: Int.random(in: 1...1000), name: "New User")
                    mediator.addUser(newUser)
                }
            }
        }
    }
    

    The Mediator manages the state of users and the selection of the current user. The views (UserListView and UserDetailView) now observe the UserMediator and react to its changes.