How to bridge global app state to model view (using #ObservedObject)? - swiftui

I'm working on a SwiftUI project where I have a centralized app state architecture (similar to Redux). This app state class is of type ObservableObject and bound to the SwiftUI view classes directly using #EnvironmentObject.
The above works well for small apps. But as the view hierarchy becomes more and more complex, performance issues start to kick in. The reason is, that ObservableObject fires an update to each view that has subscribed even though the view may only need one single property.
My idea to solve this problem is to put a model view between the global app state and the view. The model view should have a subset of properties of the global app state, the ones used by a specific view. It should subscribe to the global app state and receive notification for every change. But the model view itself should only trigger an update to the view for a change of the subset of the global app state.
So I would have to bridge between two observable objects (the global app state and the model view). How can this be done?
Here's a sketch:
class AppState: ObservableObject {
#Published var propertyA: Int
#Published var propertyB: Int
}
class ModelView: ObservableObject {
#Published var propertyA: Int
}
struct DemoView: View {
#ObservedObject private var modelView: ModelView
var body: some View {
Text("Property A has value \($modelView.propertyA)")
}
}

Here is possible approach
class ModelView: ObservableObject {
#Published var propertyA: Int = 0
private var subscribers = Set<AnyCancellable>()
init(global: AppState) {
global.$propertyA
.receive(on: RunLoop.main)
.assign(to: \.propertyA, on: self)
.store(in: &subscribers)
}
}

The "bridge" you mentioned is often referred to as Derived State.
Here's an approach to implement a redux Connect component. It re-renders when the derived state changes...
public typealias StateSelector<State, SubState> = (State) -> SubState
public struct Connect<State, SubState: Equatable, Content: View>: View {
// MARK: Public
public var store: Store<State>
public var selector: StateSelector<State, SubState>
public var content: (SubState) -> Content
public init(
store: Store<State>,
selector: #escaping StateSelector<State, SubState>,
content: #escaping (SubState) -> Content)
{
self.store = store
self.selector = selector
self.content = content
}
public var body: some View {
Group {
(state ?? selector(store.state)).map(content)
}.onReceive(store.uniqueSubStatePublisher(selector)) { state in
self.state = state
}
}
// MARK: Private
#SwiftUI.State private var state: SubState?
}
To use it, you pass in the store and the "selector" which transform the application state to the derived state:
struct MyView: View {
var index: Int
// Define your derived state
struct MyDerivedState: Equatable {
var age: Int
var name: String
}
// Inject your store
#EnvironmentObject var store: AppStore
// Connect to the store
var body: some View {
Connect(store: store, selector: selector, content: body)
}
// Render something using the selected state
private func body(_ state: MyDerivedState) -> some View {
Text("Hello \(state.name)!")
}
// Setup a state selector
private func selector(_ state: AppState) -> MyDerivedState {
.init(age: state.age, name: state.names[index])
}
}
you can see the full implementation here

Related

SwiftUI parent viewModel containing a nested array of observed objects does not update

(You can skip this part and just look at the code.) I'm creating a complicated form. The form creates, say, a Post object, but I want to be able to create several Comment objects at the same time. So I have a Post form and a Comment form. In my Post form, I can fill out the title, description, etc., and I can add several Comment forms as I create more comments. Each form has an #ObservedObject viewModel of its own type. So I have one parent Post #ObservedObject viewModel, and another #ObservedObject viewModel for the array of the Comment objects which is also a #ObservedObject viewModel.
I hope that made some sense -- here is code to minimally reproduce the issue (unrelated to Posts/Comments). The objective is to make the count of the "Childish" viewModels at the parent level count up like how they count up for the "Child" view.
import Combine
import SwiftUI
final class ParentScreenViewModel: ObservableObject {
#Published var childScreenViewModel = ChildScreenViewModel()
}
struct ParentScreen: View {
#StateObject private var viewModel = ParentScreenViewModel()
var body: some View {
Form {
NavigationLink(destination: ChildScreen(viewModel: viewModel.childScreenViewModel)) {
Text("ChildishVMs")
Spacer()
Text("\(viewModel.childScreenViewModel.myViewModelArray.count)") // FIXME: this count is never updated
}
}
}
}
struct ParentScreen_Previews: PreviewProvider {
static var previews: some View {
ParentScreen()
}
}
// MARK: - ChildScreenViewModel
final class ChildScreenViewModel: ObservableObject {
#Published var myViewModelArray: [ChildishViewModel] = []
func appendAnObservedObject() {
objectWillChange.send() // FIXME: does not work
myViewModelArray.append(ChildishViewModel())
}
}
struct ChildScreen: View {
#ObservedObject private var viewModel: ChildScreenViewModel
init(viewModel: ChildScreenViewModel = ChildScreenViewModel()) {
self.viewModel = viewModel
}
var body: some View {
Button {
viewModel.appendAnObservedObject()
} label: {
Text("Append a ChildishVM (current num: \(viewModel.myViewModelArray.count))")
}
}
}
struct ChildScreen_Previews: PreviewProvider {
static var previews: some View {
ChildScreen()
}
}
final class ChildishViewModel: ObservableObject {
#Published var myProperty = "hey!"
}
ParentView:
ChildView:
I can't run this in previews either -- seems to need to be run in the simulator. There are lots of questions similar to this one but not quite like it (e.g. the common answer of manually subscribing to the child's changes using Combine does not work). Would using #EnvironmentObject help somehow? Thanks!
First get rid of the view model objects, we don't use those in SwiftUI. The View data struct is already the model for the actual views on screen e.g. UILabels, UITables etc. that SwiftUI updates for us. It takes advantage of value semantics to resolve consistency bugs you typically get with objects, see Choosing Between Structures and Classes. SwiftUI structs uses property wrappers like #State to make these super-fast structs have features like objects. If you use actual objects on top of the View structs then you are slowing down SwiftUI and re-introducing the consistency bugs that Swift and SwiftUI were designed to eliminate - which seems to me is exactly the problem you are facing. So it of course is not a good idea to use Combine to resolve consistency issues between objects it'll only make the problem worse.
So with that out of the way, you just need correct some mistakes in your design. Model types should be structs (these can be arrays or nested structs) and have a single model object to manage the life-cycle and side effects of the struct. You can have structs within structs and use bindings to pass them between your Views when you need write access, if you don't then its simply a let and SwiftUI will automatically call body whenever a View is init with a different let from last time.
Here is a basic example:
struct Post: Identifiable {
let id = UUID()
var text = ""
}
class Model: ObservableObject {
#Published var posts: [Post] = []
// func load
// func save
// func delete a post by ID
}
struct ModelController {
static let shared = ModelController()
let model = Model()
//static var preview: ModelController {
// ...
//}()
}
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(ModelController.shared.model)
}
}
}
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
ForEach($model.posts) { $post in
ContentView2(post: post)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(ModelController.shared.preview)
}
}
struct ConventView2: View {
#Binding var post: Post
var body: some View {
TextField("Enter Text", text: $post.text)
}
}
For a more detail check out Apple's Fruta and Scrumdinger samples.

