Issue
Take this simple view. It has a @StateObject
that is used within the view to automatically load and parse some data. I have many of these views with different loaders and parsers.
struct SomeView {
@StateObject var loader: Loader<SomeParser> = Loader<SomeParser>()
var body: some View {
// Some body that uses the above loader
VStack {
// ...
}
}
}
The loaders are set to use @MainActor
and since the swift 5.6
update I get the new warning about initiating these with a default value and that it will be an error in swift 6
Expression requiring global actor ‘MainActor’ cannot appear in
default-value expression of property ‘_loader’; this is an error in
Swift 6
There’s a simple fix, as explained here. We simply set it in the 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 {
// ...
}
}
}
Now the issue I have, is that I have 20+ of these views, with different loaders and parsers and I have to go through each and add this init
.
I thought, let’s simply create a class that does it and subclass it. But it’s a View
struct
so that’s not possible to subclass.
Then I had a go at using a protocol
, but I couldn’t figure out a way to make it work as overriding the init()
in the protocol doesn’t let you set self.loader = ...
Is there a better way to do this, or is adding an init
to every view the only way?
Solution
Well, actually it is possible (I don’t know all your 20+ views, but still) to try using generics to separate common parts and generalise them via protocols and dependent views.
Here is a simplified demo of generalisation based on your provided snapshot. Tested with Xcode 13.2 / iOS 15.2
Note: as you will see the result is more generic, but it seems you will need more changes to adapt it than you would just change inits
- Separate model into protocol with associated type and required members
protocol LoaderInterface: ObservableObject { // observable
associatedtype Parser // associated parser
init() // needed to be creatable
var isLoading: Bool { get } // just for demo
}
- Generalize a view with dependent model and builder based on that model
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
}
}
- Now try to use above to create concrete view based on concrete model
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")
}
}
}
Answered By – Asperi
Answer Checked By – Robin (BugsFixing Admin)