Performing a Completion Handler Before App Launches

Performing a completion handler before app launches

I've had to handle situations similarly in my Firebase applications. What I typically do is make an InitialViewController. This is the view controller that is always loaded, no matter what. This view controller is initially set up to seamlessly look exactly like the launch screen.

This is what the InitialViewController looks like in the interface builder:
Sample Image

And this is what my launch screen looks like:
Sample Image

So when I say they look exactly the same, I mean they look exactly the same. The sole purpose of this InitialViewController is to handle this asynchronous check and decide what to do next, all while looking like the launch screen. You may even copy/paste interface builder elements between the two view controllers.

So, within this InitialViewController, you make the authentication check in viewDidAppear(). If the user is logged in, we perform a segue to the home view controller. If not, we animate the user onboarding elements into place. The gifs demonstrating what I mean are pretty large (dimension-wise and data-wise), so they may take some time to load. You can find each one below:

User previously logged in.

User not previously logged in.

This is how I perform the check within InitialViewController:

@IBOutlet var loginButton: UIButton!
@IBOutlet var signupButton: UIButton!
@IBOutlet var stackView: UIStackView!
@IBOutlet var stackViewVerticalCenterConstraint: NSLayoutConstraint!

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

//When the view appears, we want to check to see if the user is logged in.
//Remember, the interface builder is set up so that this view controller **initially** looks identical to the launch screen
//This gives the effect that the authentication check is occurring before the app actually finishes launching
checkLoginStatus()
}

func checkLoginStatus() {
//If the user was previously logged in, go ahead and segue to the main app without making them login again

guard
let email = UserDefaults.standard.string(forKey: "email"),
let password = UserDefaults.standard.string(forKey: "password")
else {
// User has no defaults, animate onboarding elements into place
presentElements()
return
}

let authentificator = Authentificator()
authentificator.login(with: email, password) { result, _ in
if result {
//User is authenticated, perform the segue to the first view controller you want the user to see when they are logged in
self.performSegue(withIdentifier: "SkipLogin", sender: self)
}
}
}

func presentElements() {

//This is the part where the illusion comes into play
//The storyboard elements, like the login and signup buttons were always here, they were just hidden
//Now, we are going to animate the onboarding UI elements into place
//If this function is never called, then the user will be unaware that the launchscreen was even replaced with this view controller that handles the authentication check for us

//Make buttons visible, but...
loginButton.isHidden = false
signupButton.isHidden = false

//...set their alpha to 0
loginButton.alpha = 0
signupButton.alpha = 0

//Calculate distance to slide up
//(stackView is the stack view that holds our elements like loginButton and signupButton. It is invisible, but it contains these buttons.)
//(stackViewVerticalCenterConstraint is the NSLayoutConstraint that determines our stackView's vertical position)
self.stackViewVerticalCenterConstraint.constant = (view.frame.height / 2) + (stackView.frame.height / 2)

//After half a second, we are going to animate the UI elements into place
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
UIView.animate(withDuration: 0.75) {
self.loginButton.alpha = 1
self.signupButton.alpha = 1

//Create correct vertical position for stackView
self.stackViewVerticalCenterConstraint.constant = (self.view.frame.height - self.navigationController!.navigationBar.frame.size.height - self.signupButton.frame.maxY - (self.stackView.frame.size.height / 2)) / 3
self.view.layoutIfNeeded()
}
}
}

Completion handler returning before task finishes

Note that query.initialResultsHandler is asynchronous and your code will execute in the following order:

private func getSteps(completion: (Int) -> ()) {
// #1
var val: Int = 0
...
// #2
query.initialResultsHandler = { query,result,error in
// #5
if let myresult = result {
myresult.enumerateStatistics(from: startDate, to: Date()) { (statistic, value) in
if let count = statistic.sumQuantity() {
// #6
val = Int(count.doubleValue(for: HKUnit.count()))
}
}
}
// #7
}
// #3
healthStore.execute(query)
// #4
completion(val)
}

Note that #4 is executed before #6.


The solution is to move completion inside the asynchronous block:

private func getSteps(completion: (Int) -> ()) {
...
query.initialResultsHandler = { query,result,error in
if let myresult = result {
myresult.enumerateStatistics(from: startDate, to: Date()) { (statistic, value) in
if let count = statistic.sumQuantity() {
val = Int(count.doubleValue(for: HKUnit.count()))
}
}
}
completion(val) // <- move here
}
healthStore.execute(query)
}

When to call completionHandler in application:performFetchWithCompletionHandler: when Background Fetch is async?

