Make ObservableObject subscribe to custom PropertyWrapper - swift

I have written a custom PropertyWrapper, that tries to wrap UserDefaults while also giving them the same behaviour as a #Published variable. It almost works, except that the ObservableObject does not propagate the changes without observing the UserDefaults themselves.
I cannot pass a objectWillChange ref to the #Setting init, as self is not available during Settings.init...
I wonder how #Published does that..
import Combine
import Foundation
class Settings: ObservableObject {
// Trying to avoid this:
/////////////////////////////////////////////
let objectWillChange = PassthroughSubject<Void, Never>()
private var didChangeCancellable: AnyCancellable?
private init(){
didChangeCancellable = NotificationCenter.default
.publisher(for: UserDefaults.didChangeNotification)
.map { _ in () }
.receive(on: DispatchQueue.main)
.subscribe(objectWillChange)
}
/////////////////////////////////////
static var shared = Settings()
#Setting(key: "isBla") var isBla = true
}
#propertyWrapper
public struct Setting<T> {
let key: String
let defaultValue: T
init(wrappedValue value: T, key: String) {
self.key = key
self.defaultValue = value
}
public var wrappedValue: T {
get {
let val = UserDefaults.standard.object(forKey: key) as? T
return val ?? defaultValue
}
set {
objectWillChange?.send()
publisher?.subject.value = newValue
UserDefaults.standard.set(newValue, forKey: key)
}
}
public struct Publisher: Combine.Publisher {
public typealias Output = T
public typealias Failure = Never
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == T, Downstream.Failure == Never {
subject.subscribe(subscriber)
}
fileprivate let subject: Combine.CurrentValueSubject<T, Never>
fileprivate init(_ output: Output) {
subject = .init(output)
}
}
private var publisher: Publisher?
internal var objectWillChange: ObservableObjectPublisher?
public var projectedValue: Publisher {
mutating get {
if let publisher = publisher {
return publisher
}
let publisher = Publisher(wrappedValue)
self.publisher = publisher
return publisher
}
}
}

Related

How can I listen to changes in a #AppStorage property when not in a view?

