Просмотр исходного кода

Adopt FLEXAlert

- Add FLEXAlert, a builder-oriented UIAlertController wrapper
- Replace all uses of UIAlertController with FLEXAlert
- Moves some alert methods from FLEXUtility to FLEXAlert
Tanner Bennett лет назад: 6
Родитель
Сommit
ac4c50b62c

+ 1 - 1
Classes/Editing/FLEXMethodCallingViewController.m

@@ -95,7 +95,7 @@
     id returnedObject = [FLEXRuntimeUtility performSelector:method_getName(self.method) onObject:self.target withArguments:arguments error:&error];
     id returnedObject = [FLEXRuntimeUtility performSelector:method_getName(self.method) onObject:self.target withArguments:arguments error:&error];
     
     
     if (error) {
     if (error) {
-        [FLEXUtility alert:@"Method Call Failed" message:[error localizedDescription] from:self];
+        [FLEXAlert showAlert:@"Method Call Failed" message:[error localizedDescription] from:self];
     } else if (returnedObject) {
     } else if (returnedObject) {
         // For non-nil (or void) return types, push an explorer view controller to display the returned object
         // For non-nil (or void) return types, push an explorer view controller to display the returned object
         returnedObject = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:returnedObject type:self.returnType];
         returnedObject = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:returnedObject type:self.returnType];

+ 1 - 1
Classes/Editing/FLEXPropertyEditorViewController.m

@@ -63,7 +63,7 @@
     NSError *error = nil;
     NSError *error = nil;
     [FLEXRuntimeUtility performSelector:setterSelector onObject:self.target withArguments:arguments error:&error];
     [FLEXRuntimeUtility performSelector:setterSelector onObject:self.target withArguments:arguments error:&error];
     if (error) {
     if (error) {
-        [FLEXUtility alert:@"Property Setter Failed" message:[error localizedDescription] from:self];
+        [FLEXAlert showAlert:@"Property Setter Failed" message:[error localizedDescription] from:self];
         self.firstInputView.inputValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
         self.firstInputView.inputValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
     } else {
     } else {
         // If the setter was called without error, pop the view controller to indicate that and make the user's life easier.
         // If the setter was called without error, pop the view controller to indicate that and make the user's life easier.

+ 22 - 30
Classes/GlobalStateExplorers/FLEXAddressExplorerCoordinator.m

@@ -28,40 +28,32 @@
 
 
 + (FLEXGlobalsTableViewControllerRowAction)globalsEntryRowAction:(FLEXGlobalsRow)row {
 + (FLEXGlobalsTableViewControllerRowAction)globalsEntryRowAction:(FLEXGlobalsRow)row {
     return ^(FLEXGlobalsTableViewController *host) {
     return ^(FLEXGlobalsTableViewController *host) {
+
         NSString *title = @"Explore Object at Address";
         NSString *title = @"Explore Object at Address";
         NSString *message = @"Paste a hexadecimal address below, starting with '0x'. "
         NSString *message = @"Paste a hexadecimal address below, starting with '0x'. "
         "Use the unsafe option if you need to bypass pointer validation, "
         "Use the unsafe option if you need to bypass pointer validation, "
         "but know that it may crash the app if the address is invalid.";
         "but know that it may crash the app if the address is invalid.";
 
 
-        UIAlertController *addressInput = [UIAlertController alertControllerWithTitle:title
-                                                                              message:message
-                                                                       preferredStyle:UIAlertControllerStyleAlert];
-        void (^handler)(UIAlertAction *) = ^(UIAlertAction *action) {
-            if (action.style == UIAlertActionStyleCancel) {
-                [host deselectSelectedRow]; return;
-            }
-            NSString *address = addressInput.textFields.firstObject.text;
-            [host tryExploreAddress:address safely:action.style == UIAlertActionStyleDefault];
-        };
-        [addressInput addTextFieldWithConfigurationHandler:^(UITextField *textField) {
-            NSString *copied = UIPasteboard.generalPasteboard.string;
-            textField.placeholder = @"0x00000070deadbeef";
-            // Go ahead and paste our clipboard if we have an address copied
-            if ([copied hasPrefix:@"0x"]) {
-                textField.text = copied;
-                [textField selectAll:nil];
-            }
-        }];
-        [addressInput addAction:[UIAlertAction actionWithTitle:@"Explore"
-                                                         style:UIAlertActionStyleDefault
-                                                       handler:handler]];
-        [addressInput addAction:[UIAlertAction actionWithTitle:@"Unsafe Explore"
-                                                         style:UIAlertActionStyleDestructive
-                                                       handler:handler]];
-        [addressInput addAction:[UIAlertAction actionWithTitle:@"Cancel"
-                                                         style:UIAlertActionStyleCancel
-                                                       handler:handler]];
-        [host presentViewController:addressInput animated:YES completion:nil];
+        [FLEXAlert makeAlert:^(FLEXAlert *make) {
+            make.title(title).message(message);
+            make.configuredTextField(^(UITextField *textField) {
+                NSString *copied = UIPasteboard.generalPasteboard.string;
+                textField.placeholder = @"0x00000070deadbeef";
+                // Go ahead and paste our clipboard if we have an address copied
+                if ([copied hasPrefix:@"0x"]) {
+                    textField.text = copied;
+                    [textField selectAll:nil];
+                }
+            });
+            make.button(@"Explore").handler(^(NSArray<NSString *> *strings) {
+                [host tryExploreAddress:strings.firstObject safely:YES];
+            });
+            make.button(@"Unsafe Explore").destructiveStyle().handler(^(NSArray *strings) {
+                [host tryExploreAddress:strings.firstObject safely:NO];
+            });
+            make.button(@"Cancel").cancelStyle();
+        } showFrom:host];
+
     };
     };
 }
 }
 
 
@@ -95,7 +87,7 @@
         FLEXObjectExplorerViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:object];
         FLEXObjectExplorerViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:object];
         [self.navigationController pushViewController:explorer animated:YES];
         [self.navigationController pushViewController:explorer animated:YES];
     } else {
     } else {
-        [FLEXUtility alert:@"Uh-oh" message:error from:self];
+        [FLEXAlert showAlert:@"Uh-oh" message:error from:self];
         [self deselectSelectedRow];
         [self deselectSelectedRow];
     }
     }
 }
 }

