【问题标题】:SwiftUI - @State property struct with binding doesn't cause redrawSwiftUI - 带有绑定的@State 属性结构不会导致重绘
【发布时间】:2021-10-03 17:16:28
【问题描述】:

以下是我绘制 SwiftUI 视图的代码,该视图使用发布者获取需要在列表中绘制的项目。这些项目都具有使用Toggle 绘制的布尔值。

我的视图是愚蠢的,所以我可以使用任何类型的布尔值,也许支持 UserDefaults、支持核心数据,或者只是某个地方的布尔属性......无论如何,当在视图之外更新布尔值时,这不会重绘其中一个布尔值已更新。 onReceive 被调用,我可以在控制台中看到输出更改,但绑定不是我的 ToggleItem 结构的一部分,因此 SwiftUI 不会重绘。

我的代码...

我有一个类似这样的结构,注意这里的绑定类型...

struct ToggleItem: Identifiable, Equatable {
    let id: String
    let name: String
    let isOn: Binding<Bool>

    public static func == (lhs: ToggleItem, rhs: ToggleItem) -> Bool {
        lhs.id == rhs.id
    }
}

在我的 SwiftUI 中,我有这个...

struct MyView: View {
    @State private var items: [ToggleItem] = []
    
    let itemsPublisher: AnyPublisher<[ToggleItem], Never>

    // ...

    var body: some View {
        List {

        // ...
        
        }
        .onReceive(itemsPublisher) { newItems in
            print("New items: \(newItems)")
            items.removeAll() // hacky redraw
            items = newItems
       }
}

我可以看到这里发生了什么,因为 Binding&lt;Bool&gt; 不是一个值,所以 SwiftUI 看到 newItems 的数组等于它已经绘制的 items,因此,这不会重绘。

我是否缺少一些东西,也许是一些巧妙的 SwiftUI/Combine 为我重绘了这个?

【问题讨论】:

  • 目前还不清楚究竟是什么没有被重绘。你能创建一个最小可重复的例子吗?不相关,但在数据模型中拥有Binding&lt;Bool&gt; 属性是没有意义的——它应该只是Bool——然后你在视图中创建一个绑定来更新它。
  • 问题是因为 ToggleItem 类型的Equatable 函数,因为您刚刚使用id 来寻找尊重!你也必须让他们所有人参与!那么它将起作用。或者完全删除 == 函数 xCode 将构建一个内部 == 函数,该函数也可以以这种方式工作,但您至少必须符合Equatable!跨度>
  • @swiftPunk 这是一个不错的建议,但在更新到lhs.id == rhs.id &amp;&amp; lhs.isOn.wrappedValue == rhs.isOn.wrappedValue 时似乎不起作用,这是您的意思吗?
  • @NewDev 我认为绑定也是问题,烦人的是我试图保持我的视图抽象,所以它不知道特定类型,但想通过改变来避免有两个事实来源它是一个简单的Bool,我的想法是绑定会包装一个事实来源 - 有没有更好的方法来做到这一点?

标签: swiftui combine publisher


【解决方案1】:

不如做这样的事情来保持一个真实的来源:

struct ToggleItem: Identifiable, Equatable {
    let id: String
    let name: String
    var isOn: Bool

    public static func == (lhs: ToggleItem, rhs: ToggleItem) -> Bool {
        lhs.id == rhs.id
    }
}

class ItemsPublisher: ObservableObject {
    @Published var items: [ToggleItem] = [ToggleItem(id: "1", name: "name1", isOn: false)]  // for testing
}

struct ContentView: View {
    @ObservedObject var itemsPublisher = ItemsPublisher()  // to be passed in from parent
    
    var body: some View {
        VStack {
            Button("Add item") {
                let randomString = UUID().uuidString
                let randomBool = Bool.random()
                itemsPublisher.items.append(ToggleItem(id: randomString, name: randomString, isOn: randomBool))
            }
            List ($itemsPublisher.items) { $item in
                Toggle(isOn: $item.isOn) {
                    Text(item.name)
                }
            }
            Spacer()
        }.padding(.top, 50)
        .onReceive(itemsPublisher.items.publisher) { newItem in
            print("----> new item: \(newItem)")
        }
    }
}

【讨论】:

