diff --git a/Signal/src/ViewControllers/ConversationView/MessagesViewController.m b/Signal/src/ViewControllers/ConversationView/MessagesViewController.m index 41322b8d9..e12348046 100644 --- a/Signal/src/ViewControllers/ConversationView/MessagesViewController.m +++ b/Signal/src/ViewControllers/ConversationView/MessagesViewController.m @@ -158,7 +158,6 @@ typedef enum : NSUInteger { OWSVoiceMemoGestureDelegate, UIDocumentMenuDelegate, UIDocumentPickerDelegate, - UIGestureRecognizerDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate, UITextViewDelegate> @@ -549,10 +548,6 @@ typedef enum : NSUInteger { // In case we're dismissing a CNContactViewController which requires default system appearance [UIUtil applySignalAppearence]; - // Since we're using a custom back button, we have to do some extra work to manage the - // interactivePopGestureRecognizer - self.navigationController.interactivePopGestureRecognizer.delegate = self; - // We need to recheck on every appearance, since the user may have left the group in the settings VC, // or on another device. [self hideInputIfNeeded]; @@ -992,10 +987,6 @@ typedef enum : NSUInteger { self.isViewVisible = NO; - // Since we're using a custom back button, we have to do some extra work to manage the - // interactivePopGestureRecognizer - self.navigationController.interactivePopGestureRecognizer.delegate = nil; - [self.audioAttachmentPlayer stop]; self.audioAttachmentPlayer = nil; diff --git a/Signal/src/ViewControllers/NewGroupViewController.m b/Signal/src/ViewControllers/NewGroupViewController.m index 03ef92704..1dd609b1b 100644 --- a/Signal/src/ViewControllers/NewGroupViewController.m +++ b/Signal/src/ViewControllers/NewGroupViewController.m @@ -50,6 +50,7 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; @property (nonatomic) NSMutableSet *memberRecipientIds; @property (nonatomic) BOOL hasUnsavedChanges; +@property (nonatomic) BOOL shouldIgnoreSavedChanges; @property (nonatomic) BOOL hasAppeared; @end @@ -551,7 +552,7 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; { [self.groupNameTextField resignFirstResponder]; - if (!self.hasUnsavedChanges) { + if (!self.hasUnsavedChanges || self.shouldIgnoreSavedChanges) { // If user made no changes, return to conversation settings view. [self.navigationController popViewControllerAnimated:YES]; return; @@ -570,6 +571,7 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; @"The label for the 'discard' button in alerts and action sheets.") style:UIAlertActionStyleDestructive handler:^(UIAlertAction *action) { + self.shouldIgnoreSavedChanges = YES; [self.navigationController popViewControllerAnimated:YES]; }]]; [controller addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", nil) @@ -651,9 +653,13 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; #pragma mark - OWSNavigationView -- (void)navBackButtonPressed +- (BOOL)shouldCancelNavigationBack { - [self backButtonPressed]; + BOOL result = self.hasUnsavedChanges && !self.shouldIgnoreSavedChanges; + if (result) { + [self backButtonPressed]; + } + return result; } @end diff --git a/Signal/src/ViewControllers/OWSNavigationController.h b/Signal/src/ViewControllers/OWSNavigationController.h index d8279dacb..a47b602df 100644 --- a/Signal/src/ViewControllers/OWSNavigationController.h +++ b/Signal/src/ViewControllers/OWSNavigationController.h @@ -4,9 +4,11 @@ #import +// Any view controller which wants to be able cancel back button +// presses and back gestures should implement this protocol. @protocol OWSNavigationView -- (void)navBackButtonPressed; +- (BOOL)shouldCancelNavigationBack; @end diff --git a/Signal/src/ViewControllers/OWSNavigationController.m b/Signal/src/ViewControllers/OWSNavigationController.m index e475f48a4..b1fe079ab 100644 --- a/Signal/src/ViewControllers/OWSNavigationController.m +++ b/Signal/src/ViewControllers/OWSNavigationController.m @@ -4,7 +4,15 @@ #import "OWSNavigationController.h" -@interface OWSNavigationController () +// We use a category to expose UINavigationController's private +// UINavigationBarDelegate methods. +@interface UINavigationController (OWSNavigationController) + +@end + +#pragma mark - + +@interface OWSNavigationController () @end @@ -12,24 +20,53 @@ @implementation OWSNavigationController -- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item +- (void)viewDidLoad { + [super viewDidLoad]; + self.interactivePopGestureRecognizer.delegate = self; +} + +#pragma mark - UINavigationBarDelegate + +// All UINavigationController serve as the UINavigationBarDelegate for their navbar. +// We override shouldPopItem: in order to cancel some back button presses - for example, +// if a view has unsaved changes. +- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item +{ + OWSAssert(self.interactivePopGestureRecognizer.delegate == self); UIViewController *topViewController = self.topViewController; - BOOL wasBackButtonClicked = topViewController.navigationItem == item; + BOOL wasBackButtonClicked = topViewController.navigationItem == item; + BOOL result = YES; if (wasBackButtonClicked) { - if ([topViewController respondsToSelector:@selector(navBackButtonPressed)]) { - // if user did press back on the view controller where you handle the navBackButtonPressed - [topViewController performSelector:@selector(navBackButtonPressed)]; - return NO; - } else { - // if user did press back but you are not on the view controller that can handle the navBackButtonPressed - [self popViewControllerAnimated:YES]; - return YES; + if ([topViewController conformsToProtocol:@protocol(OWSNavigationView)]) { + id navigationView = (id)topViewController; + result = ![navigationView shouldCancelNavigationBack]; } + } + + // If we're not going to cancel the pop/back, we need to call the super + // implementation since it has important side effects. + if (result) { + result = [super navigationBar:navigationBar shouldPopItem:item]; + OWSAssert(result); + } + return result; +} + +#pragma mark - UIGestureRecognizerDelegate + +// We serve as the UIGestureRecognizerDelegate of the interactivePopGestureRecognizer +// in order to cancel some "back" gestures - for example, +// if a view has unsaved changes. +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + UIViewController *topViewController = self.topViewController; + if ([topViewController conformsToProtocol:@protocol(OWSNavigationView)]) { + id navigationView = (id)topViewController; + return ![navigationView shouldCancelNavigationBack]; } else { - // when you call popViewController programmatically you do not want to pop it twice return YES; } } diff --git a/Signal/src/ViewControllers/ProfileViewController.m b/Signal/src/ViewControllers/ProfileViewController.m index 1c2c20f16..40f0a7ce5 100644 --- a/Signal/src/ViewControllers/ProfileViewController.m +++ b/Signal/src/ViewControllers/ProfileViewController.m @@ -44,6 +44,8 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat @property (nonatomic) BOOL hasUnsavedChanges; +@property (nonatomic) BOOL shouldIgnoreSavedChanges; + @property (nonatomic) ProfileViewMode profileViewMode; @property (nonatomic) YapDatabaseConnection *databaseConnection; @@ -207,7 +209,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat { [self.nameTextField resignFirstResponder]; - if (!self.hasUnsavedChanges) { + if (!self.hasUnsavedChanges || self.shouldIgnoreSavedChanges) { // If user made no changes, return to conversation settings view. [self profileCompletedOrSkipped]; return; @@ -226,6 +228,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat @"The label for the 'discard' button in alerts and action sheets.") style:UIAlertActionStyleDestructive handler:^(UIAlertAction *action) { + self.shouldIgnoreSavedChanges = YES; [self profileCompletedOrSkipped]; }]]; [controller addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", nil) @@ -495,9 +498,13 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat #pragma mark - OWSNavigationView -- (void)navBackButtonPressed +- (BOOL)shouldCancelNavigationBack { - [self backOrSkipButtonPressed]; + BOOL result = self.hasUnsavedChanges && !self.shouldIgnoreSavedChanges; + if (result) { + [self backOrSkipButtonPressed]; + } + return result; } #pragma mark - Logging diff --git a/Signal/src/ViewControllers/UpdateGroupViewController.m b/Signal/src/ViewControllers/UpdateGroupViewController.m index 3ebb1edf2..27ad12c2c 100644 --- a/Signal/src/ViewControllers/UpdateGroupViewController.m +++ b/Signal/src/ViewControllers/UpdateGroupViewController.m @@ -50,6 +50,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) NSMutableSet *memberRecipientIds; @property (nonatomic) BOOL hasUnsavedChanges; +@property (nonatomic) BOOL shouldIgnoreSavedChanges; @end @@ -413,7 +414,7 @@ NS_ASSUME_NONNULL_BEGIN { [self.groupNameTextField resignFirstResponder]; - if (!self.hasUnsavedChanges) { + if (!self.hasUnsavedChanges || self.shouldIgnoreSavedChanges) { // If user made no changes, return to conversation settings view. [self.navigationController popViewControllerAnimated:YES]; return; @@ -441,6 +442,7 @@ NS_ASSUME_NONNULL_BEGIN @"The label for the 'don't save' button in action sheets.") style:UIAlertActionStyleDestructive handler:^(UIAlertAction *action) { + self.shouldIgnoreSavedChanges = YES; [self.navigationController popViewControllerAnimated:YES]; }]]; [self presentViewController:controller animated:YES completion:nil]; @@ -530,9 +532,13 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - OWSNavigationView -- (void)navBackButtonPressed +- (BOOL)shouldCancelNavigationBack { - [self backButtonPressed]; + BOOL result = self.hasUnsavedChanges && !self.shouldIgnoreSavedChanges; + if (result) { + [self backButtonPressed]; + } + return result; } @end