The following is the content of a playground that illustrates the problem. Basically I have a value stored in UserDefaults and accessed by a variable wrapped in the #AppStorage property wrapper. This lets me access the updated value in a View but I'm looking for a way to listen to changes in the property in ViewModels and other non-View types.
I have it working in the follow code but I'm not sure it's the best way to do it and I'd love to avoid having to declare a PassthroughSubject for each property I want to watch.
Note: I did originally sink the ObservableObject's objectWillChange property however that will reflect any change to the object and I'd like to do something more fine grained.
So does anyone have any ideas on how to improve this technique?
import Combine
import PlaygroundSupport
import SwiftUI
class AppSettings: ObservableObject {
var myValueChanged = PassthroughSubject<Int, Never>()
#AppStorage("MyValue") var myValue = 0 {
didSet { myValueChanged.send(myValue) }
}
}
struct ContentView: View {
#ObservedObject var settings: AppSettings
#ObservedObject var viewModel: ValueViewModel
init() {
let settings = AppSettings()
self.settings = settings
viewModel = ValueViewModel(settings: settings)
}
var body: some View {
ValueView(viewModel)
.environmentObject(settings)
}
}
class ValueViewModel: ObservableObject {
#ObservedObject private var settings: AppSettings
#Published var title: String = ""
private var cancellable: AnyCancellable?
init(settings: AppSettings) {
self.settings = settings
title = "Hello \(settings.myValue)"
// Is there a nicer way to do this?????
cancellable = settings.myValueChanged.sink {
print("object changed")
self.title = "Hello \($0)"
}
}
}
struct ValueView: View {
#EnvironmentObject private var settings: AppSettings
#ObservedObject private var viewModel: ValueViewModel
init(_ viewModel: ValueViewModel) {
self.viewModel = viewModel
}
var body: some View {
Text("This is my \(viewModel.title) value: \(settings.myValue)")
.frame(width: 300.0)
Button("+1") {
settings.myValue += 1
}
}
}
PlaygroundPage.current.setLiveView(ContentView())
The accepted solution PublishingAppStorage class is not fully working when binding views and
#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: Value, _ key: String, store: UserDefaults? = nil) where Value: RawRepresentable, Value.RawValue == 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)
}
}
This one works as if you were using #AppStorage
viewModel.$fade.sink { [weak self] newFadeSetting in
}
where fade is
#PublishedAppStorage("fade") var fade: ScreenFadeSettingItem = .on
And for binding you can simply use
SettingSegmentPicker<ScreenFadeSettingItem>(
titles: ScreenFadeSettingItem.allCases,
selection: $viewModel.fade
)
The AppStorage changes in ObservableObject result in firing objectWillChange, so we can use it and code becomes much simpler
class AppSettings: ObservableObject {
#AppStorage("MyValue") var myValue = 0
}
class ValueViewModel: ObservableObject {
private var settings = AppSettings()
#Published var title: String = ""
private var cancellable: AnyCancellable?
init() {
cancellable = settings.objectWillChange.sink { [weak self] _ in
guard let self = self else { return }
self.title = "Hello \(self.settings.myValue)"
}
}
}
Yes, it is not known which property did change exactly, but assigning same (not modified) will not generate following updates (eg. .onChange, etc.).
So should be considered case-by-case, but can be applicable.
BTW, #ObservedObject works only in View, so in VM is just redundant.
I wrote this property wrapper:
/// Property wrapper that acts the same as #AppStorage, but also provides a ``Publisher`` so that non-View types
/// can receive value updates.
#propertyWrapper
struct PublishingAppStorage<Value> {
var wrappedValue: Value {
get { storage.wrappedValue }
set {
storage.wrappedValue = newValue
subject.send(storage.wrappedValue)
}
}
var projectedValue: Self {
self
}
/// Provides access to ``AppStorage.projectedValue`` for binding purposes.
var binding: Binding<Value> {
storage.projectedValue
}
/// Provides a ``Publisher`` for non view code to respond to value updates.
private let subject = PassthroughSubject<Value, Never>()
var publisher: AnyPublisher<Value, Never> {
subject.eraseToAnyPublisher()
}
private var storage: AppStorage<Value>
init(wrappedValue: Value, _ key: String) where Value == String {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value: RawRepresentable, Value.RawValue == Int {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value == Data {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value == Int {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value: RawRepresentable, Value.RawValue == String {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value == URL {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value == Double {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value == Bool {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
mutating func update() {
storage.update()
}
}
Basically it wraps #AppStorage and adds a Publisher. Using it is exactly the same from a declaration point of view:
#PublishedAppStorage("myValue") var myValue = 0
and accessing the value is exactly the same, however accessing the binding is slightly different as the projected value projects Self so it done through $myValue.binding instead of just $myValue.
And of course now my non-view can access a publisher like this:
cancellable = settings.$myValue.publisher.sink {
print("object changed")
self.title = "Hello \($0)"
}

Passing text between view and class SwiftUI

When a user pastes a hexcode into my textfield, I want my API function to take this hexcode and use it as a parameter in the API call. This means I have to share data from my View (containing the textfield) to my Class (containing API call). What is the best way to go about this? Appreciate the time and advice 🙏
View:
import SwiftUI
struct TestingText: View {
#StateObject var fetch = fetchResults()
#Binding var text: String
var body: some View {
VStack {
TextField("Paste Clout Hexcode Here", text: $text)
.font(.title2)
.padding()
Text(fetch.clout.postFound?.body ?? "n/a")
}
}
}
struct TestingText_Previews: PreviewProvider {
static var previews: some View {
TestingText(text: .constant("8004bb672ad3f46118775cd4b2cb5306c63f6d68787457990bf0d2fda3f7993a"))
}
}
Class with API call:
class fetchResults: ObservableObject {
#Published var clout = Cloutington()
#Published var dataHasLoaded = false
#State var postHashHex: String = "8004bb672ad3f46118775cd4b2cb5306c63f6d68787457990bf0d2fda3f7993a"
init() {
getData { clout in
self.clout = clout
}
}
private func getData(completion: #escaping (Cloutington) -> ()) {
let parameters = "{\r\n \"PostHashHex\": \"\(postHashHex)\"\r\n}"
let postData = parameters.data(using: .utf8)
var request = URLRequest(url: URL(string: "https://bitclout.com/api/v0/get-single-post")!,timeoutInterval: Double.infinity)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = postData
request.httpMethod = "POST"
let task = URLSession.shared.dataTask(with: request) { (responseData, response, error) in
print(error)
print(response)
print(responseData)
if let resData = responseData {
let decoder = JSONDecoder()
do
{
let finalData = try decoder.decode(Cloutington.self, from: resData)
DispatchQueue.main.async {
completion(finalData)
self.dataHasLoaded = true
}
}
catch (let error)
{
print(error)
}
}
}
task.resume()
}
}
Model with JSON data:
import Foundation
struct Cloutington: Decodable {
var postFound: PostFound?
enum CodingKeys: String, CodingKey {
case postFound = "PostFound"
}
}
struct PostFound: Decodable {
var id: String?
var postHashHex, posterPublicKeyBase58Check, parentStakeID, body: String?
var imageURLs: [String]?
// var recloutedPostEntryResponse: JSONNull?
var creatorBasisPoints, stakeMultipleBasisPoints: Int?
var timestampNanos: Double?
var isHidden: Bool?
var confirmationBlockHeight: Int?
var inMempool: Bool?
var profileEntryResponse: ProfileEntryResponse?
var likeCount, diamondCount: Int?
var isPinned: Bool?
var commentCount, recloutCount: Int?
var diamondsFromSender: Int?
enum CodingKeys: String, CodingKey {
case postHashHex = "PostHashHex"
case posterPublicKeyBase58Check = "PosterPublicKeyBase58Check"
case parentStakeID = "ParentStakeID"
case body = "Body"
case imageURLs = "ImageURLs"
case creatorBasisPoints = "CreatorBasisPoints"
case stakeMultipleBasisPoints = "StakeMultipleBasisPoints"
case timestampNanos = "TimestampNanos"
case isHidden = "IsHidden"
case confirmationBlockHeight = "ConfirmationBlockHeight"
case inMempool = "InMempool"
case profileEntryResponse = "ProfileEntryResponse"
case likeCount = "LikeCount"
case diamondCount = "DiamondCount"
case isPinned = "IsPinned"
case commentCount = "CommentCount"
case recloutCount = "RecloutCount"
case diamondsFromSender = "DiamondsFromSender"
}
}
// MARK: - ProfileEntryResponse
struct ProfileEntryResponse: Decodable {
var publicKeyBase58Check, username, profileEntryResponseDescription, profilePic: String?
var isHidden, isReserved, isVerified: Bool?
var coinPriceBitCloutNanos, stakeMultipleBasisPoints: Int?
enum CodingKeys: String, CodingKey {
case publicKeyBase58Check = "PublicKeyBase58Check"
case username = "Username"
case profileEntryResponseDescription = "Description"
case profilePic = "ProfilePic"
case isHidden = "IsHidden"
case isReserved = "IsReserved"
case isVerified = "IsVerified"
case coinPriceBitCloutNanos = "CoinPriceBitCloutNanos"
case stakeMultipleBasisPoints = "StakeMultipleBasisPoints"
}
}
First of all, do not name classes in lowercase like fetchResults, also don't call a class like an action: every programmer expects fetchResults() to be a function call. Use something like ResultFetcher to name this class. And class instance usually will be lowercased class name, or part of it:
#StateObject var resultFetcher = ResultFetcher()
// or
#StateObject var fetcher = ResultFetcher()
You can only use #State inside SwiftUI views, you cannot use it inside ObservableObject. Use #Published as you already do with other variables if you need to update the view after updating that variable, or use an unannotated variable.
In your case, it doesn't look like you need to store this hash string; you can just pass it from the view.
You can use .onChange(of: text) to track changes to the #State or #Binding variables:
TextField("Paste Clout Hexcode Here", text: $text)
.font(.title2)
.padding()
.onChange(of: text) { text in
resultFetcher.updateData(postHashHex: text)
}
Not sure if you need that initial call of updateData with hardcoded hex code, anyway your view model can be updated to this:
class ResultFetcher: ObservableObject {
#Published var clout = Cloutington()
#Published var dataHasLoaded = false
private let initialPostHashHex: String = "8004bb672ad3f46118775cd4b2cb5306c63f6d68787457990bf0d2fda3f7993a"
init() {
updateData(postHashHex: initialPostHashHex)
}
func updateData(postHashHex: String) {
getData(postHashHex: postHashHex) { clout in
self.clout = clout
}
}
private func getData(postHashHex: String, completion: #escaping (Cloutington) -> ()) {
// your code
}
}

Custom Property Wrapper that Updates View Swift

Xcode 11.3, Swift 5.1.3
I am trying currently to create a custom property wrapper that allows me to link variables to a Firebase database. When doing this, to make it update the view, I at first tried to use the #ObservedObject #Bar var foo = []. But I get an error that multiple property wrappers are not supported. Next thing I tried to do, which would honestly be ideal, was try to make my custom property wrapper update the view itself upon being changed, just like #State and #ObservedObject. This both avoids needing to go down two layers to access the underlying values and avoid the use of nesting property wrappers. To do this, I checked the SwiftUI documentation and found out that they both implement the DynamicProperty protocol. I tried to use this too but failed because I need to be able to update the view (call update()) from within my Firebase database observers, which I cannot do since .update() is mutating.
Here is my current attempt at this:
import SwiftUI
import Firebase
import CodableFirebase
import Combine
#propertyWrapper
final class DatabaseBackedArray<Element>: ObservableObject where Element: Codable & Identifiable {
typealias ObserverHandle = UInt
typealias Action = RealtimeDatabase.Action
typealias Event = RealtimeDatabase.Event
private(set) var reference: DatabaseReference
private var currentValue: [Element]
private var childAddedObserverHandle: ObserverHandle?
private var childChangedObserverHandle: ObserverHandle?
private var childRemovedObserverHandle: ObserverHandle?
private var childAddedActions: [Action<[Element]>] = []
private var childChangedActions: [Action<[Element]>] = []
private var childRemovedActions: [Action<[Element]>] = []
init(wrappedValue: [Element], _ path: KeyPath<RealtimeDatabase, RealtimeDatabase>, events: Event = .all,
actions: [Action<[Element]>] = []) {
currentValue = wrappedValue
reference = RealtimeDatabase()[keyPath: path].reference
for action in actions {
if action.event.contains(.childAdded) {
childAddedActions.append(action)
}
if action.event.contains(.childChanged) {
childChangedActions.append(action)
}
if action.event.contains(.childRemoved) {
childRemovedActions.append(action)
}
}
if events.contains(.childAdded) {
childAddedObserverHandle = reference.observe(.childAdded) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.append(decodedValue)
self.childAddedActions.forEach { $0.action(&self.currentValue) }
}
}
if events.contains(.childChanged) {
childChangedObserverHandle = reference.observe(.childChanged) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
guard let changeIndex = self.currentValue.firstIndex(where: { $0.id == decodedValue.id }) else {
return
}
self.objectWillChange.send()
self.currentValue[changeIndex] = decodedValue
self.childChangedActions.forEach { $0.action(&self.currentValue) }
}
}
if events.contains(.childRemoved) {
childRemovedObserverHandle = reference.observe(.childRemoved) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.removeAll { $0.id == decodedValue.id }
self.childRemovedActions.forEach { $0.action(&self.currentValue) }
}
}
}
private func setValue(to value: [Element]) {
guard let encodedValue = try? FirebaseEncoder().encode(currentValue) else {
fatalError("Could not encode value to Firebase.")
}
reference.setValue(encodedValue)
}
var wrappedValue: [Element] {
get {
return currentValue
}
set {
self.objectWillChange.send()
setValue(to: newValue)
}
}
var projectedValue: Binding<[Element]> {
return Binding(get: {
return self.wrappedValue
}) { newValue in
self.wrappedValue = newValue
}
}
var hasActiveObserver: Bool {
return childAddedObserverHandle != nil || childChangedObserverHandle != nil || childRemovedObserverHandle != nil
}
var hasChildAddedObserver: Bool {
return childAddedObserverHandle != nil
}
var hasChildChangedObserver: Bool {
return childChangedObserverHandle != nil
}
var hasChildRemovedObserver: Bool {
return childRemovedObserverHandle != nil
}
func connectObservers(for event: Event) {
if event.contains(.childAdded) && childAddedObserverHandle == nil {
childAddedObserverHandle = reference.observe(.childAdded) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.append(decodedValue)
self.childAddedActions.forEach { $0.action(&self.currentValue) }
}
}
if event.contains(.childChanged) && childChangedObserverHandle == nil {
childChangedObserverHandle = reference.observe(.childChanged) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
guard let changeIndex = self.currentValue.firstIndex(where: { $0.id == decodedValue.id }) else {
return
}
self.objectWillChange.send()
self.currentValue[changeIndex] = decodedValue
self.childChangedActions.forEach { $0.action(&self.currentValue) }
}
}
if event.contains(.childRemoved) && childRemovedObserverHandle == nil {
childRemovedObserverHandle = reference.observe(.childRemoved) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.removeAll { $0.id == decodedValue.id }
self.childRemovedActions.forEach { $0.action(&self.currentValue) }
}
}
}
func removeObserver(for event: Event) {
if event.contains(.childAdded), let handle = childAddedObserverHandle {
reference.removeObserver(withHandle: handle)
self.childAddedObserverHandle = nil
}
if event.contains(.childChanged), let handle = childChangedObserverHandle {
reference.removeObserver(withHandle: handle)
self.childChangedObserverHandle = nil
}
if event.contains(.childRemoved), let handle = childRemovedObserverHandle {
reference.removeObserver(withHandle: handle)
self.childRemovedObserverHandle = nil
}
}
func removeAction(_ action: Action<[Element]>) {
if action.event.contains(.childAdded) {
childAddedActions.removeAll { $0.id == action.id }
}
if action.event.contains(.childChanged) {
childChangedActions.removeAll { $0.id == action.id }
}
if action.event.contains(.childRemoved) {
childRemovedActions.removeAll { $0.id == action.id }
}
}
func removeAllActions(for event: Event) {
if event.contains(.childAdded) {
childAddedActions = []
}
if event.contains(.childChanged) {
childChangedActions = []
}
if event.contains(.childRemoved) {
childRemovedActions = []
}
}
}
struct School: Codable, Identifiable {
/// The unique id of the school.
var id: String
/// The name of the school.
var name: String
/// The city of the school.
var city: String
/// The province of the school.
var province: String
/// Email domains for student emails from the school.
var domains: [String]
}
#dynamicMemberLookup
struct RealtimeDatabase {
private var path: [String]
var reference: DatabaseReference {
var ref = Database.database().reference()
for component in path {
ref = ref.child(component)
}
return ref
}
init(previous: Self? = nil, child: String? = nil) {
if let previous = previous {
path = previous.path
} else {
path = []
}
if let child = child {
path.append(child)
}
}
static subscript(dynamicMember member: String) -> Self {
return Self(child: member)
}
subscript(dynamicMember member: String) -> Self {
return Self(child: member)
}
static subscript(dynamicMember keyPath: KeyPath<Self, Self>) -> Self {
return Self()[keyPath: keyPath]
}
static let reference = Database.database().reference()
struct Event: OptionSet, Hashable {
let rawValue: UInt
static let childAdded = Event(rawValue: 1 << 0)
static let childChanged = Event(rawValue: 1 << 1)
static let childRemoved = Event(rawValue: 1 << 2)
static let all: Event = [.childAdded, .childChanged, .childRemoved]
static let constructive: Event = [.childAdded, .childChanged]
static let destructive: Event = .childRemoved
}
struct Action<Value>: Identifiable {
let id = UUID()
let event: Event
let action: (inout Value) -> Void
private init(on event: Event, perform action: #escaping (inout Value) -> Void) {
self.event = event
self.action = action
}
static func on<Value>(_ event: RealtimeDatabase.Event, perform action: #escaping (inout Value) -> Void) -> Action<Value> {
return Action<Value>(on: event, perform: action)
}
}
}
Usage example:
struct ContentView: View {
#DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { $0.sort { $0.name < $1.name } }])
var schools: [School] = []
var body: some View {
Text("School: ").bold() +
Text(schools.isEmpty ? "Loading..." : schools.first!.name)
}
}
When I try to use this though, the view never updates with the value from Firebase even though I am positive that the .childAdded observer is being called.
One of my attempts at fixing this was to store all of these variables in a singleton that itself conforms to ObservableObject. This solution is also ideal as it allows the variables being observed to be shared throughout my application, preventing multiples instances of the same date and allowing for a single source of truth. Unfortunately, this too did not update the view with the fetched value of currentValue.
class Session: ObservableObject {
#DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { $0.sort { $0.name < $1.name } }])
var schools: [School] = []
private init() {
//Send `objectWillChange` when `schools` property changes
_schools.objectWillChange.sink {
self.objectWillChange.send()
}
}
static let current = Session()
}
struct ContentView: View {
#ObservedObject
var session = Session.current
var body: some View {
Text("School: ").bold() +
Text(session.schools.isEmpty ? "Loading..." : session.schools.first!.name)
}
}
Is there any way to make a custom property wrapper that also updates a view in SwiftUI?
Making use of the DynamicProperty protocol we can easily trigger view updates by making use of SwiftUI's existing property wrappers. (DynamicProperty tells SwiftUI to look for these within our type)
#propertyWrapper
struct OurPropertyWrapper: DynamicProperty {
// A state object that we notify of updates
#StateObject private var updater = Updater()
var wrappedValue: T {
get {
// Your getter code here
}
nonmutating set {
// Tell SwiftUI we're going to change something
updater.notifyUpdate()
// Your setter code here
}
}
class Updater: ObservableObject {
func notifyUpdate() {
objectWillChange.send()
}
}
}
The solution to this is to make a minor tweak to the solution of the singleton. Credits to #user1046037 for pointing this out to me. The problem with the singleton fix mentioned in the original post, is that it does not retain the canceller for the sink in the initializer. Here is the correct code:
class Session: ObservableObject {
#DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { $0.sort { $0.name < $1.name } }])
var schools: [School] = []
private var cancellers = [AnyCancellable]()
private init() {
_schools.objectWillChange.sink {
self.objectWillChange.send()
}.assign(to: &cancellers)
}
static let current = Session()
}

Error Using property Wrapper in class swift 5.1

I am using UserDefaults and Combine in SwiftUI.
My UserDefault.swift file:
import SwiftUI
struct UserDefault<T> {
let key: String
let defaultValue:T
var wrappedValue:T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
} set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
When using this struct in the following class as follows:
DataStore.swift file:
import SwiftUI
import Combine
final class DataStore : ObservableObject { //(1)
let didChange = PassthroughSubject<DataStore, Never>()
#UserDefault(key: "firstLaunch", defaultValue: true) //(2)
var firstLaunch:Bool{
didSet{
didChange.send(self)
}
}
}
In the above code, I am getting 2 errors:
(1):Class 'DataStore' has no initializers
(2):Generic struct 'UserDefault' cannot be used as an attribute
I think there is a change or depreciation in swift 5.1, but I am unable to find it.
Use something like this:
#propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
init(key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md
You need to add the #propertyWrapper annotation to your UserDefault struct.
#propertyWrapper
struct UserDefault<T> {
...

Unable to infer closure type in the current context in APIClient

I'm trying to migrate code from Swift 3.2 to Swift 4.2. When put in Xcode 10.1 I get this error.'Unable to infer closure type in the current context '. This is using YALAPIClient. Please help.
Unable to infer closure type in the current context
This I found on stack overflow. But I'm not using any try method here.Please help.
private func presentIndustrySearch() {
let dataProvider = RequestDataProvider { return IndustriesRequest() } /*error comes here*/
}
public class RequestDataProvider<Representation, Request>: DataProvider, NetworkClientInjectable
where
Request: SerializeableAPIRequest,
Request.Parser.Representation == [Representation]
{
public typealias Item = Representation
public typealias RequestConstructor = () -> Request
public private(set) var data = [Item]()
private let requestConstructor: RequestConstructor
public init(_ requestConstructor: #escaping RequestConstructor) {
self.requestConstructor = requestConstructor
}
public func loadData(before: () -> Void, after: #escaping (Error?) -> Void) {
let request: Request = self.requestConstructor()
before()
networkClient.execute(request: request, parser: request.parser, completion: { [weak self] task in
guard let `self` = self else { return }
if let data = task.value, data.count != 0 {
self.data.append(contentsOf: data)
after(nil)
} else if let error = task.error {
after(error as NSError)
} else {
let error = NSError.reachedEndOfPage()
after(error)
}
})
}
}
public protocol SerializeableAPIRequest: APIRequest {
associatedtype Parser: ResponseParser
var parser: Parser { get }
}
public struct IndustriesRequest: SerializeableAPIRequest, TokenizedAPIRequest, StubAPIRequest {
public private(set) var method = APIRequestMethod.get
public private(set) var path = "industries"
public private(set) var parser = KeyPathMappableArrayParser<[Industry]>(keyPath: "data")
public private(set) var parameters: [String: String]? = [:]
public private(set) var authenticationTokenRequired = true
public init(value: String = "") {
parameters!["term"] = value
}
}