|
#import "LookbackErrorPresenter.h" |
|
#import "GFFunctional.h" |
|
#import "GFLogging.h" |
|
|
|
@interface LookbackErrorPresenter () |
|
{ |
|
BOOL _errorInhibition; |
|
} |
|
@property(nonatomic) NSMutableSet *outstandingAlertContexts; |
|
@property(nonatomic) NSMutableArray *queuedAlertContexts; |
|
@end |
|
|
|
#import <UIKit/UIKit.h> |
|
|
|
@interface _LookbackErrorAlertContext : NSObject <UIAlertViewDelegate> |
|
@property(nonatomic) NSString *contextIdentifier; |
|
@property(strong, nonatomic) NSError *error; |
|
@property(copy, nonatomic) void (^completion)(BOOL recovered); |
|
@property(weak, nonatomic) LookbackErrorPresenter *presenter; |
|
@property(weak, nonatomic) UIAlertView *alert; |
|
- (void)showAlert; |
|
@end |
|
|
|
|
|
@implementation LookbackErrorPresenter |
|
+ (instancetype)presenter |
|
{ |
|
static LookbackErrorPresenter *g; |
|
static dispatch_once_t onceToken; |
|
dispatch_once(&onceToken, ^{ |
|
g = [LookbackErrorPresenter new]; |
|
}); |
|
return g; |
|
} |
|
- (instancetype)init |
|
{ |
|
if(!(self = [super init])) |
|
return nil; |
|
_outstandingAlertContexts = [NSMutableSet new]; |
|
_queuedAlertContexts = [NSMutableArray new]; |
|
return self; |
|
} |
|
|
|
- (void)presentError:(NSError*)err completion:(void(^)(BOOL recovered))completion |
|
{ |
|
[self presentError:err completion:completion forContext:nil]; |
|
} |
|
|
|
- (void)presentError:(NSError*)err completion:(void(^)(BOOL recovered))completion forContext:(NSString*)contextIdentifier |
|
{ |
|
if(!contextIdentifier) |
|
contextIdentifier = [[NSUUID UUID] UUIDString]; |
|
|
|
_LookbackErrorAlertContext *context = [_LookbackErrorAlertContext new]; |
|
context.contextIdentifier = contextIdentifier; |
|
context.error = err; |
|
context.completion = completion; |
|
context.presenter = self; |
|
[_outstandingAlertContexts addObject:context]; |
|
|
|
// Already have an alert on screen, or alerts inhibited? Queue it. |
|
BOOL alreadyHaveAlertOnScreen = _outstandingAlertContexts.count > 1; |
|
if(alreadyHaveAlertOnScreen || _errorInhibition) { |
|
GFLog(GFInfo, @"Queueing alert because %@: %@ — %@ — %@", alreadyHaveAlertOnScreen ? @"already have alert on screen" : @"alerts are inhibited", context, context.alert, context.error); |
|
[self.queuedAlertContexts addObject:context]; |
|
return; |
|
} |
|
|
|
[context showAlert]; |
|
} |
|
|
|
- (void)cancelContext:(NSString*)contextIdentifier |
|
{ |
|
NSSet *matchingContexts = [_outstandingAlertContexts gf_filter:^BOOL(_LookbackErrorAlertContext *ctx) { |
|
return [ctx.contextIdentifier isEqual:contextIdentifier]; |
|
}]; |
|
for(_LookbackErrorAlertContext *context in matchingContexts) { |
|
GFLog(GFInfo, @"Cancelling alert because its error is not relevant anymore: %@ — %@ — %@", context, context.alert, context.error); |
|
[context.alert dismissWithClickedButtonIndex:context.alert.cancelButtonIndex animated:NO]; |
|
} |
|
} |
|
|
|
- (void)inhibitErrors |
|
{ |
|
GFLog(GFDebug, @"Inhibiting error dialogs"); |
|
_errorInhibition = YES; |
|
} |
|
- (void)uninhibitAndDisplayPostponedErrors |
|
{ |
|
GFLog(GFDebug, @"Uninhibiting error dialogs"); |
|
_errorInhibition = NO; |
|
[self showQueuedErrors]; |
|
} |
|
|
|
- (void)showQueuedErrors |
|
{ |
|
_LookbackErrorAlertContext *context = [[self queuedAlertContexts] firstObject]; |
|
if(!context) |
|
return; |
|
GFLog(GFDebug, @"Showing queued alert: %@ %@", context, context.alert.title); |
|
[[self queuedAlertContexts] removeObject:context]; |
|
[context showAlert]; |
|
} |
|
@end |
|
|
|
@implementation _LookbackErrorAlertContext |
|
|
|
- (void)showAlert |
|
{ |
|
NSMutableString *desc = [NSMutableString new]; |
|
NSString *failureReason = [self.error localizedFailureReason]; |
|
NSString *recoverySuggestion = [self.error localizedRecoverySuggestion]; |
|
if(failureReason) [desc appendString:failureReason]; |
|
if(failureReason && recoverySuggestion) [desc appendString:@"\n\n"]; |
|
if(recoverySuggestion) [desc appendString:recoverySuggestion]; |
|
|
|
NSError *underlying = [self.error userInfo][NSUnderlyingErrorKey]; |
|
if(underlying) |
|
[desc appendFormat:@"%@%@ (%ld). %@", desc.length>0?@"\n\nThis is because: ":@"", underlying.localizedDescription, (long)underlying.code, underlying.localizedFailureReason]; |
|
|
|
/*#if DEBUG |
|
[desc appendFormat:@"\n\n(Lookback error %@:%ld < %@:%ld)", err.domain, (long)err.code, underlying.domain, (long)underlying.code]; |
|
#endif*/ |
|
|
|
GFLog(GFError, @"Presenting error: %@ < %@", self.error, underlying); |
|
|
|
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:[self.error localizedDescription] message:desc delegate:self cancelButtonTitle:nil otherButtonTitles:nil]; |
|
|
|
NSArray *recoveryOptions = [self.error localizedRecoveryOptions]; |
|
if(recoveryOptions.count == 0) { |
|
[alert addButtonWithTitle:@"OK"]; |
|
} else { |
|
for(NSString *recoveryOption in recoveryOptions) |
|
[alert addButtonWithTitle:recoveryOption]; |
|
} |
|
|
|
self.alert = alert; |
|
[alert show]; |
|
} |
|
|
|
- (void)alertView:(UIAlertView *)alertView willDismissWithButtonIndex:(NSInteger)buttonIndex |
|
{ |
|
BOOL recovered = [[self.error recoveryAttempter] attemptRecoveryFromError:self.error optionIndex:buttonIndex]; |
|
|
|
if(self.completion) |
|
self.completion(recovered); |
|
|
|
alertView.delegate = nil; |
|
[self.presenter.outstandingAlertContexts removeObject:self]; |
|
[self.presenter.queuedAlertContexts removeObject:self]; |
|
[self.presenter showQueuedErrors]; |
|
} |
|
@end |
|
|
|
|
|
@implementation LookbackRecoveryAttempter |
|
{ |
|
NSMutableArray *_recoveryOptions; |
|
NSMutableArray *_recoveryCallbacks; |
|
} |
|
- (id)init // designated |
|
{ |
|
if(!(self = [super init])) |
|
return nil; |
|
|
|
_recoveryOptions = [NSMutableArray new]; |
|
_recoveryCallbacks = [NSMutableArray new]; |
|
|
|
return self; |
|
} |
|
- (id)initWithNamesAndCallbacks:(id)name, ... //convenience |
|
{ |
|
if(!(self = [self init])) |
|
return nil; |
|
|
|
va_list va; |
|
va_start(va, name); |
|
|
|
LookbackRecoverCallback callback = va_arg(va, LookbackRecoverCallback); |
|
[self addRecoveryOption:name callback:callback]; |
|
|
|
while((name = va_arg(va, NSString*))) { |
|
LookbackRecoverCallback callback = va_arg(va, LookbackRecoverCallback); |
|
[self addRecoveryOption:name callback:callback]; |
|
} |
|
|
|
va_end(va); |
|
|
|
|
|
return self; |
|
} |
|
- (BOOL)attemptRecoveryFromError:(NSError *)error optionIndex:(NSUInteger)recoveryOptionIndex |
|
{ |
|
if(recoveryOptionIndex >= _recoveryCallbacks.count) |
|
return NO; |
|
|
|
LookbackRecoverCallback callback = _recoveryCallbacks[recoveryOptionIndex]; |
|
return callback(); |
|
} |
|
- (NSArray*)recoveryOptions |
|
{ |
|
return [_recoveryOptions copy]; |
|
} |
|
- (void)addRecoveryOption:(NSString*)name callback:(LookbackRecoverCallback)performRecovery |
|
{ |
|
[_recoveryOptions addObject:name]; |
|
[_recoveryCallbacks addObject:[performRecovery copy]]; |
|
} |
|
@end |