sink value is Void with publisher - swift

Consider the below Observable Object.
class User: ObservableObject {
#Published var age: Int
#Published var name: String {
didSet {
objectWillChange.send()
}
}
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
The below code prints blank value or Void block. Any reason why? If we change Integer value age it should simply print that value.
let userJohnCancellable = userJohn.objectWillChange.sink { val in
print("Changed Value \(val)")
}
userJohn.age = 21
userJohn.age = 39
We can try to print the values in the closure using userJohn.age. But why does val not return a Integer value in this case.
Also what would be the best way to handle sink changes for age and name, both, one is String other is Int.

When you look in the documentation for ObservableObject you will find thatobjectWillChange is ObservableObjectPublisher
/// A publisher that emits before the object has changed.
public var objectWillChange: ObservableObjectPublisher { get }
which in turn is defined as having an output of type Void:
final public class ObservableObjectPublisher : Publisher {
/// The kind of values published by this publisher.
public typealias Output = Void
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Never
}
There is no need to send objectWillChange from didSet - each time any of the #Published values changes objectWillChange will emit a value.
If you want to get notified when a particular property marked as #Published changes and receive the new value you have to subscribe to that particular property:
let userJohn = User(name: "Johnny", age: 17)
let objectWillChangeCancellable = userJohn
.objectWillChange
.sink {
print("object will change")
}
let ageCancellable = userJohn
.$age
.sink { age in
print("new value of age is \(age)")
}
let nameCancellable = userJohn
.$name
.sink { name in
print("new value of name is \(name)")
}
This will get printed:
new value of age is 17
new value of name is Johnny
if you add:
userJohn.name = "John"
you will see the following printed:
object will change
new value of name is John
if you add:
userJohn.age = 21
you will see the following printed:
object will change
new value of age is 21

You seem to be confused about ObservableObject. It is for use with SwiftUI. But your code is not SwiftUI code, so you don't need ObservableObject and you really can't use it in any meaningful way. If the goal is to be able to subscribe to the properties of User so as to be notified when one of them changes, then it suffices to make those properties Published:
class User {
#Published var age: Int
#Published var name: String
init(age: Int, name: String) {
self.age = age; self.name = name
}
}
Here's an example of using it; I will assume we have a user property of a UIViewController:
class ViewController: UIViewController {
var cancellables = Set<AnyCancellable>()
var user = User(age: 20, name: "Bill")
override func viewDidLoad() {
super.viewDidLoad()
user.$age.sink {print("age:", $0)}.store(in: &cancellables)
user.$name.sink {print("name:", $0)}.store(in: &cancellables)
}
}
If this view controller's user has its age or name changed, you'll see the print output in the console.
If the question is how to handle both changes in a single pipeline, they have different types, as you observe, so you'd need to define a union so that both types can come down the same pipeline:
class User {
#Published var age: Int
#Published var name: String
enum Union {
case age(Int)
case name(String)
}
var unionPublisher: AnyPublisher<Union, Never>?
init(age: Int, name: String) {
self.age = age; self.name = name
let ageToUnion = $age.map { Union.age($0) }
let nameToUnion = $name.map { Union.name($0) }
unionPublisher = ageToUnion.merge(with: nameToUnion).eraseToAnyPublisher()
}
}
And again, here's an example of using it:
class ViewController: UIViewController {
var cancellables = Set<AnyCancellable>()
var user = User(age: 20, name: "Bill")
override func viewDidLoad() {
super.viewDidLoad()
user.unionPublisher?.sink { union in
switch union {
case .age(let age): print ("age", age)
case .name(let name): print ("name", name)
}
}.store(in: &cancellables)
}
}
Again, change the user property's name or age and you'll get an appropriate message in the console.

Related

Why objectWillChange has no effect

