Wait until an asynchronous api call is completed - Swift/IOS - swift

I'm working on an ios app where in my appDelegate I have:
func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {
self.api.signInWithToken(emailstring, token: authtokenstring) {
(object: AnyObject?, error:String?) in
if(object != nil){
self.user = object as? User
// go straight to the home view if auth succeeded
var rootViewController = self.window!.rootViewController as UINavigationController
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
var homeViewController = mainStoryboard.instantiateViewControllerWithIdentifier("HomeViewController") as HomeViewControllerenter
// code here
rootViewController.pushViewController(homeViewController, animated: true)
}
}
return true
}
The api.signInWithToken is an asynchronous call made with Alamofire, and I would like to wait for it's completion before returning true at the end end of func application.

Note: You should not do it this way, as it blocks the thread. See Nate's comment above for a better way.
There is a way to wait for an async call to complete using GCD. The code would look like the following
var semaphore = dispatch_semaphore_create(0)
performSomeAsyncTask {
...
dispatch_semaphore_signal(semaphore)
}
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
dispatch_release(semaphore)
Wikipedia has an OK article in case you know nothing about semaphores.

This is the solution in Swift 3. Again this blocks the thread until asynchronous task is complete, so it should be considered only in specific cases.
let semaphore = DispatchSemaphore(value: 0)
performAsyncTask {
semaphore.signal()
}
// Thread will wait here until async task closure is complete
semaphore.wait(timeout: DispatchTime.distantFuture)

Related

Swift callkit sometimes can't activate loudspeaker after received call (only incoming call)

after follow #Marco comment, i updated code like below, but still not working, the loudspeaker sometimes can not enabled
Before report new call/ user accepted call I called the 2 methods below:
configureAudioSessionToDefaultSpeaker()
func configureAudioSessionToDefaultSpeaker() {
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(AVAudioSession.Category.playAndRecord, mode: .default)
try session.setActive(true)
try session.setMode(AVAudioSession.Mode.voiceChat)
try session.setPreferredSampleRate(44100.0)
try session.setPreferredIOBufferDuration(0.005)
} catch {
print("Failed to configure `AVAudioSession`: \(error)")
}
}
I updated more code:
func startCallWithPhoneNumber(call : CallInfoModel) {
configureAudioSessionToDefaultSpeaker()
currentCall = call
if let unwrappedCurrentCall = currentCall {
let handle = CXHandle.init(type: .generic, value: unwrappedCurrentCall.CallerDisplay ?? UNKNOWN)
let startCallAction = CXStartCallAction.init(call: unwrappedCurrentCall.uuid, handle: handle)
let transaction = CXTransaction.init()
transaction.addAction(startCallAction)
requestTransaction(transaction: transaction)
self.provider?.reportOutgoingCall(with: startCallAction.callUUID, startedConnectingAt: nil)
}
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
configureAudioSessionToDefaultSpeaker()
delegate?.callDidAnswer()
action.fulfill()
currentCall?.isAccepted = true
let sb = UIStoryboard(name: "main", bundle: nil)
let vc = sb.instantiateViewController(withIdentifier: "SingleCallVC") as! SingleCallVC
vc.modalPresentationStyle = .fullScreen
vc.callObj = currentCall
vc.isIncoming = true
let appDelegate = AppDelegate.shared
appDelegate.window?.rootViewController?.present(vc, animated: true, completion: nil)
}
My call almost work normally but sometime loudspeaker can not be enabled. I read many documents but nothing worked for me. Could someone give me some advice? Thanks.
You're configuring the AudioSession two times. The RTCAudioSession it's a proxy of AVAudioSession. You should do only one configuration to avoid unexpected results. RTCAudioSession should expose all the methods of the AVAudioSession, so you should be able to make all the configurations you want inside configureRtcAudioSession() and eliminate configureAudioSessionToDefaultSpeaker() or viceversa. I'm not sure if it will solve your issue but at least it should help to avoid unexpected behaviors.
I've had success with enabling the speaker using the method below.
let audioQueue = DispatchQueue(label: "audio")
func setSpeaker(_ isEnabled: Bool) {
audioQueue.async {
defer {
AVAudioSession.sharedInstance().unlockForConfiguration()
}
AVAudioSession.sharedInstance().lockForConfiguration()
do {
try AVAudioSession.sharedInstance().overrideOutputAudioPort(isEnabled ? .speaker : .none)
} catch {
debugPrint(error.localizedDescription)
}
}
}
// Enables the audio speaker.
setSpeaker(true)
// Disables the audio speaker.
setSpeaker(false)

