SwiftUI 组合:嵌套观察对象

时间:2021-03-31 08:44:13

标签: swift swiftui combine observableobject

让 swiftUI 仍然基于嵌套的观察对象进行更新的最佳方法是什么?

以下示例显示了我对嵌套观察对象的含义。 ball manager 的 balls 数组是一个已发布的属性,它包含一组可观察对象,每个对象都有一个已发布的属性本身(颜色字符串)。

不幸的是,当点击其中一个球时,它不会更新球名称,也不会收到更新。所以我可能搞砸了在那种情况下联合收割机是如何工作的?

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"
    }
}

4 个答案:

答案 0 :(得分:2)

您只需创建一个 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"
    }
}

答案 1 :(得分:0)

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 的初始值并且不总是一个空数组,则您还应该在 init 中调用 setCancellables()

答案 2 :(得分:0)

型号

Ball 是模型,可以是结构体或类(通常建议使用结构体,您可以找到更多信息 here

视图模型

通常您将 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)"))
        }
    }
}

答案 3 :(得分:0)

对于此示例,您不需要嵌套的 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)")
        }
    }
}
相关问题