+ 30 - 29
Classes/GlobalStateExplorers/FileBrowser/FLEXFileBrowserTableViewController.m

@@ -210,7 +210,7 @@
     UIImage *image = cell.imageView.image;
     UIImage *image = cell.imageView.image;
 
 
     if (!stillExists) {
     if (!stillExists) {
-        [FLEXUtility alert:@"File Not Found" message:@"The file at the specified path no longer exists." from:self];
+        [FLEXAlert showAlert:@"File Not Found" message:@"The file at the specified path no longer exists." from:self];
         [self reloadDisplayedPaths];
         [self reloadDisplayedPaths];
         return;
         return;
     }
     }
@@ -223,7 +223,7 @@
     } else {
     } else {
         NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
         NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
         if (!fileData.length) {
         if (!fileData.length) {
-            [FLEXUtility alert:@"Empty File" message:@"No data returned from the file." from:self];
+            [FLEXAlert showAlert:@"Empty File" message:@"No data returned from the file." from:self];
             return;
             return;
         }
         }
 
 
@@ -321,24 +321,22 @@
 
 
     BOOL stillExists = [NSFileManager.defaultManager fileExistsAtPath:self.path isDirectory:NULL];
     BOOL stillExists = [NSFileManager.defaultManager fileExistsAtPath:self.path isDirectory:NULL];
     if (stillExists) {
     if (stillExists) {
-        UIAlertController *alert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Rename %@?", fullPath.lastPathComponent]
-                                                                       message:nil
-                                                                preferredStyle:UIAlertControllerStyleAlert];
-        [alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
-            textField.placeholder = @"New file name";
-            textField.text = fullPath.lastPathComponent;
-        }];
-        __weak typeof(alert) weakAlert = alert;
-        [alert addAction:[UIAlertAction actionWithTitle:@"Rename" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
-            NSString *newFileName = weakAlert.textFields.firstObject.text;
-            NSString *newPath = [[fullPath stringByDeletingLastPathComponent] stringByAppendingPathComponent:newFileName];
-            [NSFileManager.defaultManager moveItemAtPath:fullPath toPath:newPath error:NULL];
-            [self reloadDisplayedPaths];
-        }]];
-        [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
-        [self presentViewController:alert animated:YES completion:nil];
+        [FLEXAlert makeAlert:^(FLEXAlert *make) {
+            make.title([NSString stringWithFormat:@"Rename %@?", fullPath.lastPathComponent]);
+            make.configuredTextField(^(UITextField *textField) {
+                textField.placeholder = @"New file name";
+                textField.text = fullPath.lastPathComponent;
+            });
+            make.button(@"Rename").handler(^(NSArray<NSString *> *strings) {
+                NSString *newFileName = strings.firstObject;
+                NSString *newPath = [fullPath.stringByDeletingLastPathComponent stringByAppendingPathComponent:newFileName];
+                [NSFileManager.defaultManager moveItemAtPath:fullPath toPath:newPath error:NULL];
+                [self reloadDisplayedPaths];
+            });
+            make.button(@"Cancel").cancelStyle();
+        } showFrom:self];
     } else {
     } else {
-        [FLEXUtility alert:@"File Removed" message:@"The file at the specified path no longer exists." from:self];
+        [FLEXAlert showAlert:@"File Removed" message:@"The file at the specified path no longer exists." from:self];
     }
     }
 }
 }
 
 
