Keep reference on view/data model after View update - ios

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/

Related

Passed in published property not firing onReceive in iOS 14 but is in iOS 16 [duplicate]

I have a custom ViewModifier which simply returns the same content attached with a onReceive modifier, the onReceive is not triggered, here is a sample code that you can copy, paste and run in XCode:
import SwiftUI
import Combine
class MyViewModel: ObservableObject {
#Published var myProperty: Bool = false
}
struct ContentView: View {
#ObservedObject var viewModel: MyViewModel
var body: some View {
Text("Hello, World!")
.modifier(MyOnReceive(viewModel: viewModel))
.onTapGesture {
self.viewModel.myProperty = true
}
}
}
struct MyOnReceive: ViewModifier {
#ObservedObject var viewModel: MyViewModel
func body(content: Content) -> some View {
content
.onReceive(viewModel.$myProperty) { theValue in
print("The Value is \(theValue)") // <--- this is not executed
}
}
}
is SwiftUI designed to disallow onReceive to execute inside a ViewModifier or is it a bug ? I have a view in my real life project that gets bigger with some business logic put inside onReceive, so I need to clean that view by separating it from onReceive.
ok, this works for me:
func body(content: Content) -> some View {
content
.onAppear() // <--- this makes it work
.onReceive(viewModel.$myProperty) { theValue in
print("-----> The Value is \(theValue)") // <--- this will be executed
}
}
ObservableObject and #Published are part of the Combine framework if you aren't using Combine then you shouldn't be using a class for your view data. Instead, you should be using SwiftUI as designed and avoid heavy objects and either put the data in the efficient View data struct or make a custom struct as follows:
import SwiftUI
struct MyConfig {
var myProperty: Bool = false
mutating func myMethod() {
myProperty = !myProperty
}
}
struct ContentView: View {
#State var config = MyConfig()
var body: some View {
Text("Hello, World!")
.onTapGesture {
config.myMethod()
}
}
}
Old answer:
Try onChange instead
https://developer.apple.com/documentation/swiftui/scrollview/onchange(of:perform:)
.onChange(of: viewModel.myProperty) { newValue in
print("newValue \(newValue)")
}
But please don't use the View Model object pattern in SwiftUI, try to force yourself to use value types like structs for all your data as SwiftUI was designed. Property wrappers like #State will give you the reference semantics you are used to with objects.

Using delegates in Swift

