Commit 082af3c7 authored by Ioana Pandele's avatar Ioana Pandele Committed by Commit Bot

Add progress indicator for password export

This is an alert with the title "Preparing passwords..." displayed after the reauthentication
step is finished, and while passwords are being serialized and written to the temporary file.
The alert offers the user the option to cancel the exporting operation.

If the user cancels, the exporter will wait for the async tasks to finish, perform necessary
clean-up and then become available again.

Bug: 789122
Cq-Include-Trybots: master.tryserver.chromium.mac:ios-simulator-cronet;master.tryserver.chromium.mac:ios-simulator-full-configs
Change-Id: I8dd6b9a094e055eef15f5580e3d887e5cf979408
Reviewed-on: https://chromium-review.googlesource.com/897607Reviewed-by: default avatarSylvain Defresne <sdefresne@chromium.org>
Commit-Queue: Ioana Pandele <ioanap@chromium.org>
Cr-Commit-Position: refs/heads/master@{#534046}
parent 3180cc44
......@@ -1072,6 +1072,9 @@ Handoff must also be enabled in the General section of Settings, and your device
<message name="IDS_IOS_EXPORT_PASSWORDS_ALERT_MESSAGE" desc="The message of the alert displayed as a warning to the user who tapped on the button to export passwords. [iOS only]">
Your passwords will be visible to anyone who can see the exported file.
</message>
<message name="IDS_IOS_EXPORT_PASSWORDS_PREPARING_ALERT_TITLE" desc="The title of the alert displayed to indicate that the passwords are being prepared for export. [iOS only]">
Preparing Passwords...
</message>
<message name="IDS_IOS_EXPORT_PASSWORDS_FAILED_ALERT_TITLE" desc="Title of the alert informing the user that exporting passwords has failed. [iOS only]">
Can't Export Passwords
</message>
......
......@@ -20,6 +20,12 @@ enum class WriteToURLStatus {
UNKNOWN_ERROR,
};
enum class ExportState {
IDLE,
ONGOING,
CANCELLING,
};
@protocol ReauthenticationProtocol;
@protocol FileWriterProtocol<NSObject>
......@@ -48,6 +54,10 @@ enum class WriteToURLStatus {
// in order to export passwords.
- (void)showSetPasscodeDialog;
// Displays an alert which informs the user that the passwords are being
// prepared to be exported and gives them the option of cancelling the export.
- (void)showPreparingPasswordsAlert;
// Displays an alert detailing an error that has occured during export.
- (void)showExportErrorAlertWithLocalizedReason:(NSString*)errorReason;
......@@ -60,6 +70,9 @@ enum class WriteToURLStatus {
NSArray* returnedItems,
NSError* activityError))completionHandler;
// Enables or disables the export button based on the export state.
- (void)updateExportPasswordsButton;
@end
/** Class handling all the operations necessary to export passwords.*/
......@@ -80,8 +93,11 @@ enum class WriteToURLStatus {
- (void)startExportFlow:
(std::vector<std::unique_ptr<autofill::PasswordForm>>)passwords;
// Whether an export operation is already in progress.
@property(nonatomic, assign, readonly) BOOL isExporting;
// Called when the user cancels the export operation.
- (void)cancelExport;
// State of the export operation.
@property(nonatomic, readonly, assign) ExportState exportState;
@end
......
......@@ -104,16 +104,15 @@ enum class ReauthenticationStatus {
@property(nonatomic, assign) BOOL serializingFinished;
// String containing serialized password forms.
@property(nonatomic, copy) NSString* serializedPasswords;
// Whether an export operation is ongoing. This is a readwrite property
// corresponding to the public readonly property.
@property(nonatomic, assign) BOOL isExporting;
// The exporter state.
@property(nonatomic, assign) ExportState exportState;
@end
@implementation PasswordExporter
// Public synthesized properties
@synthesize isExporting = _isExporting;
@synthesize exportState = _exportState;
// Private synthesized properties
@synthesize reauthenticationStatus = _reauthenticationStatus;
......@@ -142,8 +141,11 @@ enum class ReauthenticationStatus {
- (void)startExportFlow:
(std::vector<std::unique_ptr<autofill::PasswordForm>>)passwords {
DCHECK(!passwords.empty());
DCHECK(self.exportState == ExportState::IDLE);
if ([_weakReauthenticationModule canAttemptReauth]) {
self.isExporting = YES;
self.exportState = ExportState::ONGOING;
[_weakDelegate updateExportPasswordsButton];
[self serializePasswords:std::move(passwords)];
[self startReauthentication];
} else {
......@@ -151,6 +153,10 @@ enum class ReauthenticationStatus {
}
}
- (void)cancelExport {
self.exportState = ExportState::CANCELLING;
}
#pragma mark - Private methods
- (void)showExportErrorAlertWithLocalizedReason:(NSString*)errorReason {
......@@ -183,6 +189,7 @@ enum class ReauthenticationStatus {
return;
if (success) {
strongSelf.reauthenticationStatus = ReauthenticationStatus::SUCCESSFUL;
[strongSelf showPreparingPasswordsAlert];
} else {
strongSelf.reauthenticationStatus = ReauthenticationStatus::FAILED;
}
......@@ -196,6 +203,10 @@ enum class ReauthenticationStatus {
handler:onReauthenticationFinished];
}
- (void)showPreparingPasswordsAlert {
[_weakDelegate showPreparingPasswordsAlert];
}
- (void)tryExporting {
if (!self.serializingFinished)
return;
......@@ -217,10 +228,15 @@ enum class ReauthenticationStatus {
self.serializingFinished = NO;
self.serializedPasswords = nil;
self.reauthenticationStatus = ReauthenticationStatus::PENDING;
self.isExporting = NO;
self.exportState = ExportState::IDLE;
[_weakDelegate updateExportPasswordsButton];
}
- (void)writePasswordsToFile {
if (self.exportState == ExportState::CANCELLING) {
[self resetExportState];
return;
}
NSURL* tempPasswordsFileURL =
[[NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES]
URLByAppendingPathComponent:_tempPasswordsFileName
......@@ -232,6 +248,10 @@ enum class ReauthenticationStatus {
if (!strongSelf) {
return;
}
if (strongSelf.exportState == ExportState::CANCELLING) {
[strongSelf resetExportState];
return;
}
switch (status) {
case WriteToURLStatus::SUCCESS:
[strongSelf showActivityView];
......@@ -284,7 +304,12 @@ enum class ReauthenticationStatus {
[[NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES]
URLByAppendingPathComponent:_tempPasswordsFileName
isDirectory:NO];
if (self.exportState == ExportState::CANCELLING) {
// Initiate cleanup. Once the file is deleted, the export state will be
// reset;
[self deleteTemporaryFile:passwordsTempFileURL];
return;
}
__weak PasswordExporter* weakSelf = self;
[_weakDelegate
showActivityViewWithActivityItems:@[ passwordsTempFileURL ]
......
......@@ -39,26 +39,38 @@
}
- (void)executeHandler {
_serializedPasswordsHandler("test serialized passwords");
_serializedPasswordsHandler("test serialized passwords string");
}
@end
@interface FakePasswordFileWriter : NSObject<FileWriterProtocol>
// Allows for on demand execution of the block that should be executed after
// the file has finished writing.
- (void)executeHandler;
// Indicates if the writing of the file was finished successfully or with an
// error.
@property(nonatomic, assign) WriteToURLStatus writingStatus;
@end
@implementation FakePasswordFileWriter
@implementation FakePasswordFileWriter {
// Handler executed after the file write operation finishes.
void (^_writeStatusHandler)(WriteToURLStatus);
}
@synthesize writingStatus = _writingStatus;
- (void)writeData:(NSString*)data
toURL:(NSURL*)fileURL
handler:(void (^)(WriteToURLStatus))handler {
handler(self.writingStatus);
_writeStatusHandler = handler;
}
- (void)executeHandler {
_writeStatusHandler(self.writingStatus);
}
@end
......@@ -120,7 +132,8 @@ class PasswordExporterTest : public PlatformTest {
base::test::ScopedTaskEnvironment scoped_task_environment_;
};
TEST_F(PasswordExporterTest, PasswordFileWriteAuthSuccessful) {
// Tests that the passwords file is written if reauthentication is successful.
TEST_F(PasswordExporterTest, PasswordFileWriteReauthSucceeded) {
mock_reauthentication_module_.shouldSucceed = YES;
NSURL* passwords_file_url = GetPasswordsFileURL();
......@@ -133,11 +146,116 @@ TEST_F(PasswordExporterTest, PasswordFileWriteAuthSuccessful) {
// Wait for all asynchronous tasks to complete.
scoped_task_environment_.RunUntilIdle();
EXPECT_OCMOCK_VERIFY(password_exporter_delegate_);
EXPECT_TRUE(PasswordFileExists());
EXPECT_OCMOCK_VERIFY(password_exporter_delegate_);
}
// Tests that the exporter becomes idle after the export finishes.
TEST_F(PasswordExporterTest, ExportIdleAfterFinishing) {
mock_reauthentication_module_.shouldSucceed = YES;
NSURL* passwords_file_url = GetPasswordsFileURL();
OCMStub(
[password_exporter_delegate_
showActivityViewWithActivityItems:[OCMArg
isEqual:@[ passwords_file_url ]]
completionHandler:[OCMArg any]])
.andDo(^(NSInvocation* invocation) {
void (^completionHandler)(NSString* activityType, BOOL completed,
NSArray* returnedItems,
NSError* activityError);
[invocation getArgument:&completionHandler atIndex:3];
// Since the completion handler doesn't use any of the
// passed in parameters, dummy arguments are passed for
// convenience.
completionHandler(@"", YES, @[], nil);
});
[password_exporter_ startExportFlow:CreatePasswordList()];
// Wait for all asynchronous tasks to complete.
scoped_task_environment_.RunUntilIdle();
EXPECT_EQ(ExportState::IDLE, password_exporter_.exportState);
}
// Tests that if the file writing fails because of not enough disk space
// the appropriate error is displayed and the export operation
// is interrupted.
TEST_F(PasswordExporterTest, WritingFailedOutOfDiskSpace) {
mock_reauthentication_module_.shouldSucceed = YES;
FakePasswordFileWriter* fake_password_file_writer =
[[FakePasswordFileWriter alloc] init];
fake_password_file_writer.writingStatus =
WriteToURLStatus::OUT_OF_DISK_SPACE_ERROR;
[password_exporter_ setPasswordFileWriter:fake_password_file_writer];
OCMExpect([password_exporter_delegate_
showExportErrorAlertWithLocalizedReason:
l10n_util::GetNSString(
IDS_IOS_EXPORT_PASSWORDS_OUT_OF_SPACE_ALERT_MESSAGE)]);
[[password_exporter_delegate_ reject]
showActivityViewWithActivityItems:[OCMArg any]
completionHandler:[OCMArg any]];
[password_exporter_ startExportFlow:CreatePasswordList()];
// Wait for all asynchronous tasks to complete.
scoped_task_environment_.RunUntilIdle();
// Use @try/@catch as -reject raises an exception.
@try {
[fake_password_file_writer executeHandler];
EXPECT_OCMOCK_VERIFY(password_exporter_delegate_);
} @catch (NSException* exception) {
// The exception is raised when
// - showActivityViewWithActivityItems:completionHandler:
// is invoked. As this should not happen, mark the test as failed.
GTEST_FAIL();
}
// Failure to write the passwords file ends the export operation.
EXPECT_EQ(ExportState::IDLE, password_exporter_.exportState);
}
// Tests that if a file write fails with an error other than not having
// enough disk space, the appropriate error is displayed and the export
// operation is interrupted.
TEST_F(PasswordExporterTest, WritingFailedUnknownError) {
mock_reauthentication_module_.shouldSucceed = YES;
FakePasswordFileWriter* fake_password_file_writer =
[[FakePasswordFileWriter alloc] init];
fake_password_file_writer.writingStatus = WriteToURLStatus::UNKNOWN_ERROR;
[password_exporter_ setPasswordFileWriter:fake_password_file_writer];
OCMExpect([password_exporter_delegate_
showExportErrorAlertWithLocalizedReason:
l10n_util::GetNSString(
IDS_IOS_EXPORT_PASSWORDS_UNKNOWN_ERROR_ALERT_MESSAGE)]);
[[password_exporter_delegate_ reject]
showActivityViewWithActivityItems:[OCMArg any]
completionHandler:[OCMArg any]];
[password_exporter_ startExportFlow:CreatePasswordList()];
// Wait for all asynchronous tasks to complete.
scoped_task_environment_.RunUntilIdle();
// Use @try/@catch as -reject raises an exception.
@try {
[fake_password_file_writer executeHandler];
EXPECT_OCMOCK_VERIFY(password_exporter_delegate_);
} @catch (NSException* exception) {
// The exception is raised when
// - showActivityViewWithActivityItems:completionHandler:
// is invoked. As this should not happen, mark the test as failed.
GTEST_FAIL();
}
// Failure to write the passwords file ends the export operation.
EXPECT_EQ(ExportState::IDLE, password_exporter_.exportState);
}
TEST_F(PasswordExporterTest, PasswordFileWriteAuthFailed) {
// Tests that when reauthentication fails the export flow is interrupted.
TEST_F(PasswordExporterTest, ExportInterruptedWhenReauthFails) {
mock_reauthentication_module_.shouldSucceed = NO;
FakePasswordSerialzerBridge* fake_password_serializer_bridge =
[[FakePasswordSerialzerBridge alloc] init];
......@@ -161,18 +279,23 @@ TEST_F(PasswordExporterTest, PasswordFileWriteAuthFailed) {
GTEST_FAIL();
}
// Serializing passwords hasn't finished.
EXPECT_FALSE(PasswordFileExists());
EXPECT_TRUE(password_exporter_.isExporting);
EXPECT_EQ(ExportState::ONGOING, password_exporter_.exportState);
[fake_password_serializer_bridge executeHandler];
// Serializing password has finished, but reauthentication was not successful.
// Make sure this test doesn't pass only because file writing hasn't finished
// yet.
scoped_task_environment_.RunUntilIdle();
// Serializing passwords has finished, but reauthentication was not
// successful, so the file is not written.
EXPECT_FALSE(PasswordFileExists());
EXPECT_FALSE(password_exporter_.isExporting);
EXPECT_EQ(ExportState::IDLE, password_exporter_.exportState);
}
TEST_F(PasswordExporterTest,
PasswordFileNotWrittenBeforeSerializationFinished) {
// Tests that cancelling the export while serialization is still ongoing
// waits for it to finish before cleaning up.
TEST_F(PasswordExporterTest, CancelWaitsForSerializationFinished) {
mock_reauthentication_module_.shouldSucceed = YES;
FakePasswordSerialzerBridge* fake_password_serializer_bridge =
[[FakePasswordSerialzerBridge alloc] init];
......@@ -183,10 +306,13 @@ TEST_F(PasswordExporterTest,
showActivityViewWithActivityItems:[OCMArg any]
completionHandler:[OCMArg any]];
// Use @try/@catch as -reject raises an exception.
@try {
[password_exporter_ startExportFlow:CreatePasswordList()];
[password_exporter_ cancelExport];
EXPECT_EQ(ExportState::CANCELLING, password_exporter_.exportState);
// Use @try/@catch as -reject raises an exception.
@try {
[fake_password_serializer_bridge executeHandler];
// Wait for all asynchronous tasks to complete.
scoped_task_environment_.RunUntilIdle();
EXPECT_OCMOCK_VERIFY(password_exporter_delegate_);
......@@ -197,49 +323,70 @@ TEST_F(PasswordExporterTest,
GTEST_FAIL();
}
EXPECT_FALSE(PasswordFileExists());
EXPECT_EQ(ExportState::IDLE, password_exporter_.exportState);
}
TEST_F(PasswordExporterTest, WritingFailedOutOfDiskSpace) {
// Tests that if the export is cancelled before writing to file finishes
// successfully the request to show the activity controller isn't made.
TEST_F(PasswordExporterTest, CancelledBeforeWriteToFileFinishesSuccessfully) {
mock_reauthentication_module_.shouldSucceed = YES;
FakePasswordFileWriter* fake_password_file_writer =
[[FakePasswordFileWriter alloc] init];
fake_password_file_writer.writingStatus =
WriteToURLStatus::OUT_OF_DISK_SPACE_ERROR;
fake_password_file_writer.writingStatus = WriteToURLStatus::SUCCESS;
[password_exporter_ setPasswordFileWriter:fake_password_file_writer];
OCMExpect([password_exporter_delegate_
showExportErrorAlertWithLocalizedReason:
l10n_util::GetNSString(
IDS_IOS_EXPORT_PASSWORDS_OUT_OF_SPACE_ALERT_MESSAGE)]);
[[password_exporter_delegate_ reject]
showActivityViewWithActivityItems:[OCMArg any]
completionHandler:[OCMArg any]];
[password_exporter_ startExportFlow:CreatePasswordList()];
// Wait for all asynchronous tasks to complete.
scoped_task_environment_.RunUntilIdle();
[password_exporter_ cancelExport];
EXPECT_EQ(ExportState::CANCELLING, password_exporter_.exportState);
// Use @try/@catch as -reject raises an exception.
@try {
[fake_password_file_writer executeHandler];
EXPECT_OCMOCK_VERIFY(password_exporter_delegate_);
EXPECT_FALSE(password_exporter_.isExporting);
} @catch (NSException* exception) {
// The exception is raised when
// - showActivityViewWithActivityItems:completionHandler:
// is invoked. As this should not happen, mark the test as failed.
GTEST_FAIL();
}
EXPECT_EQ(ExportState::IDLE, password_exporter_.exportState);
}
TEST_F(PasswordExporterTest, WritingFailedUnknownError) {
// Tests that if the export is cancelled before writing to file fails
// with an error, the request to show the error alert isn't made.
TEST_F(PasswordExporterTest, CancelledBeforeWriteToFileFails) {
mock_reauthentication_module_.shouldSucceed = YES;
FakePasswordFileWriter* fake_password_file_writer =
[[FakePasswordFileWriter alloc] init];
fake_password_file_writer.writingStatus = WriteToURLStatus::UNKNOWN_ERROR;
[password_exporter_ setPasswordFileWriter:fake_password_file_writer];
OCMExpect([password_exporter_delegate_
showExportErrorAlertWithLocalizedReason:
l10n_util::GetNSString(
IDS_IOS_EXPORT_PASSWORDS_UNKNOWN_ERROR_ALERT_MESSAGE)]);
[[password_exporter_delegate_ reject]
showExportErrorAlertWithLocalizedReason:[OCMArg any]];
[password_exporter_ startExportFlow:CreatePasswordList()];
// Wait for all asynchronous tasks to complete.
scoped_task_environment_.RunUntilIdle();
[password_exporter_ cancelExport];
EXPECT_EQ(ExportState::CANCELLING, password_exporter_.exportState);
// Use @try/@catch as -reject raises an exception.
@try {
[fake_password_file_writer executeHandler];
EXPECT_OCMOCK_VERIFY(password_exporter_delegate_);
EXPECT_FALSE(password_exporter_.isExporting);
} @catch (NSException* exception) {
// The exception is raised when
// - showExportErrorAlertWithLocalizedReason:
// is invoked. As this should not happen, mark the test as failed.
GTEST_FAIL();
}
EXPECT_EQ(ExportState::IDLE, password_exporter_.exportState);
}
} // namespace
......@@ -168,6 +168,9 @@ void SavePasswordsConsumer::OnGetPasswordStoreResults(
// Boolean containing whether the export button and functionality are enabled
// or not.
BOOL exportEnabled_;
// Alert informing the user that passwords are being prepared for
// export.
UIAlertController* preparingPasswordsAlert_;
}
// Kick off async request to get logins from password store.
......@@ -194,9 +197,12 @@ void SavePasswordsConsumer::OnGetPasswordStoreResults(
browserState_ = browserState;
reauthenticationModule_ = [[ReauthenticationModule alloc]
initWithSuccessfulReauthTimeAccessor:self];
if (base::FeatureList::IsEnabled(
password_manager::features::kPasswordExport)) {
passwordExporter_ = [[PasswordExporter alloc]
initWithReauthenticationModule:reauthenticationModule_
delegate:self];
}
self.title = l10n_util::GetNSString(IDS_IOS_SAVE_PASSWORDS);
self.collectionViewAccessibilityIdentifier =
@"SavePasswordsCollectionViewController";
......@@ -279,7 +285,7 @@ void SavePasswordsConsumer::OnGetPasswordStoreResults(
exportPasswordsItem_ = [self exportPasswordsItem];
[model addItem:exportPasswordsItem_
toSectionWithIdentifier:SectionIdentifierExportPasswordsButton];
[self updateExportPasswordsItem];
[self updateExportPasswordsButton];
}
}
......@@ -484,22 +490,24 @@ void SavePasswordsConsumer::OnGetPasswordStoreResults(
[self reloadData];
}
- (void)updateExportPasswordsItem {
if (savedForms_.empty()) {
- (void)updateExportPasswordsButton {
if (!exportPasswordsItem_)
return;
if (!savedForms_.empty() &&
self.passwordExporter.exportState == ExportState::IDLE) {
exportPasswordsItem_.textColor = [[MDCPalette greyPalette] tint900];
exportPasswordsItem_.accessibilityTraits = UIAccessibilityTraitButton;
[self reconfigureCellsForItems:@[ exportPasswordsItem_ ]];
exportEnabled_ = YES;
} else {
exportPasswordsItem_.textColor = [[MDCPalette greyPalette] tint500];
exportPasswordsItem_.accessibilityTraits = UIAccessibilityTraitNotEnabled;
[self reconfigureCellsForItems:@[ exportPasswordsItem_ ]];
exportEnabled_ = NO;
} else {
exportEnabled_ = YES;
}
}
- (void)startPasswordsExportFlow {
// TODO(crbug.com/789122): Consider disabling the button while another export
// operation is in progress.
if (self.passwordExporter.isExporting)
return;
UIAlertController* exportConfirmation = [UIAlertController
alertControllerWithTitle:nil
message:l10n_util::GetNSString(
......@@ -524,6 +532,7 @@ void SavePasswordsConsumer::OnGetPasswordStoreResults(
[strongSelf.passwordExporter
startExportFlow:CopyOf(strongSelf->savedForms_)];
}];
[exportConfirmation addAction:exportAction];
[self presentViewController:exportConfirmation animated:YES completion:nil];
......@@ -696,7 +705,7 @@ void SavePasswordsConsumer::OnGetPasswordStoreResults(
[strongSelf updateEditButton];
if (base::FeatureList::IsEnabled(
password_manager::features::kPasswordExport)) {
[strongSelf updateExportPasswordsItem];
[strongSelf updateExportPasswordsButton];
}
}];
}
......@@ -775,6 +784,26 @@ void SavePasswordsConsumer::OnGetPasswordStoreResults(
[self presentViewController:alertController animated:YES completion:nil];
}
- (void)showPreparingPasswordsAlert {
preparingPasswordsAlert_ = [UIAlertController
alertControllerWithTitle:
l10n_util::GetNSString(IDS_IOS_EXPORT_PASSWORDS_PREPARING_ALERT_TITLE)
message:nil
preferredStyle:UIAlertControllerStyleAlert];
__weak SavePasswordsCollectionViewController* weakSelf = self;
UIAlertAction* cancelAction =
[UIAlertAction actionWithTitle:l10n_util::GetNSString(
IDS_IOS_EXPORT_PASSWORDS_CANCEL_BUTTON)
style:UIAlertActionStyleCancel
handler:^(UIAlertAction*) {
[weakSelf.passwordExporter cancelExport];
}];
[preparingPasswordsAlert_ addAction:cancelAction];
[self presentViewController:preparingPasswordsAlert_
animated:YES
completion:nil];
}
- (void)showExportErrorAlertWithLocalizedReason:(NSString*)localizedReason {
UIAlertController* alertController = [UIAlertController
alertControllerWithTitle:l10n_util::GetNSString(
......@@ -786,7 +815,7 @@ void SavePasswordsConsumer::OnGetPasswordStoreResults(
style:UIAlertActionStyleDefault
handler:nil];
[alertController addAction:okAction];
[self presentViewController:alertController animated:YES completion:nil];
[self presentViewController:alertController];
}
- (void)showActivityViewWithActivityItems:(NSArray*)activityItems
......@@ -826,9 +855,23 @@ void SavePasswordsConsumer::OnGetPasswordStoreResults(
activityViewController.popoverPresentationController
.permittedArrowDirections =
UIPopoverArrowDirectionDown | UIPopoverArrowDirectionDown;
[self presentViewController:activityViewController
[self presentViewController:activityViewController];
}
- (void)presentViewController:(UIViewController*)viewController {
if (self.presentedViewController == preparingPasswordsAlert_ &&
!preparingPasswordsAlert_.beingDismissed) {
__weak SavePasswordsCollectionViewController* weakSelf = self;
[self dismissViewControllerAnimated:YES
completion:^() {
[weakSelf presentViewController:viewController
animated:YES
completion:nil];
}];
} else {
[self presentViewController:viewController animated:YES completion:nil];
}
}
#pragma mark Helper methods
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment