With a Combine Publisher, I can use the following to call a closure whenever a value changes:
let cancellable = x.sink { value in … }
How can I achieve the same behaviour with a variable marked #State or #Binding?
Update
The below answer doesn't seem to work anymore, instead one can use .onChange(of:) instead on the property
.onChange(of: someProperty) { value in
//use new value for someProperty
}
You can use willSet and didSet as with any normal property
#State private var someProperty: String {
didSet {
}
willSet {
}
}
Related
I'm new with SwiftUI and i want to convert basically my computed property for being used in SwiftUI views with combine and all that. I couldn’t use it like that because "get set" doesn't work for my SwiftUI views and I kinda struggled here.
Maybe someone has a good solution how can i convert it with with combine to use in swift ui.
Storage service saves the Authdata into userdefaults.
var currentAuthData: AuthData? {
get {
return self.storageService.get(AuthData.self, forKey: authDataStorageKey)
}
set {
if let value = newValue {
self.storageService.store(value, forKey: authDataStorageKey)
}
}
}
This is how you would turn a computed property in SwiftUI, by making it a #propertyWrapper. I have added a solution to read the data using Combine if you need it. I also make you property optional.
I assume this is the model you want to save.
struct AuthData {
var name: String
var email: String
}
Prepare the protocol for your property wrapper to be able to be set to nil using Optional.
public protocol AnyOptional {
var isNil: Bool { get }
}
extension Optional: AnyOptional {
public var isNil: Bool { self == nil }
}
Extend UserDefaults to conform to this protocol and be optional.
extension UserDefault where Value: ExpressibleByNilLiteral {
init(key: String, _ container: UserDefaults = .standard) {
self.init(key: key, defaultValue: nil, container: container)
}
}
Create the property wrapper that is the same thing as a getter and setter for SwiftUI views. This is a generic one and can be use to any type. You can set them in the UserDefaults extension after this bloc of code.
import Combine
#propertyWrapper
struct UserDefault<Value> {
let key: String
let defaultValue: Value
var container: UserDefaults = .standard
// Set a Combine publisher for your new value to
// always read its changes when using Combine.
private let publisher = PassthroughSubject<Value, Never>()
var wrappedValue: Value {
get {
// Get the new value or nil if any.
container.object(forKey: key) as? Value ?? defaultValue
}
set {
// Check if the value is nil and remove it from your object.
if let optional = newValue as? AnyOptional, optional.isNil {
container.removeObject(forKey: key)
}
else {
// Set your new value inside UserDefaults.
container.set(newValue, forKey: key)
}
// Add the newValue to your combine publisher
publisher.send(newValue)
}
}
var projectedValue: AnyPublisher<Value, Never> {
publisher.eraseToAnyPublisher()
}
}
Create the extension in UserDefaults to use your property wrapper in your code.
extension UserDefaults {
#UserDefault(key: "authDataStorageKey", defaultValue: nil)
static var savedAuthData: AuthData?
// You can create as many #propertyWrapper as you want that fits your need
}
examples of use
// Set a new value
UserDefaults.savedAuthData = AuthData(name: "Muli", email: "muli#stackoverflow.com")
// Read the saved value
print(UserDefaults.savedAuthData as Any)
// When using combine
var subscriptions = Set<AnyCancellable>()
UserDefaults.$savedAuthData
.sink { savedValue in
print(savedValue as Any) } // Yours saved value that changes over time.
.store(in: &subscriptions)
Here is my code for a simple class, My goal is that observeValueOfModel() function automatically put changes of valueOfModel under control and print the correct message out!
I can manually use this func for getting the Answer, but the goal is this class be able understand and react to value change of valueOfModel. Thanks for help
class Model: ObservableObject {
var valueOfModel: Bool = Bool()
private func observeValueOfModel() {
if valueOfModel {
print("valueOfModel is True!")
}
else {
print("valueOfModel is False!")
}
}
}
The didSet fits in this case
class Model: ObservableObject {
var valueOfModel: Bool = Bool() {
didSet {
observeValueOfModel()
}
}
// ... other code
Combine will help you. Define your var as #Published to be able to subscribe to it
#Published var valueOfModel: Bool = true
You can subscribe to changes in the init or viewDidLoad for example. We store the subscription in the cancelable. Put it in the VC or as class property to keep the subscription alive.
let cancelable: AnyCancellable?
cancelable = valueOfModel.sink { [weak self] value
// this will get called as soon as valueOfModel gets updated
// do smth with value here
}
I have an AppState that can be observed:
class AppState: ObservableObject {
private init() {}
static let shared = AppState()
#Published fileprivate(set) var isLoggedIn = false
}
A View Model should decide which view to show based on the state (isLoggedIn):
class HostViewModel: ObservableObject, Identifiable {
enum DisplayableContent {
case welcome
case navigationWrapper
}
#Published var containedView: DisplayableContent = AppState.shared.isLoggedIn ? .navigationWrapper : .welcome
}
In the end a HostView observes the containedView property and displays the correct view based on it.
My problem is that isLoggedIn is not being observed with the code above and I can't seem to figure out a way to do it. I'm quite sure that there is a simple way, but after 4 hours of trial & error I hope the community here can help me out.
Working solution:
After two weeks of working with Combine I have now reworked my previous solution again (see edit history) and this is the best I could come up with now. It's still not exactly what I had in mind, because contained is not subscriber and publisher at the same time, but I think the AnyCancellable is always needed. If anyone knows a way to achieve my vision, please still let me know.
class HostViewModel: ObservableObject, Identifiable {
#Published var contained: DisplayableContent
private var containedUpdater: AnyCancellable?
init() {
self.contained = .welcome
setupPipelines()
}
private func setupPipelines() {
self.containedUpdater = AppState.shared.$isLoggedIn
.map { $0 ? DisplayableContent.mainContent : .welcome }
.assign(to: \.contained, on: self)
}
}
extension HostViewModel {
enum DisplayableContent {
case welcome
case mainContent
}
}
DISCLAIMER:
It is not full solution to the problem, it won't trigger objectWillChange, so it's useless for ObservableObject. But it may be useful for some related problems.
Main idea is to create propertyWrapper that will update property value on change in linked Publisher:
#propertyWrapper
class Subscribed<Value, P: Publisher>: ObservableObject where P.Output == Value, P.Failure == Never {
private var watcher: AnyCancellable?
init(wrappedValue value: Value, _ publisher: P) {
self.wrappedValue = value
watcher = publisher.assign(to: \.wrappedValue, on: self)
}
#Published
private(set) var wrappedValue: Value {
willSet {
objectWillChange.send()
}
}
private(set) lazy var projectedValue = self.$wrappedValue
}
Usage:
class HostViewModel: ObservableObject, Identifiable {
enum DisplayableContent {
case welcome
case navigationWrapper
}
#Subscribed(AppState.shared.$isLoggedIn.map({ $0 ? DisplayableContent.navigationWrapper : .welcome }))
var contained: DisplayableContent = .welcome
// each time `AppState.shared.isLoggedIn` changes, `contained` will change it's value
// and there's no other way to change the value of `contained`
}
When you add an ObservedObject to a View, SwiftUI adds a receiver for the objectWillChange publisher and you need to do the same. As objectWillChange is sent before isLoggedIn changes it might be an idea to add a publisher that sends in its didSet. As you are interested in the initial value as well as changes a CurrentValueSubject<Bool, Never> is probably best. In your HostViewModel you then need to subscribe to AppState's new publisher and update containedView using the published value. Using assign can cause reference cycles so sink with a weak reference to self is best.
No code but it is very straight forward. The last trap to look out for is to save the returned value from sink to an AnyCancellable? otherwise your subscriber will disappear.
A generic solution for subscribing to changes of #Published variables in embedded ObservedObjects is to pass objectWillChange notifications to the parent object.
Example:
import Combine
class Parent: ObservableObject {
#Published
var child = Child()
var sink: AnyCancellable?
init() {
sink = child.objectWillChange.sink(receiveValue: objectWillChange.send)
}
}
class Child: ObservableObject {
#Published
var counter: Int = 0
func increase() {
counter += 1
}
}
Demo use with SwiftUI:
struct ContentView: View {
#ObservedObject
var parent = Parent()
var body: some View {
VStack(spacing: 50) {
Text( "\(parent.child.counter)")
Button( action: parent.child.increase) {
Text( "Increase")
}
}
}
}
Why this code will trigger didSet when init
final public class TestDidSet {
static var _shared: TestDidSet! = TestDidSet()
func testA() { }
private var test = true {
didSet {
print("didSet test when initing!!!!!:\(test)")
}
}
private var _value: Bool! {
didSet {
print("didSet when initing!!!!!:\(_value)")
}
}
private init() {
testSet()
_value = false
test = false
}
private func testSet() {
_value = true
}
}
TestDidSet._shared.testA()
any idea?
should it not trigger didSet?
someone help!
update:
My point of view is this,
testSet() and _value = false is doing the same thing, but testSet() is outside init(), so testSet() will trigger didSet while _value = false not. Why?!
It's not optional type or other reason, that cause 'didSet', I suppose.
When you declare a property with an implicitly unwrapped optional type (Bool! in your case), it gets implicitly assigned a default value of nil. Then afterwards if you assign it with some other value in your initializer then the didSet observer gets triggered because it's already a second assignment. didSet is supposed to not be triggered only on a first one.
The didSet{} closure is called every time you assign a new value to your properties (even if you assign it at the declaration (inline) or at the initialisation).
Lets say I have a singleton Manager
class Manager {
static let sharedInstance = Manager()
var text: String {
didSet (value) { print("didSet \(value)") }
}
init () {
self.text = "hello"
}
}
If I do
Manager.sharedInstance.text = "world"
text is still 'hello'
but if I do it twice, the second time it is world
It is working fine.
The behaviour your experienced is explained by 2 facts
Fact 1
As Apple says didSet (and willSet as well) is not called during the init.
The willSet and didSet observers provide a way to observe (and to
respond appropriately) when the value of a variable or property is
being set. The observers are not called when the variable or property
is first initialized. Instead, they are called only when the value is
set outside of an initialization context.
Fact 2
The parameter of didSet does refer to the old value, so you should
rename value as oldValue.
use text in the print
So from this
didSet (value) { print("didSet \(value)") }
to this
didSet (oldValue) { print("didSet \(text)") }
Instead in your code you are printing the old value (the one that has been overwritten).