Setting up a combine publisher in swift using map - swift

iOS 13, Swift 5.x
I am trying to get the syntax right on a combine subscription/publisher. I got a working example and I have tried to copy it, but I missing something; and I cannot see the wood for the trees. Here is my code.
import Foundation
import Combine
class SwiftUIViewCModel: ObservableObject {
#Published var state : SwiftUIViewCModelState = .page1
static let shared = SwiftUIViewCModel()
private var stateUpdatingSubscriber: AnyCancellable?
init() {
self.stateUpdatingSubscriber = nil
self.stateUpdatingSubscriber = SwiftUIViewCModel.shared.$state
.map(modelCTomodelD(modelState:))
.receive(on: RunLoop.main)
.assign(to: \.state, on: self)
}
private func modelCTomodelD(modelState: SwiftUIViewCModelState) -> SwiftUIViewEModelState {
switch modelState {
case .page1:
return .page1
case .page2:
return .page2
default:
break
}
}
}
enum SwiftUIViewEModelState {
case page1
case page2
}
enum SwiftUIViewCModelState {
case page1
case page2
}
I am getting a syntax error on the compile, but I don't understand what exactly I need to do to fix it.
Cannot convert value of type '(SwiftUIViewCModelState) -> SwiftUIViewEModelState' to expected argument type 'KeyPath<Published.Publisher.Output, SwiftUIViewCModelState>' (aka 'KeyPath<SwiftUIViewCModelState, SwiftUIViewCModelState>')
How do I get the format it needs here into this code?
Thanks

I'm not really sure what the purpose of this code is.
The error you're getting is that self.state is of type SwiftUIViewCModelState, but the value was converted to a SwiftUIViewEModelState via the map operator. The last step (.assign(to: \.state, on: self)) tries to save this new value back to the original self.state var, but it can't, because it's now a different type.
I'm also not sure why you'd want to have a publisher/subscriber chain to modify a variable and save it back to itself?
One way to make the code compile is to add a new variable, var state2: SwiftUIViewEModelState, and then change the .assign line to .assign(to: \.state2, on: self). This will create a subscription chain that uses the state var as the publisher, changes its type via map, and then saves the modified value to state2.

Related

In Swift, how to assign values when generic parameters are different