You're right — you should call the completion handler only when your fetch is actually complete. Otherwise iOS will probably put your application back to sleep before the connection completes, and apps shouldn't actually be able to determine UIBackgroundFetchResultNewData versus UIBackgroundFetchResultNoData or UIBackgroundFetchResultFailed until then anyway. How do you know your connection will succeed?

You need to keep hold of the completionHandler and call it only once the connection has finished. Most likely you'll want to add an instance variable to your delegate, copy the completion handler into there, and call it when you're done.

Aside: connection:didReceiveData: doesn't signal the end of a request. Per the documentation:

Sent as a connection loads data incrementally.

[...]

The delegate should concatenate the contents of each data object
delivered to build up the complete data for a URL load.

You may receive any number of calls and the net result of the URL connection is the accumulation of all of them.

EDIT: you store a block by creating an instance variable of the correct type and copying the block to it. Blocks have unusual semantics because, unlike every other kind of Objective-C object, they're initially created on the stack. The net effect is just that you always copy them. If they're on the stack when you copy then they end up on the heap. If they're already on the heap then the copy just acts as a retain, since blocks are always immutable anyway.

So:

@implementation XXMDYourClass
{
// syntax follow the C rule; read from the centre outwards
void (^_completionHandler)(UIBackgroundFetchResult);
}

- (id)initWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
self = [super init];

if(self)
{
// keep the block by copying it; release later if
// you're not using ARC
_completionHandler = [completionHandler copy];
}

return self;
}

- (void)somethingThatHappensMuchLater
{
_completionHandler(UIBackgroundFetchResultWhatever);
}

@end

My Completion Handler for a Swift App Doesn't Seem to Work

That is how async code works. You make the call, and pass it a completion handler. The function call returns immediately. At some future date, the aysnc function finishes its work, and then it calls your completion handler.

Think of it like asking your kid to run to the store to buy some ingredients you need to finish making dinner. You give your kid the list send them on their way, and then go back to cooking the rest of dinner. When they come back, after an unpredictable delay, they tell you they have the stuff (call your completion handler) and at that point you take the items they gave you (Your completion handler can take parameters from the async function.)

If you set a breakpoint inside the completion handler, on the function call, and after the function call, you'll see the debugger first hit the function call, then the after-function-call breakpoint, and then at some future time you'll see it hit the completion handler code's breakpoint.

With an iOS quick action (shortcut item), what is the purpose of the completion handler parameter?

Short answer: parameter is not used in implementation of block in iOS 10 (guess that in iOS 9 too, but can't check right now).

Long answer: let's see what happens inside of completion block:

___50-[UIApplication _handleApplicationShortcutAction:]_block_invoke:
push rbp ; XREF=-[UIApplication _handleApplicationShortcutAction:]+132
mov rbp, rsp
mov rax, qword [ds:rdi+0x20]
mov rdx, qword [ds:rdi+0x28]
mov rsi, qword [ds:0x1179e88] ; @selector(_updateSnapshotAndStateRestorationWithAction:)
mov rdi, rax ; argument "instance" for method imp___got__objc_msgSend
pop rbp
jmp qword [ds:imp___got__objc_msgSend]
; endp

I run this on Intel64, so first argument should be stored in rdi register (when we calling block under ARC it is an instance of NSMallocBlock). There is no selector, so second parameter (bool argument) should be stored in rsi register. But rsi register is not used in code - it just sends message _updateSnapshotAndStateRestorationWithAction: to object ds:rdi+0x20 with argument ds:rdi+0x28.

Both ds:rdi+0x20 and ds:rdi+0x28 are captured pointers inside of the block.

Think that the guess with parameter as indicator for snapshot function was wrong.

How to display in a View a returned value from a completion handler in Swift?

Below is a Playground with a complete example. I'll walk through some of the important things to note.

First I simplified the "send" method since I don't have all the types and things from your original example. This send will wait 3 seconds then call the completion handler with whatever message you give it.

Inside the view, when the button is pushed, we call "send". Then, in the completion handler you'll notice:

DispatchQueue.main.async {
message = msg
}

I don't know what thread the Timer in send is going to use to call my completion handler. But UI updates need to happen on the main thread, so the DispatchQueue.main... construct will ensure that the UI update (setting message to msg) will happen on the main thread.

import UIKit
import SwiftUI
import PlaygroundSupport

func send(message: String, completionHandler: @escaping (String) -> Void) {
Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { timer in
completionHandler(message)
timer.invalidate()
}
}

struct ContentView: View {
@State var message : String = ""

var body: some View {
print("Building view")
return VStack {
TextField("blah", text: $message)
Button("Push Me", action: {
send(message: "Message Received") { msg in
DispatchQueue.main.async {
message = msg
}
}
})
}.frame(width: 320, height: 480, alignment: .center)
}
}

let myView = ContentView()
let host = UIHostingController(rootView: myView)
PlaygroundSupport.PlaygroundPage.current.liveView = host

Wait for completion handler to finish - Swift

Do not wait, use a completion handler, for convenience with an enum:

enum AuthResult {
case success(Bool), failure(Error)
}

func checkAvailabilty(completion: @escaping (AuthResult) -> ()) {

//
// other checking
//
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound], completionHandler: { (granted, error) in
if error != nil {
completion(.failure(error!))
} else {
completion(.success(granted))
}

})
}

