1

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

enter image description here

The second time I click I get

enter image description here

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

2
  • Does it need to be with Combine? It would be easier here to just use a callback. But if it is with Combine, the return value from getSessionKey has to be a publisher, whereas in your code, you sink the publisher to produce some internal value. Commented Sep 24, 2020 at 13:28
  • Ok, I understand what you say I can do it making the "old" way using the urlsessiondatatask, but I want to learn the combine patterns. And I don't quite understand them. Commented Sep 24, 2020 at 13:45

1 Answer 1

2

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 sinking 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.

Sign up to request clarification or add additional context in comments.

3 Comments

Ok, I'll use callbacks. Thanks a lot
if I want to do it in core data (a core data extension class to create objects), do you suggest to use callbacks?
I haven't used Core Data, so I don't know

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.