@@ -350,17 +348,20 @@
     BOOL isDirectory = NO;
     BOOL isDirectory = NO;
     BOOL stillExists = [NSFileManager.defaultManager fileExistsAtPath:fullPath isDirectory:&isDirectory];
     BOOL stillExists = [NSFileManager.defaultManager fileExistsAtPath:fullPath isDirectory:&isDirectory];
     if (stillExists) {
     if (stillExists) {
-        UIAlertController *alert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Delete %@?", fullPath.lastPathComponent]
-                                                                       message:[NSString stringWithFormat:@"The %@ will be deleted. This operation cannot be undone", isDirectory ? @"directory" : @"file"]
-                                                                preferredStyle:UIAlertControllerStyleAlert];
-        [alert addAction:[UIAlertAction actionWithTitle:@"Delete" style:UIAlertActionStyleDestructive handler:^(UIAlertAction * _Nonnull action) {
-            [NSFileManager.defaultManager removeItemAtPath:fullPath error:NULL];
-            [self reloadDisplayedPaths];
-        }]];
-        [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
-        [self presentViewController:alert animated:YES completion:nil];
+        [FLEXAlert makeAlert:^(FLEXAlert *make) {
+            make.title(@"Confirm Deletion");
+            make.message([NSString stringWithFormat:
+                @"The %@ '%@' will be deleted. This operation cannot be undone",
+                (isDirectory ? @"directory" : @"file"), fullPath.lastPathComponent
+            ]);
+            make.button(@"Delete").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
+                [NSFileManager.defaultManager removeItemAtPath:fullPath error:NULL];
+                [self reloadDisplayedPaths];
+            });
+            make.button(@"Cancel").cancelStyle();
+        } showFrom:self];
     } else {
     } else {
-        [FLEXUtility alert:@"File Removed" message:@"The file at the specified path no longer exists." from:self];
+        [FLEXAlert showAlert:@"File Removed" message:@"The file at the specified path no longer exists." from:self];
     }
     }
 }
 }
 
 

+ 9 - 10
Classes/GlobalStateExplorers/SystemLog/FLEXSystemLogTableViewController.m

@@ -104,16 +104,15 @@
     NSString *body = @"In iOS 10 and up, ASL is gone. The OS Log API is much more limited. "
     NSString *body = @"In iOS 10 and up, ASL is gone. The OS Log API is much more limited. "
     "To get as close to the old behavior as possible, logs must be collected manually at launch and stored.\n\n"
     "To get as close to the old behavior as possible, logs must be collected manually at launch and stored.\n\n"
     "Turn this feature on only when you need it.";
     "Turn this feature on only when you need it.";
-    
-    UIAlertController *settings = [UIAlertController alertControllerWithTitle:title message:body preferredStyle:UIAlertControllerStyleAlert];
-    [settings addAction:[UIAlertAction actionWithTitle:toggle style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
-        [[NSUserDefaults standardUserDefaults] setBool:!persistent forKey:kFLEXiOSPersistentOSLogKey];
-        logController.persistent = !persistent;
-        [logController.messages addObjectsFromArray:self.logMessages];
-    }]];
-    [settings addAction:[UIAlertAction actionWithTitle:@"Dismiss" style:UIAlertActionStyleCancel handler:nil]];
-    
-    [self presentViewController:settings animated:YES completion:nil];
+
+    [FLEXAlert makeAlert:^(FLEXAlert *make) {
+        make.title(title).message(body).button(toggle).handler(^(NSArray<NSString *> *strings) {
+            [[NSUserDefaults standardUserDefaults] setBool:!persistent forKey:kFLEXiOSPersistentOSLogKey];
+            logController.persistent = !persistent;
+            [logController.messages addObjectsFromArray:self.logMessages];
+        });
+        make.button(@"Dismiss").cancelStyle();
+    } showFrom:self];
 }
 }
 
 
 #pragma mark - FLEXGlobalsEntry
 #pragma mark - FLEXGlobalsEntry

+ 7 - 6
Classes/Network/FLEXNetworkSettingsTableViewController.m

@@ -74,14 +74,15 @@
 
 
 - (void)clearRequestsTapped:(UIButton *)sender
 - (void)clearRequestsTapped:(UIButton *)sender
 {
 {
-    UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
-    [actionSheet addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
-    [actionSheet addAction:[UIAlertAction actionWithTitle:@"Clear Recorded Requests" style:UIAlertActionStyleDestructive handler:^(UIAlertAction * _Nonnull action) {
-        [[FLEXNetworkRecorder defaultRecorder] clearRecordedActivity];
-    }]];
+    [FLEXAlert makeSheet:^(FLEXAlert *make) {
+        make.button(@"Cancel").cancelStyle();
+        make.button(@"Clear Recorded Requests").destructiveStyle().handler(^(NSArray *strings) {
+            [[FLEXNetworkRecorder defaultRecorder] clearRecordedActivity];
+        });
+    } showFrom:self];
+
     self.popoverPresentationController.sourceView = sender;
     self.popoverPresentationController.sourceView = sender;
     self.popoverPresentationController.sourceRect = sender.bounds;
     self.popoverPresentationController.sourceRect = sender.bounds;
-    [self presentViewController:actionSheet animated:YES completion:nil];
 }
 }
 
 
 #pragma mark - Table view data source
 #pragma mark - Table view data source

+ 30 - 22
Classes/Network/FLEXNetworkTransactionDetailTableViewController.m

@@ -259,7 +259,8 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
         FLEXNetworkDetailRow *postBodyRow = [FLEXNetworkDetailRow new];
         FLEXNetworkDetailRow *postBodyRow = [FLEXNetworkDetailRow new];
         postBodyRow.title = @"Request Body";
         postBodyRow.title = @"Request Body";
         postBodyRow.detailText = @"tap to view";
         postBodyRow.detailText = @"tap to view";
-        postBodyRow.selectionFuture = ^UIViewController * (void){
+        postBodyRow.selectionFuture = ^UIViewController * () {
+            // Show the body if we can
             NSString *contentType = [transaction.request valueForHTTPHeaderField:@"Content-Type"];
             NSString *contentType = [transaction.request valueForHTTPHeaderField:@"Content-Type"];
             UIViewController *detailViewController = [self detailViewControllerForMIMEType:contentType data:[self postBodyDataForTransaction:transaction]];
             UIViewController *detailViewController = [self detailViewControllerForMIMEType:contentType data:[self postBodyDataForTransaction:transaction]];
             if (detailViewController) {
             if (detailViewController) {
@@ -267,13 +268,15 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
                 return detailViewController;
                 return detailViewController;
             }
             }
 
 
-            NSString *alertMessage = [NSString stringWithFormat:@"FLEX does not have a viewer for request body data with MIME type: %@", [transaction.request valueForHTTPHeaderField:@"Content-Type"]];
-            UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Can't View Body Data"
-                                                                           message:alertMessage
-                                                                    preferredStyle:UIAlertControllerStyleAlert];
-            [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]];
-            return alert;
+            // We can't show the body, alert user
+            return [FLEXAlert makeAlert:^(FLEXAlert *make) {
+                make.title(@"Can't View HTTP Body Data");
+                make.message(@"FLEX does not have a viewer for request body data with MIME type: ");
+                make.message(contentType);
+                make.button(@"Dismiss").cancelStyle();
+            }];
         };
         };