I know that this may be tagged as a repeat question, but the ones that exist have not helped me implement delegates in Swift with SwiftUI. My issue can be see in the code below. I have a View and VM, TodaysVM. In here, I do a network request, that downloads movie data from an API, and saves the data to CoreData. In order to show graphs, the HabitVM needs to fetch the data from CoreData, and then do some long processing on it.
For this, I was looking into a way for one class to send a message notification to another class. So I stumbled onto using delegates (not sure if this is correct?). But essentially the idea would be that the TodayVM refreshes its data (which the user can do a pull to refresh on this page), it also sends a message to HabitsVM to start processing (so the data can be done by the time the user navigates to this page. I am not sure if there is a better way to do this, and I was trying to stay away from a singleton approach, passing one class into class, or creating a new instance of one of the classes.
In the code below, I have the delegates set up but 'print("Data update required!")' is never executed. From my research I understand this to be because I never set a value to the delegate? So the delegate value is nil, and therefore the
delegate?.requireUpdate(status: true)
Is never actually executed. Which makes sense, since delegate is an optional, but never assigned a value. So I was curious how I would assign it a value in SwiftUI? And if using delegates is even the best method to have to separate classes send updateRequired status updates.
import SwiftUI
import Foundation
// protocol
protocol MovieNotifierDelagate: AnyObject {
func requireUpdate(status: Bool)
}
struct HabitView {
#StateObject var habitVM = HabitsVM()
var body: some View {
Text("Hello")
}
}
struct TodaysView {
#StateObject var todayVM = TodayVM()
var body: some View {
Text("Hi!")
}
}
class HabitsVM: ObservableObject, MovieNotifierDelagate {
init() {
}
func requireUpdate(status: Bool) {
print("Data update required!")
reprocessData()
}
func reprocessData() {
print("Processing")
}
}
class TodayVM: ObservableObject {
weak var delegate: MovieNotifierDelagate?
init() {
//Does some network call and downloads data, and saves to CoreData
sendUpdate(status: true)
}
func sendUpdate(status: Bool) {
guard status else {
print ("No update")
return
}
delegate?.requireUpdate(status: true)
}
}
// Controlling Page
struct HomeView: View {
//Default the user to the today view
#State private var selection = 1
var body: some View {
TabView(selection: $selection) {
HabitView()
.tag(0)
.tabItem {
VStack {
Image(systemName: "books.vertical")
Text("Habits")
}
}
TodaysView()
.tag(1)
.tabItem {
VStack {
Image(systemName: "backward.end.alt")
Text("Rewind")
}
}
}
}
}

SwiftUI conditional causing an MVVM view's navigationTitle to not update [duplicate]

This question already has answers here:
What is the difference between #StateObject and #ObservedObject in child views in swiftUI
(3 answers)
Closed 3 months ago.
Here's a hypothetical master/detail pair of SwiftUI views that presents a button which uses NavigationLink:value:label: to navigate to a child view. The child view uses MVVM and has a .navigationTitle modifier that displays a placeholder until the real value is set (by a network operation that is omitted for the sake of brevity).
Upon first launch, tapping the button does navigate to the child view, but the "Loading child..." navigationTitle placeholder never changes to the actual value of "Alice" despite being set in the viewmodel's loadChild() method. If you navigate back and tap the button again, all subsequent navigations do set the navigationTitle correctly.
However, the child view has an if condition. If that if condition is replaced with Text("whatever") and the app is re-built and re-launched, the navigationTitle gets set properly every time. Why does the presence of an if condition inside the view affect the setting of the view's navigationTitle, and only on the first use of navigation?
import SwiftUI
// MARK: Data Structures
struct AppDestinationChild: Identifiable, Hashable {
var id: Int
}
struct Child: Identifiable, Hashable {
var id: Int
var name: String
}
// MARK: -
struct ChildView: View {
#ObservedObject var vm: ChildViewModel
init(id: Int) {
vm = ChildViewModel(id: id)
}
var body: some View {
VStack(alignment: .center) {
// Replacing this `if` condition with just some Text()
// view makes the navigationTitle *always* set properly,
// including during first use.
if vm.pets.count <= 0 {
Text("No pets")
} else {
Text("List of pets would go here")
}
}
.navigationTitle(vm.child?.name ?? "Loading child...")
.task {
vm.loadChild()
}
}
}
// MARK: -
extension ChildView {
#MainActor class ChildViewModel: ObservableObject {
#Published var id: Int
#Published var child: Child?
#Published var pets = [String]()
init(id: Int) {
self.id = id
}
func loadChild() {
// Some network operation would happen here to fetch child details by id
self.child = Child(id: id, name: "Alice")
}
}
}
// MARK: -
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink(value: AppDestinationChild(id: 42), label: {
Text("Go to child view")
})
.navigationDestination(for: AppDestinationChild.self) { destination in
ChildView(id: destination.id)
}
}
}
}
The point of .task is to get rid of the need for a reference type for async code, I recommend you replace your state object with state, e.g.
#State var child: Child?
.task {
child = await Child.load()
}
You could also catch an exception and have another state for an error message.

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())

SwiftUI: ViewModifier doesn't listen to onReceive events

I have a custom ViewModifier which simply returns the same content attached with a onReceive modifier, the onReceive is not triggered, here is a sample code that you can copy, paste and run in XCode:
import SwiftUI
import Combine
class MyViewModel: ObservableObject {
#Published var myProperty: Bool = false
}
struct ContentView: View {
#ObservedObject var viewModel: MyViewModel
var body: some View {
Text("Hello, World!")
.modifier(MyOnReceive(viewModel: viewModel))
.onTapGesture {
self.viewModel.myProperty = true
}
}
}
struct MyOnReceive: ViewModifier {
#ObservedObject var viewModel: MyViewModel
func body(content: Content) -> some View {
content
.onReceive(viewModel.$myProperty) { theValue in
print("The Value is \(theValue)") // <--- this is not executed
}
}
}
is SwiftUI designed to disallow onReceive to execute inside a ViewModifier or is it a bug ? I have a view in my real life project that gets bigger with some business logic put inside onReceive, so I need to clean that view by separating it from onReceive.
ok, this works for me:
func body(content: Content) -> some View {
content
.onAppear() // <--- this makes it work
.onReceive(viewModel.$myProperty) { theValue in
print("-----> The Value is \(theValue)") // <--- this will be executed
}
}
ObservableObject and #Published are part of the Combine framework if you aren't using Combine then you shouldn't be using a class for your view data. Instead, you should be using SwiftUI as designed and avoid heavy objects and either put the data in the efficient View data struct or make a custom struct as follows:
import SwiftUI
struct MyConfig {
var myProperty: Bool = false
mutating func myMethod() {
myProperty = !myProperty
}
}
struct ContentView: View {
#State var config = MyConfig()
var body: some View {
Text("Hello, World!")
.onTapGesture {
config.myMethod()
}
}
}
Old answer:
Try onChange instead
https://developer.apple.com/documentation/swiftui/scrollview/onchange(of:perform:)
.onChange(of: viewModel.myProperty) { newValue in
print("newValue \(newValue)")
}
But please don't use the View Model object pattern in SwiftUI, try to force yourself to use value types like structs for all your data as SwiftUI was designed. Property wrappers like #State will give you the reference semantics you are used to with objects.

Resources