Reputation: 500
I don't quite understand the pattern of waiting to retrieve data from any async call (any: network, timer, any call that executes asynchronously and I have to wait to perform another one) and use it in synchronously in a different place (how to chain to operations. I saw examples using flatmap by they referred to 2 web calls. In this case I have to retrieve data from the web (a session Id), and save it to further use (it lasts an hour). I read about Operations, DispatchGroups, and don't quite get them to work in here. I have a simple class that gets data from a web service, I have to wait till its downloaded, and save it.
import Foundation
import Combine
import CoreData
struct SessionId:Codable {
let key:String?
let dateTime:Date?
}
class ColppyModel {
var session:SessionId?
var key:String?
var cancellable:AnyCancellable?
init() {
print("saving")
let sess = SessionId(key: "1", dateTime: DateComponents(calendar:Calendar(identifier: .gregorian), year:2020, month:1, day:1).date)
guard let data = try? JSONEncoder().encode(sess) else {return}
let defaults = UserDefaults.standard
defaults.set(data, forKey: "sessionIdKey")
print("Saved \(sess)")
}
func getSessionKey(){
let requestData = ColppyAPIRequests.createSessionIdRequestData()
cancellable = ColppyAPI.sessionKeyRequest(sessionKeyJsonData: requestData)
.replaceError(with: nil)
.map{$0?.response?.data?.claveSesion}
.receive(on: DispatchQueue.global(qos: .userInteractive))
.sink(receiveValue: { (clave) in
let data = try! JSONEncoder().encode(SessionId(key: clave!, dateTime: Date()))
UserDefaults.standard.set(data, forKey: "sessionIdKey")
})
}
func getSessionIDFromUserDefaults() -> SessionId? {
let defaults = UserDefaults.standard
let data = defaults.data(forKey: "sessionIdKey")
guard let safeData = data else { return nil }
guard let sessionId = try? JSONDecoder().decode(SessionId.self, from: safeData) else {return nil}
return sessionId
}
}
And I use it in and SwiftUI View in this way
import SwiftUI
struct ContentView: View {
let ss = ColppyModel()
var body: some View {
Text("Press")
.onTapGesture {
self.getInvoices()
}
}
private func getInvoices() {
let id = ss.getSessionIDFromUserDefaults()
print(id)
ss.getSessionKey()
print(ss.getSessionIDFromUserDefaults())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
return ContentView()
}
}
The first time I click I get
The second time I click I get
The correct item saved.
How can I do to wait till the data (string in this case) is retrieved from the server and saved to fetch it from the store correctly?
Really I don't quite get the pattern in combine.
Thanks a lot
Upvotes: 1
Views: 1475
Reputation: 49580
There's no real benefit (in my view) of using Combine here if all you need is to do something when this async request completes - you can just use a regular old callback:
func getSessionKey(completion: @escaping () -> Void) {
// ...
.sink(receiveValue: { (clave) in
let data = try! JSONEncoder().encode(SessionId(key: clave!, dateTime: Date()))
UserDefaults.standard.set(data, forKey: "sessionIdKey")
completion()
})
}
(I just copied your code, but I would discourage the use of try!
)
Then, you could do (using a trailing closure syntax)
ss.getSessionKey() {
print(ss.getSessionIDFromUserDefaults())
}
If you insist on using Combine, getSessionKey
needs to return a publisher instead of sink
ing the value. Let's say the publisher emits a Void
value to signal completion:
func getSessionKey() -> AnyPublisher<Void, Never> {
// ...
return ColppyAPI
.sessionKeyRequest(sessionKeyJsonData: requestData)
.map { $0.response?.data?.claveSession }
.replaceError(with: nil)
// ignore nil values
.compactMap { $0 }
// handle side-effects
.handleEvents(receiveOutput: {
let data = try! JSONEncoder().encode(SessionId(key: $0, dateTime: Date()))
UserDefaults.standard.set(data, forKey: "sessionIdKey")
})
}
This now returns a publisher to which you could subscribe elsewhere (and store the cancellable there):
ss.getSessionKey()
.receive(on: ...)
.sink {
print(ss.getSessionIDFromUserDefaults())
}
.store(in: &cancellables)
Of course, now you need to figure out where to store the cancellable, which isn't immediately obvious how to do this in a immutable view (you'd need to make it a @State
property).
All in all, it isn't a good example to use to learn Combine patterns - just use caallbacks.
Upvotes: 2