  • 这是我的第一次尝试,效果很好!现在的想法是,我希望我的观点是听取出版商的意见,而 AFAIK 仍然是一个事实来源?我的视图听的是 ItemsPublisher 的等效项,它有一个发布者,当数组更改时触发......我的最终目标是在我的视图中没有这个对象
  • 是的,同意,itemsPublisher 是事实的来源,但是您在@State private var items 中也有同样的事情:[ToggleItem] = [],因此我的评论。如果您不希望此 itemsPublisher 在您的视图中,也许您可​​以使用某种通知系统。我显然没有理解你的问题,对不起。
  • 完全不用担心!只要确保我们在同一页上✌?就状态而言,我相信这是正确的,因为它对视图来说是私有的,所以应该是存储发布者输入的正确方法......说了我最初的希望是不将发布者的值存储到状态数组中并直接使用发布者......但我似乎无法做到这一点(据我所知),不确定你是否知道方法?
【解决方案2】:

似乎删除绑定是正确的前进方向!

现在看一下文档,这很有意义https://developer.apple.com/documentation/swiftui/binding

使用绑定在存储数据的属性与显示和更改数据的视图之间创建双向连接。

虽然不明确,但这确实表明绑定“属性包装器”仅用作 SwiftUI 视图中的属性,而不是数据模型。

我的更改

我在视图中添加了一个闭包

let itemDidToggle: (ToggleItem, Bool) -> Void

这在 Toggle 绑定的 set() 函数中被调用,该函数更新视图外部的值,使视图保持不变。这会触发发布者被调用并更新我的堆栈。再加上更新 == equatable 函数以包含 isOn 属性,一切正常......

我的代码

导入 UIKit 导入游乐场支持 进口组合 导入 SwiftUI

public struct MyItem {
    public let identifier: String
    public let description: String
}

public class ItemsManager {
    public private(set) var items: [MyItem]
    public let itemsPublisher: CurrentValueSubject<[MyItem], Never>
    private let userDefaults: UserDefaults

    public init(items: [MyItem], userDefaults: UserDefaults = .standard) {
        self.userDefaults = userDefaults
        self.items = items
        self.itemsPublisher = .init(items)
    }
    
    public func isItemEnabled(identifier: String) -> Bool {
        guard let item = item(for: identifier) else {
            return false
        }

        if let isOnValue = userDefaults.object(forKey: item.identifier) as? NSNumber {
            return isOnValue.boolValue
        } else {
            return false
        }
    }
    
    public func setEnabled(_ isEnabled: Bool, forIdentifier identifier: String) {
        userDefaults.set(isEnabled, forKey: identifier)
        
        itemsPublisher.send(items)
    }

    func item(for identifier: String) -> MyItem? {
        return items.first { $0.identifier == identifier }
    }

}

struct MyView: View {
    @State private var items: [ToggleItem] = []
    
    let itemsPublisher: AnyPublisher<[ToggleItem], Never>
    let itemDidToggle: (ToggleItem, Bool) -> Void
    
    public init(
        itemsPublisher: AnyPublisher<[ToggleItem], Never>,
        itemDidToggle: @escaping (ToggleItem, Bool) -> Void
    ) {
        self.itemsPublisher = itemsPublisher
        self.itemDidToggle = itemDidToggle
    }
        
    var body: some View {
        List {
            Section(header: Text("Items")) {
                ForEach(items) { item in
                    Toggle(
                        item.name,
                        isOn: .init(
                            get: { item.isOn },
                            set: { itemDidToggle(item, $0) }
                        )
                    )
                }
            }
        }
        .animation(.default, value: items)
        .onReceive(itemsPublisher) { newItems in
            print("New items: \(newItems)")
            items = newItems
        }
    }
    
    struct ToggleItem: Swift.Identifiable, Equatable {
        let id: String
        let name: String
        let isOn: Bool

        public static func == (lhs: ToggleItem, rhs: ToggleItem) -> Bool {
            lhs.id == rhs.id && lhs.isOn == rhs.isOn
        }
    }
}


    
let itemsManager = ItemsManager(items: (1...10).map { .init(identifier: UUID().uuidString, description: "item \($0)") })

let publisher = itemsManager.itemsPublisher
    .map { myItems in
        myItems.map { myItem in
            MyView.ToggleItem(id: myItem.identifier, name: myItem.description, isOn: itemsManager.isItemEnabled(identifier: myItem.identifier))
        }
    }
    .eraseToAnyPublisher()

let view = MyView(itemsPublisher: publisher) { item, newValue in
    itemsManager.setEnabled(newValue, forIdentifier: item.id)
}

【讨论】:

    猜你喜欢
    • 2023-01-25
    • 1970-01-01
    • 1970-01-01
    • 2021-05-18
    • 1970-01-01
    • 1970-01-01
    • 2021-01-06
    • 2020-10-17
    相关资源
    最近更新 更多