0

我试图找出使用 SwiftUI 和 Swift 5.5 并发功能的 macOS 文档应用程序的正确结构。

我想演示以线程安全的方式异步更新文档的数据,并能够在文件中读取/写入数据,同时也是线程安全的并且在后台运行。然而,我正在努力:

  • 编写干净的代码 - 与我之前使用的应用程序相比,其中一些看起来不优雅,更像是DispatchQueues笨重
  • 为参与者实现Codeable一致性

我正在寻求关于如何改进这一点的想法、更正和建议。我已经在GitHub 上发布了完整的代码,因为我只会在这里突出一些特定的元素。这是一个最小可行的应用程序,仅用于概念验证。

该应用程序

该应用程序显示一个Records带有添加更多按钮的列表。它应该能够从文件中保存和重新加载列表。

当前方法/设计

我选择了ReferenceFileDocument该类型的协议Document,因为这是我将在具有更复杂数据结构的未来应用程序中使用的协议。(即我不打算使用纯的集合structs来保存文档的数据)

Document具有表示顶级数据结构content的类型属性。RecordsModelView

RecordsModelView被注释@MainActor以确保它收到的任何更新都将在主线程上处理。

RecordsModelView有一个类型的属性RecordsModel。这是一个参与者,确保其数组的读/写Records是线程安全的,但不通过 MainActor 协调以提高效率。

该应用程序假定添加项目的函数需要很长时间,因此使用Task. 虽然这里没有演示,但我也假设addRecord可能从多个后台线程调用,因此需要是线程安全的,因此使用actor.

代码编译并运行,允许将新项目添加到列表中,但是......

问题

首先,我无法注释- 生成我无法解决的编译器错误Document@MainActor如果可以的话,我认为它可能会解决我的一些问题......

其次,因此我有一种笨拙的方式Document来初始化其内容属性(这也必须是可选的才能使其工作)。这看起来很讨厌,并且具有需要在引用它的任何地方打开它的连锁反应:

final class Document: ReferenceFileDocument {
    
    @Published var content: RecordsViewModel?
    
    init() {
        Task { await MainActor.run { self.content = RecordsViewModel() } }
    }

    // Other code here
}

最后,我无法让RecordsModelto 符合Encodable. 我尝试过encode(to encoder: Encoder)异步,但这并不能解决问题。目前,因此RecordsModel只是符合Decodable.

    func encode(to encoder: Encoder) async throws { // <-- Actor-isolated instance method 'encode(to:)' cannot be used to satisfy a protocol requirement
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(records, forKey: .records)
    }
4

1 回答 1

0

我已经找到了一个解决方案——我花了太长时间:回顾我的第一次笨拙的尝试,我觉得很愚蠢。

我的两个主要错误是将 myRecordViewModel对象(需要符合ObservableObject)放在 上MainActor,并创建一个 actor 类型,RecordsModel来保存需要隔离的属性。

这是最初的尝试:

actor RecordsModel: Decodable {
    var records: [Record] = []
    
    enum CodingKeys: String, CodingKey { case records }
    
    init() {}
    
    init(from decoder: Decoder) async throws { ... }
    
    // Unable to conform to Encodable at present with this implementation
    func encode(to encoder: Encoder) throws { ... }
        
    func addRecord() -> [Record] {
        self.records.append(Record(value: Int.random(in: 0...10))) // Assume it takes a long time to compute `value`
        return self.records
    }
}

@MainActor
class RecordsViewModel: ObservableObject {
    @Published var records: [Record]
    private let recordsModel: RecordsModel
    
    init() {
        self.records = []
        self.recordsModel = RecordsModel()
    }
    
    init(fromRecordsModel recordsModel: RecordsModel) async {
        self.records = await recordsModel.records
        self.recordsModel = recordsModel
    }
    
    func addRecord() {
        // Given addRecord takes time to complete, we run it in the background
        Task {
            self.records = await recordsModel.addRecord()
        }
    }
}

我的新方法不是创建一个actor,而是isolatedRecords在视图模型中放置一个属性,与全局actor 隔离。非隔离发布版本对此进行了补充,该版本在对其隔离双胞胎进行任何更新后在 MainActor 上进行更新。

这是新的视图模型类:

final class Records: ObservableObject, Codable {
    @Published var records: [Record]
    @MyActor private var isolatedRecords: [Record]
    
    init() {
        self.records = []
        self.isolatedRecords = []
    }
    
    enum CodingKeys: String, CodingKey { case records }
    init(from decoder: Decoder) throws { ... }
    func encode(to encoder: Encoder) throws { ... }
    
    @MyActor func append(_ value: Int) -> [Record] {
        self.isolatedRecords.append(Record(value))
        return isolatedRecords
    }
    
    func addRecord() {
        Task() {
            let newNumber = Int.random(in: 0...10) // Assume lots of processing here, hence we run it as a Task
            let newRecords = await self.append(newNumber)
            await MainActor.run { self.records = newRecords }
        }
    }
}

这成功地确保了代码没有竞争条件,同时将记录数组的处理和更新同步保持在主线程之外,并消除了我遇到的所有其他问题,例如尝试使参与者符合,Encodable将和更多。代码也更优雅。DocumentMainActor

我已将完整更新的项目留在GitHub

于 2021-08-13T10:20:39.973 回答