+
         [rows addObject:postBodyRow];
         [rows addObject:postBodyRow];
     }
     }
 
 
@@ -297,33 +300,38 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
     NSData *responseData = [[FLEXNetworkRecorder defaultRecorder] cachedResponseBodyForTransaction:transaction];
     NSData *responseData = [[FLEXNetworkRecorder defaultRecorder] cachedResponseBodyForTransaction:transaction];
     if (responseData.length > 0) {
     if (responseData.length > 0) {
         responseBodyRow.detailText = @"tap to view";
         responseBodyRow.detailText = @"tap to view";
+
         // Avoid a long lived strong reference to the response data in case we need to purge it from the cache.
         // Avoid a long lived strong reference to the response data in case we need to purge it from the cache.
         __weak NSData *weakResponseData = responseData;
         __weak NSData *weakResponseData = responseData;
-        responseBodyRow.selectionFuture = ^UIViewController * (void){
-            NSString *alertMessage = nil;
+        responseBodyRow.selectionFuture = ^UIViewController * () {
+
+            // Show the response if we can
+            NSString *contentType = transaction.response.MIMEType;
             NSData *strongResponseData = weakResponseData;
             NSData *strongResponseData = weakResponseData;
             if (strongResponseData) {
             if (strongResponseData) {
-                UIViewController *responseBodyDetailViewController = [self detailViewControllerForMIMEType:transaction.response.MIMEType data:strongResponseData];
-                if (responseBodyDetailViewController) {
-                    responseBodyDetailViewController.title = @"Response";
-                    return responseBodyDetailViewController;
+                UIViewController *bodyDetailController = [self detailViewControllerForMIMEType:contentType data:strongResponseData];
+                if (bodyDetailController) {
+                    bodyDetailController.title = @"Response";
+                    return bodyDetailController;
                 }
                 }
-
-                alertMessage = [NSString stringWithFormat:@"FLEX does not have a viewer for responses with MIME type: %@", transaction.response.MIMEType];
-            } else {
-                alertMessage = @"The response has been purged from the cache";
             }
             }
 
 
-            UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Can't View Response"
-                                                                           message:alertMessage
-                                                                    preferredStyle:UIAlertControllerStyleAlert];
-            [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]];
-            return alert;
+            // We can't show the response, alert user
+            return [FLEXAlert makeAlert:^(FLEXAlert *make) {
+                make.title(@"Unable to View Response");
+                if (strongResponseData) {
+                    make.message(@"No viewer content type: ").message(contentType);
+                } else {
+                    make.message(@"The response has been purged from the cache");
+                }
+                make.button(@"OK").cancelStyle();
+            }];
         };
         };
     } else {
     } else {
         BOOL emptyResponse = transaction.receivedDataLength == 0;
         BOOL emptyResponse = transaction.receivedDataLength == 0;
         responseBodyRow.detailText = emptyResponse ? @"empty" : @"not in cache";
         responseBodyRow.detailText = emptyResponse ? @"empty" : @"not in cache";
     }
     }
+
     [rows addObject:responseBodyRow];
     [rows addObject:responseBodyRow];
 
 
     FLEXNetworkDetailRow *responseSizeRow = [FLEXNetworkDetailRow new];
     FLEXNetworkDetailRow *responseSizeRow = [FLEXNetworkDetailRow new];

+ 79 - 0
Classes/Utility/FLEXAlert.h

