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?
1
You should define the array of users at the top level (ContentView) and then pass it to the other view.
Then you should not have a view as a State property like that, add it directly to the view body instead.
To make selection work you need amongst other thing pass the selection property to your list view.
All in all the ContentView should look something like this
struct ContentView: View {
@State private var selectedUser: User?
@State private var userList = [User]()
var body: some View {
VStack {
UserListView(users: $userList, selectedUser: $selectedUser)
UserDetailView(user: selectedUser)
Button("Add User") {
let newUser = User(id: Int.random(in: 1...1000), name: "New User")
userList.append(newUser)
}
}
}
}
and in UserListView change the property declarations to
@Binding var users: [User]
@Binding var selectedUser: User?
For the detail view the user can be let
declared since it doesn’t change
let user: User?
0
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.
2