Каков наилучший подход к тому, чтобы swiftUI по-прежнему обновлялся на основе вложенных наблюдаемых объектов?
В следующем примере показано, что я имею в виду под вложенными наблюдаемыми объектами. Массив мячей менеджера мячей - это опубликованное свойство, которое содержит массив наблюдаемых объектов, каждый из которых имеет собственно опубликованное свойство (цветовую строку).
К сожалению, при нажатии на один из шаров имя шаров не обновляется и не обновляется. Так что я мог напутать, как комбайн должен работать в таком случае?
import SwiftUI
class Ball: Identifiable, ObservableObject {
let id: UUID
@Published var color: String
init(ofColor color: String) {
self.id = UUID()
self.color = color
}
}
class BallManager: ObservableObject {
@Published var balls: [Ball]
init() {
self.balls = []
}
}
struct Arena: View {
@StateObject var bm = BallManager()
var body: some View {
VStack(spacing: 20) {
ForEach(bm.balls) { ball in
Text(ball.color)
.onTapGesture {
changeBall(ball)
}
}
}
.onAppear(perform: createBalls)
.onReceive(bm.$balls, perform: {
print("ball update: \($0)")
})
}
func createBalls() {
for i in 1..<4 {
bm.balls.append(Ball(ofColor: "c\(i)"))
}
}
func changeBall(_ ball: Ball) {
ball.color = "cx"
}
}





