简体   繁体   中英

Swift: Chaining to async functions and waiting to perform another one

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

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(@escaping completion: () -> 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM