- Add Facebook SDK

- Add PNUser Login
This commit is contained in:
giuseppenuc committed 2016-02-12 00:01:51 +01:00
1 parent f2885301b9
commit a6dcf672bb
370 files changed
+44655 -2705

No files matched your search

+42
View File
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Foundation/Foundation.h>
#import <Bolts/BFCancellationTokenRegistration.h>
NS_ASSUME_NONNULL_BEGIN
/*!
A block that will be called when a token is cancelled.
*/
typedef void(^BFCancellationBlock)();
/*!
The consumer view of a CancellationToken.
Propagates notification that operations should be canceled.
A BFCancellationToken has methods to inspect whether the token has been cancelled.
*/
@interface BFCancellationToken : NSObject
/*!
Whether cancellation has been requested for this token source.
*/
@property (nonatomic, assign, readonly, getter=isCancellationRequested) BOOL cancellationRequested;
/*!
Register a block to be notified when the token is cancelled.
If the token is already cancelled the delegate will be notified immediately.
*/
- (BFCancellationTokenRegistration *)registerCancellationObserverWithBlock:(BFCancellationBlock)block;
@end
NS_ASSUME_NONNULL_END
+141
View File
@@ -0,0 +1,141 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "BFCancellationToken.h"
#import "BFCancellationTokenRegistration.h"
@interface BFCancellationToken ()
@property (nonatomic, assign, getter=isCancellationRequested) BOOL cancellationRequested;
@property (nonatomic, strong) NSMutableArray *registrations;
@property (nonatomic, strong) NSObject *lock;
@property (nonatomic) BOOL disposed;
@end
@interface BFCancellationTokenRegistration (BFCancellationToken)
+ (instancetype)registrationWithToken:(BFCancellationToken *)token delegate:(BFCancellationBlock)delegate;
- (void)notifyDelegate;
@end
@implementation BFCancellationToken
#pragma mark - Initializer
- (instancetype)init {
self = [super init];
if (!self) return nil;
_registrations = [NSMutableArray array];
_lock = [NSObject new];
return self;
}
#pragma mark - Custom Setters/Getters
- (BOOL)isCancellationRequested {
@synchronized(self.lock) {
[self throwIfDisposed];
return _cancellationRequested;
}
}
- (void)cancel {
NSArray *registrations;
@synchronized(self.lock) {
[self throwIfDisposed];
if (_cancellationRequested) {
return;
}
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(cancelPrivate) object:nil];
_cancellationRequested = YES;
registrations = [self.registrations copy];
}
[self notifyCancellation:registrations];
}
- (void)notifyCancellation:(NSArray *)registrations {
for (BFCancellationTokenRegistration *registration in registrations) {
[registration notifyDelegate];
}
}
- (BFCancellationTokenRegistration *)registerCancellationObserverWithBlock:(BFCancellationBlock)block {
@synchronized(self.lock) {
BFCancellationTokenRegistration *registration = [BFCancellationTokenRegistration registrationWithToken:self delegate:[block copy]];
[self.registrations addObject:registration];
return registration;
}
}
- (void)unregisterRegistration:(BFCancellationTokenRegistration *)registration {
@synchronized(self.lock) {
[self throwIfDisposed];
[self.registrations removeObject:registration];
}
}
// Delay on a non-public method to prevent interference with a user calling performSelector or
// cancelPreviousPerformRequestsWithTarget on the public method
- (void)cancelPrivate {
[self cancel];
}
- (void)cancelAfterDelay:(int)millis {
[self throwIfDisposed];
if (millis < -1) {
[NSException raise:NSInvalidArgumentException format:@"Delay must be >= -1"];
}
if (millis == 0) {
[self cancel];
return;
}
@synchronized(self.lock) {
[self throwIfDisposed];
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(cancelPrivate) object:nil];
if (self.cancellationRequested) {
return;
}
if (millis != -1) {
double delay = (double)millis / 1000;
[self performSelector:@selector(cancelPrivate) withObject:nil afterDelay:delay];
}
}
}
- (void)dispose {
@synchronized(self.lock) {
if (self.disposed) {
return;
}
self.disposed = YES;
for (BFCancellationTokenRegistration *registration in self.registrations) {
[registration dispose];
}
[self.registrations removeAllObjects];
}
}
- (void)throwIfDisposed {
if (self.disposed) {
[NSException raise:NSInternalInconsistencyException format:@"Object already disposed"];
}
}
@end
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/*!
Represents the registration of a cancellation observer with a cancellation token.
Can be used to unregister the observer at a later time.
*/
@interface BFCancellationTokenRegistration : NSObject
/*!
Removes the cancellation observer registered with the token
and releases all resources associated with this registration.
*/
- (void)dispose;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "BFCancellationTokenRegistration.h"
#import "BFCancellationToken.h"
@interface BFCancellationTokenRegistration ()
@property (nonatomic, weak) BFCancellationToken *token;
@property (nonatomic, strong) BFCancellationBlock cancellationObserverBlock;
@property (nonatomic, strong) NSObject *lock;
@property (nonatomic) BOOL disposed;
@end
@interface BFCancellationToken (BFCancellationTokenRegistration)
- (void)unregisterRegistration:(BFCancellationTokenRegistration *)registration;
@end
@implementation BFCancellationTokenRegistration
+ (instancetype)registrationWithToken:(BFCancellationToken *)token delegate:(BFCancellationBlock)delegate {
BFCancellationTokenRegistration *registration = [BFCancellationTokenRegistration new];
registration.token = token;
registration.cancellationObserverBlock = delegate;
return registration;
}
- (instancetype)init {
self = [super init];
if (!self) return nil;
_lock = [NSObject new];
return self;
}
- (void)dispose {
@synchronized(self.lock) {
if (self.disposed) {
return;
}
self.disposed = YES;
}
BFCancellationToken *token = self.token;
if (token != nil) {
[token unregisterRegistration:self];
self.token = nil;
}
self.cancellationObserverBlock = nil;
}
- (void)notifyDelegate {
@synchronized(self.lock) {
[self throwIfDisposed];
self.cancellationObserverBlock();
}
}
- (void)throwIfDisposed {
NSAssert(!self.disposed, @"Object already disposed");
}
@end
@@ -0,0 +1,60 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@class BFCancellationToken;
/*!
BFCancellationTokenSource represents the producer side of a CancellationToken.
Signals to a CancellationToken that it should be canceled.
It is a cancellation token that also has methods
for changing the state of a token by cancelling it.
*/
@interface BFCancellationTokenSource : NSObject
/*!
Creates a new cancellation token source.
*/
+ (instancetype)cancellationTokenSource;
/*!
The cancellation token associated with this CancellationTokenSource.
*/
@property (nonatomic, strong, readonly) BFCancellationToken *token;
/*!
Whether cancellation has been requested for this token source.
*/
@property (nonatomic, assign, readonly, getter=isCancellationRequested) BOOL cancellationRequested;
/*!
Cancels the token if it has not already been cancelled.
*/
- (void)cancel;
/*!
Schedules a cancel operation on this CancellationTokenSource after the specified number of milliseconds.
@param millis The number of milliseconds to wait before completing the returned task.
If delay is `0` the cancel is executed immediately. If delay is `-1` any scheduled cancellation is stopped.
*/
- (void)cancelAfterDelay:(int)millis;
/*!
Releases all resources associated with this token source,
including disposing of all registrations.
*/
- (void)dispose;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,60 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "BFCancellationTokenSource.h"
#import "BFCancellationToken.h"
@interface BFCancellationToken (BFCancellationTokenSource)
- (void)cancel;
- (void)cancelAfterDelay:(int)millis;
- (void)dispose;
- (void)throwIfDisposed;
@end
@implementation BFCancellationTokenSource
#pragma mark - Initializer
- (instancetype)init {
self = [super init];
if (!self) return nil;
_token = [BFCancellationToken new];
return self;
}
+ (instancetype)cancellationTokenSource {
return [BFCancellationTokenSource new];
}
#pragma mark - Custom Setters/Getters
- (BOOL)isCancellationRequested {
return _token.isCancellationRequested;
}
- (void)cancel {
[_token cancel];
}
- (void)cancelAfterDelay:(int)millis {
[_token cancelAfterDelay:millis];
}
- (void)dispose {
[_token dispose];
}
@end
+62
View File
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/*!
An object that can run a given block.
*/
@interface BFExecutor : NSObject
/*!
Returns a default executor, which runs continuations immediately until the call stack gets too
deep, then dispatches to a new GCD queue.
*/
+ (instancetype)defaultExecutor;
/*!
Returns an executor that runs continuations on the thread where the previous task was completed.
*/
+ (instancetype)immediateExecutor;
/*!
Returns an executor that runs continuations on the main thread.
*/
+ (instancetype)mainThreadExecutor;
/*!
Returns a new executor that uses the given block to execute continuations.
@param block The block to use.
*/
+ (instancetype)executorWithBlock:(void(^)(void(^block)()))block;
/*!
Returns a new executor that runs continuations on the given queue.
@param queue The instance of `dispatch_queue_t` to dispatch all continuations onto.
*/
+ (instancetype)executorWithDispatchQueue:(dispatch_queue_t)queue;
/*!
Returns a new executor that runs continuations on the given queue.
@param queue The instance of `NSOperationQueue` to run all continuations on.
*/
+ (instancetype)executorWithOperationQueue:(NSOperationQueue *)queue;
/*!
Runs the given block using this executor's particular strategy.
@param block The block to execute.
*/
- (void)execute:(void(^)())block;
@end
NS_ASSUME_NONNULL_END
+132
View File
@@ -0,0 +1,132 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "BFExecutor.h"
#import <pthread.h>
/*!
Get the remaining stack-size of the current thread.
@param totalSize The total stack size of the current thread.
@return The remaining size, in bytes, available to the current thread.
@note This function cannot be inlined, as otherwise the internal implementation could fail to report the proper
remaining stack space.
*/
__attribute__((noinline)) static size_t remaining_stack_size(size_t *__nonnull restrict totalSize) {
pthread_t currentThread = pthread_self();
// NOTE: We must store stack pointers as uint8_t so that the pointer math is well-defined
uint8_t *endStack = pthread_get_stackaddr_np(currentThread);
*totalSize = pthread_get_stacksize_np(currentThread);
// NOTE: If the function is inlined, this value could be incorrect
uint8_t *frameAddr = __builtin_frame_address(0);
return (*totalSize) - (endStack - frameAddr);
}
@interface BFExecutor ()
@property (nonatomic, copy) void(^block)(void(^block)());
@end
@implementation BFExecutor
#pragma mark - Executor methods
+ (instancetype)defaultExecutor {
static BFExecutor *defaultExecutor = NULL;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
defaultExecutor = [self executorWithBlock:^void(void(^block)()) {
// We prefer to run everything possible immediately, so that there is callstack information
// when debugging. However, we don't want the stack to get too deep, so if the remaining stack space
// is less than 10% of the total space, we dispatch to another GCD queue.
size_t totalStackSize = 0;
size_t remainingStackSize = remaining_stack_size(&totalStackSize);
if (remainingStackSize < (totalStackSize / 10)) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block);
} else {
@autoreleasepool {
block();
}
}
}];
});
return defaultExecutor;
}
+ (instancetype)immediateExecutor {
static BFExecutor *immediateExecutor = NULL;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
immediateExecutor = [self executorWithBlock:^void(void(^block)()) {
block();
}];
});
return immediateExecutor;
}
+ (instancetype)mainThreadExecutor {
static BFExecutor *mainThreadExecutor = NULL;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
mainThreadExecutor = [self executorWithBlock:^void(void(^block)()) {
if (![NSThread isMainThread]) {
dispatch_async(dispatch_get_main_queue(), block);
} else {
@autoreleasepool {
block();
}
}
}];
});
return mainThreadExecutor;
}
+ (instancetype)executorWithBlock:(void(^)(void(^block)()))block {
return [[self alloc] initWithBlock:block];
}
+ (instancetype)executorWithDispatchQueue:(dispatch_queue_t)queue {
return [self executorWithBlock:^void(void(^block)()) {
dispatch_async(queue, block);
}];
}
+ (instancetype)executorWithOperationQueue:(NSOperationQueue *)queue {
return [self executorWithBlock:^void(void(^block)()) {
[queue addOperation:[NSBlockOperation blockOperationWithBlock:block]];
}];
}
#pragma mark - Initializer
- (instancetype)initWithBlock:(void(^)(void(^block)()))block {
self = [super init];
if (!self) return nil;
_block = block;
return self;
}
#pragma mark - Execution
- (void)execute:(void(^)())block {
self.block(block);
}
@end
+261
View File
@@ -0,0 +1,261 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Foundation/Foundation.h>
#import <Bolts/BFCancellationToken.h>
NS_ASSUME_NONNULL_BEGIN
/*!
Error domain used if there was multiple errors on <BFTask taskForCompletionOfAllTasks:>.
*/
extern NSString *const BFTaskErrorDomain;
/*!
An error code used for <BFTask taskForCompletionOfAllTasks:>, if there were multiple errors.
*/
extern NSInteger const kBFMultipleErrorsError;
/*!
An exception that is thrown if there was multiple exceptions on <BFTask taskForCompletionOfAllTasks:>.
*/
extern NSString *const BFTaskMultipleExceptionsException;
@class BFExecutor;
@class BFTask;
/*!
The consumer view of a Task. A BFTask has methods to
inspect the state of the task, and to add continuations to
be run once the task is complete.
*/
@interface BFTask<__covariant ResultType> : NSObject
/*!
A block that can act as a continuation for a task.
*/
typedef __nullable id(^BFContinuationBlock)(BFTask<ResultType> *task);
/*!
Creates a task that is already completed with the given result.
@param result The result for the task.
*/
+ (instancetype)taskWithResult:(nullable ResultType)result;
/*!
Creates a task that is already completed with the given error.
@param error The error for the task.
*/
+ (instancetype)taskWithError:(NSError *)error;
/*!
Creates a task that is already completed with the given exception.
@param exception The exception for the task.
*/
+ (instancetype)taskWithException:(NSException *)exception;
/*!
Creates a task that is already cancelled.
*/
+ (instancetype)cancelledTask;
/*!
Returns a task that will be completed (with result == nil) once
all of the input tasks have completed.
@param tasks An `NSArray` of the tasks to use as an input.
*/
+ (instancetype)taskForCompletionOfAllTasks:(nullable NSArray<BFTask *> *)tasks;
/*!
Returns a task that will be completed once all of the input tasks have completed.
If all tasks complete successfully without being faulted or cancelled the result will be
an `NSArray` of all task results in the order they were provided.
@param tasks An `NSArray` of the tasks to use as an input.
*/
+ (instancetype)taskForCompletionOfAllTasksWithResults:(nullable NSArray<BFTask *> *)tasks;
/*!
Returns a task that will be completed a certain amount of time in the future.
@param millis The approximate number of milliseconds to wait before the
task will be finished (with result == nil).
*/
+ (instancetype)taskWithDelay:(int)millis;
/*!
Returns a task that will be completed a certain amount of time in the future.
@param millis The approximate number of milliseconds to wait before the
task will be finished (with result == nil).
@param token The cancellation token (optional).
*/
+ (instancetype)taskWithDelay:(int)millis cancellationToken:(nullable BFCancellationToken *)token;
/*!
Returns a task that will be completed after the given block completes with
the specified executor.
@param executor A BFExecutor responsible for determining how the
continuation block will be run.
@param block The block to immediately schedule to run with the given executor.
@returns A task that will be completed after block has run.
If block returns a BFTask, then the task returned from
this method will not be completed until that task is completed.
*/
+ (instancetype)taskFromExecutor:(BFExecutor *)executor withBlock:(nullable id (^)())block;
// Properties that will be set on the task once it is completed.
/*!
The result of a successful task.
*/
@property (nullable, nonatomic, strong, readonly) ResultType result;
/*!
The error of a failed task.
*/
@property (nullable, nonatomic, strong, readonly) NSError *error;
/*!
The exception of a failed task.
*/
@property (nullable, nonatomic, strong, readonly) NSException *exception;
/*!
Whether this task has been cancelled.
*/
@property (nonatomic, assign, readonly, getter=isCancelled) BOOL cancelled;
/*!
Whether this task has completed due to an error or exception.
*/
@property (nonatomic, assign, readonly, getter=isFaulted) BOOL faulted;
/*!
Whether this task has completed.
*/
@property (nonatomic, assign, readonly, getter=isCompleted) BOOL completed;
/*!
Enqueues the given block to be run once this task is complete.
This method uses a default execution strategy. The block will be
run on the thread where the previous task completes, unless the
the stack depth is too deep, in which case it will be run on a
dispatch queue with default priority.
@param block The block to be run once this task is complete.
@returns A task that will be completed after block has run.
If block returns a BFTask, then the task returned from
this method will not be completed until that task is completed.
*/
- (BFTask *)continueWithBlock:(BFContinuationBlock)block;
/*!
Enqueues the given block to be run once this task is complete.
This method uses a default execution strategy. The block will be
run on the thread where the previous task completes, unless the
the stack depth is too deep, in which case it will be run on a
dispatch queue with default priority.
@param block The block to be run once this task is complete.
@param cancellationToken The cancellation token (optional).
@returns A task that will be completed after block has run.
If block returns a BFTask, then the task returned from
this method will not be completed until that task is completed.
*/
- (BFTask *)continueWithBlock:(BFContinuationBlock)block cancellationToken:(nullable BFCancellationToken *)cancellationToken;
/*!
Enqueues the given block to be run once this task is complete.
@param executor A BFExecutor responsible for determining how the
continuation block will be run.
@param block The block to be run once this task is complete.
@returns A task that will be completed after block has run.
If block returns a BFTask, then the task returned from
this method will not be completed until that task is completed.
*/
- (BFTask *)continueWithExecutor:(BFExecutor *)executor withBlock:(BFContinuationBlock)block;
/*!
Enqueues the given block to be run once this task is complete.
@param executor A BFExecutor responsible for determining how the
continuation block will be run.
@param block The block to be run once this task is complete.
@param cancellationToken The cancellation token (optional).
@returns A task that will be completed after block has run.
If block returns a BFTask, then the task returned from
his method will not be completed until that task is completed.
*/
- (BFTask *)continueWithExecutor:(BFExecutor *)executor
block:(BFContinuationBlock)block
cancellationToken:(nullable BFCancellationToken *)cancellationToken;
/*!
Identical to continueWithBlock:, except that the block is only run
if this task did not produce a cancellation, error, or exception.
If it did, then the failure will be propagated to the returned
task.
@param block The block to be run once this task is complete.
@returns A task that will be completed after block has run.
If block returns a BFTask, then the task returned from
this method will not be completed until that task is completed.
*/
- (BFTask *)continueWithSuccessBlock:(BFContinuationBlock)block;
/*!
Identical to continueWithBlock:, except that the block is only run
if this task did not produce a cancellation, error, or exception.
If it did, then the failure will be propagated to the returned
task.
@param block The block to be run once this task is complete.
@param cancellationToken The cancellation token (optional).
@returns A task that will be completed after block has run.
If block returns a BFTask, then the task returned from
this method will not be completed until that task is completed.
*/
- (BFTask *)continueWithSuccessBlock:(BFContinuationBlock)block cancellationToken:(nullable BFCancellationToken *)cancellationToken;
/*!
Identical to continueWithExecutor:withBlock:, except that the block
is only run if this task did not produce a cancellation, error, or
exception. If it did, then the failure will be propagated to the
returned task.
@param executor A BFExecutor responsible for determining how the
continuation block will be run.
@param block The block to be run once this task is complete.
@returns A task that will be completed after block has run.
If block returns a BFTask, then the task returned from
this method will not be completed until that task is completed.
*/
- (BFTask *)continueWithExecutor:(BFExecutor *)executor withSuccessBlock:(BFContinuationBlock)block;
/*!
Identical to continueWithExecutor:withBlock:, except that the block
is only run if this task did not produce a cancellation, error, or
exception. If it did, then the failure will be propagated to the
returned task.
@param executor A BFExecutor responsible for determining how the
continuation block will be run.
@param block The block to be run once this task is complete.
@param cancellationToken The cancellation token (optional).
@returns A task that will be completed after block has run.
If block returns a BFTask, then the task returned from
this method will not be completed until that task is completed.
*/
- (BFTask *)continueWithExecutor:(BFExecutor *)executor
successBlock:(BFContinuationBlock)block
cancellationToken:(nullable BFCancellationToken *)cancellationToken;
/*!
Waits until this operation is completed.
This method is inefficient and consumes a thread resource while
it's running. It should be avoided. This method logs a warning
message if it is used on the main thread.
*/
- (void)waitUntilFinished;
@end
NS_ASSUME_NONNULL_END
+472
View File
@@ -0,0 +1,472 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "BFTask.h"
#import <libkern/OSAtomic.h>
#import "Bolts.h"
__attribute__ ((noinline)) void warnBlockingOperationOnMainThread() {
NSLog(@"Warning: A long-running operation is being executed on the main thread. \n"
" Break on warnBlockingOperationOnMainThread() to debug.");
}
NSString *const BFTaskErrorDomain = @"bolts";
NSInteger const kBFMultipleErrorsError = 80175001;
NSString *const BFTaskMultipleExceptionsException = @"BFMultipleExceptionsException";
@interface BFTask () {
id _result;
NSError *_error;
NSException *_exception;
}
@property (nonatomic, assign, readwrite, getter=isCancelled) BOOL cancelled;
@property (nonatomic, assign, readwrite, getter=isFaulted) BOOL faulted;
@property (nonatomic, assign, readwrite, getter=isCompleted) BOOL completed;
@property (nonatomic, strong) NSObject *lock;
@property (nonatomic, strong) NSCondition *condition;
@property (nonatomic, strong) NSMutableArray *callbacks;
@end
@implementation BFTask
#pragma mark - Initializer
- (instancetype)init {
self = [super init];
if (!self) return nil;
_lock = [[NSObject alloc] init];
_condition = [[NSCondition alloc] init];
_callbacks = [NSMutableArray array];
return self;
}
- (instancetype)initWithResult:(id)result {
self = [super init];
if (!self) return nil;
[self trySetResult:result];
return self;
}
- (instancetype)initWithError:(NSError *)error {
self = [super init];
if (!self) return nil;
[self trySetError:error];
return self;
}
- (instancetype)initWithException:(NSException *)exception {
self = [super init];
if (!self) return nil;
[self trySetException:exception];
return self;
}
- (instancetype)initCancelled {
self = [super init];
if (!self) return nil;
[self trySetCancelled];
return self;
}
#pragma mark - Task Class methods
+ (instancetype)taskWithResult:(id)result {
return [[self alloc] initWithResult:result];
}
+ (instancetype)taskWithError:(NSError *)error {
return [[self alloc] initWithError:error];
}
+ (instancetype)taskWithException:(NSException *)exception {
return [[self alloc] initWithException:exception];
}
+ (instancetype)cancelledTask {
return [[self alloc] initCancelled];
}
+ (instancetype)taskForCompletionOfAllTasks:(NSArray<BFTask *> *)tasks {
__block int32_t total = (int32_t)tasks.count;
if (total == 0) {
return [self taskWithResult:nil];
}
__block int32_t cancelled = 0;
NSObject *lock = [[NSObject alloc] init];
NSMutableArray *errors = [NSMutableArray array];
NSMutableArray *exceptions = [NSMutableArray array];
BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource];
for (BFTask *task in tasks) {
[task continueWithBlock:^id(BFTask *task) {
if (task.exception) {
@synchronized (lock) {
[exceptions addObject:task.exception];
}
} else if (task.error) {
@synchronized (lock) {
[errors addObject:task.error];
}
} else if (task.cancelled) {
OSAtomicIncrement32(&cancelled);
}
if (OSAtomicDecrement32(&total) == 0) {
if (exceptions.count > 0) {
if (exceptions.count == 1) {
tcs.exception = [exceptions firstObject];
} else {
NSException *exception =
[NSException exceptionWithName:BFTaskMultipleExceptionsException
reason:@"There were multiple exceptions."
userInfo:@{ @"exceptions": exceptions }];
tcs.exception = exception;
}
} else if (errors.count > 0) {
if (errors.count == 1) {
tcs.error = [errors firstObject];
} else {
NSError *error = [NSError errorWithDomain:BFTaskErrorDomain
code:kBFMultipleErrorsError
userInfo:@{ @"errors": errors }];
tcs.error = error;
}
} else if (cancelled > 0) {
[tcs cancel];
} else {
tcs.result = nil;
}
}
return nil;
}];
}
return tcs.task;
}
+ (instancetype)taskForCompletionOfAllTasksWithResults:(NSArray<BFTask *> *)tasks {
return [[self taskForCompletionOfAllTasks:tasks] continueWithSuccessBlock:^id(BFTask *task) {
return [tasks valueForKey:@"result"];
}];
}
+ (instancetype)taskWithDelay:(int)millis {
BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource];
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, millis * NSEC_PER_MSEC);
dispatch_after(popTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
tcs.result = nil;
});
return tcs.task;
}
+ (instancetype)taskWithDelay:(int)millis
cancellationToken:(BFCancellationToken *)token {
if (token.cancellationRequested) {
return [BFTask cancelledTask];
}
BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource];
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, millis * NSEC_PER_MSEC);
dispatch_after(popTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
if (token.cancellationRequested) {
[tcs cancel];
return;
}
tcs.result = nil;
});
return tcs.task;
}
+ (instancetype)taskFromExecutor:(BFExecutor *)executor withBlock:(nullable id (^)())block {
return [[self taskWithResult:nil] continueWithExecutor:executor withBlock:^id(BFTask *task) {
return block();
}];
}
#pragma mark - Custom Setters/Getters
- (id)result {
@synchronized(self.lock) {
return _result;
}
}
- (BOOL)trySetResult:(id)result {
@synchronized(self.lock) {
if (self.completed) {
return NO;
}
self.completed = YES;
_result = result;
[self runContinuations];
return YES;
}
}
- (NSError *)error {
@synchronized(self.lock) {
return _error;
}
}
- (BOOL)trySetError:(NSError *)error {
@synchronized(self.lock) {
if (self.completed) {
return NO;
}
self.completed = YES;
self.faulted = YES;
_error = error;
[self runContinuations];
return YES;
}
}
- (NSException *)exception {
@synchronized(self.lock) {
return _exception;
}
}
- (BOOL)trySetException:(NSException *)exception {
@synchronized(self.lock) {
if (self.completed) {
return NO;
}
self.completed = YES;
self.faulted = YES;
_exception = exception;
[self runContinuations];
return YES;
}
}
- (BOOL)isCancelled {
@synchronized(self.lock) {
return _cancelled;
}
}
- (BOOL)isFaulted {
@synchronized(self.lock) {
return _faulted;
}
}
- (BOOL)trySetCancelled {
@synchronized(self.lock) {
if (self.completed) {
return NO;
}
self.completed = YES;
self.cancelled = YES;
[self runContinuations];
return YES;
}
}
- (BOOL)isCompleted {
@synchronized(self.lock) {
return _completed;
}
}
- (void)setCompleted {
@synchronized(self.lock) {
_completed = YES;
}
}
- (void)runContinuations {
@synchronized(self.lock) {
[self.condition lock];
[self.condition broadcast];
[self.condition unlock];
for (void (^callback)() in self.callbacks) {
callback();
}
[self.callbacks removeAllObjects];
}
}
#pragma mark - Chaining methods
- (BFTask *)continueWithExecutor:(BFExecutor *)executor
withBlock:(BFContinuationBlock)block {
return [self continueWithExecutor:executor block:block cancellationToken:nil];
}
- (BFTask *)continueWithExecutor:(BFExecutor *)executor
block:(BFContinuationBlock)block
cancellationToken:(BFCancellationToken *)cancellationToken {
BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource];
// Capture all of the state that needs to used when the continuation is complete.
void (^wrappedBlock)() = ^() {
[executor execute:^{
if (cancellationToken.cancellationRequested) {
[tcs cancel];
return;
}
id result = nil;
@try {
result = block(self);
} @catch (NSException *exception) {
tcs.exception = exception;
return;
}
if ([result isKindOfClass:[BFTask class]]) {
id (^setupWithTask) (BFTask *) = ^id(BFTask *task) {
if (cancellationToken.cancellationRequested || task.cancelled) {
[tcs cancel];
} else if (task.exception) {
tcs.exception = task.exception;
} else if (task.error) {
tcs.error = task.error;
} else {
tcs.result = task.result;
}
return nil;
};
BFTask *resultTask = (BFTask *)result;
if (resultTask.completed) {
setupWithTask(resultTask);
} else {
[resultTask continueWithBlock:setupWithTask];
}
} else {
tcs.result = result;
}
}];
};
BOOL completed;
@synchronized(self.lock) {
completed = self.completed;
if (!completed) {
[self.callbacks addObject:[wrappedBlock copy]];
}
}
if (completed) {
wrappedBlock();
}
return tcs.task;
}
- (BFTask *)continueWithBlock:(BFContinuationBlock)block {
return [self continueWithExecutor:[BFExecutor defaultExecutor] block:block cancellationToken:nil];
}
- (BFTask *)continueWithBlock:(BFContinuationBlock)block
cancellationToken:(BFCancellationToken *)cancellationToken {
return [self continueWithExecutor:[BFExecutor defaultExecutor] block:block cancellationToken:cancellationToken];
}
- (BFTask *)continueWithExecutor:(BFExecutor *)executor
withSuccessBlock:(BFContinuationBlock)block {
return [self continueWithExecutor:executor successBlock:block cancellationToken:nil];
}
- (BFTask *)continueWithExecutor:(BFExecutor *)executor
successBlock:(BFContinuationBlock)block
cancellationToken:(BFCancellationToken *)cancellationToken {
if (cancellationToken.cancellationRequested) {
return [BFTask cancelledTask];
}
return [self continueWithExecutor:executor block:^id(BFTask *task) {
if (task.faulted || task.cancelled) {
return task;
} else {
return block(task);
}
} cancellationToken:cancellationToken];
}
- (BFTask *)continueWithSuccessBlock:(BFContinuationBlock)block {
return [self continueWithExecutor:[BFExecutor defaultExecutor] successBlock:block cancellationToken:nil];
}
- (BFTask *)continueWithSuccessBlock:(BFContinuationBlock)block
cancellationToken:(BFCancellationToken *)cancellationToken {
return [self continueWithExecutor:[BFExecutor defaultExecutor] successBlock:block cancellationToken:cancellationToken];
}
#pragma mark - Syncing Task (Avoid it)
- (void)warnOperationOnMainThread {
warnBlockingOperationOnMainThread();
}
- (void)waitUntilFinished {
if ([NSThread isMainThread]) {
[self warnOperationOnMainThread];
}
@synchronized(self.lock) {
if (self.completed) {
return;
}
[self.condition lock];
}
[self.condition wait];
[self.condition unlock];
}
#pragma mark - NSObject
- (NSString *)description {
// Acquire the data from the locked properties
BOOL completed;
BOOL cancelled;
BOOL faulted;
NSString *resultDescription = nil;
@synchronized(self.lock) {
completed = self.completed;
cancelled = self.cancelled;
faulted = self.faulted;
resultDescription = completed ? [NSString stringWithFormat:@" result = %@", self.result] : @"";
}
// Description string includes status information and, if available, the
// result since in some ways this is what a promise actually "is".
return [NSString stringWithFormat:@"<%@: %p; completed = %@; cancelled = %@; faulted = %@;%@>",
NSStringFromClass([self class]),
self,
completed ? @"YES" : @"NO",
cancelled ? @"YES" : @"NO",
faulted ? @"YES" : @"NO",
resultDescription];
}
@end
+89
View File
@@ -0,0 +1,89 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@class BFTask<ResultType>;
/*!
A BFTaskCompletionSource represents the producer side of tasks.
It is a task that also has methods for changing the state of the
task by settings its completion values.
*/
@interface BFTaskCompletionSource<__covariant ResultType> : NSObject
/*!
Creates a new unfinished task.
*/
+ (instancetype)taskCompletionSource;
/*!
The task associated with this TaskCompletionSource.
*/
@property (nonatomic, strong, readonly) BFTask<ResultType> *task;
/*!
Completes the task by setting the result.
Attempting to set this for a completed task will raise an exception.
@param result The result of the task.
*/
- (void)setResult:(nullable ResultType)result;
/*!
Completes the task by setting the error.
Attempting to set this for a completed task will raise an exception.
@param error The error for the task.
*/
- (void)setError:(NSError *)error;
/*!
Completes the task by setting an exception.
Attempting to set this for a completed task will raise an exception.
@param exception The exception for the task.
*/
- (void)setException:(NSException *)exception;
/*!
Completes the task by marking it as cancelled.
Attempting to set this for a completed task will raise an exception.
*/
- (void)cancel;
/*!
Sets the result of the task if it wasn't already completed.
@returns whether the new value was set.
*/
- (BOOL)trySetResult:(nullable ResultType)result;
/*!
Sets the error of the task if it wasn't already completed.
@param error The error for the task.
@returns whether the new value was set.
*/
- (BOOL)trySetError:(NSError *)error;
/*!
Sets the exception of the task if it wasn't already completed.
@param exception The exception for the task.
@returns whether the new value was set.
*/
- (BOOL)trySetException:(NSException *)exception;
/*!
Sets the cancellation state of the task if it wasn't already completed.
@returns whether the new value was set.
*/
- (BOOL)trySetCancelled;
@end
NS_ASSUME_NONNULL_END
+93
View File
@@ -0,0 +1,93 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "BFTaskCompletionSource.h"
#import "BFTask.h"
@interface BFTaskCompletionSource ()
@property (nonatomic, strong, readwrite) BFTask *task;
@end
@interface BFTask (BFTaskCompletionSource)
- (BOOL)trySetResult:(id)result;
- (BOOL)trySetError:(NSError *)error;
- (BOOL)trySetException:(NSException *)exception;
- (BOOL)trySetCancelled;
@end
@implementation BFTaskCompletionSource
#pragma mark - Initializer
+ (instancetype)taskCompletionSource {
return [[self alloc] init];
}
- (instancetype)init {
self = [super init];
if (!self) return nil;
_task = [[BFTask alloc] init];
return self;
}
#pragma mark - Custom Setters/Getters
- (void)setResult:(id)result {
if (![self.task trySetResult:result]) {
[NSException raise:NSInternalInconsistencyException
format:@"Cannot set the result on a completed task."];
}
}
- (void)setError:(NSError *)error {
if (![self.task trySetError:error]) {
[NSException raise:NSInternalInconsistencyException
format:@"Cannot set the error on a completed task."];
}
}
- (void)setException:(NSException *)exception {
if (![self.task trySetException:exception]) {
[NSException raise:NSInternalInconsistencyException
format:@"Cannot set the exception on a completed task."];
}
}
- (void)cancel {
if (![self.task trySetCancelled]) {
[NSException raise:NSInternalInconsistencyException
format:@"Cannot cancel a completed task."];
}
}
- (BOOL)trySetResult:(id)result {
return [self.task trySetResult:result];
}
- (BOOL)trySetError:(NSError *)error {
return [self.task trySetError:error];
}
- (BOOL)trySetException:(NSException *)exception {
return [self.task trySetException:exception];
}
- (BOOL)trySetCancelled {
return [self.task trySetCancelled];
}
@end
+43
View File
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Bolts/BoltsVersion.h>
#import <Bolts/BFCancellationToken.h>
#import <Bolts/BFCancellationTokenRegistration.h>
#import <Bolts/BFCancellationTokenSource.h>
#import <Bolts/BFExecutor.h>
#import <Bolts/BFTask.h>
#import <Bolts/BFTaskCompletionSource.h>
#if __has_include(<Bolts/BFAppLink.h>) && TARGET_OS_IPHONE && !TARGET_OS_WATCH && !TARGET_OS_TV
#import <Bolts/BFAppLink.h>
#import <Bolts/BFAppLinkNavigation.h>
#import <Bolts/BFAppLinkResolving.h>
#import <Bolts/BFAppLinkReturnToRefererController.h>
#import <Bolts/BFAppLinkReturnToRefererView.h>
#import <Bolts/BFAppLinkTarget.h>
#import <Bolts/BFMeasurementEvent.h>
#import <Bolts/BFURL.h>
#import <Bolts/BFWebViewAppLinkResolver.h>
#endif
NS_ASSUME_NONNULL_BEGIN
@interface Bolts : NSObject
/*!
Returns the version of the Bolts Framework as an NSString.
@returns The NSString representation of the current version.
*/
+ (NSString *)version;
@end
NS_ASSUME_NONNULL_END
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "Bolts.h"
@implementation Bolts
+ (NSString *)version {
return BOLTS_VERSION;
}
@end
+1
View File
@@ -0,0 +1 @@
#define BOLTS_VERSION @"1.6.0"
+49
View File
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Foundation/Foundation.h>
/*! The version of the App Link protocol that this library supports */
FOUNDATION_EXPORT NSString *const BFAppLinkVersion;
/*!
Contains App Link metadata relevant for navigation on this device
derived from the HTML at a given URL.
*/
@interface BFAppLink : NSObject
/*!
Creates a BFAppLink with the given list of BFAppLinkTargets and target URL.
Generally, this will only be used by implementers of the BFAppLinkResolving protocol,
as these implementers will produce App Link metadata for a given URL.
@param sourceURL the URL from which this App Link is derived
@param targets an ordered list of BFAppLinkTargets for this platform derived
from App Link metadata.
@param webURL the fallback web URL, if any, for the app link.
*/
+ (instancetype)appLinkWithSourceURL:(NSURL *)sourceURL
targets:(NSArray *)targets
webURL:(NSURL *)webURL;
/*! The URL from which this BFAppLink was derived */
@property (nonatomic, strong, readonly) NSURL *sourceURL;
/*!
The ordered list of targets applicable to this platform that will be used
for navigation.
*/
@property (nonatomic, copy, readonly) NSArray *targets;
/*! The fallback web URL to use if no targets are installed on this device. */
@property (nonatomic, strong, readonly) NSURL *webURL;
@end
+62
View File
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "BFAppLink_Internal.h"
NSString *const BFAppLinkDataParameterName = @"al_applink_data";
NSString *const BFAppLinkTargetKeyName = @"target_url";
NSString *const BFAppLinkUserAgentKeyName = @"user_agent";
NSString *const BFAppLinkExtrasKeyName = @"extras";
NSString *const BFAppLinkRefererAppLink = @"referer_app_link";
NSString *const BFAppLinkRefererAppName = @"app_name";
NSString *const BFAppLinkRefererUrl = @"url";
NSString *const BFAppLinkVersionKeyName = @"version";
NSString *const BFAppLinkVersion = @"1.0";
@interface BFAppLink ()
@property (nonatomic, strong, readwrite) NSURL *sourceURL;
@property (nonatomic, copy, readwrite) NSArray *targets;
@property (nonatomic, strong, readwrite) NSURL *webURL;
@property (nonatomic, assign, readwrite, getter=isBackToReferrer) BOOL backToReferrer;
@end
@implementation BFAppLink
+ (instancetype)appLinkWithSourceURL:(NSURL *)sourceURL
targets:(NSArray *)targets
webURL:(NSURL *)webURL
isBackToReferrer:(BOOL)isBackToReferrer {
BFAppLink *link = [[self alloc] initWithIsBackToReferrer:isBackToReferrer];
link.sourceURL = sourceURL;
link.targets = [targets copy];
link.webURL = webURL;
return link;
}
+ (instancetype)appLinkWithSourceURL:(NSURL *)sourceURL
targets:(NSArray *)targets
webURL:(NSURL *)webURL {
return [self appLinkWithSourceURL:sourceURL
targets:targets
webURL:webURL
isBackToReferrer:NO];
}
- (BFAppLink *)initWithIsBackToReferrer:(BOOL)backToReferrer {
if ((self = [super init])) {
_backToReferrer = backToReferrer;
}
return self;
}
@end
+93
View File
@@ -0,0 +1,93 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Foundation/Foundation.h>
#import <Bolts/BFAppLink.h>
/*!
The result of calling navigate on a BFAppLinkNavigation
*/
typedef NS_ENUM(NSInteger, BFAppLinkNavigationType) {
/*! Indicates that the navigation failed and no app was opened */
BFAppLinkNavigationTypeFailure,
/*! Indicates that the navigation succeeded by opening the URL in the browser */
BFAppLinkNavigationTypeBrowser,
/*! Indicates that the navigation succeeded by opening the URL in an app on the device */
BFAppLinkNavigationTypeApp
};
@protocol BFAppLinkResolving;
@class BFTask;
/*!
Represents a pending request to navigate to an App Link. Most developers will
simply use navigateToURLInBackground: to open a URL, but developers can build
custom requests with additional navigation and app data attached to them by
creating BFAppLinkNavigations themselves.
*/
@interface BFAppLinkNavigation : NSObject
/*!
The extras for the AppLinkNavigation. This will generally contain application-specific
data that should be passed along with the request, such as advertiser or affiliate IDs or
other such metadata relevant on this device.
*/
@property (nonatomic, copy, readonly) NSDictionary *extras;
/*!
The al_applink_data for the AppLinkNavigation. This will generally contain data common to
navigation attempts such as back-links, user agents, and other information that may be used
in routing and handling an App Link request.
*/
@property (nonatomic, copy, readonly) NSDictionary *appLinkData;
/*! The AppLink to navigate to */
@property (nonatomic, strong, readonly) BFAppLink *appLink;
/*! Creates an AppLinkNavigation with the given link, extras, and App Link data */
+ (instancetype)navigationWithAppLink:(BFAppLink *)appLink
extras:(NSDictionary *)extras
appLinkData:(NSDictionary *)appLinkData;
/*! Performs the navigation */
- (BFAppLinkNavigationType)navigate:(NSError **)error;
/*! Returns a BFAppLink for the given URL */
+ (BFTask *)resolveAppLinkInBackground:(NSURL *)destination;
/*! Returns a BFAppLink for the given URL using the given App Link resolution strategy */
+ (BFTask *)resolveAppLinkInBackground:(NSURL *)destination resolver:(id<BFAppLinkResolving>)resolver;
/*! Navigates to a BFAppLink and returns whether it opened in-app or in-browser */
+ (BFAppLinkNavigationType)navigateToAppLink:(BFAppLink *)link error:(NSError **)error;
/*! Navigates to a URL (an asynchronous action) and returns a BFNavigationType */
+ (BFTask *)navigateToURLInBackground:(NSURL *)destination;
/*!
Navigates to a URL (an asynchronous action) using the given App Link resolution
strategy and returns a BFNavigationType
*/
+ (BFTask *)navigateToURLInBackground:(NSURL *)destination resolver:(id<BFAppLinkResolving>)resolver;
/*!
Gets the default resolver to be used for App Link resolution. If the developer has not set one explicitly,
a basic, built-in resolver will be used.
*/
+ (id<BFAppLinkResolving>)defaultResolver;
/*!
Sets the default resolver to be used for App Link resolution. Setting this to nil will revert the
default resolver to the basic, built-in resolver provided by Bolts.
*/
+ (void)setDefaultResolver:(id<BFAppLinkResolving>)resolver;
@end
+248
View File
@@ -0,0 +1,248 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <UIKit/UIKit.h>
#import "BFAppLinkNavigation.h"
#import "BFTaskCompletionSource.h"
#import "BFAppLinkTarget.h"
#import "BoltsVersion.h"
#import "BFWebViewAppLinkResolver.h"
#import "BFExecutor.h"
#import "BFTask.h"
#import "BFMeasurementEvent_Internal.h"
#import "BFAppLink_Internal.h"
FOUNDATION_EXPORT NSString *const BFAppLinkDataParameterName;
FOUNDATION_EXPORT NSString *const BFAppLinkTargetKeyName;
FOUNDATION_EXPORT NSString *const BFAppLinkUserAgentKeyName;
FOUNDATION_EXPORT NSString *const BFAppLinkExtrasKeyName;
FOUNDATION_EXPORT NSString *const BFAppLinkVersionKeyName;
static id<BFAppLinkResolving> defaultResolver;
@interface BFAppLinkNavigation ()
@property (nonatomic, copy, readwrite) NSDictionary *extras;
@property (nonatomic, copy, readwrite) NSDictionary *appLinkData;
@property (nonatomic, strong, readwrite) BFAppLink *appLink;
@end
@implementation BFAppLinkNavigation
+ (instancetype)navigationWithAppLink:(BFAppLink *)appLink
extras:(NSDictionary *)extras
appLinkData:(NSDictionary *)appLinkData {
BFAppLinkNavigation *navigation = [[self alloc] init];
navigation.appLink = appLink;
navigation.extras = extras;
navigation.appLinkData = appLinkData;
return navigation;
}
- (NSString *)stringByEscapingQueryString:(NSString *)string {
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0 || __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_9
return [string stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
#else
return (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(NULL,
(CFStringRef)string,
NULL,
(CFStringRef) @":/?#[]@!$&'()*+,;=",
kCFStringEncodingUTF8));
#endif
}
- (NSURL *)appLinkURLWithTargetURL:(NSURL *)targetUrl error:(NSError **)error {
NSMutableDictionary *appLinkData = [NSMutableDictionary dictionaryWithDictionary:self.appLinkData ?: @{}];
// Add applink protocol data
if (!appLinkData[BFAppLinkUserAgentKeyName]) {
appLinkData[BFAppLinkUserAgentKeyName] = [NSString stringWithFormat:@"Bolts iOS %@", BOLTS_VERSION];
}
if (!appLinkData[BFAppLinkVersionKeyName]) {
appLinkData[BFAppLinkVersionKeyName] = BFAppLinkVersion;
}
appLinkData[BFAppLinkTargetKeyName] = [self.appLink.sourceURL absoluteString];
appLinkData[BFAppLinkExtrasKeyName] = self.extras ?: @{};
// JSON-ify the applink data
NSError *jsonError = nil;
NSData *jsonBlob = [NSJSONSerialization dataWithJSONObject:appLinkData options:0 error:&jsonError];
if (!jsonError) {
NSString *jsonString = [[NSString alloc] initWithData:jsonBlob encoding:NSUTF8StringEncoding];
NSString *encoded = [self stringByEscapingQueryString:jsonString];
NSString *endUrlString = [NSString stringWithFormat:@"%@%@%@=%@",
[targetUrl absoluteString],
targetUrl.query ? @"&" : @"?",
BFAppLinkDataParameterName,
encoded];
return [NSURL URLWithString:endUrlString];
} else {
if (error) {
*error = jsonError;
}
// If there was an error encoding the app link data, fail hard.
return nil;
}
}
- (BFAppLinkNavigationType)navigate:(NSError **)error {
NSURL *openedURL = nil;
NSError *encodingError = nil;
BFAppLinkNavigationType retType = BFAppLinkNavigationTypeFailure;
// Find the first eligible/launchable target in the BFAppLink.
for (BFAppLinkTarget *target in self.appLink.targets) {
NSURL *appLinkAppURL = [self appLinkURLWithTargetURL:target.URL error:&encodingError];
if (encodingError || !appLinkAppURL) {
if (error) {
*error = encodingError;
}
} else if ([[UIApplication sharedApplication] openURL:appLinkAppURL]) {
retType = BFAppLinkNavigationTypeApp;
openedURL = appLinkAppURL;
break;
}
}
if (!openedURL && self.appLink.webURL) {
// Fall back to opening the url in the browser if available.
NSURL *appLinkBrowserURL = [self appLinkURLWithTargetURL:self.appLink.webURL error:&encodingError];
if (encodingError || !appLinkBrowserURL) {
// If there was an error encoding the app link data, fail hard.
if (error) {
*error = encodingError;
}
} else if ([[UIApplication sharedApplication] openURL:appLinkBrowserURL]) {
// This was a browser navigation.
retType = BFAppLinkNavigationTypeBrowser;
openedURL = appLinkBrowserURL;
}
}
[self postAppLinkNavigateEventNotificationWithTargetURL:openedURL
error:error ? *error : nil
type:retType];
return retType;
}
- (void)postAppLinkNavigateEventNotificationWithTargetURL:(NSURL *)outputURL error:(NSError *)error type:(BFAppLinkNavigationType)type {
NSString *const EVENT_YES_VAL = @"1";
NSString *const EVENT_NO_VAL = @"0";
NSMutableDictionary *logData = [[NSMutableDictionary alloc] init];
NSString *outputURLScheme = [outputURL scheme];
NSString *outputURLString = [outputURL absoluteString];
if (outputURLScheme) {
logData[@"outputURLScheme"] = outputURLScheme;
}
if (outputURLString) {
logData[@"outputURL"] = outputURLString;
}
NSString *sourceURLString = [self.appLink.sourceURL absoluteString];
NSString *sourceURLHost = [self.appLink.sourceURL host];
NSString *sourceURLScheme = [self.appLink.sourceURL scheme];
if (sourceURLString) {
logData[@"sourceURL"] = sourceURLString;
}
if (sourceURLHost) {
logData[@"sourceHost"] = sourceURLHost;
}
if (sourceURLScheme) {
logData[@"sourceScheme"] = sourceURLScheme;
}
if ([error localizedDescription]) {
logData[@"error"] = [error localizedDescription];
}
NSString *success = nil; //no
NSString *linkType = nil; // unknown;
switch (type) {
case BFAppLinkNavigationTypeFailure:
success = EVENT_NO_VAL;
linkType = @"fail";
break;
case BFAppLinkNavigationTypeBrowser:
success = EVENT_YES_VAL;
linkType = @"web";
break;
case BFAppLinkNavigationTypeApp:
success = EVENT_YES_VAL;
linkType = @"app";
break;
default:
break;
}
if (success) {
logData[@"success"] = success;
}
if (linkType) {
logData[@"type"] = linkType;
}
if ([self.appLink isBackToReferrer]) {
[BFMeasurementEvent postNotificationForEventName:BFAppLinkNavigateBackToReferrerEventName args:logData];
} else {
[BFMeasurementEvent postNotificationForEventName:BFAppLinkNavigateOutEventName args:logData];
}
}
+ (BFTask *)resolveAppLinkInBackground:(NSURL *)destination resolver:(id<BFAppLinkResolving>)resolver {
return [resolver appLinkFromURLInBackground:destination];
}
+ (BFTask *)resolveAppLinkInBackground:(NSURL *)destination {
return [self resolveAppLinkInBackground:destination resolver:[self defaultResolver]];
}
+ (BFTask *)navigateToURLInBackground:(NSURL *)destination {
return [self navigateToURLInBackground:destination
resolver:[self defaultResolver]];
}
+ (BFTask *)navigateToURLInBackground:(NSURL *)destination
resolver:(id<BFAppLinkResolving>)resolver {
BFTask *resolutionTask = [self resolveAppLinkInBackground:destination
resolver:resolver];
return [resolutionTask continueWithExecutor:[BFExecutor mainThreadExecutor]
withSuccessBlock:^id(BFTask *task) {
NSError *error = nil;
BFAppLinkNavigationType result = [self navigateToAppLink:task.result
error:&error];
if (error) {
return [BFTask taskWithError:error];
} else {
return @(result);
}
}];
}
+ (BFAppLinkNavigationType)navigateToAppLink:(BFAppLink *)link error:(NSError **)error {
return [[BFAppLinkNavigation navigationWithAppLink:link
extras:nil
appLinkData:nil] navigate:error];
}
+ (id<BFAppLinkResolving>)defaultResolver {
if (defaultResolver) {
return defaultResolver;
}
return [BFWebViewAppLinkResolver sharedInstance];
}
+ (void)setDefaultResolver:(id<BFAppLinkResolving>)resolver {
defaultResolver = resolver;
}
@end
+30
View File
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Foundation/Foundation.h>
@class BFTask;
/*!
Implement this protocol to provide an alternate strategy for resolving
App Links that may include pre-fetching, caching, or querying for App Link
data from an index provided by a service provider.
*/
@protocol BFAppLinkResolving <NSObject>
/*!
Asynchronously resolves App Link data for a given URL.
@param url The URL to resolve into an App Link.
@returns A BFTask that will return a BFAppLink for the given URL.
*/
- (BFTask *)appLinkFromURLInBackground:(NSURL *)url;
@end
@@ -0,0 +1,87 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <Bolts/BFAppLinkReturnToRefererView.h>
@class BFAppLink;
@class BFAppLinkReturnToRefererController;
/*!
Protocol that a class can implement in order to be notified when the user has navigated back
to the referer of an App Link.
*/
@protocol BFAppLinkReturnToRefererControllerDelegate <NSObject>
@optional
/*! Called when the user has tapped to navigate, but before the navigation has been performed. */
- (void)returnToRefererController:(BFAppLinkReturnToRefererController *)controller
willNavigateToAppLink:(BFAppLink *)appLink;
/*! Called after the navigation has been attempted, with an indication of whether the referer
app link was successfully opened. */
- (void)returnToRefererController:(BFAppLinkReturnToRefererController *)controller
didNavigateToAppLink:(BFAppLink *)url
type:(BFAppLinkNavigationType)type;
@end
/*!
A controller class that implements default behavior for a BFAppLinkReturnToRefererView, including
the ability to display the view above the navigation bar for navigation-based apps.
*/
@interface BFAppLinkReturnToRefererController : NSObject <BFAppLinkReturnToRefererViewDelegate>
/*!
The delegate that will be notified when the user navigates back to the referer.
*/
@property (nonatomic, weak) id<BFAppLinkReturnToRefererControllerDelegate> delegate;
/*!
The BFAppLinkReturnToRefererView this controller is controlling.
*/
@property (nonatomic, strong) BFAppLinkReturnToRefererView *view;
/*!
Initializes a controller suitable for controlling a BFAppLinkReturnToRefererView that is to be displayed
contained within another UIView (i.e., not displayed above the navigation bar).
*/
- (instancetype)init;
/*!
Initializes a controller suitable for controlling a BFAppLinkReturnToRefererView that is to be displayed
displayed above the navigation bar.
*/
- (instancetype)initForDisplayAboveNavController:(UINavigationController *)navController;
/*!
Removes the view entirely from the navigation controller it is currently displayed in.
*/
- (void)removeFromNavController;
/*!
Shows the BFAppLinkReturnToRefererView with the specified referer information. If nil or missing data,
the view will not be displayed. */
- (void)showViewForRefererAppLink:(BFAppLink *)refererAppLink;
/*!
Shows the BFAppLinkReturnToRefererView with referer information extracted from the specified URL.
If nil or missing referer App Link data, the view will not be displayed. */
- (void)showViewForRefererURL:(NSURL *)url;
/*!
Closes the view, possibly animating it.
*/
- (void)closeViewAnimated:(BOOL)animated;
@end
@@ -0,0 +1,230 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "BFAppLinkReturnToRefererController.h"
#import "BFAppLink.h"
#import "BFAppLinkReturnToRefererView_Internal.h"
#import "BFURL_Internal.h"
static const CFTimeInterval kBFViewAnimationDuration = 0.25f;
@implementation BFAppLinkReturnToRefererController {
UINavigationController *_navigationController;
BFAppLinkReturnToRefererView *_view;
}
#pragma mark - Object lifecycle
- (instancetype)init {
return [self initForDisplayAboveNavController:nil];
}
- (instancetype)initForDisplayAboveNavController:(UINavigationController *)navController {
self = [super init];
if (self) {
_navigationController = navController;
if (_navigationController != nil) {
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:@selector(statusBarFrameWillChange:)
name:UIApplicationWillChangeStatusBarFrameNotification
object:nil];
[nc addObserver:self
selector:@selector(statusBarFrameDidChange:)
name:UIApplicationDidChangeStatusBarFrameNotification
object:nil];
[nc addObserver:self
selector:@selector(orientationDidChange:)
name:UIDeviceOrientationDidChangeNotification
object:nil];
}
}
return self;
}
- (void)dealloc {
_view.delegate = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Public API
- (BFAppLinkReturnToRefererView *)view {
if (!_view) {
self.view = [[BFAppLinkReturnToRefererView alloc] initWithFrame:CGRectZero];
if (_navigationController) {
[_navigationController.view addSubview:_view];
}
}
return _view;
}
- (void)setView:(BFAppLinkReturnToRefererView *)view {
if (_view != view) {
_view.delegate = nil;
}
_view = view;
_view.delegate = self;
if (_navigationController) {
_view.includeStatusBarInSize = BFIncludeStatusBarInSizeAlways;
}
}
- (void)showViewForRefererAppLink:(BFAppLink *)refererAppLink {
self.view.refererAppLink = refererAppLink;
[_view sizeToFit];
if (_navigationController) {
if (!_view.closed) {
dispatch_async(dispatch_get_main_queue(), ^{
[self moveNavigationBar];
});
}
}
}
- (void)showViewForRefererURL:(NSURL *)url {
BFAppLink *appLink = [BFURL URLForRenderBackToReferrerBarURL:url].appLinkReferer;
[self showViewForRefererAppLink:appLink];
}
- (void)removeFromNavController {
if (_navigationController) {
[_view removeFromSuperview];
_navigationController = nil;
}
}
#pragma mark - BFAppLinkReturnToRefererViewDelegate
- (void)returnToRefererViewDidTapInsideCloseButton:(BFAppLinkReturnToRefererView *)view {
[self closeViewAnimated:YES explicitlyClosed:YES];
}
- (void)returnToRefererViewDidTapInsideLink:(BFAppLinkReturnToRefererView *)view
link:(BFAppLink *)link {
[self openRefererAppLink:link];
[self closeViewAnimated:NO explicitlyClosed:NO];
}
#pragma mark - Private
- (void)statusBarFrameWillChange:(NSNotification *)notification {
NSValue *rectValue = [[notification userInfo] valueForKey:UIApplicationStatusBarFrameUserInfoKey];
CGRect newFrame;
[rectValue getValue:&newFrame];
if (_navigationController && !_view.closed) {
if (CGRectGetHeight(newFrame) == 40) {
UIViewAnimationOptions options = UIViewAnimationOptionBeginFromCurrentState;
[UIView animateWithDuration:kBFViewAnimationDuration delay:0.0 options:options animations:^{
_view.frame = CGRectMake(0.0, 0.0, CGRectGetWidth(_view.bounds), 0.0);
} completion:nil];
}
}
}
- (void)statusBarFrameDidChange:(NSNotification *)notification {
NSValue *rectValue = [[notification userInfo] valueForKey:UIApplicationStatusBarFrameUserInfoKey];
CGRect newFrame;
[rectValue getValue:&newFrame];
if (_navigationController && !_view.closed) {
if (CGRectGetHeight(newFrame) == 40) {
UIViewAnimationOptions options = UIViewAnimationOptionBeginFromCurrentState;
[UIView animateWithDuration:kBFViewAnimationDuration delay:0.0 options:options animations:^{
[_view sizeToFit];
[self moveNavigationBar];
} completion:nil];
}
}
}
- (void)orientationDidChange:(NSNotificationCenter *)notification {
if (_navigationController && !_view.closed && CGRectGetHeight(_view.bounds) > 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[self moveNavigationBar];
});
}
}
- (void)moveNavigationBar {
if (_view.closed || !_view.refererAppLink) {
return;
}
[self updateNavigationBarY:CGRectGetHeight(_view.bounds)];
}
- (void)updateNavigationBarY:(CGFloat)y {
UINavigationBar *navigationBar = _navigationController.navigationBar;
CGRect navigationBarFrame = navigationBar.frame;
CGFloat oldContainerViewY = CGRectGetMaxY(navigationBarFrame);
navigationBarFrame.origin.y = y;
navigationBar.frame = navigationBarFrame;
CGFloat dy = CGRectGetMaxY(navigationBarFrame) - oldContainerViewY;
UIView *containerView = _navigationController.visibleViewController.view.superview;
containerView.frame = UIEdgeInsetsInsetRect(containerView.frame, UIEdgeInsetsMake(dy, 0.0, 0.0, 0.0));
}
- (void)closeViewAnimated:(BOOL)animated {
[self closeViewAnimated:animated explicitlyClosed:YES];
}
- (void)closeViewAnimated:(BOOL)animated explicitlyClosed:(BOOL)explicitlyClosed {
void (^closer)(void) = ^{
if (_navigationController) {
[self updateNavigationBarY:_view.statusBarHeight];
}
CGRect frame = _view.frame;
frame.size.height = 0.0;
_view.frame = frame;
};
if (animated) {
[UIView animateWithDuration:kBFViewAnimationDuration animations:^{
closer();
} completion:^(BOOL finished) {
if (explicitlyClosed) {
_view.closed = YES;
}
}];
} else {
closer();
if (explicitlyClosed) {
_view.closed = YES;
}
}
}
- (void)openRefererAppLink:(BFAppLink *)refererAppLink {
if (refererAppLink) {
id<BFAppLinkReturnToRefererControllerDelegate> delegate = _delegate;
if ([delegate respondsToSelector:@selector(returnToRefererController:willNavigateToAppLink:)]) {
[delegate returnToRefererController:self willNavigateToAppLink:refererAppLink];
}
NSError *error = nil;
BFAppLinkNavigationType type = [BFAppLinkNavigation navigateToAppLink:refererAppLink error:&error];
if ([delegate respondsToSelector:@selector(returnToRefererController:didNavigateToAppLink:type:)]) {
[delegate returnToRefererController:self didNavigateToAppLink:refererAppLink type:type];
}
}
}
@end
@@ -0,0 +1,77 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <Bolts/BFAppLinkNavigation.h>
@class BFAppLinkReturnToRefererView;
@class BFURL;
typedef NS_ENUM(NSUInteger, BFIncludeStatusBarInSize) {
BFIncludeStatusBarInSizeNever,
BFIncludeStatusBarInSizeIOS7AndLater,
BFIncludeStatusBarInSizeAlways,
};
/*!
Protocol that a class can implement in order to be notified when the user has navigated back
to the referer of an App Link.
*/
@protocol BFAppLinkReturnToRefererViewDelegate <NSObject>
/*!
Called when the user has tapped inside the close button.
*/
- (void)returnToRefererViewDidTapInsideCloseButton:(BFAppLinkReturnToRefererView *)view;
/*!
Called when the user has tapped inside the App Link portion of the view.
*/
- (void)returnToRefererViewDidTapInsideLink:(BFAppLinkReturnToRefererView *)view
link:(BFAppLink *)link;
@end
/*!
Provides a UIView that displays a button allowing users to navigate back to the
application that launched the App Link currently being handled, if the App Link
contained referer data. The user can also close the view by clicking a close button
rather than navigating away. If the view is provided an App Link that does not contain
referer data, it will have zero size and no UI will be displayed.
*/
@interface BFAppLinkReturnToRefererView : UIView
/*!
The delegate that will be notified when the user navigates back to the referer.
*/
@property (nonatomic, weak) id<BFAppLinkReturnToRefererViewDelegate> delegate;
/*!
The color of the text label and close button.
*/
@property (nonatomic, strong) UIColor *textColor;
@property (nonatomic, strong) BFAppLink *refererAppLink;
/*!
Indicates whether to extend the size of the view to include the current status bar
size, for use in scenarios where the view might extend under the status bar on iOS 7 and
above; this property has no effect on earlier versions of iOS.
*/
@property (nonatomic, assign) BFIncludeStatusBarInSize includeStatusBarInSize;
/*!
Indicates whether the user has closed the view by clicking the close button.
*/
@property (nonatomic, assign) BOOL closed;
@end
@@ -0,0 +1,269 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "BFAppLinkReturnToRefererView.h"
#import "BFAppLink.h"
#import "BFAppLinkTarget.h"
static const CGFloat BFMarginX = 8.5f;
static const CGFloat BFMarginY = 8.5f;
static NSString *const BFRefererAppLink = @"referer_app_link";
static NSString *const BFRefererAppName = @"app_name";
static NSString *const BFRefererUrl = @"url";
static const CGFloat BFCloseButtonWidth = 12.0;
static const CGFloat BFCloseButtonHeight = 12.0;
@interface BFAppLinkReturnToRefererView ()
@property (nonatomic, strong) UILabel *labelView;
@property (nonatomic, strong) UIButton *closeButton;
@property (nonatomic, strong) UITapGestureRecognizer *insideTapGestureRecognizer;
@property (nonatomic, strong) UIView *viewToMoveWithNavController;
@end
@implementation BFAppLinkReturnToRefererView {
BOOL _explicitlyHidden;
}
#pragma mark - Initialization
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self commonInit];
[self sizeToFit];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self commonInit];
}
return self;
}
- (void)commonInit {
// Initialization code
_includeStatusBarInSize = BFIncludeStatusBarInSizeIOS7AndLater;
// iOS 7 system blue color
self.backgroundColor = [UIColor colorWithRed:0.0f green:122.0f / 255.0f blue:1.0f alpha:1.0f];
self.textColor = [UIColor whiteColor];
self.clipsToBounds = YES;
[self initViews];
}
- (void)initViews {
if (!_labelView && !_closeButton) {
_closeButton = [UIButton buttonWithType:UIButtonTypeCustom];
_closeButton.backgroundColor = [UIColor clearColor];
_closeButton.userInteractionEnabled = YES;
_closeButton.clipsToBounds = YES;
_closeButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin;
_closeButton.contentMode = UIViewContentModeCenter;
[_closeButton addTarget:self action:@selector(closeButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:_closeButton];
_labelView = [[UILabel alloc] initWithFrame:CGRectZero];
_labelView.font = [UIFont systemFontOfSize:[UIFont smallSystemFontSize]];
_labelView.textColor = [UIColor whiteColor];
_labelView.backgroundColor = [UIColor clearColor];
#ifdef __IPHONE_6_0
_labelView.textAlignment = NSTextAlignmentCenter;
#else
_labelView.textAlignment = UITextAlignmentCenter;
#endif
_labelView.clipsToBounds = YES;
[self updateLabelText];
[self addSubview:_labelView];
_insideTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapInside:)];
_labelView.userInteractionEnabled = YES;
[_labelView addGestureRecognizer:_insideTapGestureRecognizer];
[self updateColors];
}
}
#pragma mark - Layout
- (CGSize)intrinsicContentSize {
CGSize size = self.bounds.size;
if (_closed || !self.hasRefererData) {
size.height = 0.0;
} else {
CGSize labelSize = [_labelView sizeThatFits:size];
size = CGSizeMake(size.width, labelSize.height + 2 * BFMarginY + self.statusBarHeight);
}
return size;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGRect bounds = self.bounds;
_labelView.preferredMaxLayoutWidth = _labelView.bounds.size.width;
CGSize labelSize = [_labelView sizeThatFits:bounds.size];
_labelView.frame = CGRectMake(BFMarginX,
CGRectGetMaxY(bounds) - labelSize.height - 1.5f * BFMarginY,
CGRectGetMaxX(bounds) - BFCloseButtonWidth - 3 * BFMarginX,
labelSize.height + BFMarginY);
_closeButton.frame = CGRectMake(CGRectGetMaxX(bounds) - BFCloseButtonWidth - 2 * BFMarginX,
_labelView.center.y - BFCloseButtonHeight / 2.0f - BFMarginY,
BFCloseButtonWidth + 2 * BFMarginX,
BFCloseButtonHeight + 2 * BFMarginY);
}
- (CGSize)sizeThatFits:(CGSize)size {
if (_closed || !self.hasRefererData) {
size = CGSizeMake(size.width, 0.0);
} else {
CGSize labelSize = [_labelView sizeThatFits:size];
size = CGSizeMake(size.width, labelSize.height + 2 * BFMarginY + self.statusBarHeight);
}
return size;
}
- (CGFloat)statusBarHeight {
UIApplication *application = [UIApplication sharedApplication];
BOOL include;
switch (_includeStatusBarInSize) {
case BFIncludeStatusBarInSizeAlways:
include = YES;
break;
case BFIncludeStatusBarInSizeIOS7AndLater: {
float systemVersion = [[[UIDevice currentDevice] systemVersion] floatValue];
include = (systemVersion >= 7.0);
break;
}
case BFIncludeStatusBarInSizeNever:
include = NO;
break;
}
if (include && !application.statusBarHidden) {
BOOL landscape = UIInterfaceOrientationIsLandscape(application.statusBarOrientation);
CGRect statusBarFrame = application.statusBarFrame;
return landscape ? CGRectGetWidth(statusBarFrame) : CGRectGetHeight(statusBarFrame);
}
return 0;
}
#pragma mark - Public API
- (void)setIncludeStatusBarInSize:(BFIncludeStatusBarInSize)includeStatusBarInSize {
_includeStatusBarInSize = includeStatusBarInSize;
[self setNeedsLayout];
[self invalidateIntrinsicContentSize];
}
- (void)setTextColor:(UIColor *)textColor {
_textColor = textColor;
[self updateColors];
}
- (void)setRefererAppLink:(BFAppLink *)refererAppLink {
_refererAppLink = refererAppLink;
[self updateLabelText];
[self updateHidden];
[self invalidateIntrinsicContentSize];
}
- (void)setClosed:(BOOL)closed {
if (_closed != closed) {
_closed = closed;
[self updateHidden];
[self invalidateIntrinsicContentSize];
}
}
- (void)setHidden:(BOOL)hidden {
_explicitlyHidden = hidden;
[self updateHidden];
}
#pragma mark - Private
- (void)updateLabelText {
NSString *appName = (_refererAppLink && _refererAppLink.targets[0]) ? [_refererAppLink.targets[0] appName] : nil;
_labelView.text = [self localizedLabelForReferer:appName];
}
- (void)updateColors {
UIImage *closeButtonImage = [self drawCloseButtonImageWithColor:_textColor];
_labelView.textColor = _textColor;
[_closeButton setImage:closeButtonImage forState:UIControlStateNormal];
}
- (UIImage *)drawCloseButtonImageWithColor:(UIColor *)color {
UIGraphicsBeginImageContextWithOptions(CGSizeMake(BFCloseButtonWidth, BFCloseButtonHeight), NO, 0.0f);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [color CGColor]);
CGContextSetFillColorWithColor(context, [color CGColor]);
CGContextSetLineWidth(context, 1.25f);
CGFloat inset = 0.5f;
CGContextMoveToPoint(context, inset, inset);
CGContextAddLineToPoint(context, BFCloseButtonWidth - inset, BFCloseButtonHeight - inset);
CGContextStrokePath(context);
CGContextMoveToPoint(context, BFCloseButtonWidth - inset, inset);
CGContextAddLineToPoint(context, inset, BFCloseButtonHeight - inset);
CGContextStrokePath(context);
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
}
- (NSString *)localizedLabelForReferer:(NSString *)refererName {
if (!refererName) {
return nil;
}
NSString *format = NSLocalizedString(@"Touch to return to %1$@", @"Format for the string to return to a calling app.");
return [NSString stringWithFormat:format, refererName];
}
- (BOOL)hasRefererData {
return _refererAppLink && _refererAppLink.targets[0];
}
- (void)closeButtonTapped:(id)sender {
[_delegate returnToRefererViewDidTapInsideCloseButton:self];
}
- (void)onTapInside:(UIGestureRecognizer *)sender {
[_delegate returnToRefererViewDidTapInsideLink:self link:_refererAppLink];
}
- (void)updateHidden {
[super setHidden:_explicitlyHidden || _closed || !self.hasRefererData];
}
@end
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Bolts/BFAppLinkReturnToRefererView.h>
@interface BFAppLinkReturnToRefererView (Internal)
- (CGFloat)statusBarHeight;
@end
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Foundation/Foundation.h>
/*!
Represents a target defined in App Link metadata, consisting of at least
a URL, and optionally an App Store ID and name.
*/
@interface BFAppLinkTarget : NSObject
/*! Creates a BFAppLinkTarget with the given app site and target URL. */
+ (instancetype)appLinkTargetWithURL:(NSURL *)url
appStoreId:(NSString *)appStoreId
appName:(NSString *)appName;
/*! The URL prefix for this app link target */
@property (nonatomic, strong, readonly) NSURL *URL;
/*! The app ID for the app store */
@property (nonatomic, copy, readonly) NSString *appStoreId;
/*! The name of the app */
@property (nonatomic, copy, readonly) NSString *appName;
@end
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "BFAppLinkTarget.h"
@interface BFAppLinkTarget ()
@property (nonatomic, strong, readwrite) NSURL *URL;
@property (nonatomic, copy, readwrite) NSString *appStoreId;
@property (nonatomic, copy, readwrite) NSString *appName;
@end
@implementation BFAppLinkTarget
+ (instancetype)appLinkTargetWithURL:(NSURL *)url
appStoreId:(NSString *)appStoreId
appName:(NSString *)appName {
BFAppLinkTarget *target = [[self alloc] init];
target.URL = url;
target.appStoreId = appStoreId;
target.appName = appName;
return target;
}
@end
+32
View File
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Bolts/BFAppLink.h>
FOUNDATION_EXPORT NSString *const BFAppLinkDataParameterName;
FOUNDATION_EXPORT NSString *const BFAppLinkTargetKeyName;
FOUNDATION_EXPORT NSString *const BFAppLinkUserAgentKeyName;
FOUNDATION_EXPORT NSString *const BFAppLinkExtrasKeyName;
FOUNDATION_EXPORT NSString *const BFAppLinkVersionKeyName;
FOUNDATION_EXPORT NSString *const BFAppLinkRefererAppLink;
FOUNDATION_EXPORT NSString *const BFAppLinkRefererAppName;
FOUNDATION_EXPORT NSString *const BFAppLinkRefererUrl;
@interface BFAppLink (Internal)
+ (instancetype)appLinkWithSourceURL:(NSURL *)sourceURL
targets:(NSArray *)targets
webURL:(NSURL *)webURL
isBackToReferrer:(BOOL)isBackToReferrer;
/*! return if this AppLink is to go back to referrer. */
@property (nonatomic, assign, readonly, getter=isBackToReferrer) BOOL backToReferrer;
@end
+45
View File
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Foundation/Foundation.h>
/*! The name of the notification posted by BFMeasurementEvent */
FOUNDATION_EXPORT NSString *const BFMeasurementEventNotificationName;
/*! Defines keys in the userInfo object for the notification named BFMeasurementEventNotificationName */
/*! The string field for the name of the event */
FOUNDATION_EXPORT NSString *const BFMeasurementEventNameKey;
/*! The dictionary field for the arguments of the event */
FOUNDATION_EXPORT NSString *const BFMeasurementEventArgsKey;
/*! Bolts Events raised by BFMeasurementEvent for Applink */
/*!
The name of the event posted when [BFURL URLWithURL:] is called successfully. This represents the successful parsing of an app link URL.
*/
FOUNDATION_EXPORT NSString *const BFAppLinkParseEventName;
/*!
The name of the event posted when [BFURL URLWithInboundURL:] is called successfully.
This represents parsing an inbound app link URL from a different application
*/
FOUNDATION_EXPORT NSString *const BFAppLinkNavigateInEventName;
/*! The event raised when the user navigates from your app to other apps */
FOUNDATION_EXPORT NSString *const BFAppLinkNavigateOutEventName;
/*!
The event raised when the user navigates out from your app and back to the referrer app.
e.g when the user leaves your app after tapping the back-to-referrer navigation bar
*/
FOUNDATION_EXPORT NSString *const BFAppLinkNavigateBackToReferrerEventName;
@interface BFMeasurementEvent : NSObject
@end
+62
View File
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "BFMeasurementEvent_Internal.h"
NSString *const BFMeasurementEventNotificationName = @"com.parse.bolts.measurement_event";
NSString *const BFMeasurementEventNameKey = @"event_name";
NSString *const BFMeasurementEventArgsKey = @"event_args";
/* app Link Event raised by this BFURL */
NSString *const BFAppLinkParseEventName = @"al_link_parse";
NSString *const BFAppLinkNavigateInEventName = @"al_nav_in";
/*! AppLink events raised in this class */
NSString *const BFAppLinkNavigateOutEventName = @"al_nav_out";
NSString *const BFAppLinkNavigateBackToReferrerEventName = @"al_ref_back_out";
__attribute__((noinline)) void warnOnMissingEventName() {
NSLog(@"Warning: Missing event name when logging bolts measurement event. \n"
" Ignoring this event in logging.");
}
@implementation BFMeasurementEvent {
NSString *_name;
NSDictionary *_args;
}
- (void)postNotification {
if (!_name) {
warnOnMissingEventName();
return;
}
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
NSDictionary *userInfo = @{BFMeasurementEventNameKey : _name,
BFMeasurementEventArgsKey : _args};
[center postNotificationName:BFMeasurementEventNotificationName
object:self
userInfo:userInfo];
}
- (instancetype)initEventWithName:(NSString *)name args:(NSDictionary *)args {
if ((self = [super init])) {
_name = name;
_args = args ? args : @{};
}
return self;
}
+ (void)postNotificationForEventName:(NSString *)name args:(NSDictionary *)args {
[[[self alloc] initEventWithName:name args:args] postNotification];
}
@end
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Bolts/BFMeasurementEvent.h>
/*!
Provides methods for posting notifications from the Bolts framework
*/
@interface BFMeasurementEvent (Internal)
+ (void)postNotificationForEventName:(NSString *)name args:(NSDictionary *)args;
@end
+75
View File
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Foundation/Foundation.h>
@class BFAppLink;
/*!
Provides a set of utilities for working with NSURLs, such as parsing of query parameters
and handling for App Link requests.
*/
@interface BFURL : NSObject
/*!
Creates a link target from a raw URL.
On success, this posts the BFAppLinkParseEventName measurement event. If you are constructing the BFURL within your application delegate's
application:openURL:sourceApplication:annotation:, you should instead use URLWithInboundURL:sourceApplication:
to support better BFMeasurementEvent notifications
@param url The instance of `NSURL` to create BFURL from.
*/
+ (BFURL *)URLWithURL:(NSURL *)url;
/*!
Creates a link target from a raw URL received from an external application. This is typically called from the app delegate's
application:openURL:sourceApplication:annotation: and will post the BFAppLinkNavigateInEventName measurement event.
@param url The instance of `NSURL` to create BFURL from.
@param sourceApplication the bundle ID of the app that is requesting your app to open the URL. The same sourceApplication in application:openURL:sourceApplication:annotation:
*/
+ (BFURL *)URLWithInboundURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication;
/*!
Gets the target URL. If the link is an App Link, this is the target of the App Link.
Otherwise, it is the url that created the target.
*/
@property (nonatomic, strong, readonly) NSURL *targetURL;
/*!
Gets the query parameters for the target, parsed into an NSDictionary.
*/
@property (nonatomic, strong, readonly) NSDictionary *targetQueryParameters;
/*!
If this link target is an App Link, this is the data found in al_applink_data.
Otherwise, it is nil.
*/
@property (nonatomic, strong, readonly) NSDictionary *appLinkData;
/*!
If this link target is an App Link, this is the data found in extras.
*/
@property (nonatomic, strong, readonly) NSDictionary *appLinkExtras;
/*!
The App Link indicating how to navigate back to the referer app, if any.
*/
@property (nonatomic, strong, readonly) BFAppLink *appLinkReferer;
/*!
The URL that was used to create this BFURL.
*/
@property (nonatomic, strong, readonly) NSURL *inputURL;
/*!
The query parameters of the inputURL, parsed into an NSDictionary.
*/
@property (nonatomic, strong, readonly) NSDictionary *inputQueryParameters;
@end
+142
View File
@@ -0,0 +1,142 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "BFURL_Internal.h"
#import "BFAppLink_Internal.h"
#import "BFAppLinkTarget.h"
#import "BFMeasurementEvent_Internal.h"
@implementation BFURL
- (instancetype)initWithURL:(NSURL *)url forOpenInboundURL:(BOOL)forOpenURLEvent sourceApplication:(NSString *)sourceApplication forRenderBackToReferrerBar:(BOOL)forRenderBackToReferrerBar {
self = [super init];
if (!self) return nil;
_inputURL = url;
_targetURL = url;
// Parse the query string parameters for the base URL
NSDictionary *baseQuery = [BFURL queryParametersForURL:url];
_inputQueryParameters = baseQuery;
_targetQueryParameters = baseQuery;
// Check for applink_data
NSString *appLinkDataString = baseQuery[BFAppLinkDataParameterName];
if (appLinkDataString) {
// Try to parse the JSON
NSError *error = nil;
NSDictionary *applinkData = [NSJSONSerialization JSONObjectWithData:[appLinkDataString dataUsingEncoding:NSUTF8StringEncoding]
options:0
error:&error];
if (!error && [applinkData isKindOfClass:[NSDictionary class]]) {
// If the version is not specified, assume it is 1.
NSString *version = applinkData[BFAppLinkVersionKeyName] ?: @"1.0";
NSString *target = applinkData[BFAppLinkTargetKeyName];
if ([version isKindOfClass:[NSString class]] &&
[version isEqual:BFAppLinkVersion]) {
// There's applink data! The target should actually be the applink target.
_appLinkData = applinkData;
id applinkExtras = applinkData[BFAppLinkExtrasKeyName];
if (applinkExtras && [applinkExtras isKindOfClass:[NSDictionary class]]) {
_appLinkExtras = applinkExtras;
}
_targetURL = ([target isKindOfClass:[NSString class]] ? [NSURL URLWithString:target] : url);
_targetQueryParameters = [BFURL queryParametersForURL:_targetURL];
NSDictionary *refererAppLink = _appLinkData[BFAppLinkRefererAppLink];
NSString *refererURLString = refererAppLink[BFAppLinkRefererUrl];
NSString *refererAppName = refererAppLink[BFAppLinkRefererAppName];
if (refererURLString && refererAppName) {
BFAppLinkTarget *target = [BFAppLinkTarget appLinkTargetWithURL:[NSURL URLWithString:refererURLString]
appStoreId:nil
appName:refererAppName];
_appLinkReferer = [BFAppLink appLinkWithSourceURL:[NSURL URLWithString:refererURLString]
targets:@[ target ]
webURL:nil
isBackToReferrer:YES];
}
// Raise Measurement Event
NSString *const EVENT_YES_VAL = @"1";
NSString *const EVENT_NO_VAL = @"0";
NSMutableDictionary *logData = [[NSMutableDictionary alloc] init];
logData[@"version"] = version;
if (refererURLString) {
logData[@"refererURL"] = refererURLString;
}
if (refererAppName) {
logData[@"refererAppName"] = refererAppName;
}
if (sourceApplication) {
logData[@"sourceApplication"] = sourceApplication;
}
if ([_targetURL absoluteString]) {
logData[@"targetURL"] = [_targetURL absoluteString];
}
if ([_inputURL absoluteString]) {
logData[@"inputURL"] = [_inputURL absoluteString];
}
if ([_inputURL scheme]) {
logData[@"inputURLScheme"] = [_inputURL scheme];
}
logData[@"forRenderBackToReferrerBar"] = forRenderBackToReferrerBar ? EVENT_YES_VAL : EVENT_NO_VAL;
logData[@"forOpenUrl"] = forOpenURLEvent ? EVENT_YES_VAL : EVENT_NO_VAL;
[BFMeasurementEvent postNotificationForEventName:BFAppLinkParseEventName args:logData];
if (forOpenURLEvent) {
[BFMeasurementEvent postNotificationForEventName:BFAppLinkNavigateInEventName args:logData];
}
}
}
}
return self;
}
+ (BFURL *)URLWithURL:(NSURL *)url {
return [[BFURL alloc] initWithURL:url forOpenInboundURL:NO sourceApplication:nil forRenderBackToReferrerBar:NO];
}
+ (BFURL *)URLWithInboundURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication {
return [[BFURL alloc] initWithURL:url forOpenInboundURL:YES sourceApplication:sourceApplication forRenderBackToReferrerBar:NO];
}
+ (BFURL *)URLForRenderBackToReferrerBarURL:(NSURL *)url {
return [[BFURL alloc] initWithURL:url forOpenInboundURL:NO sourceApplication:nil forRenderBackToReferrerBar:YES];
}
+ (NSString *)decodeURLString:(NSString *)string {
return (NSString *)CFBridgingRelease(CFURLCreateStringByReplacingPercentEscapes(NULL,
(CFStringRef)string,
CFSTR("")));
}
+ (NSDictionary *)queryParametersForURL:(NSURL *)url {
NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
NSString *query = url.query;
if ([query isEqualToString:@""]) {
return @{};
}
NSArray *queryComponents = [query componentsSeparatedByString:@"&"];
for (NSString *component in queryComponents) {
NSRange equalsLocation = [component rangeOfString:@"="];
if (equalsLocation.location == NSNotFound) {
// There's no equals, so associate the key with NSNull
parameters[[self decodeURLString:component]] = [NSNull null];
} else {
NSString *key = [self decodeURLString:[component substringToIndex:equalsLocation.location]];
NSString *value = [self decodeURLString:[component substringFromIndex:equalsLocation.location + 1]];
parameters[key] = value;
}
}
return [NSDictionary dictionaryWithDictionary:parameters];
}
@end
+15
View File
@@ -0,0 +1,15 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Bolts/BFURL.h>
@interface BFURL (Internal)
+ (BFURL *)URLForRenderBackToReferrerBarURL:(NSURL *)url;
@end
+26
View File
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <Foundation/Foundation.h>
#import <Bolts/BFAppLinkResolving.h>
/*!
A reference implementation for an App Link resolver that uses a hidden UIWebView
to parse the HTML containing App Link metadata.
*/
@interface BFWebViewAppLinkResolver : NSObject <BFAppLinkResolving>
/*!
Gets the instance of a BFWebViewAppLinkResolver.
*/
+ (instancetype)sharedInstance;
@end
+302
View File
@@ -0,0 +1,302 @@
/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import <UIKit/UIKit.h>
#import "BFWebViewAppLinkResolver.h"
#import "BFAppLink.h"
#import "BFAppLinkTarget.h"
#import "BFTask.h"
#import "BFTaskCompletionSource.h"
#import "BFExecutor.h"
// Defines JavaScript to extract app link tags from HTML content
static NSString *const BFWebViewAppLinkResolverTagExtractionJavaScript = @""
"(function() {"
" var metaTags = document.getElementsByTagName('meta');"
" var results = [];"
" for (var i = 0; i < metaTags.length; i++) {"
" var property = metaTags[i].getAttribute('property');"
" if (property && property.substring(0, 'al:'.length) === 'al:') {"
" var tag = { \"property\": metaTags[i].getAttribute('property') };"
" if (metaTags[i].hasAttribute('content')) {"
" tag['content'] = metaTags[i].getAttribute('content');"
" }"
" results.push(tag);"
" }"
" }"
" return JSON.stringify(results);"
"})()";
static NSString *const BFWebViewAppLinkResolverIOSURLKey = @"url";
static NSString *const BFWebViewAppLinkResolverIOSAppStoreIdKey = @"app_store_id";
static NSString *const BFWebViewAppLinkResolverIOSAppNameKey = @"app_name";
static NSString *const BFWebViewAppLinkResolverDictionaryValueKey = @"_value";
static NSString *const BFWebViewAppLinkResolverPreferHeader = @"Prefer-Html-Meta-Tags";
static NSString *const BFWebViewAppLinkResolverMetaTagPrefix = @"al";
static NSString *const BFWebViewAppLinkResolverWebKey = @"web";
static NSString *const BFWebViewAppLinkResolverIOSKey = @"ios";
static NSString *const BFWebViewAppLinkResolverIPhoneKey = @"iphone";
static NSString *const BFWebViewAppLinkResolverIPadKey = @"ipad";
static NSString *const BFWebViewAppLinkResolverWebURLKey = @"url";
static NSString *const BFWebViewAppLinkResolverShouldFallbackKey = @"should_fallback";
@interface BFWebViewAppLinkResolverWebViewDelegate : NSObject <UIWebViewDelegate>
@property (nonatomic, copy) void (^didFinishLoad)(UIWebView *webView);
@property (nonatomic, copy) void (^didFailLoadWithError)(UIWebView *webView, NSError *error);
@property (nonatomic, assign) BOOL hasLoaded;
@end
@implementation BFWebViewAppLinkResolverWebViewDelegate
- (void)webViewDidFinishLoad:(UIWebView *)webView {
if (self.didFinishLoad) {
self.didFinishLoad(webView);
}
}
- (void)webViewDidStartLoad:(UIWebView *)webView {
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
if (self.didFailLoadWithError) {
self.didFailLoadWithError(webView, error);
}
}
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
if (self.hasLoaded) {
// Consider loading a second resource to be "success", since it indicates an inner frame
// or redirect is happening. We can run the tag extraction script at this point.
self.didFinishLoad(webView);
return NO;
}
self.hasLoaded = YES;
return YES;
}
@end
@implementation BFWebViewAppLinkResolver
+ (instancetype)sharedInstance {
static id instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (BFTask *)followRedirects:(NSURL *)url {
// This task will be resolved with either the redirect NSURL
// or a dictionary with the response data to be returned.
BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setValue:BFWebViewAppLinkResolverMetaTagPrefix forHTTPHeaderField:BFWebViewAppLinkResolverPreferHeader];
void (^completion)(NSURLResponse *response, NSData *data, NSError *error) = ^(NSURLResponse *response, NSData *data, NSError *error) {
if (error) {
[tcs setError:error];
return;
}
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
// NSURLConnection usually follows redirects automatically, but the
// documentation is unclear what the default is. This helps it along.
if (httpResponse.statusCode >= 300 && httpResponse.statusCode < 400) {
NSString *redirectString = httpResponse.allHeaderFields[@"Location"];
NSURL *redirectURL = [NSURL URLWithString:redirectString];
[tcs setResult:redirectURL];
return;
}
}
[tcs setResult:@{ @"response" : response, @"data" : data }];
};
#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0 || __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_9
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
completion(response, data, error);
}] resume];
#else
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:completion];
#endif
return [tcs.task continueWithSuccessBlock:^id(BFTask *task) {
// If we redirected, just keep recursing.
if ([task.result isKindOfClass:[NSURL class]]) {
return [self followRedirects:task.result];
}
return task;
}];
}
- (BFTask *)appLinkFromURLInBackground:(NSURL *)url {
return [[self followRedirects:url] continueWithExecutor:[BFExecutor mainThreadExecutor]
withSuccessBlock:^id(BFTask *task) {
NSData *responseData = task.result[@"data"];
NSHTTPURLResponse *response = task.result[@"response"];
BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource];
UIWebView *webView = [[UIWebView alloc] init];
BFWebViewAppLinkResolverWebViewDelegate *listener = [[BFWebViewAppLinkResolverWebViewDelegate alloc] init];
__block BFWebViewAppLinkResolverWebViewDelegate *retainedListener = listener;
listener.didFinishLoad = ^(UIWebView *view) {
if (retainedListener) {
NSDictionary *ogData = [self getALDataFromLoadedPage:view];
[view removeFromSuperview];
view.delegate = nil;
retainedListener = nil;
[tcs setResult:[self appLinkFromALData:ogData destination:url]];
}
};
listener.didFailLoadWithError = ^(UIWebView* view, NSError *error) {
if (retainedListener) {
[view removeFromSuperview];
view.delegate = nil;
retainedListener = nil;
[tcs setError:error];
}
};
webView.delegate = listener;
webView.hidden = YES;
[webView loadData:responseData
MIMEType:response.MIMEType
textEncodingName:response.textEncodingName
baseURL:response.URL];
UIWindow *window = [UIApplication sharedApplication].windows.firstObject;
[window addSubview:webView];
return tcs.task;
}];
}
/*
Builds up a data structure filled with the app link data from the meta tags on a page.
The structure of this object is a dictionary where each key holds an array of app link
data dictionaries. Values are stored in a key called "_value".
*/
- (NSDictionary *)parseALData:(NSArray *)dataArray {
NSMutableDictionary *al = [NSMutableDictionary dictionary];
for (NSDictionary *tag in dataArray) {
NSString *name = tag[@"property"];
if (![name isKindOfClass:[NSString class]]) {
continue;
}
NSArray *nameComponents = [name componentsSeparatedByString:@":"];
if (![nameComponents[0] isEqualToString:BFWebViewAppLinkResolverMetaTagPrefix]) {
continue;
}
NSMutableDictionary *root = al;
for (int i = 1; i < nameComponents.count; i++) {
NSMutableArray *children = root[nameComponents[i]];
if (!children) {
children = [NSMutableArray array];
root[nameComponents[i]] = children;
}
NSMutableDictionary *child = children.lastObject;
if (!child || i == nameComponents.count - 1) {
child = [NSMutableDictionary dictionary];
[children addObject:child];
}
root = child;
}
if (tag[@"content"]) {
root[BFWebViewAppLinkResolverDictionaryValueKey] = tag[@"content"];
}
}
return al;
}
- (NSDictionary *)getALDataFromLoadedPage:(UIWebView *)webView {
// Run some JavaScript in the webview to fetch the meta tags.
NSString *jsonString = [webView stringByEvaluatingJavaScriptFromString:BFWebViewAppLinkResolverTagExtractionJavaScript];
NSError *error = nil;
NSArray *arr = [NSJSONSerialization JSONObjectWithData:[jsonString dataUsingEncoding:NSUTF8StringEncoding]
options:0
error:&error];
return [self parseALData:arr];
}
/*
Converts app link data into a BFAppLink containing the targets relevant for this platform.
*/
- (BFAppLink *)appLinkFromALData:(NSDictionary *)appLinkDict destination:(NSURL *)destination {
NSMutableArray *linkTargets = [NSMutableArray array];
NSArray *platformData = nil;
switch (UI_USER_INTERFACE_IDIOM()) {
case UIUserInterfaceIdiomPad:
platformData = @[ appLinkDict[BFWebViewAppLinkResolverIPadKey] ?: @{},
appLinkDict[BFWebViewAppLinkResolverIOSKey] ?: @{} ];
break;
case UIUserInterfaceIdiomPhone:
platformData = @[ appLinkDict[BFWebViewAppLinkResolverIPhoneKey] ?: @{},
appLinkDict[BFWebViewAppLinkResolverIOSKey] ?: @{} ];
break;
#ifdef __TVOS_9_0
case UIUserInterfaceIdiomTV:
#endif
case UIUserInterfaceIdiomUnspecified:
default:
// Future-proofing. Other User Interface idioms should only hit ios.
platformData = @[ appLinkDict[BFWebViewAppLinkResolverIOSKey] ?: @{} ];
break;
}
for (NSArray *platformObjects in platformData) {
for (NSDictionary *platformDict in platformObjects) {
// The schema requires a single url/app store id/app name,
// but we could find multiple of them. We'll make a best effort
// to interpret this data.
NSArray *urls = platformDict[BFWebViewAppLinkResolverIOSURLKey];
NSArray *appStoreIds = platformDict[BFWebViewAppLinkResolverIOSAppStoreIdKey];
NSArray *appNames = platformDict[BFWebViewAppLinkResolverIOSAppNameKey];
NSUInteger maxCount = MAX(urls.count, MAX(appStoreIds.count, appNames.count));
for (NSUInteger i = 0; i < maxCount; i++) {
NSString *urlString = urls[i][BFWebViewAppLinkResolverDictionaryValueKey];
NSURL *url = urlString ? [NSURL URLWithString:urlString] : nil;
NSString *appStoreId = appStoreIds[i][BFWebViewAppLinkResolverDictionaryValueKey];
NSString *appName = appNames[i][BFWebViewAppLinkResolverDictionaryValueKey];
BFAppLinkTarget *target = [BFAppLinkTarget appLinkTargetWithURL:url
appStoreId:appStoreId
appName:appName];
[linkTargets addObject:target];
}
}
}
NSDictionary *webDict = appLinkDict[BFWebViewAppLinkResolverWebKey][0];
NSString *webUrlString = webDict[BFWebViewAppLinkResolverWebURLKey][0][BFWebViewAppLinkResolverDictionaryValueKey];
NSString *shouldFallbackString = webDict[BFWebViewAppLinkResolverShouldFallbackKey][0][BFWebViewAppLinkResolverDictionaryValueKey];
NSURL *webUrl = destination;
if (shouldFallbackString &&
[@[ @"no", @"false", @"0" ] containsObject:[shouldFallbackString lowercaseString]]) {
webUrl = nil;
}
if (webUrl && webUrlString) {
webUrl = [NSURL URLWithString:webUrlString];
}
return [BFAppLink appLinkWithSourceURL:destination
targets:linkTargets
webURL:webUrl];
}
@end