I am trying to create a class that executes data loading once and returns the data to all callers of the method while the data was loading to not perform the data loading for the same item (identifier) more than once. The issue I am having is that it seems to crash on the first initialization of CurrentValueSubject for an identifier. This only happens if the downloadStuff returns an Error I have no idea what's wrong. Here is a reproduction of the issue.
Class that does the synchronization:
class FetchSynchronizer<T, ItemIdentifier: Hashable> {
typealias CustomParams = (isFirstLoad: Bool, result: Result<T, Error>)
enum FetchCondition {
// executes data fetching only once
case executeFetchOnlyOnce
// re-executes fetch if request failed
case retryOnlyIfFailure
// always executes fetch even if response is cached
case noDataCache
// custom condition
case custom((CustomParams) -> Bool)
}
struct LoadingState<T> {
let result: Result<T, Error>
let isLoading: Bool
init(result: Result<T, Error>? = nil, isLoading: Bool = false) {
self.result = result ?? .failure(NoResultsError())
self.isLoading = isLoading
}
}
private var cancellables = Set<AnyCancellable>()
private var isLoading: [ItemIdentifier: CurrentValueSubject<LoadingState<T>, Never>] = [:]
func startLoading(identifier: ItemIdentifier,
fetchCondition: FetchCondition = .executeFetchOnlyOnce,
loaderMethod: #escaping () async -> Result<T, Error>) async -> Result<T, Error> {
// initialize loading tracker for identifier on first execution
var isFirstExecution = false
if isLoading[identifier] == nil {
print("----0")
isLoading[identifier] = CurrentValueSubject<LoadingState<T>, Never>(LoadingState<T>())
isFirstExecution = true
}
guard let currentIsLoading = isLoading[identifier] else {
assertionFailure("Should never be nil because it's set above")
return .failure(NoResultsError())
}
if currentIsLoading.value.isLoading {
// loading in progress, wait for finish and call pending callbacks
return await withCheckedContinuation { continuation in
currentIsLoading.filter { !$0.isLoading }.sink { currentIsLoading in
continuation.resume(returning: currentIsLoading.result)
}.store(in: &cancellables)
}
} else {
// no fetching in progress, check if it can be executed
let shouldFetchData: Bool
switch fetchCondition {
case .executeFetchOnlyOnce:
// first execution -> fetch data
shouldFetchData = isFirstExecution
case .retryOnlyIfFailure:
// no cached data -> fetch data
switch currentIsLoading.value.result {
case .success:
shouldFetchData = false
case .failure:
shouldFetchData = true
}
case .noDataCache:
// always fetch
shouldFetchData = true
case .custom(let completion):
shouldFetchData = completion((isFirstLoad: isFirstExecution,
result: currentIsLoading.value.result))
}
if shouldFetchData {
currentIsLoading.send(LoadingState(isLoading: true))
// fetch data
return await withCheckedContinuation { continuation in
Task {
// execute loader method
let result = await loaderMethod()
let state = LoadingState(result: result,
isLoading: false)
currentIsLoading.send(state)
continuation.resume(returning: result)
}
}
} else {
// use existing data
return currentIsLoading.value.result
}
}
}
}
Example usage:
class Executer {
let fetchSynchronizer = FetchSynchronizer<Data?, String>()
func downloadStuff() async -> Result<Data?, Error> {
await fetchSynchronizer.startLoading(identifier: "1") {
return await withCheckedContinuation { continuation in
sleep(UInt32.random(in: 1...3))
print("-------request")
continuation.resume(returning: .failure(NSError() as Error))
}
}
}
init() {
start()
}
func start() {
Task {
await downloadStuff()
print("-----3")
}
DispatchQueue.global(qos: .utility).async {
Task {
await self.downloadStuff()
print("-----2")
}
}
DispatchQueue.global(qos: .background).async {
Task {
await self.downloadStuff()
print("-----1")
}
}
}
}
Start the execution:
Executer()
Crashes at
isLoading[identifier] = CurrentValueSubject<LoadingState<T>, Never>(LoadingState<T>())
Any guidance would be appreciated.
Swift Dictionary is not thread-safe.
You need to make sure it is being accessed from only one thread (i.e queue) or using locks.
EDIT - another solution suggested by #Bogdan the question writer is to make the class an actor class which the concurrency safety is taken care of by the compiler!
By dispatching to a global queue, you increase the chance that two threads will try and write into the dictionary “at the same time” which probably causes the crash
Take a look at these examples.
How to implement a Thread Safe HashTable (PhoneBook) Data Structure in Swift?
https://github.com/iThink32/Thread-Safe-Dictionary/blob/main/ThreadSafeDictionary.swift
Related
I wanted to make multiple API call in the same screen, but when one api fails other api should not be called? The below code is working fine. but what I need is , how can I refactor this in a more simpler way?
ApplicationService.requestAppEndPointUrl { success, error in
if success {
ApplicationService.appLinkDownload { success, error in
if success{
ApplicationService.requestApplicationSession { success, error in
if success {
ApplicationService.validateSdk { success, error in
if success {
ApplicationService.requestApplicationDetails { success, error in
if success{
print("Success")
}
else{
self.showErrorAlert(error)
}
}
}else{
self.showErrorAlert(error)
}
}
}else{
self.showErrorAlert(error)
}
}
}else{
self.showErrorAlert(error)
}
}
}else{
self.showErrorAlert(error)
}
}
If ApplicationService is a class/struct that you can modify, you could convert the synchronous function calls with completion handler to asynchronous function calls, using Swift 5.5 concurrency. The code would look like:
do {
try await ApplicationService.requestAppEndPointUrl()
try await ApplicationService.appLinkDownload()
try await ApplicationService.requestApplicationSession()
try await ApplicationService.validateSdk()
try await ApplicationService.requestApplicationDetails()
} catch {
self.showErrorAlert(error)
}
Then, the 1st error would break the call chain, would be cought and shown.
If you had to use the synchronous ApplicationService functions, you could convert them to asynchronous functions by using async wrappers as shown in the link cited above.
for this, you need to use OperationQueue & ** DispatchGroup** where you can make your API calls in BlockOperation and one operation depends on another, and their dispatch group helps you to hold the API calls.
func apiCall() {
let dispatchGroup = DispatchGroup()
let queue = OperationQueue()
let operation1 = BlockOperation {
dispatchGroup.enter()
ApplicationService.requestAppEndPointUrl {
Thread.sleep(forTimeInterval: Double.random(in: 1...3))
print("ApplicationService.requestAppEndPointUrl()")
dispatchGroup.leave()
}
dispatchGroup.wait()
}
let operation2 = BlockOperation {
dispatchGroup.enter()
ApplicationService.appLinkDownload {
print("ApplicationService.appLinkDownload()")
dispatchGroup.leave()
}
dispatchGroup.wait()
}
operation2.addDependency(operation1)
let operation3 = BlockOperation {
dispatchGroup.enter()
ApplicationService.requestApplicationSession {
Thread.sleep(forTimeInterval: Double.random(in: 1...3))
print("ApplicationService.requestApplicationSession()")
dispatchGroup.leave()
}
dispatchGroup.wait()
}
operation3.addDependency(operation2)
let operation4 = BlockOperation {
dispatchGroup.enter()
ApplicationService.validateSdk {
print("ApplicationService.validateSdk()")
dispatchGroup.leave()
}
dispatchGroup.wait()
}
operation4.addDependency(operation3)
let operation5 = BlockOperation {
dispatchGroup.enter()
ApplicationService.requestApplicationDetails {
Thread.sleep(forTimeInterval: Double.random(in: 1...3))
print("ApplicationService.requestApplicationDetails()")
dispatchGroup.leave()
}
dispatchGroup.wait()
}
operation5.addDependency(operation4)
queue.addOperations([operation1, operation2, operation3, operation4, operation5], waitUntilFinished: true)
}
I know code is a little bit nasty.
and if you are using swift 5.5 then use Async await mentioned by #Reinhard Männer
Thats a pretty question. So if you don't want to pass data from one api call to the other the solution is pretty easy.
All you have to do is to make a Service Manager that will manage all the service calls.
You can make an enum based on the services you want to call
enum ServiceEnum {
case requestAppEndPointUrl
case appLinkDownload
case requestApplicationSession
case validateSdk
case requestApplicationDetails
}
enum ServiceState {
case notStarted
case inProgress
case finished
case failed
}
Then you can make a class of the service
class Service {
var service: ServiceEnum
var serviceState: ServiceState = .notStarted
init(service: ServiceEnum) {
self.service = service
}
func updateState(serviceState: ServiceState) {
self.serviceState = serviceState
}
}
After that you will need to make the Service Manager
class ServiceManager {
private var services = [Services]()
private var indexForService: Int {
didSet {
self.startServices()
}
}
init() {
services.append(.requestAppEndPointUrl)
services.append(.appLinkDownload)
services.append(.requestApplicationSession)
services.append(.validateSdk)
services.append(.requestApplicationDetails)
indexForService = 0
}
func startServices() {
// Check if the process has ended
guard indexForService < services.count else {
// If you want you can call an event here.
// All the services have been completed
return
}
// Check that we have services to call
guard services.count > 0 else {
return
}
handleService(service: services[indexForService])
}
func haveAllFinished() -> Bool {
return services.fist(where: { $0.serviceState == .failed || $0.serviceState == .notStarted || $0.serviceState == .inProgress }) == nil
}
func handleService(service: Service) -> Bool {
switch(service) {
case .requestAppEndPointUrl:
ApplicationService.requestAppEndPointUrl { success, error in
let serviceState = success ? .finished : .failed
service.updateState(serviceState: serviceState)
if success {
self.indexForService += 1
} else {
self.showErrorAlert(error)
}
}
// Do the same for other services
}
}
}
I am trying to pass the value of gyroX to another function but it just ends up in it having a value of 0 when I use it as gyroX in that other function.
Here is the code:
var gyroX = Float()
motion.startGyroUpdates(to: .main) { (data, error) in
if let myData = data {
gyroX = Float(myData.rotationRate.x)
}
}
With Xcode 13 Beta and Swift 5.5
This is a problem that we can now solve with Async/Await's Continuations
We would first make a function that converts the callback into an awaitable result like:
func getXRotation(from motion: CMMotionManager) async throws -> Float {
try await withCheckedThrowingContinuation { continuation in
class GyroUpdateFailure: Error {} // make error to throw
motion.startGyroUpdates(to: .main) { (data, error) in
if let myData = data {
continuation.resume(returning: Float(myData.rotationRate.x))
} else {
throw GyroUpdateFailure()
}
}
}
}
Then we can assign the variable and use it like so:
let gyroX = try await getXRotation(from: motion)
callSomeOtherFunction(with: gyroX)
With Xcode <= 12 and Combine
In the current release of Swift and Xcode we can use the Combine framework to make callback handling a little easier for us. First we'll convert the closure from the motion manager into a "Future". Then we can use that future in a combine chain.
func getXRotation(from motion: CMMotionManager) -> Future<CMGyroData, Error> {
Future { promise in
class GyroUpdateFailure: Error {} // make error to throw
motion.startGyroUpdates(to: .main) { (data, error) in
if let myData = data {
promise(.success(myData))
} else {
promise(.failure(GyroUpdateFailure()))
}
}
}
}
// This is the other function you want to call
func someOtherFunction(_ x: Float) {}
// Then we can use it like so
_ = getXRotation(from: motion)
.eraseToAnyPublisher()
.map { Float($0.rotationRate.x) }
.map(someOtherFunction)
.sink { completion in
switch completion {
case .failure(let error):
print(error.localizedDescription)
default: break
}
} receiveValue: {
print($0)
}
There are some important parts to the combine flow. The _ = is one of them. The result of "sinking" on a publisher is a "cancellable" object. If we don't store that in a local variable the system can clean up the task before it fishes executing. So you will want to do that for sure.
I highly recommend you checkout SwiftBySundell.com to learn more about Combine or Async/Await and RayWenderlich.com for mobile development in general.
Hi So i am trying to wrap an async network request with Combine's Future/Promise.
my code goes something like that:
enum State {
case initial
case loading
case success(movies: [Movie])
case error(message: String)
}
protocol ViewModelProtocol {
var statePublisher: AnyPublisher<State, Never> { get }
func load(genreId: String)
}
class ViewModel: ViewModelProtocol {
var remoteDataSource = RemoteDataSource()
#Published state: State = .initial
var statePublisher: AnyPublisher<State, Never> { $state.eraseToAnyPubliher() }
public func load(genreId: String) {
self.state = .loading
self.getMovies(for: genreId)
.sink { [weak self] (moveis) in
guard let self = self else { return }
if let movies = movies {
self.state = .success(movies: movies)
} else {
self.state = .error(message: "failed to load movies")
}
}
}
func getMovies(for genreId: String) -> AnyPublisher<[Movie]?, Never> {
Future { promise in
self.remoteDataSource.getMovies(for: genreId) { (result) in
switch result {
case .success(let movies): promise(.success(movies))
case .failure: promise(.success(nil))
}
}
}.eraseToAnyPublisher()
}
}
I was trying to see if there are any memory leaks and found that there is a reference to the Future that is not being deallocated
same as here: Combine Future Publisher is not getting deallocated
You are strongly capturing self inside of your escaping Future init (just capture remoteDataSource). It doesn't seem to me like this should cause a memory leak. As the link you put in the question suggests Future does not behave like most other publishers; it does work as soon as you create it and not when you subscribe. It also memoizes and shares its results. I strongly suggest that you do no use this behind an AnyPublisher because its very non obvious to the caller that this thing is backed by a Future and that it will start work immediately. I would use URLSession.shared.dataTaskPublisher instead and then you get a regular publisher and you don't need the completion handler or the Future. Otherwise wrap the Future in a Deferred so you don't get the eager evaluation.
How to convert:
func getResults(completion: ([Result]?, Error) -> Void)
Into
var resultsPublisher: AnyPublisher<[Result], Error>
Just a scheme how I see it is (this syntax doesn't exist):
var resultsPublisher: AnyPublisher<[Result], Error> {
let publisher: AnyPublisher = ... // init
getResults { results, error in
guard let results = results else {
publisher.produce(error: error) // this syntax doesn't exist
return
}
publisher.produce(results: results) // this syntax doesn't exist
}
return publisher
}
I need that because some 3d party SDKs use completion closures and I want to write extensions to them that return Publishers.
The answer is Future Publisher as matt explained:
var resultsPublisher: AnyPublisher<[Result], Error> {
// need deferred when want
// to start request only when somebody subscribes
// + without Deferred it's impossible to .retry() later
Deferred {
Future { promise in
self.getResults { results, error in
guard let results = results else {
promise(.failure(error))
return
}
promise(.success(results))
}
}
}
.eraseToAnyPublisher()
}
I have created a function getFriends that reads a User's friendlist from firestore and puts each friend in a LocalUser object (which is my custom user class) in order to display the friendlist in a tableview. I need the DispatchSemaphore.wait() because I need the for loop to iterate only when the completion handler inside the for loop is called.
When loading the view, the app freezes. I know that the problem is that semaphore.wait() is called in the main thread. However, from reading DispatchQueue-tutorials I still don't understand how to fix this in my case.
Also: do you see any easier ways to implement what I want to do?
This is my call to the function in viewDidLoad():
self.getFriends() { (friends) in
self.foundFriends = friends
self.friendsTable.reloadData()
}
And the function getFriends:
let semaphore = DispatchSemaphore(value: 0)
func getFriends(completion: #escaping ([LocalUser]) -> ()) {
var friendsUID = [String : Any]()
database.collection("friends").document(self.uid).getDocument { (docSnapshot, error) in
if error != nil {
print("Error:", error!)
return
}
friendsUID = (docSnapshot?.data())!
var friends = [LocalUser]()
let friendsIdents = Array(friendsUID.keys)
for (idx,userID) in friendsIdents.enumerated() {
self.getUser(withUID: userID, completion: { (usr) in
var tempUser: LocalUser
tempUser = usr
friends.append(tempUser)
self.semaphore.signal()
})
if idx == friendsIdents.endIndex-1 {
print("friends at for loop completion:", friends.count)
completion(friends)
}
self.semaphore.wait()
}
}
}
friendsUID is a dict with each friend's uid as a key and true as the value. Since I only need the keys, I store them in the array friendsIdents. Function getUser searches the passed uid in firestore and creates the corresponding LocalUser (usr). This finally gets appended in friends array.
You should almost never have a semaphore.wait() on the main thread. Unless you expect to wait for < 10ms.
Instead, consider dispatching your friends list processing to a background thread. The background thread can perform the dispatch to your database/api and wait() without blocking the main thread.
Just make sure to use DispatchQueue.main.async {} from that thread if you need to trigger any UI work.
let semaphore = DispatchSemaphore(value: 0)
func getFriends(completion: #escaping ([LocalUser]) -> ()) {
var friendsUID = [String : Any]()
database.collection("friends").document(self.uid).getDocument { (docSnapshot, error) in
if error != nil {
print("Error:", error!)
return
}
DispatchQueue.global(qos: .userInitiated).async {
friendsUID = (docSnapshot?.data())!
var friends = [LocalUser]()
let friendsIdents = Array(friendsUID.keys)
for (idx,userID) in friendsIdents.enumerated() {
self.getUser(withUID: userID, completion: { (usr) in
var tempUser: LocalUser
tempUser = usr
friends.append(tempUser)
self.semaphore.signal()
})
if idx == friendsIdents.endIndex-1 {
print("friends at for loop completion:", friends.count)
completion(friends)
}
self.semaphore.wait()
}
// Insert here a DispatchQueue.main.async {} if you need something to happen
// on the main queue after you are done processing all entries
}
}