I am trying to subscribe to multiple publishers. The Output type of publishers may not be determined.
static func listen<T>(publisher: Published<T>.Publisher){
publisher.sink { _Arg in
// do something
}.store(in: &cancellables)
}
listen(publisher: env.$showMenuIcon)
listen(publisher: env.$dateFormatLunar)
listen(publisher: env.$dateFormatAd)
listen(publisher: env.$showWeek)
listen(publisher: env.$showWeather)
// in env class
#Published var timeItem = true
#Published var dateFormatAd = "yyyy-MM-dd"
Each of my publishers may have different generic parameter types, and I can only call listen by copying multiple lines of code like this. Is there any way to modify the Listen method to accept an array type? Or is there another way I can simplify my code?
First of all, I would advise you against your idea of a generic function for subscribing to publishers of different types. Why? Imagine that this is possible (and it is possible in principle, an example is below). How do you want to distinguish between different data types in your sink block? In my opinion, the only way to bring different data types under one roof and then still have the possibility to distinguish them from each other is to create your own data type, that is not generic. E.g. something like this:
struct Result {
let type: Type // E.g. an enum of possible types.
let value: Any
}
Then you have to look in your sink block each time for the data type of your value and casts it accordingly. In my opinion, this makes your logic very complicated. I am not a fan of universal functions. They are very often the big sources of errors.
An example/idea for you on how to realise your wish:
class MyClass {
// Common data type
typealias Value = Any
#Published var numberPublisher: Value?
#Published var stringPublisher: Value?
#Published var booleanPublisher: Value?
var subscription: AnyCancellable?
init() {
// Start listening of publishers.
listen(publishers: [
$numberPublisher,
$stringPublisher,
$booleanPublisher
])
}
func listen(publishers: [Published<Value?>.Publisher]) {
let mergedPublishers = Publishers.MergeMany(publishers)
subscription = mergedPublishers
// Skipping initial nil values and just not seeing them.
.dropFirst(publishers.count)
.sink(receiveCompletion: { completion in
print("Completion \(completion)")
}, receiveValue: { value in
print("Value \(String(describing: value))")
})
}
}
As I wrote above, I created a common data type for all my publishers. Even if your function is generic, you can not pass parameters of different types to it at the same time.
I tested my code in the Playground:
let myClass = MyClass()
myClass.numberPublisher = 123
myClass.stringPublisher = "ABC"
myClass.booleanPublisher = false
// Console output
Value Optional(123)
Value Optional("ABC")
Value Optional(false)
In your place I would subscribe to each publisher separately and directly, without any functions in between (the way you're already doing it).
Hopefully I could help you.

Swift memory conflict where it should not happen

I am working on a SwiftUI project, where I use the MVVM-architecture.
When changing a View-model object property from the SwiftUI view, it causes a memory conflict crash in the view-model object.
The error is of the type: Simultaneous accesses to 0x600003591b48, but modification requires exclusive access.
In steps, here is what happens:
View-model property is changed from view
View-model property changes model property
Model property notifies about changes
View-model receives change notification
View-model access model object
Crash occur due to memory conflict
Relevant code snippets are seen below. Xcode project is a standard SwiftUI project.
The error will happen, after first clicking the add button, and then the modify button.
If the "update" code is moved into the "receiveValue" closure, the error will not occur. Likewise, the error will not occur, if the View-model class is made non-generic.
To my best knowledge, the code is all-right, so I suspect it is a compiler problem. But I am not sure.
import Foundation
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var item: ViewModel<Model> = ViewModel<Model>()
var body: some View {
VStack {
Button("Add", action: { item.add(model:Model()) })
Button("Modify", action: { item.selected.toggle() })
}
}
}
protocol ModelType {
var objectDidChange: ObservableObjectPublisher { get }
var selected: Bool { get set }
}
class Model: ModelType {
let objectDidChange = ObservableObjectPublisher()
var selected = false {
didSet {
objectDidChange.send()
}
}
}
class ViewModel<Model:ModelType>: ObservableObject {
var selected = false {
didSet {
model.selected = selected
}
}
func add(model: Model) {
self.model = model
cancellable = model.objectDidChange.sink(receiveValue: { _ in
self.update()
})
}
private var model: Model! = nil
private var cancellable: AnyCancellable? = nil
func update() {
// Crash log: Simultaneous accesses to 0x600003591b48, but modification requires exclusive access.
print("update \(model.selected)")
}
}
Short version: require AnyObject for ModelType.
Long version:
You're trying to read from self.model while you're in the middle of setting self.model. When you say "If the "update" code is moved into the "receiveValue" closure, the error will not occur," this isn't quite correct. I expect what you mean is you wrote this:
cancellable = model.objectDidChange.sink(receiveValue: { _ in
print("update \(model.selected)")
})
And that worked, but that's completely different code. model in this case is the local variable, not the property self.model. You'll get the same crash if you write it this way:
cancellable = model.objectDidChange.sink(receiveValue: { _ in
print("update \(self.model.selected)")
})
The path that gets you here is:
ViewModel.selected.didSet
WRITE to Model.selected <---
Model.selected.didSet
(observer-closure)
ViewModel.update
READ from ViewModel.model <---
This is a read and write to the same value, and that violates exclusive access. Note that the "value" in question is "the entire ViewModel value," not ViewModel.selected. You can show this by changing the update function to:
print("update \(model!)")
You'll get the same crash.
So why does this work when you take out the generic? Because this particularly strict version of exclusivity only applies to value types (like structs). It doesn't apply to classes. So when this is concrete, Swift knows viewModel is a class, and that's ok. (The why behind this difference a bit complex, so I suggest reading the proposal that explains it.)
When you make this generic, Swift has to be very cautious. It doesn't know that Model is a class, so it applies stricter rules. You can fix this by promising that it's a class:
protocol ModelType: AnyObject { ... }

Combine: can't use `.assign` with structs - why?

