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
App Tracking Transparency Dialog Does Not Appear on iOS
How to Use Indices.Contains() in a Collection Extension in Swift 3
How to Loop Through View Outlets in a Uiviewcontroller with Swift
Swiftui - Passing Data from Swiftuiview to Scenekit
How to Transpose an Array More Swiftly
Swift 2 to 3 Migration for Prepareforsegue
Convert a Swift Array of String to a to a C String Array Pointer
How to Load an Image from Documents Directory on MACos Swift
Swift 2.1 [Uint8] --Utf8--> String
Fixing Nsurlconnection Deprecation from Swift 1.2 to 2.0
How to Convert a Uiimage to a Cvpixelbuffer
Scrollview + Navigationview Animation Glitch Swiftui
How to Save a Struct to Realm in Swift
Why Is Manually Setup Root View Controller Showing Black Screen