1

I have a class (WatchlistClass) that conforms to the "ObservableObject" protocol and that holds a @Published var (watchlist). The var, in turn, holds a whole array of stocks and some information ([StockInformation]), which is supposed to update my view (WatchlistView) with the current stock prices and stuff. That part works perfectly fine, but the class should access the @StateObject in the view to change the data. Accessing it directly in the class doesn't work because it doesn't read the data from the @StateObject. I tried to access the @StateObject in the view directly but that also creates a new instance of the class that has an empty array. Using "static" on a @Published var throws an error. I can't, for the life of me, figure out how to access a @StateObject directly inside the view to read and modify the data that it holds. Any help would be appreciated.

class WatchlistClass: ObservableObject {
    static let shared = WatchlistClass()
    
    @Published var watchlist: [StockInformation] = []
    
    struct StockInformation: Identifiable {
        ...
    }
    

    func WatchlistTasksGetFundamentalsDelegate(string: String) {
        ...            
            DispatchQueue.main.async {
                self.watchlist.append(stockInformation) // This works and updates the view as expected
            }
        ...
    }


    private let timer = Timer.scheduledTimer(withTimeInterval: 4.0, repeats: true) { _ in
        if self.watchlist.isEmpty == false { // This does not work
            ...
    }
}
struct WatchlistView: View {
    @StateObject var watchlistClass = WatchlistClass()
    ...
}

2 Answers2

1

When using a singleton pattern, you usually want to declare init on the object as private so you're sure you can only create one instance of it.

Then, to access that singleton version, you'll use WatchlistClass.shared:

class WatchlistClass: ObservableObject {
    static let shared = WatchlistClass()
    
    @Published var watchlist: [StockInformation] = []
    
    struct StockInformation: Identifiable {
        //...
    }
    
    private init() { }
}

struct WatchlistView: View {
    @StateObject var watchlistClass = WatchlistClass.shared
    
    var body: some View {
        //...
    }
}

In terms of your Timer, it would be helpful to have more information about what purpose it is serving here. For example, if it's just checking watchlist to react to it, my suggestion would be to use Combine to watch it instead of a Timer.

If the purpose of the singleton version was just to try to just access to the Timer and have it be able to access the array, maybe a pattern like this would be better:

import Combine

class WatchlistClass: ObservableObject {
    
    @Published var watchlist: [StockInformation] = []
    
    private var cancellable : AnyCancellable?
    
    struct StockInformation: Identifiable {
        var id = UUID()
        //...
    }
    
    public init() {
        cancellable = Timer.publish(every: 1, on: .main, in: .default)
            .autoconnect()
            .receive(on: RunLoop.main)
            .sink { _ in
                if !self.watchlist.isEmpty {
                    
                }
            }
    }
}

struct WatchlistView: View {
    @StateObject var watchlistClass = WatchlistClass()
    
    var body: some View {
        Text("Hello, world!")
    }
}

This would create the Timer on init and give it access to the instance's watchlist array.

jnpdx
  • 45,847
  • 6
  • 64
  • 94
  • The singleton is an attempt of mine to solve the problem myself, but it is not really needed since I only initialize the class once. "timer" downloads stock market data every 4 seconds and iterates over each object in "watchlist" to update its price and volume which is the reason I have to access that very instance of "watchlist". I have tried to access it via watchlistClass.shared but that didn't work, I assume because it is not an instance of the class so it doesn't have access to self. The answer provided by Scott Thompson has solved my problem but thank you for reaching out to me. – llizatovic.main.developer Nov 12 '21 at 16:24
1

Your timer is an instance variable but its closure is not a instance of the class and has no self.

You're going to have to do something to put "self" into the scope of the timer's block. One way to do that would be to create the timer in a member function of the instance:

private func makeTimer() -> Timer {
    return Timer.scheduledTimer(withTimeInterval: 4.0, repeats: true) { [weak self] _ in
        if let empty = self?.watchlist.isEmpty,
            empty == false {
        }
    }
}

When you call makeTimer() your code will be executing in the context of an instance. It will have access to a self.

Note that I have changed your block so that it capture's self "weakly" because the timer could exist beyond the life of the object so you have to be proactive against that possibility.

You could call makeTimer from your initializer.

Scott Thompson
  • 22,629
  • 4
  • 32
  • 34
  • I have implemented your solution and it worked perfectly fine, so thank you for that. I do, however, not understand why the closure is not an instance of the class? I tried searching online but I couldn't really find any sources, so I wanted to ask if you had any? Thank you. – llizatovic.main.developer Nov 12 '21 at 16:16
  • The closure is within the "lexical scope" of the class, but it is an argument of the function `scheduledTimer(withTimeInterval:repeats)` and has no association with the class itself. The resulting timer is stored as an instance variable of the class, but the the closure itself is not part of the class. – Scott Thompson Nov 14 '21 at 21:44