I got this sample code about ObservableObject from Apple official website
import Foundation
class Contact: ObservableObject {
#Published var name: String
#Published var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func changeAge() -> Int {
self.age += 1
return self.age
}
}
class Test {
init() {
let john = Contact(name: "John Appleseed", age: 24)
_ = john.objectWillChange
.sink { _ in
print("\(john.age) will change")
}
print(john.changeAge())
}
}
let test = Test()
When running on terminal by swift Contact.swift, the result is only 25, but official website shows the result should be
// Prints "24 will change"
// Prints "25"
Why the first line Prints "24 will change" is not shown?
Thank you.
You have to store the AnyCancellable that's returned by sink, otherwise it cancels the subscription as soon as it's deinitialized when you assign to _.
In your simple example just assigning to a local variable is enough to get the printout that you want, because the variable still lives when you change the age:
let c = john.objectWillChange.sink {...}
But in a real app, you'd want to assign it to a property, which would keep the subscription for the duration that you need. Typically, it's done like this:
class Test {
private var c: Set<AnyCancellable> = []
init() {
let john = Contact(name: "John Appleseed", age: 24)
john.objectWillChange
.sink { _ in
print("\(john.age) will change")
}
.store(in: &c) // store here
print(john.changeAge())
}
}

Swift Combine - How to subscribe to nested Observable Objects

