SwiftUI Combine: вложенные наблюдаемые объекты

Каков наилучший подход к тому, чтобы 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"
    }
}
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
0
74
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Ответ принят как подходящий

Когда 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.

спасибо за ваш ответ, но это оставило некоторые проблемы / открытые вопросы: .onReceive (bm. $ balls) никогда не срабатывает, если мяч внутри мячей изменяется. Я также пробовал .onReceive (bm.objectWillChange), который, конечно, запускается, но в этот момент сохраняет старые значения. Вы знаете, как его настроить дальше?

lenny 31.03.2021 13:14

@lenny Исправлено! Теперь вы можете использовать ballPublisher для получения нового Ball.

George_E 31.03.2021 17:52

спасибо за редактирование, теперь получение работает, но экземпляр шара в onReceive по-прежнему имеет старое значение вместо нового. Итак, если я редактирую его n раз, экземпляр шара, доступный с помощью onReceive, будет иметь значения из n-1-го редактирования. Что имеет смысл, потому что издатель публикует до фактического изменения объекта, но должен быть простой способ получить новые значения? Если вы знаете, как выполнить эту последнюю деталь, я буду рад принять ваш ответ. Anywasy спасибо за всю помощь до сих пор и за использование комбинированного способа, о котором я просил;)

lenny 01.04.2021 21:06

@lenny Я относительно новичок, так что спасибо за терпение! Меня немного смущает то, что вы подразумеваете под «экземпляром шара в onReceive все еще имеет старое значение вместо нового значения». В моем тестировании я не могу воспроизвести это (.onReceive вызывается мгновенно, а при печати color, например, он отражает новое значение). Извините, я новичок в Combine, это определенно может быть причиной того, что я вызвал. Для меня внутри, где .send(ball) находится color, это старое значение, как вы описали. Однако в onReceive это новейшее значение.

George_E 01.04.2021 23:30

Вы абсолютно правы, проблема возникла на моей стороне. Я сделал изменение в другом потоке (не в основном потоке), и, таким образом, возникло описанное отложенное поведение. Так что все, что мне нужно было заменить ball.objectWillChange.sink на ball.objectWillChange.receive(on: DispatchQueue.main).sink, теперь он работает как шарм :) Кстати, зачем нам часть [weak self] in \n guard let self = self else { return }?

lenny 02.04.2021 00:04

@lenny Рад, что это работает! Кроме того, этот [weak self] предотвращает любые утечки памяти в закрытии, поскольку в противном случае self будет сохранен. Подробнее здесь.

George_E 02.04.2021 00:33

Модель

Ball - это модель, которая может быть структурой или классом (обычно рекомендуется структура, вы можете найти дополнительную информацию здесь)

ViewModel

Обычно вы используете 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 и, следовательно, должен быть классом. Но я ценю ваши усилия!

lenny 31.03.2021 13:16

В этом примере вложенный 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, просто разделив представления на их маленькие части.

lenny 02.04.2021 00:06

Другие вопросы по теме