Skip to content

Commit

Permalink
SelectorCoordinator for presenting a picker view from an infobar
Browse files Browse the repository at this point in the history
Add coordinator for presenting a picker view from an infobar, as needed
in update passwords where there are multiple accounts, or in the
translate infobar for language selection. Starting the coordinator
animates the picker view in from the bottom edge of the
baseViewController's view, and stopping animates it out.

BUG=622244

Review-Url: https://codereview.chromium.org/2105253004
Cr-Commit-Position: refs/heads/master@{#403780}
  • Loading branch information
jyquinn authored and Commit bot committed Jul 5, 2016
1 parent 0e19eef commit b7a60b6
Show file tree
Hide file tree
Showing 11 changed files with 507 additions and 5 deletions.
2 changes: 2 additions & 0 deletions ios/chrome/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ test("ios_chrome_unittests") {
"browser/translate/translate_service_ios_unittest.cc",
"browser/ui/commands/set_up_for_testing_command_unittest.mm",
"browser/ui/context_menu/context_menu_coordinator_unittest.mm",
"browser/ui/elements/selector_coordinator_unittest.mm",
"browser/ui/elements/selector_picker_view_controller_unittest.mm",
"browser/ui/keyboard/UIKeyCommand+ChromeTest.mm",
"browser/ui/keyboard/hardware_keyboard_watcher_unittest.mm",
"browser/ui/native_content_controller_unittest.mm",
Expand Down
5 changes: 5 additions & 0 deletions ios/chrome/browser/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,11 @@ source_set("browser") {
"ui/commands/show_signin_command.mm",
"ui/context_menu/context_menu_coordinator.h",
"ui/context_menu/context_menu_coordinator.mm",
"ui/elements/selector_coordinator.h",
"ui/elements/selector_coordinator.mm",
"ui/elements/selector_picker_view_controller.h",
"ui/elements/selector_picker_view_controller.mm",
"ui/elements/selector_view_controller_delegate.h",
"ui/file_locations.h",
"ui/file_locations.mm",
"ui/image_util.h",
Expand Down
33 changes: 33 additions & 0 deletions ios/chrome/browser/ui/elements/selector_coordinator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef IOS_CHROME_BROWSER_UI_ELEMENTS_SELECTOR_COORDINATOR_H_
#define IOS_CHROME_BROWSER_UI_ELEMENTS_SELECTOR_COORDINATOR_H_

#import "ios/chrome/browser/chrome_coordinator.h"

@class SelectorCoordinator;

// Delegate protocol for SelectorCoordinator
@protocol SelectorCoordinatorDelegate<NSObject>
// Called when selection UI has completed.
- (void)selectorCoordinator:(nonnull SelectorCoordinator*)coordinator
didCompleteWithSelection:(nonnull NSString*)selection;
@end

// Coordinator for displaying UI to allow the user to pick among options.
@interface SelectorCoordinator : ChromeCoordinator

// Options to present to the user.
@property(nonatomic, nullable, assign) NSOrderedSet<NSString*>* options;

// The default option. Starts out selected, and is set as the selected option
// if the user performs a cancel action.
@property(nonatomic, nullable, copy) NSString* defaultOption;

@property(nonatomic, nullable, assign) id<SelectorCoordinatorDelegate> delegate;

@end

#endif // IOS_CHROME_BROWSER_UI_ELEMENTS_SELECTOR_COORDINATOR_H_
70 changes: 70 additions & 0 deletions ios/chrome/browser/ui/elements/selector_coordinator.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/ui/elements/selector_coordinator.h"

#import "base/mac/objc_property_releaser.h"
#import "ios/chrome/browser/ui/elements/selector_picker_view_controller.h"
#import "ios/chrome/browser/ui/elements/selector_view_controller_delegate.h"

@interface SelectorCoordinator ()<SelectorViewControllerDelegate> {
base::mac::ObjCPropertyReleaser _propertyReleaser_SelectorCoordinator;
__unsafe_unretained id<SelectorCoordinatorDelegate> _delegate;
__unsafe_unretained NSOrderedSet<NSString*>* _options;
}

// Redeclaration of infoBarPickerController as readwrite.
@property(nonatomic, nullable, retain)
SelectorPickerViewController* selectorPickerViewController;

@end

@implementation SelectorCoordinator

@synthesize options = _options;
@synthesize defaultOption = _defaultOption;
@synthesize delegate = _delegate;
@synthesize selectorPickerViewController = _selectorPickerViewController;

- (nullable instancetype)initWithBaseViewController:
(nullable UIViewController*)viewController {
self = [super initWithBaseViewController:viewController];
if (self) {
_propertyReleaser_SelectorCoordinator.Init(self,
[SelectorCoordinator class]);
}
return self;
}

- (void)start {
self.selectorPickerViewController =
[[SelectorPickerViewController alloc] initWithOptions:self.options
default:self.defaultOption];
self.selectorPickerViewController.delegate = self;
// TODO(crbug.com/622244): Display via custom UIPresentionController to
// show as a bottom sheet.
self.selectorPickerViewController.modalTransitionStyle =
UIModalTransitionStyleCoverVertical;
self.selectorPickerViewController.modalPresentationStyle =
UIModalPresentationFormSheet;
[self.baseViewController
presentViewController:self.selectorPickerViewController
animated:YES
completion:nil];
}

- (void)stop {
[self.selectorPickerViewController dismissViewControllerAnimated:YES
completion:nil];
}

#pragma mark SelectorViewControllerDelegate

- (void)selectorViewController:(UIViewController*)viewController
didSelectOption:(NSString*)option {
[self.delegate selectorCoordinator:self didCompleteWithSelection:option];
[self stop];
}

@end
70 changes: 70 additions & 0 deletions ios/chrome/browser/ui/elements/selector_coordinator_unittest.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/ui/elements/selector_coordinator.h"

#include "base/test/ios/wait_util.h"
#import "ios/chrome/browser/ui/elements/selector_picker_view_controller.h"
#import "ios/chrome/browser/ui/elements/selector_view_controller_delegate.h"
#include "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#include "third_party/ocmock/OCMock/OCMock.h"
#include "third_party/ocmock/gtest_support.h"

@interface SelectorCoordinator ()<SelectorViewControllerDelegate>
// The view controller for the picker view the coordinator presents. Exposed for
// testing.
@property SelectorPickerViewController* selectorPickerViewController;
@end

// Tests that invoking start on the coordinator presents the selector view, and
// that invoking stop dismisses the view and invokes the delegate.
TEST(SelectorCoordinatorTest, StartAndStop) {
UIWindow* keyWindow = [[UIApplication sharedApplication] keyWindow];
UIViewController* rootViewController = keyWindow.rootViewController;
SelectorCoordinator* coordinator = [[[SelectorCoordinator alloc]
initWithBaseViewController:rootViewController] autorelease];

void (^testSteps)(void) = ^{
[coordinator start];
EXPECT_NSEQ(coordinator.selectorPickerViewController,
rootViewController.presentedViewController);

[coordinator stop];
base::test::ios::WaitUntilCondition(^{
return !rootViewController.presentedViewController;
});
};
// Ensure any other presented controllers are dismissed before starting the
// coordinator.
[rootViewController dismissViewControllerAnimated:NO completion:testSteps];
}

// Tests that calling the view controller delegate method invokes the
// SelectorCoordinatorDelegate method and stops the coordinator.
TEST(SelectorCoordinatorTest, Delegate) {
UIWindow* keyWindow = [[UIApplication sharedApplication] keyWindow];
UIViewController* rootViewController = keyWindow.rootViewController;
SelectorCoordinator* coordinator = [[[SelectorCoordinator alloc]
initWithBaseViewController:rootViewController] autorelease];
id delegate =
[OCMockObject mockForProtocol:@protocol(SelectorCoordinatorDelegate)];
coordinator.delegate = delegate;

void (^testSteps)(void) = ^{
[coordinator start];
NSString* testOption = @"Test Option";
[[delegate expect] selectorCoordinator:coordinator
didCompleteWithSelection:testOption];
[coordinator selectorViewController:coordinator.selectorPickerViewController
didSelectOption:testOption];
base::test::ios::WaitUntilCondition(^{
return !rootViewController.presentedViewController;
});
};
// Ensure any other presented controllers are dismissed before starting the
// coordinator.
[rootViewController dismissViewControllerAnimated:NO completion:testSteps];
EXPECT_OCMOCK_VERIFY(delegate);
}
30 changes: 30 additions & 0 deletions ios/chrome/browser/ui/elements/selector_picker_view_controller.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef IOS_CHROME_BROWSER_UI_ELEMENTS_SELECTOR_PICKER_VIEW_CONTROLLER_H_
#define IOS_CHROME_BROWSER_UI_ELEMENTS_SELECTOR_PICKER_VIEW_CONTROLLER_H_

#import <UIKit/UIKit.h>

@protocol SelectorViewControllerDelegate;

// View controller for displaying a UIPickerView topped by a UINavigationBar
// displaying "Done" on the right and "Cancel" on the left.
@interface SelectorPickerViewController : UIViewController

// Initializer for view controller that will display |options|. |defaultOptions|
// will be selected initially, and pressing cancel will set |defaultOption| as
// the selected option.
- (instancetype)initWithOptions:(NSOrderedSet<NSString*>*)options
default:(NSString*)defaultOption
NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithNibName:(NSString*)nibNameOrNil
bundle:(NSBundle*)nibBundleOrNil NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder*)aDecoder NS_UNAVAILABLE;

@property(nonatomic, weak) id<SelectorViewControllerDelegate> delegate;

@end

#endif // IOS_CHROME_BROWSER_UI_ELEMENTS_SELECTOR_PICKER_VIEW_CONTROLLER_H_
157 changes: 157 additions & 0 deletions ios/chrome/browser/ui/elements/selector_picker_view_controller.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/ui/elements/selector_picker_view_controller.h"

#import "ios/chrome/browser/ui/elements/selector_view_controller_delegate.h"
#include "base/logging.h"
#import "base/mac/objc_property_releaser.h"

namespace {
// Font size of text in the picker view.
CGFloat kUIPickerFontSize = 26;
} // namespace

@interface SelectorPickerViewController ()<UIPickerViewDelegate,
UIPickerViewDataSource> {
base::mac::ObjCPropertyReleaser
_propertyReleaser_SelectorPickerViewController;
__unsafe_unretained id<SelectorViewControllerDelegate> _delegate;
}
// Options to display.
@property(nonatomic, copy) NSOrderedSet<NSString*>* options;
// The default option.
@property(nonatomic, copy) NSString* defaultOption;
// The displayed UINavigationBar. Exposed for testing.
@property(nonatomic, retain) UINavigationBar* navigationBar;
// The displayed UIPickerView. Exposed for testing.
@property(nonatomic, retain) UIPickerView* pickerView;
// Action for the "Done" button.
- (void)doneButtonPressed;
// Action for the "Cancel" button.
- (void)cancelButtonPressed;
@end

@implementation SelectorPickerViewController

@synthesize pickerView = _pickerView;
@synthesize navigationBar = _navigationBar;

@synthesize options = _options;
@synthesize defaultOption = _defaultOption;

- (instancetype)initWithOptions:(NSOrderedSet<NSString*>*)options
default:(NSString*)defaultOption {
self = [super initWithNibName:nil bundle:nil];
if (self) {
_propertyReleaser_SelectorPickerViewController.Init(
self, [SelectorPickerViewController class]);
_options = [options copy];
_defaultOption = [defaultOption copy];
}
return self;
}

- (instancetype)initWithNibName:(NSString*)nibName bundle:(NSBundle*)nibBundle {
NOTREACHED();
return nil;
}

- (instancetype)initWithCoder:(NSCoder*)aDecoder {
NOTREACHED();
return nil;
}

- (void)loadView {
self.pickerView = [[UIPickerView alloc] initWithFrame:CGRectZero];
self.navigationBar = [[UINavigationBar alloc] initWithFrame:CGRectZero];
self.pickerView.translatesAutoresizingMaskIntoConstraints = NO;
self.navigationBar.translatesAutoresizingMaskIntoConstraints = NO;
UIStackView* stackView = [[[UIStackView alloc]
initWithArrangedSubviews:@[ self.navigationBar, self.pickerView ]]
autorelease];
stackView.axis = UILayoutConstraintAxisVertical;
stackView.translatesAutoresizingMaskIntoConstraints = NO;
self.view = stackView;
}

- (void)viewDidLoad {
self.view.backgroundColor = [UIColor whiteColor];

self.pickerView.showsSelectionIndicator = YES;
self.pickerView.backgroundColor = [UIColor whiteColor];
self.pickerView.delegate = self;
self.pickerView.dataSource = self;
NSInteger initialIndex = [self.options indexOfObject:self.defaultOption];
if (initialIndex != NSNotFound) {
[self.pickerView selectRow:initialIndex inComponent:0 animated:NO];
}

UINavigationItem* navigationItem =
[[[UINavigationItem alloc] initWithTitle:@""] autorelease];
UIBarButtonItem* doneButton = [[[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
target:nil
action:@selector(doneButtonPressed)] autorelease];
UIBarButtonItem* cancelButton = [[[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
target:nil
action:@selector(cancelButtonPressed)] autorelease];
[navigationItem setRightBarButtonItem:doneButton];
[navigationItem setLeftBarButtonItem:cancelButton];
[navigationItem setHidesBackButton:YES];
[self.navigationBar pushNavigationItem:navigationItem animated:NO];
}

- (id<SelectorViewControllerDelegate>)delegate {
return _delegate;
}

- (void)setDelegate:(id<SelectorViewControllerDelegate>)delegate {
_delegate = delegate;
}

#pragma mark UIPickerViewDataSource

- (NSInteger)numberOfComponentsInPickerView:(UIPickerView*)pickerView {
return 1;
}

- (NSInteger)pickerView:(UIPickerView*)pickerView
numberOfRowsInComponent:(NSInteger)component {
return [self.options count];
}

#pragma mark UIPickerViewDelegate

- (UIView*)pickerView:(UIPickerView*)pickerView
viewForRow:(NSInteger)row
forComponent:(NSInteger)component
reusingView:(UIView*)view {
DCHECK_EQ(0, component);
UILabel* label = [view isKindOfClass:[UILabel class]]
? (UILabel*)view
: [[[UILabel alloc] init] autorelease];
NSString* text = self.options[row];
label.text = text;
label.textAlignment = NSTextAlignmentCenter;
label.font = [text isEqualToString:self.defaultOption]
? [UIFont boldSystemFontOfSize:kUIPickerFontSize]
: [UIFont systemFontOfSize:kUIPickerFontSize];
return label;
}

#pragma mark Private methods

- (void)doneButtonPressed {
NSInteger selectedIndex = [self.pickerView selectedRowInComponent:0];
[_delegate selectorViewController:self
didSelectOption:self.options[selectedIndex]];
}

- (void)cancelButtonPressed {
[_delegate selectorViewController:self didSelectOption:self.defaultOption];
}

@end
Loading

0 comments on commit b7a60b6

Please sign in to comment.