Is `completion` block of `UIViewController.present` guaranteed to be running on main thread?

Seems a simple question, but I don't see any definitive answer.
In UIViewController function:
func present(_ viewControllerToPresent: UIViewController,
animated flag: Bool,
completion: (() -> Void)? = nil)
Is completion block guaranteed to be running on main thread?
In other words. Can I do this:
vc.present(anotherVC, animated: animated) { [weak self] in
guard let self = self else { return }
// do some UI operations
self.<...>
}
Or should I do this:
vc.present(anotherVC, animated: animated) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// do some UI operations
self.<...>
}
}
Note: I tried to test it a bunch of times, and seems it's always running on main thread. But that could be by accident, and Apple's doc doesn't say anything explicit.
The completion callback runs on the main thread. There is no reason otherwise since all UI-related manipulations by cocoa are done on the main thread. However you should be calling the present function from the main thread as well.

fetchRecordCompletionBlock and semaphores - help understanding execution order

Context:
App with all data in CloudKit
ViewController calls a query to load the data for a tableview
tableview crashes because the array of data for the tableview hasn't
come back from CK
I've researched semaphores and have it nearly
working But can't seem to figure out where to place the
semaphore.signal() to get the exact right behaviour
within viewDidLoad, I call the function:
Week.fetchWeeks(for: challenge!.weeks!) { weeks in
self.weeks = weeks
}
and the function:
static func fetchWeeks(for references: [CKRecord.Reference],
_ completion: #escaping ([Week]) -> Void) {
let recordIDs = references.map { $0.recordID }
let operation = CKFetchRecordsOperation(recordIDs: recordIDs)
operation.qualityOfService = .utility
let semaphore = DispatchSemaphore(value: 0)
operation.fetchRecordsCompletionBlock = { records, error in
let weeks = records?.values.map(Week.init) ?? []
DispatchQueue.main.async {
completion(weeks)
//Option 1: putting semaphore.signal() here means it never completes
// beyond initialization of the week records
}
//Option 2: putting semaphore.signal() here means it completes after the
// initialization of the Week items, but before completion(weeks) is done
// so the array isn't initialized in the view controller in time. so the
// VC tries to use weeks and unwraps a nil.
semaphore.signal()
}
Model.currentModel.publicDB.add(operation)
semaphore.wait() // blocking the thread until .signal is called
}
Note: I have tested that the weeks array within the view controller is properly set eventually - so it does seem to be purely a timing issue :)
I've tested placement of .signal() and if I put it within the 'DispatchQueue.main.async' block, it never gets triggered - probably because that block itself is waiting for the signal.
However if I put it anywhere else, then the viewcontroller picks up at that point and the completion(weeks) doesn't get called in time.
Maybe it is obvious - but as my first time working with semaphores - I'm struggling to figure it out!
Update 1: It works with DispatchQueue(label: "background")
I was able to get it working once I twigged that the semaphore.wait() was never going to get called with semaphore.signal() on the main thread.
So I changed it from:
DispatchQueue.main.async
to
DispatchQueue(label: "background").async and popped the semaphore.signal() inside and it did the trick
Comments/critiques welcome!
static func fetchWeeks(for references: [CKRecord.Reference],
_ completion: #escaping ([Week]) -> Void) {
NSLog("inside fetchWeeks in Week ")
let recordIDs = references.map { $0.recordID }
let operation = CKFetchRecordsOperation(recordIDs: recordIDs)
operation.qualityOfService = .utility
let semaphore = DispatchSemaphore(value: 0)
operation.fetchRecordsCompletionBlock = { records, error in
if error != nil {
print(error?.localizedDescription)
}
let weeks = records?.values.map(Week.init) ?? []
DispatchQueue(label: "background").async {
completion(weeks)
semaphore.signal()
}
}
Model.currentModel.publicDB.add(operation)
semaphore.wait() // blocking the thread until .signal is called
}
}
Update 2: Trying to avoid use of semaphores
Per comment thread - we shouldn't need to use semaphores with CloudKit - so it is likely that I'm doing something stupid :)
moving fetchWeeks() to the viewController to try to isolate the issue...but it still blows up as fetchWeeks() has't completed before the code tries to execute the line after and use the weeks array
my viewController:
class ChallengeDetailViewController: UIViewController {
#IBOutlet weak var rideTableView: UITableView!
//set by the inbound segue
var challenge: Challenge?
// set in fetchWeeks based on the challenge
var weeks: [Week]?
override func viewDidLoad() {
super.viewDidLoad()
rideTableView.dataSource = self
rideTableView.register(UINib(nibName: K.cellNibName, bundle: nil), forCellReuseIdentifier: K.cellIdentifier)
rideTableView.delegate = self
fetchWeeks(for: challenge!.weeks!) { weeks in
self.weeks = weeks
}
//This is where it blows up as weeks is nil
weeks = weeks!.sorted(by: { $0.weekSequence < $1.weekSequence })
}
//moved this to the view controller
func fetchWeeks(for references: [CKRecord.Reference],
_ completion: #escaping ([Week]) -> Void) {
let recordIDs = references.map { $0.recordID }
let operation = CKFetchRecordsOperation(recordIDs: recordIDs)
operation.qualityOfService = .utility
operation.fetchRecordsCompletionBlock = { records, error in
if error != nil {
print(error?.localizedDescription)
}
let weeks = records?.values.map(Week.init) ?? []
DispatchQueue.main.sync {
completion(weeks)
}
}
Model.currentModel.publicDB.add(operation)
}
Once again: Never use semaphores with the CloudKit API.
First of all declare data source arrays always as non-optional empty arrays to get rid of unnecessary unwrapping the optional
var weeks = [Week]()
The mistake is that you don't use the fetched data at the right place.
As the closure is asynchronous you have to proceed inside the closure
fetchWeeks(for: challenge!.weeks!) { [weak self] weeks in
self?.weeks = weeks
self?.weeks = weeks.sorted(by: { $0.weekSequence < $1.weekSequence })
}
or simpler
fetchWeeks(for: challenge!.weeks!) { [weak self] weeks in
self?.weeks = weeks.sorted{ $0.weekSequence < $1.weekSequence }
}
And if you need to reload the table view do it also inside the closure
fetchWeeks(for: challenge!.weeks!) { [weak self] weeks in
self?.weeks = weeks.sorted{ $0.weekSequence < $1.weekSequence }
self?.rideTableView.reloadData()
}
To do so you have to call completion on the main thread
DispatchQueue.main.async {
completion(weeks)
}
And finally delete the ugly semaphore!
let semaphore = DispatchSemaphore(value: 0)
...
semaphore.signal()
...
semaphore.wait()

Run a function N time anywhere in the app in Swift

I trying to make a calling app for my project and I want to add a function that keeps checking if someone if calling. My app uses Firebase where I have a key for each users to check if he made a call or not.
There's two problem I am facing here, the first one is, as I said, that I want my function to keep checking anywhere in the app for an incoming call. The other problem is that i have a viewcontroller that I want to pop up when someone is calling. I have found this code on github but it uses navigationcontroller which I am not using in my app :
extension UIViewController{
func presentViewControllerFromVisibleViewController(viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
if let navigationController = self as? UINavigationController, let topViewController = navigationController.topViewController {
topViewController.presentViewControllerFromVisibleViewController(viewControllerToPresent: viewControllerToPresent, animated: true, completion: completion)
} else if (presentedViewController != nil) {
presentedViewController!.presentViewControllerFromVisibleViewController(viewControllerToPresent: viewControllerToPresent, animated: true, completion: completion)
} else {
present(viewControllerToPresent, animated: true, completion: completion)
}
}
}
For your question on monitoring when incoming calls occur and to be called as a result, see this answer. It's probably what you need (I've never tried it, however). The example shows creating a CXCallObserver and setting your AppDelegate as delegate.
For your second question, I'd first try this answer which leverages the window.rootViewController so you can do this from your AppDelegate. Generally, the root VC is your friend when trying to do UI your AppDelegate. :)
A better answer based on Alex's added comments:
I'd first look at how to set up an observer to your Firebase model so that you can get a callback. If you don't have a way to do that, I'd use KVO on the Firebase model property. But to do exactly as you're requesting, and to do so lazily from AppDelegate (rather than from a singleton), see this code:
// In AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool
{
self.timerToCheckForCalls = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(timerFired), userInfo: nil, repeats: true)
}
func timerFired()
{
let didCall = // TODO: query your Firebase model...
guard didCall == true else
{
return
}
self.displayCallerView()
}
func displayCallerView()
{
// See below link.
}
See this answer for how to present your view controller, even when your app might be showing an action sheet, alert, etc... which I think you'd especially value since you need to display the caller regardless of what your app is doing.
Note while user is scrolling a UITextView, the timer won't fire yet. There may be other situations where the timer could be delayed too. So it really would be best to observe your Firebase model or receive a KVO callback than to use a timer.
If you want to make a function that can be called from anywhere, use a singleton pattern. You can also use that to store your special view controller.
Bear in mind that this code SHOULD NOT considered fully functioning code and will need to be customized by you to suit your needs.
class MyClass {
let shared = MyClass()
var viewController: SpecialViewController?
func checkForCall() {
// do function stuff
}
func getSpecialViewController() {
let storyBoard = UIStoryboard.init(name: "main", bundle: nil)
// keep it so we don't have to instantiate it every time
if viewController == nil {
viewController = storyBoard.instantiateViewController(withIdentifier: "SomeViewController")
}
return viewController
}
}
// Make an extension for UIViewController so that they can all
// use this function
extension UIViewController {
func presentSpecialViewController() {
let vc = MyClass.shared.getSpecialViewController()
present(vc, animated: false, completion: nil)
}
}
Somewhere in your code:
// in some function
MyClass.shared.checkForCall()
Somewhere else in code:
presentSpecialViewController()