@@ -0,0 +1,79 @@
+//
+//  FLEXAlert.h
+//  FLEX
+//
+//  Created by Tanner Bennett on 8/20/19.
+//  Copyright © 2019 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@class FLEXAlert, FLEXAlertAction;
+
+typedef void (^FLEXAlertReveal)();
+typedef void (^FLEXAlertBuilder)(FLEXAlert *make);
+typedef FLEXAlert *(^FLEXAlertStringProperty)(NSString *);
+typedef FLEXAlert *(^FLEXAlertStringArg)(NSString *);
+typedef FLEXAlert *(^FLEXAlertTextField)(void(^configurationHandler)(UITextField *textField));
+typedef FLEXAlertAction *(^FLEXAlertAddAction)(NSString *title);
+typedef FLEXAlertAction *(^FLEXAlertActionStringProperty)(NSString *);
+typedef FLEXAlertAction *(^FLEXAlertActionProperty)();
+typedef FLEXAlertAction *(^FLEXAlertActionBOOLProperty)(BOOL);
+typedef FLEXAlertAction *(^FLEXAlertActionHandler)(void(^handler)(NSArray<NSString *> *strings));
+
+@interface FLEXAlert : NSObject
+
+/// Shows a simple alert with one button which says "Dismiss"
++ (void)showAlert:(NSString *)title message:(NSString *)message from:(UIViewController *)viewController;
+
+/// Construct and display an alert
++ (void)makeAlert:(FLEXAlertBuilder)block showFrom:(UIViewController *)viewController;
+/// Construct and display an action sheet-style alert
++ (void)makeSheet:(FLEXAlertBuilder)block showFrom:(UIViewController *)viewController;
+
+/// Construct an alert
++ (UIAlertController *)makeAlert:(FLEXAlertBuilder)block;
+/// Construct an action sheet-style alert
++ (UIAlertController *)makeSheet:(FLEXAlertBuilder)block;
+
+/// Set the alert's title.
+///
+/// Call in succession to append strings to the title.
+@property (nonatomic, readonly) FLEXAlertStringProperty title;
+/// Set the alert's message.
+///
+/// Call in succession to append strings to the message.
+@property (nonatomic, readonly) FLEXAlertStringProperty message;
+/// Add a button with a given title with the default style and no action.
+@property (nonatomic, readonly) FLEXAlertAddAction button;
+/// Add a text field with the given (optional) placeholder text.
+@property (nonatomic, readonly) FLEXAlertStringArg textField;
+/// Add and configure the given text field.
+///
+/// Use this if you need to more than set the placeholder, such as
+/// supply a delegate, make it secure entry, or change other attributes.
+@property (nonatomic, readonly) FLEXAlertTextField configuredTextField;
+
+@end
+
+@interface FLEXAlertAction : NSObject
+
+/// Set the action's title.
+///
+/// Call in succession to append strings to the title.
+@property (nonatomic, readonly) FLEXAlertActionStringProperty title;
+/// Make the action destructive. It appears with red text.
+@property (nonatomic, readonly) FLEXAlertActionProperty destructiveStyle;
+/// Make the action cancel-style. It appears with a bolder font.
+@property (nonatomic, readonly) FLEXAlertActionProperty cancelStyle;
+/// Enable or disable the action. Enabled by default.
+@property (nonatomic, readonly) FLEXAlertActionBOOLProperty enabled;
+/// Give the button an action. The action takes an array of text field strings.
+@property (nonatomic, readonly) FLEXAlertActionHandler handler;
+/// Access the underlying UIAlertAction, should you need to change it while
+/// the encompassing alert is being displayed. For example, you may want to
+/// enable or disable a button based on the input of some text fields in the alert.
+/// Do not call this more than once per instance.
+@property (nonatomic, readonly) UIAlertAction *action;
+
+@end

+ 208 - 0
Classes/Utility/FLEXAlert.m

