Add @Published Behaviour for Computed Property

Add @Published behaviour for computed property

Updated:
With the EnclosingSelf subscript, one can do it!

Works like a charm!

import Combine
import Foundation

class LocalSettings: ObservableObject {
static var shared = LocalSettings()

@Setting(key: "TabSelection")
var tabSelection: Int = 0
}

@propertyWrapper
struct Setting<T> {
private let key: String
private let defaultValue: T

init(wrappedValue value: T, key: String) {
self.key = key
self.defaultValue = value
}

var wrappedValue: T {
get {
UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}

public static subscript<EnclosingSelf: ObservableObject>(
_enclosingInstance object: EnclosingSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, T>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Setting<T>>
) -> T {
get {
return object[keyPath: storageKeyPath].wrappedValue
}
set {
(object.objectWillChange as? ObservableObjectPublisher)?.send()
UserDefaults.standard.set(newValue, forKey: object[keyPath: storageKeyPath].key)
}
}
}

Combine @Published property send values also when not updated

The problem is the updatedImagesPublisher is a computed property. It means that you create a new instance every time you access it. What happens in your code. The Text object subscribes to updatedImagesPublisher, when it receives a new value, it updates the number property of the Model. number is @Published property, it means that objectWillChange method will be called every time you change it and the body will be recreated. New Text will subscribe to new updatedImagesPublisher (because it is computed property) and receive the value again. To avoid such behaviour just use lazy property instead of computed property.

lazy var updatedImagesPublisher: AnyPublisher<Int, Never> = {
return $myString
.removeDuplicates()
.print()
.flatMap { newImageType in
return Future<Int, Never> { promise in

print("Executing...")

// Simulate network request
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
let newNumber = Int.random(in: 1...200)
return promise(.success(newNumber))
}
}
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}()

How to re-render UI in response to computed property buried in a nested class?

Below is what I came up with.

The root of the sequence is the textPublisher. This begins the sequence with the values sent to text.

didSet sends the text to the sequence and saves it in the person's name just as the original code does.

isComplete becomes a publisher that sends true or false depending on whether the components are valid. The chain of map operators each take the value through one step of the computations in your original code. You could easily reduce this to a single map I would think. (or filter the computations out into functions with meaningful names and substitute the functions for the closures)

An external Subscriber could subscribe to isComplete and respond when it emits a true value.

final class PersonName {
var person: Person

private let formatter = PersonNameComponentsFormatter()

let textPublisher = PassthroughSubject<String, Never>()
var text: String {
get { person.name ?? "" }
set { textPublisher.send(newValue); person.name = newValue }
}

var isComplete : AnyPublisher<Bool, Never>!

init(for person: Person) {
self.person = person

isComplete = textPublisher
.map{ self.formatter.personNameComponents(from: $0) }
.map{ (components: PersonNameComponents?) -> Set<Elements> in
var collection = Set<Elements>()

if let components = components {
if self.isValid(component: components.givenName) { collection.insert(.givenName) }
if self.isValid(component: components.familyName) { collection.insert(.familyName) }
}

return collection
}
.map { Set([Elements.givenName, Elements.familyName]).isSubset(of: $0) }
.eraseToAnyPublisher()
}

private func isValid(component: String?) -> Bool {
if let name = component, name.count > 1 {
return true
}
return false
}
}

Mimic Swift Combine @Published to create @PublishedAppStorage

Thanks for the direction from David, I've managed to get it working. I'm posting here my final Property Wrapper in case anyone finds it helpful.

import SwiftUI
import Combine

@propertyWrapper
public struct PublishedAppStorage<Value> {

// Based on: https://github.com/OpenCombine/OpenCombine/blob/master/Sources/OpenCombine/Published.swift

@AppStorage
private var storedValue: Value

private var publisher: Publisher?
internal var objectWillChange: ObservableObjectPublisher?

/// A publisher for properties marked with the `@Published` attribute.
public struct Publisher: Combine.Publisher {

public typealias Output = Value

public typealias Failure = Never

public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Value, Downstream.Failure == Never
{
subject.subscribe(subscriber)
}

fileprivate let subject: Combine.CurrentValueSubject<Value, Never>

fileprivate init(_ output: Output) {
subject = .init(output)
}
}

public var projectedValue: Publisher {
mutating get {
if let publisher = publisher {
return publisher
}
let publisher = Publisher(storedValue)
self.publisher = publisher
return publisher
}
}

@available(*, unavailable, message: """
@Published is only available on properties of classes
""")
public var wrappedValue: Value {
get { fatalError() }
set { fatalError() }
}

public static subscript<EnclosingSelf: ObservableObject>(
_enclosingInstance object: EnclosingSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, PublishedAppStorage<Value>>
) -> Value {
get {
return object[keyPath: storageKeyPath].storedValue
}
set {
// https://stackoverflow.com/a/59067605/14314783
(object.objectWillChange as? ObservableObjectPublisher)?.send()
object[keyPath: storageKeyPath].publisher?.subject.send(newValue)
object[keyPath: storageKeyPath].storedValue = newValue
}
}

// MARK: - Initializers

// RawRepresentable
init(wrappedValue: Value, _ key: String, store: UserDefaults? = nil) where Value : RawRepresentable, Value.RawValue == String {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}

// String
init(wrappedValue: String, _ key: String, store: UserDefaults? = nil) where Value == String {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}

// Data
init(wrappedValue: Data, _ key: String, store: UserDefaults? = nil) where Value == Data {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}

// Int
init(wrappedValue: Int, _ key: String, store: UserDefaults? = nil) where Value == Int {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}

// URL
init(wrappedValue: URL, _ key: String, store: UserDefaults? = nil) where Value == URL {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}

// Double
init(wrappedValue: Double, _ key: String, store: UserDefaults? = nil) where Value == Double {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}

// Bool
init(wrappedValue: Bool, _ key: String, store: UserDefaults? = nil) where Value == Bool {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}

}

And this how it is used:

public class MyDefaults: ObservableObject
{
@PublishedAppStorage("useVerboseLog")
public var useVerboseLog: Bool = true
}

Access environment object property within a struct's computed property

You should use @EnvironmentObject in your view and pass it down to your model. And don't forget to call .environmentObject to inject your CardViewModel in SceneDelegate. For exemple :

let myView = MyView().environmentObject(CardViewModel())

Which is an appropriate protocol constraint to identify a property as being '@Published'

As far as I could replicate your code here is a solution (tested with Xcode 12.1 / iOS 14.1)

extension Diskable where T: Identifiable {
func binding(for object: T) -> Binding<T> {
let index = fetchedItems.firstIndex(where: { $0.id == object.id } )!
return Binding(
get: { self.fetchedItems[index] },
set: { self.fetchedItems[index] = $0 }
)
}
}

@Published property wrapper not working on subclass of ObservableObject

Finally figured out a solution/workaround to this issue. If you remove the property wrapper from the subclass, and call the baseclass objectWillChange.send() on the variable the state is updated properly.

NOTE: Do not redeclare let objectWillChange = PassthroughSubject<Void, Never>() on the subclass as that will again cause the state not to update properly.

I hope this is something that will be fixed in future releases as the objectWillChange.send() is a lot of boilerplate to maintain.

Here is a fully working example:

    import SwiftUI

class MyTestObject: ObservableObject {
@Published var aString: String = ""

}

class MyInheritedObject: MyTestObject {
// Using @Published doesn't work on a subclass
// @Published var anotherString: String = ""

// If you add the following to the subclass updating the state also doesn't work properly
// let objectWillChange = PassthroughSubject<Void, Never>()

// But if you update the value you want to maintain state
// of using the objectWillChange.send() method provided by the
// baseclass the state gets updated properly... Jaayy!
var anotherString: String = "" {
willSet { self.objectWillChange.send() }
}
}

struct MyTestView: View {
@ObservedObject var myTestObject = MyTestObject()
@ObservedObject var myInheritedObject = MyInheritedObject()

var body: some View {
NavigationView {
VStack(alignment: .leading) {
TextField("Update aString", text: self.$myTestObject.aString)
Text("Value of aString is: \(self.myTestObject.aString)")

TextField("Update anotherString", text: self.$myInheritedObject.anotherString)
Text("Value of anotherString is: \(self.myInheritedObject.anotherString)")
}
}
}
}


Related Topics



Leave a reply



Submit