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?
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.