Когда Ball в массиве balls изменяется, вы можете вызвать objectWillChange.send(), чтобы обновить ObservableObject.
Следующее должно сработать для вас:
class BallManager: ObservableObject {
@Published var balls: [Ball] {
didSet { setCancellables() }
}
let ballPublisher = PassthroughSubject<Ball, Never>()
private var cancellables = [AnyCancellable]()
init() {
self.balls = []
}
private func setCancellables() {
cancellables = balls.map { ball in
ball.objectWillChange.sink { [weak self] in
guard let self = self else { return }
self.objectWillChange.send()
self.ballPublisher.send(ball)
}
}
}
}
И получите изменения с помощью:
.onReceive(bm.ballPublisher) { ball in
print("ball update:", ball.id, ball.color)
}
Примечание. Если было передано начальное значение balls, а не всегда пустой массив, вам также следует вызвать setCancellables() в файле init.
@lenny Исправлено! Теперь вы можете использовать ballPublisher для получения нового Ball.
спасибо за редактирование, теперь получение работает, но экземпляр шара в onReceive по-прежнему имеет старое значение вместо нового. Итак, если я редактирую его n раз, экземпляр шара, доступный с помощью onReceive, будет иметь значения из n-1-го редактирования. Что имеет смысл, потому что издатель публикует до фактического изменения объекта, но должен быть простой способ получить новые значения? Если вы знаете, как выполнить эту последнюю деталь, я буду рад принять ваш ответ. Anywasy спасибо за всю помощь до сих пор и за использование комбинированного способа, о котором я просил;)
@lenny Я относительно новичок, так что спасибо за терпение! Меня немного смущает то, что вы подразумеваете под «экземпляром шара в onReceive все еще имеет старое значение вместо нового значения». В моем тестировании я не могу воспроизвести это (.onReceive вызывается мгновенно, а при печати color, например, он отражает новое значение). Извините, я новичок в Combine, это определенно может быть причиной того, что я вызвал. Для меня внутри, где .send(ball) находится color, это старое значение, как вы описали. Однако в onReceive это новейшее значение.
Вы абсолютно правы, проблема возникла на моей стороне. Я сделал изменение в другом потоке (не в основном потоке), и, таким образом, возникло описанное отложенное поведение. Так что все, что мне нужно было заменить ball.objectWillChange.sink на ball.objectWillChange.receive(on: DispatchQueue.main).sink, теперь он работает как шарм :) Кстати, зачем нам часть [weak self] in \n guard let self = self else { return }?
@lenny Рад, что это работает! Кроме того, этот [weak self] предотвращает любые утечки памяти в закрытии, поскольку в противном случае self будет сохранен. Подробнее здесь.
Ball - это модель, которая может быть структурой или классом (обычно рекомендуется структура, вы можете найти дополнительную информацию здесь)
Обычно вы используете ObservableObject как ViewModel или компонент, который управляет данными. Обычно для шаров (моделей) обычно устанавливают значение по умолчанию, поэтому вы можете установить пустой массив. Затем вы можете заполнить модели сетевым запросом из базы данных или хранилища.
BallManager можно переименовать в BallViewModel
BallViewModel имеет функцию, которая изменяет базовую модель на основе индекса в компоненте ForEach. id: \.self в основном отображает мяч (модель) для текущего индекса.
Следующие изменения будут работать для достижения того, что вы хотите сделать.
struct Ball: Identifiable {
let id: UUID
var color: String
init(ofColor color: String) {
self.id = UUID()
self.color = color
}
}
class BallViewModel: ObservableObject {
@Published var balls: [Ball] = []
func changeBallColor(in index: Int, color: String) {
balls[index] = Ball(ofColor: color)
}
}
struct Arena: View {
@StateObject var bm = BallViewModel()
var body: some View {
VStack(spacing: 20) {
ForEach(bm.balls.indices, id: \.self) { index in
Button(bm.balls[index].color) {
bm.changeBallColor(in: index, color: "cx")
}
}
}
.onAppear(perform: createBalls)
.onReceive(bm.$balls, perform: {
print("ball update: \($0)")
})
}
func createBalls() {
for i in 1..<4 {
bm.balls.append(Ball(ofColor: "c\(i)"))
}
}
}
спасибо за ответ, мой пример выше был действительно просто примером, поэтому, к сожалению, в моем конкретном случае "Ball" на самом деле является CBPeripheral и, следовательно, должен быть классом. Но я ценю ваши усилия!
В этом примере вложенный ObserverObjects не нужен:
Модель должна быть простой структурой:
struct Ball: Identifiable {
let id: UUID
let color: String
init(id: UUID = UUID(),
color: String) {
self.id = id
self.color = color
}
}
ViewModel должен обрабатывать всю логику, поэтому я переместил сюда все функции, управляющие шарами, и сделал массив шаров приватным. Поскольку вызов changeBall заменяет одну структуру в массиве другой, запускается objectWillChange, обновляется представление и запускается onReceive.
class BallManager: ObservableObject {
@Published private (set) var balls = [Ball]()
func changeBall(_ ball: Ball) {
guard let index = balls.firstIndex(where: { $0.id == ball.id }) else { return }
balls[index] = Ball(id: ball.id, color: "cx")
}
func createBalls() {
for i in 1..<4 {
balls.append(Ball(color: "c\(i)"))
}
}
}
View должен просто сообщать намерения пользователя ViewModel:
struct Arena: View {
@StateObject var ballManager = BallManager()
var body: some View {
VStack(spacing: 20) {
ForEach(ballManager.balls) { ball in
Text(ball.color)
.onTapGesture {
ballManager.changeBall(ball)
}
}
}
.onAppear(perform: ballManager.createBalls)
.onReceive(ballManager.$balls) {
print("ball update: \($0)")
}
}
}
Вы просто создаете BallView, наблюдаете за ним и вносите изменения оттуда. Вы должны наблюдать за каждым ObservableObject напрямую
struct Arena: View {
@StateObject var bm = BallManager()
var body: some View {
VStack(spacing: 20) {
ForEach(bm.balls) { ball in
BallView(ball: ball)
}
}
.onAppear(perform: createBalls)
.onReceive(bm.$balls, perform: {
print("ball update: \($0)")
})
}
func createBalls() {
for i in 1..<4 {
bm.balls.append(Ball(ofColor: "c\(i)"))
}
}
}
struct BallView: View {
@ObservedObject var ball: Ball
var body: some View {
Text(ball.color)
.onTapGesture {
changeBall(ball)
}
}
func changeBall(_ ball: Ball) {
ball.color = "cx"
}
}
спасибо, это очень интересный подход к моей проблеме. Я искал здесь способ, похожий на комбинирование, потому что мне нравится иметь его в одном представлении, но довольно приятно видеть, насколько легко можно получить в swiftUI, просто разделив представления на их маленькие части.
спасибо за ваш ответ, но это оставило некоторые проблемы / открытые вопросы: .onReceive (bm. $ balls) никогда не срабатывает, если мяч внутри мячей изменяется. Я также пробовал .onReceive (bm.objectWillChange), который, конечно, запускается, но в этот момент сохраняет старые значения. Вы знаете, как его настроить дальше?