skip to Main Content

I have a question about the logic of the @StateObject. Let’s say we have a class initialised inside the view which has 0 @Published properties, but we don’t want it to be reinitialised by the View. Should it be made a @StateObject to prevent that or is it bad practice to conform to ObservableObject without having observable data. Example:

class Downloader {
    private var isDownloading = false

    func download(for id: String) {
        isDownloading = true
        // download
        isDownloading = false
    }
}

struct DummyView: View {
    var downloader: Downloader()

    var body: some View {
        Text("Hello")
            .onAppear {
                downloader.download(for: "id")
            }
    }
}

2

Answers


  1. your aim should be avoid class whenever you can, shared mutable state leads to bugs which is why SwiftUI uses View struct for view data, .task makes it possible to do async work with no need for a class but still have the lifetime tied to something on screen, e.g.

    struct Downloader {
       
        func download(for id: String) async throws -> [Items] {
            
        }
    }
    
    struct DummyView: View {
       
        let downloadID: String
        @State var isDownloading = false
        @State var result: [Items]?
        var body: some View {
            Text("Hello")
                .task(id: downloadID){
                    isDownloading = true
                    let downloader: Downloader() // usually this is in the @Environment
                    result = try? await downloader.download(for: downloadID)
                    isDownloading = false
                }
        }
    }
    
    Login or Signup to reply.
  2. If the Downloader is only for SwiftUI.Views you can put it into the Environment.

    actor DownloaderProvider {
        func download(for id: String) async throws -> [String] {
            try await Task.sleep(for: .seconds(2))
            return [UUID().uuidString, UUID().uuidString]
        }
    }
    
    private struct DownloaderProviderKey: EnvironmentKey {
        static let defaultValue: DownloaderProvider = .init() //Init once
    }
    
    
    extension EnvironmentValues {
        var downloaderProvider: DownloaderProvider {
            get { self[DownloaderProviderKey.self] }
            set { self[DownloaderProviderKey.self] = newValue }
        }
    }
    

    Then you can access it in any SwiftUI.View

    struct DummyView: View {
        @Environment(.downloaderProvider) var downloaderProvider
        @State private var downloadID: String? = "id"
        @State var result: [String] = []
        var body: some View {
            Text("isDownloading == ((downloadID != nil).description)")
            Text("Hello")
                .task(id: downloadID){ //Triggers a download when the id changes.
                    guard let downloadID else {return}
                    do {
                        result = try await downloaderProvider.download(for: downloadID)
                    } catch {
                        print(error) // << present error to user
                    }
                    self.downloadID = nil
                }
        }
    }
    

    If you need to access the same instance of Downloader from non-Views you have to implement something similar to EnvironmentValues.

    https://www.avanderlee.com/swift/dependency-injection/

    I also suggest looking at the pros/cons of actor when dealing with something like a "Downloader" and actor can be beneficial.

    https://www.swiftbysundell.com/articles/swift-actors/

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search