onReceive callback not executing - ios

So I am working on a view in SwiftUI which will update its state when an event is published.
The view looks like this:
struct MyView: View {
#EnvironmentObject var dataSource: DataSource
#State var data: [Model] = []
func refreshData() {
self.data = dataSource.getData()
}
var body: some View {
VStack {
List(self.data) { model in
Row(model: model)
}
}
.onAppear {
self.refreshData()
}
.onReceive(self.dataSource.didUpdate) { _ in
print("on receive")
self.refreshData()
}
}
}
class DataSource: ObservableObject {
var didUpdate: PassthroughSubject<Model,Never> ...
}
So with this setup, the onAppear block is called and works as expected. But the onReceive callback is never called. I have been able to verify that .send is being called correctly on the DataSource.didUpdate subject, but it appears the subscriber is not being notified.
Is there something I am missing to make this work?

As you are correctly declaring your DataSource as an observable object class, what you need now is to use the #Published property wrapper on you didUpdate variable. Then, with SwiftUI you can listen to it using .onChange(of:) { }. Just note that it does not work with computed vars: in such case, use the computed var to update the published var. Also, I assume you are correctly injecting your model instance in the environment (otherwise it will never work).
Like this:
struct MyView: View {
#EnvironmentObject var dataSource: DataSource // Remember to inject the specific instance in the environment
#State var data: [Model] = []
func refreshData() {
self.data = dataSource.getData()
}
var body: some View {
VStack {
List(self.data) { model in
Row(model: model)
}
}
.onAppear {
self.refreshData()
}
// .onReceive(self.dataSource.didUpdate) { _ in
// print("on receive")
.onChange(of: dataSource.didUpdate) { _ in // Listen to changes in ObservableObject property that publishes
print("on change")
self.refreshData()
}
}
}
class DataSource: ObservableObject {
// Note the property wrapper, but it does not work with computed vars:
// in such case, use the computed var to update the published var.
// Also: update it only in the main thread!
#Published var didUpdate: PassthroughSubject<Model,Never> ...
}

Related

How do you edit an ObservableObject’s properties in SwiftUI from another class?

I’m looking for the proper pattern and syntax to address my goal of having an ObservableObject instance that I can share amongst multiple views, but while keeping logic associated with it contained to another class. I’m looking to do this to allow different ‘controller’ classes to manipulate the properties of the state without the view needing to know which controller is acting on it (injected).
Here is a simplification that illustrates the issue:
import SwiftUI
class State: ObservableObject {
#Published var text = "foo"
}
class Controller {
var state : State
init(_ state: State) {
self.state = state
}
func changeState() {
state.text = "bar"
}
}
struct ContentView: View {
#StateObject var state = State()
var controller: Controller!
init() {
controller = Controller(state)
}
var body: some View {
VStack {
Text(controller.state.text) // always shows 'foo'
Button("Press Me") {
print(controller.state.text) // prints 'foo'
controller.changeState()
print(controller.state.text) // prints 'bar'
}
}
}
}
I know that I can use my ObservableObject directly and manipulate its properties such that the UI is updated in response, but in my case, doing so prevents me from having different ‘controller’ instances depending on what needs to happen. Please advise with the best way to accomplish this type of scenario in SwiftUI
To make SwiftUI view follow state updates, your controller needs to be ObservableObject.
SwiftUI view will update when objectWillChange is triggered - it's done automatically for properties annotated with Published, but you can trigger it manually too.
Using same publisher of your state, you can sync two observable objects, for example like this:
class Controller: ObservableObject {
let state: State
private var cancellable: AnyCancellable?
init(_ state: State) {
self.state = state
cancellable = state.objectWillChange.sink {
self.objectWillChange.send()
}
}
func changeState() {
state.text = "bar"
}
}
struct ContentView: View {
#StateObject var controller = Controller(State())

A View.environmentObject(_:) may be missing as an ancestor of this view - but not always…

I'm getting this error in production and can't find a way to reproduce it.
Fatal error > No ObservableObject of type PurchaseManager found. A
View.environmentObject(_:) for PurchaseManager may be missing as an
ancestor of this view. > PurchaseManager > SwiftUI
The crash comes from this view:
struct PaywallView: View {
#EnvironmentObject private var purchaseManager: PurchaseManager
var body: some View {
// Call to purchaseManager causing the crash
}
}
And this view is instantiated in subviews of the MainView
#main
struct MyApp: App {
let purchasesManager = PurchaseManager.shared
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(purchasesManager)
}
}
}
}
or, when called from a UIKit controller, from this controler:
final class PaywallHostingController: UIHostingController<AnyView> {
init() {
super.init(rootView:
AnyView(
PaywallView()
.environmentObject(PurchaseManager.shared)
)
)
}
#objc required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I tested all the use cases that trigger the PaywallView to show up, and I never got a crash.
FWIW, the PurchaseManager looks like this:
public class PurchaseManager: ObservableObject {
static let shared = PurchaseManager()
init() {
setupRevenueCat()
fetchOfferings()
refreshPurchaserInfo()
}
}
Why would the ObservableObject go missing? In which circumstances?
The reason your problem is intermittent, is probably because the PurchaseManager init()
could finish
before all the data is setup properly, due to the "delays" of the
async functions in init(). So sometimes the data will be available
when the View wants it, and sometimes it will not be there and crash your app.
You could try the following approach that includes #atultw advice of using
StateObject.
import SwiftUI
#main
struct TestApp: App {
#StateObject var purchaseManager = PurchaseManager() // <-- here
var body: some Scene {
WindowGroup {
MainView()
.onAppear {
purchaseManager.startMeUp() // <-- here
}
.environmentObject(purchaseManager)
}
}
}
struct MainView: View {
#EnvironmentObject var purchaseManager: PurchaseManager
var body: some View {
Text("testing")
List {
ForEach(purchaseManager.offerings, id: \.self) { offer in
Text(offer)
}
}
}
}
public class PurchaseManager: ObservableObject {
#Published var offerings: [String] = []
// -- here --
func startMeUp() {
// setupRevenueCat()
fetchOfferings()
// refreshPurchaserInfo()
}
func fetchOfferings() {
DispatchQueue.main.asyncAfter(deadline: .now()+2) {
self.offerings = ["offer 1","offer 2","offer 3","offer 4"]
}
}
}
Try not to use the singleton pattern here (.shared), EnvironmentObject is meant to be a replacement for it. You should instantiate PurchasesManager in MyApp.
#main
struct MyApp: App {
#StateObject var purchasesManager = PurchaseManager()
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(purchasesManager)
}
}
}
}
without state object compiles fine but needed if you want child views to update automatically.
Doing those things with a dummy PurchasesManager runs fine for me.

