[SOLVED] SwiftUI – Best pattern to simplify a view init() that's the same across different views

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

  1. 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
}
  1. 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 
    }
}
  1. 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)

Leave a Reply

Your email address will not be published.