#swiftui
#swiftui
Вопрос:
Сегодня я с чем-то борюсь и не могу найти для этого хорошего решения. У меня есть простой класс (User) с некоторыми атрибутами и другой класс (UsersContainer) в качестве ObservableObject с массивом первого класса:
struct User: Identifiable {
let id = UUID()
var name: String
var isActive: Bool
}
class UsersContainer : ObservableObject{
@Published var users = [User]()
init() {
users = [User(name: "John", isActive: true), User(name: "Paul", isActive: true), User(name: "Jack", isActive: false)]
}
}
В моем представлении содержимого у меня есть список, в котором я показываю имя пользователя и меняю цвет и зачеркивание в зависимости от того, активен пользователь или нет:
struct ContentView: View {
@ObservedObject var usersContainer = UsersContainer()
var body: some View {
VStack{
List(usersContainer.users, id: .id){user in
Text(user.name)
.strikethrough(!user.isActive)
.foregroundColor(user.isActive ? Color.black : Color.gray)
.onTapGesture {
print("Tap")
}
}
}
}
}
Результат в эмуляторе — это то, что я ожидал:
Моя проблема в TapGesture в contentView. Я хочу изменить строку, когда пользователь нажимает на строку с помощью этого кода:
.onTapGesture {
user.isActive.toggle()
}
Но я получаю сообщение об ошибке:
Невозможно использовать изменяемый элемент для неизменяемого значения: ‘user’ — это константа ‘let’
Как я могу изменить значение пользователя на переменную?
Я надеюсь, что смог хорошо объяснить свою проблему.
Спасибо за помощь!
Ответ №1:
Вот возможный подход
List(Array(usersContainer.users.enumerated()), id: .1.id) { index, user in
Text(user.name)
.strikethrough(!user.isActive)
.foregroundColor(user.isActive ? Color.black : Color.gray)
.onTapGesture {
self.usersContainer.users[index].isActive.toggle()
}
}
Комментарии:
1. Это работает! В этом случае вы меняете источник списка, а не объект в каждой строке. Это нормальный подход / лучшая практика в данном случае? Спасибо
2. Это соответствует концепции единого источника истины.
Ответ №2:
SwiftUI 2022
Одна из наиболее важных концепций, которую вы должны иметь в виду, — это источник истины и способы обработки этого из разных представлений в вашем приложении.
В вашем примере источник истины находится в вашем представлении содержимого. И вы пытаетесь манипулировать или изменять этот источник истины из другого представления в иерархии представлений.
В вашем примере вы изменяете его из onTapGesture в TextView.
Text(user.name)
.strikethrough(!user.isActive)
.foregroundColor(user.isActive ? Color.black : Color.gray)
.onTapGesture {
print("Tap")
user.isActive.toggle()
}
Используя оболочку свойства @ObservedObject в вашем contentView для UsersContainer(), дает вам возможность «привязать» этот источник истины к другим представлениям. Это означает, что это позволит этим представлениям изменять данные, которые принадлежат где-то выше в иерархии представлений, и в то же время запускать повторный рендеринг представлений.
В списках теперь есть новый API, который значительно упрощает ожидание.
Ваш код будет выглядеть примерно так:
struct ContentView: View {
@ObservedObject var usersContainer = UsersContainer()
var body: some View {
VStack{
List($usersContainer.users){ $user in
Text(user.name)
.strikethrough(!user.isActive)
.foregroundColor(user.isActive ? Color.black : Color.gray)
.onTapGesture {
user.isActive.toggle()
print("Tap")
}
}
}
}
}
Префикс $ показывает, что вы передаете данные в представление, которое позволяет манипулировать его источником достоверности.
Несколько ключевых моментов, которые нужно извлечь из этого нового кода:
- Поскольку ваш «Пользователь» соответствует идентификатору, вы можете использовать список без параметра «id».
- Когда отображается представление, которому не принадлежат данные, используйте оболочку @Binding, чтобы иметь возможность манипулировать этими данными в иерархии.
- Всегда сохраняйте ясный источник истины
Счастливого кодирования!