Need keypath syntax for use in assign(to:_) in SwiftUI - swift

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.

Related

Is there a way to pass #Published object as a func argument UIKit?

I'm using UIKit not SwiftUI. I found solutions which all in SwiftUI but not solved my problem.
I have a #Published object like:
#Published var searchText = ""
I'm using that #Published object for search functionality like in following function. Also, I'm trying to reach that function outside of corresponding class which is final class MainViewModel
final class MainViewModel {
#Published var searchText = ""
//subscribes to the searchText 'Publisher'
func searchTextManipulation(searchText: String) {
$searchText
.debounce(for: .seconds(1.0), scheduler: RunLoop.main)
.removeDuplicates()
.sink { [weak self] (text) in //(text) is always empty string ""
...
I want to use that function parameter searchText: String as $searchText to search with text come from another screen something like:
final class MainViewController {
let viewModel = MainViewModel()
#Published var searchText = ""
override func viewDidLoad() {
viewModel.searchTextManipulation($searchText: searchText) //but it's not possible.
}
}
Can you show me a workaround for solving that crucial part?
Your code suggests that need to understand how Combine works a little bit better.
When you put together a publisher with a set of operators:
$searchText
.debounce(for: .seconds(1.0), scheduler: RunLoop.main)
.removeDuplicates()
.sink { text in doSomething(text) }
It's like you're putting together a set of pipes. You only have to put them together once. When they are assembled, and you put something into the input side, the pipes will transform the value and deliver it to the output side. If you subscribe to the output side, using sink or assign for example, then you can catch the transformed value and do something with it.
But you only need to build the pipeline once.
After you build the pipeline, you also have to keep ahold of it.
Your searchTextManipulation function is building a pipeline that is immediately destroyed when you leave the function. To prevent that you have to store a subscription (in the form of an AnyCancellable). sink and assign return subscriptions and you should retain those to keep the pipeline from being destroyed.
An #Published property creates a Publisher, with the same name and a $ prefix, that emits a value when the property is initialized or changed. In other words, when you change searchText the system will put the value into the input of a pipeline named $searchText for you.
So a complete example of what it looks like you are trying to do is the playground below.
MainViewModel has two published properties. One is searchText and the other is searchResults. We set up the model so that when searchText changes (with a debounce and uniq) a new value of searchResults is published.
In init we build a pipeline that starts with $searchText. Through the pipeline we transform a search string into an array of search results. (The idea that each step in a pipeline transforms a value from one thing to another is the model I use in my head to decide how to chain operators.)
The assign at the end of the pipeline takes the transformed result and assigns it to the searchResults property.
Note: To ensure our pipeline is not destroyed when init ends, we have to capture and store the subscription done by assign.
With that pipeline in place, whenever code changes searchText the pipeline transform that value into an array of search results, and store the result in searchResults. Since MainViewModel owns the subscription, the pipeline will only be destroyed when the view model instance is destroyed.
So how do you use it? Well, the searchResults get published to a pipeline named $searchResults whenever they change. So all you have to do is listen that pipeline for changes.
In MainViewController we want to listen for those changes. In viewDidLoad we set up a short pipeline that starts with $searchResults, drops the first result (it's sent when searchResults is initialized) and uses sink` to print the results to the console.
Again note: we are constructing the pipeline once, keeping track of it by storing the subscription, and it will run automatically every time a new value is put into the seachResults.
I added a function called userDoesSomething that should be called whenever you want to kick off the search process.
userDoesSomething assigns some text value that came from the user to the model's searchText property.
Because it's #Published, the system sends the new value into the $searchText pipeline created by the view model's init function.
The pipeline transforms the text into and array of search results and assigns the array to searchResults
Because searchResults is #Published it sends the array into the pipeline called $searchResults
The MainViewController created a pipeline to listen to $searchResults and that pipeline runs with the new results.
That pipeline sends the value to a sink that simply prints it to the console.
The bit at the very end of the playground simulates a user sending a string with each of the letters in the English alphabet ('a'-'z') once every 0.2 seconds to userDoesSomething. That kicks of the numbered sequence I described above and gives you something interesting to look at on the Playground console.
import UIKit
import Combine
import PlaygroundSupport
let TheItems = ["The", "Quick", "Brown", "Fox", "Jumped", "Over", "the", "Lazy", "Dogs"]
final class MainViewModel {
#Published var searchText = ""
#Published private(set) var searchResults = []
var subscription: AnyCancellable! = nil
init() {
self.subscription = $searchText
.debounce(for: .seconds(0.1), scheduler: RunLoop.main)
.removeDuplicates()
.map(findItems)
.assign(to: \.searchResults, on: self)
}
}
private func findItems(searchText: String) -> [String] {
if let firstChar = searchText.lowercased().first {
return TheItems.filter { $0.lowercased().contains(firstChar) }
}
return []
}
final class MainViewController: UIViewController {
var searchResultsSubscription : AnyCancellable! = nil
let viewModel = MainViewModel()
override func loadView() {
self.view = UIView(frame: CGRect(x: 0,y: 0,width: 200,height: 200))
}
override func viewDidLoad() {
searchResultsSubscription = viewModel.$searchResults
.dropFirst()
.sink{
// do what you like here when search results change
debugPrint($0)
}
}
func userDoesSomething(generates searchText: String) {
viewModel.searchText = searchText;
}
}
let model = MainViewModel()
let controller = MainViewController()
PlaygroundSupport.PlaygroundPage.current.liveView = controller
(UInt8(ascii: "a")...UInt8(ascii:"z"))
.enumerated()
.publisher
.sink { index, char in
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200 * index)) {
controller.userDoesSomething(generates: String(UnicodeScalar(char)))
}
}
Yes, you can pass a #Published wrapper around, by changing the function signature to expect the projected value of the property wrapper:
func searchTextManipulation(searchText: Published<String>.Publisher) {
...
}
, and later in your viewDidLoad just use the $:
override func viewDidLoad() {
viewModel.searchTextManipulation(searchText: $searchText)
}
Everytime you write $someVar, what the compiler does is to de-sugar it into _someVar.projectedValue, and in case of Published, the projected value is of type Published<T>.Publisher. This is not something specific to SwiftUI, and it's part of the language, so you can freely use it in any parts of your code (assuming it makes sense, ofcourse).

Swift #Published causing didSet recursion

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)
}...

Setting up a combine publisher in swift using map

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.

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.

Referencing instance method requires equivalency (SWIFT)

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.