Browse Source

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 4 years ago
parent
commit
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];
     
     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) {
         // For non-nil (or void) return types, push an explorer view controller to display the returned object
         returnedObject = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:returnedObject type:self.returnType];

+ 1 - 1
Classes/Editing/FLEXPropertyEditorViewController.m

@@ -63,7 +63,7 @@
     NSError *error = nil;
     [FLEXRuntimeUtility performSelector:setterSelector onObject:self.target withArguments:arguments error:&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];
     } else {
         // 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 {
     return ^(FLEXGlobalsTableViewController *host) {
+
         NSString *title = @"Explore Object at Address";
         NSString *message = @"Paste a hexadecimal address below, starting with '0x'. "
         "Use the unsafe option if you need to bypass pointer validation, "
         "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];
         [self.navigationController pushViewController:explorer animated:YES];
     } else {
-        [FLEXUtility alert:@"Uh-oh" message:error from:self];
+        [FLEXAlert showAlert:@"Uh-oh" message:error from:self];
         [self deselectSelectedRow];
     }
 }

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

@@ -210,7 +210,7 @@
     UIImage *image = cell.imageView.image;
 
     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];
         return;
     }
@@ -223,7 +223,7 @@
     } else {
         NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
         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;
         }
 
@@ -321,24 +321,22 @@
 
     BOOL stillExists = [NSFileManager.defaultManager fileExistsAtPath:self.path isDirectory:NULL];
     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 {
-        [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 stillExists = [NSFileManager.defaultManager fileExistsAtPath:fullPath isDirectory:&isDirectory];
     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 {
-        [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. "
     "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.";
-    
-    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

+ 7 - 6
Classes/Network/FLEXNetworkSettingsTableViewController.m

@@ -74,14 +74,15 @@
 
 - (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.sourceRect = sender.bounds;
-    [self presentViewController:actionSheet animated:YES completion:nil];
 }
 
 #pragma mark - Table view data source

+ 30 - 22
Classes/Network/FLEXNetworkTransactionDetailTableViewController.m

@@ -259,7 +259,8 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
         FLEXNetworkDetailRow *postBodyRow = [FLEXNetworkDetailRow new];
         postBodyRow.title = @"Request Body";
         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"];
             UIViewController *detailViewController = [self detailViewControllerForMIMEType:contentType data:[self postBodyDataForTransaction:transaction]];
             if (detailViewController) {
@@ -267,13 +268,15 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
                 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];
     }
 
@@ -297,33 +300,38 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
     NSData *responseData = [[FLEXNetworkRecorder defaultRecorder] cachedResponseBodyForTransaction:transaction];
     if (responseData.length > 0) {
         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.
         __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;
             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 {
         BOOL emptyResponse = transaction.receivedDataLength == 0;
         responseBodyRow.detailText = emptyResponse ? @"empty" : @"not in cache";
     }
+
     [rows addObject:responseBodyRow];
 
     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 <UIKit/UIKit.h>
 #import <objc/runtime.h>
+#import "FLEXAlert.h"
 
 #define FLEXFloor(x) (floor(UIScreen.mainScreen.scale * (x)) / UIScreen.mainScreen.scale)
 
@@ -54,8 +55,6 @@
 
 + (NSArray<UIWindow *> *)allWindows;
 
-+ (void)alert:(NSString *)title message:(NSString *)message from:(UIViewController *)viewController;
-
 // Swizzling utilities
 
 + (SEL)swizzledSelectorForSelector:(SEL)selector;

+ 6 - 4
Classes/Utility/FLEXUtility.m

@@ -411,11 +411,13 @@
     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

+ 8 - 0
FLEX.xcodeproj/project.pbxproj

@@ -182,6 +182,8 @@
 		C387C88422E0D24A00750E58 /* UIView+Layout.m in Sources */ = {isa = PBXBuildFile; fileRef = C387C88222E0D24A00750E58 /* UIView+Layout.m */; };
 		C38DF0EA22CFE4370077B4AD /* FLEXTableViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = C38DF0E822CFE4370077B4AD /* FLEXTableViewController.h */; };
 		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 */; };
 		C395D6DA21789BD800BEAD4D /* FLEXColorExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C395D6D821789BD800BEAD4D /* FLEXColorExplorerViewController.m */; };
 		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>"; };
 		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>"; };
+		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>"; };
 		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>"; };
@@ -518,6 +522,8 @@
 				3A4C945C1B5B21410088C3F2 /* FLEXRuntimeUtility.m */,
 				3A4C945D1B5B21410088C3F2 /* FLEXUtility.h */,
 				3A4C945E1B5B21410088C3F2 /* FLEXUtility.m */,
+				C38F3F2F230C958F004E3731 /* FLEXAlert.h */,
+				C38F3F30230C958F004E3731 /* FLEXAlert.m */,
 				942DCD821BAE0AD300DB5DC2 /* FLEXKeyboardShortcutManager.h */,
 				942DCD831BAE0AD300DB5DC2 /* FLEXKeyboardShortcutManager.m */,
 				94AAF0361BAF2E1F00DE8760 /* FLEXKeyboardHelpViewController.h */,
@@ -845,6 +851,7 @@
 				3A4C94ED1B5B21410088C3F2 /* FLEXArgumentInputColorView.h in Headers */,
 				3A4C94EB1B5B21410088C3F2 /* FLEXImagePreviewViewController.h in Headers */,
 				C3DA55FE21A76406005DDA60 /* FLEXMutableFieldEditorViewController.h in Headers */,
+				C38F3F31230C958F004E3731 /* FLEXAlert.h in Headers */,
 				3A4C95381B5B21410088C3F2 /* FLEXNetworkRecorder.h in Headers */,
 				3A4C94251B5B20570088C3F2 /* FLEX.h in Headers */,
 				3A4C95051B5B21410088C3F2 /* FLEXArgumentInputViewFactory.h in Headers */,
@@ -1067,6 +1074,7 @@
 				94AAF0391BAF2E1F00DE8760 /* FLEXKeyboardHelpViewController.m in Sources */,
 				3A4C951F1B5B21410088C3F2 /* FLEXClassesTableViewController.m in Sources */,
 				3A4C94C61B5B21410088C3F2 /* FLEXArrayExplorerViewController.m in Sources */,
+				C38F3F32230C958F004E3731 /* FLEXAlert.m in Sources */,
 				C3DA55FF21A76406005DDA60 /* FLEXMutableFieldEditorViewController.m in Sources */,
 				3A4C95081B5B21410088C3F2 /* FLEXDefaultEditorViewController.m in Sources */,
 				779B1ED11C0C4D7C001F5E49 /* FLEXMultiColumnTableView.m in Sources */,