This is a slightly more abstract version of this question. In the app, these nested Observable Objects are indeed used in a view (so I'd rather use Observable Objects rather than straight Publishers). However, I would like to be able to simply subscribe to the view models in order to test them. The protocol is there so I can mock out Nested in tests.
This is the basic setup:
protocol NestedProtocol: AnyObject {
var string: String { get set }
}
class Nested: ObservableObject, NestedProtocol {
#Published var string = ""
}
class Parent: ObservableObject {
#Published var nested: NestedProtocol
init(nested: NestedProtocol) {
self.nested = nested
}
}
var sinkHole = Set<AnyCancellable>()
let nested = Nested()
let parent = Parent(nested: nested)
parent.$nested.sink { newValue in
print("NEW VALUE \(newValue.string)")
}.store(in: &sinkHole)
Then this command
nested.string = "foo1" outputs "NEW VALUE ", which is expected as the initial value of nested. I would like it to output "NEW VALUE foo1". (TIL published variables seem to be current value publishers.)
Of course I could do
nested.string = "foo1"
parent.nested = nested
and I would get "NEW VALUE foo1", but that's smelly.
I tried
protocol NestedProtocol: ObservableObject {
var string: String { get set }
}
class Nested<T>: ObservableObject where T: NestedProtocol {
...
But in real life, I would like nested to declare some static constants, which is not allowed in generic types. So that doesn't work.
From the cited question/answer, I also tried combinations of
Parent
init() {
nested.objectWillChange.sink { [weak self] (_) in
self?.objectWillChange.send()
}.store(in: sinkHole)
}
Nested
init() {
string.sink { [weak self] (_) in
self?.objectWillChange.send()
}.store(in: sinkHole)
}
No dice. Those methods were getting called but that outer-level sink was still just returning "NEW VALUE "
I also tried calling
parent.nested.string = "foo1"
So now I'm modifying the parent, and that should work, right? Wrong.
There's a bunch to unpack here.
First, you might know that if a property is a value-type, like a struct or String, then marking it as #Published just works:
class Outer {
#Published var str: String = "default"
}
let outer = Outer()
outer.$str.sink { print($0) }
outer.str = "changed"
Will output:
default
changed
Your question, however, is about a nested observable object, which is a reference type. So, the above wouldn't work with a reference-type.
But in your example you're using a protocol as an existential (i.e. in place of an eventual instance), and as you noted, without inheriting from AnyObject, then it really behaves like a value-type:
protocol InnerProtocol {
var str: String { get set }
}
class Inner: InnerProtocol {
#Published var str: String = "default"
}
class Outer {
#Published var inner: InnerProtocol
init(_ inner: InnerProtocol) { self.inner = inner }
}
let inner = Inner()
let outer = Outer(inner)
outer.$inner.sink { print($0.str) }
outer.inner.str = "changed"
This would also output:
default
changed
which looks like what you wanted, but in fact it doesn't really "observe" any changes in the nested object. When you do outer.inner.str, it has value-type semantics, so it's as-if you re-assigned the .inner property. But if you are truly interested in observing changes of the object itself, then this approach wouldn't work at all. For example:
nested.str = "inner changed"
would not cause an output. Neither would there be an output if the inner object changed its own property, e.g.:
init() {
DisplatchQueue.main.asyncAfter(.now() + 1) {
self.str = "async changed"
}
}
So, it's unclear what exactly you're trying to achieve. If you want to observe a reference type property, you'd need to observe it directly.
class Inner: ObservableObject {
#Published var str: String
//...
}
class Outer: ObservableObject {
var inner: Inner
//...
}
//...
outer.inner.$str.sink { ... }
// or
outer.inner.objectWillChange.sink { ... }
You can achieve this with a protocol too, if you insist:
protocol InnerProtocol: ObservableObject {
var str: String { get set }
}
class Inner: InnerProtocol {
#Published var str: String = "default"
}
class Outer<T: InnerProtocol>: ObservableObject {
var inner: T
init(_ inner: T) { self.inner = inner }
}
let inner = Inner()
let outer = Outer(inner)
outer.inner.$str.sink { ... }
inner.str = "changed"
This took me hours and I stumbled upon it by accident while trying to modify my protocol in various ways.
Lesson 1:
protocol NestedProtocol: AnyObject {
var string: String { get set }
}
should be
protocol NestedProtocol {
var string: String { get set }
}
Why? I'm not sure. Apparently, if the Parent cannot assume that the published object is a class, then it watches modifications on it more closely? My instinct tells me the exact opposite, but it goes to show how much I can trust my instinct.
Lesson 2:
Indeed my 4th idea was correct and you need to name the parent in the nested object modification:
nested.string = "foo1"
should be
parent.nested.string = "foo1"
Again, they're all classes so it goes slightly against my understanding, but I don't know all the magic that goes on under #Published.
The final complete version looks like this:
protocol NestedProtocol {
var string: String { get set }
}
class Nested: ObservableObject, NestedProtocol {
#Published var string = ""
}
class Parent: ObservableObject {
#Published var nested: NestedProtocol
init(nested: NestedProtocol) {
self.nested = nested
}
}
var sinkHole = Set<AnyCancellable>()
let nested = Nested()
let parent = Parent(nested: nested)
parent.$nested.sink { newValue in
print("NEW VALUE \(newValue.string)")
}.store(in: &sinkHole)
and
nested.string = "foo1"
parent.nested.string = "foo2"
returns
"NEW VALUE "
"NEW VALUE foo2"

How to save and load GKGameModelPlayer from Realm in Swift?

I am attempting to implement a GKGameModel in my application. In it, it holds variables to a few things, but for the purposes of my question I'm interested in the following two variables:
import GameplayKit
final class GameModel: NSObject, GKGameModel {
var players: [GKGameModelPlayer]?
var activePlayer: GKGameModelPlayer?
}
I do something like this to initialise the game with 3 players (not exact)
let game = GameModel.init()
game.players = [Player(),Player(),Player()] // Create 3 players
guard let firstPlayer = game.players.first else {
return
}
game.activePlayer = firstPlayer
A player class is defined as:
class Player : NSObject, GKGameModelPlayer {
var playerId: Int // GKGameModelPlayer protocol variable
let name: String
var cash: Int = 0
}
In my project I have Realm Entities and the models seperated. So there will be a PlayerEntity and a Player class.
I'm wanting to use RealmSwift to save and load the GKGameModelPlayer data, and more specifically the ability to store/re-store the active player.
I think the key here is the playerId variable; but I am not sure.
But what I'm not sure about is retrieving this information and then re-mapping it into a valid GKGameModelPlayer format
My current idea/theory is that I need to map my model to an entity class and vice-versa.
Ie:
// [REALM] Player entity
class PlayerEntity: Object {
#objc dynamic var id = UUID().uuidString
#objc dynamic var playerId: Int = 0
#objc dynamic var name: String = ""
#objc dynamic var cash: Int = 0
override static func primaryKey() -> String {
return "id"
}
}
And then I extend this class to do some "mapping":
extension PlayerEntity {
// Map model -> entity
convenience init(model: Player) {
self.init()
self.playerId = model.playerId
self.name = model.name
self.cash = model.cash
}
}
extension Player {
// Map entity -> model
convenience init(entity: PlayerEntity) {
let playerId = entity.playerId
let name = entity.name
let cash = entity.cash
self.init(id: playerId, name: name, cash: cash)
}
}
Right now, the playerId is always zero (0) because I'm not really sure how to set it.
I can save a player to realm.
The issue comes from when I try to restore the player, and I want to restore the activePlayer variable in the GameModel
Therefore, my question is:
How would I go about saving and restoring the activePlayer variable so that it continues to comply to GKGameModelPlayer?
I appreciate any assistance on this.
With thanks
While you could use those extensions, sometimes simpler is better. Here's a rough example:
class PlayerEntity: Object {
#objc dynamic var playerId: Int = 0
#objc dynamic var name: String = ""
#objc dynamic var cash: Int = 0
convenience init(withPlayer: PlayerClass) {
self.init()
self.playerId = withPlayer.playerId
self.name = withPlayer.name
self.cash = withPlayer.cash
}
func getPlayer() -> Player {
let p = Player()
p.playerId = self.playerId
p.name = self.name
p.cash = self.cash
return p
}
override static func primaryKey() -> String {
return "playerId"
}
}
to load all the players into an array... this will do it
let playerResults = realm.objects(PlayerEntity.self)
for player in playerResults {
let aPlayer = player.getPlayer()
self.playerArray.append(aPlayer)
}
Notice the removal of
#objc dynamic var id = UUID().uuidString
because it's not really being used to identify the object as a primary key.
The primary key is really
var playerId: Int // GKGameModelPlayer protocol variable
which is fine to use as long as it's unique.

How to wrap relationship children into array in Vapor?

I have a parent-child relationship, and the children need to wrap into array how can I do it?
event.testPrices = release.testPrices
final class Event: Content {
var id: String
var inProgress: Bool?
var name: String
var purpose: String?
var testPrices: [TestPrice]
init(id: String, name: String) {
self.id = id
self.name = name
}
}
extension Release {
var testPrices: Children<Release, TestPrice> {
return children(\.releaseId)
}
}
The assignment gives the error:
Cannot assign value of type 'Children' to type '[TestPrice]'
You can use a query to form the Future array and then map it. Assuming you are in some controller/route where event contains the appropriate Event and release contains the appropriate Release, try this:
{
release, event in
_ = release.testPrices.query(on:request).all().map { testP in
// testP is now [TestPrice]
event.testPrices = testP
}
}

Change the value that is being set in variable's willSet block

I'm trying to sort the array that is being set before setting it but the argument of willSet is immutable and sort mutates the value. How can I overcome this limit?
var files:[File]! = [File]() {
willSet(newFiles) {
newFiles.sort { (a:File, b:File) -> Bool in
return a.created_at > b.created_at
}
}
}
To put this question out of my own project context, I made this gist:
class Person {
var name:String!
var age:Int!
init(name:String, age:Int) {
self.name = name
self.age = age
}
}
let scott = Person(name: "Scott", age: 28)
let will = Person(name: "Will", age: 27)
let john = Person(name: "John", age: 32)
let noah = Person(name: "Noah", age: 15)
var sample = [scott,will,john,noah]
var people:[Person] = [Person]() {
willSet(newPeople) {
newPeople.sort({ (a:Person, b:Person) -> Bool in
return a.age > b.age
})
}
}
people = sample
people[0]
I get the error stating that newPeople is not mutable and sort is trying to mutate it.
It's not possible to mutate the value inside willSet. If you implement a willSet observer, it is passed the new property value as a constant parameter.
What about modifying it to use didSet?
var people:[Person] = [Person]()
{
didSet
{
people.sort({ (a:Person, b:Person) -> Bool in
return a.age > b.age
})
}
}
willSet is called just before the value is stored.
didSet is called immediately after the new value is stored.
You can read more about property observers here
https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Properties.html
You can also write a custom getter and setter like below. But didSet seems more convenient.
var _people = [Person]()
var people: [Person] {
get {
return _people
}
set(newPeople) {
_people = newPeople.sorted({ (a:Person, b:Person) -> Bool in
return a.age > b.age
})
}
}
It is not possible to change value types (including arrays) before they are set inside of willSet. You will need to instead use a computed property and backing storage like so:
var _people = [Person]()
var people: [Person] {
get {
return _people
}
set(newPeople) {
_people = newPeople.sorted { $0.age > $1.age }
}
}
Another solution for people who like abstracting away behavior like this (especially those who are used to features like C#'s custom attributes) is to use a Property Wrapper, available since Swift 5.1 (Xcode 11.0).
First, create a new property wrapper struct that can sort Comparable elements:
#propertyWrapper
public struct Sorting<V : MutableCollection & RandomAccessCollection>
where V.Element : Comparable
{
var value: V
public init(wrappedValue: V) {
value = wrappedValue
value.sort()
}
public var wrappedValue: V {
get { value }
set {
value = newValue
value.sort()
}
}
}
and then assuming you implement Comparable-conformance for Person:
extension Person : Comparable {
static func < (lhs: Person, rhs: Person) -> Bool {
lhs.age < lhs.age
}
static func == (lhs: Person, rhs: Person) -> Bool {
lhs.age == lhs.age
}
}
you can declare your property like this and it will be auto-sorted on init or set:
struct SomeStructOrClass
{
#Sorting var people: [Person]
}
// … (given `someStructOrClass` is an instance of `SomeStructOrClass`)
someStructOrClass.people = sample
let oldestPerson = someStructOrClass.people.last
Caveat: Property wrappers are not allowed (as of time of writing, Swift 5.7.1) in top-level code— they need to be applied to a property var in a struct, class, or enum.
To more literally follow your sample code, you could easily also create a ReverseSorting property wrapper:
#propertyWrapper
public struct ReverseSorting<V : MutableCollection & RandomAccessCollection & BidirectionalCollection>
where V.Element : Comparable
{
// Implementation is almost the same, except you'll want to also call `value.reverse()`:
// value = …
// value.sort()
// value.reverse()
}
and then the oldest person will be at the first element:
// …
#Sorting var people: [Person]
// …
someStructOrClass.people = sample
let oldestPerson = someStructOrClass.people[0]
And even more directly, if your use-case demands using a comparison closure via sort(by:…) instead of implementing Comparable conformance, you can do that to:
#propertyWrapper
public struct SortingBy<V : MutableCollection & RandomAccessCollection>
{
var value: V
private var _areInIncreasingOrder: (V.Element, V.Element) -> Bool
public init(wrappedValue: V, by areInIncreasingOrder: #escaping (V.Element, V.Element) -> Bool) {
_areInIncreasingOrder = areInIncreasingOrder
value = wrappedValue
value.sort(by: _areInIncreasingOrder)
}
public var wrappedValue: V {
get { value }
set {
value = newValue
value.sort(by: _areInIncreasingOrder)
}
}
}
// …
#SortingBy(by: { a, b in a.age > b.age }) var people: [Person] = []
// …
someStructOrClass.people = sample
let oldestPerson = someStructOrClass.people[0]
Caveat: The way SortingBy's init currently works, you'll need to specify an initial value ([]). You can remove this requirement with an additional init (see Swift docs), but that approach is much less complicated when your property wrapper works on a concrete type (e.g. if you wrote a non-generic PersonArraySortingBy property wrapper), as opposed to a generic-on-protocols property wrapper.