I'm seeing some struct vs class behavior that I don't really don't understand, when trying to assign a value using Combine.
Code:
import Foundation
import Combine
struct Passengers {
var women = 0
var men = 0
}
class Controller {
#Published var passengers = Passengers()
var cancellables = Set<AnyCancellable>()
let minusButtonTapPublisher: AnyPublisher<Void, Never>
init() {
// Of course the real code has a real publisher for button taps :)
minusButtonTapPublisher = Empty<Void, Never>().eraseToAnyPublisher()
// Works fine:
minusButtonTapPublisher
.map { self.passengers.women - 1 }
.sink { [weak self] value in
self?.passengers.women = value
}.store(in: &cancellables)
// Doesn't work:
minusButtonTapPublisher
.map { self.passengers.women - 1 }
.assign(to: \.women, on: passengers)
.store(in: &cancellables)
}
}
The error I get is Key path value type 'ReferenceWritableKeyPath<Passengers, Int>' cannot be converted to contextual type 'WritableKeyPath<Passengers, Int>'.
The version using sink instead of assign works fine, and when I turn Passengers into a class, the assign version also works fine. My question is: why does it only work with a class? The two versions (sink and assign) really do the same thing in the end, right? They both update the women property on passengers.
(When I do change Passengers to a class, then the sink version no longer works though.)
Actually it is explicitly documented - Assigns each element from a Publisher to a property on an object. This is a feature, design, of Assign subscriber - to work only with reference types.
extension Publisher where Self.Failure == Never {
/// Assigns each element from a Publisher to a property on an object.
///
/// - Parameters:
/// - keyPath: The key path of the property to assign.
/// - object: The object on which to assign the value.
/// - Returns: A cancellable instance; used when you end assignment of the received value. Deallocation of the result will tear down the subscription stream.
public func assign<Root>(to keyPath: ReferenceWritableKeyPath<Root, Self.Output>, on object: Root) -> AnyCancellable
}
The answer from Asperi is correct in so far as it explains the framework's design. The conceptual reason is that since passengers is a value type, passing it to assign(to:on:) would cause the copy of passengers passed to assign to be modified, which wouldn't update the value in your class instance. That's why the API prevents that. What you want to do is update the passengers.women property of self, which is what your closure example does:
minusButtonTapPublisher
.map { self.passengers.women - 1 }
// WARNING: Leaks memory!
.assign(to: \.passengers.women, on: self)
.store(in: &cancellables)
}
Unfortunately this version will create a retain cycle because assign(to:on:) holds a strong reference to the object passed, and the cancellables collection holds a strong reference back. See How to prevent strong reference cycles when using Apple's new Combine framework (.assign is causing problems) for further discussion, but tl;dr: use the weak self block based version if the object being assigned to is also the owner of the cancellable.

Is there an alternative to Combine's #Published that signals a value change after it has taken place instead of before?