@@ -0,0 +1,208 @@
+//
+//  FLEXAlert.m
+//  FLEX
+//
+//  Created by Tanner Bennett on 8/20/19.
+//  Copyright © 2019 Flipboard. All rights reserved.
+//
+
+#import "FLEXAlert.h"
+
+@interface FLEXAlert ()
+@property (nonatomic, readonly) UIAlertController *_controller;
+@property (nonatomic, readonly) NSMutableArray<FLEXAlertAction *> *_actions;
+@end
+
+#define FLEXAlertActionMutationAssertion() \
+NSAssert(!self._action, @"Cannot mutate action after retreiving underlying UIAlertAction");
+
+@interface FLEXAlertAction ()
+@property (nonatomic) UIAlertController *_controller;
+@property (nonatomic) NSString *_title;
+@property (nonatomic) UIAlertActionStyle _style;
+@property (nonatomic) BOOL _disable;
+@property (nonatomic) void(^_handler)(UIAlertAction *action);
+@property (nonatomic) UIAlertAction *_action;
+@end
+
+@implementation FLEXAlert
+
++ (void)showAlert:(NSString *)title message:(NSString *)message from:(UIViewController *)viewController {
+    [self makeAlert:^(FLEXAlert *make) {
+        make.title(title).message(message).button(@"Dismiss").cancelStyle();
+    } showFrom:viewController];
+}
+
+#pragma mark Initialization
+
+- (instancetype)initWithController:(UIAlertController *)controller {
+    self = [super init];
+    if (self) {
+        __controller = controller;
+        __actions = [NSMutableArray new];
+    }
+
+    return self;
+}
+
++ (UIAlertController *)make:(FLEXAlertBuilder)block withStyle:(UIAlertControllerStyle)style {
+    // Create alert builder
+    FLEXAlert *alert = [[self alloc] initWithController:
+        [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:style]
+    ];
+
+    // Configure alert
+    block(alert);
+
+    // Add actions
+    for (FLEXAlertAction *builder in alert._actions) {
+        [alert._controller addAction:builder.action];
+    }
+
+    return alert._controller;
+}
+
++ (void)make:(FLEXAlertBuilder)block withStyle:(UIAlertControllerStyle)style showFrom:(UIViewController *)viewController {
+    UIAlertController *alert = [self make:block withStyle:style];
+    [viewController presentViewController:alert animated:YES completion:nil];
+}
+
++ (void)makeAlert:(FLEXAlertBuilder)block showFrom:(UIViewController *)viewController {
+    [self make:block withStyle:UIAlertControllerStyleAlert showFrom:viewController];
+}
+
++ (void)makeSheet:(FLEXAlertBuilder)block showFrom:(UIViewController *)viewController {
+    [self make:block withStyle:UIAlertControllerStyleActionSheet showFrom:viewController];
+}
+
++ (UIAlertController *)makeAlert:(FLEXAlertBuilder)block {
+    return [self make:block withStyle:UIAlertControllerStyleAlert];
+}
+
++ (UIAlertController *)makeSheet:(FLEXAlertBuilder)block {
+    return [self make:block withStyle:UIAlertControllerStyleActionSheet];
+}
+
+#pragma mark Configuration
+
+- (FLEXAlertStringProperty)title {
+    return ^FLEXAlert *(NSString *title) {
+        if (self._controller.title) {
+            self._controller.title = [self._controller.title stringByAppendingString:title];
+        } else {
+            self._controller.title = title;
+        }
+        return self;
+    };
+}
+
+- (FLEXAlertStringProperty)message {
+    return ^FLEXAlert *(NSString *message) {
+        if (self._controller.message) {
+            self._controller.message = [self._controller.message stringByAppendingString:message];
+        } else {
+            self._controller.message = message;
+        }
+        return self;
+    };
+}
+
+- (FLEXAlertAddAction)button {
+    return ^FLEXAlertAction *(NSString *title) {
+        FLEXAlertAction *action = FLEXAlertAction.new.title(title);
+        action._controller = self._controller;
+        [self._actions addObject:action];
+        return action;
+    };
+}
+
+- (FLEXAlertStringArg)textField {
+    return ^FLEXAlert *(NSString *placeholder) {
+        [self._controller addTextFieldWithConfigurationHandler:^(UITextField *textField) {
+            textField.placeholder = placeholder;
+        }];
+
+        return self;
+    };
+}
+
+- (FLEXAlertTextField)configuredTextField {
+    return ^FLEXAlert *(void(^configurationHandler)(UITextField *)) {
+        [self._controller addTextFieldWithConfigurationHandler:configurationHandler];
+        return self;
+    };
+}
+
+@end
+
+@implementation FLEXAlertAction
+
+- (FLEXAlertActionStringProperty)title {
+    return ^FLEXAlertAction *(NSString *title) {
+        FLEXAlertActionMutationAssertion();
+        if (self._title) {
+            self._title = [self._title stringByAppendingString:title];
+        } else {
+            self._title = title;
+        }
+        return self;
+    };
+}
+
+- (FLEXAlertActionProperty)destructiveStyle {
+    return ^FLEXAlertAction *() {
+        FLEXAlertActionMutationAssertion();
+        self._style = UIAlertActionStyleDestructive;
+        return self;
+    };
+}
+
+- (FLEXAlertActionProperty)cancelStyle {
+    return ^FLEXAlertAction *() {
+        FLEXAlertActionMutationAssertion();
+        self._style = UIAlertActionStyleCancel;
+        return self;
+    };
+}
+
+- (FLEXAlertActionBOOLProperty)enabled {
+    return ^FLEXAlertAction *(BOOL enabled) {
+        FLEXAlertActionMutationAssertion();
+        self._disable = !enabled;
+        return self;
+    };
+}
+
+- (FLEXAlertActionHandler)handler {
+    return ^FLEXAlertAction *(void(^handler)(NSArray<NSString *> *)) {
+        FLEXAlertActionMutationAssertion();
+
+        // Get weak reference to the alert to avoid block <--> alert retain cycle
+        __weak __typeof(self._controller) weakController = self._controller;
+        self._handler = ^(UIAlertAction *action) {
+            // Strongify that reference and pass the text field strings to the handler
+            __strong __typeof(weakController) controller = weakController;
+            NSArray *strings = [controller.textFields valueForKeyPath:@"text"];
+            handler(strings);
+        };
+
+        return self;
+    };
+}
+
+- (UIAlertAction *)action {
+    if (self._action) {
+        return self._action;
+    }
+
+    self._action = [UIAlertAction
+        actionWithTitle:self._title
+        style:self._style
+        handler:self._handler
+    ];
+    self._action.enabled = !self._disable;
+
+    return self._action;
+}
+
+@end

+ 1 - 2
Classes/Utility/FLEXUtility.h

@@ -11,6 +11,7 @@
 #import <Foundation/Foundation.h>
 #import <Foundation/Foundation.h>
 #import <UIKit/UIKit.h>
 #import <UIKit/UIKit.h>
 #import <objc/runtime.h>
 #import <objc/runtime.h>
+#import "FLEXAlert.h"
 
 
 #define FLEXFloor(x) (floor(UIScreen.mainScreen.scale * (x)) / UIScreen.mainScreen.scale)
 #define FLEXFloor(x) (floor(UIScreen.mainScreen.scale * (x)) / UIScreen.mainScreen.scale)
 
 