SwiftUI: How to initialize a new StateObject in a parent view?

I have an app architecture similar to the below (simplified) code. I use a WorkoutManager StateObject which I initialize in the set up view, then pass down to its children via EnvironmentObject. The problem is that upon dismissing the .sheet there isn't any life cycle event which initializes a new WorkoutManager, which I need in order to be able to start new workouts consecutively. How in this example below can I give WorkoutView the ability to reinitialize WorkoutManager so that it is a clean object?
import SwiftUI
import HealthKit
class WorkoutManager: ObservableObject {
var workout: HKWorkout?
}
struct ContentView: View {
#StateObject var workoutManager = WorkoutManager()
#State var showingWorkoutView = false
var body: some View {
Button {
showingWorkoutView.toggle()
} label: {
Text("Start Workout")
}
.sheet(isPresented: $showingWorkoutView) {
WorkoutView(showingWorkoutView: $showingWorkoutView)
}
}
}
struct WorkoutView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#Binding var showingWorkoutView: Bool
var body: some View {
Text("Workout Started")
.padding()
Button {
showingWorkoutView.toggle()
//Here I want to initialize a new WorkoutManager to clear out the previous workout's state, how?
} label: {
Text("End Workout")
}
}
}
As mentioned in the comments already, the route you probably want to take is reseting the state within the same WorkoutManager. You wouldn't be able to assign a new object to a #StateObject anyway -- you'll end up with compiler errors because of the View's immutable self.
Secondly, I'd suggest that you probably don't want to rely on the Button in your WorkoutView to do this. For example, if the user dismissed the sheet by swiping, that wouldn't get called. Instead, you could listen for the sheet's state in onChange (another method would be using the onDismiss parameter of sheet):
class WorkoutManager: ObservableObject {
var workout: HKWorkout?
func resetState() {
//do whatever you need to do to reset the state
print("Reset state")
}
}
struct ContentView: View {
#StateObject var workoutManager = WorkoutManager()
#State var showingWorkoutView = false
var body: some View {
Button {
showingWorkoutView.toggle()
} label: {
Text("Start Workout")
}
.sheet(isPresented: $showingWorkoutView) {
WorkoutView(showingWorkoutView: $showingWorkoutView)
}
.onChange(of: showingWorkoutView) { newValue in
if !newValue {
workoutManager.resetState()
}
}
}
}
struct WorkoutView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#Binding var showingWorkoutView: Bool
var body: some View {
Text("Workout Started")
.padding()
Button {
showingWorkoutView.toggle()
} label: {
Text("End Workout")
}
}
}

How do I call a function on a view when the #ObservedObject is updated

I have an Observed Object that's working properly. When I update a String, the label updates.
However, I have a Bool that needs to call a custom function when it changes inside of an Observable Object. When the Bool is set to true, I need to flash the background color for 0.1 seconds.
class Event: ObservableObject {
static let current = Event()
#Published var name = ""
#Published var pass = false
}
struct EnterDistanceView: View {
#ObservedObject var event = Event.current
//when event.pass == true, call this
func flash() {
//UI update of flash
}
}
How do I call a method when a property inside of the #ObservedObject changes? Is this possible, or do I need to create
You can use onReceive to do imperative actions when a published value changes:
struct EnterDistanceView: View {
#ObservedObject var event = Event.current
func flash() {
//UI update of flash
}
var body: some View {
Text("Hello world")
.onReceive(event.$pass) { value in
if value {
//do your imperitive code here
flash()
}
}
}
}
Inside your flash method, I assume you'll want to change the value of a #State variable representing the screen color and then use DispatchQueue.main.asyncAfter to change it back shortly thereafter.

