I develop a project which utilizes a dynamic data structure (tree-like). Both number of nodes in the structure change over time as well as data that is kept in the nodes. Some properties affect other - both within a certain node but also inside parent/children.
I've managed to get to the point where changes propagate correctly but a problem arose. The piece of code which sets up subscriptions and contains logic of propagation is a complete mess - due to the fact that I nest subscriptions set up. I'm new to the Combine framework so probably I don't know how to use it correctly. I'm hoping to get a suggestion.
Story that hopefully illustrate the problem
Imagine that you have a tree and if you subscribe to a node's data
stream you are going to receive data from the node itself as well as
from its ancestors. The problem is that in order to get a data from
one subject you must go through a different one.
Code ready for copying and pasting to the playground:
//
// Copyright © 2021 Mateusz Stompór. All rights reserved.
//
import Combine
// Helper
struct WeakRef<T: AnyObject> {
weak var reference: T?
init(_ reference: T?) {
self.reference = reference
}
}
// Parent is observable in order to react for change
class Node<T> {
let data: T
var parent: CurrentValueSubject<WeakRef<Node>, Error>
let stream: CurrentValueSubject<T, Error>
private var parentSubscription: AnyCancellable?
private var parentStreamSubscription: AnyCancellable?
init(data: T, parent: Node?) {
self.data = data
self.parent = CurrentValueSubject(WeakRef(parent))
self.stream = CurrentValueSubject(data)
setup()
}
func setup() {
parentSubscription = parent.compactMap({ $0.reference?.stream }).sink(receiveCompletion: { [weak self] _ in
self?.parentStreamSubscription?.cancel()
}, receiveValue: { [weak self] stream in
self?.parentStreamSubscription = stream.sink { _ in
// Nothing needed
} receiveValue: { value in
guard let self = self else {
return
}
self.stream.send(value)
}
})
}
}
let parent = Node(data: 2, parent: nil)
let child = Node(data: 1, parent: nil)
let subscription = child.stream.sink { _ in
// nothing needed
} receiveValue: { value in
print(value)
}
// '1' is printed right away
// Setup connection
child.parent.send(WeakRef(parent))
// '2' is printed once connection is set
parent.stream.send(3)
// '3' is printed
// Changing child's parent
let newParent = Node(data: 4, parent: nil)
child.parent.send(WeakRef(newParent))
// '4' is printed as parent change
parent.stream.send(5)
// '5' is NOT printed, node is no longer part of the tree
newParent.stream.send(6)
// '6' is printed
The core question: is there a way to avoid this kind of nesting?
Related
I have created a piece of code to send a local notification at 3 pm and the user can toggle it with a toggle switch. I used an AppStorage wrapper to remember the state it was in. The code works until I toggle the switch on and off (simulating whether the user switches it multiple times) and the number of times I switched the toggle, that same number comes as a notification (i.e. I toggle 5 times, I get 5 notifications all at once). this is the code
struct trial: View {
#AppStorage("3") var three = false
func firstNotify() {
let content = UNMutableNotificationContent()
content.title = "It is 3pm"
content.subtitle = "Read the Prime Hour"
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "notifysound.wav"))
// content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "notifysound"))
var dateComponents = DateComponents()
dateComponents.hour = 15
dateComponents.minute = 00
dateComponents.second = 00
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
// choose a random identifier
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
// add our notification request
UNUserNotificationCenter.current().add(request)
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
if success {
print("All set!")
} else if let error = error {
print(error.localizedDescription)
}
}
}
#State private var name: String = "Tim"
var body: some View {
VStack {
Toggle(isOn: $three) {
Text("3 pm")
}
.padding(.horizontal)
.onChange(of: three) { newValue in
if newValue {
firstNotify()
}
}
}
}
}
struct trial_Previews: PreviewProvider {
static var previews: some View {
trial()
}
}
I think what's happening is that the func is not reading whether the toggle is on or off but just adds a notification each time the switch gets turned on. Would anyone know the solution to this???
The toggle is really only scheduling a new notification on every change of the value to true.
This piece of code means that a new notification will be scheduled each time this method is called.
// choose a random identifier
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
// add our notification request
UNUserNotificationCenter.current().add(request)
Not entirely sure what the desired logic is, but you could solve this by either updating the existing notification or clearing out the old one and adding a new one. Rather than creating a new UUID for each notification, it may be best to have an ID convention or otherwise store it so it can be accessed or removed.
You could store the rather than the toggle value:
#AppStorage("notification_id") var notificationID = nil // I don't know if this can be a string or nil, but hopefully it leads you in the right direction.
// Then when creating the request.
let id = notificationID ?? UUID().uuidString
let request = UNNotificationRequest(identifier: id, content: content, trigger: trigger)
notificationID = id
And there needs to be another method when to unschedule when they toggle the other direction. You could simply check to see if the id exists.
if let id = notificationID {
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers identifiers: [id])
// OR
UNUserNotificationCenter.current()removeAllPendingNotificationRequests()
}
im breaking my mind around how to do this in RX.
The actual usecase is mapping of LowerLevelEvent(val userId: String) to HigherLevelEvent(val user: User), where the User is provided by observable, so it can emit n times, so example output
LowerLevelEvent1(abc) -> HigherLevelEvent1(userAbc(nameVariation1)
LowerLevelEvent2(abc) -> HigherLevelEvent2(userAbc(nameVariation1)
LowerLevelEvent3(abc) -> HigherLevelEvent3(userAbc(nameVariation1)
LowerLevelEvent4(abc) -> HigherLevelEvent4(userAbc(nameVariation1)
HigherLevelEvent4(userAbc(nameVariation2)
HigherLevelEvent4(userAbc(nameVariation3)
So my naive solution was to use combineLatest. So while userId is not changed user observable is subscribed, i.e. not resubscribed when new lowerLevelEmits & its userId is not changed
val _lowerLevelEventObservable: Observable<LowerLevelEvent> = lowerLevelEventObservable
.replayingShare()
val _higherLevelEventObservable: Observable<HigherLevelEvent> = Observables
.combineLatest(
_lowerLevelEventObservable,
_lowerLevelEventObservable
.map { it.userId }
.distinctUntilChanged()
.switchMap { userRepository.findByIdObservable(it)
) { lowerLevelEvent, user -> createHigherLevelInstance... }
However this has glitch issues, since both sources in combineLatest originate from same observable.
Then I thought about
lowerLevelObservable.
.switchMap { lowerLevelEvent ->
userRepository.findByIdObservable(lowerLevelEvent.userId)
.map { user -> createHigherLevelInstance... }
}
This however can break if lowerLevelObservable emits fast, and since user observable can take some time, given lowerLevelX event can be skipped, which I cannot have. Also it resubscribes user observable each emit, which is wasteful since it wont change most likely
So, maybe concatMap? That has issue of that the user observable doesnt complete, so concatMap wouldnt work.
Anyone have a clue?
Thanks a lot
// Clarification:
basically its mapping of A variants (A1, A2..) to A' variants (A1', A2'..) while attaching a queried object to it, where the query is observable so it might reemit after the mapping was made, so AX' needs to be reemited with new query result. But the query is cold and doesnt complete
So example A1(1) -> A1'(user1), A2(1) -> A2'(user1), A3(1) -> A3'(user1) -- now somebody changes user1 somewhere else in the app, so next emit is A3'(user1')
Based on the comments you have made, the below would work in RxSwift. I have no idea how to translate it to RxJava. Honestly though, I think there is a fundamental misuse of Rx here. Good luck.
How it works: If it's allowed to subscribe it will, otherwise it will add the event to a buffer for later use. It is allowed to subscribe if it currently isn't subscribed to an inner event, or if the inner Observable it's currently subscribed to has emitted an element.
WARNING: It doesn't handle completions properly as it stands. I'll leave that to you as an exercise.
func example(lowerLevelEventObservable: Observable<LowerLevelEvent>, userRepository: UserRepository) {
let higherLevelEventObservable = lowerLevelEventObservable
.flatMapAtLeastOnce { event in // RxSwift's switchLatest I think.
Observable.combineLatest(
Observable.just(event),
userRepository.findByIdObservable(event.userId),
resultSelector: { (lowLevelEvent: $0, user: $1) }
)
}
.map { createHigherLevelInstance($0.lowLevelEvent, $0.user) }
// use higherLevelEventObservable
}
extension ObservableType {
func flatMapAtLeastOnce<U>(from fn: #escaping (E) -> Observable<U>) -> Observable<U> {
return Observable.create { observer in
let disposables = CompositeDisposable()
var nexts: [E] = []
var disposeKey: CompositeDisposable.DisposeKey?
var isAllowedToSubscribe = true
let lock = NSRecursiveLock()
func nextSubscription() {
isAllowedToSubscribe = true
if !nexts.isEmpty {
let e = nexts[0]
nexts.remove(at: 0)
subscribeToInner(e)
}
}
func subscribeToInner(_ element: E) {
isAllowedToSubscribe = false
if let key = disposeKey {
disposables.remove(for: key)
}
let disposable = fn(element).subscribe { innerEvent in
lock.lock(); defer { lock.unlock() }
switch innerEvent {
case .next:
observer.on(innerEvent)
nextSubscription()
case .error:
observer.on(innerEvent)
case .completed:
nextSubscription()
}
}
disposeKey = disposables.insert(disposable)
}
let disposable = self.subscribe { event in
lock.lock(); defer { lock.unlock() }
switch event {
case let .next(element):
if isAllowedToSubscribe == true {
subscribeToInner(element)
}
else {
nexts.append(element)
}
case let .error(error):
observer.onError(error)
case .completed:
observer.onCompleted()
}
}
_ = disposables.insert(disposable)
return disposables
}
}
}
I'm using promisekit 3.0 to help chain alamofire callbacks in a clean way. The objective is to start with a network call, with a promise to return an array of urls.
Then, I'm looking to execute network calls on as many of those urls as needed to find the next link i'm looking for. As soon as this link is found, I can pass it to the next step.
This part is where I'm stuck.
I can pick an arbitrary index in the array that I know has what I want, but I can't figure out the looping to keep it going until the right information is returned.
I tried learning from this obj-c example, but i couldn't get it working in swift.
https://stackoverflow.com/a/30693077/1079379
He's a more tangible example of what i've done.
Network.sharedInstance.makeFirstPromise(.GET, url: NSURL(string: fullSourceLink)! )
.then { (idArray) -> Promise<AnyObject> in
let ids = idArray as! [String]
//how do i do that in swift? (from the example SO answer)
//PMKPromise *p = [PMKPromise promiseWithValue: nil]; // create empty promise
//only thing i could do was feed it the first value
var p:Promise<AnyObject> = Network.sharedInstance.makePromiseRequestHostLink(.POST, id: ids[0])
//var to hold my eventual promise value, doesn't really work unless i set it to something first
var goodValue:Promise<AnyObject>
for item in ids {
//use continue to offset the promise from before the loop started
continue
//hard part
p = p.then{ returnValue -> Promise<AnyObject> in
//need a way to check if what i get is what i wanted then we can break the loop and move on
if returnValue = "whatIwant" {
goodvalue = returnValue
break
//or else we try again with the next on the list
}else {
return Network.sharedInstance.makeLoopingPromise(.POST, id: item)
}
}
}
return goodValue
}.then { (finalLink) -> Void in
//do stuck with finalLink
}
Can someone show me how to structure this properly, please?
Is nesting promises like that anti-pattern to avoid? In that case, what is the best approach.
I have finally figured this out with a combination of your post and the link you posted. It works, but I'll be glad if anyone has input on a proper solution.
func download(arrayOfObjects: [Object]) -> Promise<AnyObject> {
// This stopped the compiler from complaining
var promise : Promise<AnyObject> = Promise<AnyObject>("emptyPromise")
for object in arrayOfObjects {
promise = promise.then { _ in
return Promise { fulfill, reject in
Service.getData(stuff: object.stuff completion: { success, data in
if success {
print("Got the data")
}
fulfill(successful)
})
}
}
}
return promise
}
The only thing I'm not doing is showing in this example is retaining the received data, but I'm assuming you can do that with the results array you have now.
The key to figuring out my particular issue was using the "when" function. It keeps going until all the calls you inputted are finished. The map makes it easier to look at (and think about in my head)
}.then { (idArray) -> Void in
when(idArray.map({Network.sharedInstance.makePromiseRequest(.POST, params: ["thing":$0])})).then{ link -> Promise<String> in
return Promise { fulfill, reject in
let stringLink:[String] = link as! [String]
for entry in stringLink {
if entry != "" {
fulfill(entry)
break
}
}
}
}.then {
}
}
The problem I'm having is that my property's willSet and didSet are being called even though I'm only reading the property, and this breaks my app.
I condensed my problem into a playground. Uncomment #1 to see the problem, or #2 to see the expected behavior.
What's going on here?
protocol Departure
{
var line: String? { get }
}
class MyDeparture : Departure
{
var line: String? = "SomeString"
}
#if true
// #1: this causes a write to tableContet later (!)
typealias TableSection = (title: String, rows: [Departure])
#else
// #2: this doesn't cause a write to tableContent later
typealias TableSection = (title: String, rows: [MyDeparture])
#endif
var tableContent: [TableSection] = [ TableSection(title: "MySectionTitle", rows: [ MyDeparture() ]) ]
{
willSet { print("willSet tableContent") }
didSet { print("didSet tableContent") }
}
func getDepartureDescription() -> String? {
print("start getDepartureDescription")
defer { print("end getDepartureDescription") }
#if true
// writes to tableContent in case #1
let lineNumber = tableContent[0].rows[0].line
#else
// never writes to table content
let row = tableContent[0].rows[0]
let lineNumber = row.line
#endif
return "Your line is \(lineNumber)"
}
getDepartureDescription()
This prints
start getDepartureDescription
willSet tableContent
didSet tableContent
end getDepartureDescription
I'm using Xcode 7 (7A218) GM seed. Everything worked as expected in Xcode 6.4 and Swift 1.2.
Side note:
At first I thought that the runtime was--on reading TableSection.rows--creating a new [Departure] array from the [MyDeparture] array that was assigned to it. But even correcting for that in the most explicit way I could think of didn't get rid of the problem:
// More correct types makes no difference:
var departures: [Departure] {
var result = Array<Departure>()
result.append(MyDeparture())
return result
}
var tableContent: [TableSection] = [ TableSection(title: "MyTitle", rows: departures ) ]
There's some sort of lazy initialisation going on. It's only when it gets to the line
let lineNumber = tableContent[0].rows[0].line
that the run time seems to be filling in the contents of the array.
If you declared the array as containing elements that conform to the Departure protocol, the runtime does not know how big the elements of tableContent actually are because both classes and structs can conform to Departure. Hence, I think it is recreating the array and triggering didSet and willSet erroneously.
I tried limiting your protocol to classes like so:
protocol Departure: class
{
var line: String? { get }
}
and the problem goes away because now the runtime knows the array can only contain class references.
I think this is a bug and you should raise it with Apple.
I'm trying to have variables in swift that are critical app-wide user settings so they must be persisted to disk after every change. There is a small amount of these variables and I'm content with the first read happening from disk after the app starts.
I have code that looks similar to this:
var _myEnumMember:MyEnum?
var myEnumMember:MyEnum {
get {
if let value = _myEnumMember { // in memory
return value
}
var c:Cache = Cache()
var storedValue:MyEnum? = c.get("SomeStorageKey");
if let value = storedValue { // exists on disk
self.myEnumMember = value // call setter to persist
return self.myEnumMember // call getter again with value set
}
self.myEnumMember = .DefaultValue // assign via setter
return self.rankingDuration // call getter after `set`
}
set (newValue){
self._myEnumMember = newValue // assign to memory
var c:Cache = Cache()
c.put("SomeStorageKey", data: ser) // store in disk
}
I have about 5-6 properties that need to do this - I don't want to repeat myself over and over - is there any way to DRY this code up so I won't have to repeat this logic in several places?
(Note: Asking here and not CR.SE because I'd like answers to explain how to DRY getters/setters in these situations rather than receive critique on a particular piece of code)
I was working on something similar recently - this may be your best bet. I used this as a nested struct, but it doesn't strictly need to be nested.
First, define a LocalCache type that will handle the persistence of your properties:
struct LocalCache {
// set up keys as constants
// these could live in your class instead
static let EnumKey = "EnumKey"
static let IntKey = "IntKey"
static let StringKey = "StringKey"
// use a single cache variable, hopefully?
var cache = Cache()
// in-memory values go in a Dictionary
var localValues: [String: Any] = [:]
// fetch from local storage or from disk
// note that the default value also sets the return type
mutating func fetch<T>(key: String, defaultValue: T) -> T {
if localValues[key] == nil {
localValues[key] = cache.get(key) ?? defaultValue
}
return localValues[key]! as T
}
// save in both local storage and to disk
mutating func store(key: String, _ value: Any) {
localValues[key] = value
cache.put(key, data: value)
}
}
Then add a LocalCache instance to your class, and you can have much simpler getter/setters.
class Test {
// instance of the local cache
var localCache = LocalCache()
var enumPropery: MyEnum {
get { return localCache.fetch(LocalCache.EnumKey, defaultValue: MyEnum.DefaultValue) }
set { localCache.store(LocalCache.EnumKey, newValue) }
}
var intProperty: Int {
get { return localCache.fetch(LocalCache.IntKey, defaultValue: 0) }
set { localCache.store(LocalCache.IntKey, newValue) }
}
var stringProperty: String {
get { return localCache.fetch(LocalCache.StringKey, defaultValue: "---") }
set { localCache.store(LocalCache.StringKey, newValue) }
}
}
If you're using swift in an iOS or OS X context then NSUserDefaults are ABSOLUTELY the right way to do this.
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSUserDefaults_Class/index.html
http://www.codingexplorer.com/nsuserdefaults-a-swift-introduction/