freshOS / Then

:clapper: Tame async code with battle-tested promises
MIT License
993 stars 77 forks source link

Await is working ? #40

Closed NasH5169 closed 6 years ago

NasH5169 commented 6 years ago

Hi all,

Like PromiseKit wrapping delegates example on CLLocationManager, I try to do the same with freshOS/then.

https://github.com/mxcl/PromiseKit/blob/master/Documentation/CommonPatterns.md#wrapping-delegate-systems

The promise is working with:

LocatorManager.shared.getLocation()
            .timeout(0.5)
            .then { location in
                print("then" + String(data: try! JSONEncoder().encode(location), encoding: .utf8)!)
            }.onError { error in
                print(error)
        }

but not with await, my app is blocked. An idea..?

Thank a lot for helping.

do {
            let location = try await(LocatorManager.shared.getLocation().timeout(0.2))
            print("await" + String(data: try! JSONEncoder().encode(location), encoding: .utf8)!)

        } catch {
            print(error)
        }

All LocatorManager singleton class code

import Foundation
import CoreLocation

public class LocatorManager: NSObject, CLLocationManagerDelegate {
    static let shared = LocatorManager()
    private let locationManager = CLLocationManager()
    private var promise = Promise<DeviceLocation>()

    override private init() {
        super.init()
        locationManager.delegate = self
        locationManager.requestWhenInUseAuthorization()
    }

    func getLocation() -> Promise<DeviceLocation> {
        self.promise = Promise<DeviceLocation> { _, reject, _ in
            if !CLLocationManager.locationServicesEnabled() {
                reject(LocatorError.locationServiceDisabled)
                return
            }

            if CLLocationManager.authorizationStatus() == .authorizedWhenInUse ||
                CLLocationManager.authorizationStatus() == .authorizedAlways {
                    self.locationManager.requestLocation()                
            } else {
                reject(LocatorError.noUserAuthorization)
            }
        }

        return self.promise
    }

    public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        let deviceLocation = DeviceLocation(latitude: locations.last!.coordinate.latitude,
                                            longitude: locations.last!.coordinate.longitude)
        self.promise.fulfill(deviceLocation)
    }

    public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        self.promise.reject(error)
    }
}

public enum LocatorError: Error {
    case locationServiceDisabled
    case noUserAuthorization
}
s4cha commented 6 years ago

@NasH5169 Please excuse such a late reply 🙏 . I'm looking at it now :)

s4cha commented 6 years ago

@NasH5169 After a bit of test, this seems to come from the fact that CLLocationManager needs a runLoop to work, so typically on the main thread.

LocatorManager.shared.getLocation()
    .then { location in
        print("then" + String(data: try! JSONEncoder().encode(location), encoding: .utf8)!)
}

works while the following doesn't

DispatchQueue.global(qos: .background).async {
    LocatorManager.shared.getLocation()
        .then { location in
            print("then" + String(data: try! JSONEncoder().encode(location), encoding: .utf8)!)
    }
}

So this isn't await per se but the fact that await is using Dispatch group. Will report back when I have it working :)

s4cha commented 6 years ago

When using async wrapping await, we get a message that corroborates our hypothesis : A location manager (0xe86bdf0) was created on a dispatch queue executing on a thread other than the main thread. It is the developer's responsibility to ensure that there is a run loop running on the thread on which the location manager object is allocated. In particular, creating location managers in arbitrary dispatch queues (not attached to the main queue) is not supported and will result in callbacks not being received.

s4cha commented 6 years ago

Another fun fact ! 😃

// Doesn't work
async {
    let userLocation = try await(LocatorManager.shared.getLocation())
    print(userLocation)
}

// Works
let promise = LocatorManager.shared.getLocation()
async {
    let userLocation = try await(promise)
    print(userLocation)
}
s4cha commented 6 years ago

@NasH5169 Ok so I think I've got it now :)

First, you want to replace your do & catch withasync & onError

do {
    let location = try await(LocatorManager.shared.getLocation().timeout(0.2))
    print("await" + String(data: try! JSONEncoder().encode(location), encoding: .utf8)!)

} catch {
    print(error)
}

Becomes

async {
    let location = try await(LocatorManager.shared.getLocation())
    print("await" + String(data: try! JSONEncoder().encode(location), encoding: .utf8)!)
}.onError { error in
    print(error)
}

await without async is going to block the main thread, and thus block CLLocactionManager callbacks !

Then concerning the issue we described above, you want to make sure your Location Manager is inited on the main thread, which you can do like so :

static let shared:LocatorManager = {
        return DispatchQueue.main.sync { LocatorManager() }
}()

Doing these two things should solve you issue (at least it does on my end)

Hope this helps,

s4cha commented 6 years ago

@NasH5169 any news?

NasH5169 commented 6 years ago

Yes, same result on my side. Solved thanks :-)