Keep reference on view/data model after View update

Consider we have a RootView and a DetailView. DetailView has it's own BindableObject, let's call it DetailViewModel and we have scenario:
RootView may be updated by some kind of global event e.g. missed
internet connection or by it's own data/view model
When RootView handling event it's
content is updated and this is causes new struct of DetailView to
be created
If DetailViewModel is created by DetailView on init,
there would be another reference of DetailViewModel and it's state (e.g. selected object) will be missed
How can we avoid this situation?
Store all ViewModels as EnvironmentObjects that is basically a singleton pool. This approach is causes to store unneeded objects in memory when they are not used
Pass throw all ViewModels from RootView to it's children and to children of child (has cons as above + painfull dependencies)
Store View independent DataObjects (aka workers) as EnvironmentObjects. In that case where do we store view dependent states that corresponds to Model? If we store it in View it will end up in situation where we cross-changing #States what is forbidden by SwiftUI
Better approach?
Sorry me for not providing any code. This question is on architecture concept of Swift UI where we trying to combine declarative structs and reference objects with data.
For now I don't see da way to keep references that corresponds to appropriate view only and don't keep them in memory/environment forever in their current states.
Update:
Lets add some code to see whats happening if VM is created by it's View
import SwiftUI
import Combine
let trigger = Timer.publish(every: 2.0, on: .main, in: .default)
struct ContentView: View {
#State var state: Date = Date()
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: ContentDetailView(), label: {
Text("Navigation push")
.padding()
.background(Color.orange)
})
Text("\(state)")
.padding()
.background(Color.green)
ContentDetailView()
}
}
.onAppear {
_ = trigger.connect()
}
.onReceive(trigger) { (date) in
self.state = date
}
}
}
struct ContentDetailView: View {
#ObservedObject var viewModel = ContentDetailViewModel()
#State var once = false
var body: some View {
let vmdesc = "View model uuid:\n\(viewModel.uuid)"
print("State of once: \(once)")
print(vmdesc)
return Text(vmdesc)
.multilineTextAlignment(.center)
.padding()
.background(Color.blue)
.onAppear {
self.once = true
}
}
}
class ContentDetailViewModel: ObservableObject, Identifiable {
let uuid = UUID()
}
Update 2:
It seems that if we store ObservableObject as #State in view (not as ObservedObject) View keeps reference on VM
#State var viewModel = ContentDetailViewModel()
Any negative effects? Can we use it like this?
Update 3:
It seems that if ViewModel kept in View's #State:
and ViewModel is retained by closure with strong reference - deinit will never be executed -> memory leak
and ViewModel is retained by closure with weak reference - deinit invokes every time on view update, all subs will be reseted, but properties will be the same
Mehhh...
Update 4:
This approach also allows you to keep strong references in bindings closures
import Foundation
import Combine
import SwiftUI
/**
static func instanceInView() -> UIViewController {
let vm = ContentViewModel()
let vc = UIHostingController(rootView: ContentView(viewModel: vm))
vm.bind(uiViewController: vc)
return vc
}
*/
public protocol ViewModelProtocol: class {
static func instanceInView() -> UIViewController
var bindings: Set<AnyCancellable> { get set }
func onAppear()
func onDisappear()
}
extension ViewModelProtocol {
func bind(uiViewController: UIViewController) {
uiViewController.publisher(for: \.parent)
.sink(receiveValue: { [weak self] (parent) in
if parent == nil {
self?.bindings.cancel()
}
})
.store(in: &bindings)
}
}
struct ModelView<ViewModel: ViewModelProtocol>: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<ModelView>) -> UIViewController {
return ViewModel.instanceInView()
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<ModelView>) {
//
}
}
struct RootView: View {
var body: some View {
ModelView<ParkingViewModel>()
.edgesIgnoringSafeArea(.vertical)
}
}
Apple says that we should use ObservableObject for the data that lives outside of SwiftUI. It means you have to manage your data source yourself.
It looks like a single state container fits best for SwiftUI architecture.
typealias Reducer<State, Action> = (inout State, Action) -> Void
final class Store<State, Action>: ObservableObject {
#Published private(set) var state: State
private let reducer: Reducer<State, Action>
init(initialState: State, reducer: #escaping Reducer<State, Action>) {
self.state = initialState
self.reducer = reducer
}
func send(_ action: Action) {
reducer(&state, action)
}
}
You can pass the instance of the store into the environment of your SwiftUI app and it will be available in all views and will store your app state without data losses.
I wrote a blog post about this approach, take a look at it for more information
https://swiftwithmajid.com/2019/09/18/redux-like-state-container-in-swiftui/

Resources