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

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.

Related

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()

Displaying Activity indicator with OCR and using threads

I am trying to display an activity indicator whilst some text recognition happens. If i just start and stop[ the indicator around the recognition code it never shows. The issue i have is that if use:
activityIndicator.startAnimating()
DispatchQueue.main.async( execute: {
self.performTextRecognition()
})
activityIndicator.stopAnimating()
self.performSegue(withIdentifier: "segueToMyBills", sender: self)
The indicator never shows as it performs the segue and the table view in the next view controller shows no information because the text recognition hasn't completed. I've never touched on threads until now so a little insight on what to do would be much appreciated. Thanks!
Well your problem is that your OCR happens on the main thread. That blocks the thread thus never having time to draw the activity indicator. Try to modify your code into this:
activityIndicator.startAnimating()
DispatchQueue.global(qos: .background).async { [weak weaKSelf = self] in
// Use a weak reference to self here to make sure no retain cycle is created
weakSelf?.performTextRecognition()
DispatchQueue.main.async {
// Make sure weakSelf is still around
guard let weakSelf = weakSelf else { return }
weakSelf.activityIndicator.stopAnimating()
weakSelf.performSegue(withIdentifier: "segueToMyBills", sender: weakSelf)
}
}
Try to add a completion handler to your self.performTextRecognition() function in this way
function performTextRecognition(completion: ((Error?) -> Void)? = .none) {
//you can replace Error with Any type or leave it nil
//do your logic here
completion?(error)
}
and then call the function like this :
performTextRecognition(completion: { error in
activityIndicator.stopAnimating()
self.performSegue(withIdentifier: "segueToMyBills", sender: self)
})

How do I run an asynchronous thread that only runs as long as the view that uses it is presented?

How do I run an asynchronous thread that only runs as long as the view that uses it is presented?
I want the view to run this asynchronous thread. However, as soon as the view disappears, I want that thread to stop running. What's the best way to do this? I'm not sure where to start and might be thinking about this the wrong way. Nevertheless, what I described is how I want it to behave to the user.
You can use NSOperation to achieve what you want, NSOperation and NSOperationQueue are built on top of GCD. As a very general rule, Apple recommends using the highest-level abstraction, and then dropping down to lower levels when measurements show they are needed.
For example, You want to download images asynchronously when the view is loaded and cancel the task when the view is disappeared. First create a ImageDownloader object subclass to NSOperation. Notice that we check if the operation is cancelled twice, this is because the NSOperation has 3 states: isReady -> isExecuting -> isFinish and when the operation starts executing, it won't be cancelled automatically, we need to do it ourself.
class ImageDownloader: NSOperation {
//1
var photoRecord: NSURL = NSURL(string: "fortest")!
//2
init(photoRecord: NSURL) {
self.photoRecord = photoRecord
}
//3
override func main() {
//4
if self.cancelled {
return
}
//5
let imageData = NSData(contentsOfURL:self.photoRecord)
//6
if self.cancelled {
return
}
}
}
Then you can use it like: downloader.cancel(), downloader.start(). Notice that we need to check if the operation is cancelled in the completion block.
import UIKit
class ViewController: UIViewController {
let downloder = ImageDownloader(photoRecord: NSURL(string: "test")!)
override func viewDidLoad() {
super.viewDidLoad()
downloder.completionBlock = {
if self.downloder.cancelled {
return
}
print("image downloaded")
}
//Start the task when the view is loaded
downloder.start()
}
override func viewWillDisappear(animated: Bool) {
//Cancel the task when the view will disappear
downloder.cancel()
}
}
Once DetailViewController is presented, the asyncOperation method will be executed asynchronously.
Note: currently the asyncOperation method is executed every second so if you want the method to be called only once, you must change the repeats property to false.
class DetailViewController: UIViewController {
// timer that will execute
// asynchronously an operation
var timer: NSTimer!
// counter used in the async operation.
var counter = 0
// when view is about to appear
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// setting up the timer
timer = NSTimer.scheduledTimerWithTimeInterval(
1.0,
target: self,
selector: #selector(asyncOperation),
userInfo: nil,
repeats: true //set up false if you don't want the operation repeats its execution.
)
}
// when view is about to disappear
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
// stopping the timer
timer.invalidate()
}
// async operation that will
// be executed
func asyncOperation() {
counter += 1
print("counter: \(counter)")
}
}
Source: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSTimer_Class/
Result:

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
})
...

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

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)