SwiftUI - 简化在不同视图中相同的视图 init() 的最佳模式

SwiftUI - Best pattern to simplify a view init() that's the same across different views

采取这种简单的观点。它有一个 @StateObject 用于在视图中自动加载和解析一些数据。我有很多这样的视图,使用不同的加载器和解析器。

struct SomeView {
    
    @StateObject var loader: Loader<SomeParser> = Loader<SomeParser>()
    
    var body: some View {
        // Some body that uses the above loader
        VStack {
            // ...
        }
    }
}

加载程序设置为使用 @MainActor,自从 swift 5.6 更新后,我收到有关使用默认值启动这些程序的新警告,这将是 swift 6 中的错误

Expression requiring global actor 'MainActor' cannot appear in default-value expression of property '_loader'; this is an error in Swift 6

here 所述,有一个简单的修复方法。我们只需将它设置在 init

struct SomeView {
    
    @StateObject var loader: Loader<SomeParser>
    
    init() {
        self._loader = StateObject(wrappedValue: Loader<SomeParser>())
    }
    
    var body: some View {
        // Some body that uses the above loader
        VStack {
            // ...
        }
    }
}

现在我遇到的问题是,我有 20 多个这样的视图,具有不同的加载器和解析器,我必须检查每个视图并添加这个 init

我想,让我们简单地创建一个 class 来执行它并子class 它。但它是 View struct 所以不可能 subclass.

然后我尝试使用 protocol,但我想不出一种方法让它工作,因为重写协议中的 init() 不允许您设置 self.loader = ...

是否有更好的方法来做到这一点,或者向每个视图添加 init 是唯一的方法吗?

嗯,实际上有可能(我不知道你所有的 20 多个观点,但仍然)尝试使用泛型来分离公共部分并通过协议和依赖视图概括它们。

这是一个基于您提供的快照的简化泛化演示。使用 Xcode 13.2 / iOS 15.2

测试

注意:正如您将看到的结果更通用,但看起来您需要进行更多更改来适应它,而不是仅更改 inits

  1. 将模型分离到具有关联类型和必需成员的协议中
protocol LoaderInterface: ObservableObject {  // observable
    associatedtype Parser    // associated parser
    init()                   // needed to be creatable

    var isLoading: Bool { get }   // just for demo
}
  1. 使用依赖模型和基于该模型的构建器概括视图
struct LoadingView<Loader, Content>: View where Loader: LoaderInterface, Content: View {

    @StateObject private var loader: Loader
    private var content: (Loader) -> Content

    init(@ViewBuilder content: @escaping (Loader) -> Content) {
        self._loader = StateObject(wrappedValue: Loader())
        self.content = content
    }

    var body: some View {
        content(loader)    // build content with loader inline
                           // so observing got worked 
    }
}
  1. 现在尝试使用上面的方法创建基于具体模型的具体视图
protocol Creatable {    // just helper
    init()
}


// another generic loader (as you would probably already has)
class MyLoader<T>: LoaderInterface where T: Creatable {
    typealias Parser = T    // confirm to LoaderInterface

    var isLoading = false

    private var parser: T
    required init() {       // confirm to LoaderInterface
        parser = T()
    }
}

class MyParser: Creatable {
    required init() {}      // confirm to Creatable
    func parse() {}
}

// demo for specified `LoadingView<MyLoader<MyParser>>`
struct LoaderDemoView: View {
    var body: some View {
        LoadingView { (loader: MyLoader<MyParser>) in
            Text(loader.isLoading ? "Loading..." : "Completed")
        }
    }
}