Go to another view when NSURLSession finishes its job

My problem is when I try to go another view after some api call finishes, it wont go to the next view. Here is the code below. Thanks in advance.
var task = NSURLSession.sharedSession().dataTaskWithURL(url!, completionHandler: {
(data, response, error) in //println(response)
//println(error)
self.myWeather.apiCallData = JSON(data: data)
self.myWeather.apiCallError = error
println("api call finished")
if error == nil {
println("no error")
let weatherView = self.storyboard?.instantiateViewControllerWithIdentifier("WeatherView") as WeatherViewController
weatherView.myWeather = self.myWeather
self.navigationController?.pushViewController(weatherView, animated: false)
}
}).resume()
It does print api call finished and no error on console. But it doesn't go to the other scene.
The problem is that the completion handler code of the dataTaskWithURL method runs in a background secondary thread, not in the main thread (where view controller transition can happen).
Wrap the call to pushViewController in a main thread queue closure:
...
dispatch_async(dispatch_get_main_queue(), {
let navigationVC = self.navigationController
navigationVC?.pushViewController(weatherView, animated: false)
})
...
I have written the code in two lines to avoid a swift compiler bug (single statement and closure return value: see this). You can also write it as:
...
dispatch_async(dispatch_get_main_queue(), {
(self.navigationController)?.pushViewController(weatherView, animated: false)
return
})
...