@@ -54,8 +55,6 @@
 
 
 + (NSArray<UIWindow *> *)allWindows;
 + (NSArray<UIWindow *> *)allWindows;
 
 
-+ (void)alert:(NSString *)title message:(NSString *)message from:(UIViewController *)viewController;
-
 // Swizzling utilities
 // Swizzling utilities
 
 
 + (SEL)swizzledSelectorForSelector:(SEL)selector;
 + (SEL)swizzledSelectorForSelector:(SEL)selector;

+ 6 - 4
Classes/Utility/FLEXUtility.m

@@ -411,11 +411,13 @@
     return windows;
     return windows;
 }
 }
 
 
-+ (void)alert:(NSString *)title message:(NSString *)message from:(UIViewController *)viewController
++ (UIAlertController *)alert:(NSString *)title message:(NSString *)message
 {
 {
-    UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
-    [alert addAction:[UIAlertAction actionWithTitle:@"Dismiss" style:UIAlertActionStyleDefault handler:nil]];
-    [viewController presentViewController:alert animated:YES completion:nil];
+    return [UIAlertController
+        alertControllerWithTitle:title
+        message:message
+        preferredStyle:UIAlertControllerStyleAlert
+    ];
 }
 }
 
 
 + (SEL)swizzledSelectorForSelector:(SEL)selector
 + (SEL)swizzledSelectorForSelector:(SEL)selector

+ 8 - 0
FLEX.xcodeproj/project.pbxproj

@@ -182,6 +182,8 @@
 		C387C88422E0D24A00750E58 /* UIView+Layout.m in Sources */ = {isa = PBXBuildFile; fileRef = C387C88222E0D24A00750E58 /* UIView+Layout.m */; };
 		C387C88422E0D24A00750E58 /* UIView+Layout.m in Sources */ = {isa = PBXBuildFile; fileRef = C387C88222E0D24A00750E58 /* UIView+Layout.m */; };
 		C38DF0EA22CFE4370077B4AD /* FLEXTableViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = C38DF0E822CFE4370077B4AD /* FLEXTableViewController.h */; };
 		C38DF0EA22CFE4370077B4AD /* FLEXTableViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = C38DF0E822CFE4370077B4AD /* FLEXTableViewController.h */; };
 		C38DF0EB22CFE4370077B4AD /* FLEXTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C38DF0E922CFE4370077B4AD /* FLEXTableViewController.m */; };
 		C38DF0EB22CFE4370077B4AD /* FLEXTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C38DF0E922CFE4370077B4AD /* FLEXTableViewController.m */; };
+		C38F3F31230C958F004E3731 /* FLEXAlert.h in Headers */ = {isa = PBXBuildFile; fileRef = C38F3F2F230C958F004E3731 /* FLEXAlert.h */; };
+		C38F3F32230C958F004E3731 /* FLEXAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = C38F3F30230C958F004E3731 /* FLEXAlert.m */; };
 		C395D6D921789BD800BEAD4D /* FLEXColorExplorerViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = C395D6D721789BD800BEAD4D /* FLEXColorExplorerViewController.h */; };
 		C395D6D921789BD800BEAD4D /* FLEXColorExplorerViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = C395D6D721789BD800BEAD4D /* FLEXColorExplorerViewController.h */; };
 		C395D6DA21789BD800BEAD4D /* FLEXColorExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C395D6D821789BD800BEAD4D /* FLEXColorExplorerViewController.m */; };
 		C395D6DA21789BD800BEAD4D /* FLEXColorExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C395D6D821789BD800BEAD4D /* FLEXColorExplorerViewController.m */; };
 		C39ED92822D63F3200B5773A /* FLEXAddressExplorerCoordinator.h in Headers */ = {isa = PBXBuildFile; fileRef = C39ED92622D63F3200B5773A /* FLEXAddressExplorerCoordinator.h */; };
 		C39ED92822D63F3200B5773A /* FLEXAddressExplorerCoordinator.h in Headers */ = {isa = PBXBuildFile; fileRef = C39ED92622D63F3200B5773A /* FLEXAddressExplorerCoordinator.h */; };
