I am trying to leverage SwiftUI and Combine to store user defaults for my application. Looking at suggestions in a few other posts, I have updated my code as you see below. However, I am now getting the error of "Referencing instance method 'send()' on 'Subject' requires the types 'Setup' and 'Void' be equivalent". It has been suggested that I change "Setup" to void in the PassthroughSubject, however this then gives a hard crash in the app at startup - " Fatal error: No observable object of type Setup.Type found."
I am at a bit of loss... any pointers would be welcomed.
============== DataStoreClass ============
import SwiftUI
import Foundation
import Combine
class Setup: ObservableObject {
private var notificationSubscription: AnyCancellable?
let objectWillChange = PassthroughSubject<Setup,Never>()
#UserDefault(key: "keyValueBool", defaultValue: false)
var somevalueBool: Bool {
didSet{
objectWillChange.send() // <====== Referencing instance method 'send()' on 'Subject' requires the types 'Setup' and 'Void' be equivalent
}
}
init() {
notificationSubscription = NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification).sink { _ in
self.objectWillChange.send()
}
}
}
============= property wrapper ===========
import Foundation
#propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get {
UserDefaults(suiteName: "group.com.my.app")!.value(forKey: key) as? T ?? defaultValue
} set {
UserDefaults(suiteName: "group.com.my.app")!.set(newValue, forKey: key)
}
}
}
The error comes from the fact that you have declared your Output type as Setup, but you are calling objectWillChange with Void.
So you have to pass self to objectWillChange:
self.objectWillChange.send(self)
Important thing to notice is that you should call objectWillChange not in didSet but in willSet:
var somevalueBool: Bool {
willSet{
objectWillChange.send(self
}
}
You never set somevalueBool, so this bit of code will not get called anyway.
Your setup should look roughly like this:
class Setup: ObservableObject {
private var notificationSubscription: AnyCancellable?
public let objectWillChange = PassthroughSubject<Setup,Never>()
#UserDefault(key: "keyValueBool", defaultValue: false)
var somevalueBool: Bool
init() {
notificationSubscription = NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification).sink { _ in
self.objectWillChange.send(self)
}
}
}
The send method requires you to pass the input type of the subject, or a failure completion. So your send lines should pass the Setup;
objectWillChange.send(self)
That said, in most SwiftUI code, PassthroughSubject is <Void, Never> (such that send() does not require a parameter). It's not clear what the source of the crash you're describing is; we would need to see the code that's involved in the crash to debug that. I haven't reproduced it so far.
SwiftUI doesn't use a PassthroughSubject, it uses an ObservableObjectPublisher. I am pretty sure that that is an alias for PassthroughSubject<Void, Never> but I'm not sure. The ObservableObject protocol defines a correct objectWillChange for you so the best thing you can do is to remove your definition.
The publisher is objectWillChange and as its name suggests it should be sent in willSet and not didSet, I don't suppose that it matters much but Apple changed from didSet to willSet and my guess is that they had a good reason.
Related
What is wrong with the following test code ? When I enter a character into the field, the didSet goes into a recursive loop. If the inout + & are removed, the code functions as expected with didSet being triggered for each keystroke. Similarly, if I remove the #Published, but leave the inout and & the didSet recursion does not occur.
Why am I trying to do this? I have a form field type (a 4 char hex field) that requires common processing which is then re-encoded back to a base structure. Thus, the intent, is to abstract all the common code into a function that was triggered after each var had been set in the ModelView zone. This code is just a minimal example to reproduce the issue.
It looks like merely taking the address of an #published var triggers the associated didSet. An interpretation of this would be that using inout always re-writes the target var even if no change is made.
class A: ObservableObject
{
#Published var publishedVar = "abc" {
didSet {
print("didSet Triggered")
doNothing(commonText: &publishedVar)
}
}
private func doNothing(commonText: inout String)
{
}
}
struct ContentView: View {
#ObservedObject var a = A()
var body: some View {
TextField("dummy", text: $a.publishedVar)
}
}
In case it is relevant this is being run on Mac running Catlina as a Mac App (not IOS, or emulators). Xcode Version 12.4 (12D4e)
OK, I've tried done reading on sink and tried to follow the advice (updated class A below) and now I find that it is recursing within the doNothing function. Looks like I am missing something basic 8-(
class A: ObservableObject
{
var cancellable: AnyCancellable?
#Published var publishedVar: String = "def"
private func doNothing(commonText: inout String)
{
print("In doNothing \(commonText)")
}
init()
{
cancellable = $publishedVar.sink { value in
self.doNothing(commonText: &self.publishedVar)
print("saw sink \(value)")
}
}
}
The recursion will occur because of the inout argument to doNothing. Each time it is called it will set the result back to publishedVar.
Please also note, that $publishedVar.sink will be activated on willSet, not didSet, and so is not interchangeable with your original code.
If you want to be notified when a Published variable changes you shouldn’t be using didSet instead you should use combine and observe the variable.
I.E
$publishedVar.sink {
print("didSet Triggered")
doNothing(commonText: &publishedVar)
}...
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 { ... }
I'm new to SwiftUI and have been following along with #peter-friese's example SwiftUI/Firebase app.
My example app works perfectly, but am now trying to modify slightly but am unable to get the correct keypath syntax of in the assign/map in order to set a property.
Relevantly (and slightly simplified), there is a struct for the data model:
struct Task: Codable, Identifiable {
var title: String
var status: Bool
var flag: Bool
}
and a class for a view model:
class TaskCellViewModel {
#Published var task: Task
var iconName:String = ""
}
In this tutorial a map/assign is used within the TaskCellViewModel to set the value of the iconName property in the instance of the TaskCellViewModel as follows:
private var cancellables = Set<AnyCancellable>()
init(task: Task) {
self.task = task
$task
.map { task in
task.status ? "checkmark.circle.fill" : "circle"
}
.assign(to: \.iconName, on: self)
.store(in: &cancellables)
}
This works for setting the iconName. What is the correct syntax to set the flag property on the Task itself?
I have tried various combinations but none work, including:
.assign(to .\Task.task.flag, on: self)
.assign(to .\task.flag, on: self)
.assign(to .\Task.task.flag, on: TaskCellViewModel)
Using Task.task.flag it fails at runtime with EXC_BAD_ACCESS error or a type conversion compile error:
Cannot convert value of type AnswerRowViewModel.Type to expected argument type AnswerRowViewModel.
PS given I'm learning and trying to follow along with the tutorials, I'm hoping for a answer to the assign/map question - as opposed to a workaround/alternative.
Use sink instead
.sink { [weak self] flag in
self?.task.flag = flag
}
This is probably the assign arguments you were looking for:
.assign(to: \Task.flag, on: self.task)
BUT this won't work here since Task is a struct. The first argument is a ReferenceWritableKeyPath (docs) which doesn't work with a struct's value semantics. Your TaskCellViewModel is a class, so that's why it worked with self.
As Asperi already answered, you can use sink for this case and manually assign the value in the closure.
Currently I have the code below which I'm trying to use as a navigation switch so I can navigate through different views without using the crappy NavigationLinks and otherwise. I'm by default a WebDev, so I've been having a mountain of issues transferring my knowledge over to Swift, the syntax feels completely dissimilar to any code I've written before. Anyways, here's the code;
import Foundation
import Combine
import SwiftUI
class ViewRouter: ObservableObject {
let objectWillChange: PassthroughSubject<ViewRouter,Never>
#Published var currentPage: String = "page1" {
didSet {
objectWillChange.send(self)
}
}
init(currentPage: String) {
self.currentPage = currentPage
}
}
As you can see, it's really simple, and I just use the object to switch values and display different views on another file, the only errors which prevent me from building it is the fact that the initializer is saying "Return from initializer without initializing all stored properties", even though the only variable is the currentPage variable which is defined. I know it's saying that objectWillChange is not defined by the message, but objectWillChange doesn't have any value to be assigned. Any help would be appreciated.
You just declare objectWillChange, but don't initialise it.
Simply change the declaration from
let objectWillChange: PassthroughSubject<ViewRouter,Never>
to
let objectWillChange = PassthroughSubject<ViewRouter,Never>()
However, using a PassthroughSubject shouldn't be necessary. currentPage is already #Published, so you can simply subscribe to its publisher. What you are trying to achieve using a PassthroughSubject and didSet is already defined by the swiftUI property wrappers, ObservableObject and Published.
class ViewRouter: ObservableObject {
#Published var currentPage: String
init(currentPage: String) {
self.currentPage = currentPage
}
}
Then you can simply do
let router = ViewRouter(currentPage: "a")
router.$currentPage.sink { page in print(page) }
router.currentPage = "b" // the above subscription prints `"b"`
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")