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)
}
}
Related
See my code below. My problem is if i change accessToken via UserService.shared.currentStore.accessToken = xxx, SwiftUI doesn't publish, and there's no update on StoreBanner at all.
//SceneDelegate
let contentView = ContentView()
.environmentObject(UserService.shared)
//Define
class UserService: ObservableObject {
#Published var currentStore = Store.defaultValues()
static let shared = UserService()
}
struct Store: Codable, Hashable {
var storeName: String = ""
var accessToken: String = ""
}
//Use it
struct StoreBanner: View {
var body: some View {
Group {
if UserService.shared.currentStore.accessToken.isNotEmpty {
ShopifyLinkedBanner()
} else {
ShopifyLinkBanner()
}
}
}
}
You're trying to use UserService inside StoreBanner without using a property wrapper to tell the view to respond to updates. Without the #ObservedObject property wrapper, the View doesn't have a mechanism to know that any of the #Published properties have been updated.
Try this:
struct StoreBanner: View {
#ObservedObject private var userService = UserService.shared
var body: some View {
Group {
if userService.currentStore.accessToken.isNotEmpty {
ShopifyLinkedBanner()
} else {
ShopifyLinkBanner()
}
}
}
}
This should work assuming you set accessToken somewhere in your code on the same instance of UserService.
If I have an ObservableObject like...
class Foo: ObservableObject {
#Published var value: Int = 1
func update() {
value = 1
}
}
And then a view like...
struct BarView: View {
#ObservedObject var foo: Foo
var body: some View {
Text("\(foo.value)")
.onAppear { foo.update() }
}
}
Does this cause the view to constantly refresh? Or does SwiftUI do something akin to removeDuplicates in the subscribers that it creates?
I imagine the latter but I've been struggling to find any documentation on this.
onAppear is called when the view is first brought on screen. It's not called again when the view is refreshed because a published property has updated, so your code here would just bump the value once, and update the view.
If you added something inside the body of view that updated the object, that would probably trigger some sort of exception, which I now want to try.
OK, this:
class Huh: ObservableObject {
#Published var value = 1
func update() {
value += 1
}
}
struct TestView: View {
#StateObject var huh = Huh()
var body: some View {
huh.update()
return VStack {
Text("\(huh.value)")
}.onAppear(perform: {
huh.update()
})
}
}
Just puts SwiftUI into an infinite loop. If I hadn't just bought a new Mac, it would have crashed by now :D
Given the following...
import SwiftUI
class ViewModel: ObservableObject {
var value: Bool
init(value: Bool) {
self.value = value
}
func update() {
value = !value
}
}
struct A: View {
#ObservedObject let viewModel: ViewModel
init(value: Bool) {
viewModel = ViewModel(value: value)
}
var body: some View {
Text("\(String(viewModel.value))")
.onTapGesture {
viewModel.update()
}
}
}
struct B: View {
#State var val = [true, false, true]
var body: some View {
A(value: val[0])
}
}
How do I get viewModel to update B's val? It looks like I should be able to use #Binding inside of A but I can't use #Binding inside ViewModel, which is where I want the modification code to run. Then, I don't think I'd need #ObservedObject because the renders would flow from B.
You either need Binding, or an equivalent that does the same thing, in ViewModel. Why do you say you can't use it?
struct A: View {
#ObservedObject var model: Model
init(value: Binding<Bool>) {
model = .init(value: value)
}
var body: some View {
Text(String(model.value))
.onTapGesture(perform: model.update)
}
}
extension A {
final class Model: ObservableObject {
#Binding private(set) var value: Bool
init(value: Binding<Bool>) {
_value = value
}
func update() {
value.toggle()
}
}
}
struct B: View {
#State var val = [true, false, true]
var body: some View {
A(value: $val[0])
}
}
If you want to update the value owned by a parent, you need to pass a Binding from the parent to the child. The child changes the Binding, which updates the value for the parent.
Then you'd need to update that Binding when the child's own view model updates. You can do this by subscribing to a #Published property:
struct A: View {
#ObservedObject var viewModel: ViewModel
#Binding var value: Bool // add a binding
init(value: Binding<Bool>) {
_value = value
viewModel = ViewModel(value: _value.wrappedValue)
}
var body: some View {
Button("\(String(viewModel.value))") {
viewModel.update()
}
// subscribe to changes in view model
.onReceive(viewModel.$value, perform: {
value = $0 // update the binding
})
}
}
Also, don't forget to actually make the view model's property #Published:
class ViewModel: ObservableObject {
#Published var value: Bool
// ...
}
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
I want a #Published variable to be persisted, so that it's the same every time when I relaunch my app.
I want to use both the #UserDefault and #Published property wrappers on one variable. For example I need a '#PublishedUserDefault var isLogedIn'.
I have the following propertyWrapper
import Foundation
#propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
This is my Settings class
import SwiftUI
import Combine
class Settings: ObservableObject {
#Published var isLogedIn : Bool = false
func doLogin(params:[String:String]) {
Webservice().login(params: params) { response in
if let myresponse = response {
self.login = myresponse.login
}
}
}
}
My View class
struct HomeView : View {
#EnvironmentObject var settings: Settings
var body: some View {
VStack {
if settings.isLogedIn {
Text("Loged in")
} else{
Text("Not Loged in")
}
}
}
}
Is there a way to make a single property wrapper that covers both the persisting and the publishing?
import SwiftUI
import Combine
fileprivate var cancellables = [String : AnyCancellable] ()
public extension Published {
init(wrappedValue defaultValue: Value, key: String) {
let value = UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
self.init(initialValue: value)
cancellables[key] = projectedValue.sink { val in
UserDefaults.standard.set(val, forKey: key)
}
}
}
class Settings: ObservableObject {
#Published(key: "isLogedIn") var isLogedIn = false
...
}
Sample: https://youtu.be/TXdAg_YvBNE
Version for all Codable types check out here
To persist your data you could use the #AppStorage property wrapper.
However, without using #Published your ObservableObject will no longer put out the news about the changed data. To fix this, simply call objectWillChange.send() from the property's willSet observer.
import SwiftUI
class Settings: ObservableObject {
#AppStorage("Example") var example: Bool = false {
willSet {
// Call objectWillChange manually since #AppStorage is not published
objectWillChange.send()
}
}
}
It should be possible to compose a new property wrapper:
Composition was left out of the first revision of this proposal,
because one can manually compose property wrapper types. For example,
the composition #A #B could be implemented as an AB wrapper:
#propertyWrapper
struct AB<Value> {
private var storage: A<B<Value>>
var wrappedValue: Value {
get { storage.wrappedValue.wrappedValue }
set { storage.wrappedValue.wrappedValue = newValue }
}
}
The main benefit of this approach is its predictability: the author of
AB decides how to best achieve the composition of A and B, names it
appropriately, and provides the right API and documentation of its
semantics. On the other hand, having to manually write out each of the
compositions is a lot of boilerplate, particularly for a feature whose
main selling point is the elimination of boilerplate. It is also
unfortunate to have to invent names for each composition---when I try
the compose A and B via #A #B, how do I know to go look for the
manually-composed property wrapper type AB? Or maybe that should be
BA?
Ref: Property WrappersProposal: SE-0258
You currently can't wrap #UserDefault around #Published since that is not currently allowed.
The way to implement #PublishedUserDefault is to pass an objectWillChange into the wrapper and call it before setting the variable.
struct HomeView : View {
#StateObject var auth = Auth()
#AppStorage("username") var username: String = "Anonymous"
var body: some View {
VStack {
if username != "Anonymous" {
Text("Logged in")
} else{
Text("Not Logged in")
}
}
.onAppear(){
auth.login()
}
}
}
import SwiftUI
import Combine
class Auth: ObservableObject {
func login(params:[String:String]) {
Webservice().login(params: params) { response in
if let myresponse = response {
UserDefaults.standard.set(myresponse.login, forKey: "username")`
}
}
}
}