@@ -395,6 +397,8 @@
 		C387C88222E0D24A00750E58 /* UIView+Layout.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIView+Layout.m"; sourceTree = "<group>"; };
 		C387C88222E0D24A00750E58 /* UIView+Layout.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIView+Layout.m"; sourceTree = "<group>"; };
 		C38DF0E822CFE4370077B4AD /* FLEXTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXTableViewController.h; sourceTree = "<group>"; };
 		C38DF0E822CFE4370077B4AD /* FLEXTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXTableViewController.h; sourceTree = "<group>"; };
 		C38DF0E922CFE4370077B4AD /* FLEXTableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXTableViewController.m; sourceTree = "<group>"; };
 		C38DF0E922CFE4370077B4AD /* FLEXTableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXTableViewController.m; sourceTree = "<group>"; };
+		C38F3F2F230C958F004E3731 /* FLEXAlert.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXAlert.h; sourceTree = "<group>"; };
+		C38F3F30230C958F004E3731 /* FLEXAlert.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXAlert.m; sourceTree = "<group>"; };
 		C395D6D721789BD800BEAD4D /* FLEXColorExplorerViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXColorExplorerViewController.h; sourceTree = "<group>"; };
 		C395D6D721789BD800BEAD4D /* FLEXColorExplorerViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXColorExplorerViewController.h; sourceTree = "<group>"; };
 		C395D6D821789BD800BEAD4D /* FLEXColorExplorerViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXColorExplorerViewController.m; sourceTree = "<group>"; };
 		C395D6D821789BD800BEAD4D /* FLEXColorExplorerViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXColorExplorerViewController.m; sourceTree = "<group>"; };
 		C39ED92622D63F3200B5773A /* FLEXAddressExplorerCoordinator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXAddressExplorerCoordinator.h; sourceTree = "<group>"; };
 		C39ED92622D63F3200B5773A /* FLEXAddressExplorerCoordinator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXAddressExplorerCoordinator.h; sourceTree = "<group>"; };
@@ -518,6 +522,8 @@
 				3A4C945C1B5B21410088C3F2 /* FLEXRuntimeUtility.m */,
 				3A4C945C1B5B21410088C3F2 /* FLEXRuntimeUtility.m */,
 				3A4C945D1B5B21410088C3F2 /* FLEXUtility.h */,
 				3A4C945D1B5B21410088C3F2 /* FLEXUtility.h */,
 				3A4C945E1B5B21410088C3F2 /* FLEXUtility.m */,
 				3A4C945E1B5B21410088C3F2 /* FLEXUtility.m */,
+				C38F3F2F230C958F004E3731 /* FLEXAlert.h */,
+				C38F3F30230C958F004E3731 /* FLEXAlert.m */,
 				942DCD821BAE0AD300DB5DC2 /* FLEXKeyboardShortcutManager.h */,
 				942DCD821BAE0AD300DB5DC2 /* FLEXKeyboardShortcutManager.h */,
 				942DCD831BAE0AD300DB5DC2 /* FLEXKeyboardShortcutManager.m */,
 				942DCD831BAE0AD300DB5DC2 /* FLEXKeyboardShortcutManager.m */,
 				94AAF0361BAF2E1F00DE8760 /* FLEXKeyboardHelpViewController.h */,
 				94AAF0361BAF2E1F00DE8760 /* FLEXKeyboardHelpViewController.h */,
@@ -845,6 +851,7 @@
 				3A4C94ED1B5B21410088C3F2 /* FLEXArgumentInputColorView.h in Headers */,
 				3A4C94ED1B5B21410088C3F2 /* FLEXArgumentInputColorView.h in Headers */,
 				3A4C94EB1B5B21410088C3F2 /* FLEXImagePreviewViewController.h in Headers */,
 				3A4C94EB1B5B21410088C3F2 /* FLEXImagePreviewViewController.h in Headers */,
 				C3DA55FE21A76406005DDA60 /* FLEXMutableFieldEditorViewController.h in Headers */,
 				C3DA55FE21A76406005DDA60 /* FLEXMutableFieldEditorViewController.h in Headers */,
+				C38F3F31230C958F004E3731 /* FLEXAlert.h in Headers */,
 				3A4C95381B5B21410088C3F2 /* FLEXNetworkRecorder.h in Headers */,
 				3A4C95381B5B21410088C3F2 /* FLEXNetworkRecorder.h in Headers */,
 				3A4C94251B5B20570088C3F2 /* FLEX.h in Headers */,
 				3A4C94251B5B20570088C3F2 /* FLEX.h in Headers */,
 				3A4C95051B5B21410088C3F2 /* FLEXArgumentInputViewFactory.h in Headers */,
 				3A4C95051B5B21410088C3F2 /* FLEXArgumentInputViewFactory.h in Headers */,
@@ -1067,6 +1074,7 @@
 				94AAF0391BAF2E1F00DE8760 /* FLEXKeyboardHelpViewController.m in Sources */,
 				94AAF0391BAF2E1F00DE8760 /* FLEXKeyboardHelpViewController.m in Sources */,
 				3A4C951F1B5B21410088C3F2 /* FLEXClassesTableViewController.m in Sources */,
 				3A4C951F1B5B21410088C3F2 /* FLEXClassesTableViewController.m in Sources */,
 				3A4C94C61B5B21410088C3F2 /* FLEXArrayExplorerViewController.m in Sources */,
 				3A4C94C61B5B21410088C3F2 /* FLEXArrayExplorerViewController.m in Sources */,
+				C38F3F32230C958F004E3731 /* FLEXAlert.m in Sources */,
 				C3DA55FF21A76406005DDA60 /* FLEXMutableFieldEditorViewController.m in Sources */,
 				C3DA55FF21A76406005DDA60 /* FLEXMutableFieldEditorViewController.m in Sources */,
 				3A4C95081B5B21410088C3F2 /* FLEXDefaultEditorViewController.m in Sources */,
 				3A4C95081B5B21410088C3F2 /* FLEXDefaultEditorViewController.m in Sources */,
 				779B1ED11C0C4D7C001F5E49 /* FLEXMultiColumnTableView.m in Sources */,
 				779B1ED11C0C4D7C001F5E49 /* FLEXMultiColumnTableView.m in Sources */,