From ffb4b3f9d2964838c227617a56d0374d1ef2dadd Mon Sep 17 00:00:00 2001 From: Matthew Chen <charlesmchen@gmail.com> Date: Tue, 15 Aug 2017 17:55:07 -0400 Subject: [PATCH 1/6] Add profile view to registration workflow. // FREEBIE --- .../AppSettingsViewController.m | 1 + .../CodeVerificationViewController.m | 19 ++-- .../ViewControllers/ProfileViewController.h | 8 ++ .../ViewControllers/ProfileViewController.m | 104 +++++++++++++++--- .../translations/en.lproj/Localizable.strings | 3 + 5 files changed, 110 insertions(+), 25 deletions(-) diff --git a/Signal/src/ViewControllers/AppSettingsViewController.m b/Signal/src/ViewControllers/AppSettingsViewController.m index fd1b6366c..44f107faf 100644 --- a/Signal/src/ViewControllers/AppSettingsViewController.m +++ b/Signal/src/ViewControllers/AppSettingsViewController.m @@ -317,6 +317,7 @@ - (void)showProfile { ProfileViewController *vc = [[ProfileViewController alloc] init]; + vc.profileViewMode = ProfileViewMode_AppSettings; [self.navigationController pushViewController:vc animated:YES]; } diff --git a/Signal/src/ViewControllers/CodeVerificationViewController.m b/Signal/src/ViewControllers/CodeVerificationViewController.m index 505fbd4d8..837a4b3ae 100644 --- a/Signal/src/ViewControllers/CodeVerificationViewController.m +++ b/Signal/src/ViewControllers/CodeVerificationViewController.m @@ -3,10 +3,8 @@ // #import "CodeVerificationViewController.h" -#import "AppDelegate.h" +#import "ProfileViewController.h" #import "Signal-Swift.h" -#import "SignalsNavigationController.h" -#import "SignalsViewController.h" #import "StringUtil.h" #import "UIViewController+OWS.h" #import <PromiseKit/AnyPromise.h> @@ -269,14 +267,7 @@ NS_ASSUME_NONNULL_BEGIN DDLogInfo(@"%@ Successfully registered Signal account.", weakSelf.tag); dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf stopActivityIndicator]; - - SignalsViewController *homeView = [SignalsViewController new]; - homeView.newlyRegisteredUser = YES; - SignalsNavigationController *navigationController = - [[SignalsNavigationController alloc] initWithRootViewController:homeView]; - AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate; - appDelegate.window.rootViewController = navigationController; - OWSAssert([navigationController.topViewController isKindOfClass:[SignalsViewController class]]); + [weakSelf vericationWasCompleted]; }); }) .catch(^(NSError *_Nonnull error) { @@ -290,6 +281,12 @@ NS_ASSUME_NONNULL_BEGIN }); } +- (void)vericationWasCompleted +{ + ProfileViewController *vc = [[ProfileViewController alloc] init]; + vc.profileViewMode = ProfileViewMode_Registration; + [self.navigationController pushViewController:vc animated:YES]; +} - (void)presentAlertWithVerificationError:(NSError *)error { diff --git a/Signal/src/ViewControllers/ProfileViewController.h b/Signal/src/ViewControllers/ProfileViewController.h index 16471c80f..d366ac026 100644 --- a/Signal/src/ViewControllers/ProfileViewController.h +++ b/Signal/src/ViewControllers/ProfileViewController.h @@ -6,8 +6,16 @@ NS_ASSUME_NONNULL_BEGIN +typedef NS_ENUM(NSInteger, ProfileViewMode) { + ProfileViewMode_AppSettings = 0, + ProfileViewMode_Registration, + ProfileViewMode_UpgradeOrNag, +}; + @interface ProfileViewController : OWSTableViewController +@property (nonatomic) ProfileViewMode profileViewMode; + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ProfileViewController.m b/Signal/src/ViewControllers/ProfileViewController.m index 350bbfd04..a122a3fe9 100644 --- a/Signal/src/ViewControllers/ProfileViewController.m +++ b/Signal/src/ViewControllers/ProfileViewController.m @@ -3,9 +3,12 @@ // #import "ProfileViewController.h" +#import "AppDelegate.h" #import "AvatarViewHelper.h" #import "OWSProfileManager.h" #import "Signal-Swift.h" +#import "SignalsNavigationController.h" +#import "SignalsViewController.h" #import "UIColor+OWS.h" #import "UIFont+OWS.h" #import "UIView+OWS.h" @@ -42,8 +45,6 @@ NS_ASSUME_NONNULL_BEGIN self.view.backgroundColor = [UIColor whiteColor]; [self.navigationController.navigationBar setTranslucent:NO]; self.title = NSLocalizedString(@"PROFILE_VIEW_TITLE", @"Title for the profile view."); - self.navigationItem.leftBarButtonItem = - [self createOWSBackButtonWithTarget:self selector:@selector(backButtonPressed:)]; _avatarViewHelper = [AvatarViewHelper new]; _avatarViewHelper.delegate = self; @@ -51,6 +52,7 @@ NS_ASSUME_NONNULL_BEGIN _avatar = [OWSProfileManager.sharedManager localProfileAvatarImage]; [self createViews]; + [self updateNavigationItem]; } - (void)createViews @@ -164,11 +166,20 @@ NS_ASSUME_NONNULL_BEGIN - (void)backButtonPressed:(id)sender { + [self leaveViewCheckingForUnsavedChanges:^{ + [self.navigationController popViewControllerAnimated:YES]; + }]; +} + +- (void)leaveViewCheckingForUnsavedChanges:(void (^_Nonnull)())leaveViewBlock +{ + OWSAssert(leaveViewBlock); + [self.nameTextField resignFirstResponder]; if (!self.hasUnsavedChanges) { // If user made no changes, return to conversation settings view. - [self.navigationController popViewControllerAnimated:YES]; + leaveViewBlock(); return; } @@ -185,7 +196,7 @@ NS_ASSUME_NONNULL_BEGIN @"The label for the 'discard' button in alerts and action sheets.") style:UIAlertActionStyleDestructive handler:^(UIAlertAction *action) { - [self.navigationController popViewControllerAnimated:YES]; + leaveViewBlock(); }]]; [controller addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", nil) style:UIAlertActionStyleCancel @@ -204,15 +215,43 @@ NS_ASSUME_NONNULL_BEGIN { _hasUnsavedChanges = hasUnsavedChanges; - if (hasUnsavedChanges) { - self.navigationItem.rightBarButtonItem = (self.hasUnsavedChanges - ? [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"EDIT_GROUP_UPDATE_BUTTON", - @"The title for the 'update group' button.") - style:UIBarButtonItemStylePlain - target:self - action:@selector(updatePressed)] - : nil); + [self updateNavigationItem]; +} + +- (void)updateNavigationItem +{ + // The navigation bar is hidden in the registration workflow. + [self.navigationController setNavigationBarHidden:NO animated:YES]; + + UIBarButtonItem *rightItem = nil; + switch (self.profileViewMode) { + case ProfileViewMode_AppSettings: + self.navigationItem.leftBarButtonItem = + [self createOWSBackButtonWithTarget:self selector:@selector(backButtonPressed:)]; + break; + case ProfileViewMode_Registration: + case ProfileViewMode_UpgradeOrNag: + // Registration and "upgrade or nag" mode don't need a back button. + self.navigationItem.hidesBackButton = YES; + + // Registration and "upgrade or nag" mode have "skip" or "update". + // + // TODO: Should this be some other verb instead of "update"? + rightItem = [[UIBarButtonItem alloc] + initWithTitle:NSLocalizedString(@"NAVIGATION_ITEM_SKIP_BUTTON", @"A button to skip a view.") + style:UIBarButtonItemStylePlain + target:self + action:@selector(skipPressed)]; + break; } + if (self.hasUnsavedChanges) { + rightItem = [[UIBarButtonItem alloc] + initWithTitle:NSLocalizedString(@"EDIT_GROUP_UPDATE_BUTTON", @"The title for the 'update group' button.") + style:UIBarButtonItemStylePlain + target:self + action:@selector(updatePressed)]; + } + self.navigationItem.rightBarButtonItem = rightItem; } - (void)updatePressed @@ -239,8 +278,7 @@ NS_ASSUME_NONNULL_BEGIN success:^{ [alertController dismissViewControllerAnimated:NO completion:^{ - [weakSelf.navigationController - popViewControllerAnimated:YES]; + [weakSelf updateProfileCompleted]; }]; } failure:^{ @@ -265,6 +303,44 @@ NS_ASSUME_NONNULL_BEGIN return [self.nameTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; } +- (void)updateProfileCompleted +{ + [self profileCompletedOrSkipped]; +} + +- (void)skipPressed +{ + [self leaveViewCheckingForUnsavedChanges:^{ + [self profileCompletedOrSkipped]; + }]; +} + +- (void)profileCompletedOrSkipped +{ + switch (self.profileViewMode) { + case ProfileViewMode_AppSettings: + [self.navigationController popViewControllerAnimated:YES]; + break; + case ProfileViewMode_Registration: + [self showHomeView]; + break; + case ProfileViewMode_UpgradeOrNag: + [self dismissViewControllerAnimated:YES completion:nil]; + break; + } +} + +- (void)showHomeView +{ + SignalsViewController *homeView = [SignalsViewController new]; + homeView.newlyRegisteredUser = YES; + SignalsNavigationController *navigationController = + [[SignalsNavigationController alloc] initWithRootViewController:homeView]; + AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate; + appDelegate.window.rootViewController = navigationController; + OWSAssert([navigationController.topViewController isKindOfClass:[SignalsViewController class]]); +} + #pragma mark - UITextFieldDelegate - (BOOL)textField:(UITextField *)textField diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 4b924e6ea..4da0fd5b3 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -861,6 +861,9 @@ /* An explanation of the consequences of muting a thread. */ "MUTE_BEHAVIOR_EXPLANATION" = "You will not receive notifications for muted conversations."; +/* A button to skip a view. */ +"NAVIGATION_ITEM_SKIP_BUTTON" = "Skip"; + /* No comment provided by engineer. */ "NETWORK_ERROR_RECOVERY" = "Please check you're online and try again."; From 9d8c39684834a45dbce4a0d4a6037c4ba78e1dcf Mon Sep 17 00:00:00 2001 From: Matthew Chen <charlesmchen@gmail.com> Date: Wed, 16 Aug 2017 10:25:36 -0400 Subject: [PATCH 2/6] Add profile view to upgrade/nag workflow. // FREEBIE --- .../AppSettingsViewController.m | 4 +- .../CodeVerificationViewController.m | 4 +- .../ViewControllers/ProfileViewController.h | 14 +- .../ViewControllers/ProfileViewController.m | 137 +++++++++++++----- .../ViewControllers/SignalsViewController.m | 6 + 5 files changed, 119 insertions(+), 46 deletions(-) diff --git a/Signal/src/ViewControllers/AppSettingsViewController.m b/Signal/src/ViewControllers/AppSettingsViewController.m index 44f107faf..9a91317fb 100644 --- a/Signal/src/ViewControllers/AppSettingsViewController.m +++ b/Signal/src/ViewControllers/AppSettingsViewController.m @@ -316,9 +316,7 @@ - (void)showProfile { - ProfileViewController *vc = [[ProfileViewController alloc] init]; - vc.profileViewMode = ProfileViewMode_AppSettings; - [self.navigationController pushViewController:vc animated:YES]; + [ProfileViewController presentForAppSettings:self.navigationController]; } - (void)showAdvanced diff --git a/Signal/src/ViewControllers/CodeVerificationViewController.m b/Signal/src/ViewControllers/CodeVerificationViewController.m index 837a4b3ae..3040e5224 100644 --- a/Signal/src/ViewControllers/CodeVerificationViewController.m +++ b/Signal/src/ViewControllers/CodeVerificationViewController.m @@ -283,9 +283,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)vericationWasCompleted { - ProfileViewController *vc = [[ProfileViewController alloc] init]; - vc.profileViewMode = ProfileViewMode_Registration; - [self.navigationController pushViewController:vc animated:YES]; + [ProfileViewController presentForRegistration:self.navigationController]; } - (void)presentAlertWithVerificationError:(NSError *)error diff --git a/Signal/src/ViewControllers/ProfileViewController.h b/Signal/src/ViewControllers/ProfileViewController.h index d366ac026..fdad9ce4d 100644 --- a/Signal/src/ViewControllers/ProfileViewController.h +++ b/Signal/src/ViewControllers/ProfileViewController.h @@ -6,15 +6,17 @@ NS_ASSUME_NONNULL_BEGIN -typedef NS_ENUM(NSInteger, ProfileViewMode) { - ProfileViewMode_AppSettings = 0, - ProfileViewMode_Registration, - ProfileViewMode_UpgradeOrNag, -}; +@class SignalsViewController; @interface ProfileViewController : OWSTableViewController -@property (nonatomic) ProfileViewMode profileViewMode; +- (instancetype)init NS_UNAVAILABLE; + ++ (BOOL)shouldDisplayProfileViewOnLaunch; + ++ (void)presentForAppSettings:(UINavigationController *)navigationController; ++ (void)presentForRegistration:(UINavigationController *)navigationController; ++ (void)presentForUpgradeOrNag:(SignalsViewController *)presentingController; @end diff --git a/Signal/src/ViewControllers/ProfileViewController.m b/Signal/src/ViewControllers/ProfileViewController.m index a122a3fe9..0230df82d 100644 --- a/Signal/src/ViewControllers/ProfileViewController.m +++ b/Signal/src/ViewControllers/ProfileViewController.m @@ -13,9 +13,20 @@ #import "UIFont+OWS.h" #import "UIView+OWS.h" #import "UIViewController+OWS.h" +#import <SignalServiceKit/NSDate+OWS.h> +#import <SignalServiceKit/TSStorageManager.h> NS_ASSUME_NONNULL_BEGIN +typedef NS_ENUM(NSInteger, ProfileViewMode) { + ProfileViewMode_AppSettings = 0, + ProfileViewMode_Registration, + ProfileViewMode_UpgradeOrNag, +}; + +NSString *const kProfileView_Collection = @"kProfileView_Collection"; +NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDate"; + @interface ProfileViewController () <UITextFieldDelegate, AvatarViewHelperDelegate> @property (nonatomic, readonly) AvatarViewHelper *avatarViewHelper; @@ -32,12 +43,34 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) BOOL hasUnsavedChanges; +@property (nonatomic) ProfileViewMode profileViewMode; + +@property (nonatomic) YapDatabaseConnection *databaseConnection; + @end #pragma mark - @implementation ProfileViewController +- (instancetype)initWithMode:(ProfileViewMode)profileViewMode +{ + self = [super init]; + + if (!self) { + return self; + } + + self.profileViewMode = profileViewMode; + self.databaseConnection = [[TSStorageManager sharedManager] newDatabaseConnection]; + + [self.databaseConnection setDate:[NSDate new] + forKey:kProfileView_LastPresentedDate + inCollection:kProfileView_Collection]; + + return self; +} + - (void)loadView { [super loadView]; @@ -164,22 +197,18 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Event Handling -- (void)backButtonPressed:(id)sender +- (void)backOrSkipButtonPressed { - [self leaveViewCheckingForUnsavedChanges:^{ - [self.navigationController popViewControllerAnimated:YES]; - }]; + [self leaveViewCheckingForUnsavedChanges]; } -- (void)leaveViewCheckingForUnsavedChanges:(void (^_Nonnull)())leaveViewBlock +- (void)leaveViewCheckingForUnsavedChanges { - OWSAssert(leaveViewBlock); - [self.nameTextField resignFirstResponder]; if (!self.hasUnsavedChanges) { // If user made no changes, return to conversation settings view. - leaveViewBlock(); + [self profileCompletedOrSkipped]; return; } @@ -196,7 +225,7 @@ NS_ASSUME_NONNULL_BEGIN @"The label for the 'discard' button in alerts and action sheets.") style:UIAlertActionStyleDestructive handler:^(UIAlertAction *action) { - leaveViewBlock(); + [self profileCompletedOrSkipped]; }]]; [controller addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", nil) style:UIAlertActionStyleCancel @@ -221,37 +250,39 @@ NS_ASSUME_NONNULL_BEGIN - (void)updateNavigationItem { // The navigation bar is hidden in the registration workflow. - [self.navigationController setNavigationBarHidden:NO animated:YES]; + if (self.navigationController.navigationBarHidden) { + [self.navigationController setNavigationBarHidden:NO animated:YES]; + } - UIBarButtonItem *rightItem = nil; + // Always display a left item to leave the view without making changes. + // This might be a "back", "skip" or "cancel" button depending on the + // context. switch (self.profileViewMode) { case ProfileViewMode_AppSettings: self.navigationItem.leftBarButtonItem = - [self createOWSBackButtonWithTarget:self selector:@selector(backButtonPressed:)]; + [self createOWSBackButtonWithTarget:self selector:@selector(backOrSkipButtonPressed)]; break; - case ProfileViewMode_Registration: case ProfileViewMode_UpgradeOrNag: - // Registration and "upgrade or nag" mode don't need a back button. - self.navigationItem.hidesBackButton = YES; - - // Registration and "upgrade or nag" mode have "skip" or "update". - // - // TODO: Should this be some other verb instead of "update"? - rightItem = [[UIBarButtonItem alloc] + self.navigationItem.leftBarButtonItem = + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel + target:self + action:@selector(backOrSkipButtonPressed)]; + break; + case ProfileViewMode_Registration: + self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"NAVIGATION_ITEM_SKIP_BUTTON", @"A button to skip a view.") style:UIBarButtonItemStylePlain target:self - action:@selector(skipPressed)]; + action:@selector(backOrSkipButtonPressed)]; break; } if (self.hasUnsavedChanges) { - rightItem = [[UIBarButtonItem alloc] - initWithTitle:NSLocalizedString(@"EDIT_GROUP_UPDATE_BUTTON", @"The title for the 'update group' button.") - style:UIBarButtonItemStylePlain - target:self - action:@selector(updatePressed)]; + // If we have a unsaved changes, right item should be a "save" button. + self.navigationItem.rightBarButtonItem = + [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave + target:self + action:@selector(updatePressed)]; } - self.navigationItem.rightBarButtonItem = rightItem; } - (void)updatePressed @@ -308,15 +339,9 @@ NS_ASSUME_NONNULL_BEGIN [self profileCompletedOrSkipped]; } -- (void)skipPressed -{ - [self leaveViewCheckingForUnsavedChanges:^{ - [self profileCompletedOrSkipped]; - }]; -} - - (void)profileCompletedOrSkipped { + // Dismiss this view. switch (self.profileViewMode) { case ProfileViewMode_AppSettings: [self.navigationController popViewControllerAnimated:YES]; @@ -389,6 +414,50 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - AvatarViewHelperDelegate ++ (BOOL)shouldDisplayProfileViewOnLaunch +{ + // Only nag until the user sets a profile _name_. Profile names are + // recommended; profile avatars are optional. + if ([OWSProfileManager sharedManager].localProfileName.length > 0) { + return NO; + } + + NSTimeInterval kProfileNagFrequency = kDayInterval * 30; + NSDate *_Nullable lastPresentedDate = + [[[TSStorageManager sharedManager] dbReadConnection] dateForKey:kProfileView_LastPresentedDate + inCollection:kProfileView_Collection]; + return (!lastPresentedDate || fabs([lastPresentedDate timeIntervalSinceNow]) > kProfileNagFrequency); +} + ++ (void)presentForAppSettings:(UINavigationController *)navigationController +{ + OWSAssert(navigationController); + + ProfileViewController *vc = [[ProfileViewController alloc] initWithMode:ProfileViewMode_AppSettings]; + [navigationController pushViewController:vc animated:YES]; +} + ++ (void)presentForRegistration:(UINavigationController *)navigationController +{ + OWSAssert(navigationController); + + ProfileViewController *vc = [[ProfileViewController alloc] initWithMode:ProfileViewMode_Registration]; + [navigationController pushViewController:vc animated:YES]; +} + ++ (void)presentForUpgradeOrNag:(SignalsViewController *)presentingController +{ + OWSAssert(presentingController); + + ProfileViewController *vc = [[ProfileViewController alloc] initWithMode:ProfileViewMode_UpgradeOrNag]; + UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:vc]; + [presentingController presentTopLevelModalViewController:navigationController + animateDismissal:YES + animatePresentation:YES]; +} + +#pragma mark - AvatarViewHelperDelegate + - (NSString *)avatarActionSheetTitle { return NSLocalizedString( diff --git a/Signal/src/ViewControllers/SignalsViewController.m b/Signal/src/ViewControllers/SignalsViewController.m index 84290579d..9db269e52 100644 --- a/Signal/src/ViewControllers/SignalsViewController.m +++ b/Signal/src/ViewControllers/SignalsViewController.m @@ -10,6 +10,7 @@ #import "MessagesViewController.h" #import "NSDate+millisecondTimeStamp.h" #import "OWSContactsManager.h" +#import "ProfileViewController.h" #import "PropertyListPreferences.h" #import "PushManager.h" #import "Signal-Swift.h" @@ -49,6 +50,7 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; @property (nonatomic) BOOL isViewVisible; @property (nonatomic) BOOL isAppInBackground; @property (nonatomic) BOOL shouldObserveDBModifications; +@property (nonatomic) BOOL hasBeenPresented; // Dependencies @@ -528,7 +530,11 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; completion:^{ [self markAllUpgradeExperiencesAsSeen]; }]; + } else if (!self.hasBeenPresented && [ProfileViewController shouldDisplayProfileViewOnLaunch]) { + [ProfileViewController presentForUpgradeOrNag:self]; } + + self.hasBeenPresented = YES; } - (void)tableViewSetUp { From 08347478a2b2f0a62513b76f1a1e29b84169d049 Mon Sep 17 00:00:00 2001 From: Matthew Chen <charlesmchen@gmail.com> Date: Thu, 17 Aug 2017 12:37:21 -0400 Subject: [PATCH 3/6] Implement alternative approach to veto-able back buttons. // FREEBIE --- Signal.xcodeproj/project.pbxproj | 6 +++ Signal/src/AppDelegate.m | 5 ++- .../ViewControllers/NewGroupViewController.m | 15 ++++++-- .../OWSConversationSettingsViewController.m | 5 +-- .../ViewControllers/OWSNavigationController.h | 20 ++++++++++ .../ViewControllers/OWSNavigationController.m | 37 +++++++++++++++++++ .../ViewControllers/ProfileViewController.m | 16 ++++++-- .../SignalsNavigationController.h | 4 +- .../ViewControllers/SignalsViewController.m | 7 ++-- .../UpdateGroupViewController.m | 19 +++++++--- Signal/src/util/UIViewController+OWS.h | 2 - 11 files changed, 109 insertions(+), 27 deletions(-) create mode 100644 Signal/src/ViewControllers/OWSNavigationController.h create mode 100644 Signal/src/ViewControllers/OWSNavigationController.m diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index e10cb3b11..85a903bd3 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -74,6 +74,7 @@ 34B3F89C1E8DF3270035BE1A /* BlockListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F89B1E8DF3270035BE1A /* BlockListViewController.m */; }; 34B3F89F1E8DF5490035BE1A /* OWSTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F89E1E8DF5490035BE1A /* OWSTableViewController.m */; }; 34B3F8A21E8EA6040035BE1A /* ViewControllerUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8A11E8EA6040035BE1A /* ViewControllerUtils.m */; }; + 34C42D5B1F45F7A80072EC04 /* OWSNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C42D5A1F45F7A80072EC04 /* OWSNavigationController.m */; }; 34CCAF381F0C0599004084F4 /* AppUpdateNag.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CCAF371F0C0599004084F4 /* AppUpdateNag.m */; }; 34CCAF3B1F0C2748004084F4 /* OWSAddToContactViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CCAF3A1F0C2748004084F4 /* OWSAddToContactViewController.m */; }; 34CE88E71F2FB9A10098030F /* ProfileViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CE88E61F2FB9A10098030F /* ProfileViewController.m */; }; @@ -505,6 +506,8 @@ 34B3F89E1E8DF5490035BE1A /* OWSTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSTableViewController.m; sourceTree = "<group>"; }; 34B3F8A01E8EA6040035BE1A /* ViewControllerUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ViewControllerUtils.h; sourceTree = "<group>"; }; 34B3F8A11E8EA6040035BE1A /* ViewControllerUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ViewControllerUtils.m; sourceTree = "<group>"; }; + 34C42D591F45F7A80072EC04 /* OWSNavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSNavigationController.h; sourceTree = "<group>"; }; + 34C42D5A1F45F7A80072EC04 /* OWSNavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSNavigationController.m; sourceTree = "<group>"; }; 34CCAF361F0C0599004084F4 /* AppUpdateNag.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppUpdateNag.h; sourceTree = "<group>"; }; 34CCAF371F0C0599004084F4 /* AppUpdateNag.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppUpdateNag.m; sourceTree = "<group>"; }; 34CCAF391F0C2748004084F4 /* OWSAddToContactViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAddToContactViewController.h; sourceTree = "<group>"; }; @@ -1026,6 +1029,8 @@ 34B3F85F1E8DF1700035BE1A /* OWSLinkedDevicesTableViewController.h */, 34B3F8601E8DF1700035BE1A /* OWSLinkedDevicesTableViewController.m */, 34B3F8611E8DF1700035BE1A /* OWSMessagesToolbarContentView.xib */, + 34C42D591F45F7A80072EC04 /* OWSNavigationController.h */, + 34C42D5A1F45F7A80072EC04 /* OWSNavigationController.m */, 34B3F8621E8DF1700035BE1A /* OWSQRCodeScanningViewController.h */, 34B3F8631E8DF1700035BE1A /* OWSQRCodeScanningViewController.m */, 34B3F89D1E8DF5490035BE1A /* OWSTableViewController.h */, @@ -2307,6 +2312,7 @@ 341BB7491DB727EE001E2975 /* JSQMediaItem+OWS.m in Sources */, 34B3F89C1E8DF3270035BE1A /* BlockListViewController.m in Sources */, 45F2B1941D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.m in Sources */, + 34C42D5B1F45F7A80072EC04 /* OWSNavigationController.m in Sources */, BFB074C919A5611000F2947C /* ObservableValue.m in Sources */, B68EF9BA1C0B1EBD009C3DCD /* FLAnimatedImage.m in Sources */, B68112EA1A4D9EC400BA82FF /* UIImage+normalizeImage.m in Sources */, diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index b97adf95b..c38553cdf 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -11,6 +11,7 @@ #import "NotificationsManager.h" #import "OWSContactsManager.h" #import "OWSContactsSyncing.h" +#import "OWSNavigationController.h" #import "OWSProfileManager.h" #import "OWSStaleNotificationObserver.h" #import "Pastelog.h" @@ -802,8 +803,8 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; self.window.rootViewController = navigationController; } else { RegistrationViewController *viewController = [RegistrationViewController new]; - UINavigationController *navigationController = - [[UINavigationController alloc] initWithRootViewController:viewController]; + OWSNavigationController *navigationController = + [[OWSNavigationController alloc] initWithRootViewController:viewController]; navigationController.navigationBarHidden = YES; self.window.rootViewController = navigationController; } diff --git a/Signal/src/ViewControllers/NewGroupViewController.m b/Signal/src/ViewControllers/NewGroupViewController.m index 11f350826..03ef92704 100644 --- a/Signal/src/ViewControllers/NewGroupViewController.m +++ b/Signal/src/ViewControllers/NewGroupViewController.m @@ -10,6 +10,7 @@ #import "ContactsViewHelper.h" #import "Environment.h" #import "OWSContactsManager.h" +#import "OWSNavigationController.h" #import "OWSTableViewController.h" #import "Signal-Swift.h" #import "SignalKeyingStorage.h" @@ -34,7 +35,8 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; AvatarViewHelperDelegate, AddToGroupViewControllerDelegate, OWSTableViewControllerDelegate, - UINavigationControllerDelegate> + UINavigationControllerDelegate, + OWSNavigationView> @property (nonatomic, readonly) OWSMessageSender *messageSender; @property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper; @@ -97,8 +99,6 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; [super loadView]; self.title = NSLocalizedString(@"NEW_GROUP_DEFAULT_TITLE", @"The navbar title for the 'new group' view."); - self.navigationItem.leftBarButtonItem = - [self createOWSBackButtonWithTarget:self selector:@selector(backButtonPressed:)]; self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"NEW_GROUP_CREATE_BUTTON", @"The title for the 'create group' button.") @@ -547,7 +547,7 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; #pragma mark - Event Handling -- (void)backButtonPressed:(id)sender +- (void)backButtonPressed { [self.groupNameTextField resignFirstResponder]; @@ -649,6 +649,13 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; return [self.memberRecipientIds containsObject:recipientId]; } +#pragma mark - OWSNavigationView + +- (void)navBackButtonPressed +{ + [self backButtonPressed]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/OWSConversationSettingsViewController.m b/Signal/src/ViewControllers/OWSConversationSettingsViewController.m index de045a625..a72897566 100644 --- a/Signal/src/ViewControllers/OWSConversationSettingsViewController.m +++ b/Signal/src/ViewControllers/OWSConversationSettingsViewController.m @@ -827,10 +827,7 @@ NS_ASSUME_NONNULL_BEGIN updateGroupViewController.conversationSettingsViewDelegate = self.conversationSettingsViewDelegate; updateGroupViewController.thread = (TSGroupThread *)self.thread; updateGroupViewController.mode = mode; - - UINavigationController *navigationController = - [[UINavigationController alloc] initWithRootViewController:updateGroupViewController]; - [self presentViewController:navigationController animated:YES completion:nil]; + [self.navigationController pushViewController:updateGroupViewController animated:YES]; } - (void)presentContactViewController diff --git a/Signal/src/ViewControllers/OWSNavigationController.h b/Signal/src/ViewControllers/OWSNavigationController.h new file mode 100644 index 000000000..d8279dacb --- /dev/null +++ b/Signal/src/ViewControllers/OWSNavigationController.h @@ -0,0 +1,20 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import <UIKit/UIKit.h> + +@protocol OWSNavigationView <NSObject> + +- (void)navBackButtonPressed; + +@end + +#pragma mark - + +// This navigation controller subclass should be used anywhere we might +// want to cancel back button presses or back gestures due to, for example, +// unsaved changes. +@interface OWSNavigationController : UINavigationController + +@end diff --git a/Signal/src/ViewControllers/OWSNavigationController.m b/Signal/src/ViewControllers/OWSNavigationController.m new file mode 100644 index 000000000..e475f48a4 --- /dev/null +++ b/Signal/src/ViewControllers/OWSNavigationController.m @@ -0,0 +1,37 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "OWSNavigationController.h" + +@interface OWSNavigationController () + +@end + +#pragma mark - + +@implementation OWSNavigationController + +- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item +{ + + UIViewController *topViewController = self.topViewController; + BOOL wasBackButtonClicked = topViewController.navigationItem == item; + + 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; + } + } else { + // when you call popViewController programmatically you do not want to pop it twice + return YES; + } +} + +@end diff --git a/Signal/src/ViewControllers/ProfileViewController.m b/Signal/src/ViewControllers/ProfileViewController.m index 0230df82d..1c2c20f16 100644 --- a/Signal/src/ViewControllers/ProfileViewController.m +++ b/Signal/src/ViewControllers/ProfileViewController.m @@ -5,6 +5,7 @@ #import "ProfileViewController.h" #import "AppDelegate.h" #import "AvatarViewHelper.h" +#import "OWSNavigationController.h" #import "OWSProfileManager.h" #import "Signal-Swift.h" #import "SignalsNavigationController.h" @@ -27,7 +28,7 @@ typedef NS_ENUM(NSInteger, ProfileViewMode) { NSString *const kProfileView_Collection = @"kProfileView_Collection"; NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDate"; -@interface ProfileViewController () <UITextFieldDelegate, AvatarViewHelperDelegate> +@interface ProfileViewController () <UITextFieldDelegate, AvatarViewHelperDelegate, OWSNavigationView> @property (nonatomic, readonly) AvatarViewHelper *avatarViewHelper; @@ -259,8 +260,6 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat // context. switch (self.profileViewMode) { case ProfileViewMode_AppSettings: - self.navigationItem.leftBarButtonItem = - [self createOWSBackButtonWithTarget:self selector:@selector(backOrSkipButtonPressed)]; break; case ProfileViewMode_UpgradeOrNag: self.navigationItem.leftBarButtonItem = @@ -432,6 +431,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat + (void)presentForAppSettings:(UINavigationController *)navigationController { OWSAssert(navigationController); + OWSAssert([navigationController isKindOfClass:[OWSNavigationController class]]); ProfileViewController *vc = [[ProfileViewController alloc] initWithMode:ProfileViewMode_AppSettings]; [navigationController pushViewController:vc animated:YES]; @@ -440,6 +440,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat + (void)presentForRegistration:(UINavigationController *)navigationController { OWSAssert(navigationController); + OWSAssert([navigationController isKindOfClass:[OWSNavigationController class]]); ProfileViewController *vc = [[ProfileViewController alloc] initWithMode:ProfileViewMode_Registration]; [navigationController pushViewController:vc animated:YES]; @@ -450,7 +451,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat OWSAssert(presentingController); ProfileViewController *vc = [[ProfileViewController alloc] initWithMode:ProfileViewMode_UpgradeOrNag]; - UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:vc]; + OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:vc]; [presentingController presentTopLevelModalViewController:navigationController animateDismissal:YES animatePresentation:YES]; @@ -492,6 +493,13 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat self.avatar = nil; } +#pragma mark - OWSNavigationView + +- (void)navBackButtonPressed +{ + [self backOrSkipButtonPressed]; +} + #pragma mark - Logging + (NSString *)tag diff --git a/Signal/src/ViewControllers/SignalsNavigationController.h b/Signal/src/ViewControllers/SignalsNavigationController.h index 88d3f97ff..7d8b6a041 100644 --- a/Signal/src/ViewControllers/SignalsNavigationController.h +++ b/Signal/src/ViewControllers/SignalsNavigationController.h @@ -2,8 +2,8 @@ // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // -#import <UIKit/UIKit.h> +#import "OWSNavigationController.h" -@interface SignalsNavigationController : UINavigationController +@interface SignalsNavigationController : OWSNavigationController @end diff --git a/Signal/src/ViewControllers/SignalsViewController.m b/Signal/src/ViewControllers/SignalsViewController.m index 9db269e52..86d63a7be 100644 --- a/Signal/src/ViewControllers/SignalsViewController.m +++ b/Signal/src/ViewControllers/SignalsViewController.m @@ -10,6 +10,7 @@ #import "MessagesViewController.h" #import "NSDate+millisecondTimeStamp.h" #import "OWSContactsManager.h" +#import "OWSNavigationController.h" #import "ProfileViewController.h" #import "PropertyListPreferences.h" #import "PushManager.h" @@ -309,7 +310,7 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; - (void)settingsButtonPressed:(id)sender { AppSettingsViewController *vc = [AppSettingsViewController new]; - UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:vc]; + OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:vc]; [self presentViewController:navigationController animated:YES completion:nil]; } @@ -353,8 +354,8 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; // // We just want to make sure contact access is *complete* before showing the compose // screen to avoid flicker. - UINavigationController *navigationController = - [[UINavigationController alloc] initWithRootViewController:viewController]; + OWSNavigationController *navigationController = + [[OWSNavigationController alloc] initWithRootViewController:viewController]; [self presentTopLevelModalViewController:navigationController animateDismissal:YES animatePresentation:YES]; }]; } diff --git a/Signal/src/ViewControllers/UpdateGroupViewController.m b/Signal/src/ViewControllers/UpdateGroupViewController.m index 0bf85eafc..3ebb1edf2 100644 --- a/Signal/src/ViewControllers/UpdateGroupViewController.m +++ b/Signal/src/ViewControllers/UpdateGroupViewController.m @@ -10,6 +10,7 @@ #import "ContactsViewHelper.h" #import "Environment.h" #import "OWSContactsManager.h" +#import "OWSNavigationController.h" #import "OWSTableViewController.h" #import "Signal-Swift.h" #import "SignalKeyingStorage.h" @@ -33,7 +34,8 @@ NS_ASSUME_NONNULL_BEGIN AvatarViewHelperDelegate, AddToGroupViewControllerDelegate, OWSTableViewControllerDelegate, - UINavigationControllerDelegate> + UINavigationControllerDelegate, + OWSNavigationView> @property (nonatomic, readonly) OWSMessageSender *messageSender; @property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper; @@ -103,8 +105,6 @@ NS_ASSUME_NONNULL_BEGIN self.previousMemberRecipientIds = [NSSet setWithArray:self.thread.groupModel.groupMemberIds]; self.title = NSLocalizedString(@"EDIT_GROUP_DEFAULT_TITLE", @"The navbar title for the 'update group' view."); - self.navigationItem.leftBarButtonItem = - [self createOWSBackButtonWithTarget:self selector:@selector(backButtonPressed:)]; // First section. @@ -409,13 +409,13 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Event Handling -- (void)backButtonPressed:(id)sender +- (void)backButtonPressed { [self.groupNameTextField resignFirstResponder]; if (!self.hasUnsavedChanges) { // If user made no changes, return to conversation settings view. - [self dismissViewControllerAnimated:YES completion:nil]; + [self.navigationController popViewControllerAnimated:YES]; return; } @@ -441,7 +441,7 @@ NS_ASSUME_NONNULL_BEGIN @"The label for the 'don't save' button in action sheets.") style:UIAlertActionStyleDestructive handler:^(UIAlertAction *action) { - [self dismissViewControllerAnimated:YES completion:nil]; + [self.navigationController popViewControllerAnimated:YES]; }]]; [self presentViewController:controller animated:YES completion:nil]; } @@ -528,6 +528,13 @@ NS_ASSUME_NONNULL_BEGIN return [self.memberRecipientIds containsObject:recipientId]; } +#pragma mark - OWSNavigationView + +- (void)navBackButtonPressed +{ + [self backButtonPressed]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/util/UIViewController+OWS.h b/Signal/src/util/UIViewController+OWS.h index 5bbd8a077..ac00b42e9 100644 --- a/Signal/src/util/UIViewController+OWS.h +++ b/Signal/src/util/UIViewController+OWS.h @@ -17,8 +17,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (UIBarButtonItem *)createOWSBackButton; -- (UIBarButtonItem *)createOWSBackButtonWithTarget:(id)target selector:(SEL)selector; - @end NS_ASSUME_NONNULL_END From 25b0f79615e541879d8dc3f6ef6a431ebf97eb1c Mon Sep 17 00:00:00 2001 From: Matthew Chen <charlesmchen@gmail.com> Date: Fri, 18 Aug 2017 09:58:16 -0400 Subject: [PATCH 4/6] Rework "cancel navigate back" logic. // FREEBIE --- .../ConversationView/MessagesViewController.m | 9 --- .../ViewControllers/NewGroupViewController.m | 12 +++- .../ViewControllers/OWSNavigationController.h | 4 +- .../ViewControllers/OWSNavigationController.m | 61 +++++++++++++++---- .../ViewControllers/ProfileViewController.m | 13 +++- .../UpdateGroupViewController.m | 12 +++- 6 files changed, 80 insertions(+), 31 deletions(-) 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<NSString *> *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 <UIKit/UIKit.h> +// Any view controller which wants to be able cancel back button +// presses and back gestures should implement this protocol. @protocol OWSNavigationView <NSObject> -- (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) <UINavigationBarDelegate> + +@end + +#pragma mark - + +@interface OWSNavigationController () <UIGestureRecognizerDelegate> @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<OWSNavigationView> navigationView = (id<OWSNavigationView>)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<OWSNavigationView> navigationView = (id<OWSNavigationView>)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<NSString *> *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 From 1b055c485d81e5f587774b2f014c47beabd9f4d2 Mon Sep 17 00:00:00 2001 From: Matthew Chen <charlesmchen@gmail.com> Date: Fri, 18 Aug 2017 10:02:45 -0400 Subject: [PATCH 5/6] Rework "cancel navigate back" logic. // FREEBIE --- Signal/src/ViewControllers/NewGroupViewController.m | 8 +++----- Signal/src/ViewControllers/OWSNavigationController.h | 2 ++ Signal/src/ViewControllers/OWSNavigationController.m | 2 ++ Signal/src/ViewControllers/ProfileViewController.m | 7 ++----- Signal/src/ViewControllers/UpdateGroupViewController.m | 6 ++---- 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/Signal/src/ViewControllers/NewGroupViewController.m b/Signal/src/ViewControllers/NewGroupViewController.m index 1dd609b1b..7af2fbaab 100644 --- a/Signal/src/ViewControllers/NewGroupViewController.m +++ b/Signal/src/ViewControllers/NewGroupViewController.m @@ -50,7 +50,6 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; @property (nonatomic) NSMutableSet<NSString *> *memberRecipientIds; @property (nonatomic) BOOL hasUnsavedChanges; -@property (nonatomic) BOOL shouldIgnoreSavedChanges; @property (nonatomic) BOOL hasAppeared; @end @@ -552,7 +551,7 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; { [self.groupNameTextField resignFirstResponder]; - if (!self.hasUnsavedChanges || self.shouldIgnoreSavedChanges) { + if (!self.hasUnsavedChanges) { // If user made no changes, return to conversation settings view. [self.navigationController popViewControllerAnimated:YES]; return; @@ -571,7 +570,6 @@ 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) @@ -655,8 +653,8 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; - (BOOL)shouldCancelNavigationBack { - BOOL result = self.hasUnsavedChanges && !self.shouldIgnoreSavedChanges; - if (result) { + BOOL result = self.hasUnsavedChanges; + if (self.hasUnsavedChanges) { [self backButtonPressed]; } return result; diff --git a/Signal/src/ViewControllers/OWSNavigationController.h b/Signal/src/ViewControllers/OWSNavigationController.h index a47b602df..26900edde 100644 --- a/Signal/src/ViewControllers/OWSNavigationController.h +++ b/Signal/src/ViewControllers/OWSNavigationController.h @@ -8,6 +8,8 @@ // presses and back gestures should implement this protocol. @protocol OWSNavigationView <NSObject> +// shouldCancelNavigationBack will be called if the back button was pressed or +// if a back gesture was performed but not if the view is popped programmatically. - (BOOL)shouldCancelNavigationBack; @end diff --git a/Signal/src/ViewControllers/OWSNavigationController.m b/Signal/src/ViewControllers/OWSNavigationController.m index b1fe079ab..0f21f71d0 100644 --- a/Signal/src/ViewControllers/OWSNavigationController.m +++ b/Signal/src/ViewControllers/OWSNavigationController.m @@ -37,6 +37,8 @@ OWSAssert(self.interactivePopGestureRecognizer.delegate == self); UIViewController *topViewController = self.topViewController; + // wasBackButtonClicked is YES if the back button was pressed but not + // if a back gesture was performed or if the view is popped programmatically. BOOL wasBackButtonClicked = topViewController.navigationItem == item; BOOL result = YES; if (wasBackButtonClicked) { diff --git a/Signal/src/ViewControllers/ProfileViewController.m b/Signal/src/ViewControllers/ProfileViewController.m index 40f0a7ce5..b48343bc9 100644 --- a/Signal/src/ViewControllers/ProfileViewController.m +++ b/Signal/src/ViewControllers/ProfileViewController.m @@ -44,8 +44,6 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat @property (nonatomic) BOOL hasUnsavedChanges; -@property (nonatomic) BOOL shouldIgnoreSavedChanges; - @property (nonatomic) ProfileViewMode profileViewMode; @property (nonatomic) YapDatabaseConnection *databaseConnection; @@ -209,7 +207,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat { [self.nameTextField resignFirstResponder]; - if (!self.hasUnsavedChanges || self.shouldIgnoreSavedChanges) { + if (!self.hasUnsavedChanges) { // If user made no changes, return to conversation settings view. [self profileCompletedOrSkipped]; return; @@ -228,7 +226,6 @@ 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) @@ -500,7 +497,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat - (BOOL)shouldCancelNavigationBack { - BOOL result = self.hasUnsavedChanges && !self.shouldIgnoreSavedChanges; + BOOL result = self.hasUnsavedChanges; if (result) { [self backOrSkipButtonPressed]; } diff --git a/Signal/src/ViewControllers/UpdateGroupViewController.m b/Signal/src/ViewControllers/UpdateGroupViewController.m index 27ad12c2c..384367ebc 100644 --- a/Signal/src/ViewControllers/UpdateGroupViewController.m +++ b/Signal/src/ViewControllers/UpdateGroupViewController.m @@ -50,7 +50,6 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) NSMutableSet<NSString *> *memberRecipientIds; @property (nonatomic) BOOL hasUnsavedChanges; -@property (nonatomic) BOOL shouldIgnoreSavedChanges; @end @@ -414,7 +413,7 @@ NS_ASSUME_NONNULL_BEGIN { [self.groupNameTextField resignFirstResponder]; - if (!self.hasUnsavedChanges || self.shouldIgnoreSavedChanges) { + if (!self.hasUnsavedChanges) { // If user made no changes, return to conversation settings view. [self.navigationController popViewControllerAnimated:YES]; return; @@ -442,7 +441,6 @@ 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]; @@ -534,7 +532,7 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)shouldCancelNavigationBack { - BOOL result = self.hasUnsavedChanges && !self.shouldIgnoreSavedChanges; + BOOL result = self.hasUnsavedChanges; if (result) { [self backButtonPressed]; } From 27e496ad06593cdd17f12af60945286c7dbabbee Mon Sep 17 00:00:00 2001 From: Matthew Chen <charlesmchen@gmail.com> Date: Fri, 18 Aug 2017 10:11:37 -0400 Subject: [PATCH 6/6] Respond to CR. // FREEBIE --- .../src/ViewControllers/ProfileViewController.m | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Signal/src/ViewControllers/ProfileViewController.m b/Signal/src/ViewControllers/ProfileViewController.m index b48343bc9..eb14108f8 100644 --- a/Signal/src/ViewControllers/ProfileViewController.m +++ b/Signal/src/ViewControllers/ProfileViewController.m @@ -46,8 +46,6 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat @property (nonatomic) ProfileViewMode profileViewMode; -@property (nonatomic) YapDatabaseConnection *databaseConnection; - @end #pragma mark - @@ -63,11 +61,11 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat } self.profileViewMode = profileViewMode; - self.databaseConnection = [[TSStorageManager sharedManager] newDatabaseConnection]; - [self.databaseConnection setDate:[NSDate new] - forKey:kProfileView_LastPresentedDate - inCollection:kProfileView_Collection]; + // Use the TSStorageManager.dbReadWriteConnection for consistency with the reads below. + [[[TSStorageManager sharedManager] dbReadWriteConnection] setDate:[NSDate new] + forKey:kProfileView_LastPresentedDate + inCollection:kProfileView_Collection]; return self; } @@ -421,10 +419,11 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat return NO; } + // Use the TSStorageManager.dbReadWriteConnection for consistency with the writes above. NSTimeInterval kProfileNagFrequency = kDayInterval * 30; NSDate *_Nullable lastPresentedDate = - [[[TSStorageManager sharedManager] dbReadConnection] dateForKey:kProfileView_LastPresentedDate - inCollection:kProfileView_Collection]; + [[[TSStorageManager sharedManager] dbReadWriteConnection] dateForKey:kProfileView_LastPresentedDate + inCollection:kProfileView_Collection]; return (!lastPresentedDate || fabs([lastPresentedDate timeIntervalSinceNow]) > kProfileNagFrequency); }