I would like to use Combine's #Published attribute to respond to changes in a property, but it seems that it signals before the change to the property has taken place, like a willSet observer. The following code:
import Combine
class A {
#Published var foo = false
}
let a = A()
let fooSink = a.$foo.dropFirst().sink { _ in // `dropFirst()` is to ignore the initial value
print("foo is now \(a.foo)")
}
a.foo = true
outputs:
foo is now false
I'd like the sink to run after the property has changed like a didSet observer so that foo would be true at that point. Is there an alternative publisher that signals then, or a way of making #Published work like that?
There is a thread on the Swift forums for this issue. Reasons of why they made the decision to fire signals on "willSet" and not "didSet" explained by Tony_Parker
We (and SwiftUI) chose willChange because it has some advantages over
didChange:
It enables snapshotting the state of the object (since you
have access to both the old and new value, via the current value of
the property and the value you receive). This is important for
SwiftUI's performance, but has other applications.
"will" notifications are easier to coalesce at a low level, because you can
skip further notifications until some other event (e.g., a run loop
spin). Combine makes this coalescing straightforward with operators
like removeDuplicates, although I do think we need a few more grouping
operators to help with things like run loop integration.
It's easier to make the mistake of getting a half-modified object with did,
because one change is finished but another may not be done yet.
I do not intuitively understand that I'm getting willSend event instead of didSet, when I receive a value. It does not seem like a convenient solution for me. For example, what do you do, when in ViewController you receiving a "new items event" from ViewModel, and should reload your table/collection? In table view's numberOfRowsInSection and cellForRowAt methods you can't access new items with self.viewModel.item[x] because it's not set yet. In this case, you have to create a redundant state variable just for the caching of the new values within receiveValue: block.
Maybe it's good for SwiftUI inner mechanisms, but IMHO, not so obvious and convenient for other usecases.
User clayellis in the thread above proposed solution which I'm using:
Publisher+didSet.swift
extension Published.Publisher {
var didSet: AnyPublisher<Value, Never> {
self.receive(on: RunLoop.main).eraseToAnyPublisher()
}
}
Now I can use it like this and get didSet value:
self.viewModel.$items.didSet.sink { [weak self] (models) in
self?.updateData()
}.store(in: &self.subscriptions)
I'm not sure if it is stable for future Combine updates, though.
UPD: Worth to mention that it can possibly cause bugs (races) if you set value from a different thread than the main.
Original topic link: https://forums.swift.org/t/is-this-a-bug-in-published/31292/37?page=2
You can write your own custom property wrapper:
import Combine
#propertyWrapper
class DidSet<Value> {
private var val: Value
private let subject: CurrentValueSubject<Value, Never>
init(wrappedValue value: Value) {
val = value
subject = CurrentValueSubject(value)
wrappedValue = value
}
var wrappedValue: Value {
set {
val = newValue
subject.send(val)
}
get { val }
}
public var projectedValue: CurrentValueSubject<Value, Never> {
get { subject }
}
}
Further to Eluss's good explanation, I'll add some code that works. You need to create your own PassthroughSubject to make a publisher, and use the property observer didSet to send changes after the change has taken place.
import Combine
class A {
public var fooDidChange = PassthroughSubject<Void, Never>()
var foo = false { didSet { fooDidChange.send() } }
}
let a = A()
let fooSink = a.fooDidChange.sink { _ in
print("foo is now \(a.foo)")
}
a.foo = true
Before the introduction of ObservableObject SwiftUI used to work the way that you specify - it would notify you after the change has been made. The change to willChange was made intentionally and is probably caused by some optimizations, so using ObservableObjsect with #Published will always notify you before the changed by design. Of course you could decide not to use the #Published property wrapper and implement the notifications yourself in a didChange callback and send them via objectWillChange property, but this would be against the convention and might cause issues with updating views. (https://developer.apple.com/documentation/combine/observableobject/3362556-objectwillchange) and it's done automatically when used with #Published.
If you need the sink for something else than ui updates, then I would implement another publisher and not go agains the ObservableObject convention.
Another alternative is to just use a CurrentValueSubject instead of a member variable with the #Published attribute. So for example, the following:
#Published public var foo: Int = 10
would become:
public let foo: CurrentValueSubject<Int, Never> = CurrentValueSubject(10)
This obviously has some disadvantages, not least of which is that you need to access the value as object.foo.value instead of just object.foo. It does give you the behavior you're looking for, however.

Value of type 'Observable<String>' has no member 'bind'

I am trying to bind a string value in ViewModel to a label in my ViewController but I am getting following error:
Value of type 'Observable' has no member 'bind'
My code for binding in ViewController:
self.viewModel.myNum
.map( { $0 })
.bind(to: serialNumberLabel.rx.text)
myNum is defined in viewModel as following:
var myNum: Observable<String>
No I have 2 problems here:
1. Above error in ViewController
2. Initializing myNum in ViewModel
I tried following for initializing myNum but I am getting error:
materialNum = Observable<String>("")
I think that you might forget to use import RxCocoa.
For me the code works, but .map({ $0 }) is redundant because it returns the same value it gets, and you of course forgot to add .disposed(by:) at the end:
self.viewModel.myNum
.bind(to: serialNumberLabel.rx.text)
.disposed(by:self.disposeBag)
About the initialization you might do as Valérian said:
materialNum = Observable.just("My string")
But if you change observable later you will need again bind the label text.
EDIT: Example (author request)
#pankaj, I would recommend you to download RxSwift project from GitHub and check their playgrounds.
import RxSwift
import RxCocoa
class MyViewModel: ReactiveCompatible {
fileprivate lazy var _text = BehaviorRelay<String>(value: "My Initial Text")
var text: String {
get { return _text.value }
set { _text.accept(newValue) }
}
}
extension Reactive where Base: MyViewModel {
var text:Observable<String> {
return base._text.asObservable()
}
var setText: AnyObserver<String> {
return Binder<String>(base, binding: { viewModel, value in
viewModel.text = value
}).asObserver()
}
}
Binder setText in Reactive extension above is not required in your case but may be useful in other cases.
Then you can bind:
self.viewModel.rx.text
.bind(to: serialNumberLabel.rx.text)
.disposed(by:self.disposeBag)
bind is for Relays (and Variable which is deprecated !).
Simply use subscribe
self.viewModel.myNum.subscribe(serialNumberLabel.rx.text)
You need to use one of the existing methods to create your observable:
materialNum = Observable.just("My string")