SwiftUI Architecture - Using ObservableObjects to control Views

From an architecture point of view, is it a good idea to use Published properties to control UI elements in a SwiftUI view hierarchy?
Consider the following problem,
I have a store as such
#MainActor final class SomeStore: ObservableObject {
#Published var shouldShowSuccessAlert: Boolean = false
#Published var someData: String = false
//Called by some networking library
public func networkRequestDidComplete(with data: String) {
self.someData = data
self.shouldShowSuccessAlert = false
}
}
//App
struct SomeApp: App {
#StateObject private var someStore: SomeStore = SomeStore()
var body: some Scene {
WindowGroup {
SomeView(shouldShowSuccessAlert: self.someStore.shouldShowSuccessAlert)
}
}
}
//View
struct SomeView: View {
#ObservedObject private var someStore: SomeStore
var body: some View {
if self.someStore.shouldShowSuccessAlert {
//some arbitrary alertView
AlertView(onComplete: {
self.someStore.shouldShowSuccessAlert = false
})
}
}
}
The only problem I can see is that changing this one Boolean will cause the entire view hierarchy to re-render. If SomeView has a deep view hierarchy, all of its descendants will be re-rendered. In React, for example, there is a concept of a PureComponent that you can use to prevent re-renders of views (called components in React) only if the properties they use change (by using reference equality checks) in order to improve performance. Is there something similar in SwiftUI?
Are there any other patterns that can be used like absorbing this boolean into the view's state and then controlled locally within the view?

SwiftUI pass Binding by ref to a child ViewModel