And call it:

checkAvailabilty { result in
switch result {
case .success(let granted) :
if granted {
print("access is granted")
} else {
print("access is denied")
}
case .failure(let error): print(error)
}
}

In Swift 5.5 with async/await it does wait indeed

func checkAvailabilty() async throws -> Bool {

//
// other checking
//

return try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound])
}

And call it:

Task {
do {
let granted = try await checkAvailabilty()
if granted {
print("access is granted")
} else {
print("access is denied")
}
} catch {
print(error)
}
}

How do you open a FBSession with a completion handler that does not get retained and called on every session state change?

If you do not care about receiving state changes from FBSessionStateHandler like I did then the the code below should allow you to login to Facebook and provide you with a completion block that is not retained.

The completion block that is passed into the method will always get used when any state change occurs and then immediately nil'd out so that if another state change occurs it is ignored.

typedef void(^FacebookLoginCompletionBlock)(id<FBGraphUser> user);

- (void)loginToFacebookFromWithCompletionBlock:(FacebookLoginCompletionBlock)completionBlock
{
// If the user is not logged into Facebook attempt to open a session with "publish_actions" permissions.
if([[FBSession activeSession] isOpen] == NO)
{
// Copy the completion block to a local variable so it can be nil'd out after it is used to prevent retain loops.
__block FacebookLoginCompletionBlock copiedCompletionBlock = completionBlock;

[FBSession openActiveSessionWithPublishPermissions:@[ @"publish_actions" ] defaultAudience:FBSessionDefaultAudienceFriends allowLoginUI:YES completionHandler:^(FBSession *session, FBSessionState status, NSError *error)
{
// Only attempt to run any of this code if there is a completion block to call back to. If completion block is nil than it has already been used and this is a state change that we do not care about.
if(copiedCompletionBlock != nil)
{
// Because this method is only concerned with the user logging into Facebook just worry about the open state occuring with no errors.
if(status == FBSessionStateOpen && error == nil)
{
// If the user successfully logged into Facebook download their basic profile information so the app can save the information to display to the user what account they are logged in under.
[FBRequestConnection startForMeWithCompletionHandler:^(FBRequestConnection *connection, id<FBGraphUser> user, NSError *error)
{
if(copiedCompletionBlock != nil)
copiedCompletionBlock(user);

// nil out the copied completion block so it is not retained and called everytime the active FBSession's state changes.
copiedCompletionBlock = nil;
}];
}
// Because this method is only concerned with the user logging into Facebook if any other state is triggered call the completion block indicating that there was a failure.
else
{
if(copiedCompletionBlock != nil)
copiedCompletionBlock(nil);

// nil out the copied completion block so it is not retained and called everytime the active FBSession's state changes.
copiedCompletionBlock = nil;
}
}

// This block will exist the lifespan of the application because for some bizarre reason Facebook retains the completion handler for their open active session methods. Throw in some code that will display an error to the user if any session state changes occur that Facebook thinks the user should be aware of. Your code should be always checking if a active Facebook session exists before taking any action so not being aware of these changes should not be any issue. Worst case scenario you can listen for FBSessionDidSetActiveSessionNotification, FBSessionDidUnsetActiveSessionNotification, FBSessionDidBecomeOpenActiveSessionNotification or FBSessionDidBecomeClosedActiveSessionNotification notifications.
if ([error fberrorShouldNotifyUser] == YES)
{
NSString *alertTitle = @"Error logging into Facebook";
NSString *alertMessage = [error fberrorUserMessage];

if ([alertMessage length] == 0)
alertMessage = @"Please try again later.";

UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:alertTitle message:alertMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];

[alertView show];
}
}];
}
// If the user is already logged into Facebook immediately call the completion block with the user object that should have been saved when the user previously logged in.
else
{
if(completionBlock != nil)
completionBlock([self facebookUser]);
}
}


Related Topics



Leave a reply



Submit