In SwiftUI, I am trying to create some binding between a parent ViewModel and a child ViewModel, here is a simplified example of my scenario:
The parent component:
class ParentViewModel : ObservableObject {
#Published var name = "John Doe"
func updateName() {
self.name = "Jonnie Deer"
}
}
struct ParentView: View {
#StateObject var viewModel = ParentViewModel()
var body: some View {
VStack {
Text(viewModel.name)
ChildView(name: $viewModel.name)
// tapping the button the text on parent view is updated but not in child view
Button("Update", action: viewModel.updateName)
}
}
}
The child component:
class ChildViewModel : ObservableObject {
var name: Binding<String>
var displayName: String {
get {
return "Hello " + name.wrappedValue
}
}
init(name: Binding<String>) {
self.name = name
}
}
struct ChildView: View {
#StateObject var viewModel: ChildViewModel
var body: some View {
Text(viewModel.displayName)
}
init(name: Binding<String>) {
_viewModel = StateObject(wrappedValue: ChildViewModel(name: name))
}
}
So, as stated in the comments, when I tap the button on the parent component the name is not getting updated in ChildView, as if the binding is lost...
Is there any other way to update view model with the updated value? say something like getDerivedStateFromProps in React (becuase when tapping the button the ChildView::init method is called with the new name.
Thanks.
Apple is very big on the concept of a Single Source of Truth(SSoT), and keeping it in mind will keep you from getting into the weeds in code like this. The problem you are having is that while you are using a Binding to instantiate the child view, you are turning around and using it as a #StateObject. When you do that, you are breaking the connection as #StateObject is supposed to sit at the top of the SSoT hierarchy. It designates your SSoT. Otherwise, you have two SSoTs, so you can only update one. The view model in ChildView should be an #ObservedObject so that it connects back up the hierarchy. Also, you can directly instantiate the ChildViewModel when you call ChildView. The initializer just serves to decouple things. Your views would look like this:
struct ParentView: View {
#StateObject var viewModel = ParentViewModel()
var body: some View {
VStack {
Text(viewModel.name)
// You can directly use the ChildViewModel to instantiate the ChildView
ChildView(viewModel: ChildViewModel(name: $viewModel.name))
Button("Update", action: viewModel.updateName)
}
}
}
struct ChildView: View {
// Make this an #ObservedObject not a #StateObject
#ObservedObject var viewModel: ChildViewModel
var body: some View {
Text(viewModel.displayName)
}
}
Neither view model is changed.
Get rid of the view model objects and do #State var name = “John” in ParentView and #Binding var name: String in ChildView. And pass $name into ChildView’s init which gives you write access as if ParentView was a view model object.
By using #State and #Binding you get the reference type semantics you want inside a value type which is the power of SwiftUI. If you just use objects you lose that benefit and have more work to do.
We usually only use ObservableObject for model data but we can also use it for loaders/fetchers where we want to tie some controller behaviour to the view lifecycle but for data transient to a view we always use #State and #Binding. You can extract related vars into their own struct and use mutating funcs for other logic and thus have a single #State struct used by body instead of multiple. This way it can still be testable like a view model object in UIKit would be.

SwiftUI: Updating Model with a timer and reflecting it in UI

I created a little demo app to demonstrate my problem:
I have a model that has an Int which gets incremented every second. A ViewModel is observed by the view and converts this Int to a String which should be displayed.
The problem is that I see the incrementation in the console but the UI is not getting updated. Where is the problem? I used the same approach in other apps. Is the timer the problem?
I'm aware that the naming is not good, it is just for simplicity here. Heartbeatemitter is a separate class because the Timer needs it and I will use in different views inside my app where I pass around the same instance. Why is my Viewmodel not recognising the change of the model?
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
Text(viewModel.number)
}
}
class ViewModel: ObservableObject{
#Published private var model: Model
init() {
model = Model(emitter: HeartbeatEmitter())
model.heartbeatEmitter.delegate = model
}
var number: String {
"\(model.number)"
}
}
protocol Heartbeat {
mutating func beat()
}
struct Model: Heartbeat {
var heartbeatEmitter: HeartbeatEmitter
var number: Int = 0
init(emitter: HeartbeatEmitter){
self.heartbeatEmitter = emitter
}
mutating func beat() {
number += 1
print(number)
}
}
class HeartbeatEmitter {
private var timer: Timer!
var delegate: Heartbeat?
init() {
setupTimer()
}
func setupTimer() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(notifyDelegate), userInfo: nil, repeats: true)
RunLoop.main.add(timer, forMode: .common)
}
#objc
func notifyDelegate() {
delegate?.beat()
}
}
I wouldn't rely on a mutating member of the struct to project the changes to the #Published wrapper. I'd use something that explicitly gets/sets the model.
This, for example, works:
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
Text(viewModel.number)
}
}
class ViewModel: ObservableObject, HeartbeatModifier {
#Published private var model = Model()
var emitter = HeartbeatEmitter()
init() {
emitter.delegate = self
}
func beat() {
model.number += 1
}
var number: String {
"\(model.number)"
}
}
protocol HeartbeatModifier {
func beat()
}
protocol Heartbeat {
var number : Int { get set }
}
struct Model: Heartbeat {
var number: Int = 0
}
class HeartbeatEmitter {
private var timer: Timer!
var delegate: HeartbeatModifier?
init() {
setupTimer()
}
func setupTimer() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(notifyDelegate), userInfo: nil, repeats: true)
RunLoop.main.add(timer, forMode: .common)
}
#objc
func notifyDelegate() {
delegate?.beat()
}
}
Model is a struct, which makes it a value type. That means that when you pass a Model to a function, you're actually making a copy of it and passing that copy to the function.
In particular, this line makes a copy of model:
model.heartbeatEmitter.delegate = model
The “function call” is the call to the setter of delegate. So the delegate property of the HeartbeatEmitter stores its own copy of Model, which is completely independent of the copy stored in the model property of the ViewModel. The Model stored in the HeartbeatEmitter's delegate is being modified, but the Model stored in the ViewModel's model property is not being modified.
I recommend you move the responsibility for side effects (like events based on the passage of time) out of your model object and into a control object: in this case, your ViewModel.
Furthermore, your code will be simpler if you use the Combine framework's Timer.Publisher to emit your time-based events, instead of hand-rolling your own Heartbeat and HeartbeatEmitter types.
Finally, this line also has a problem:
#ObservedObject var viewModel = ViewModel()
The problem here is that you are not being honest with SwiftUI about the lifetime of viewModel and so it may be replaced with a new instance of ViewModel when you don't expect. You should be using #StateObject here. Read this article by Paul Hudson for more details.
Thus:
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
Text(viewModel.number)
}
}
class ViewModel: ObservableObject{
#Published private var model: Model
private var tickets: [AnyCancellable] = []
init() {
model = Model()
Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in self?.model.beat() }
.store(in: &tickets)
}
var number: String {
"\(model.number)"
}
}
struct Model {
var number: Int = 0
mutating func beat() {
number += 1
print(number)
}
}

SwiftUI List data update from destination view causes unexpected behaviour

I have this rather common situation where a List is displaying some items from an #ObservedObject data store as NavigationLinks.
On selecting a NavigationLink a DetailView is presented. This view has a simple Toggle conected to a #Published var on its ViewModel class.
When the DetailView appears (onAppear:) its View Model sets the published var that controls the Toggle to true and also triggers an async request that will update the main data store, causing the List in the previous screen to update too.
The problem is that when this happens (the List is reloaded from an action triggered in the Detail View) multiple instances of the DetailViewModel seem to retained and the DetailView starts receiving events from the wrong Publishers.
In the first time the Detail screen is reached the behaviour is correct (as shown in the code below), the toggle is set to true and the store is updated, however, on navigating back to the List and then again to another DetailView the toggle is set to true on appearing but this time when the code that reloads the store executes, the toggle is set back to false.
My understanding is that when the List is reloaded and a new DetailView and ViewModel are created (as the destination of the NavigationLink) the initial value from the isOn variable that controls the Toggle (which is false) is somehow triggering an update to the Toggle of the currently displayed screen.
Am I missing something here?
import SwiftUI
import Combine
class Store: ObservableObject {
static var shared: Store = .init()
#Published var items = ["one", "two"]
private init() { }
}
struct ContentView: View {
#ObservedObject var store = Store.shared
var body: some View {
NavigationView {
List(store.items, id: \.self) { item in
NavigationLink(item, destination: ItemDetailView())
}
}
}
}
struct ItemDetailView: View {
#ObservedObject var viewModel = ItemDetailViewModel()
var body: some View {
VStack {
Toggle("A toggle", isOn: $viewModel.isOn)
Text(viewModel.title)
Spacer()
} .onAppear(perform: viewModel.onAppear)
}
}
class ItemDetailViewModel: ObservableObject {
#ObservedObject var store: Store = .shared
#Published var isOn = false
var title: String {
store.items[0]
}
func onAppear() {
isOn = true
asyncOperationThatUpdatesTheStoreData()
}
private func asyncOperationThatUpdatesTheStoreData() {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
self?.store.items = ["three", "four"]
}
}
}
You're controlling lifetimes and objects in a way that's not a pattern in this UI framework. The VieModel is not going to magically republish a singleton value type; it's going to instantiate with that value and then mutate its state without ever checking in with the shared instance again, unless it is rebuilt.
class Store: ObservableObject {
static var shared: Store = .init()
struct ContentView: View {
#ObservedObject var store = Store.shared
struct ItemDetailView: View {
#ObservedObject var viewModel = ItemDetailViewModel()
class ViewModel: ObservableObject {
#Published var items: [String] = Store.shared.items
There are lots of potential viable patterns. For example:
Create class RootStore: ObservableObject and house it as an #StateObject in App. The lifetime of a StateObject is the lifetime of that view hierarchy's lifetime. You can expose it (a) directly to child views by .environmentObject(store) or (b) create a container object, such as Factory that vends ViewModels and pass that via the environment without exposing the store to views, or (c) do both a and b.
If you reference the store in another class, hold a weak reference weak var store: Store?. If you keep state in that RootStore on #Published, then you can subscribe directly to that publisher or to the RootStore's own ObjectWillChangePublisher. You could also use other Combine publishers like CurrentValueSubject to achieve the same as #Published.
For code examples, see
Setting a StateObject value from child view causes NavigationView to pop all views
SwiftUI #Published Property Being Updated In DetailView