mirror of
				https://github.com/SoPat712/YTLitePlus.git
				synced 2025-10-30 20:34:03 -04:00 
			
		
		
		
	added files via upload
This commit is contained in:
		
							
								
								
									
										1
									
								
								Tweaks/FLEX/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								Tweaks/FLEX/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| .DS_Store | ||||
| @@ -0,0 +1,89 @@ | ||||
| // | ||||
| //  FLEXFilteringTableViewController.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/9/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableViewController.h" | ||||
|  | ||||
| #pragma mark - FLEXTableViewFiltering | ||||
| @protocol FLEXTableViewFiltering <FLEXSearchResultsUpdating> | ||||
|  | ||||
| /// An array of visible, "filtered" sections. For example, | ||||
| /// if you have 3 sections in \c allSections and the user searches | ||||
| /// for something that matches rows in only one section, then | ||||
| /// this property would only contain that on matching section. | ||||
| @property (nonatomic, copy) NSArray<FLEXTableViewSection *> *sections; | ||||
| /// An array of all possible sections. Empty sections are to be removed | ||||
| /// and the resulting array stored in the \c section property. Setting | ||||
| /// this property should immediately set \c sections to \c nonemptySections  | ||||
| /// | ||||
| /// Do not manually initialize this property, it will be | ||||
| /// initialized for you using the result of \c makeSections. | ||||
| @property (nonatomic, copy) NSArray<FLEXTableViewSection *> *allSections; | ||||
|  | ||||
| /// This computed property should filter \c allSections for assignment to \c sections | ||||
| @property (nonatomic, readonly, copy) NSArray<FLEXTableViewSection *> *nonemptySections; | ||||
|  | ||||
| /// This should be able to re-initialize \c allSections | ||||
| - (NSArray<FLEXTableViewSection *> *)makeSections; | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| #pragma mark - FLEXFilteringTableViewController | ||||
| /// A table view which implements \c UITableView* methods using arrays of | ||||
| /// \c FLEXTableViewSection objects provied by a special delegate. | ||||
| @interface FLEXFilteringTableViewController : FLEXTableViewController <FLEXTableViewFiltering> | ||||
|  | ||||
| /// Stores the current search query. | ||||
| @property (nonatomic, copy) NSString *filterText; | ||||
|  | ||||
| /// This property is set to \c self by default. | ||||
| /// | ||||
| /// This property is used to power almost all of the table view's data source | ||||
| /// and delegate methods automatically, including row and section filtering | ||||
| /// when the user searches, 3D Touch context menus, row selection, etc. | ||||
| /// | ||||
| /// Setting this property will also set \c searchDelegate to that object. | ||||
| @property (nonatomic, weak) id<FLEXTableViewFiltering> filterDelegate; | ||||
|  | ||||
| /// Defaults to \c NO. If enabled, all filtering will be done by calling | ||||
| /// \c onBackgroundQueue:thenOnMainQueue: with the UI updated on the main queue. | ||||
| @property (nonatomic) BOOL filterInBackground; | ||||
|  | ||||
| /// Defaults to \c NO. If enabled, one • will be supplied as an index title for each section. | ||||
| @property (nonatomic) BOOL wantsSectionIndexTitles; | ||||
|  | ||||
| /// Recalculates the non-empty sections and reloads the table view. | ||||
| /// | ||||
| /// Subclasses may override to perform additional reloading logic, | ||||
| /// such as calling \c -reloadSections if needed. Be sure to call | ||||
| /// \c super after any logic that would affect the appearance of  | ||||
| /// the table view, since the table view is reloaded last. | ||||
| /// | ||||
| /// Called at the end of this class's implementation of \c updateSearchResults: | ||||
| - (void)reloadData; | ||||
|  | ||||
| /// Invoke this method to call \c -reloadData on each section | ||||
| /// in \c self.filterDelegate.allSections. | ||||
| - (void)reloadSections; | ||||
|  | ||||
| #pragma mark FLEXTableViewFiltering | ||||
|  | ||||
| @property (nonatomic, copy) NSArray<FLEXTableViewSection *> *sections; | ||||
| @property (nonatomic, copy) NSArray<FLEXTableViewSection *> *allSections; | ||||
|  | ||||
| /// Subclasses can override to hide specific sections under certain conditions | ||||
| /// if using \c self as the \c filterDelegate, as is the default. | ||||
| /// | ||||
| /// For example, the object explorer hides the description section when searching. | ||||
| @property (nonatomic, readonly, copy) NSArray<FLEXTableViewSection *> *nonemptySections; | ||||
|  | ||||
| /// If using \c self as the \c filterDelegate, as is the default, | ||||
| /// subclasses should override to provide the sections for the table view. | ||||
| - (NSArray<FLEXTableViewSection *> *)makeSections; | ||||
|  | ||||
| @end | ||||
							
								
								
									
										209
									
								
								Tweaks/FLEX/Core/Controllers/FLEXFilteringTableViewController.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								Tweaks/FLEX/Core/Controllers/FLEXFilteringTableViewController.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,209 @@ | ||||
| // | ||||
| //  FLEXFilteringTableViewController.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/9/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXFilteringTableViewController.h" | ||||
| #import "FLEXTableViewSection.h" | ||||
| #import "NSArray+FLEX.h" | ||||
| #import "FLEXMacros.h" | ||||
|  | ||||
| @interface FLEXFilteringTableViewController () | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FLEXFilteringTableViewController | ||||
| @synthesize allSections = _allSections; | ||||
|  | ||||
| #pragma mark - View controller lifecycle | ||||
|  | ||||
| - (void)loadView { | ||||
|     [super loadView]; | ||||
|      | ||||
|     if (!self.filterDelegate) { | ||||
|         self.filterDelegate = self; | ||||
|     } else { | ||||
|         [self _registerCellsForReuse]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)_registerCellsForReuse { | ||||
|     for (FLEXTableViewSection *section in self.filterDelegate.allSections) { | ||||
|         if (section.cellRegistrationMapping) { | ||||
|             [self.tableView registerCells:section.cellRegistrationMapping]; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Public | ||||
|  | ||||
| - (void)setFilterDelegate:(id<FLEXTableViewFiltering>)filterDelegate { | ||||
|     _filterDelegate = filterDelegate; | ||||
|     filterDelegate.allSections = [filterDelegate makeSections]; | ||||
|      | ||||
|     if (self.isViewLoaded) { | ||||
|         [self _registerCellsForReuse]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)reloadData { | ||||
|     [self reloadData:self.nonemptySections]; | ||||
| } | ||||
|  | ||||
| - (void)reloadData:(NSArray *)nonemptySections { | ||||
|     // Recalculate displayed sections | ||||
|     self.filterDelegate.sections = nonemptySections; | ||||
|  | ||||
|     // Refresh table view | ||||
|     if (self.isViewLoaded) { | ||||
|         [self.tableView reloadData]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)reloadSections { | ||||
|     for (FLEXTableViewSection *section in self.filterDelegate.allSections) { | ||||
|         [section reloadData]; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Search | ||||
|  | ||||
| - (void)updateSearchResults:(NSString *)newText { | ||||
|     NSArray *(^filter)(void) = ^NSArray *{ | ||||
|         self.filterText = newText; | ||||
|  | ||||
|         // Sections will adjust data based on this property | ||||
|         for (FLEXTableViewSection *section in self.filterDelegate.allSections) { | ||||
|             section.filterText = newText; | ||||
|         } | ||||
|          | ||||
|         return nil; | ||||
|     }; | ||||
|      | ||||
|     if (self.filterInBackground) { | ||||
|         [self onBackgroundQueue:filter thenOnMainQueue:^(NSArray *unused) { | ||||
|             if ([self.searchText isEqualToString:newText]) { | ||||
|                 [self reloadData]; | ||||
|             } | ||||
|         }]; | ||||
|     } else { | ||||
|         filter(); | ||||
|         [self reloadData]; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark Filtering | ||||
|  | ||||
| - (NSArray<FLEXTableViewSection *> *)nonemptySections { | ||||
|     return [self.filterDelegate.allSections flex_filtered:^BOOL(FLEXTableViewSection *section, NSUInteger idx) { | ||||
|         return section.numberOfRows > 0; | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| - (NSArray<FLEXTableViewSection *> *)makeSections { | ||||
|     return @[]; | ||||
| } | ||||
|  | ||||
| - (void)setAllSections:(NSArray<FLEXTableViewSection *> *)allSections { | ||||
|     _allSections = allSections.copy; | ||||
|     // Only display nonempty sections | ||||
|     self.sections = self.nonemptySections; | ||||
| } | ||||
|  | ||||
| - (void)setSections:(NSArray<FLEXTableViewSection *> *)sections { | ||||
|     // Allow sections to reload a portion of the table view at will | ||||
|     [sections enumerateObjectsUsingBlock:^(FLEXTableViewSection *s, NSUInteger idx, BOOL *stop) { | ||||
|         [s setTable:self.tableView section:idx]; | ||||
|     }]; | ||||
|     _sections = sections.copy; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - UITableViewDataSource | ||||
|  | ||||
| - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { | ||||
|     return self.filterDelegate.sections.count; | ||||
| } | ||||
|  | ||||
| - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { | ||||
|     return self.filterDelegate.sections[section].numberOfRows; | ||||
| } | ||||
|  | ||||
| - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { | ||||
|     return self.filterDelegate.sections[section].title; | ||||
| } | ||||
|  | ||||
| - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     NSString *reuse = [self.filterDelegate.sections[indexPath.section] reuseIdentifierForRow:indexPath.row]; | ||||
|     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuse forIndexPath:indexPath]; | ||||
|     [self.filterDelegate.sections[indexPath.section] configureCell:cell forRow:indexPath.row]; | ||||
|     return cell; | ||||
| } | ||||
|  | ||||
| - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     return UITableViewAutomaticDimension; | ||||
| } | ||||
|  | ||||
| - (NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView { | ||||
|     if (self.wantsSectionIndexTitles) { | ||||
|         return [NSArray flex_forEachUpTo:self.filterDelegate.sections.count map:^id(NSUInteger i) { | ||||
|             return @"⦁"; | ||||
|         }]; | ||||
|     } | ||||
|      | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - UITableViewDelegate | ||||
|  | ||||
| - (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     return [self.filterDelegate.sections[indexPath.section] canSelectRow:indexPath.row]; | ||||
| } | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     FLEXTableViewSection *section = self.filterDelegate.sections[indexPath.section]; | ||||
|  | ||||
|     void (^action)(UIViewController *) = [section didSelectRowAction:indexPath.row]; | ||||
|     UIViewController *details = [section viewControllerToPushForRow:indexPath.row]; | ||||
|  | ||||
|     if (action) { | ||||
|         action(self); | ||||
|         [tableView deselectRowAtIndexPath:indexPath animated:YES]; | ||||
|     } else if (details) { | ||||
|         [self.navigationController pushViewController:details animated:YES]; | ||||
|     } else { | ||||
|         [NSException raise:NSInternalInconsistencyException | ||||
|                     format:@"Row is selectable but has no action or view controller"]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath { | ||||
|     [self.filterDelegate.sections[indexPath.section] didPressInfoButtonAction:indexPath.row](self); | ||||
| } | ||||
|  | ||||
| - (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) { | ||||
|     FLEXTableViewSection *section = self.filterDelegate.sections[indexPath.section]; | ||||
|     NSString *title = [section menuTitleForRow:indexPath.row]; | ||||
|     NSArray<UIMenuElement *> *menuItems = [section menuItemsForRow:indexPath.row sender:self]; | ||||
|      | ||||
|     if (menuItems.count) { | ||||
|         return [UIContextMenuConfiguration | ||||
|             configurationWithIdentifier:nil | ||||
|             previewProvider:nil | ||||
|             actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) { | ||||
|                 return [UIMenu menuWithTitle:title children:menuItems]; | ||||
|             } | ||||
|         ]; | ||||
|     } | ||||
|      | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										19
									
								
								Tweaks/FLEX/Core/Controllers/FLEXNavigationController.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								Tweaks/FLEX/Core/Controllers/FLEXNavigationController.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| // | ||||
| //  FLEXNavigationController.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 1/30/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FLEXNavigationController : UINavigationController | ||||
|  | ||||
| + (instancetype)withRootViewController:(UIViewController *)rootVC; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
							
								
								
									
										196
									
								
								Tweaks/FLEX/Core/Controllers/FLEXNavigationController.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								Tweaks/FLEX/Core/Controllers/FLEXNavigationController.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,196 @@ | ||||
| // | ||||
| //  FLEXNavigationController.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 1/30/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXNavigationController.h" | ||||
| #import "FLEXExplorerViewController.h" | ||||
| #import "FLEXTabList.h" | ||||
|  | ||||
| @interface UINavigationController (Private) <UIGestureRecognizerDelegate> | ||||
| - (void)_gestureRecognizedInteractiveHide:(UIGestureRecognizer *)sender; | ||||
| @end | ||||
| @interface UIPanGestureRecognizer (Private) | ||||
| - (void)_setDelegate:(id)delegate; | ||||
| @end | ||||
|  | ||||
| @interface FLEXNavigationController () | ||||
| @property (nonatomic, readonly) BOOL toolbarWasHidden; | ||||
| @property (nonatomic) BOOL waitingToAddTab; | ||||
| @property (nonatomic, readonly) BOOL canShowToolbar; | ||||
| @property (nonatomic) BOOL didSetupPendingDismissButtons; | ||||
| @property (nonatomic) UISwipeGestureRecognizer *navigationBarSwipeGesture; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXNavigationController | ||||
|  | ||||
| + (instancetype)withRootViewController:(UIViewController *)rootVC { | ||||
|     return [[self alloc] initWithRootViewController:rootVC]; | ||||
| } | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|      | ||||
|     self.waitingToAddTab = YES; | ||||
|      | ||||
|     // Add gesture to reveal toolbar if hidden | ||||
|     UITapGestureRecognizer *navbarTapGesture = [[UITapGestureRecognizer alloc] | ||||
|         initWithTarget:self action:@selector(handleNavigationBarTap:) | ||||
|     ]; | ||||
|      | ||||
|     // Don't cancel touches to work around bug on versions of iOS prior to 13 | ||||
|     navbarTapGesture.cancelsTouchesInView = NO; | ||||
|     [self.navigationBar addGestureRecognizer:navbarTapGesture]; | ||||
|      | ||||
|     // Add gesture to dismiss if not presented with a sheet style | ||||
|     if (@available(iOS 13, *)) { | ||||
|         switch (self.modalPresentationStyle) { | ||||
|             case UIModalPresentationAutomatic: | ||||
|             case UIModalPresentationPageSheet: | ||||
|             case UIModalPresentationFormSheet: | ||||
|                 break; | ||||
|                  | ||||
|             default: | ||||
|                 [self addNavigationBarSwipeGesture]; | ||||
|                 break; | ||||
|         } | ||||
|     } else { | ||||
|         [self addNavigationBarSwipeGesture]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)viewWillAppear:(BOOL)animated { | ||||
|     [super viewWillAppear:animated]; | ||||
|      | ||||
|     if (self.beingPresented && !self.didSetupPendingDismissButtons) { | ||||
|         for (UIViewController *vc in self.viewControllers) { | ||||
|             [self addNavigationBarItemsToViewController:vc.navigationItem]; | ||||
|         } | ||||
|          | ||||
|         self.didSetupPendingDismissButtons = YES; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)viewDidAppear:(BOOL)animated { | ||||
|     [super viewDidAppear:animated]; | ||||
|      | ||||
|     if (self.waitingToAddTab) { | ||||
|         // Only add new tab if we're presented properly | ||||
|         if ([self.presentingViewController isKindOfClass:[FLEXExplorerViewController class]]) { | ||||
|             // New navigation controllers always add themselves as new tabs, | ||||
|             // tabs are closed by FLEXExplorerViewController | ||||
|             [FLEXTabList.sharedList addTab:self]; | ||||
|             self.waitingToAddTab = NO; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated { | ||||
|     [super pushViewController:viewController animated:animated]; | ||||
|     [self addNavigationBarItemsToViewController:viewController.navigationItem]; | ||||
| } | ||||
|  | ||||
| - (void)dismissAnimated { | ||||
|     // Tabs are only closed if the done button is pressed; this | ||||
|     // allows you to leave a tab open by dragging down to dismiss | ||||
|     if ([self.presentingViewController isKindOfClass:[FLEXExplorerViewController class]]) { | ||||
|         [FLEXTabList.sharedList closeTab:self];         | ||||
|     } | ||||
|      | ||||
|     [self.presentingViewController dismissViewControllerAnimated:YES completion:nil]; | ||||
| } | ||||
|  | ||||
| - (BOOL)canShowToolbar { | ||||
|     return self.topViewController.toolbarItems.count > 0; | ||||
| } | ||||
|  | ||||
| - (void)addNavigationBarItemsToViewController:(UINavigationItem *)navigationItem { | ||||
|     if (!self.presentingViewController) { | ||||
|         return; | ||||
|     } | ||||
|      | ||||
|     // Check if a done item already exists | ||||
|     for (UIBarButtonItem *item in navigationItem.rightBarButtonItems) { | ||||
|         if (item.style == UIBarButtonItemStyleDone) { | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Give root view controllers a Done button if it does not already have one | ||||
|     UIBarButtonItem *done = [[UIBarButtonItem alloc] | ||||
|         initWithBarButtonSystemItem:UIBarButtonSystemItemDone | ||||
|         target:self | ||||
|         action:@selector(dismissAnimated) | ||||
|     ]; | ||||
|      | ||||
|     // Prepend the button if other buttons exist already | ||||
|     NSArray *existingItems = navigationItem.rightBarButtonItems; | ||||
|     if (existingItems.count) { | ||||
|         navigationItem.rightBarButtonItems = [@[done] arrayByAddingObjectsFromArray:existingItems]; | ||||
|     } else { | ||||
|         navigationItem.rightBarButtonItem = done; | ||||
|     } | ||||
|      | ||||
|     // Keeps us from calling this method again on | ||||
|     // the same view controllers in -viewWillAppear: | ||||
|     self.didSetupPendingDismissButtons = YES; | ||||
| } | ||||
|  | ||||
| - (void)addNavigationBarSwipeGesture { | ||||
|     UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] | ||||
|         initWithTarget:self action:@selector(handleNavigationBarSwipe:) | ||||
|     ]; | ||||
|     swipe.direction = UISwipeGestureRecognizerDirectionDown; | ||||
|     swipe.delegate = self; | ||||
|     self.navigationBarSwipeGesture = swipe; | ||||
|     [self.navigationBar addGestureRecognizer:swipe]; | ||||
| } | ||||
|  | ||||
| - (void)handleNavigationBarSwipe:(UISwipeGestureRecognizer *)sender { | ||||
|     if (sender.state == UIGestureRecognizerStateRecognized) { | ||||
|         [self.presentingViewController dismissViewControllerAnimated:YES completion:nil]; | ||||
|     } | ||||
| } | ||||
|       | ||||
| - (void)handleNavigationBarTap:(UIGestureRecognizer *)sender { | ||||
|     // Don't reveal the toolbar if we were just tapping a button | ||||
|     CGPoint location = [sender locationInView:self.navigationBar]; | ||||
|     UIView *hitView = [self.navigationBar hitTest:location withEvent:nil]; | ||||
|     if ([hitView isKindOfClass:[UIControl class]]) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     if (sender.state == UIGestureRecognizerStateRecognized) { | ||||
|         if (self.toolbarHidden && self.canShowToolbar) { | ||||
|             [self setToolbarHidden:NO animated:YES]; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (BOOL)gestureRecognizer:(UIGestureRecognizer *)g1 shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)g2 { | ||||
|     if (g1 == self.navigationBarSwipeGesture && g2 == self.barHideOnSwipeGestureRecognizer) { | ||||
|         return YES; | ||||
|     } | ||||
|      | ||||
|     return NO; | ||||
| } | ||||
|  | ||||
| - (void)_gestureRecognizedInteractiveHide:(UIPanGestureRecognizer *)sender { | ||||
|     if (sender.state == UIGestureRecognizerStateRecognized) { | ||||
|         BOOL show = self.canShowToolbar; | ||||
|         CGFloat yTranslation = [sender translationInView:self.view].y; | ||||
|         CGFloat yVelocity = [sender velocityInView:self.view].y; | ||||
|         if (yVelocity > 2000) { | ||||
|             [self setToolbarHidden:YES animated:YES]; | ||||
|         } else if (show && yTranslation > 20 && yVelocity > 250) { | ||||
|             [self setToolbarHidden:NO animated:YES]; | ||||
|         } else if (yTranslation < -20) { | ||||
|             [self setToolbarHidden:YES animated:YES]; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										153
									
								
								Tweaks/FLEX/Core/Controllers/FLEXTableViewController.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								Tweaks/FLEX/Core/Controllers/FLEXTableViewController.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | ||||
| // | ||||
| //  FLEXTableViewController.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 7/5/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
| #import "FLEXTableView.h" | ||||
| @class FLEXScopeCarousel, FLEXWindow, FLEXTableViewSection; | ||||
|  | ||||
| typedef CGFloat FLEXDebounceInterval; | ||||
| /// No delay, all events delivered | ||||
| extern CGFloat const kFLEXDebounceInstant; | ||||
| /// Small delay which makes UI seem smoother by avoiding rapid events | ||||
| extern CGFloat const kFLEXDebounceFast; | ||||
| /// Slower than Fast, faster than ExpensiveIO | ||||
| extern CGFloat const kFLEXDebounceForAsyncSearch; | ||||
| /// The least frequent, at just over once per second; for I/O or other expensive operations | ||||
| extern CGFloat const kFLEXDebounceForExpensiveIO; | ||||
|  | ||||
| @protocol FLEXSearchResultsUpdating <NSObject> | ||||
| /// A method to handle search query update events. | ||||
| /// | ||||
| /// \c searchBarDebounceInterval is used to reduce the frequency at which this | ||||
| /// method is called. This method is also called when the search bar becomes | ||||
| /// the first responder, and when the selected search bar scope index changes. | ||||
| - (void)updateSearchResults:(NSString *)newText; | ||||
| @end | ||||
|  | ||||
| @interface FLEXTableViewController : UITableViewController < | ||||
|     UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate | ||||
| > | ||||
|  | ||||
| /// A grouped table view. Inset on iOS 13. | ||||
| /// | ||||
| /// Simply calls into \c initWithStyle: | ||||
| - (id)init; | ||||
|  | ||||
| /// Subclasses may override to configure the controller before \c viewDidLoad: | ||||
| - (id)initWithStyle:(UITableViewStyle)style; | ||||
|  | ||||
| @property (nonatomic) FLEXTableView *tableView; | ||||
|  | ||||
| /// If your subclass conforms to \c FLEXSearchResultsUpdating | ||||
| /// then this property is assigned to \c self automatically. | ||||
| /// | ||||
| /// Setting \c filterDelegate will also set this property to that object. | ||||
| @property (nonatomic, weak) id<FLEXSearchResultsUpdating> searchDelegate; | ||||
|  | ||||
| /// Defaults to NO. | ||||
| /// | ||||
| /// Setting this to YES will initialize the carousel and the view. | ||||
| @property (nonatomic) BOOL showsCarousel; | ||||
| /// A horizontally scrolling list with functionality similar to | ||||
| /// that of a search bar's scope bar. You'd want to use this when | ||||
| /// you have potentially more than 4 scope options. | ||||
| @property (nonatomic) FLEXScopeCarousel *carousel; | ||||
|  | ||||
| /// Defaults to NO. | ||||
| /// | ||||
| /// Setting this to YES will initialize searchController and the view. | ||||
| @property (nonatomic) BOOL showsSearchBar; | ||||
| /// Defaults to NO. | ||||
| /// | ||||
| /// Setting this to YES will make the search bar appear whenever the view appears. | ||||
| /// Otherwise, iOS will only show the search bar when you scroll up. | ||||
| @property (nonatomic) BOOL showSearchBarInitially; | ||||
| /// Defaults to NO. | ||||
| /// | ||||
| /// Setting this to YES will make the search bar activate whenever the view appears. | ||||
| @property (nonatomic) BOOL activatesSearchBarAutomatically; | ||||
|  | ||||
| /// nil unless showsSearchBar is set to YES. | ||||
| /// | ||||
| /// self is used as the default search results updater and delegate. | ||||
| /// The search bar will not dim the background or hide the navigation bar by default. | ||||
| /// On iOS 11 and up, the search bar will appear in the navigation bar below the title. | ||||
| @property (nonatomic) UISearchController *searchController; | ||||
| /// Used to initialize the search controller. Defaults to nil. | ||||
| @property (nonatomic) UIViewController *searchResultsController; | ||||
| /// Defaults to "Fast" | ||||
| /// | ||||
| /// Determines how often search bar results will be "debounced." | ||||
| /// Empty query events are always sent instantly. Query events will | ||||
| /// be sent when the user has not changed the query for this interval. | ||||
| @property (nonatomic) FLEXDebounceInterval searchBarDebounceInterval; | ||||
| /// Whether the search bar stays at the top of the view while scrolling. | ||||
| /// | ||||
| /// Calls into self.navigationItem.hidesSearchBarWhenScrolling. | ||||
| /// Do not change self.navigationItem.hidesSearchBarWhenScrolling directly, | ||||
| /// or it will not be respsected. Use this instead. | ||||
| /// Defaults to NO. | ||||
| @property (nonatomic) BOOL pinSearchBar; | ||||
| /// By default, we will show the search bar's cancel button when | ||||
| /// search becomes active and hide it when search is dismissed. | ||||
| /// | ||||
| /// Do not set the showsCancelButton property on the searchController's | ||||
| /// searchBar manually. Set this property after turning on showsSearchBar. | ||||
| /// | ||||
| /// Does nothing pre-iOS 13, safe to call on any version. | ||||
| @property (nonatomic) BOOL automaticallyShowsSearchBarCancelButton; | ||||
|  | ||||
| /// If using the scope bar, self.searchController.searchBar.selectedScopeButtonIndex. | ||||
| /// Otherwise, this is the selected index of the carousel, or NSNotFound if using neither. | ||||
| @property (nonatomic) NSInteger selectedScope; | ||||
| /// self.searchController.searchBar.text | ||||
| @property (nonatomic, readonly, copy) NSString *searchText; | ||||
|  | ||||
| /// A totally optional delegate to forward search results updater calls to. | ||||
| /// If a delegate is set, updateSearchResults: is not called on this view controller. | ||||
| @property (nonatomic, weak) id<FLEXSearchResultsUpdating> searchResultsUpdater; | ||||
|  | ||||
| /// self.view.window as a \c FLEXWindow | ||||
| @property (nonatomic, readonly) FLEXWindow *window; | ||||
|  | ||||
| /// Convenient for doing some async processor-intensive searching | ||||
| /// in the background before updating the UI back on the main queue. | ||||
| - (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock; | ||||
|  | ||||
| /// Adds up to 3 additional items to the toolbar in right-to-left order. | ||||
| /// | ||||
| /// That is, the first item in the given array will be the rightmost item behind | ||||
| /// any existing toolbar items. By default, buttons for bookmarks and tabs are shown. | ||||
| /// | ||||
| /// If you wish to have more control over how the buttons are arranged or which | ||||
| /// buttons are displayed, you can access the properties for the pre-existing | ||||
| /// toolbar items directly and manually set \c self.toolbarItems by overriding | ||||
| /// the \c setupToolbarItems method below. | ||||
| - (void)addToolbarItems:(NSArray<UIBarButtonItem *> *)items; | ||||
|  | ||||
| /// Subclasses may override. You should not need to call this method directly. | ||||
| - (void)setupToolbarItems; | ||||
|  | ||||
| @property (nonatomic, readonly) UIBarButtonItem *shareToolbarItem; | ||||
| @property (nonatomic, readonly) UIBarButtonItem *bookmarksToolbarItem; | ||||
| @property (nonatomic, readonly) UIBarButtonItem *openTabsToolbarItem; | ||||
|  | ||||
| /// Whether or not to display the "share" icon in the middle of the toolbar. NO by default. | ||||
| /// | ||||
| /// Turning this on after you have added custom toolbar items will | ||||
| /// push off the leftmost toolbar item and shift the others leftward. | ||||
| @property (nonatomic) BOOL showsShareToolbarItem; | ||||
| /// Called when the share button is pressed. | ||||
| /// Default implementation does nothign. Subclasses may override. | ||||
| - (void)shareButtonPressed:(UIBarButtonItem *)sender; | ||||
|  | ||||
| /// Subclasses may call this to opt-out of all toolbar related behavior. | ||||
| /// This is necessary if you want to disable the gesture which reveals the toolbar. | ||||
| - (void)disableToolbar; | ||||
|  | ||||
| @end | ||||
							
								
								
									
										618
									
								
								Tweaks/FLEX/Core/Controllers/FLEXTableViewController.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										618
									
								
								Tweaks/FLEX/Core/Controllers/FLEXTableViewController.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,618 @@ | ||||
| // | ||||
| //  FLEXTableViewController.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 7/5/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableViewController.h" | ||||
| #import "FLEXExplorerViewController.h" | ||||
| #import "FLEXBookmarksViewController.h" | ||||
| #import "FLEXTabsViewController.h" | ||||
| #import "FLEXScopeCarousel.h" | ||||
| #import "FLEXTableView.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXResources.h" | ||||
| #import "UIBarButtonItem+FLEX.h" | ||||
| #import <objc/runtime.h> | ||||
|  | ||||
| @interface Block : NSObject | ||||
| - (void)invoke; | ||||
| @end | ||||
|  | ||||
| CGFloat const kFLEXDebounceInstant = 0.f; | ||||
| CGFloat const kFLEXDebounceFast = 0.05; | ||||
| CGFloat const kFLEXDebounceForAsyncSearch = 0.15; | ||||
| CGFloat const kFLEXDebounceForExpensiveIO = 0.5; | ||||
|  | ||||
| @interface FLEXTableViewController () | ||||
| @property (nonatomic) NSTimer *debounceTimer; | ||||
| @property (nonatomic) BOOL didInitiallyRevealSearchBar; | ||||
| @property (nonatomic) UITableViewStyle style; | ||||
|  | ||||
| @property (nonatomic) BOOL hasAppeared; | ||||
| @property (nonatomic, readonly) UIView *tableHeaderViewContainer; | ||||
|  | ||||
| @property (nonatomic, readonly) BOOL manuallyDeactivateSearchOnDisappear; | ||||
|  | ||||
| @property (nonatomic) UIBarButtonItem *middleToolbarItem; | ||||
| @property (nonatomic) UIBarButtonItem *middleLeftToolbarItem; | ||||
| @property (nonatomic) UIBarButtonItem *leftmostToolbarItem; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXTableViewController | ||||
| @dynamic tableView; | ||||
| @synthesize showsShareToolbarItem = _showsShareToolbarItem; | ||||
| @synthesize tableHeaderViewContainer = _tableHeaderViewContainer; | ||||
| @synthesize automaticallyShowsSearchBarCancelButton = _automaticallyShowsSearchBarCancelButton; | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| - (id)init { | ||||
|     if (@available(iOS 13.0, *)) { | ||||
|         self = [self initWithStyle:UITableViewStyleInsetGrouped]; | ||||
|     } else { | ||||
|         self = [self initWithStyle:UITableViewStyleGrouped]; | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (id)initWithStyle:(UITableViewStyle)style { | ||||
|     self = [super initWithStyle:style]; | ||||
|      | ||||
|     if (self) { | ||||
|         _searchBarDebounceInterval = kFLEXDebounceFast; | ||||
|         _showSearchBarInitially = YES; | ||||
|         _style = style; | ||||
|         _manuallyDeactivateSearchOnDisappear = ( | ||||
|             NSProcessInfo.processInfo.operatingSystemVersion.majorVersion < 11 | ||||
|         ); | ||||
|          | ||||
|         // We will be our own search delegate if we implement this method | ||||
|         if ([self respondsToSelector:@selector(updateSearchResults:)]) { | ||||
|             self.searchDelegate = (id)self; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Public | ||||
|  | ||||
| - (FLEXWindow *)window { | ||||
|     return (id)self.view.window; | ||||
| } | ||||
|  | ||||
| - (void)setShowsSearchBar:(BOOL)showsSearchBar { | ||||
|     if (_showsSearchBar == showsSearchBar) return; | ||||
|     _showsSearchBar = showsSearchBar; | ||||
|      | ||||
|     if (showsSearchBar) { | ||||
|         UIViewController *results = self.searchResultsController; | ||||
|         self.searchController = [[UISearchController alloc] initWithSearchResultsController:results]; | ||||
|         self.searchController.searchBar.placeholder = @"Filter"; | ||||
|         self.searchController.searchResultsUpdater = (id)self; | ||||
|         self.searchController.delegate = (id)self; | ||||
|         self.searchController.dimsBackgroundDuringPresentation = NO; | ||||
|         self.searchController.hidesNavigationBarDuringPresentation = NO; | ||||
|         /// Not necessary in iOS 13; remove this when iOS 13 is the minimum deployment target | ||||
|         self.searchController.searchBar.delegate = self; | ||||
|  | ||||
|         self.automaticallyShowsSearchBarCancelButton = YES; | ||||
|  | ||||
|         if (@available(iOS 13, *)) { | ||||
|             self.searchController.automaticallyShowsScopeBar = NO; | ||||
|         } | ||||
|          | ||||
|         [self addSearchController:self.searchController]; | ||||
|     } else { | ||||
|         // Search already shown and just set to NO, so remove it | ||||
|         [self removeSearchController:self.searchController]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)setShowsCarousel:(BOOL)showsCarousel { | ||||
|     if (_showsCarousel == showsCarousel) return; | ||||
|     _showsCarousel = showsCarousel; | ||||
|      | ||||
|     if (showsCarousel) { | ||||
|         _carousel = ({ weakify(self) | ||||
|              | ||||
|             FLEXScopeCarousel *carousel = [FLEXScopeCarousel new]; | ||||
|             carousel.selectedIndexChangedAction = ^(NSInteger idx) { strongify(self); | ||||
|                 [self.searchDelegate updateSearchResults:self.searchText]; | ||||
|             }; | ||||
|  | ||||
|             // UITableView won't update the header size unless you reset the header view | ||||
|             [carousel registerBlockForDynamicTypeChanges:^(FLEXScopeCarousel *_) { strongify(self); | ||||
|                 [self layoutTableHeaderIfNeeded]; | ||||
|             }]; | ||||
|  | ||||
|             carousel; | ||||
|         }); | ||||
|         [self addCarousel:_carousel]; | ||||
|     } else { | ||||
|         // Carousel already shown and just set to NO, so remove it | ||||
|         [self removeCarousel:_carousel]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (NSInteger)selectedScope { | ||||
|     if (self.searchController.searchBar.showsScopeBar) { | ||||
|         return self.searchController.searchBar.selectedScopeButtonIndex; | ||||
|     } else if (self.showsCarousel) { | ||||
|         return self.carousel.selectedIndex; | ||||
|     } else { | ||||
|         return 0; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)setSelectedScope:(NSInteger)selectedScope { | ||||
|     if (self.searchController.searchBar.showsScopeBar) { | ||||
|         self.searchController.searchBar.selectedScopeButtonIndex = selectedScope; | ||||
|     } else if (self.showsCarousel) { | ||||
|         self.carousel.selectedIndex = selectedScope; | ||||
|     } | ||||
|  | ||||
|     [self.searchDelegate updateSearchResults:self.searchText]; | ||||
| } | ||||
|  | ||||
| - (NSString *)searchText { | ||||
|     return self.searchController.searchBar.text; | ||||
| } | ||||
|  | ||||
| - (BOOL)automaticallyShowsSearchBarCancelButton { | ||||
|     if (@available(iOS 13, *)) { | ||||
|         return self.searchController.automaticallyShowsCancelButton; | ||||
|     } | ||||
|  | ||||
|     return _automaticallyShowsSearchBarCancelButton; | ||||
| } | ||||
|  | ||||
| - (void)setAutomaticallyShowsSearchBarCancelButton:(BOOL)value { | ||||
|     if (@available(iOS 13, *)) { | ||||
|         self.searchController.automaticallyShowsCancelButton = value; | ||||
|     } | ||||
|  | ||||
|     _automaticallyShowsSearchBarCancelButton = value; | ||||
| } | ||||
|  | ||||
| - (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock { | ||||
|     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ | ||||
|         NSArray *items = backgroundBlock(); | ||||
|         dispatch_async(dispatch_get_main_queue(), ^{ | ||||
|             mainBlock(items); | ||||
|         }); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| - (void)setsShowsShareToolbarItem:(BOOL)showsShareToolbarItem { | ||||
|     _showsShareToolbarItem = showsShareToolbarItem; | ||||
|     if (self.isViewLoaded) { | ||||
|         [self setupToolbarItems]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)disableToolbar { | ||||
|     self.navigationController.toolbarHidden = YES; | ||||
|     self.navigationController.hidesBarsOnSwipe = NO; | ||||
|     self.toolbarItems = nil; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - View Controller Lifecycle | ||||
|  | ||||
| - (void)loadView { | ||||
|     self.view = [FLEXTableView style:self.style]; | ||||
|     self.tableView.dataSource = self; | ||||
|     self.tableView.delegate = self; | ||||
|      | ||||
|     _shareToolbarItem = FLEXBarButtonItemSystem(Action, self, @selector(shareButtonPressed:)); | ||||
|     _bookmarksToolbarItem = [UIBarButtonItem | ||||
|         flex_itemWithImage:FLEXResources.bookmarksIcon target:self action:@selector(showBookmarks) | ||||
|     ]; | ||||
|     _openTabsToolbarItem = [UIBarButtonItem | ||||
|         flex_itemWithImage:FLEXResources.openTabsIcon target:self action:@selector(showTabSwitcher) | ||||
|     ]; | ||||
|      | ||||
|     self.leftmostToolbarItem = UIBarButtonItem.flex_fixedSpace; | ||||
|     self.middleLeftToolbarItem = UIBarButtonItem.flex_fixedSpace; | ||||
|     self.middleToolbarItem = UIBarButtonItem.flex_fixedSpace; | ||||
| } | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|      | ||||
|     self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag; | ||||
|      | ||||
|     // Toolbar | ||||
|     self.navigationController.toolbarHidden = self.toolbarItems.count > 0; | ||||
|     self.navigationController.hidesBarsOnSwipe = YES; | ||||
|  | ||||
|     // On iOS 13, the root view controller shows it's search bar no matter what. | ||||
|     // Turning this off avoids some weird flash the navigation bar does when we | ||||
|     // toggle navigationItem.hidesSearchBarWhenScrolling on and off. The flash | ||||
|     // will still happen on subsequent view controllers, but we can at least | ||||
|     // avoid it for the root view controller | ||||
|     if (@available(iOS 13, *)) { | ||||
|         if (self.navigationController.viewControllers.firstObject == self) { | ||||
|             _showSearchBarInitially = NO; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)viewWillAppear:(BOOL)animated { | ||||
|     [super viewWillAppear:animated]; | ||||
|      | ||||
|     if (@available(iOS 11.0, *)) { | ||||
|         // When going back, make the search bar reappear instead of hiding | ||||
|         if ((self.pinSearchBar || self.showSearchBarInitially) && !self.didInitiallyRevealSearchBar) { | ||||
|             self.navigationItem.hidesSearchBarWhenScrolling = NO; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Make the keyboard seem to appear faster | ||||
|     if (self.activatesSearchBarAutomatically) { | ||||
|         [self makeKeyboardAppearNow]; | ||||
|     } | ||||
|  | ||||
|     [self setupToolbarItems]; | ||||
| } | ||||
|  | ||||
| - (void)viewDidAppear:(BOOL)animated { | ||||
|     [super viewDidAppear:animated]; | ||||
|  | ||||
|     // Allow scrolling to collapse the search bar, only if we don't want it pinned | ||||
|     if (@available(iOS 11.0, *)) { | ||||
|         if (self.showSearchBarInitially && !self.pinSearchBar && !self.didInitiallyRevealSearchBar) { | ||||
|             // All this mumbo jumbo is necessary to work around a bug in iOS 13 up to 13.2 | ||||
|             // wherein quickly toggling navigationItem.hidesSearchBarWhenScrolling to make | ||||
|             // the search bar appear initially results in a bugged search bar that | ||||
|             // becomes transparent and floats over the screen as you scroll | ||||
|             [UIView animateWithDuration:0.2 animations:^{ | ||||
|                 self.navigationItem.hidesSearchBarWhenScrolling = YES; | ||||
|                 [self.navigationController.view setNeedsLayout]; | ||||
|                 [self.navigationController.view layoutIfNeeded]; | ||||
|             }]; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     if (self.activatesSearchBarAutomatically) { | ||||
|         // Keyboard has appeared, now we call this as we soon present our search bar | ||||
|         [self removeDummyTextField]; | ||||
|          | ||||
|         // Activate the search bar | ||||
|         dispatch_async(dispatch_get_main_queue(), ^{ | ||||
|             // This doesn't work unless it's wrapped in this dispatch_async call | ||||
|             [self.searchController.searchBar becomeFirstResponder]; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // We only want to reveal the search bar when the view controller first appears. | ||||
|     self.didInitiallyRevealSearchBar = YES; | ||||
| } | ||||
|  | ||||
| - (void)viewWillDisappear:(BOOL)animated { | ||||
|     [super viewWillDisappear:animated]; | ||||
|      | ||||
|     if (self.manuallyDeactivateSearchOnDisappear && self.searchController.isActive) { | ||||
|         self.searchController.active = NO; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)didMoveToParentViewController:(UIViewController *)parent { | ||||
|     [super didMoveToParentViewController:parent]; | ||||
|     // Reset this since we are re-appearing under a new | ||||
|     // parent view controller and need to show it again | ||||
|     self.didInitiallyRevealSearchBar = NO; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Toolbar, Public | ||||
|  | ||||
| - (void)setupToolbarItems { | ||||
|     if (!self.isViewLoaded) { | ||||
|         return; | ||||
|     } | ||||
|      | ||||
|     self.toolbarItems = @[ | ||||
|         self.leftmostToolbarItem, | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         self.middleLeftToolbarItem, | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         self.middleToolbarItem, | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         self.bookmarksToolbarItem, | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         self.openTabsToolbarItem, | ||||
|     ]; | ||||
|      | ||||
|     for (UIBarButtonItem *item in self.toolbarItems) { | ||||
|         [item _setWidth:60]; | ||||
|         // This does not work for anything but fixed spaces for some reason | ||||
|         // item.width = 60; | ||||
|     } | ||||
|      | ||||
|     // Disable tabs entirely when not presented by FLEXExplorerViewController | ||||
|     UIViewController *presenter = self.navigationController.presentingViewController; | ||||
|     if (![presenter isKindOfClass:[FLEXExplorerViewController class]]) { | ||||
|         self.openTabsToolbarItem.enabled = NO; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)addToolbarItems:(NSArray<UIBarButtonItem *> *)items { | ||||
|     if (self.showsShareToolbarItem) { | ||||
|         // Share button is in the middle, skip middle button | ||||
|         if (items.count > 0) { | ||||
|             self.middleLeftToolbarItem = items[0]; | ||||
|         } | ||||
|         if (items.count > 1) { | ||||
|             self.leftmostToolbarItem = items[1]; | ||||
|         } | ||||
|     } else { | ||||
|         // Add buttons right-to-left | ||||
|         if (items.count > 0) { | ||||
|             self.middleToolbarItem = items[0]; | ||||
|         } | ||||
|         if (items.count > 1) { | ||||
|             self.middleLeftToolbarItem = items[1]; | ||||
|         } | ||||
|         if (items.count > 2) { | ||||
|             self.leftmostToolbarItem = items[2]; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     [self setupToolbarItems]; | ||||
| } | ||||
|  | ||||
| - (void)setShowsShareToolbarItem:(BOOL)showShare { | ||||
|     if (_showsShareToolbarItem != showShare) { | ||||
|         _showsShareToolbarItem = showShare; | ||||
|          | ||||
|         if (showShare) { | ||||
|             // Push out leftmost item | ||||
|             self.leftmostToolbarItem = self.middleLeftToolbarItem; | ||||
|             self.middleLeftToolbarItem = self.middleToolbarItem; | ||||
|              | ||||
|             // Use share for middle | ||||
|             self.middleToolbarItem = self.shareToolbarItem; | ||||
|         } else { | ||||
|             // Remove share, shift custom items rightward | ||||
|             self.middleToolbarItem = self.middleLeftToolbarItem; | ||||
|             self.middleLeftToolbarItem = self.leftmostToolbarItem; | ||||
|             self.leftmostToolbarItem = UIBarButtonItem.flex_fixedSpace; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     [self setupToolbarItems]; | ||||
| } | ||||
|  | ||||
| - (void)shareButtonPressed:(UIBarButtonItem *)sender { | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Private | ||||
|  | ||||
| - (void)debounce:(void(^)(void))block { | ||||
|     [self.debounceTimer invalidate]; | ||||
|      | ||||
|     self.debounceTimer = [NSTimer | ||||
|         scheduledTimerWithTimeInterval:self.searchBarDebounceInterval | ||||
|         target:block | ||||
|         selector:@selector(invoke) | ||||
|         userInfo:nil | ||||
|         repeats:NO | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| - (void)layoutTableHeaderIfNeeded { | ||||
|     if (self.showsCarousel) { | ||||
|         self.carousel.frame = FLEXRectSetHeight( | ||||
|             self.carousel.frame, self.carousel.intrinsicContentSize.height | ||||
|         ); | ||||
|     } | ||||
|      | ||||
|     self.tableView.tableHeaderView = self.tableView.tableHeaderView; | ||||
| } | ||||
|  | ||||
| - (void)addCarousel:(FLEXScopeCarousel *)carousel { | ||||
|     if (@available(iOS 11.0, *)) { | ||||
|         self.tableView.tableHeaderView = carousel; | ||||
|     } else { | ||||
|         carousel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; | ||||
|          | ||||
|         CGRect frame = self.tableHeaderViewContainer.frame; | ||||
|         CGRect subviewFrame = carousel.frame; | ||||
|         subviewFrame.origin.y = 0; | ||||
|          | ||||
|         // Put the carousel below the search bar if it's already there | ||||
|         if (self.showsSearchBar) { | ||||
|             carousel.frame = subviewFrame = FLEXRectSetY( | ||||
|                 subviewFrame, self.searchController.searchBar.frame.size.height | ||||
|             ); | ||||
|             frame.size.height += carousel.intrinsicContentSize.height; | ||||
|         } else { | ||||
|             frame.size.height = carousel.intrinsicContentSize.height; | ||||
|         } | ||||
|          | ||||
|         self.tableHeaderViewContainer.frame = frame; | ||||
|         [self.tableHeaderViewContainer addSubview:carousel]; | ||||
|     } | ||||
|      | ||||
|     [self layoutTableHeaderIfNeeded]; | ||||
| } | ||||
|  | ||||
| - (void)removeCarousel:(FLEXScopeCarousel *)carousel { | ||||
|     [carousel removeFromSuperview]; | ||||
|      | ||||
|     if (@available(iOS 11.0, *)) { | ||||
|         self.tableView.tableHeaderView = nil; | ||||
|     } else { | ||||
|         if (self.showsSearchBar) { | ||||
|             [self removeSearchController:self.searchController]; | ||||
|             [self addSearchController:self.searchController]; | ||||
|         } else { | ||||
|             self.tableView.tableHeaderView = nil; | ||||
|             _tableHeaderViewContainer = nil; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)addSearchController:(UISearchController *)controller { | ||||
|     if (@available(iOS 11.0, *)) { | ||||
|         self.navigationItem.searchController = controller; | ||||
|     } else { | ||||
|         controller.searchBar.autoresizingMask |= UIViewAutoresizingFlexibleBottomMargin; | ||||
|         [self.tableHeaderViewContainer addSubview:controller.searchBar]; | ||||
|         CGRect subviewFrame = controller.searchBar.frame; | ||||
|         CGRect frame = self.tableHeaderViewContainer.frame; | ||||
|         frame.size.width = MAX(frame.size.width, subviewFrame.size.width); | ||||
|         frame.size.height = subviewFrame.size.height; | ||||
|          | ||||
|         // Move the carousel down if it's already there | ||||
|         if (self.showsCarousel) { | ||||
|             self.carousel.frame = FLEXRectSetY( | ||||
|                 self.carousel.frame, subviewFrame.size.height | ||||
|             ); | ||||
|             frame.size.height += self.carousel.frame.size.height; | ||||
|         } | ||||
|          | ||||
|         self.tableHeaderViewContainer.frame = frame; | ||||
|         [self layoutTableHeaderIfNeeded]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)removeSearchController:(UISearchController *)controller { | ||||
|     if (@available(iOS 11.0, *)) { | ||||
|         self.navigationItem.searchController = nil; | ||||
|     } else { | ||||
|         [controller.searchBar removeFromSuperview]; | ||||
|          | ||||
|         if (self.showsCarousel) { | ||||
| //            self.carousel.frame = FLEXRectRemake(CGPointZero, self.carousel.frame.size); | ||||
|             [self removeCarousel:self.carousel]; | ||||
|             [self addCarousel:self.carousel]; | ||||
|         } else { | ||||
|             self.tableView.tableHeaderView = nil; | ||||
|             _tableHeaderViewContainer = nil; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (UIView *)tableHeaderViewContainer { | ||||
|     if (!_tableHeaderViewContainer) { | ||||
|         _tableHeaderViewContainer = [UIView new]; | ||||
|         self.tableView.tableHeaderView = self.tableHeaderViewContainer; | ||||
|     } | ||||
|      | ||||
|     return _tableHeaderViewContainer; | ||||
| } | ||||
|  | ||||
| - (void)showBookmarks { | ||||
|     UINavigationController *nav = [[UINavigationController alloc] | ||||
|         initWithRootViewController:[FLEXBookmarksViewController new] | ||||
|     ]; | ||||
|     [self presentViewController:nav animated:YES completion:nil]; | ||||
| } | ||||
|  | ||||
| - (void)showTabSwitcher { | ||||
|     UINavigationController *nav = [[UINavigationController alloc] | ||||
|         initWithRootViewController:[FLEXTabsViewController new] | ||||
|     ]; | ||||
|     [self presentViewController:nav animated:YES completion:nil]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Search Bar | ||||
|  | ||||
| #pragma mark Faster keyboard | ||||
|  | ||||
| static UITextField *kDummyTextField = nil; | ||||
|  | ||||
| /// Make the keyboard appear instantly. We use this to make the | ||||
| /// keyboard appear faster when the search bar is set to appear initially. | ||||
| /// You must call \c -removeDummyTextField before your search bar is to appear. | ||||
| - (void)makeKeyboardAppearNow { | ||||
|     if (!kDummyTextField) { | ||||
|         kDummyTextField = [UITextField new]; | ||||
|         kDummyTextField.autocorrectionType = UITextAutocorrectionTypeNo; | ||||
|     } | ||||
|      | ||||
|     kDummyTextField.inputAccessoryView = self.searchController.searchBar.inputAccessoryView; | ||||
|     [UIApplication.sharedApplication.keyWindow addSubview:kDummyTextField]; | ||||
|     [kDummyTextField becomeFirstResponder]; | ||||
| } | ||||
|  | ||||
| - (void)removeDummyTextField { | ||||
|     if (kDummyTextField.superview) { | ||||
|         [kDummyTextField removeFromSuperview]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| #pragma mark UISearchResultsUpdating | ||||
|  | ||||
| - (void)updateSearchResultsForSearchController:(UISearchController *)searchController { | ||||
|     [self.debounceTimer invalidate]; | ||||
|     NSString *text = searchController.searchBar.text; | ||||
|      | ||||
|     void (^updateSearchResults)(void) = ^{ | ||||
|         if (self.searchResultsUpdater) { | ||||
|             [self.searchResultsUpdater updateSearchResults:text]; | ||||
|         } else { | ||||
|             [self.searchDelegate updateSearchResults:text]; | ||||
|         } | ||||
|     }; | ||||
|      | ||||
|     // Only debounce if we want to, and if we have a non-empty string | ||||
|     // Empty string events are sent instantly | ||||
|     if (text.length && self.searchBarDebounceInterval > kFLEXDebounceInstant) { | ||||
|         [self debounce:updateSearchResults]; | ||||
|     } else { | ||||
|         updateSearchResults(); | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark UISearchControllerDelegate | ||||
|  | ||||
| - (void)willPresentSearchController:(UISearchController *)searchController { | ||||
|     // Manually show cancel button for < iOS 13 | ||||
|     if (!@available(iOS 13, *) && self.automaticallyShowsSearchBarCancelButton) { | ||||
|         [searchController.searchBar setShowsCancelButton:YES animated:YES]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)willDismissSearchController:(UISearchController *)searchController { | ||||
|     // Manually hide cancel button for < iOS 13 | ||||
|     if (!@available(iOS 13, *) && self.automaticallyShowsSearchBarCancelButton) { | ||||
|         [searchController.searchBar setShowsCancelButton:NO animated:YES]; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark UISearchBarDelegate | ||||
|  | ||||
| /// Not necessary in iOS 13; remove this when iOS 13 is the deployment target | ||||
| - (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope { | ||||
|     [self updateSearchResultsForSearchController:self.searchController]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark Table View | ||||
|  | ||||
| /// Not having a title in the first section looks weird with a rounded-corner table view style | ||||
| - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { | ||||
|     if (@available(iOS 13, *)) { | ||||
|         if (self.style == UITableViewStyleInsetGrouped) { | ||||
|             return @" "; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return nil; // For plain/gropued style | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										28
									
								
								Tweaks/FLEX/Core/FLEXSingleRowSection.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								Tweaks/FLEX/Core/FLEXSingleRowSection.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| // | ||||
| //  FLEXSingleRowSection.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 9/25/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableViewSection.h" | ||||
|  | ||||
| /// A section providing a specific single row. | ||||
| /// | ||||
| /// You may optionally provide a view controller to push when the row | ||||
| /// is selected, or an action to perform when it is selected. | ||||
| /// Which one is used first is up to the table view data source. | ||||
| @interface FLEXSingleRowSection : FLEXTableViewSection | ||||
|  | ||||
| /// @param reuseIdentifier if nil, kFLEXDefaultCell is used. | ||||
| + (instancetype)title:(NSString *)sectionTitle | ||||
|                 reuse:(NSString *)reuseIdentifier | ||||
|                  cell:(void(^)(__kindof UITableViewCell *cell))cellConfiguration; | ||||
|  | ||||
| @property (nonatomic) UIViewController *pushOnSelection; | ||||
| @property (nonatomic) void (^selectionAction)(UIViewController *host); | ||||
| /// Called to determine whether the single row should display itself or not. | ||||
| @property (nonatomic) BOOL (^filterMatcher)(NSString *filterText); | ||||
|  | ||||
| @end | ||||
							
								
								
									
										87
									
								
								Tweaks/FLEX/Core/FLEXSingleRowSection.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								Tweaks/FLEX/Core/FLEXSingleRowSection.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| // | ||||
| //  FLEXSingleRowSection.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 9/25/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXSingleRowSection.h" | ||||
| #import "FLEXTableView.h" | ||||
|  | ||||
| @interface FLEXSingleRowSection () | ||||
| @property (nonatomic, readonly) NSString *reuseIdentifier; | ||||
| @property (nonatomic, readonly) void (^cellConfiguration)(__kindof UITableViewCell *cell); | ||||
|  | ||||
| @property (nonatomic) NSString *lastTitle; | ||||
| @property (nonatomic) NSString *lastSubitle; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXSingleRowSection | ||||
|  | ||||
| #pragma mark - Public | ||||
|  | ||||
| + (instancetype)title:(NSString *)title | ||||
|                 reuse:(NSString *)reuse | ||||
|                  cell:(void (^)(__kindof UITableViewCell *))config { | ||||
|     return [[self alloc] initWithTitle:title reuse:reuse cell:config]; | ||||
| } | ||||
|  | ||||
| - (id)initWithTitle:(NSString *)sectionTitle | ||||
|               reuse:(NSString *)reuseIdentifier | ||||
|                cell:(void (^)(__kindof UITableViewCell *))cellConfiguration { | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         _title = sectionTitle; | ||||
|         _reuseIdentifier = reuseIdentifier ?: kFLEXDefaultCell; | ||||
|         _cellConfiguration = cellConfiguration; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| #pragma mark - Overrides | ||||
|  | ||||
| - (NSInteger)numberOfRows { | ||||
|     if (self.filterMatcher && self.filterText.length) { | ||||
|         return self.filterMatcher(self.filterText) ? 1 : 0; | ||||
|     } | ||||
|      | ||||
|     return 1; | ||||
| } | ||||
|  | ||||
| - (BOOL)canSelectRow:(NSInteger)row { | ||||
|     return self.pushOnSelection || self.selectionAction; | ||||
| } | ||||
|  | ||||
| - (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row { | ||||
|     return self.selectionAction; | ||||
| } | ||||
|  | ||||
| - (UIViewController *)viewControllerToPushForRow:(NSInteger)row { | ||||
|     return self.pushOnSelection; | ||||
| } | ||||
|  | ||||
| - (NSString *)reuseIdentifierForRow:(NSInteger)row { | ||||
|     return self.reuseIdentifier; | ||||
| } | ||||
|  | ||||
| - (void)configureCell:(__kindof UITableViewCell *)cell forRow:(NSInteger)row { | ||||
|     cell.textLabel.text = nil; | ||||
|     cell.detailTextLabel.text = nil; | ||||
|     cell.accessoryType = UITableViewCellAccessoryNone; | ||||
|      | ||||
|     self.cellConfiguration(cell); | ||||
|     self.lastTitle = cell.textLabel.text; | ||||
|     self.lastSubitle = cell.detailTextLabel.text; | ||||
| } | ||||
|  | ||||
| - (NSString *)titleForRow:(NSInteger)row { | ||||
|     return self.lastTitle; | ||||
| } | ||||
|  | ||||
| - (NSString *)subtitleForRow:(NSInteger)row { | ||||
|     return self.lastSubitle; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										146
									
								
								Tweaks/FLEX/Core/FLEXTableViewSection.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								Tweaks/FLEX/Core/FLEXTableViewSection.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,146 @@ | ||||
| // | ||||
| //  FLEXTableViewSection.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 1/29/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
| #import "NSArray+FLEX.h" | ||||
| @class FLEXTableView; | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| #pragma mark FLEXTableViewSection | ||||
|  | ||||
| /// An abstract base class for table view sections. | ||||
| /// | ||||
| /// Many properties or methods here return nil or some logical equivalent by default. | ||||
| /// Even so, most of the methods with defaults are intended to be overriden by subclasses. | ||||
| /// Some methods are not implemented at all and MUST be implemented by a subclass. | ||||
| @interface FLEXTableViewSection : NSObject { | ||||
|     @protected | ||||
|     /// Unused by default, use if you want | ||||
|     NSString *_title; | ||||
|      | ||||
|     @private | ||||
|     __weak UITableView *_tableView; | ||||
|     NSInteger _sectionIndex; | ||||
| } | ||||
|  | ||||
| #pragma mark - Data | ||||
|  | ||||
| /// A title to be displayed for the custom section. | ||||
| /// Subclasses may override or use the \c _title ivar. | ||||
| @property (nonatomic, readonly, nullable, copy) NSString *title; | ||||
| /// The number of rows in this section. Subclasses must override. | ||||
| /// This should not change until \c filterText is changed or \c reloadData is called. | ||||
| @property (nonatomic, readonly) NSInteger numberOfRows; | ||||
| /// A map of reuse identifiers to \c UITableViewCell (sub)class objects. | ||||
| /// Subclasses \e may override this as necessary, but are not required to. | ||||
| /// See \c FLEXTableView.h for more information. | ||||
| /// @return nil by default. | ||||
| @property (nonatomic, readonly, nullable) NSDictionary<NSString *, Class> *cellRegistrationMapping; | ||||
|  | ||||
| /// The section should filter itself based on the contents of this property | ||||
| /// as it is set. If it is set to nil or an empty string, it should not filter. | ||||
| /// Subclasses should override or observe this property and react to changes. | ||||
| /// | ||||
| /// It is common practice to use two arrays for the underlying model: | ||||
| /// One to hold all rows, and one to hold unfiltered rows. When \c setFilterText: | ||||
| /// is called, call \c super to store the new value, and re-filter your model accordingly. | ||||
| @property (nonatomic, nullable) NSString *filterText; | ||||
|  | ||||
| /// Provides an avenue for the section to refresh data or change the number of rows. | ||||
| /// | ||||
| /// This is called before reloading the table view itself. If your section pulls data | ||||
| /// from an external data source, this is a good place to refresh that data entirely. | ||||
| /// If your section does not, then it might be simpler for you to just override | ||||
| /// \c setFilterText: to call \c super and call \c reloadData. | ||||
| - (void)reloadData; | ||||
|  | ||||
| /// Like \c reloadData, but optionally reloads the table view section | ||||
| /// associated with this section object, if any. Do not override. | ||||
| /// Do not call outside of the main thread. | ||||
| - (void)reloadData:(BOOL)updateTable; | ||||
|  | ||||
| /// Provide a table view and section index to allow the section to efficiently reload | ||||
| /// its own section of the table when something changes it. The table reference is | ||||
| /// held weakly, and subclasses cannot access it or the index. Call this method again | ||||
| /// if the section numbers have changed since you last called it. | ||||
| - (void)setTable:(UITableView *)tableView section:(NSInteger)index; | ||||
|  | ||||
| #pragma mark - Row Selection | ||||
|  | ||||
| /// Whether the given row should be selectable, such as if tapping the cell | ||||
| /// should take the user to a new screen or trigger an action. | ||||
| /// Subclasses \e may override this as necessary, but are not required to. | ||||
| /// @return \c NO by default | ||||
| - (BOOL)canSelectRow:(NSInteger)row; | ||||
|  | ||||
| /// An action "future" to be triggered when the row is selected, if the row | ||||
| /// supports being selected as indicated by \c canSelectRow:. Subclasses | ||||
| /// must implement this in accordance with how they implement \c canSelectRow: | ||||
| /// if they do not implement \c viewControllerToPushForRow: | ||||
| /// @return This returns \c nil if no view controller is provided by | ||||
| /// \c viewControllerToPushForRow: — otherwise it pushes that view controller | ||||
| /// onto \c host.navigationController | ||||
| - (nullable void(^)(__kindof UIViewController *host))didSelectRowAction:(NSInteger)row; | ||||
|  | ||||
| /// A view controller to display when the row is selected, if the row | ||||
| /// supports being selected as indicated by \c canSelectRow:. Subclasses | ||||
| /// must implement this in accordance with how they implement \c canSelectRow: | ||||
| /// if they do not implement \c didSelectRowAction: | ||||
| /// @return \c nil by default | ||||
| - (nullable UIViewController *)viewControllerToPushForRow:(NSInteger)row; | ||||
|  | ||||
| /// Called when the accessory view's detail button is pressed. | ||||
| /// @return \c nil by default. | ||||
| - (nullable void(^)(__kindof UIViewController *host))didPressInfoButtonAction:(NSInteger)row; | ||||
|  | ||||
| #pragma mark - Context Menus | ||||
|  | ||||
| /// By default, this is the title of the row. | ||||
| /// @return The title of the context menu, if any. | ||||
| - (nullable NSString *)menuTitleForRow:(NSInteger)row API_AVAILABLE(ios(13.0)); | ||||
| /// Protected, not intended for public use. \c menuTitleForRow: | ||||
| /// already includes the value returned from this method. | ||||
| ///  | ||||
| /// By default, this returns \c @"". Subclasses may override to | ||||
| /// provide a detailed description of the target of the context menu. | ||||
| - (NSString *)menuSubtitleForRow:(NSInteger)row API_AVAILABLE(ios(13.0)); | ||||
| /// The context menu items, if any. Subclasses may override. | ||||
| /// By default, only inludes items for \c copyMenuItemsForRow:. | ||||
| - (nullable NSArray<UIMenuElement *> *)menuItemsForRow:(NSInteger)row sender:(UIViewController *)sender API_AVAILABLE(ios(13.0)); | ||||
| /// Subclasses may override to return a list of copiable items. | ||||
| /// | ||||
| /// Every two elements in the list compose a key-value pair, where the key | ||||
| /// should be a description of what will be copied, and the values should be | ||||
| /// the strings to copy. Return an empty string as a value to show a disabled action. | ||||
| - (nullable NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row API_AVAILABLE(ios(13.0)); | ||||
|  | ||||
| #pragma mark - Cell Configuration | ||||
|  | ||||
| /// Provide a reuse identifier for the given row. Subclasses should override. | ||||
| /// | ||||
| /// Custom reuse identifiers should be specified in \c cellRegistrationMapping. | ||||
| /// You may return any of the identifiers in \c FLEXTableView.h | ||||
| /// without including them in the \c cellRegistrationMapping. | ||||
| /// @return \c kFLEXDefaultCell by default. | ||||
| - (NSString *)reuseIdentifierForRow:(NSInteger)row; | ||||
| /// Configure a cell for the given row. Subclasses must override. | ||||
| - (void)configureCell:(__kindof UITableViewCell *)cell forRow:(NSInteger)row; | ||||
|  | ||||
| #pragma mark - External Convenience | ||||
|  | ||||
| /// For use by whatever view controller uses your section. Not required. | ||||
| /// @return An optional title. | ||||
| - (nullable NSString *)titleForRow:(NSInteger)row; | ||||
| /// For use by whatever view controller uses your section. Not required. | ||||
| /// @return An optional subtitle. | ||||
| - (nullable NSString *)subtitleForRow:(NSInteger)row; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
							
								
								
									
										137
									
								
								Tweaks/FLEX/Core/FLEXTableViewSection.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								Tweaks/FLEX/Core/FLEXTableViewSection.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | ||||
| // | ||||
| //  FLEXTableViewSection.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 1/29/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableViewSection.h" | ||||
| #import "FLEXTableView.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "UIMenu+FLEX.h" | ||||
|  | ||||
| #pragma clang diagnostic push | ||||
| #pragma clang diagnostic ignored "-Wincomplete-implementation" | ||||
|  | ||||
| @implementation FLEXTableViewSection | ||||
|  | ||||
| - (NSInteger)numberOfRows { | ||||
|     return 0; | ||||
| } | ||||
|  | ||||
| - (void)reloadData { } | ||||
|  | ||||
| - (void)reloadData:(BOOL)updateTable { | ||||
|     [self reloadData]; | ||||
|     if (updateTable) { | ||||
|         NSIndexSet *index = [NSIndexSet indexSetWithIndex:_sectionIndex]; | ||||
|         [_tableView reloadSections:index withRowAnimation:UITableViewRowAnimationNone]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)setTable:(UITableView *)tableView section:(NSInteger)index { | ||||
|     _tableView = tableView; | ||||
|     _sectionIndex = index; | ||||
| } | ||||
|  | ||||
| - (NSDictionary<NSString *,Class> *)cellRegistrationMapping { | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| - (BOOL)canSelectRow:(NSInteger)row { return NO; } | ||||
|  | ||||
| - (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row { | ||||
|     UIViewController *toPush = [self viewControllerToPushForRow:row]; | ||||
|     if (toPush) { | ||||
|         return ^(UIViewController *host) { | ||||
|             [host.navigationController pushViewController:toPush animated:YES]; | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| - (UIViewController *)viewControllerToPushForRow:(NSInteger)row { | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| - (void (^)(__kindof UIViewController *))didPressInfoButtonAction:(NSInteger)row { | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| - (NSString *)reuseIdentifierForRow:(NSInteger)row { | ||||
|     return kFLEXDefaultCell; | ||||
| } | ||||
|  | ||||
| - (NSString *)menuTitleForRow:(NSInteger)row { | ||||
|     NSString *title = [self titleForRow:row]; | ||||
|     NSString *subtitle = [self menuSubtitleForRow:row]; | ||||
|      | ||||
|     if (subtitle.length) { | ||||
|         return [NSString stringWithFormat:@"%@\n\n%@", title, subtitle]; | ||||
|     } | ||||
|      | ||||
|     return title; | ||||
| } | ||||
|  | ||||
| - (NSString *)menuSubtitleForRow:(NSInteger)row { | ||||
|     return @""; | ||||
| } | ||||
|  | ||||
| - (NSArray<UIMenuElement *> *)menuItemsForRow:(NSInteger)row sender:(UIViewController *)sender API_AVAILABLE(ios(13)) { | ||||
|     NSArray<NSString *> *copyItems = [self copyMenuItemsForRow:row]; | ||||
|     NSAssert(copyItems.count % 2 == 0, @"copyMenuItemsForRow: should return an even list"); | ||||
|      | ||||
|     if (copyItems.count) { | ||||
|         NSInteger numberOfActions = copyItems.count / 2; | ||||
|         BOOL collapseMenu = numberOfActions > 4; | ||||
|         UIImage *copyIcon = [UIImage systemImageNamed:@"doc.on.doc"]; | ||||
|          | ||||
|         NSMutableArray *actions = [NSMutableArray new]; | ||||
|          | ||||
|         for (NSInteger i = 0; i < copyItems.count; i += 2) { | ||||
|             NSString *key = copyItems[i], *value = copyItems[i+1]; | ||||
|             NSString *title = collapseMenu ? key : [@"Copy " stringByAppendingString:key]; | ||||
|              | ||||
|             UIAction *copy = [UIAction | ||||
|                 actionWithTitle:title | ||||
|                 image:copyIcon | ||||
|                 identifier:nil | ||||
|                 handler:^(__kindof UIAction *action) { | ||||
|                     UIPasteboard.generalPasteboard.string = value; | ||||
|                 } | ||||
|             ]; | ||||
|             if (!value.length) { | ||||
|                 copy.attributes = UIMenuElementAttributesDisabled; | ||||
|             } | ||||
|              | ||||
|             [actions addObject:copy]; | ||||
|         } | ||||
|          | ||||
|         UIMenu *copyMenu = [UIMenu | ||||
|             flex_inlineMenuWithTitle:@"Copy…"  | ||||
|             image:copyIcon | ||||
|             children:actions | ||||
|         ]; | ||||
|          | ||||
|         if (collapseMenu) { | ||||
|             return @[[copyMenu flex_collapsed]]; | ||||
|         } else { | ||||
|             return @[copyMenu]; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return @[]; | ||||
| } | ||||
|  | ||||
| - (NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row { | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| - (NSString *)titleForRow:(NSInteger)row { return nil; } | ||||
| - (NSString *)subtitleForRow:(NSInteger)row { return nil; } | ||||
|  | ||||
| @end | ||||
|  | ||||
| #pragma clang diagnostic pop | ||||
							
								
								
									
										15
									
								
								Tweaks/FLEX/Core/Views/Carousel/FLEXCarouselCell.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								Tweaks/FLEX/Core/Views/Carousel/FLEXCarouselCell.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| // | ||||
| //  FLEXCarouselCell.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 7/17/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| @interface FLEXCarouselCell : UICollectionViewCell | ||||
|  | ||||
| @property (nonatomic, copy) NSString *title; | ||||
|  | ||||
| @end | ||||
							
								
								
									
										93
									
								
								Tweaks/FLEX/Core/Views/Carousel/FLEXCarouselCell.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								Tweaks/FLEX/Core/Views/Carousel/FLEXCarouselCell.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | ||||
| // | ||||
| //  FLEXCarouselCell.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 7/17/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXCarouselCell.h" | ||||
| #import "FLEXColor.h" | ||||
| #import "UIView+FLEX_Layout.h" | ||||
|  | ||||
| @interface FLEXCarouselCell () | ||||
| @property (nonatomic, readonly) UILabel *titleLabel; | ||||
| @property (nonatomic, readonly) UIView *selectionIndicatorStripe; | ||||
| @property (nonatomic) BOOL constraintsInstalled; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXCarouselCell | ||||
|  | ||||
| - (instancetype)initWithFrame:(CGRect)frame { | ||||
|     self = [super initWithFrame:frame]; | ||||
|     if (self) { | ||||
|         _titleLabel = [UILabel new]; | ||||
|         _selectionIndicatorStripe = [UIView new]; | ||||
|  | ||||
|         self.titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; | ||||
|         self.selectionIndicatorStripe.backgroundColor = self.tintColor; | ||||
|         if (@available(iOS 10, *)) { | ||||
|             self.titleLabel.adjustsFontForContentSizeCategory = YES; | ||||
|         } | ||||
|  | ||||
|         [self.contentView addSubview:self.titleLabel]; | ||||
|         [self.contentView addSubview:self.selectionIndicatorStripe]; | ||||
|  | ||||
|         [self installConstraints]; | ||||
|  | ||||
|         [self updateAppearance]; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)updateAppearance { | ||||
|     self.selectionIndicatorStripe.hidden = !self.selected; | ||||
|  | ||||
|     if (self.selected) { | ||||
|         self.titleLabel.textColor = self.tintColor; | ||||
|     } else { | ||||
|         self.titleLabel.textColor = FLEXColor.deemphasizedTextColor; | ||||
|     } | ||||
| } | ||||
|  | ||||
| #pragma mark Public | ||||
|  | ||||
| - (NSString *)title { | ||||
|     return self.titleLabel.text; | ||||
| } | ||||
|  | ||||
| - (void)setTitle:(NSString *)title { | ||||
|     self.titleLabel.text = title; | ||||
|     [self.titleLabel sizeToFit]; | ||||
|     [self setNeedsLayout]; | ||||
| } | ||||
|  | ||||
| #pragma mark Overrides | ||||
|  | ||||
| - (void)prepareForReuse { | ||||
|     [super prepareForReuse]; | ||||
|     [self updateAppearance]; | ||||
| } | ||||
|  | ||||
| - (void)installConstraints { | ||||
|     CGFloat stripeHeight = 2; | ||||
|  | ||||
|     self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO; | ||||
|     self.selectionIndicatorStripe.translatesAutoresizingMaskIntoConstraints = NO; | ||||
|  | ||||
|     UIView *superview = self.contentView; | ||||
|     [self.titleLabel flex_pinEdgesToSuperviewWithInsets:UIEdgeInsetsMake(10, 15, 8 + stripeHeight, 15)]; | ||||
|  | ||||
|     [self.selectionIndicatorStripe.leadingAnchor constraintEqualToAnchor:superview.leadingAnchor].active = YES; | ||||
|     [self.selectionIndicatorStripe.bottomAnchor constraintEqualToAnchor:superview.bottomAnchor].active = YES; | ||||
|     [self.selectionIndicatorStripe.trailingAnchor constraintEqualToAnchor:superview.trailingAnchor].active = YES; | ||||
|     [self.selectionIndicatorStripe.heightAnchor constraintEqualToConstant:stripeHeight].active = YES; | ||||
| } | ||||
|  | ||||
| - (void)setSelected:(BOOL)selected { | ||||
|     super.selected = selected; | ||||
|     [self updateAppearance]; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										20
									
								
								Tweaks/FLEX/Core/Views/Carousel/FLEXScopeCarousel.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								Tweaks/FLEX/Core/Views/Carousel/FLEXScopeCarousel.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| // | ||||
| //  FLEXScopeCarousel.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 7/17/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| /// Only use on iOS 10 and up. Requires iOS 10 APIs for calculating row sizes. | ||||
| @interface FLEXScopeCarousel : UIControl | ||||
|  | ||||
| @property (nonatomic, copy) NSArray<NSString *> *items; | ||||
| @property (nonatomic) NSInteger selectedIndex; | ||||
| @property (nonatomic) void(^selectedIndexChangedAction)(NSInteger idx); | ||||
|  | ||||
| - (void)registerBlockForDynamicTypeChanges:(void(^)(FLEXScopeCarousel *))handler; | ||||
|  | ||||
| @end | ||||
							
								
								
									
										204
									
								
								Tweaks/FLEX/Core/Views/Carousel/FLEXScopeCarousel.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								Tweaks/FLEX/Core/Views/Carousel/FLEXScopeCarousel.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,204 @@ | ||||
| // | ||||
| //  FLEXScopeCarousel.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 7/17/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXScopeCarousel.h" | ||||
| #import "FLEXCarouselCell.h" | ||||
| #import "FLEXColor.h" | ||||
| #import "FLEXMacros.h" | ||||
| #import "UIView+FLEX_Layout.h" | ||||
|  | ||||
| const CGFloat kCarouselItemSpacing = 0; | ||||
| NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier"; | ||||
|  | ||||
| @interface FLEXScopeCarousel () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout> | ||||
| @property (nonatomic, readonly) UICollectionView *collectionView; | ||||
| @property (nonatomic, readonly) FLEXCarouselCell *sizingCell; | ||||
|  | ||||
| @property (nonatomic, readonly) id dynamicTypeObserver; | ||||
| @property (nonatomic, readonly) NSMutableArray *dynamicTypeHandlers; | ||||
|  | ||||
| @property (nonatomic) BOOL constraintsInstalled; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXScopeCarousel | ||||
|  | ||||
| - (id)initWithFrame:(CGRect)frame { | ||||
|     self = [super initWithFrame:frame]; | ||||
|     if (self) { | ||||
|         self.backgroundColor = FLEXColor.primaryBackgroundColor; | ||||
|         self.autoresizingMask = UIViewAutoresizingFlexibleWidth; | ||||
|         self.translatesAutoresizingMaskIntoConstraints = YES; | ||||
|         _dynamicTypeHandlers = [NSMutableArray new]; | ||||
|          | ||||
|         CGSize itemSize = CGSizeZero; | ||||
|         if (@available(iOS 10.0, *)) { | ||||
|             itemSize = UICollectionViewFlowLayoutAutomaticSize; | ||||
|         } | ||||
|  | ||||
|         // Collection view layout | ||||
|         UICollectionViewFlowLayout *layout = ({ | ||||
|             UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new]; | ||||
|             layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; | ||||
|             layout.sectionInset = UIEdgeInsetsZero; | ||||
|             layout.minimumLineSpacing = kCarouselItemSpacing; | ||||
|             layout.itemSize = itemSize; | ||||
|             layout.estimatedItemSize = itemSize; | ||||
|             layout; | ||||
|         }); | ||||
|  | ||||
|         // Collection view | ||||
|         _collectionView = ({ | ||||
|             UICollectionView *cv = [[UICollectionView alloc] | ||||
|                 initWithFrame:CGRectZero | ||||
|                 collectionViewLayout:layout | ||||
|             ]; | ||||
|             cv.showsHorizontalScrollIndicator = NO; | ||||
|             cv.backgroundColor = UIColor.clearColor; | ||||
|             cv.delegate = self; | ||||
|             cv.dataSource = self; | ||||
|             [cv registerClass:[FLEXCarouselCell class] forCellWithReuseIdentifier:kCarouselCellReuseIdentifier]; | ||||
|  | ||||
|             [self addSubview:cv]; | ||||
|             cv; | ||||
|         }); | ||||
|  | ||||
|  | ||||
|         // Sizing cell | ||||
|         _sizingCell = [FLEXCarouselCell new]; | ||||
|         self.sizingCell.title = @"NSObject"; | ||||
|  | ||||
|         // Dynamic type | ||||
|         weakify(self); | ||||
|         _dynamicTypeObserver = [NSNotificationCenter.defaultCenter | ||||
|             addObserverForName:UIContentSizeCategoryDidChangeNotification | ||||
|             object:nil queue:nil usingBlock:^(NSNotification *note) { strongify(self) | ||||
|                 [self.collectionView setNeedsLayout]; | ||||
|                 [self setNeedsUpdateConstraints]; | ||||
|  | ||||
|                 // Notify observers | ||||
|                 for (void (^block)(FLEXScopeCarousel *) in self.dynamicTypeHandlers) { | ||||
|                     block(self); | ||||
|                 } | ||||
|             } | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)dealloc { | ||||
|     [NSNotificationCenter.defaultCenter removeObserver:self.dynamicTypeObserver]; | ||||
| } | ||||
|  | ||||
| #pragma mark - Overrides | ||||
|  | ||||
| - (void)drawRect:(CGRect)rect { | ||||
|     [super drawRect:rect]; | ||||
|  | ||||
|     CGFloat width = 1.f / UIScreen.mainScreen.scale; | ||||
|  | ||||
|     // Draw hairline | ||||
|     CGContextRef context = UIGraphicsGetCurrentContext(); | ||||
|     CGContextSetStrokeColorWithColor(context, FLEXColor.hairlineColor.CGColor); | ||||
|     CGContextSetLineWidth(context, width); | ||||
|     CGContextMoveToPoint(context, 0, rect.size.height - width); | ||||
|     CGContextAddLineToPoint(context, rect.size.width, rect.size.height - width); | ||||
|     CGContextStrokePath(context); | ||||
| } | ||||
|  | ||||
| + (BOOL)requiresConstraintBasedLayout { | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (void)updateConstraints { | ||||
|     if (!self.constraintsInstalled) { | ||||
|         self.collectionView.translatesAutoresizingMaskIntoConstraints = NO; | ||||
|         [self.collectionView flex_pinEdgesToSuperview]; | ||||
|          | ||||
|         self.constraintsInstalled = YES; | ||||
|     } | ||||
|      | ||||
|     [super updateConstraints]; | ||||
| } | ||||
|  | ||||
| - (CGSize)intrinsicContentSize { | ||||
|     return CGSizeMake( | ||||
|         UIViewNoIntrinsicMetric, | ||||
|         [self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height | ||||
|     ); | ||||
| } | ||||
|  | ||||
| #pragma mark - Public | ||||
|  | ||||
| - (void)setItems:(NSArray<NSString *> *)items { | ||||
|     NSParameterAssert(items.count); | ||||
|  | ||||
|     _items = items.copy; | ||||
|  | ||||
|     // Refresh list, select first item initially | ||||
|     [self.collectionView reloadData]; | ||||
|     self.selectedIndex = 0; | ||||
| } | ||||
|  | ||||
| - (void)setSelectedIndex:(NSInteger)idx { | ||||
|     NSParameterAssert(idx < self.items.count); | ||||
|  | ||||
|     _selectedIndex = idx; | ||||
|     NSIndexPath *path = [NSIndexPath indexPathForItem:idx inSection:0]; | ||||
|     [self.collectionView selectItemAtIndexPath:path | ||||
|                                       animated:YES | ||||
|                                 scrollPosition:UICollectionViewScrollPositionCenteredHorizontally]; | ||||
|     [self collectionView:self.collectionView didSelectItemAtIndexPath:path]; | ||||
| } | ||||
|  | ||||
| - (void)registerBlockForDynamicTypeChanges:(void (^)(FLEXScopeCarousel *))handler { | ||||
|     [self.dynamicTypeHandlers addObject:handler]; | ||||
| } | ||||
|  | ||||
| #pragma mark - UICollectionView | ||||
|  | ||||
| - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { | ||||
| //    if (@available(iOS 10.0, *)) { | ||||
| //        return UICollectionViewFlowLayoutAutomaticSize; | ||||
| //    } | ||||
|      | ||||
|     self.sizingCell.title = self.items[indexPath.item]; | ||||
|     return [self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; | ||||
| } | ||||
|  | ||||
| - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { | ||||
|     return self.items.count; | ||||
| } | ||||
|  | ||||
| - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView | ||||
|                   cellForItemAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     FLEXCarouselCell *cell = (id)[collectionView dequeueReusableCellWithReuseIdentifier:kCarouselCellReuseIdentifier | ||||
|                                                                            forIndexPath:indexPath]; | ||||
|     cell.title = self.items[indexPath.row]; | ||||
|     return cell; | ||||
| } | ||||
|  | ||||
| - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     _selectedIndex = indexPath.item; // In case self.selectedIndex didn't trigger this call | ||||
|  | ||||
|     if (self.selectedIndexChangedAction) { | ||||
|         self.selectedIndexChangedAction(indexPath.row); | ||||
|     } | ||||
|  | ||||
|     // TODO: dynamically choose a scroll position. Very wide items should | ||||
|     // get "Left" while smaller items should not scroll at all, unless | ||||
|     // they are only partially on the screen, in which case they | ||||
|     // should get "HorizontallyCentered" to bring them onto the screen. | ||||
|     // For now, everything goes to the left, as this has a similar effect. | ||||
|     [collectionView scrollToItemAtIndexPath:indexPath | ||||
|                            atScrollPosition:UICollectionViewScrollPositionLeft | ||||
|                                    animated:YES]; | ||||
|     [self sendActionsForControlEvents:UIControlEventValueChanged]; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										17
									
								
								Tweaks/FLEX/Core/Views/Cells/FLEXCodeFontCell.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								Tweaks/FLEX/Core/Views/Cells/FLEXCodeFontCell.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| // | ||||
| //  FLEXCodeFontCell.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 12/27/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXMultilineTableViewCell.h" | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FLEXCodeFontCell : FLEXMultilineDetailTableViewCell | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
							
								
								
									
										34
									
								
								Tweaks/FLEX/Core/Views/Cells/FLEXCodeFontCell.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								Tweaks/FLEX/Core/Views/Cells/FLEXCodeFontCell.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| // | ||||
| //  FLEXCodeFontCell.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 12/27/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXCodeFontCell.h" | ||||
| #import "UIFont+FLEX.h" | ||||
|  | ||||
| @implementation FLEXCodeFontCell | ||||
|  | ||||
| - (void)postInit { | ||||
|     [super postInit]; | ||||
|      | ||||
|     self.titleLabel.font = UIFont.flex_codeFont; | ||||
|     self.subtitleLabel.font = UIFont.flex_codeFont; | ||||
|  | ||||
|     self.titleLabel.adjustsFontSizeToFitWidth = YES; | ||||
|     self.titleLabel.minimumScaleFactor = 0.9; | ||||
|     self.subtitleLabel.adjustsFontSizeToFitWidth = YES; | ||||
|     self.subtitleLabel.minimumScaleFactor = 0.75; | ||||
|      | ||||
|     // Disable mutli-line pre iOS 11 | ||||
|     if (@available(iOS 11, *)) { | ||||
|         self.subtitleLabel.numberOfLines = 5; | ||||
|     } else { | ||||
|         self.titleLabel.numberOfLines = 1; | ||||
|         self.subtitleLabel.numberOfLines = 1; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										13
									
								
								Tweaks/FLEX/Core/Views/Cells/FLEXKeyValueTableViewCell.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								Tweaks/FLEX/Core/Views/Cells/FLEXKeyValueTableViewCell.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| // | ||||
| //  FLEXKeyValueTableViewCell.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 1/23/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableViewCell.h" | ||||
|  | ||||
| @interface FLEXKeyValueTableViewCell : FLEXTableViewCell | ||||
|  | ||||
| @end | ||||
							
								
								
									
										17
									
								
								Tweaks/FLEX/Core/Views/Cells/FLEXKeyValueTableViewCell.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								Tweaks/FLEX/Core/Views/Cells/FLEXKeyValueTableViewCell.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| // | ||||
| //  FLEXKeyValueTableViewCell.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 1/23/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXKeyValueTableViewCell.h" | ||||
|  | ||||
| @implementation FLEXKeyValueTableViewCell | ||||
|  | ||||
| - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { | ||||
|     return [super initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:reuseIdentifier]; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										24
									
								
								Tweaks/FLEX/Core/Views/Cells/FLEXMultilineTableViewCell.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								Tweaks/FLEX/Core/Views/Cells/FLEXMultilineTableViewCell.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| // | ||||
| //  FLEXMultilineTableViewCell.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Ryan Olson on 2/13/15. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableViewCell.h" | ||||
|  | ||||
| /// A cell with both labels set to be multi-line capable. | ||||
| @interface FLEXMultilineTableViewCell : FLEXTableViewCell | ||||
|  | ||||
| + (CGFloat)preferredHeightWithAttributedText:(NSAttributedString *)attributedText | ||||
|                                     maxWidth:(CGFloat)contentViewWidth | ||||
|                                        style:(UITableViewStyle)style | ||||
|                               showsAccessory:(BOOL)showsAccessory; | ||||
|  | ||||
| @end | ||||
|  | ||||
| /// A \c FLEXMultilineTableViewCell initialized with \c UITableViewCellStyleSubtitle | ||||
| @interface FLEXMultilineDetailTableViewCell : FLEXMultilineTableViewCell | ||||
|  | ||||
| @end | ||||
							
								
								
									
										67
									
								
								Tweaks/FLEX/Core/Views/Cells/FLEXMultilineTableViewCell.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								Tweaks/FLEX/Core/Views/Cells/FLEXMultilineTableViewCell.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| // | ||||
| //  FLEXMultilineTableViewCell.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Ryan Olson on 2/13/15. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXMultilineTableViewCell.h" | ||||
| #import "UIView+FLEX_Layout.h" | ||||
| #import "FLEXUtility.h" | ||||
|  | ||||
| @interface FLEXMultilineTableViewCell () | ||||
| @property (nonatomic, readonly) UILabel *_titleLabel; | ||||
| @property (nonatomic, readonly) UILabel *_subtitleLabel; | ||||
| @property (nonatomic) BOOL constraintsUpdated; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXMultilineTableViewCell | ||||
|  | ||||
| - (void)postInit { | ||||
|     [super postInit]; | ||||
|      | ||||
|     self.titleLabel.numberOfLines = 0; | ||||
|     self.subtitleLabel.numberOfLines = 0; | ||||
| } | ||||
|  | ||||
| + (UIEdgeInsets)labelInsets { | ||||
|     return UIEdgeInsetsMake(10.0, 16.0, 10.0, 8.0); | ||||
| } | ||||
|  | ||||
| + (CGFloat)preferredHeightWithAttributedText:(NSAttributedString *)attributedText | ||||
|                                     maxWidth:(CGFloat)contentViewWidth | ||||
|                                        style:(UITableViewStyle)style | ||||
|                               showsAccessory:(BOOL)showsAccessory { | ||||
|     CGFloat labelWidth = contentViewWidth; | ||||
|  | ||||
|     // Content view inset due to accessory view observed on iOS 8.1 iPhone 6. | ||||
|     if (showsAccessory) { | ||||
|         labelWidth -= 34.0; | ||||
|     } | ||||
|  | ||||
|     UIEdgeInsets labelInsets = [self labelInsets]; | ||||
|     labelWidth -= (labelInsets.left + labelInsets.right); | ||||
|  | ||||
|     CGSize constrainSize = CGSizeMake(labelWidth, CGFLOAT_MAX); | ||||
|     CGRect boundingBox = [attributedText | ||||
|         boundingRectWithSize:constrainSize | ||||
|         options:NSStringDrawingUsesLineFragmentOrigin | ||||
|         context:nil | ||||
|     ]; | ||||
|     CGFloat preferredLabelHeight = FLEXFloor(boundingBox.size.height); | ||||
|     CGFloat preferredCellHeight = preferredLabelHeight + labelInsets.top + labelInsets.bottom + 1.0; | ||||
|  | ||||
|     return preferredCellHeight; | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| @implementation FLEXMultilineDetailTableViewCell | ||||
|  | ||||
| - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { | ||||
|     return [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier]; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										14
									
								
								Tweaks/FLEX/Core/Views/Cells/FLEXSubtitleTableViewCell.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								Tweaks/FLEX/Core/Views/Cells/FLEXSubtitleTableViewCell.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| // | ||||
| //  FLEXSubtitleTableViewCell.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 4/17/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableViewCell.h" | ||||
|  | ||||
| /// A cell initialized with \c UITableViewCellStyleSubtitle | ||||
| @interface FLEXSubtitleTableViewCell : FLEXTableViewCell | ||||
|  | ||||
| @end | ||||
							
								
								
									
										17
									
								
								Tweaks/FLEX/Core/Views/Cells/FLEXSubtitleTableViewCell.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								Tweaks/FLEX/Core/Views/Cells/FLEXSubtitleTableViewCell.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| // | ||||
| //  FLEXSubtitleTableViewCell.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 4/17/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXSubtitleTableViewCell.h" | ||||
|  | ||||
| @implementation FLEXSubtitleTableViewCell | ||||
|  | ||||
| - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { | ||||
|     return [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier]; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										23
									
								
								Tweaks/FLEX/Core/Views/Cells/FLEXTableViewCell.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								Tweaks/FLEX/Core/Views/Cells/FLEXTableViewCell.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| // | ||||
| //  FLEXTableViewCell.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 4/17/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| @interface FLEXTableViewCell : UITableViewCell | ||||
|  | ||||
| /// Use this instead of .textLabel | ||||
| @property (nonatomic, readonly) UILabel *titleLabel; | ||||
| /// Use this instead of .detailTextLabel | ||||
| @property (nonatomic, readonly) UILabel *subtitleLabel; | ||||
|  | ||||
| /// Subclasses can override this instead of initializers to | ||||
| /// perform additional initialization without lots of boilerplate. | ||||
| /// Remember to call super! | ||||
| - (void)postInit; | ||||
|  | ||||
| @end | ||||
							
								
								
									
										57
									
								
								Tweaks/FLEX/Core/Views/Cells/FLEXTableViewCell.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								Tweaks/FLEX/Core/Views/Cells/FLEXTableViewCell.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| // | ||||
| //  FLEXTableViewCell.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 4/17/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableViewCell.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXColor.h" | ||||
| #import "FLEXTableView.h" | ||||
|  | ||||
| @interface UITableView (Internal) | ||||
| // Exists at least since iOS 5 | ||||
| - (BOOL)_canPerformAction:(SEL)action forCell:(UITableViewCell *)cell sender:(id)sender; | ||||
| - (void)_performAction:(SEL)action forCell:(UITableViewCell *)cell sender:(id)sender; | ||||
| @end | ||||
|  | ||||
| @interface UITableViewCell (Internal) | ||||
| // Exists at least since iOS 5 | ||||
| @property (nonatomic, readonly) FLEXTableView *_tableView; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXTableViewCell | ||||
|  | ||||
| - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { | ||||
|     self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; | ||||
|     if (self) { | ||||
|         [self postInit]; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)postInit { | ||||
|     UIFont *cellFont = UIFont.flex_defaultTableCellFont; | ||||
|     self.titleLabel.font = cellFont; | ||||
|     self.subtitleLabel.font = cellFont; | ||||
|     self.subtitleLabel.textColor = FLEXColor.deemphasizedTextColor; | ||||
|      | ||||
|     self.titleLabel.lineBreakMode = NSLineBreakByTruncatingMiddle; | ||||
|     self.subtitleLabel.lineBreakMode = NSLineBreakByTruncatingMiddle; | ||||
|      | ||||
|     self.titleLabel.numberOfLines = 1; | ||||
|     self.subtitleLabel.numberOfLines = 1; | ||||
| } | ||||
|  | ||||
| - (UILabel *)titleLabel { | ||||
|     return self.textLabel; | ||||
| } | ||||
|  | ||||
| - (UILabel *)subtitleLabel { | ||||
|     return self.detailTextLabel; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										48
									
								
								Tweaks/FLEX/Core/Views/FLEXTableView.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								Tweaks/FLEX/Core/Views/FLEXTableView.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| // | ||||
| //  FLEXTableView.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 4/17/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| #pragma mark Reuse identifiers | ||||
|  | ||||
| typedef NSString * FLEXTableViewCellReuseIdentifier; | ||||
|  | ||||
| /// A regular \c FLEXTableViewCell initialized with \c UITableViewCellStyleDefault | ||||
| extern FLEXTableViewCellReuseIdentifier const kFLEXDefaultCell; | ||||
| /// A \c FLEXSubtitleTableViewCell initialized with \c UITableViewCellStyleSubtitle | ||||
| extern FLEXTableViewCellReuseIdentifier const kFLEXDetailCell; | ||||
| /// A \c FLEXMultilineTableViewCell initialized with \c UITableViewCellStyleDefault | ||||
| extern FLEXTableViewCellReuseIdentifier const kFLEXMultilineCell; | ||||
| /// A \c FLEXMultilineTableViewCell initialized with \c UITableViewCellStyleSubtitle | ||||
| extern FLEXTableViewCellReuseIdentifier const kFLEXMultilineDetailCell; | ||||
| /// A \c FLEXTableViewCell initialized with \c UITableViewCellStyleValue1 | ||||
| extern FLEXTableViewCellReuseIdentifier const kFLEXKeyValueCell; | ||||
| /// A \c FLEXSubtitleTableViewCell which uses monospaced fonts for both labels | ||||
| extern FLEXTableViewCellReuseIdentifier const kFLEXCodeFontCell; | ||||
|  | ||||
| #pragma mark - FLEXTableView | ||||
| @interface FLEXTableView : UITableView | ||||
|  | ||||
| + (instancetype)flexDefaultTableView; | ||||
| + (instancetype)groupedTableView; | ||||
| + (instancetype)plainTableView; | ||||
| + (instancetype)style:(UITableViewStyle)style; | ||||
|  | ||||
| /// You do not need to register classes for any of the default reuse identifiers above | ||||
| /// (annotated as \c FLEXTableViewCellReuseIdentifier types) unless you wish to provide | ||||
| /// a custom cell for any of those reuse identifiers. By default, \c FLEXTableViewCell, | ||||
| /// \c FLEXSubtitleTableViewCell, and \c FLEXMultilineTableViewCell are used, respectively. | ||||
| /// | ||||
| /// @param registrationMapping A map of reuse identifiers to \c UITableViewCell (sub)class objects. | ||||
| - (void)registerCells:(NSDictionary<NSString *, Class> *)registrationMapping; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
							
								
								
									
										83
									
								
								Tweaks/FLEX/Core/Views/FLEXTableView.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								Tweaks/FLEX/Core/Views/FLEXTableView.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| // | ||||
| //  FLEXTableView.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 4/17/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableView.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXSubtitleTableViewCell.h" | ||||
| #import "FLEXMultilineTableViewCell.h" | ||||
| #import "FLEXKeyValueTableViewCell.h" | ||||
| #import "FLEXCodeFontCell.h" | ||||
|  | ||||
| FLEXTableViewCellReuseIdentifier const kFLEXDefaultCell = @"kFLEXDefaultCell"; | ||||
| FLEXTableViewCellReuseIdentifier const kFLEXDetailCell = @"kFLEXDetailCell"; | ||||
| FLEXTableViewCellReuseIdentifier const kFLEXMultilineCell = @"kFLEXMultilineCell"; | ||||
| FLEXTableViewCellReuseIdentifier const kFLEXMultilineDetailCell = @"kFLEXMultilineDetailCell"; | ||||
| FLEXTableViewCellReuseIdentifier const kFLEXKeyValueCell = @"kFLEXKeyValueCell"; | ||||
| FLEXTableViewCellReuseIdentifier const kFLEXCodeFontCell = @"kFLEXCodeFontCell"; | ||||
|  | ||||
| #pragma mark Private | ||||
|  | ||||
| @interface UITableView (Private) | ||||
| - (CGFloat)_heightForHeaderInSection:(NSInteger)section; | ||||
| - (NSString *)_titleForHeaderInSection:(NSInteger)section; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXTableView | ||||
|  | ||||
| + (instancetype)flexDefaultTableView { | ||||
|     if (@available(iOS 13.0, *)) { | ||||
|         return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped]; | ||||
|     } else { | ||||
|         return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| + (id)groupedTableView { | ||||
|     if (@available(iOS 13.0, *)) { | ||||
|         return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped]; | ||||
|     } else { | ||||
|         return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| + (id)plainTableView { | ||||
|     return [[self alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; | ||||
| } | ||||
|  | ||||
| + (id)style:(UITableViewStyle)style { | ||||
|     return [[self alloc] initWithFrame:CGRectZero style:style]; | ||||
| } | ||||
|  | ||||
| - (id)initWithFrame:(CGRect)frame style:(UITableViewStyle)style { | ||||
|     self = [super initWithFrame:frame style:style]; | ||||
|     if (self) { | ||||
|         [self registerCells:@{ | ||||
|             kFLEXDefaultCell : [FLEXTableViewCell class], | ||||
|             kFLEXDetailCell : [FLEXSubtitleTableViewCell class], | ||||
|             kFLEXMultilineCell : [FLEXMultilineTableViewCell class], | ||||
|             kFLEXMultilineDetailCell : [FLEXMultilineDetailTableViewCell class], | ||||
|             kFLEXKeyValueCell : [FLEXKeyValueTableViewCell class], | ||||
|             kFLEXCodeFontCell : [FLEXCodeFontCell class], | ||||
|         }]; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Public | ||||
|  | ||||
| - (void)registerCells:(NSDictionary<NSString*, Class> *)registrationMapping { | ||||
|     [registrationMapping enumerateKeysAndObjectsUsingBlock:^(NSString *identifier, Class cellClass, BOOL *stop) { | ||||
|         [self registerClass:cellClass forCellReuseIdentifier:identifier]; | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,13 @@ | ||||
| // | ||||
| //  FLEXArgumentInputColorView.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 6/30/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputView.h" | ||||
|  | ||||
| @interface FLEXArgumentInputColorView : FLEXArgumentInputView | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,311 @@ | ||||
| // | ||||
| //  FLEXArgumentInputColorView.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 6/30/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputColorView.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
|  | ||||
| @protocol FLEXColorComponentInputViewDelegate; | ||||
|  | ||||
| @interface FLEXColorComponentInputView : UIView | ||||
|  | ||||
| @property (nonatomic) UISlider *slider; | ||||
| @property (nonatomic) UILabel *valueLabel; | ||||
|  | ||||
| @property (nonatomic, weak) id <FLEXColorComponentInputViewDelegate> delegate; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @protocol FLEXColorComponentInputViewDelegate <NSObject> | ||||
|  | ||||
| - (void)colorComponentInputViewValueDidChange:(FLEXColorComponentInputView *)colorComponentInputView; | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| @implementation FLEXColorComponentInputView | ||||
|  | ||||
| - (id)initWithFrame:(CGRect)frame { | ||||
|     self = [super initWithFrame:frame]; | ||||
|     if (self) { | ||||
|         self.slider = [UISlider new]; | ||||
|         [self.slider addTarget:self action:@selector(sliderChanged:) forControlEvents:UIControlEventValueChanged]; | ||||
|         [self addSubview:self.slider]; | ||||
|          | ||||
|         self.valueLabel = [UILabel new]; | ||||
|         self.valueLabel.backgroundColor = self.backgroundColor; | ||||
|         self.valueLabel.font = [UIFont systemFontOfSize:14.0]; | ||||
|         self.valueLabel.textAlignment = NSTextAlignmentRight; | ||||
|         [self addSubview:self.valueLabel]; | ||||
|          | ||||
|         [self updateValueLabel]; | ||||
|     } | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)setBackgroundColor:(UIColor *)backgroundColor { | ||||
|     [super setBackgroundColor:backgroundColor]; | ||||
|     self.slider.backgroundColor = backgroundColor; | ||||
|     self.valueLabel.backgroundColor = backgroundColor; | ||||
| } | ||||
|  | ||||
| - (void)layoutSubviews { | ||||
|     [super layoutSubviews]; | ||||
|      | ||||
|     const CGFloat kValueLabelWidth = 50.0; | ||||
|      | ||||
|     [self.slider sizeToFit]; | ||||
|     CGFloat sliderWidth = self.bounds.size.width - kValueLabelWidth; | ||||
|     self.slider.frame = CGRectMake(0, 0, sliderWidth, self.slider.frame.size.height); | ||||
|      | ||||
|     [self.valueLabel sizeToFit]; | ||||
|     CGFloat valueLabelOriginX = CGRectGetMaxX(self.slider.frame); | ||||
|     CGFloat valueLabelOriginY = FLEXFloor((self.slider.frame.size.height - self.valueLabel.frame.size.height) / 2.0); | ||||
|     self.valueLabel.frame = CGRectMake(valueLabelOriginX, valueLabelOriginY, kValueLabelWidth, self.valueLabel.frame.size.height); | ||||
| } | ||||
|  | ||||
| - (void)sliderChanged:(id)sender { | ||||
|     [self.delegate colorComponentInputViewValueDidChange:self]; | ||||
|     [self updateValueLabel]; | ||||
| } | ||||
|  | ||||
| - (void)updateValueLabel { | ||||
|     self.valueLabel.text = [NSString stringWithFormat:@"%.3f", self.slider.value]; | ||||
| } | ||||
|  | ||||
| - (CGSize)sizeThatFits:(CGSize)size { | ||||
|     CGFloat height = [self.slider sizeThatFits:size].height; | ||||
|     return CGSizeMake(size.width, height); | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
| @interface FLEXColorPreviewBox : UIView | ||||
|  | ||||
| @property (nonatomic) UIColor *color; | ||||
|  | ||||
| @property (nonatomic) UIView *colorOverlayView; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FLEXColorPreviewBox | ||||
|  | ||||
| - (id)initWithFrame:(CGRect)frame { | ||||
|     self = [super initWithFrame:frame]; | ||||
|     if (self) { | ||||
|         self.layer.borderWidth = 1.0; | ||||
|         self.layer.borderColor = UIColor.blackColor.CGColor; | ||||
|         self.backgroundColor = [UIColor colorWithPatternImage:[[self class] backgroundPatternImage]]; | ||||
|          | ||||
|         self.colorOverlayView = [[UIView alloc] initWithFrame:self.bounds]; | ||||
|         self.colorOverlayView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; | ||||
|         self.colorOverlayView.backgroundColor = UIColor.clearColor; | ||||
|         [self addSubview:self.colorOverlayView]; | ||||
|     } | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)setColor:(UIColor *)color { | ||||
|     self.colorOverlayView.backgroundColor = color; | ||||
| } | ||||
|  | ||||
| - (UIColor *)color { | ||||
|     return self.colorOverlayView.backgroundColor; | ||||
| } | ||||
|  | ||||
| + (UIImage *)backgroundPatternImage { | ||||
|     const CGFloat kSquareDimension = 5.0; | ||||
|     CGSize squareSize = CGSizeMake(kSquareDimension, kSquareDimension); | ||||
|     CGSize imageSize = CGSizeMake(2.0 * kSquareDimension, 2.0 * kSquareDimension); | ||||
|      | ||||
|     UIGraphicsBeginImageContextWithOptions(imageSize, YES, UIScreen.mainScreen.scale); | ||||
|      | ||||
|     [UIColor.whiteColor setFill]; | ||||
|     UIRectFill(CGRectMake(0, 0, imageSize.width, imageSize.height)); | ||||
|      | ||||
|     [UIColor.grayColor setFill]; | ||||
|     UIRectFill(CGRectMake(squareSize.width, 0, squareSize.width, squareSize.height)); | ||||
|     UIRectFill(CGRectMake(0, squareSize.height, squareSize.width, squareSize.height)); | ||||
|      | ||||
|     UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); | ||||
|     UIGraphicsEndImageContext(); | ||||
|      | ||||
|     return image; | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
| @interface FLEXArgumentInputColorView () <FLEXColorComponentInputViewDelegate> | ||||
|  | ||||
| @property (nonatomic) FLEXColorPreviewBox *colorPreviewBox; | ||||
| @property (nonatomic) UILabel *hexLabel; | ||||
| @property (nonatomic) FLEXColorComponentInputView *alphaInput; | ||||
| @property (nonatomic) FLEXColorComponentInputView *redInput; | ||||
| @property (nonatomic) FLEXColorComponentInputView *greenInput; | ||||
| @property (nonatomic) FLEXColorComponentInputView *blueInput; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FLEXArgumentInputColorView | ||||
|  | ||||
| - (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding { | ||||
|     self = [super initWithArgumentTypeEncoding:typeEncoding]; | ||||
|     if (self) { | ||||
|         self.colorPreviewBox = [FLEXColorPreviewBox new]; | ||||
|         [self addSubview:self.colorPreviewBox]; | ||||
|          | ||||
|         self.hexLabel = [UILabel new]; | ||||
|         self.hexLabel.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.9]; | ||||
|         self.hexLabel.textAlignment = NSTextAlignmentCenter; | ||||
|         self.hexLabel.font = [UIFont systemFontOfSize:12.0]; | ||||
|         [self addSubview:self.hexLabel]; | ||||
|          | ||||
|         self.alphaInput = [FLEXColorComponentInputView new]; | ||||
|         self.alphaInput.slider.minimumTrackTintColor = UIColor.blackColor; | ||||
|         self.alphaInput.delegate = self; | ||||
|         [self addSubview:self.alphaInput]; | ||||
|          | ||||
|         self.redInput = [FLEXColorComponentInputView new]; | ||||
|         self.redInput.slider.minimumTrackTintColor = UIColor.redColor; | ||||
|         self.redInput.delegate = self; | ||||
|         [self addSubview:self.redInput]; | ||||
|          | ||||
|         self.greenInput = [FLEXColorComponentInputView new]; | ||||
|         self.greenInput.slider.minimumTrackTintColor = UIColor.greenColor; | ||||
|         self.greenInput.delegate = self; | ||||
|         [self addSubview:self.greenInput]; | ||||
|          | ||||
|         self.blueInput = [FLEXColorComponentInputView new]; | ||||
|         self.blueInput.slider.minimumTrackTintColor = UIColor.blueColor; | ||||
|         self.blueInput.delegate = self; | ||||
|         [self addSubview:self.blueInput]; | ||||
|     } | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)setBackgroundColor:(UIColor *)backgroundColor { | ||||
|     [super setBackgroundColor:backgroundColor]; | ||||
|     self.alphaInput.backgroundColor = backgroundColor; | ||||
|     self.redInput.backgroundColor = backgroundColor; | ||||
|     self.greenInput.backgroundColor = backgroundColor; | ||||
|     self.blueInput.backgroundColor = backgroundColor; | ||||
| } | ||||
|  | ||||
| - (void)layoutSubviews { | ||||
|     [super layoutSubviews]; | ||||
|      | ||||
|     CGFloat runningOriginY = 0; | ||||
|     CGSize constrainSize = CGSizeMake(self.bounds.size.width, CGFLOAT_MAX); | ||||
|      | ||||
|     self.colorPreviewBox.frame = CGRectMake(0, runningOriginY, self.bounds.size.width, [[self class] colorPreviewBoxHeight]); | ||||
|     runningOriginY = CGRectGetMaxY(self.colorPreviewBox.frame) + [[self class] inputViewVerticalPadding]; | ||||
|      | ||||
|     [self.hexLabel sizeToFit]; | ||||
|     const CGFloat kLabelVerticalOutsetAmount = 0.0; | ||||
|     const CGFloat kLabelHorizontalOutsetAmount = 2.0; | ||||
|     UIEdgeInsets labelOutset = UIEdgeInsetsMake(-kLabelVerticalOutsetAmount, -kLabelHorizontalOutsetAmount, -kLabelVerticalOutsetAmount, -kLabelHorizontalOutsetAmount); | ||||
|     self.hexLabel.frame = UIEdgeInsetsInsetRect(self.hexLabel.frame, labelOutset); | ||||
|     CGFloat hexLabelOriginX = self.colorPreviewBox.layer.borderWidth; | ||||
|     CGFloat hexLabelOriginY = CGRectGetMaxY(self.colorPreviewBox.frame) - self.colorPreviewBox.layer.borderWidth - self.hexLabel.frame.size.height; | ||||
|     self.hexLabel.frame = CGRectMake(hexLabelOriginX, hexLabelOriginY, self.hexLabel.frame.size.width, self.hexLabel.frame.size.height); | ||||
|      | ||||
|     NSArray<FLEXColorComponentInputView *> *colorComponentInputViews = @[self.alphaInput, self.redInput, self.greenInput, self.blueInput]; | ||||
|     for (FLEXColorComponentInputView *inputView in colorComponentInputViews) { | ||||
|         CGSize fitSize = [inputView sizeThatFits:constrainSize]; | ||||
|         inputView.frame = CGRectMake(0, runningOriginY, fitSize.width, fitSize.height); | ||||
|         runningOriginY = CGRectGetMaxY(inputView.frame) + [[self class] inputViewVerticalPadding]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)setInputValue:(id)inputValue { | ||||
|     if ([inputValue isKindOfClass:[UIColor class]]) { | ||||
|         [self updateWithColor:inputValue]; | ||||
|     } else if ([inputValue isKindOfClass:[NSValue class]]) { | ||||
|         const char *type = [inputValue objCType]; | ||||
|         if (strcmp(type, @encode(CGColorRef)) == 0) { | ||||
|             CGColorRef colorRef; | ||||
|             [inputValue getValue:&colorRef]; | ||||
|             UIColor *color = [[UIColor alloc] initWithCGColor:colorRef]; | ||||
|             [self updateWithColor:color]; | ||||
|         } | ||||
|     } else { | ||||
|         [self updateWithColor:UIColor.clearColor]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (id)inputValue { | ||||
|     return [UIColor colorWithRed:self.redInput.slider.value green:self.greenInput.slider.value blue:self.blueInput.slider.value alpha:self.alphaInput.slider.value]; | ||||
| } | ||||
|  | ||||
| - (void)colorComponentInputViewValueDidChange:(FLEXColorComponentInputView *)colorComponentInputView { | ||||
|     [self updateColorPreview]; | ||||
| } | ||||
|  | ||||
| - (void)updateWithColor:(UIColor *)color { | ||||
|     CGFloat red, green, blue, white, alpha; | ||||
|     if ([color getRed:&red green:&green blue:&blue alpha:&alpha]) { | ||||
|         self.alphaInput.slider.value = alpha; | ||||
|         [self.alphaInput updateValueLabel]; | ||||
|         self.redInput.slider.value = red; | ||||
|         [self.redInput updateValueLabel]; | ||||
|         self.greenInput.slider.value = green; | ||||
|         [self.greenInput updateValueLabel]; | ||||
|         self.blueInput.slider.value = blue; | ||||
|         [self.blueInput updateValueLabel]; | ||||
|     } else if ([color getWhite:&white alpha:&alpha]) { | ||||
|         self.alphaInput.slider.value = alpha; | ||||
|         [self.alphaInput updateValueLabel]; | ||||
|         self.redInput.slider.value = white; | ||||
|         [self.redInput updateValueLabel]; | ||||
|         self.greenInput.slider.value = white; | ||||
|         [self.greenInput updateValueLabel]; | ||||
|         self.blueInput.slider.value = white; | ||||
|         [self.blueInput updateValueLabel]; | ||||
|     } | ||||
|     [self updateColorPreview]; | ||||
| } | ||||
|  | ||||
| - (void)updateColorPreview { | ||||
|     self.colorPreviewBox.color = self.inputValue; | ||||
|     unsigned char redByte = self.redInput.slider.value * 255; | ||||
|     unsigned char greenByte = self.greenInput.slider.value * 255; | ||||
|     unsigned char blueByte = self.blueInput.slider.value * 255; | ||||
|     self.hexLabel.text = [NSString stringWithFormat:@"#%02X%02X%02X", redByte, greenByte, blueByte]; | ||||
|     [self setNeedsLayout]; | ||||
| } | ||||
|  | ||||
| - (CGSize)sizeThatFits:(CGSize)size { | ||||
|     CGFloat height = 0; | ||||
|     height += [[self class] colorPreviewBoxHeight]; | ||||
|     height += [[self class] inputViewVerticalPadding]; | ||||
|     height += [self.alphaInput sizeThatFits:size].height; | ||||
|     height += [[self class] inputViewVerticalPadding]; | ||||
|     height += [self.redInput sizeThatFits:size].height; | ||||
|     height += [[self class] inputViewVerticalPadding]; | ||||
|     height += [self.greenInput sizeThatFits:size].height; | ||||
|     height += [[self class] inputViewVerticalPadding]; | ||||
|     height += [self.blueInput sizeThatFits:size].height; | ||||
|     return CGSizeMake(size.width, height); | ||||
| } | ||||
|  | ||||
| + (CGFloat)inputViewVerticalPadding { | ||||
|     return 10.0; | ||||
| } | ||||
|  | ||||
| + (CGFloat)colorPreviewBoxHeight { | ||||
|     return 40.0; | ||||
| } | ||||
|  | ||||
| + (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value { | ||||
|     NSParameterAssert(type); | ||||
|  | ||||
|     // We don't care if currentValue is a color or not; we will default to +clearColor | ||||
|     return (strcmp(type, @encode(CGColorRef)) == 0) || (strcmp(type, FLEXEncodeClass(UIColor)) == 0); | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,13 @@ | ||||
| // | ||||
| //  FLEXArgumentInputDataView.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Daniel Rodriguez Troitino on 2/14/15. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputView.h" | ||||
|  | ||||
| @interface FLEXArgumentInputDateView : FLEXArgumentInputView | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,58 @@ | ||||
| // | ||||
| //  FLEXArgumentInputDataView.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Daniel Rodriguez Troitino on 2/14/15. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputDateView.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
|  | ||||
| @interface FLEXArgumentInputDateView () | ||||
|  | ||||
| @property (nonatomic) UIDatePicker *datePicker; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FLEXArgumentInputDateView | ||||
|  | ||||
| - (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding { | ||||
|     self = [super initWithArgumentTypeEncoding:typeEncoding]; | ||||
|     if (self) { | ||||
|         self.datePicker = [UIDatePicker new]; | ||||
|         self.datePicker.datePickerMode = UIDatePickerModeDateAndTime; | ||||
|         // Using UTC, because that's what the NSDate description prints | ||||
|         self.datePicker.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian]; | ||||
|         self.datePicker.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; | ||||
|         [self addSubview:self.datePicker]; | ||||
|     } | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)setInputValue:(id)inputValue { | ||||
|     if ([inputValue isKindOfClass:[NSDate class]]) { | ||||
|         self.datePicker.date = inputValue; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (id)inputValue { | ||||
|     return self.datePicker.date; | ||||
| } | ||||
|  | ||||
| - (void)layoutSubviews { | ||||
|     [super layoutSubviews]; | ||||
|     self.datePicker.frame = self.bounds; | ||||
| } | ||||
|  | ||||
| - (CGSize)sizeThatFits:(CGSize)size { | ||||
|     CGFloat height = [self.datePicker sizeThatFits:size].height; | ||||
|     return CGSizeMake(size.width, height); | ||||
| } | ||||
|  | ||||
| + (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value { | ||||
|     NSParameterAssert(type); | ||||
|     return strcmp(type, FLEXEncodeClass(NSDate)) == 0; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,13 @@ | ||||
| // | ||||
| //  FLEXArgumentInputFontView.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 6/28/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputView.h" | ||||
|  | ||||
| @interface FLEXArgumentInputFontView : FLEXArgumentInputView | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,109 @@ | ||||
| // | ||||
| //  FLEXArgumentInputFontView.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 6/28/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputFontView.h" | ||||
| #import "FLEXArgumentInputViewFactory.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
| #import "FLEXArgumentInputFontsPickerView.h" | ||||
|  | ||||
| @interface FLEXArgumentInputFontView () | ||||
|  | ||||
| @property (nonatomic) FLEXArgumentInputView *fontNameInput; | ||||
| @property (nonatomic) FLEXArgumentInputView *pointSizeInput; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FLEXArgumentInputFontView | ||||
|  | ||||
| - (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding { | ||||
|     self = [super initWithArgumentTypeEncoding:typeEncoding]; | ||||
|     if (self) { | ||||
|         self.fontNameInput = [[FLEXArgumentInputFontsPickerView alloc] initWithArgumentTypeEncoding:FLEXEncodeClass(NSString)]; | ||||
|         self.fontNameInput.targetSize = FLEXArgumentInputViewSizeSmall; | ||||
|         self.fontNameInput.title = @"Font Name:"; | ||||
|         [self addSubview:self.fontNameInput]; | ||||
|          | ||||
|         self.pointSizeInput = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:@encode(CGFloat)]; | ||||
|         self.pointSizeInput.targetSize = FLEXArgumentInputViewSizeSmall; | ||||
|         self.pointSizeInput.title = @"Point Size:"; | ||||
|         [self addSubview:self.pointSizeInput]; | ||||
|     } | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)setBackgroundColor:(UIColor *)backgroundColor { | ||||
|     [super setBackgroundColor:backgroundColor]; | ||||
|     self.fontNameInput.backgroundColor = backgroundColor; | ||||
|     self.pointSizeInput.backgroundColor = backgroundColor; | ||||
| } | ||||
|  | ||||
| - (void)setInputValue:(id)inputValue { | ||||
|     if ([inputValue isKindOfClass:[UIFont class]]) { | ||||
|         UIFont *font = (UIFont *)inputValue; | ||||
|         self.fontNameInput.inputValue = font.fontName; | ||||
|         self.pointSizeInput.inputValue = @(font.pointSize); | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (id)inputValue { | ||||
|     CGFloat pointSize = 0; | ||||
|     if ([self.pointSizeInput.inputValue isKindOfClass:[NSValue class]]) { | ||||
|         NSValue *pointSizeValue = (NSValue *)self.pointSizeInput.inputValue; | ||||
|         if (strcmp([pointSizeValue objCType], @encode(CGFloat)) == 0) { | ||||
|             [pointSizeValue getValue:&pointSize]; | ||||
|         } | ||||
|     } | ||||
|     return [UIFont fontWithName:self.fontNameInput.inputValue size:pointSize]; | ||||
| } | ||||
|  | ||||
| - (BOOL)inputViewIsFirstResponder { | ||||
|     return [self.fontNameInput inputViewIsFirstResponder] || [self.pointSizeInput inputViewIsFirstResponder]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Layout and Sizing | ||||
|  | ||||
| - (void)layoutSubviews { | ||||
|     [super layoutSubviews]; | ||||
|      | ||||
|     CGFloat runningOriginY = self.topInputFieldVerticalLayoutGuide; | ||||
|      | ||||
|     CGSize fontNameFitSize = [self.fontNameInput sizeThatFits:self.bounds.size]; | ||||
|     self.fontNameInput.frame = CGRectMake(0, runningOriginY, fontNameFitSize.width, fontNameFitSize.height); | ||||
|     runningOriginY = CGRectGetMaxY(self.fontNameInput.frame) + [[self class] verticalPaddingBetweenFields]; | ||||
|      | ||||
|     CGSize pointSizeFitSize = [self.pointSizeInput sizeThatFits:self.bounds.size]; | ||||
|     self.pointSizeInput.frame = CGRectMake(0, runningOriginY, pointSizeFitSize.width, pointSizeFitSize.height); | ||||
| } | ||||
|  | ||||
| + (CGFloat)verticalPaddingBetweenFields { | ||||
|     return 10.0; | ||||
| } | ||||
|  | ||||
| - (CGSize)sizeThatFits:(CGSize)size { | ||||
|     CGSize fitSize = [super sizeThatFits:size]; | ||||
|      | ||||
|     CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX); | ||||
|      | ||||
|     CGFloat height = fitSize.height; | ||||
|     height += [self.fontNameInput sizeThatFits:constrainSize].height; | ||||
|     height += [[self class] verticalPaddingBetweenFields]; | ||||
|     height += [self.pointSizeInput sizeThatFits:constrainSize].height; | ||||
|      | ||||
|     return CGSizeMake(fitSize.width, height); | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - | ||||
|  | ||||
| + (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value { | ||||
|     NSParameterAssert(type); | ||||
|     return strcmp(type, FLEXEncodeClass(UIFont)) == 0; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,12 @@ | ||||
| // | ||||
| //  FLEXArgumentInputFontsPickerView.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by 啟倫 陳 on 2014/7/27. | ||||
| //  Copyright (c) 2014年 f. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputTextView.h" | ||||
|  | ||||
| @interface FLEXArgumentInputFontsPickerView : FLEXArgumentInputTextView <UIPickerViewDataSource, UIPickerViewDelegate> | ||||
| @end | ||||
| @@ -0,0 +1,96 @@ | ||||
| // | ||||
| //  FLEXArgumentInputFontsPickerView.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by 啟倫 陳 on 2014/7/27. | ||||
| //  Copyright (c) 2014年 f. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputFontsPickerView.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
|  | ||||
| @interface FLEXArgumentInputFontsPickerView () | ||||
|  | ||||
| @property (nonatomic) NSMutableArray<NSString *> *availableFonts; | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| @implementation FLEXArgumentInputFontsPickerView | ||||
|  | ||||
| - (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding { | ||||
|     self = [super initWithArgumentTypeEncoding:typeEncoding]; | ||||
|     if (self) { | ||||
|         self.targetSize = FLEXArgumentInputViewSizeSmall; | ||||
|         [self createAvailableFonts]; | ||||
|         self.inputTextView.inputView = [self createFontsPicker]; | ||||
|     } | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)setInputValue:(id)inputValue { | ||||
|     self.inputTextView.text = inputValue; | ||||
|     if ([self.availableFonts indexOfObject:inputValue] == NSNotFound) { | ||||
|         [self.availableFonts insertObject:inputValue atIndex:0]; | ||||
|     } | ||||
|     [(UIPickerView *)self.inputTextView.inputView selectRow:[self.availableFonts indexOfObject:inputValue] inComponent:0 animated:NO]; | ||||
| } | ||||
|  | ||||
| - (id)inputValue { | ||||
|     return self.inputTextView.text.length > 0 ? [self.inputTextView.text copy] : nil; | ||||
| } | ||||
|  | ||||
| #pragma mark - private | ||||
|  | ||||
| - (UIPickerView*)createFontsPicker { | ||||
|     UIPickerView *fontsPicker = [UIPickerView new]; | ||||
|     fontsPicker.dataSource = self; | ||||
|     fontsPicker.delegate = self; | ||||
|     fontsPicker.showsSelectionIndicator = YES; | ||||
|     return fontsPicker; | ||||
| } | ||||
|  | ||||
| - (void)createAvailableFonts { | ||||
|     NSMutableArray<NSString *> *unsortedFontsArray = [NSMutableArray new]; | ||||
|     for (NSString *eachFontFamily in UIFont.familyNames) { | ||||
|         for (NSString *eachFontName in [UIFont fontNamesForFamilyName:eachFontFamily]) { | ||||
|             [unsortedFontsArray addObject:eachFontName]; | ||||
|         } | ||||
|     } | ||||
|     self.availableFonts = [NSMutableArray arrayWithArray:[unsortedFontsArray sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]]; | ||||
| } | ||||
|  | ||||
| #pragma mark - UIPickerViewDataSource | ||||
|  | ||||
| - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView { | ||||
|     return 1; | ||||
| } | ||||
|  | ||||
| - (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component { | ||||
|     return self.availableFonts.count; | ||||
| } | ||||
|  | ||||
| #pragma mark - UIPickerViewDelegate | ||||
|  | ||||
| - (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view { | ||||
|     UILabel *fontLabel; | ||||
|     if (!view) { | ||||
|         fontLabel = [UILabel new]; | ||||
|         fontLabel.backgroundColor = UIColor.clearColor; | ||||
|         fontLabel.textAlignment = NSTextAlignmentCenter; | ||||
|     } else { | ||||
|         fontLabel = (UILabel*)view; | ||||
|     } | ||||
|     UIFont *font = [UIFont fontWithName:self.availableFonts[row] size:15.0]; | ||||
|     NSDictionary<NSString *, id> *attributesDictionary = [NSDictionary<NSString *, id> dictionaryWithObject:font forKey:NSFontAttributeName]; | ||||
|     NSAttributedString *attributesString = [[NSAttributedString alloc] initWithString:self.availableFonts[row] attributes:attributesDictionary]; | ||||
|     fontLabel.attributedText = attributesString; | ||||
|     [fontLabel sizeToFit]; | ||||
|     return fontLabel; | ||||
| } | ||||
|  | ||||
| - (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component { | ||||
|     self.inputTextView.text = self.availableFonts[row]; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,13 @@ | ||||
| // | ||||
| //  FLEXArgumentInputNotSupportedView.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 6/18/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputTextView.h" | ||||
|  | ||||
| @interface FLEXArgumentInputNotSupportedView : FLEXArgumentInputTextView | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,25 @@ | ||||
| // | ||||
| //  FLEXArgumentInputNotSupportedView.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 6/18/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputNotSupportedView.h" | ||||
| #import "FLEXColor.h" | ||||
|  | ||||
| @implementation FLEXArgumentInputNotSupportedView | ||||
|  | ||||
| - (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding { | ||||
|     self = [super initWithArgumentTypeEncoding:typeEncoding]; | ||||
|     if (self) { | ||||
|         self.inputTextView.userInteractionEnabled = NO; | ||||
|         self.inputTextView.backgroundColor = [FLEXColor secondaryGroupedBackgroundColorWithAlpha:0.5]; | ||||
|         self.inputPlaceholderText = @"nil  (type not supported)"; | ||||
|         self.targetSize = FLEXArgumentInputViewSizeSmall; | ||||
|     } | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,13 @@ | ||||
| // | ||||
| //  FLEXArgumentInputNumberView.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 6/15/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputTextView.h" | ||||
|  | ||||
| @interface FLEXArgumentInputNumberView : FLEXArgumentInputTextView | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,62 @@ | ||||
| // | ||||
| //  FLEXArgumentInputNumberView.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 6/15/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputNumberView.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
|  | ||||
| @implementation FLEXArgumentInputNumberView | ||||
|  | ||||
| - (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding { | ||||
|     self = [super initWithArgumentTypeEncoding:typeEncoding]; | ||||
|     if (self) { | ||||
|         self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation; | ||||
|         self.targetSize = FLEXArgumentInputViewSizeSmall; | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)setInputValue:(id)inputValue { | ||||
|     if ([inputValue respondsToSelector:@selector(stringValue)]) { | ||||
|         self.inputTextView.text = [inputValue stringValue]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (id)inputValue { | ||||
|     return [FLEXRuntimeUtility valueForNumberWithObjCType:self.typeEncoding.UTF8String fromInputString:self.inputTextView.text]; | ||||
| } | ||||
|  | ||||
| + (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value { | ||||
|     NSParameterAssert(type); | ||||
|      | ||||
|     static NSArray<NSString *> *supportedTypes = nil; | ||||
|     static dispatch_once_t onceToken; | ||||
|     dispatch_once(&onceToken, ^{ | ||||
|         supportedTypes = @[ | ||||
|             @FLEXEncodeClass(NSNumber), | ||||
|             @FLEXEncodeClass(NSDecimalNumber), | ||||
|             @(@encode(char)), | ||||
|             @(@encode(int)), | ||||
|             @(@encode(short)), | ||||
|             @(@encode(long)), | ||||
|             @(@encode(long long)), | ||||
|             @(@encode(unsigned char)), | ||||
|             @(@encode(unsigned int)), | ||||
|             @(@encode(unsigned short)), | ||||
|             @(@encode(unsigned long)), | ||||
|             @(@encode(unsigned long long)), | ||||
|             @(@encode(float)), | ||||
|             @(@encode(double)), | ||||
|             @(@encode(long double)) | ||||
|         ]; | ||||
|     }); | ||||
|      | ||||
|     return type && [supportedTypes containsObject:@(type)]; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,13 @@ | ||||
| // | ||||
| //  FLEXArgumentInputObjectView.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 6/15/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputTextView.h" | ||||
|  | ||||
| @interface FLEXArgumentInputObjectView : FLEXArgumentInputTextView | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,232 @@ | ||||
| // | ||||
| //  FLEXArgumentInputJSONObjectView.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 6/15/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputObjectView.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
|  | ||||
| static const CGFloat kSegmentInputMargin = 10; | ||||
|  | ||||
| typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) { | ||||
|     FLEXArgInputObjectTypeJSON, | ||||
|     FLEXArgInputObjectTypeAddress | ||||
| }; | ||||
|  | ||||
| @interface FLEXArgumentInputObjectView () | ||||
|  | ||||
| @property (nonatomic) UISegmentedControl *objectTypeSegmentControl; | ||||
| @property (nonatomic) FLEXArgInputObjectType inputType; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FLEXArgumentInputObjectView | ||||
|  | ||||
| - (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding { | ||||
|     self = [super initWithArgumentTypeEncoding:typeEncoding]; | ||||
|     if (self) { | ||||
|         // Start with the numbers and punctuation keyboard since quotes, curly braces, or | ||||
|         // square brackets are likely to be the first characters type for the JSON. | ||||
|         self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation; | ||||
|         self.targetSize = FLEXArgumentInputViewSizeLarge; | ||||
|  | ||||
|         self.objectTypeSegmentControl = [[UISegmentedControl alloc] initWithItems:@[@"Value", @"Address"]]; | ||||
|         [self.objectTypeSegmentControl addTarget:self action:@selector(didChangeType) forControlEvents:UIControlEventValueChanged]; | ||||
|         self.objectTypeSegmentControl.selectedSegmentIndex = 0; | ||||
|         [self addSubview:self.objectTypeSegmentControl]; | ||||
|  | ||||
|         self.inputType = [[self class] preferredDefaultTypeForObjCType:typeEncoding withCurrentValue:nil]; | ||||
|         self.objectTypeSegmentControl.selectedSegmentIndex = self.inputType; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)didChangeType { | ||||
|     self.inputType = self.objectTypeSegmentControl.selectedSegmentIndex; | ||||
|  | ||||
|     if (super.inputValue) { | ||||
|         // Trigger an update to the text field to show | ||||
|         // the address of the stored object we were given, | ||||
|         // or to show a JSON representation of the object | ||||
|         [self populateTextAreaFromValue:super.inputValue]; | ||||
|     } else { | ||||
|         // Clear the text field | ||||
|         [self populateTextAreaFromValue:nil]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)setInputType:(FLEXArgInputObjectType)inputType { | ||||
|     if (_inputType == inputType) return; | ||||
|  | ||||
|     _inputType = inputType; | ||||
|  | ||||
|     // Resize input view | ||||
|     switch (inputType) { | ||||
|         case FLEXArgInputObjectTypeJSON: | ||||
|             self.targetSize = FLEXArgumentInputViewSizeLarge; | ||||
|             break; | ||||
|         case FLEXArgInputObjectTypeAddress: | ||||
|             self.targetSize = FLEXArgumentInputViewSizeSmall; | ||||
|             break; | ||||
|     } | ||||
|  | ||||
|     // Change placeholder | ||||
|     switch (inputType) { | ||||
|         case FLEXArgInputObjectTypeJSON: | ||||
|             self.inputPlaceholderText = | ||||
|             @"You can put any valid JSON here, such as a string, number, array, or dictionary:" | ||||
|             "\n\"This is a string\"" | ||||
|             "\n1234" | ||||
|             "\n{ \"name\": \"Bob\", \"age\": 47 }" | ||||
|             "\n[" | ||||
|             "\n   1, 2, 3" | ||||
|             "\n]"; | ||||
|             break; | ||||
|         case FLEXArgInputObjectTypeAddress: | ||||
|             self.inputPlaceholderText = @"0x0000deadb33f"; | ||||
|             break; | ||||
|     } | ||||
|  | ||||
|     [self setNeedsLayout]; | ||||
|     [self.superview setNeedsLayout]; | ||||
| } | ||||
|  | ||||
| - (void)setInputValue:(id)inputValue { | ||||
|     super.inputValue = inputValue; | ||||
|     [self populateTextAreaFromValue:inputValue]; | ||||
| } | ||||
|  | ||||
| - (id)inputValue { | ||||
|     switch (self.inputType) { | ||||
|         case FLEXArgInputObjectTypeJSON: | ||||
|             return [FLEXRuntimeUtility objectValueFromEditableJSONString:self.inputTextView.text]; | ||||
|         case FLEXArgInputObjectTypeAddress: { | ||||
|             NSScanner *scanner = [NSScanner scannerWithString:self.inputTextView.text]; | ||||
|  | ||||
|             unsigned long long objectPointerValue; | ||||
|             if ([scanner scanHexLongLong:&objectPointerValue]) { | ||||
|                 return (__bridge id)(void *)objectPointerValue; | ||||
|             } | ||||
|  | ||||
|             return nil; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)populateTextAreaFromValue:(id)value { | ||||
|     if (!value) { | ||||
|         self.inputTextView.text = nil; | ||||
|     } else { | ||||
|         if (self.inputType == FLEXArgInputObjectTypeJSON) { | ||||
|             self.inputTextView.text = [FLEXRuntimeUtility editableJSONStringForObject:value]; | ||||
|         } else if (self.inputType == FLEXArgInputObjectTypeAddress) { | ||||
|             self.inputTextView.text = [NSString stringWithFormat:@"%p", value]; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Delegate methods are not called for programmatic changes | ||||
|     [self textViewDidChange:self.inputTextView]; | ||||
| } | ||||
|  | ||||
| - (CGSize)sizeThatFits:(CGSize)size { | ||||
|     CGSize fitSize = [super sizeThatFits:size]; | ||||
|     fitSize.height += [self.objectTypeSegmentControl sizeThatFits:size].height + kSegmentInputMargin; | ||||
|  | ||||
|     return fitSize; | ||||
| } | ||||
|  | ||||
| - (void)layoutSubviews { | ||||
|     CGFloat segmentHeight = [self.objectTypeSegmentControl sizeThatFits:self.frame.size].height; | ||||
|     self.objectTypeSegmentControl.frame = CGRectMake( | ||||
|         0.0, | ||||
|         // Our segmented control is taking the position | ||||
|         // of the text view, as far as super is concerned, | ||||
|         // and we override this property to be different | ||||
|         super.topInputFieldVerticalLayoutGuide, | ||||
|         self.frame.size.width, | ||||
|         segmentHeight | ||||
|     ); | ||||
|  | ||||
|     [super layoutSubviews]; | ||||
| } | ||||
|  | ||||
| - (CGFloat)topInputFieldVerticalLayoutGuide { | ||||
|     // Our text view is offset from the segmented control | ||||
|     CGFloat segmentHeight = [self.objectTypeSegmentControl sizeThatFits:self.frame.size].height; | ||||
|     return segmentHeight + super.topInputFieldVerticalLayoutGuide + kSegmentInputMargin; | ||||
| } | ||||
|  | ||||
| + (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value { | ||||
|     NSParameterAssert(type); | ||||
|     // Must be object type | ||||
|     return type[0] == FLEXTypeEncodingObjcObject || type[0] == FLEXTypeEncodingObjcClass; | ||||
| } | ||||
|  | ||||
| + (FLEXArgInputObjectType)preferredDefaultTypeForObjCType:(const char *)type withCurrentValue:(id)value { | ||||
|     NSParameterAssert(type[0] == FLEXTypeEncodingObjcObject || type[0] == FLEXTypeEncodingObjcClass); | ||||
|  | ||||
|     if (value) { | ||||
|         // If there's a current value, it must be serializable to JSON | ||||
|         // to display the JSON editor. Otherwise display the address field. | ||||
|         if ([FLEXRuntimeUtility editableJSONStringForObject:value]) { | ||||
|             return FLEXArgInputObjectTypeJSON; | ||||
|         } else { | ||||
|             return FLEXArgInputObjectTypeAddress; | ||||
|         } | ||||
|     } else { | ||||
|         // Otherwise, see if we have more type information than just 'id'. | ||||
|         // If we do, make sure the encoding is something serializable to JSON. | ||||
|         // Properties and ivars keep more detailed type encoding information than method arguments. | ||||
|         if (strcmp(type, @encode(id)) != 0) { | ||||
|             BOOL isJSONSerializableType = NO; | ||||
|  | ||||
|             // Parse class name out of the string, | ||||
|             // which is in the form `@"ClassName"` | ||||
|             Class cls = NSClassFromString(({ | ||||
|                 NSString *className = nil; | ||||
|                 NSScanner *scan = [NSScanner scannerWithString:@(type)]; | ||||
|                 NSCharacterSet *allowed = [NSCharacterSet | ||||
|                     characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$" | ||||
|                 ]; | ||||
|  | ||||
|                 // Skip over the @" then scan the name | ||||
|                 if ([scan scanString:@"@\"" intoString:nil]) { | ||||
|                     [scan scanCharactersFromSet:allowed intoString:&className]; | ||||
|                 } | ||||
|  | ||||
|                 className; | ||||
|             })); | ||||
|  | ||||
|             // Note: we can't use @encode(NSString) here because that drops | ||||
|             // the class information and just goes to @encode(id). | ||||
|             NSArray<Class> *jsonTypes = @[ | ||||
|                 [NSString class], | ||||
|                 [NSNumber class], | ||||
|                 [NSArray class], | ||||
|                 [NSDictionary class], | ||||
|             ]; | ||||
|  | ||||
|             // Look for matching types | ||||
|             for (Class jsonClass in jsonTypes) { | ||||
|                 if ([cls isSubclassOfClass:jsonClass]) { | ||||
|                     isJSONSerializableType = YES; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (isJSONSerializableType) { | ||||
|                 return FLEXArgInputObjectTypeJSON; | ||||
|             } else { | ||||
|                 return FLEXArgInputObjectTypeAddress; | ||||
|             } | ||||
|         } else { | ||||
|             return FLEXArgInputObjectTypeAddress; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,13 @@ | ||||
| // | ||||
| //  FLEXArgumentInputStringView.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 6/28/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputTextView.h" | ||||
|  | ||||
| @interface FLEXArgumentInputStringView : FLEXArgumentInputTextView | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,129 @@ | ||||
| // | ||||
| //  FLEXArgumentInputStringView.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 6/28/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputStringView.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
|  | ||||
| @implementation FLEXArgumentInputStringView | ||||
|  | ||||
| - (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding { | ||||
|     self = [super initWithArgumentTypeEncoding:typeEncoding]; | ||||
|     if (self) { | ||||
|         FLEXTypeEncoding type = typeEncoding[0]; | ||||
|         if (type == FLEXTypeEncodingConst) { | ||||
|             // A crash here would mean an invalid type encoding string | ||||
|             type = typeEncoding[1]; | ||||
|         } | ||||
|  | ||||
|         // Selectors don't need a multi-line text box | ||||
|         if (type == FLEXTypeEncodingSelector) { | ||||
|             self.targetSize = FLEXArgumentInputViewSizeSmall; | ||||
|         } else { | ||||
|             self.targetSize = FLEXArgumentInputViewSizeLarge; | ||||
|         } | ||||
|     } | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)setInputValue:(id)inputValue { | ||||
|     if ([inputValue isKindOfClass:[NSString class]]) { | ||||
|         self.inputTextView.text = inputValue; | ||||
|     } else if ([inputValue isKindOfClass:[NSValue class]]) { | ||||
|         NSValue *value = (id)inputValue; | ||||
|         NSParameterAssert(strlen(value.objCType) == 1); | ||||
|  | ||||
|         // C-String or SEL from NSValue | ||||
|         FLEXTypeEncoding type = value.objCType[0]; | ||||
|         if (type == FLEXTypeEncodingConst) { | ||||
|             // A crash here would mean an invalid type encoding string | ||||
|             type = value.objCType[1]; | ||||
|         } | ||||
|  | ||||
|         if (type == FLEXTypeEncodingCString) { | ||||
|             self.inputTextView.text = @((const char *)value.pointerValue); | ||||
|         } else if (type == FLEXTypeEncodingSelector) { | ||||
|             self.inputTextView.text = NSStringFromSelector((SEL)value.pointerValue); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (id)inputValue { | ||||
|     NSString *text = self.inputTextView.text; | ||||
|     // Interpret empty string as nil. We loose the ability to set empty string as a string value, | ||||
|     // but we accept that tradeoff in exchange for not having to type quotes for every string. | ||||
|     if (!text.length) { | ||||
|         return nil; | ||||
|     } | ||||
|  | ||||
|     // Case: C-strings and SELs | ||||
|     if (self.typeEncoding.length <= 2) { | ||||
|         FLEXTypeEncoding type = [self.typeEncoding characterAtIndex:0]; | ||||
|         if (type == FLEXTypeEncodingConst) { | ||||
|             // A crash here would mean an invalid type encoding string | ||||
|             type = [self.typeEncoding characterAtIndex:1]; | ||||
|         } | ||||
|  | ||||
|         if (type == FLEXTypeEncodingCString || type == FLEXTypeEncodingSelector) { | ||||
|             const char *encoding = self.typeEncoding.UTF8String; | ||||
|             SEL selector = NSSelectorFromString(text); | ||||
|             return [NSValue valueWithBytes:&selector objCType:encoding]; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Case: NSStrings | ||||
|     return self.inputTextView.text.copy; | ||||
| } | ||||
|  | ||||
| // TODO: Support using object address for strings, as in the object arg view. | ||||
|  | ||||
| + (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value { | ||||
|     NSParameterAssert(type); | ||||
|     unsigned long len = strlen(type); | ||||
|  | ||||
|     BOOL isConst = type[0] == FLEXTypeEncodingConst; | ||||
|     NSInteger i = isConst ? 1 : 0; | ||||
|  | ||||
|     BOOL typeIsString = strcmp(type, FLEXEncodeClass(NSString)) == 0; | ||||
|     BOOL typeIsCString = len <= 2 && type[i] == FLEXTypeEncodingCString; | ||||
|     BOOL typeIsSEL = len <= 2 && type[i] == FLEXTypeEncodingSelector; | ||||
|     BOOL valueIsString = [value isKindOfClass:[NSString class]]; | ||||
|  | ||||
|     BOOL typeIsPrimitiveString = typeIsSEL || typeIsCString; | ||||
|     BOOL typeIsSupported = typeIsString || typeIsCString || typeIsSEL; | ||||
|  | ||||
|     BOOL valueIsNSValueWithCorrectType = NO; | ||||
|     if ([value isKindOfClass:[NSValue class]]) { | ||||
|         NSValue *v = (id)value; | ||||
|         len = strlen(v.objCType); | ||||
|         if (len == 1) { | ||||
|             FLEXTypeEncoding type = v.objCType[i]; | ||||
|             if (type == FLEXTypeEncodingCString && typeIsCString) { | ||||
|                 valueIsNSValueWithCorrectType = YES; | ||||
|             } else if (type == FLEXTypeEncodingSelector && typeIsSEL) { | ||||
|                 valueIsNSValueWithCorrectType = YES; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if (!value && typeIsSupported) { | ||||
|         return YES; | ||||
|     } | ||||
|  | ||||
|     if (typeIsString && valueIsString) { | ||||
|         return YES; | ||||
|     } | ||||
|  | ||||
|     // Primitive strings can be input as NSStrings or NSValues | ||||
|     if (typeIsPrimitiveString && (valueIsString || valueIsNSValueWithCorrectType)) { | ||||
|         return YES; | ||||
|     } | ||||
|  | ||||
|     return NO; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,13 @@ | ||||
| // | ||||
| //  FLEXArgumentInputStructView.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 6/16/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputView.h" | ||||
|  | ||||
| @interface FLEXArgumentInputStructView : FLEXArgumentInputView | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,220 @@ | ||||
| // | ||||
| //  FLEXArgumentInputStructView.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 6/16/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputStructView.h" | ||||
| #import "FLEXArgumentInputViewFactory.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
| #import "FLEXTypeEncodingParser.h" | ||||
|  | ||||
| @interface FLEXArgumentInputStructView () | ||||
|  | ||||
| @property (nonatomic) NSArray<FLEXArgumentInputView *> *argumentInputViews; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FLEXArgumentInputStructView | ||||
|  | ||||
| - (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding { | ||||
|     self = [super initWithArgumentTypeEncoding:typeEncoding]; | ||||
|     if (self) { | ||||
|         NSMutableArray<FLEXArgumentInputView *> *inputViews = [NSMutableArray new]; | ||||
|         NSArray<NSString *> *customTitles = [[self class] customFieldTitlesForTypeEncoding:typeEncoding]; | ||||
|         [FLEXRuntimeUtility enumerateTypesInStructEncoding:typeEncoding usingBlock:^(NSString *structName, | ||||
|                                                                                      const char *fieldTypeEncoding, | ||||
|                                                                                      NSString *prettyTypeEncoding, | ||||
|                                                                                      NSUInteger fieldIndex, | ||||
|                                                                                      NSUInteger fieldOffset) { | ||||
|              | ||||
|             FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:fieldTypeEncoding]; | ||||
|             inputView.targetSize = FLEXArgumentInputViewSizeSmall; | ||||
|              | ||||
|             if (fieldIndex < customTitles.count) { | ||||
|                 inputView.title = customTitles[fieldIndex]; | ||||
|             } else { | ||||
|                 inputView.title = [NSString stringWithFormat:@"%@ field %lu (%@)", | ||||
|                     structName, (unsigned long)fieldIndex, prettyTypeEncoding | ||||
|                 ]; | ||||
|             } | ||||
|  | ||||
|             [inputViews addObject:inputView]; | ||||
|             [self addSubview:inputView]; | ||||
|         }]; | ||||
|         self.argumentInputViews = inputViews; | ||||
|     } | ||||
|     return self; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Superclass Overrides | ||||
|  | ||||
| - (void)setBackgroundColor:(UIColor *)backgroundColor { | ||||
|     [super setBackgroundColor:backgroundColor]; | ||||
|     for (FLEXArgumentInputView *inputView in self.argumentInputViews) { | ||||
|         inputView.backgroundColor = backgroundColor; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)setInputValue:(id)inputValue { | ||||
|     if ([inputValue isKindOfClass:[NSValue class]]) { | ||||
|         const char *structTypeEncoding = [inputValue objCType]; | ||||
|         if (strcmp(self.typeEncoding.UTF8String, structTypeEncoding) == 0) { | ||||
|             NSUInteger valueSize = 0; | ||||
|              | ||||
|             if (FLEXGetSizeAndAlignment(structTypeEncoding, &valueSize, NULL)) { | ||||
|                 void *unboxedValue = malloc(valueSize); | ||||
|                 [inputValue getValue:unboxedValue]; | ||||
|                 [FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName, | ||||
|                                                                                                    const char *fieldTypeEncoding, | ||||
|                                                                                                    NSString *prettyTypeEncoding, | ||||
|                                                                                                    NSUInteger fieldIndex, | ||||
|                                                                                                    NSUInteger fieldOffset) { | ||||
|                      | ||||
|                     void *fieldPointer = unboxedValue + fieldOffset; | ||||
|                     FLEXArgumentInputView *inputView = self.argumentInputViews[fieldIndex]; | ||||
|                      | ||||
|                     if (fieldTypeEncoding[0] == FLEXTypeEncodingObjcObject || fieldTypeEncoding[0] == FLEXTypeEncodingObjcClass) { | ||||
|                         inputView.inputValue = (__bridge id)fieldPointer; | ||||
|                     } else { | ||||
|                         NSValue *boxedField = [FLEXRuntimeUtility valueForPrimitivePointer:fieldPointer objCType:fieldTypeEncoding]; | ||||
|                         inputView.inputValue = boxedField; | ||||
|                     } | ||||
|                 }]; | ||||
|                 free(unboxedValue); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (id)inputValue { | ||||
|     NSValue *boxedStruct = nil; | ||||
|     const char *structTypeEncoding = self.typeEncoding.UTF8String; | ||||
|     NSUInteger structSize = 0; | ||||
|      | ||||
|     if (FLEXGetSizeAndAlignment(structTypeEncoding, &structSize, NULL)) { | ||||
|         void *unboxedStruct = malloc(structSize); | ||||
|         [FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName, | ||||
|                                                                                            const char *fieldTypeEncoding, | ||||
|                                                                                            NSString *prettyTypeEncoding, | ||||
|                                                                                            NSUInteger fieldIndex, | ||||
|                                                                                            NSUInteger fieldOffset) { | ||||
|              | ||||
|             void *fieldPointer = unboxedStruct + fieldOffset; | ||||
|             FLEXArgumentInputView *inputView = self.argumentInputViews[fieldIndex]; | ||||
|              | ||||
|             if (fieldTypeEncoding[0] == FLEXTypeEncodingObjcObject || fieldTypeEncoding[0] == FLEXTypeEncodingObjcClass) { | ||||
|                 // Object fields | ||||
|                 memcpy(fieldPointer, (__bridge void *)inputView.inputValue, sizeof(id)); | ||||
|             } else { | ||||
|                 // Boxed primitive/struct fields | ||||
|                 id inputValue = inputView.inputValue; | ||||
|                 if ([inputValue isKindOfClass:[NSValue class]] && strcmp([inputValue objCType], fieldTypeEncoding) == 0) { | ||||
|                     [inputValue getValue:fieldPointer]; | ||||
|                 } | ||||
|             } | ||||
|         }]; | ||||
|          | ||||
|         boxedStruct = [NSValue value:unboxedStruct withObjCType:structTypeEncoding]; | ||||
|         free(unboxedStruct); | ||||
|     } | ||||
|      | ||||
|     return boxedStruct; | ||||
| } | ||||
|  | ||||
| - (BOOL)inputViewIsFirstResponder { | ||||
|     BOOL isFirstResponder = NO; | ||||
|     for (FLEXArgumentInputView *inputView in self.argumentInputViews) { | ||||
|         if ([inputView inputViewIsFirstResponder]) { | ||||
|             isFirstResponder = YES; | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
|     return isFirstResponder; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Layout and Sizing | ||||
|  | ||||
| - (void)layoutSubviews { | ||||
|     [super layoutSubviews]; | ||||
|      | ||||
|     CGFloat runningOriginY = self.topInputFieldVerticalLayoutGuide; | ||||
|      | ||||
|     for (FLEXArgumentInputView *inputView in self.argumentInputViews) { | ||||
|         CGSize inputFitSize = [inputView sizeThatFits:self.bounds.size]; | ||||
|         inputView.frame = CGRectMake(0, runningOriginY, inputFitSize.width, inputFitSize.height); | ||||
|         runningOriginY = CGRectGetMaxY(inputView.frame) + [[self class] verticalPaddingBetweenFields]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| + (CGFloat)verticalPaddingBetweenFields { | ||||
|     return 10.0; | ||||
| } | ||||
|  | ||||
| - (CGSize)sizeThatFits:(CGSize)size { | ||||
|     CGSize fitSize = [super sizeThatFits:size]; | ||||
|      | ||||
|     CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX); | ||||
|     CGFloat height = fitSize.height; | ||||
|      | ||||
|     for (FLEXArgumentInputView *inputView in self.argumentInputViews) { | ||||
|         height += [inputView sizeThatFits:constrainSize].height; | ||||
|         height += [[self class] verticalPaddingBetweenFields]; | ||||
|     } | ||||
|      | ||||
|     return CGSizeMake(fitSize.width, height); | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Class Helpers | ||||
|  | ||||
| + (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value { | ||||
|     NSParameterAssert(type); | ||||
|     if (type[0] == FLEXTypeEncodingStructBegin) { | ||||
|         return FLEXGetSizeAndAlignment(type, nil, nil); | ||||
|     } | ||||
|  | ||||
|     return NO; | ||||
| } | ||||
|  | ||||
| + (NSArray<NSString *> *)customFieldTitlesForTypeEncoding:(const char *)typeEncoding { | ||||
|     NSArray<NSString *> *customTitles = nil; | ||||
|     if (strcmp(typeEncoding, @encode(CGRect)) == 0) { | ||||
|         customTitles = @[@"CGPoint origin", @"CGSize size"]; | ||||
|     } else if (strcmp(typeEncoding, @encode(CGPoint)) == 0) { | ||||
|         customTitles = @[@"CGFloat x", @"CGFloat y"]; | ||||
|     } else if (strcmp(typeEncoding, @encode(CGSize)) == 0) { | ||||
|         customTitles = @[@"CGFloat width", @"CGFloat height"]; | ||||
|     } else if (strcmp(typeEncoding, @encode(CGVector)) == 0) { | ||||
|         customTitles = @[@"CGFloat dx", @"CGFloat dy"]; | ||||
|     } else if (strcmp(typeEncoding, @encode(UIEdgeInsets)) == 0) { | ||||
|         customTitles = @[@"CGFloat top", @"CGFloat left", @"CGFloat bottom", @"CGFloat right"]; | ||||
|     } else if (strcmp(typeEncoding, @encode(UIOffset)) == 0) { | ||||
|         customTitles = @[@"CGFloat horizontal", @"CGFloat vertical"]; | ||||
|     } else if (strcmp(typeEncoding, @encode(NSRange)) == 0) { | ||||
|         customTitles = @[@"NSUInteger location", @"NSUInteger length"]; | ||||
|     } else if (strcmp(typeEncoding, @encode(CATransform3D)) == 0) { | ||||
|         customTitles = @[@"CGFloat m11", @"CGFloat m12", @"CGFloat m13", @"CGFloat m14", | ||||
|                          @"CGFloat m21", @"CGFloat m22", @"CGFloat m23", @"CGFloat m24", | ||||
|                          @"CGFloat m31", @"CGFloat m32", @"CGFloat m33", @"CGFloat m34", | ||||
|                          @"CGFloat m41", @"CGFloat m42", @"CGFloat m43", @"CGFloat m44"]; | ||||
|     } else if (strcmp(typeEncoding, @encode(CGAffineTransform)) == 0) { | ||||
|         customTitles = @[@"CGFloat a", @"CGFloat b", | ||||
|                          @"CGFloat c", @"CGFloat d", | ||||
|                          @"CGFloat tx", @"CGFloat ty"]; | ||||
|     } else { | ||||
|         if (@available(iOS 11.0, *)) { | ||||
|             if (strcmp(typeEncoding, @encode(NSDirectionalEdgeInsets)) == 0) { | ||||
|                 customTitles = @[@"CGFloat top", @"CGFloat leading", | ||||
|                                  @"CGFloat bottom", @"CGFloat trailing"]; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     return customTitles; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,13 @@ | ||||
| // | ||||
| //  FLEXArgumentInputSwitchView.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 6/16/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputView.h" | ||||
|  | ||||
| @interface FLEXArgumentInputSwitchView : FLEXArgumentInputView | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,81 @@ | ||||
| // | ||||
| //  FLEXArgumentInputSwitchView.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 6/16/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputSwitchView.h" | ||||
|  | ||||
| @interface FLEXArgumentInputSwitchView () | ||||
|  | ||||
| @property (nonatomic) UISwitch *inputSwitch; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FLEXArgumentInputSwitchView | ||||
|  | ||||
| - (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding { | ||||
|     self = [super initWithArgumentTypeEncoding:typeEncoding]; | ||||
|     if (self) { | ||||
|         self.inputSwitch = [UISwitch new]; | ||||
|         [self.inputSwitch addTarget:self action:@selector(switchValueDidChange:) forControlEvents:UIControlEventValueChanged]; | ||||
|         [self.inputSwitch sizeToFit]; | ||||
|         [self addSubview:self.inputSwitch]; | ||||
|     } | ||||
|     return self; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark Input/Output | ||||
|  | ||||
| - (void)setInputValue:(id)inputValue { | ||||
|     BOOL on = NO; | ||||
|     if ([inputValue isKindOfClass:[NSNumber class]]) { | ||||
|         NSNumber *number = (NSNumber *)inputValue; | ||||
|         on = [number boolValue]; | ||||
|     } else if ([inputValue isKindOfClass:[NSValue class]]) { | ||||
|         NSValue *value = (NSValue *)inputValue; | ||||
|         if (strcmp([value objCType], @encode(BOOL)) == 0) { | ||||
|             [value getValue:&on]; | ||||
|         } | ||||
|     } | ||||
|     self.inputSwitch.on = on; | ||||
| } | ||||
|  | ||||
| - (id)inputValue { | ||||
|     BOOL isOn = [self.inputSwitch isOn]; | ||||
|     NSValue *boxedBool = [NSValue value:&isOn withObjCType:@encode(BOOL)]; | ||||
|     return boxedBool; | ||||
| } | ||||
|  | ||||
| - (void)switchValueDidChange:(id)sender { | ||||
|     [self.delegate argumentInputViewValueDidChange:self]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Layout and Sizing | ||||
|  | ||||
| - (void)layoutSubviews { | ||||
|     [super layoutSubviews]; | ||||
|      | ||||
|     self.inputSwitch.frame = CGRectMake(0, self.topInputFieldVerticalLayoutGuide, self.inputSwitch.frame.size.width, self.inputSwitch.frame.size.height); | ||||
| } | ||||
|  | ||||
| - (CGSize)sizeThatFits:(CGSize)size { | ||||
|     CGSize fitSize = [super sizeThatFits:size]; | ||||
|     fitSize.height += self.inputSwitch.frame.size.height; | ||||
|     return fitSize; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Class Helpers | ||||
|  | ||||
| + (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value { | ||||
|     NSParameterAssert(type); | ||||
|     // Only BOOLs. Current value is irrelevant. | ||||
|     return strcmp(type, @encode(BOOL)) == 0; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,18 @@ | ||||
| // | ||||
| //  FLEXArgumentInputTextView.h | ||||
| //  FLEXInjected | ||||
| // | ||||
| //  Created by Ryan Olson on 6/15/14. | ||||
| // | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputView.h" | ||||
|  | ||||
| @interface FLEXArgumentInputTextView : FLEXArgumentInputView <UITextViewDelegate> | ||||
|  | ||||
| // For subclass eyes only | ||||
|  | ||||
| @property (nonatomic, readonly) UITextView *inputTextView; | ||||
| @property (nonatomic) NSString *inputPlaceholderText; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,155 @@ | ||||
| // | ||||
| //  FLEXArgumentInputTextView.m | ||||
| //  FLEXInjected | ||||
| // | ||||
| //  Created by Ryan Olson on 6/15/14. | ||||
| // | ||||
| // | ||||
|  | ||||
| #import "FLEXColor.h" | ||||
| #import "FLEXArgumentInputTextView.h" | ||||
| #import "FLEXUtility.h" | ||||
|  | ||||
| @interface FLEXArgumentInputTextView () | ||||
|  | ||||
| @property (nonatomic) UITextView *inputTextView; | ||||
| @property (nonatomic) UILabel *placeholderLabel; | ||||
| @property (nonatomic, readonly) NSUInteger numberOfInputLines; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FLEXArgumentInputTextView | ||||
|  | ||||
| - (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding { | ||||
|     self = [super initWithArgumentTypeEncoding:typeEncoding]; | ||||
|     if (self) { | ||||
|         self.inputTextView = [UITextView new]; | ||||
|         self.inputTextView.font = [[self class] inputFont]; | ||||
|         self.inputTextView.backgroundColor = FLEXColor.secondaryGroupedBackgroundColor; | ||||
|         self.inputTextView.layer.cornerRadius = 10.f; | ||||
|         self.inputTextView.contentInset = UIEdgeInsetsMake(0, 5, 0, 0); | ||||
|         self.inputTextView.autocapitalizationType = UITextAutocapitalizationTypeNone; | ||||
|         self.inputTextView.autocorrectionType = UITextAutocorrectionTypeNo; | ||||
|         self.inputTextView.delegate = self; | ||||
|         self.inputTextView.inputAccessoryView = [self createToolBar]; | ||||
|         if (@available(iOS 11, *)) { | ||||
|             self.inputTextView.smartQuotesType = UITextSmartQuotesTypeNo; | ||||
|             [self.inputTextView.layer setValue:@YES forKey:@"continuousCorners"]; | ||||
|         } else { | ||||
|             self.inputTextView.layer.borderWidth = 1.f; | ||||
|             self.inputTextView.layer.borderColor = FLEXColor.borderColor.CGColor; | ||||
|         } | ||||
|  | ||||
|         self.placeholderLabel = [UILabel new]; | ||||
|         self.placeholderLabel.font = self.inputTextView.font; | ||||
|         self.placeholderLabel.textColor = FLEXColor.deemphasizedTextColor; | ||||
|         self.placeholderLabel.numberOfLines = 0; | ||||
|  | ||||
|         [self addSubview:self.inputTextView]; | ||||
|         [self.inputTextView addSubview:self.placeholderLabel]; | ||||
|  | ||||
|     } | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| #pragma mark - Private | ||||
|  | ||||
| - (UIToolbar *)createToolBar { | ||||
|     UIToolbar *toolBar = [UIToolbar new]; | ||||
|     [toolBar sizeToFit]; | ||||
|     UIBarButtonItem *spaceItem = [[UIBarButtonItem alloc] | ||||
|         initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace | ||||
|         target:nil action:nil | ||||
|     ]; | ||||
|     UIBarButtonItem *pasteItem = [[UIBarButtonItem alloc] | ||||
|         initWithTitle:@"Paste" style:UIBarButtonItemStyleDone | ||||
|         target:self.inputTextView action:@selector(paste:) | ||||
|     ]; | ||||
|     UIBarButtonItem *doneItem = [[UIBarButtonItem alloc] | ||||
|         initWithBarButtonSystemItem:UIBarButtonSystemItemDone | ||||
|         target:self.inputTextView action:@selector(resignFirstResponder) | ||||
|     ]; | ||||
|     toolBar.items = @[spaceItem, pasteItem, doneItem]; | ||||
|     return toolBar; | ||||
| } | ||||
|  | ||||
| - (void)setInputPlaceholderText:(NSString *)placeholder { | ||||
|     self.placeholderLabel.text = placeholder; | ||||
|     if (placeholder.length) { | ||||
|         if (!self.inputTextView.text.length) { | ||||
|             self.placeholderLabel.hidden = NO; | ||||
|         } else { | ||||
|             self.placeholderLabel.hidden = YES; | ||||
|         } | ||||
|     } else { | ||||
|         self.placeholderLabel.hidden = YES; | ||||
|     } | ||||
|  | ||||
|     [self setNeedsLayout]; | ||||
| } | ||||
|  | ||||
| - (NSString *)inputPlaceholderText { | ||||
|     return self.placeholderLabel.text; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Superclass Overrides | ||||
|  | ||||
| - (BOOL)inputViewIsFirstResponder { | ||||
|     return self.inputTextView.isFirstResponder; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Layout and Sizing | ||||
|  | ||||
| - (void)layoutSubviews { | ||||
|     [super layoutSubviews]; | ||||
|      | ||||
|     self.inputTextView.frame = CGRectMake(0, self.topInputFieldVerticalLayoutGuide, self.bounds.size.width, [self inputTextViewHeight]); | ||||
|     // Placeholder label is positioned by insetting then origin | ||||
|     // by the content inset then the text container inset | ||||
|     CGSize s = self.inputTextView.frame.size; | ||||
|     self.placeholderLabel.frame = CGRectMake(0, 0, s.width, s.height); | ||||
|     self.placeholderLabel.frame = UIEdgeInsetsInsetRect( | ||||
|         UIEdgeInsetsInsetRect(self.placeholderLabel.frame, self.inputTextView.contentInset), | ||||
|         self.inputTextView.textContainerInset | ||||
|     ); | ||||
| } | ||||
|  | ||||
| - (NSUInteger)numberOfInputLines { | ||||
|     switch (self.targetSize) { | ||||
|         case FLEXArgumentInputViewSizeDefault: | ||||
|             return 2; | ||||
|         case FLEXArgumentInputViewSizeSmall: | ||||
|             return 1; | ||||
|         case FLEXArgumentInputViewSizeLarge: | ||||
|             return 8; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (CGFloat)inputTextViewHeight { | ||||
|     return ceil([[self class] inputFont].lineHeight * self.numberOfInputLines) + 16.0; | ||||
| } | ||||
|  | ||||
| - (CGSize)sizeThatFits:(CGSize)size { | ||||
|     CGSize fitSize = [super sizeThatFits:size]; | ||||
|     fitSize.height += [self inputTextViewHeight]; | ||||
|     return fitSize; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Class Helpers | ||||
|  | ||||
| + (UIFont *)inputFont { | ||||
|     return [UIFont systemFontOfSize:14.0]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - UITextViewDelegate | ||||
|  | ||||
| - (void)textViewDidChange:(UITextView *)textView { | ||||
|     [self.delegate argumentInputViewValueDidChange:self]; | ||||
|     self.placeholderLabel.hidden = !(self.inputPlaceholderText.length && !textView.text.length); | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,64 @@ | ||||
| // | ||||
| //  FLEXArgumentInputView.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 5/30/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) { | ||||
|     /// 2 lines, medium-sized | ||||
|     FLEXArgumentInputViewSizeDefault = 0, | ||||
|     /// One line | ||||
|     FLEXArgumentInputViewSizeSmall, | ||||
|     /// Several lines | ||||
|     FLEXArgumentInputViewSizeLarge | ||||
| }; | ||||
|  | ||||
| @protocol FLEXArgumentInputViewDelegate; | ||||
|  | ||||
| @interface FLEXArgumentInputView : UIView | ||||
|  | ||||
| - (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding; | ||||
|  | ||||
| /// The name of the field. Optional (can be nil). | ||||
| @property (nonatomic, copy) NSString *title; | ||||
|  | ||||
| /// To populate the filed with an initial value, set this property. | ||||
| /// To reteive the value input by the user, access the property. | ||||
| /// Primitive types and structs should/will be boxed in NSValue containers. | ||||
| /// Concrete subclasses should override both the setter and getter for this property. | ||||
| /// Subclasses can call super.inputValue to access a backing store for the value. | ||||
| @property (nonatomic) id inputValue; | ||||
|  | ||||
| /// Setting this value to large will make some argument input views increase the size of their input field(s). | ||||
| /// Useful to increase the use of space if there is only one input view on screen (i.e. for property and ivar editing). | ||||
| @property (nonatomic) FLEXArgumentInputViewSize targetSize; | ||||
|  | ||||
| /// Users of the input view can get delegate callbacks for incremental changes in user input. | ||||
| @property (nonatomic, weak) id <FLEXArgumentInputViewDelegate> delegate; | ||||
|  | ||||
| // Subclasses can override | ||||
|  | ||||
| /// If the input view has one or more text views, returns YES when one of them is focused. | ||||
| @property (nonatomic, readonly) BOOL inputViewIsFirstResponder; | ||||
|  | ||||
| /// For subclasses to indicate that they can handle editing a field the give type and value. | ||||
| /// Used by FLEXArgumentInputViewFactory to create appropriate input views. | ||||
| + (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value; | ||||
|  | ||||
| // For subclass eyes only | ||||
|  | ||||
| @property (nonatomic, readonly) UILabel *titleLabel; | ||||
| @property (nonatomic, readonly) NSString *typeEncoding; | ||||
| @property (nonatomic, readonly) CGFloat topInputFieldVerticalLayoutGuide; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @protocol FLEXArgumentInputViewDelegate <NSObject> | ||||
|  | ||||
| - (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView; | ||||
|  | ||||
| @end | ||||
							
								
								
									
										114
									
								
								Tweaks/FLEX/Editing/ArgumentInputViews/FLEXArgumentInputView.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								Tweaks/FLEX/Editing/ArgumentInputViews/FLEXArgumentInputView.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| // | ||||
| //  FLEXArgumentInputView.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 5/30/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputView.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXColor.h" | ||||
|  | ||||
| @interface FLEXArgumentInputView () | ||||
|  | ||||
| @property (nonatomic) UILabel *titleLabel; | ||||
| @property (nonatomic) NSString *typeEncoding; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FLEXArgumentInputView | ||||
|  | ||||
| - (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding { | ||||
|     self = [super initWithFrame:CGRectZero]; | ||||
|     if (self) { | ||||
|         self.typeEncoding = typeEncoding != NULL ? @(typeEncoding) : nil; | ||||
|     } | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)layoutSubviews { | ||||
|     [super layoutSubviews]; | ||||
|      | ||||
|     if (self.showsTitle) { | ||||
|         CGSize constrainSize = CGSizeMake(self.bounds.size.width, CGFLOAT_MAX); | ||||
|         CGSize labelSize = [self.titleLabel sizeThatFits:constrainSize]; | ||||
|         self.titleLabel.frame = CGRectMake(0, 0, labelSize.width, labelSize.height); | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)setBackgroundColor:(UIColor *)backgroundColor { | ||||
|     [super setBackgroundColor:backgroundColor]; | ||||
|     self.titleLabel.backgroundColor = backgroundColor; | ||||
| } | ||||
|  | ||||
| - (void)setTitle:(NSString *)title { | ||||
|     if (![_title isEqual:title]) { | ||||
|         _title = title; | ||||
|         self.titleLabel.text = title; | ||||
|         [self setNeedsLayout]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (UILabel *)titleLabel { | ||||
|     if (!_titleLabel) { | ||||
|         _titleLabel = [UILabel new]; | ||||
|         _titleLabel.font = [[self class] titleFont]; | ||||
|         _titleLabel.textColor = FLEXColor.primaryTextColor; | ||||
|         _titleLabel.numberOfLines = 0; | ||||
|         [self addSubview:_titleLabel]; | ||||
|     } | ||||
|     return _titleLabel; | ||||
| } | ||||
|  | ||||
| - (BOOL)showsTitle { | ||||
|     return self.title.length > 0; | ||||
| } | ||||
|  | ||||
| - (CGFloat)topInputFieldVerticalLayoutGuide { | ||||
|     CGFloat verticalLayoutGuide = 0; | ||||
|     if (self.showsTitle) { | ||||
|         CGFloat titleHeight = [self.titleLabel sizeThatFits:self.bounds.size].height; | ||||
|         verticalLayoutGuide = titleHeight + [[self class] titleBottomPadding]; | ||||
|     } | ||||
|     return verticalLayoutGuide; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Subclasses Can Override | ||||
|  | ||||
| - (BOOL)inputViewIsFirstResponder { | ||||
|     return NO; | ||||
| } | ||||
|  | ||||
| + (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value { | ||||
|     return NO; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Class Helpers | ||||
|  | ||||
| + (UIFont *)titleFont { | ||||
|     return [UIFont systemFontOfSize:12.0]; | ||||
| } | ||||
|  | ||||
| + (CGFloat)titleBottomPadding { | ||||
|     return 4.0; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Sizing | ||||
|  | ||||
| - (CGSize)sizeThatFits:(CGSize)size { | ||||
|     CGFloat height = 0; | ||||
|      | ||||
|     if (self.title.length > 0) { | ||||
|         CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX); | ||||
|         height += ceil([self.titleLabel sizeThatFits:constrainSize].height); | ||||
|         height += [[self class] titleBottomPadding]; | ||||
|     } | ||||
|      | ||||
|     return CGSizeMake(size.width, height); | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										24
									
								
								Tweaks/FLEX/Editing/FLEXArgumentInputViewFactory.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								Tweaks/FLEX/Editing/FLEXArgumentInputViewFactory.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| // | ||||
| //  FLEXArgumentInputViewFactory.h | ||||
| //  FLEXInjected | ||||
| // | ||||
| //  Created by Ryan Olson on 6/15/14. | ||||
| // | ||||
| // | ||||
|  | ||||
| #import <Foundation/Foundation.h> | ||||
| #import "FLEXArgumentInputSwitchView.h" | ||||
|  | ||||
| @interface FLEXArgumentInputViewFactory : NSObject | ||||
|  | ||||
| /// Forwards to argumentInputViewForTypeEncoding:currentValue: with a nil currentValue. | ||||
| + (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding; | ||||
|  | ||||
| /// The main factory method for making argument input view subclasses that are the best fit for the type. | ||||
| + (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue; | ||||
|  | ||||
| /// A way to check if we should try editing a filed given its type encoding and value. | ||||
| /// Useful when deciding whether to edit or explore a property, ivar, or NSUserDefaults value. | ||||
| + (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue; | ||||
|  | ||||
| @end | ||||
							
								
								
									
										70
									
								
								Tweaks/FLEX/Editing/FLEXArgumentInputViewFactory.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								Tweaks/FLEX/Editing/FLEXArgumentInputViewFactory.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| // | ||||
| //  FLEXArgumentInputViewFactory.m | ||||
| //  FLEXInjected | ||||
| // | ||||
| //  Created by Ryan Olson on 6/15/14. | ||||
| // | ||||
| // | ||||
|  | ||||
| #import "FLEXArgumentInputViewFactory.h" | ||||
| #import "FLEXArgumentInputView.h" | ||||
| #import "FLEXArgumentInputObjectView.h" | ||||
| #import "FLEXArgumentInputNumberView.h" | ||||
| #import "FLEXArgumentInputSwitchView.h" | ||||
| #import "FLEXArgumentInputStructView.h" | ||||
| #import "FLEXArgumentInputNotSupportedView.h" | ||||
| #import "FLEXArgumentInputStringView.h" | ||||
| #import "FLEXArgumentInputFontView.h" | ||||
| #import "FLEXArgumentInputColorView.h" | ||||
| #import "FLEXArgumentInputDateView.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
|  | ||||
| @implementation FLEXArgumentInputViewFactory | ||||
|  | ||||
| + (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding { | ||||
|     return [self argumentInputViewForTypeEncoding:typeEncoding currentValue:nil]; | ||||
| } | ||||
|  | ||||
| + (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue { | ||||
|     Class subclass = [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:currentValue]; | ||||
|     if (!subclass) { | ||||
|         // Fall back to a FLEXArgumentInputNotSupportedView if we can't find a subclass that fits the type encoding. | ||||
|         // The unsupported view shows "nil" and does not allow user input. | ||||
|         subclass = [FLEXArgumentInputNotSupportedView class]; | ||||
|     } | ||||
|     // Remove the field name if there is any (e.g. \"width\"d -> d) | ||||
|     const NSUInteger fieldNameOffset = [FLEXRuntimeUtility fieldNameOffsetForTypeEncoding:typeEncoding]; | ||||
|     return [[subclass alloc] initWithArgumentTypeEncoding:typeEncoding + fieldNameOffset]; | ||||
| } | ||||
|  | ||||
| + (Class)argumentInputViewSubclassForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue { | ||||
|     // Remove the field name if there is any (e.g. \"width\"d -> d) | ||||
|     const NSUInteger fieldNameOffset = [FLEXRuntimeUtility fieldNameOffsetForTypeEncoding:typeEncoding]; | ||||
|     Class argumentInputViewSubclass = nil; | ||||
|     NSArray<Class> *inputViewClasses = @[[FLEXArgumentInputColorView class], | ||||
|                                          [FLEXArgumentInputFontView class], | ||||
|                                          [FLEXArgumentInputStringView class], | ||||
|                                          [FLEXArgumentInputStructView class], | ||||
|                                          [FLEXArgumentInputSwitchView class], | ||||
|                                          [FLEXArgumentInputDateView class], | ||||
|                                          [FLEXArgumentInputNumberView class], | ||||
|                                          [FLEXArgumentInputObjectView class]]; | ||||
|  | ||||
|     // Note that order is important here since multiple subclasses may support the same type. | ||||
|     // An example is the number subclass and the bool subclass for the type @encode(BOOL). | ||||
|     // Both work, but we'd prefer to use the bool subclass. | ||||
|     for (Class inputViewClass in inputViewClasses) { | ||||
|         if ([inputViewClass supportsObjCType:typeEncoding + fieldNameOffset withCurrentValue:currentValue]) { | ||||
|             argumentInputViewSubclass = inputViewClass; | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return argumentInputViewSubclass; | ||||
| } | ||||
|  | ||||
| + (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue { | ||||
|     return [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:currentValue] != nil; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										21
									
								
								Tweaks/FLEX/Editing/FLEXDefaultEditorViewController.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								Tweaks/FLEX/Editing/FLEXDefaultEditorViewController.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| // | ||||
| //  FLEXDefaultEditorViewController.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 5/23/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXFieldEditorViewController.h" | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FLEXDefaultEditorViewController : FLEXVariableEditorViewController | ||||
|  | ||||
| + (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)(void))onCommit; | ||||
|  | ||||
| + (BOOL)canEditDefaultWithValue:(nullable id)currentValue; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
							
								
								
									
										80
									
								
								Tweaks/FLEX/Editing/FLEXDefaultEditorViewController.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								Tweaks/FLEX/Editing/FLEXDefaultEditorViewController.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| // | ||||
| //  FLEXDefaultEditorViewController.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 5/23/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXDefaultEditorViewController.h" | ||||
| #import "FLEXFieldEditorView.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
| #import "FLEXArgumentInputView.h" | ||||
| #import "FLEXArgumentInputViewFactory.h" | ||||
|  | ||||
| @interface FLEXDefaultEditorViewController () | ||||
|  | ||||
| @property (nonatomic, readonly) NSUserDefaults *defaults; | ||||
| @property (nonatomic, readonly) NSString *key; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FLEXDefaultEditorViewController | ||||
|  | ||||
| + (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)(void))onCommit { | ||||
|     FLEXDefaultEditorViewController *editor = [self target:defaults data:key commitHandler:onCommit]; | ||||
|     editor.title = @"Edit Default"; | ||||
|     return editor; | ||||
| } | ||||
|  | ||||
| - (NSUserDefaults *)defaults { | ||||
|     return [_target isKindOfClass:[NSUserDefaults class]] ? _target : nil; | ||||
| } | ||||
|  | ||||
| - (NSString *)key { | ||||
|     return _data; | ||||
| } | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|      | ||||
|     self.fieldEditorView.fieldDescription = self.key; | ||||
|  | ||||
|     id currentValue = [self.defaults objectForKey:self.key]; | ||||
|     FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory | ||||
|         argumentInputViewForTypeEncoding:FLEXEncodeObject(currentValue) | ||||
|         currentValue:currentValue | ||||
|     ]; | ||||
|     inputView.backgroundColor = self.view.backgroundColor; | ||||
|     inputView.inputValue = currentValue; | ||||
|     self.fieldEditorView.argumentInputViews = @[inputView]; | ||||
| } | ||||
|  | ||||
| - (void)actionButtonPressed:(id)sender { | ||||
|     id value = self.firstInputView.inputValue; | ||||
|     if (value) { | ||||
|         [self.defaults setObject:value forKey:self.key]; | ||||
|     } else { | ||||
|         [self.defaults removeObjectForKey:self.key]; | ||||
|     } | ||||
|     [self.defaults synchronize]; | ||||
|      | ||||
|     // Dismiss keyboard and handle committed changes | ||||
|     [super actionButtonPressed:sender]; | ||||
|      | ||||
|     // Go back after setting, but not for switches. | ||||
|     if (sender) { | ||||
|         [self.navigationController popViewControllerAnimated:YES]; | ||||
|     } else { | ||||
|         self.firstInputView.inputValue = [self.defaults objectForKey:self.key]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| + (BOOL)canEditDefaultWithValue:(id)currentValue { | ||||
|     return [FLEXArgumentInputViewFactory | ||||
|         canEditFieldWithTypeEncoding:FLEXEncodeObject(currentValue) | ||||
|         currentValue:currentValue | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										20
									
								
								Tweaks/FLEX/Editing/FLEXFieldEditorView.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								Tweaks/FLEX/Editing/FLEXFieldEditorView.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| // | ||||
| //  FLEXFieldEditorView.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 5/16/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| @class FLEXArgumentInputView; | ||||
|  | ||||
| @interface FLEXFieldEditorView : UIView | ||||
|  | ||||
| @property (nonatomic, copy) NSString *targetDescription; | ||||
| @property (nonatomic, copy) NSString *fieldDescription; | ||||
|  | ||||
| @property (nonatomic, copy) NSArray<FLEXArgumentInputView *> *argumentInputViews; | ||||
|  | ||||
| @end | ||||
							
								
								
									
										172
									
								
								Tweaks/FLEX/Editing/FLEXFieldEditorView.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								Tweaks/FLEX/Editing/FLEXFieldEditorView.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | ||||
| // | ||||
| //  FLEXFieldEditorView.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 5/16/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXFieldEditorView.h" | ||||
| #import "FLEXArgumentInputView.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXColor.h" | ||||
|  | ||||
| @interface FLEXFieldEditorView () | ||||
|  | ||||
| @property (nonatomic) UILabel *targetDescriptionLabel; | ||||
| @property (nonatomic) UIView *targetDescriptionDivider; | ||||
| @property (nonatomic) UILabel *fieldDescriptionLabel; | ||||
| @property (nonatomic) UIView *fieldDescriptionDivider; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FLEXFieldEditorView | ||||
|  | ||||
| - (id)initWithFrame:(CGRect)frame { | ||||
|     self = [super initWithFrame:frame]; | ||||
|     if (self) { | ||||
|         self.targetDescriptionLabel = [UILabel new]; | ||||
|         self.targetDescriptionLabel.numberOfLines = 0; | ||||
|         self.targetDescriptionLabel.font = [[self class] labelFont]; | ||||
|         [self addSubview:self.targetDescriptionLabel]; | ||||
|          | ||||
|         self.targetDescriptionDivider = [[self class] dividerView]; | ||||
|         [self addSubview:self.targetDescriptionDivider]; | ||||
|          | ||||
|         self.fieldDescriptionLabel = [UILabel new]; | ||||
|         self.fieldDescriptionLabel.numberOfLines = 0; | ||||
|         self.fieldDescriptionLabel.font = [[self class] labelFont]; | ||||
|         [self addSubview:self.fieldDescriptionLabel]; | ||||
|          | ||||
|         self.fieldDescriptionDivider = [[self class] dividerView]; | ||||
|         [self addSubview:self.fieldDescriptionDivider]; | ||||
|     } | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)layoutSubviews { | ||||
|     [super layoutSubviews]; | ||||
|      | ||||
|     CGFloat horizontalPadding = [[self class] horizontalPadding]; | ||||
|     CGFloat verticalPadding = [[self class] verticalPadding]; | ||||
|     CGFloat dividerLineHeight = [[self class] dividerLineHeight]; | ||||
|      | ||||
|     CGFloat originY = verticalPadding; | ||||
|     CGFloat originX = horizontalPadding; | ||||
|     CGFloat contentWidth = self.bounds.size.width - 2.0 * horizontalPadding; | ||||
|     CGSize constrainSize = CGSizeMake(contentWidth, CGFLOAT_MAX); | ||||
|      | ||||
|     CGSize instanceDescriptionSize = [self.targetDescriptionLabel sizeThatFits:constrainSize]; | ||||
|     self.targetDescriptionLabel.frame = CGRectMake(originX, originY, instanceDescriptionSize.width, instanceDescriptionSize.height); | ||||
|     originY = CGRectGetMaxY(self.targetDescriptionLabel.frame) + verticalPadding; | ||||
|      | ||||
|     self.targetDescriptionDivider.frame = CGRectMake(originX, originY, contentWidth, dividerLineHeight); | ||||
|     originY = CGRectGetMaxY(self.targetDescriptionDivider.frame) + verticalPadding; | ||||
|      | ||||
|     CGSize fieldDescriptionSize = [self.fieldDescriptionLabel sizeThatFits:constrainSize]; | ||||
|     self.fieldDescriptionLabel.frame = CGRectMake(originX, originY, fieldDescriptionSize.width, fieldDescriptionSize.height); | ||||
|     originY = CGRectGetMaxY(self.fieldDescriptionLabel.frame) + verticalPadding; | ||||
|      | ||||
|     self.fieldDescriptionDivider.frame = CGRectMake(originX, originY, contentWidth, dividerLineHeight); | ||||
|     originY = CGRectGetMaxY(self.fieldDescriptionDivider.frame) + verticalPadding; | ||||
|  | ||||
|     for (UIView *argumentInputView in self.argumentInputViews) { | ||||
|         CGSize inputViewSize = [argumentInputView sizeThatFits:constrainSize]; | ||||
|         argumentInputView.frame = CGRectMake(originX, originY, inputViewSize.width, inputViewSize.height); | ||||
|         originY = CGRectGetMaxY(argumentInputView.frame) + verticalPadding; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)setBackgroundColor:(UIColor *)backgroundColor { | ||||
|     [super setBackgroundColor:backgroundColor]; | ||||
|     self.targetDescriptionLabel.backgroundColor = backgroundColor; | ||||
|     self.fieldDescriptionLabel.backgroundColor = backgroundColor; | ||||
| } | ||||
|  | ||||
| - (void)setTargetDescription:(NSString *)targetDescription { | ||||
|     if (![_targetDescription isEqual:targetDescription]) { | ||||
|         _targetDescription = targetDescription; | ||||
|         self.targetDescriptionLabel.text = targetDescription; | ||||
|         [self setNeedsLayout]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)setFieldDescription:(NSString *)fieldDescription { | ||||
|     if (![_fieldDescription isEqual:fieldDescription]) { | ||||
|         _fieldDescription = fieldDescription; | ||||
|         self.fieldDescriptionLabel.text = fieldDescription; | ||||
|         [self setNeedsLayout]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)setArgumentInputViews:(NSArray<FLEXArgumentInputView *> *)argumentInputViews { | ||||
|     if (![_argumentInputViews isEqual:argumentInputViews]) { | ||||
|          | ||||
|         for (FLEXArgumentInputView *inputView in _argumentInputViews) { | ||||
|             [inputView removeFromSuperview]; | ||||
|         } | ||||
|          | ||||
|         _argumentInputViews = argumentInputViews; | ||||
|          | ||||
|         for (FLEXArgumentInputView *newInputView in argumentInputViews) { | ||||
|             [self addSubview:newInputView]; | ||||
|         } | ||||
|          | ||||
|         [self setNeedsLayout]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| + (UIView *)dividerView { | ||||
|     UIView *dividerView = [UIView new]; | ||||
|     dividerView.backgroundColor = [self dividerColor]; | ||||
|     return dividerView; | ||||
| } | ||||
|  | ||||
| + (UIColor *)dividerColor { | ||||
|     return FLEXColor.tertiaryBackgroundColor; | ||||
| } | ||||
|  | ||||
| + (CGFloat)horizontalPadding { | ||||
|     return 10.0; | ||||
| } | ||||
|  | ||||
| + (CGFloat)verticalPadding { | ||||
|     return 20.0; | ||||
| } | ||||
|  | ||||
| + (UIFont *)labelFont { | ||||
|     return [UIFont systemFontOfSize:14.0]; | ||||
| } | ||||
|  | ||||
| + (CGFloat)dividerLineHeight { | ||||
|     return 1.0; | ||||
| } | ||||
|  | ||||
| - (CGSize)sizeThatFits:(CGSize)size { | ||||
|     CGFloat horizontalPadding = [[self class] horizontalPadding]; | ||||
|     CGFloat verticalPadding = [[self class] verticalPadding]; | ||||
|     CGFloat dividerLineHeight = [[self class] dividerLineHeight]; | ||||
|      | ||||
|     CGFloat height = 0; | ||||
|     CGFloat availableWidth = size.width - 2.0 * horizontalPadding; | ||||
|     CGSize constrainSize = CGSizeMake(availableWidth, CGFLOAT_MAX); | ||||
|      | ||||
|     height += verticalPadding; | ||||
|     height += ceil([self.targetDescriptionLabel sizeThatFits:constrainSize].height); | ||||
|     height += verticalPadding; | ||||
|     height += dividerLineHeight; | ||||
|     height += verticalPadding; | ||||
|     height += ceil([self.fieldDescriptionLabel sizeThatFits:constrainSize].height); | ||||
|     height += verticalPadding; | ||||
|     height += dividerLineHeight; | ||||
|     height += verticalPadding; | ||||
|      | ||||
|     for (FLEXArgumentInputView *inputView in self.argumentInputViews) { | ||||
|         height += [inputView sizeThatFits:constrainSize].height; | ||||
|         height += verticalPadding; | ||||
|     } | ||||
|      | ||||
|     return CGSizeMake(size.width, height); | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										29
									
								
								Tweaks/FLEX/Editing/FLEXFieldEditorViewController.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								Tweaks/FLEX/Editing/FLEXFieldEditorViewController.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| // | ||||
| //  FLEXFieldEditorViewController.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 11/22/18. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXVariableEditorViewController.h" | ||||
| #import "FLEXProperty.h" | ||||
| #import "FLEXIvar.h" | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FLEXFieldEditorViewController : FLEXVariableEditorViewController | ||||
|  | ||||
| /// @return nil if the property is readonly or if the type is unsupported | ||||
| + (nullable instancetype)target:(id)target property:(FLEXProperty *)property commitHandler:(void(^_Nullable)(void))onCommit; | ||||
| /// @return nil if the ivar type is unsupported | ||||
| + (nullable instancetype)target:(id)target ivar:(FLEXIvar *)ivar commitHandler:(void(^_Nullable)(void))onCommit; | ||||
|  | ||||
| /// Subclasses can change the button title via the \c title property | ||||
| @property (nonatomic, readonly) UIBarButtonItem *getterButton; | ||||
|  | ||||
| - (void)getterButtonPressed:(id)sender; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
							
								
								
									
										149
									
								
								Tweaks/FLEX/Editing/FLEXFieldEditorViewController.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								Tweaks/FLEX/Editing/FLEXFieldEditorViewController.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | ||||
| // | ||||
| //  FLEXFieldEditorViewController.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 11/22/18. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXFieldEditorViewController.h" | ||||
| #import "FLEXFieldEditorView.h" | ||||
| #import "FLEXArgumentInputViewFactory.h" | ||||
| #import "FLEXPropertyAttributes.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXColor.h" | ||||
| #import "UIBarButtonItem+FLEX.h" | ||||
|  | ||||
| @interface FLEXFieldEditorViewController () <FLEXArgumentInputViewDelegate> | ||||
|  | ||||
| @property (nonatomic) FLEXProperty *property; | ||||
| @property (nonatomic) FLEXIvar *ivar; | ||||
|  | ||||
| @property (nonatomic, readonly) id currentValue; | ||||
| @property (nonatomic, readonly) const FLEXTypeEncoding *typeEncoding; | ||||
| @property (nonatomic, readonly) NSString *fieldDescription; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FLEXFieldEditorViewController | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| + (instancetype)target:(id)target property:(nonnull FLEXProperty *)property commitHandler:(void(^_Nullable)(void))onCommit { | ||||
|     FLEXFieldEditorViewController *editor = [self target:target data:property commitHandler:onCommit]; | ||||
|     editor.title = [@"Property: " stringByAppendingString:property.name]; | ||||
|     editor.property = property; | ||||
|     return editor; | ||||
| } | ||||
|  | ||||
| + (instancetype)target:(id)target ivar:(nonnull FLEXIvar *)ivar commitHandler:(void(^_Nullable)(void))onCommit { | ||||
|     FLEXFieldEditorViewController *editor = [self target:target data:ivar commitHandler:onCommit]; | ||||
|     editor.title = [@"Ivar: " stringByAppendingString:ivar.name]; | ||||
|     editor.ivar = ivar; | ||||
|     return editor; | ||||
| } | ||||
|  | ||||
| #pragma mark - Overrides | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|  | ||||
|     self.view.backgroundColor = FLEXColor.groupedBackgroundColor; | ||||
|  | ||||
|     // Create getter button | ||||
|     _getterButton = [[UIBarButtonItem alloc] | ||||
|         initWithTitle:@"Get" | ||||
|         style:UIBarButtonItemStyleDone | ||||
|         target:self | ||||
|         action:@selector(getterButtonPressed:) | ||||
|     ]; | ||||
|     self.toolbarItems = @[ | ||||
|         UIBarButtonItem.flex_flexibleSpace, self.getterButton, self.actionButton | ||||
|     ]; | ||||
|  | ||||
|     // Configure input view | ||||
|     self.fieldEditorView.fieldDescription = self.fieldDescription; | ||||
|     FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:self.typeEncoding]; | ||||
|     inputView.inputValue = self.currentValue; | ||||
|     inputView.delegate = self; | ||||
|     self.fieldEditorView.argumentInputViews = @[inputView]; | ||||
|  | ||||
|     // Don't show a "set" button for switches; we mutate when the switch is flipped | ||||
|     if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) { | ||||
|         self.actionButton.enabled = NO; | ||||
|         self.actionButton.title = @"Flip the switch to call the setter"; | ||||
|         // Put getter button before setter button  | ||||
|         self.toolbarItems = @[ | ||||
|             UIBarButtonItem.flex_flexibleSpace, self.actionButton, self.getterButton | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)actionButtonPressed:(id)sender { | ||||
|     if (self.property) { | ||||
|         id userInputObject = self.firstInputView.inputValue; | ||||
|         NSArray *arguments = userInputObject ? @[userInputObject] : nil; | ||||
|         SEL setterSelector = self.property.likelySetter; | ||||
|         NSError *error = nil; | ||||
|         [FLEXRuntimeUtility performSelector:setterSelector onObject:self.target withArguments:arguments error:&error]; | ||||
|         if (error) { | ||||
|             [FLEXAlert showAlert:@"Property Setter Failed" message:error.localizedDescription from:self]; | ||||
|             sender = nil; // Don't pop back | ||||
|         } | ||||
|     } else { | ||||
|         // TODO: check mutability and use mutableCopy if necessary; | ||||
|         // this currently could and would assign NSArray to NSMutableArray | ||||
|         [self.ivar setValue:self.firstInputView.inputValue onObject:self.target]; | ||||
|     } | ||||
|      | ||||
|     // Dismiss keyboard and handle committed changes | ||||
|     [super actionButtonPressed:sender]; | ||||
|  | ||||
|     // Go back after setting, but not for switches. | ||||
|     if (sender) { | ||||
|         [self.navigationController popViewControllerAnimated:YES]; | ||||
|     } else { | ||||
|         self.firstInputView.inputValue = self.currentValue; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)getterButtonPressed:(id)sender { | ||||
|     [self.fieldEditorView endEditing:YES]; | ||||
|  | ||||
|     [self exploreObjectOrPopViewController:self.currentValue]; | ||||
| } | ||||
|  | ||||
| - (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView { | ||||
|     if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) { | ||||
|         [self actionButtonPressed:nil]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| #pragma mark - Private | ||||
|  | ||||
| - (id)currentValue { | ||||
|     if (self.property) { | ||||
|         return [self.property getValue:self.target]; | ||||
|     } else { | ||||
|         return [self.ivar getValue:self.target]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (const FLEXTypeEncoding *)typeEncoding { | ||||
|     if (self.property) { | ||||
|         return self.property.attributes.typeEncoding.UTF8String; | ||||
|     } else { | ||||
|         return self.ivar.typeEncoding.UTF8String; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (NSString *)fieldDescription { | ||||
|     if (self.property) { | ||||
|         return self.property.fullDescription; | ||||
|     } else { | ||||
|         return self.ivar.description; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										16
									
								
								Tweaks/FLEX/Editing/FLEXMethodCallingViewController.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								Tweaks/FLEX/Editing/FLEXMethodCallingViewController.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| // | ||||
| //  FLEXMethodCallingViewController.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 5/23/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXVariableEditorViewController.h" | ||||
| #import "FLEXMethod.h" | ||||
|  | ||||
| @interface FLEXMethodCallingViewController : FLEXVariableEditorViewController | ||||
|  | ||||
| + (instancetype)target:(id)target method:(FLEXMethod *)method; | ||||
|  | ||||
| @end | ||||
							
								
								
									
										110
									
								
								Tweaks/FLEX/Editing/FLEXMethodCallingViewController.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								Tweaks/FLEX/Editing/FLEXMethodCallingViewController.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| // | ||||
| //  FLEXMethodCallingViewController.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 5/23/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXMethodCallingViewController.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
| #import "FLEXFieldEditorView.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
| #import "FLEXObjectExplorerViewController.h" | ||||
| #import "FLEXArgumentInputView.h" | ||||
| #import "FLEXArgumentInputViewFactory.h" | ||||
| #import "FLEXUtility.h" | ||||
|  | ||||
| @interface FLEXMethodCallingViewController () | ||||
| @property (nonatomic, readonly) FLEXMethod *method; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXMethodCallingViewController | ||||
|  | ||||
| + (instancetype)target:(id)target method:(FLEXMethod *)method { | ||||
|     return [[self alloc] initWithTarget:target method:method]; | ||||
| } | ||||
|  | ||||
| - (id)initWithTarget:(id)target method:(FLEXMethod *)method { | ||||
|     NSParameterAssert(method.isInstanceMethod == !object_isClass(target)); | ||||
|  | ||||
|     self = [super initWithTarget:target data:method commitHandler:nil]; | ||||
|     if (self) { | ||||
|         self.title = method.isInstanceMethod ? @"Method: " : @"Class Method: "; | ||||
|         self.title = [self.title stringByAppendingString:method.selectorString]; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|  | ||||
|     self.actionButton.title = @"Call"; | ||||
|  | ||||
|     // Configure field editor view | ||||
|     self.fieldEditorView.argumentInputViews = [self argumentInputViews]; | ||||
|     self.fieldEditorView.fieldDescription = [NSString stringWithFormat: | ||||
|         @"Signature:\n%@\n\nReturn Type:\n%s", | ||||
|         self.method.description, (char *)self.method.returnType | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| - (NSArray<FLEXArgumentInputView *> *)argumentInputViews { | ||||
|     Method method = self.method.objc_method; | ||||
|     NSArray *methodComponents = [FLEXRuntimeUtility prettyArgumentComponentsForMethod:method]; | ||||
|     NSMutableArray<FLEXArgumentInputView *> *argumentInputViews = [NSMutableArray new]; | ||||
|     unsigned int argumentIndex = kFLEXNumberOfImplicitArgs; | ||||
|  | ||||
|     for (NSString *methodComponent in methodComponents) { | ||||
|         char *argumentTypeEncoding = method_copyArgumentType(method, argumentIndex); | ||||
|         FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:argumentTypeEncoding]; | ||||
|         free(argumentTypeEncoding); | ||||
|  | ||||
|         inputView.backgroundColor = self.view.backgroundColor; | ||||
|         inputView.title = methodComponent; | ||||
|         [argumentInputViews addObject:inputView]; | ||||
|         argumentIndex++; | ||||
|     } | ||||
|  | ||||
|     return argumentInputViews; | ||||
| } | ||||
|  | ||||
| - (void)actionButtonPressed:(id)sender { | ||||
|     // Gather arguments | ||||
|     NSMutableArray *arguments = [NSMutableArray new]; | ||||
|     for (FLEXArgumentInputView *inputView in self.fieldEditorView.argumentInputViews) { | ||||
|         // Use NSNull as a nil placeholder; it will be interpreted as nil | ||||
|         [arguments addObject:inputView.inputValue ?: NSNull.null]; | ||||
|     } | ||||
|  | ||||
|     // Call method | ||||
|     NSError *error = nil; | ||||
|     id returnValue = [FLEXRuntimeUtility | ||||
|         performSelector:self.method.selector | ||||
|         onObject:self.target | ||||
|         withArguments:arguments | ||||
|         error:&error | ||||
|     ]; | ||||
|      | ||||
|     // Dismiss keyboard and handle committed changes | ||||
|     [super actionButtonPressed:sender]; | ||||
|  | ||||
|     // Display return value or error | ||||
|     if (error) { | ||||
|         [FLEXAlert showAlert:@"Method Call Failed" message:error.localizedDescription from:self]; | ||||
|     } else if (returnValue) { | ||||
|         // For non-nil (or void) return types, push an explorer view controller to display the returned object | ||||
|         returnValue = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:returnValue type:self.method.returnType]; | ||||
|         FLEXObjectExplorerViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:returnValue]; | ||||
|         [self.navigationController pushViewController:explorer animated:YES]; | ||||
|     } else { | ||||
|         [self exploreObjectOrPopViewController:returnValue]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (FLEXMethod *)method { | ||||
|     return _data; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										55
									
								
								Tweaks/FLEX/Editing/FLEXVariableEditorViewController.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								Tweaks/FLEX/Editing/FLEXVariableEditorViewController.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| // | ||||
| //  FLEXVariableEditorViewController.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 5/16/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| @class FLEXFieldEditorView; | ||||
| @class FLEXArgumentInputView; | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| /// An abstract screen for editing or configuring one or more variables. | ||||
| /// "Target" is the target of the edit operation, and "data" is the data | ||||
| /// you want to mutate or pass to the target when the action is performed. | ||||
| /// The action may be something like calling a method, setting an ivar, etc. | ||||
| @interface FLEXVariableEditorViewController : UIViewController { | ||||
|     @protected | ||||
|     id _target; | ||||
|     _Nullable id _data; | ||||
|     void (^_Nullable _commitHandler)(void); | ||||
| } | ||||
|  | ||||
| /// @param target The target of the operation | ||||
| /// @param data The data associated with the operation | ||||
| /// @param onCommit An action to perform when the data changes  | ||||
| + (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit; | ||||
| /// @param target The target of the operation | ||||
| /// @param data The data associated with the operation | ||||
| /// @param onCommit An action to perform when the data changes  | ||||
| - (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit; | ||||
|  | ||||
| @property (nonatomic, readonly) id target; | ||||
|  | ||||
| /// Convenience accessor since many subclasses only use one input view | ||||
| @property (nonatomic, readonly, nullable) FLEXArgumentInputView *firstInputView; | ||||
|  | ||||
| @property (nonatomic, readonly) FLEXFieldEditorView *fieldEditorView; | ||||
| /// Subclasses can change the button title via the button's \c title property | ||||
| @property (nonatomic, readonly) UIBarButtonItem *actionButton; | ||||
|  | ||||
| /// Subclasses should override to provide "set" functionality. | ||||
| /// The commit handler--if present--is called here. | ||||
| - (void)actionButtonPressed:(nullable id)sender; | ||||
|  | ||||
| /// Pushes an explorer view controller for the given object | ||||
| /// or pops the current view controller. | ||||
| - (void)exploreObjectOrPopViewController:(nullable id)objectOrNil; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
							
								
								
									
										141
									
								
								Tweaks/FLEX/Editing/FLEXVariableEditorViewController.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								Tweaks/FLEX/Editing/FLEXVariableEditorViewController.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | ||||
| // | ||||
| //  FLEXVariableEditorViewController.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 5/16/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXColor.h" | ||||
| #import "FLEXVariableEditorViewController.h" | ||||
| #import "FLEXFieldEditorView.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
| #import "FLEXArgumentInputView.h" | ||||
| #import "FLEXArgumentInputViewFactory.h" | ||||
| #import "FLEXObjectExplorerViewController.h" | ||||
| #import "UIBarButtonItem+FLEX.h" | ||||
|  | ||||
| @interface FLEXVariableEditorViewController () <UIScrollViewDelegate> | ||||
| @property (nonatomic) UIScrollView *scrollView; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXVariableEditorViewController | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| + (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit { | ||||
|     return [[self alloc] initWithTarget:target data:data commitHandler:onCommit]; | ||||
| } | ||||
|  | ||||
| - (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit { | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         _target = target; | ||||
|         _data = data; | ||||
|         _commitHandler = onCommit; | ||||
|         [NSNotificationCenter.defaultCenter | ||||
|             addObserver:self selector:@selector(keyboardDidShow:) | ||||
|             name:UIKeyboardDidShowNotification object:nil | ||||
|         ]; | ||||
|         [NSNotificationCenter.defaultCenter | ||||
|             addObserver:self selector:@selector(keyboardWillHide:) | ||||
|             name:UIKeyboardWillHideNotification object:nil | ||||
|         ]; | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)dealloc { | ||||
|     [NSNotificationCenter.defaultCenter removeObserver:self]; | ||||
| } | ||||
|  | ||||
| #pragma mark - UIViewController methods | ||||
|  | ||||
| - (void)keyboardDidShow:(NSNotification *)notification { | ||||
|     CGRect keyboardRectInWindow = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; | ||||
|     CGSize keyboardSize = [self.view convertRect:keyboardRectInWindow fromView:nil].size; | ||||
|     UIEdgeInsets scrollInsets = self.scrollView.contentInset; | ||||
|     scrollInsets.bottom = keyboardSize.height; | ||||
|     self.scrollView.contentInset = scrollInsets; | ||||
|     self.scrollView.scrollIndicatorInsets = scrollInsets; | ||||
|      | ||||
|     // Find the active input view and scroll to make sure it's visible. | ||||
|     for (FLEXArgumentInputView *argumentInputView in self.fieldEditorView.argumentInputViews) { | ||||
|         if (argumentInputView.inputViewIsFirstResponder) { | ||||
|             CGRect scrollToVisibleRect = [self.scrollView convertRect:argumentInputView.bounds fromView:argumentInputView]; | ||||
|             [self.scrollView scrollRectToVisible:scrollToVisibleRect animated:YES]; | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)keyboardWillHide:(NSNotification *)notification { | ||||
|     UIEdgeInsets scrollInsets = self.scrollView.contentInset; | ||||
|     scrollInsets.bottom = 0.0; | ||||
|     self.scrollView.contentInset = scrollInsets; | ||||
|     self.scrollView.scrollIndicatorInsets = scrollInsets; | ||||
| } | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|      | ||||
|     self.view.backgroundColor = FLEXColor.scrollViewBackgroundColor; | ||||
|      | ||||
|     self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; | ||||
|     self.scrollView.backgroundColor = self.view.backgroundColor; | ||||
|     self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; | ||||
|     self.scrollView.delegate = self; | ||||
|     [self.view addSubview:self.scrollView]; | ||||
|      | ||||
|     _fieldEditorView = [FLEXFieldEditorView new]; | ||||
|     self.fieldEditorView.targetDescription = [NSString stringWithFormat:@"%@ %p", [self.target class], self.target]; | ||||
|     [self.scrollView addSubview:self.fieldEditorView]; | ||||
|      | ||||
|     _actionButton = [[UIBarButtonItem alloc] | ||||
|         initWithTitle:@"Set" | ||||
|         style:UIBarButtonItemStyleDone | ||||
|         target:self | ||||
|         action:@selector(actionButtonPressed:) | ||||
|     ]; | ||||
|      | ||||
|     self.navigationController.toolbarHidden = NO; | ||||
|     self.toolbarItems = @[UIBarButtonItem.flex_flexibleSpace, self.actionButton]; | ||||
| } | ||||
|  | ||||
| - (void)viewWillLayoutSubviews { | ||||
|     CGSize constrainSize = CGSizeMake(self.scrollView.bounds.size.width, CGFLOAT_MAX); | ||||
|     CGSize fieldEditorSize = [self.fieldEditorView sizeThatFits:constrainSize]; | ||||
|     self.fieldEditorView.frame = CGRectMake(0, 0, fieldEditorSize.width, fieldEditorSize.height); | ||||
|     self.scrollView.contentSize = fieldEditorSize; | ||||
| } | ||||
|  | ||||
| #pragma mark - Public | ||||
|  | ||||
| - (FLEXArgumentInputView *)firstInputView { | ||||
|     return [self.fieldEditorView argumentInputViews].firstObject; | ||||
| } | ||||
|  | ||||
| - (void)actionButtonPressed:(id)sender { | ||||
|     // Subclasses can override | ||||
|     [self.fieldEditorView endEditing:YES]; | ||||
|     if (_commitHandler) { | ||||
|         _commitHandler(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)exploreObjectOrPopViewController:(id)objectOrNil { | ||||
|     if (objectOrNil) { | ||||
|         // For non-nil (or void) return types, push an explorer view controller to display the object | ||||
|         FLEXObjectExplorerViewController *explorerViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:objectOrNil]; | ||||
|         [self.navigationController pushViewController:explorerViewController animated:YES]; | ||||
|     } else { | ||||
|         // If we didn't get a returned object but the method call succeeded, | ||||
|         // pop this view controller off the stack to indicate that the call went through. | ||||
|         [self.navigationController popViewControllerAnimated:YES]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,19 @@ | ||||
| // | ||||
| //  FLEXBookmarkManager.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 2/6/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <Foundation/Foundation.h> | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FLEXBookmarkManager : NSObject | ||||
|  | ||||
| @property (nonatomic, readonly, class) NSMutableArray *bookmarks; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
| @@ -0,0 +1,25 @@ | ||||
| // | ||||
| //  FLEXBookmarkManager.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 2/6/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXBookmarkManager.h" | ||||
|  | ||||
| static NSMutableArray *kFLEXBookmarkManagerBookmarks = nil; | ||||
|  | ||||
| @implementation FLEXBookmarkManager | ||||
|  | ||||
| + (void)initialize { | ||||
|     if (self == [FLEXBookmarkManager class]) { | ||||
|         kFLEXBookmarkManagerBookmarks = [NSMutableArray new]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| + (NSMutableArray *)bookmarks { | ||||
|     return kFLEXBookmarkManagerBookmarks; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,17 @@ | ||||
| // | ||||
| //  FLEXBookmarksViewController.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 2/6/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableViewController.h" | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FLEXBookmarksViewController : FLEXTableViewController | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
| @@ -0,0 +1,235 @@ | ||||
| // | ||||
| //  FLEXBookmarksViewController.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 2/6/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXBookmarksViewController.h" | ||||
| #import "FLEXExplorerViewController.h" | ||||
| #import "FLEXNavigationController.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
| #import "FLEXBookmarkManager.h" | ||||
| #import "UIBarButtonItem+FLEX.h" | ||||
| #import "FLEXColor.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
| #import "FLEXTableView.h" | ||||
|  | ||||
| @interface FLEXBookmarksViewController () | ||||
| @property (nonatomic, copy) NSArray *bookmarks; | ||||
| @property (nonatomic, readonly) FLEXExplorerViewController *corePresenter; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXBookmarksViewController | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| - (id)init { | ||||
|     return [self initWithStyle:UITableViewStylePlain]; | ||||
| } | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|      | ||||
|     self.navigationController.hidesBarsOnSwipe = NO; | ||||
|     self.tableView.allowsMultipleSelectionDuringEditing = YES; | ||||
|      | ||||
|     [self reloadData]; | ||||
| } | ||||
|  | ||||
| - (void)viewWillAppear:(BOOL)animated { | ||||
|     [super viewWillAppear:animated]; | ||||
|     [self setupDefaultBarItems]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Private | ||||
|  | ||||
| - (void)reloadData { | ||||
|     // We assume the bookmarks aren't going to change out from under us, since | ||||
|     // presenting any other tool via keyboard shortcuts should dismiss us first | ||||
|     self.bookmarks = FLEXBookmarkManager.bookmarks; | ||||
|     self.title = [NSString stringWithFormat:@"Bookmarks (%@)", @(self.bookmarks.count)]; | ||||
| } | ||||
|  | ||||
| - (void)setupDefaultBarItems { | ||||
|     self.navigationItem.rightBarButtonItem = FLEXBarButtonItemSystem(Done, self, @selector(dismissAnimated)); | ||||
|     self.toolbarItems = @[ | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         FLEXBarButtonItemSystem(Edit, self, @selector(toggleEditing)), | ||||
|     ]; | ||||
|      | ||||
|     // Disable editing if no bookmarks available | ||||
|     self.toolbarItems.lastObject.enabled = self.bookmarks.count > 0; | ||||
| } | ||||
|  | ||||
| - (void)setupEditingBarItems { | ||||
|     self.navigationItem.rightBarButtonItem = nil; | ||||
|     self.toolbarItems = @[ | ||||
|         [UIBarButtonItem flex_itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed:)], | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         // We use a non-system done item because we change its title dynamically | ||||
|         [UIBarButtonItem flex_doneStyleitemWithTitle:@"Done" target:self action:@selector(toggleEditing)] | ||||
|     ]; | ||||
|      | ||||
|     self.toolbarItems.firstObject.tintColor = FLEXColor.destructiveColor; | ||||
| } | ||||
|  | ||||
| - (FLEXExplorerViewController *)corePresenter { | ||||
|     // We must be presented by a FLEXExplorerViewController, or presented | ||||
|     // by another view controller that was presented by FLEXExplorerViewController | ||||
|     FLEXExplorerViewController *presenter = (id)self.presentingViewController; | ||||
|     presenter = (id)presenter.presentingViewController ?: presenter; | ||||
|     presenter = (id)presenter.presentingViewController ?: presenter; | ||||
|     NSAssert( | ||||
|         [presenter isKindOfClass:[FLEXExplorerViewController class]], | ||||
|         @"The bookmarks view controller expects to be presented by the explorer controller" | ||||
|     ); | ||||
|     return presenter; | ||||
| } | ||||
|  | ||||
| #pragma mark Button Actions | ||||
|  | ||||
| - (void)dismissAnimated { | ||||
|     [self dismissAnimated:nil]; | ||||
| } | ||||
|  | ||||
| - (void)dismissAnimated:(id)selectedObject { | ||||
|     if (selectedObject) { | ||||
|         UIViewController *explorer = [FLEXObjectExplorerFactory | ||||
|             explorerViewControllerForObject:selectedObject | ||||
|         ]; | ||||
|         if ([self.presentingViewController isKindOfClass:[FLEXNavigationController class]]) { | ||||
|             // I am presented on an existing navigation stack, so | ||||
|             // dismiss myself and push the bookmark there | ||||
|             UINavigationController *presenter = (id)self.presentingViewController; | ||||
|             [presenter dismissViewControllerAnimated:YES completion:^{ | ||||
|                 [presenter pushViewController:explorer animated:YES]; | ||||
|             }]; | ||||
|         } else { | ||||
|             // Dismiss myself and present explorer | ||||
|             UIViewController *presenter = self.corePresenter; | ||||
|             [presenter dismissViewControllerAnimated:YES completion:^{ | ||||
|                 [presenter presentViewController:[FLEXNavigationController | ||||
|                     withRootViewController:explorer | ||||
|                 ] animated:YES completion:nil]; | ||||
|             }]; | ||||
|         } | ||||
|     } else { | ||||
|         // Just dismiss myself | ||||
|         [self dismissViewControllerAnimated:YES completion:nil]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)toggleEditing { | ||||
|     NSArray<NSIndexPath *> *selected = self.tableView.indexPathsForSelectedRows; | ||||
|     self.editing = !self.editing; | ||||
|      | ||||
|     if (self.isEditing) { | ||||
|         [self setupEditingBarItems]; | ||||
|     } else { | ||||
|         [self setupDefaultBarItems]; | ||||
|          | ||||
|         // Get index set of bookmarks to close | ||||
|         NSMutableIndexSet *indexes = [NSMutableIndexSet new]; | ||||
|         for (NSIndexPath *ip in selected) { | ||||
|             [indexes addIndex:ip.row]; | ||||
|         } | ||||
|          | ||||
|         if (selected.count) { | ||||
|             // Close bookmarks and update data source | ||||
|             [FLEXBookmarkManager.bookmarks removeObjectsAtIndexes:indexes]; | ||||
|             [self reloadData]; | ||||
|              | ||||
|             // Remove deleted rows | ||||
|             [self.tableView deleteRowsAtIndexPaths:selected withRowAnimation:UITableViewRowAnimationAutomatic]; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)closeAllButtonPressed:(UIBarButtonItem *)sender { | ||||
|     [FLEXAlert makeSheet:^(FLEXAlert *make) { | ||||
|         NSInteger count = self.bookmarks.count; | ||||
|         NSString *title = FLEXPluralFormatString(count, @"Remove %@ bookmarks", @"Remove %@ bookmark"); | ||||
|         make.button(title).destructiveStyle().handler(^(NSArray<NSString *> *strings) { | ||||
|             [self closeAll]; | ||||
|             [self toggleEditing]; | ||||
|         }); | ||||
|         make.button(@"Cancel").cancelStyle(); | ||||
|     } showFrom:self source:sender]; | ||||
| } | ||||
|  | ||||
| - (void)closeAll { | ||||
|     NSInteger rowCount = self.bookmarks.count; | ||||
|      | ||||
|     // Close bookmarks and update data source | ||||
|     [FLEXBookmarkManager.bookmarks removeAllObjects]; | ||||
|     [self reloadData]; | ||||
|      | ||||
|     // Delete rows from table view | ||||
|     NSArray<NSIndexPath *> *allRows = [NSArray flex_forEachUpTo:rowCount map:^id(NSUInteger row) { | ||||
|         return [NSIndexPath indexPathForRow:row inSection:0]; | ||||
|     }]; | ||||
|     [self.tableView deleteRowsAtIndexPaths:allRows withRowAnimation:UITableViewRowAnimationAutomatic]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Table View Data Source | ||||
|  | ||||
| - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { | ||||
|     return self.bookmarks.count; | ||||
| } | ||||
|  | ||||
| - (UITableViewCell *)tableView:(FLEXTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath]; | ||||
|      | ||||
|     id object = self.bookmarks[indexPath.row]; | ||||
|     cell.textLabel.text = [FLEXRuntimeUtility safeDescriptionForObject:object]; | ||||
|     cell.detailTextLabel.text = [NSString stringWithFormat:@"%@ — %p", [object class], object]; | ||||
|      | ||||
|     return cell; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Table View Delegate | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     if (self.editing) { | ||||
|         // Case: editing with multi-select | ||||
|         self.toolbarItems.lastObject.title = @"Remove Selected"; | ||||
|         self.toolbarItems.lastObject.tintColor = FLEXColor.destructiveColor; | ||||
|     } else { | ||||
|         // Case: selected a bookmark | ||||
|         [self dismissAnimated:self.bookmarks[indexPath.row]]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     NSParameterAssert(self.editing); | ||||
|      | ||||
|     if (tableView.indexPathsForSelectedRows.count == 0) { | ||||
|         self.toolbarItems.lastObject.title = @"Done"; | ||||
|         self.toolbarItems.lastObject.tintColor = self.view.tintColor; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (void)tableView:(UITableView *)table | ||||
| commitEditingStyle:(UITableViewCellEditingStyle)edit | ||||
| forRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     NSParameterAssert(edit == UITableViewCellEditingStyleDelete); | ||||
|      | ||||
|     // Remove bookmark and update data source | ||||
|     [FLEXBookmarkManager.bookmarks removeObjectAtIndex:indexPath.row]; | ||||
|     [self reloadData]; | ||||
|      | ||||
|     // Delete row from table view | ||||
|     [table deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										61
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXExplorerViewController.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXExplorerViewController.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| // | ||||
| //  FLEXExplorerViewController.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 4/4/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXExplorerToolbar.h" | ||||
|  | ||||
| @class FLEXWindow; | ||||
| @protocol FLEXExplorerViewControllerDelegate; | ||||
|  | ||||
| /// A view controller that manages the FLEX toolbar. | ||||
| @interface FLEXExplorerViewController : UIViewController | ||||
|  | ||||
| @property (nonatomic, weak) id <FLEXExplorerViewControllerDelegate> delegate; | ||||
| @property (nonatomic, readonly) BOOL wantsWindowToBecomeKey; | ||||
|  | ||||
| @property (nonatomic, readonly) FLEXExplorerToolbar *explorerToolbar; | ||||
|  | ||||
| - (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates; | ||||
|  | ||||
| /// @brief Used to present (or dismiss) a modal view controller ("tool"), | ||||
| /// typically triggered by pressing a button in the toolbar. | ||||
| /// | ||||
| /// If a tool is already presented, this method simply dismisses it and calls the completion block. | ||||
| /// If no tool is presented, @code future() @endcode is presented and the completion block is called. | ||||
| - (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future | ||||
|                                   completion:(void (^)(void))completion; | ||||
|  | ||||
| /// @brief Used to present (or dismiss) a modal view controller ("tool"), | ||||
| /// typically triggered by pressing a button in the toolbar. | ||||
| /// | ||||
| /// If a tool is already presented, this method dismisses it and presents the given tool. | ||||
| /// The completion block is called once the tool has been presented. | ||||
| - (void)presentTool:(UINavigationController *(^)(void))future | ||||
|          completion:(void (^)(void))completion; | ||||
|  | ||||
| // Keyboard shortcut helpers | ||||
|  | ||||
| - (void)toggleSelectTool; | ||||
| - (void)toggleMoveTool; | ||||
| - (void)toggleViewsTool; | ||||
| - (void)toggleMenuTool; | ||||
|  | ||||
| /// @return YES if the explorer used the key press to perform an action, NO otherwise | ||||
| - (BOOL)handleDownArrowKeyPressed; | ||||
| /// @return YES if the explorer used the key press to perform an action, NO otherwise | ||||
| - (BOOL)handleUpArrowKeyPressed; | ||||
| /// @return YES if the explorer used the key press to perform an action, NO otherwise | ||||
| - (BOOL)handleRightArrowKeyPressed; | ||||
| /// @return YES if the explorer used the key press to perform an action, NO otherwise | ||||
| - (BOOL)handleLeftArrowKeyPressed; | ||||
|  | ||||
| @end | ||||
|  | ||||
| #pragma mark - | ||||
| @protocol FLEXExplorerViewControllerDelegate <NSObject> | ||||
| - (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController; | ||||
| @end | ||||
							
								
								
									
										1050
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXExplorerViewController.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1050
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXExplorerViewController.m
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -0,0 +1,19 @@ | ||||
| // | ||||
| //  FLEXViewControllersViewController.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 2/13/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXFilteringTableViewController.h" | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FLEXViewControllersViewController : FLEXFilteringTableViewController | ||||
|  | ||||
| + (instancetype)controllersForViews:(NSArray<UIView *> *)views; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
| @@ -0,0 +1,79 @@ | ||||
| // | ||||
| //  FLEXViewControllersViewController.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 2/13/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXViewControllersViewController.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
| #import "FLEXMutableListSection.h" | ||||
| #import "FLEXUtility.h" | ||||
|  | ||||
| @interface FLEXViewControllersViewController () | ||||
| @property (nonatomic, readonly) FLEXMutableListSection *section; | ||||
| @property (nonatomic, readonly) NSArray<UIViewController *> *controllers; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXViewControllersViewController | ||||
| @dynamic sections, allSections; | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| + (instancetype)controllersForViews:(NSArray<UIView *> *)views { | ||||
|     return [[self alloc] initWithViews:views]; | ||||
| } | ||||
|  | ||||
| - (id)initWithViews:(NSArray<UIView *> *)views { | ||||
|     NSParameterAssert(views.count); | ||||
|      | ||||
|     self = [self initWithStyle:UITableViewStylePlain]; | ||||
|     if (self) { | ||||
|         _controllers = [views flex_mapped:^id(UIView *view, NSUInteger idx) { | ||||
|             return [FLEXUtility viewControllerForView:view]; | ||||
|         }]; | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|      | ||||
|     self.title = @"View Controllers at Tap"; | ||||
|     self.showsSearchBar = YES; | ||||
|     [self disableToolbar]; | ||||
| } | ||||
|  | ||||
| - (NSArray<FLEXTableViewSection *> *)makeSections { | ||||
|     _section = [FLEXMutableListSection list:self.controllers | ||||
|         cellConfiguration:^(UITableViewCell *cell, UIViewController *controller, NSInteger row) { | ||||
|             cell.textLabel.text = [NSString | ||||
|                 stringWithFormat:@"%@ — %p", NSStringFromClass(controller.class), controller | ||||
|             ]; | ||||
|             cell.detailTextLabel.text = controller.view.description; | ||||
|             cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; | ||||
|             cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail; | ||||
|     } filterMatcher:^BOOL(NSString *filterText, UIViewController *controller) { | ||||
|         return [NSStringFromClass(controller.class) localizedCaseInsensitiveContainsString:filterText]; | ||||
|     }]; | ||||
|      | ||||
|     self.section.selectionHandler = ^(UIViewController *host, UIViewController *controller) { | ||||
|         [host.navigationController pushViewController: | ||||
|             [FLEXObjectExplorerFactory explorerViewControllerForObject:controller] | ||||
|         animated:YES]; | ||||
|     }; | ||||
|      | ||||
|     self.section.customTitle = @"View Controllers"; | ||||
|     return @[self.section]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Private | ||||
|  | ||||
| - (void)dismissAnimated { | ||||
|     [self dismissViewControllerAnimated:YES completion:nil]; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										29
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXWindow.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXWindow.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| // | ||||
| //  FLEXWindow.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 4/13/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| @protocol FLEXWindowEventDelegate <NSObject> | ||||
|  | ||||
| - (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow; | ||||
| - (BOOL)canBecomeKeyWindow; | ||||
|  | ||||
| @end | ||||
|  | ||||
| #pragma mark - | ||||
| @interface FLEXWindow : UIWindow | ||||
|  | ||||
| @property (nonatomic, weak) id <FLEXWindowEventDelegate> eventDelegate; | ||||
|  | ||||
| /// Tracked so we can restore the key window after dismissing a modal. | ||||
| /// We need to become key after modal presentation so we can correctly capture input. | ||||
| /// If we're just showing the toolbar, we want the main app's window to remain key | ||||
| /// so that we don't interfere with input, status bar, etc. | ||||
| @property (nonatomic, readonly) UIWindow *previousKeyWindow; | ||||
|  | ||||
| @end | ||||
							
								
								
									
										72
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXWindow.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXWindow.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| // | ||||
| //  FLEXWindow.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 4/13/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXWindow.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import <objc/runtime.h> | ||||
|  | ||||
| @implementation FLEXWindow | ||||
|  | ||||
| - (id)initWithFrame:(CGRect)frame { | ||||
|     self = [super initWithFrame:frame]; | ||||
|     if (self) { | ||||
|         // Some apps have windows at UIWindowLevelStatusBar + n. | ||||
|         // If we make the window level too high, we block out UIAlertViews. | ||||
|         // There's a balance between staying above the app's windows and staying below alerts. | ||||
|         // UIWindowLevelStatusBar + 100 seems to hit that balance. | ||||
|         self.windowLevel = UIWindowLevelStatusBar + 100.0; | ||||
|     } | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { | ||||
|     BOOL pointInside = NO; | ||||
|     if ([self.eventDelegate shouldHandleTouchAtPoint:point]) { | ||||
|         pointInside = [super pointInside:point withEvent:event]; | ||||
|     } | ||||
|     return pointInside; | ||||
| } | ||||
|  | ||||
| - (BOOL)shouldAffectStatusBarAppearance { | ||||
|     return [self isKeyWindow]; | ||||
| } | ||||
|  | ||||
| - (BOOL)canBecomeKeyWindow { | ||||
|     return [self.eventDelegate canBecomeKeyWindow]; | ||||
| } | ||||
|  | ||||
| - (void)makeKeyWindow { | ||||
|     _previousKeyWindow = FLEXUtility.appKeyWindow; | ||||
|     [super makeKeyWindow]; | ||||
| } | ||||
|  | ||||
| - (void)resignKeyWindow { | ||||
|     [super resignKeyWindow]; | ||||
|     _previousKeyWindow = nil; | ||||
| } | ||||
|  | ||||
| + (void)initialize { | ||||
|     // This adds a method (superclass override) at runtime which gives us the status bar behavior we want. | ||||
|     // The FLEX window is intended to be an overlay that generally doesn't affect the app underneath. | ||||
|     // Most of the time, we want the app's main window(s) to be in control of status bar behavior. | ||||
|     // Done at runtime with an obfuscated selector because it is private API. But you shouldn't ship this to the App Store anyways... | ||||
|     NSString *canAffectSelectorString = [@[@"_can", @"Affect", @"Status", @"Bar", @"Appearance"] componentsJoinedByString:@""]; | ||||
|     SEL canAffectSelector = NSSelectorFromString(canAffectSelectorString); | ||||
|     Method shouldAffectMethod = class_getInstanceMethod(self, @selector(shouldAffectStatusBarAppearance)); | ||||
|     IMP canAffectImplementation = method_getImplementation(shouldAffectMethod); | ||||
|     class_addMethod(self, canAffectSelector, canAffectImplementation, method_getTypeEncoding(shouldAffectMethod)); | ||||
|  | ||||
|     // One more... | ||||
|     NSString *canBecomeKeySelectorString = [NSString stringWithFormat:@"_%@", NSStringFromSelector(@selector(canBecomeKeyWindow))]; | ||||
|     SEL canBecomeKeySelector = NSSelectorFromString(canBecomeKeySelectorString); | ||||
|     Method canBecomeKeyMethod = class_getInstanceMethod(self, @selector(canBecomeKeyWindow)); | ||||
|     IMP canBecomeKeyImplementation = method_getImplementation(canBecomeKeyMethod); | ||||
|     class_addMethod(self, canBecomeKeySelector, canBecomeKeyImplementation, method_getTypeEncoding(canBecomeKeyMethod)); | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										17
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXWindowManagerController.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXWindowManagerController.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| // | ||||
| //  FLEXWindowManagerController.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 2/6/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableViewController.h" | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FLEXWindowManagerController : FLEXTableViewController | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
							
								
								
									
										302
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXWindowManagerController.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										302
									
								
								Tweaks/FLEX/ExplorerInterface/FLEXWindowManagerController.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,302 @@ | ||||
| // | ||||
| //  FLEXWindowManagerController.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 2/6/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXWindowManagerController.h" | ||||
| #import "FLEXManager+Private.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
|  | ||||
| @interface FLEXWindowManagerController () | ||||
| @property (nonatomic) UIWindow *keyWindow; | ||||
| @property (nonatomic, copy) NSString *keyWindowSubtitle; | ||||
| @property (nonatomic, copy) NSArray<UIWindow *> *windows; | ||||
| @property (nonatomic, copy) NSArray<NSString *> *windowSubtitles; | ||||
| @property (nonatomic, copy) NSArray<UIScene *> *scenes API_AVAILABLE(ios(13)); | ||||
| @property (nonatomic, copy) NSArray<NSString *> *sceneSubtitles; | ||||
| @property (nonatomic, copy) NSArray<NSArray *> *sections; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXWindowManagerController | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| - (id)init { | ||||
|     return [self initWithStyle:UITableViewStylePlain]; | ||||
| } | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|      | ||||
|     self.title = @"Windows"; | ||||
|     if (@available(iOS 13, *)) { | ||||
|         self.title = @"Windows and Scenes"; | ||||
|     } | ||||
|      | ||||
|     [self disableToolbar]; | ||||
|     [self reloadData]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Private | ||||
|  | ||||
| - (void)reloadData { | ||||
|     self.keyWindow = UIApplication.sharedApplication.keyWindow; | ||||
|     self.windows = UIApplication.sharedApplication.windows; | ||||
|     self.keyWindowSubtitle = self.windowSubtitles[[self.windows indexOfObject:self.keyWindow]]; | ||||
|     self.windowSubtitles = [self.windows flex_mapped:^id(UIWindow *window, NSUInteger idx) { | ||||
|         return [NSString stringWithFormat:@"Level: %@ — Root: %@", | ||||
|             @(window.windowLevel), window.rootViewController | ||||
|         ]; | ||||
|     }]; | ||||
|      | ||||
|     if (@available(iOS 13, *)) { | ||||
|         self.scenes = UIApplication.sharedApplication.connectedScenes.allObjects; | ||||
|         self.sceneSubtitles = [self.scenes flex_mapped:^id(UIScene *scene, NSUInteger idx) { | ||||
|             return [self sceneDescription:scene]; | ||||
|         }]; | ||||
|          | ||||
|         self.sections = @[@[self.keyWindow], self.windows, self.scenes]; | ||||
|     } else { | ||||
|         self.sections = @[@[self.keyWindow], self.windows]; | ||||
|     } | ||||
|      | ||||
|     [self.tableView reloadData]; | ||||
| } | ||||
|  | ||||
| - (void)dismissAnimated { | ||||
|     [self dismissViewControllerAnimated:YES completion:nil]; | ||||
| } | ||||
|  | ||||
| - (void)showRevertOrDismissAlert:(void(^)(void))revertBlock { | ||||
|     [self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES]; | ||||
|     [self reloadData]; | ||||
|     [self.tableView reloadData]; | ||||
|      | ||||
|     UIWindow *highestWindow = UIApplication.sharedApplication.keyWindow; | ||||
|     UIWindowLevel maxLevel = 0; | ||||
|     for (UIWindow *window in UIApplication.sharedApplication.windows) { | ||||
|         if (window.windowLevel > maxLevel) { | ||||
|             maxLevel = window.windowLevel; | ||||
|             highestWindow = window; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         make.title(@"Keep Changes?"); | ||||
|         make.message(@"If you do not wish to keep these settings, choose 'Revert Changes' below."); | ||||
|          | ||||
|         make.button(@"Keep Changes").destructiveStyle(); | ||||
|         make.button(@"Keep Changes and Dismiss").destructiveStyle().handler(^(NSArray<NSString *> *strings) { | ||||
|             [self dismissAnimated]; | ||||
|         }); | ||||
|         make.button(@"Revert Changes").cancelStyle().handler(^(NSArray<NSString *> *strings) { | ||||
|             revertBlock(); | ||||
|             [self reloadData]; | ||||
|             [self.tableView reloadData]; | ||||
|         }); | ||||
|     } showFrom:[FLEXUtility topViewControllerInWindow:highestWindow]]; | ||||
| } | ||||
|  | ||||
| - (NSString *)sceneDescription:(UIScene *)scene API_AVAILABLE(ios(13)) { | ||||
|     NSString *state = [self stringFromSceneState:scene.activationState]; | ||||
|     NSString *title = scene.title.length ? scene.title : nil; | ||||
|     NSString *suffix = nil; | ||||
|      | ||||
|     if ([scene isKindOfClass:[UIWindowScene class]]) { | ||||
|         UIWindowScene *windowScene = (id)scene; | ||||
|         suffix = FLEXPluralString(windowScene.windows.count, @"windows", @"window"); | ||||
|     } | ||||
|      | ||||
|     NSMutableString *description = state.mutableCopy; | ||||
|     if (title) { | ||||
|         [description appendFormat:@" — %@", title]; | ||||
|     } | ||||
|     if (suffix) { | ||||
|         [description appendFormat:@" — %@", suffix]; | ||||
|     } | ||||
|      | ||||
|     return description.copy; | ||||
| } | ||||
|  | ||||
| - (NSString *)stringFromSceneState:(UISceneActivationState)state API_AVAILABLE(ios(13)) { | ||||
|     switch (state) { | ||||
|         case UISceneActivationStateUnattached: | ||||
|             return @"Unattached"; | ||||
|         case UISceneActivationStateForegroundActive: | ||||
|             return @"Active"; | ||||
|         case UISceneActivationStateForegroundInactive: | ||||
|             return @"Inactive"; | ||||
|         case UISceneActivationStateBackground: | ||||
|             return @"Backgrounded"; | ||||
|     } | ||||
|      | ||||
|     return [NSString stringWithFormat:@"Unknown state: %@", @(state)]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Table View Data Source | ||||
|  | ||||
| - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { | ||||
|     return self.sections.count; | ||||
| } | ||||
|  | ||||
| - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { | ||||
|     return self.sections[section].count; | ||||
| } | ||||
|  | ||||
| - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { | ||||
|     switch (section) { | ||||
|         case 0: return @"Key Window"; | ||||
|         case 1: return @"Windows"; | ||||
|         case 2: return @"Connected Scenes"; | ||||
|     } | ||||
|      | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath]; | ||||
|     cell.accessoryType = UITableViewCellAccessoryDetailButton; | ||||
|     cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail; | ||||
|      | ||||
|     UIWindow *window = nil; | ||||
|     NSString *subtitle = nil; | ||||
|      | ||||
|     switch (indexPath.section) { | ||||
|         case 0: | ||||
|             window = self.keyWindow; | ||||
|             subtitle = self.keyWindowSubtitle; | ||||
|             break; | ||||
|         case 1: | ||||
|             window = self.windows[indexPath.row]; | ||||
|             subtitle = self.windowSubtitles[indexPath.row]; | ||||
|             break; | ||||
|         case 2: | ||||
|             if (@available(iOS 13, *)) { | ||||
|                 UIScene *scene = self.scenes[indexPath.row]; | ||||
|                 cell.textLabel.text = scene.description; | ||||
|                 cell.detailTextLabel.text = self.sceneSubtitles[indexPath.row]; | ||||
|                 return cell; | ||||
|             } | ||||
|     } | ||||
|      | ||||
|     cell.textLabel.text = window.description; | ||||
|     cell.detailTextLabel.text = [NSString | ||||
|         stringWithFormat:@"Level: %@ — Root: %@", | ||||
|         @((NSInteger)window.windowLevel), window.rootViewController.class | ||||
|     ]; | ||||
|      | ||||
|     return cell; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Table View Delegate | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     UIWindow *window = nil; | ||||
|     NSString *subtitle = nil; | ||||
|     FLEXWindow *flex = FLEXManager.sharedManager.explorerWindow; | ||||
|      | ||||
|     id cancelHandler = ^{ | ||||
|         [self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES]; | ||||
|     }; | ||||
|      | ||||
|     switch (indexPath.section) { | ||||
|         case 0: | ||||
|             window = self.keyWindow; | ||||
|             subtitle = self.keyWindowSubtitle; | ||||
|             break; | ||||
|         case 1: | ||||
|             window = self.windows[indexPath.row]; | ||||
|             subtitle = self.windowSubtitles[indexPath.row]; | ||||
|             break; | ||||
|         case 2: | ||||
|             if (@available(iOS 13, *)) { | ||||
|                 UIScene *scene = self.scenes[indexPath.row]; | ||||
|                 UIWindowScene *oldScene = flex.windowScene; | ||||
|                 BOOL isWindowScene = [scene isKindOfClass:[UIWindowScene class]]; | ||||
|                 BOOL isFLEXScene = isWindowScene ? flex.windowScene == scene : NO; | ||||
|                  | ||||
|                 [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|                     make.title(NSStringFromClass(scene.class)); | ||||
|                      | ||||
|                     if (isWindowScene) { | ||||
|                         if (isFLEXScene) { | ||||
|                             make.message(@"Already the FLEX window scene"); | ||||
|                         } | ||||
|                          | ||||
|                         make.button(@"Set as FLEX Window Scene") | ||||
|                         .handler(^(NSArray<NSString *> *strings) { | ||||
|                             flex.windowScene = (id)scene; | ||||
|                             [self showRevertOrDismissAlert:^{ | ||||
|                                 flex.windowScene = oldScene; | ||||
|                             }]; | ||||
|                         }).enabled(!isFLEXScene); | ||||
|                         make.button(@"Cancel").cancelStyle(); | ||||
|                     } else { | ||||
|                         make.message(@"Not a UIWindowScene"); | ||||
|                         make.button(@"Dismiss").cancelStyle().handler(cancelHandler); | ||||
|                     } | ||||
|                 } showFrom:self]; | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     __block UIWindow *targetWindow = nil, *oldKeyWindow = nil; | ||||
|     __block UIWindowLevel oldLevel; | ||||
|     __block BOOL wasVisible; | ||||
|      | ||||
|     subtitle = [subtitle stringByAppendingString: | ||||
|         @"\n\n1) Adjust the FLEX window level relative to this window,\n" | ||||
|         "2) adjust this window's level relative to the FLEX window,\n" | ||||
|         "3) set this window's level to a specific value, or\n" | ||||
|         "4) make this window the key window if it isn't already." | ||||
|     ]; | ||||
|      | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         make.title(NSStringFromClass(window.class)).message(subtitle); | ||||
|         make.button(@"Adjust FLEX Window Level").handler(^(NSArray<NSString *> *strings) { | ||||
|             targetWindow = flex; oldLevel = flex.windowLevel; | ||||
|             flex.windowLevel = window.windowLevel + strings.firstObject.integerValue; | ||||
|              | ||||
|             [self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }]; | ||||
|         }); | ||||
|         make.button(@"Adjust This Window's Level").handler(^(NSArray<NSString *> *strings) { | ||||
|             targetWindow = window; oldLevel = window.windowLevel; | ||||
|             window.windowLevel = flex.windowLevel + strings.firstObject.integerValue; | ||||
|              | ||||
|             [self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }]; | ||||
|         }); | ||||
|         make.button(@"Set This Window's Level").handler(^(NSArray<NSString *> *strings) { | ||||
|             targetWindow = window; oldLevel = window.windowLevel; | ||||
|             window.windowLevel = strings.firstObject.integerValue; | ||||
|              | ||||
|             [self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }]; | ||||
|         }); | ||||
|         make.button(@"Make Key And Visible").handler(^(NSArray<NSString *> *strings) { | ||||
|             oldKeyWindow = UIApplication.sharedApplication.keyWindow; | ||||
|             wasVisible = window.hidden; | ||||
|             [window makeKeyAndVisible]; | ||||
|              | ||||
|             [self showRevertOrDismissAlert:^{ | ||||
|                 window.hidden = wasVisible; | ||||
|                 [oldKeyWindow makeKeyWindow]; | ||||
|             }]; | ||||
|         }).enabled(!window.isKeyWindow && !window.hidden); | ||||
|         make.button(@"Cancel").cancelStyle().handler(cancelHandler); | ||||
|          | ||||
|         make.textField(@"+/- window level, i.e. 5 or -10"); | ||||
|     } showFrom:self]; | ||||
| } | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)ip { | ||||
|     [self.navigationController pushViewController: | ||||
|         [FLEXObjectExplorerFactory explorerViewControllerForObject:self.sections[ip.section][ip.row]] | ||||
|     animated:YES]; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										45
									
								
								Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabList.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabList.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| // | ||||
| //  FLEXTabList.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 2/1/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FLEXTabList : NSObject | ||||
|  | ||||
| @property (nonatomic, readonly, class) FLEXTabList *sharedList; | ||||
|  | ||||
| @property (nonatomic, readonly, nullable) UINavigationController *activeTab; | ||||
| @property (nonatomic, readonly) NSArray<UINavigationController *> *openTabs; | ||||
| /// Snapshots of each tab when they were last active. | ||||
| @property (nonatomic, readonly) NSArray<UIImage *> *openTabSnapshots; | ||||
| /// \c NSNotFound if no tabs are present. | ||||
| /// Setting this property changes the active tab to one of the already open tabs. | ||||
| @property (nonatomic) NSInteger activeTabIndex; | ||||
|  | ||||
| /// Adds a new tab and sets the new tab as the active tab. | ||||
| - (void)addTab:(UINavigationController *)newTab; | ||||
| /// Closes the given tab. If this tab was the active tab, | ||||
| /// the most recent tab before that becomes the active tab. | ||||
| - (void)closeTab:(UINavigationController *)tab; | ||||
| /// Closes a tab at the given index. If this tab was the active tab, | ||||
| /// the most recent tab before that becomes the active tab. | ||||
| - (void)closeTabAtIndex:(NSInteger)idx; | ||||
| /// Closes all of the tabs at the given indexes. If the active tab | ||||
| /// is included, the most recent still-open tab becomes the active tab. | ||||
| - (void)closeTabsAtIndexes:(NSIndexSet *)indexes; | ||||
| /// A shortcut to close the active tab. | ||||
| - (void)closeActiveTab; | ||||
| /// A shortcut to close \e every tab. | ||||
| - (void)closeAllTabs; | ||||
|  | ||||
| - (void)updateSnapshotForActiveTab; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
							
								
								
									
										133
									
								
								Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabList.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabList.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | ||||
| // | ||||
| //  FLEXTabList.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 2/1/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTabList.h" | ||||
| #import "FLEXUtility.h" | ||||
|  | ||||
| @interface FLEXTabList () { | ||||
|     NSMutableArray *_openTabs; | ||||
|     NSMutableArray *_openTabSnapshots; | ||||
| } | ||||
| @end | ||||
| #pragma mark - | ||||
| @implementation FLEXTabList | ||||
|  | ||||
| #pragma mark Initialization | ||||
|  | ||||
| + (FLEXTabList *)sharedList { | ||||
|     static FLEXTabList *sharedList = nil; | ||||
|     static dispatch_once_t onceToken; | ||||
|     dispatch_once(&onceToken, ^{ | ||||
|         sharedList = [self new]; | ||||
|     }); | ||||
|      | ||||
|     return sharedList; | ||||
| } | ||||
|  | ||||
| - (id)init { | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         _openTabs = [NSMutableArray new]; | ||||
|         _openTabSnapshots = [NSMutableArray new]; | ||||
|         _activeTabIndex = NSNotFound; | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark Private | ||||
|  | ||||
| - (void)chooseNewActiveTab { | ||||
|     if (self.openTabs.count) { | ||||
|         self.activeTabIndex = self.openTabs.count - 1; | ||||
|     } else { | ||||
|         self.activeTabIndex = NSNotFound; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark Public | ||||
|  | ||||
| - (void)setActiveTabIndex:(NSInteger)idx { | ||||
|     NSParameterAssert(idx < self.openTabs.count || idx == NSNotFound); | ||||
|     if (_activeTabIndex == idx) return; | ||||
|      | ||||
|     _activeTabIndex = idx; | ||||
|     _activeTab = (idx == NSNotFound) ? nil : self.openTabs[idx]; | ||||
| } | ||||
|  | ||||
| - (void)addTab:(UINavigationController *)newTab { | ||||
|     NSParameterAssert(newTab); | ||||
|      | ||||
|     // Update snapshot of the last active tab | ||||
|     if (self.activeTab) { | ||||
|         [self updateSnapshotForActiveTab]; | ||||
|     } | ||||
|      | ||||
|     // Add new tab and snapshot, | ||||
|     // update active tab and index | ||||
|     [_openTabs addObject:newTab]; | ||||
|     [_openTabSnapshots addObject:[FLEXUtility previewImageForView:newTab.view]]; | ||||
|     _activeTab = newTab; | ||||
|     _activeTabIndex = self.openTabs.count - 1; | ||||
| } | ||||
|  | ||||
| - (void)closeTab:(UINavigationController *)tab { | ||||
|     NSParameterAssert(tab); | ||||
|     NSInteger idx = [self.openTabs indexOfObject:tab]; | ||||
|     if (idx != NSNotFound) { | ||||
|         [self closeTabAtIndex:idx]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)closeTabAtIndex:(NSInteger)idx { | ||||
|     NSParameterAssert(idx < self.openTabs.count); | ||||
|      | ||||
|     // Remove old tab and snapshot | ||||
|     [_openTabs removeObjectAtIndex:idx]; | ||||
|     [_openTabSnapshots removeObjectAtIndex:idx]; | ||||
|      | ||||
|     // Update active tab and index if needed | ||||
|     if (self.activeTabIndex == idx) { | ||||
|         [self chooseNewActiveTab]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)closeTabsAtIndexes:(NSIndexSet *)indexes { | ||||
|     // Remove old tabs and snapshot | ||||
|     [_openTabs removeObjectsAtIndexes:indexes]; | ||||
|     [_openTabSnapshots removeObjectsAtIndexes:indexes]; | ||||
|      | ||||
|     // Update active tab and index if needed | ||||
|     if ([indexes containsIndex:self.activeTabIndex]) { | ||||
|         [self chooseNewActiveTab]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)closeActiveTab { | ||||
|     [self closeTab:self.activeTab]; | ||||
| } | ||||
|  | ||||
| - (void)closeAllTabs { | ||||
|     // Remove tabs and snapshots | ||||
|     [_openTabs removeAllObjects]; | ||||
|     [_openTabSnapshots removeAllObjects]; | ||||
|      | ||||
|     // Update active tab index | ||||
|     self.activeTabIndex = NSNotFound; | ||||
| } | ||||
|  | ||||
| - (void)updateSnapshotForActiveTab { | ||||
|     if (self.activeTabIndex != NSNotFound) { | ||||
|         UIImage *newSnapshot = [FLEXUtility previewImageForView:self.activeTab.view]; | ||||
|         [_openTabSnapshots replaceObjectAtIndex:self.activeTabIndex withObject:newSnapshot]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										13
									
								
								Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabsViewController.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabsViewController.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| // | ||||
| //  FLEXTabsViewController.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 2/4/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableViewController.h" | ||||
|  | ||||
| @interface FLEXTabsViewController : FLEXTableViewController | ||||
|  | ||||
| @end | ||||
							
								
								
									
										335
									
								
								Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabsViewController.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										335
									
								
								Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabsViewController.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,335 @@ | ||||
| // | ||||
| //  FLEXTabsViewController.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 2/4/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTabsViewController.h" | ||||
| #import "FLEXNavigationController.h" | ||||
| #import "FLEXTabList.h" | ||||
| #import "FLEXBookmarkManager.h" | ||||
| #import "FLEXTableView.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXColor.h" | ||||
| #import "UIBarButtonItem+FLEX.h" | ||||
| #import "FLEXExplorerViewController.h" | ||||
| #import "FLEXGlobalsViewController.h" | ||||
| #import "FLEXBookmarksViewController.h" | ||||
|  | ||||
| @interface FLEXTabsViewController () | ||||
| @property (nonatomic, copy) NSArray<UINavigationController *> *openTabs; | ||||
| @property (nonatomic, copy) NSArray<UIImage *> *tabSnapshots; | ||||
| @property (nonatomic) NSInteger activeIndex; | ||||
| @property (nonatomic) BOOL presentNewActiveTabOnDismiss; | ||||
|  | ||||
| @property (nonatomic, readonly) FLEXExplorerViewController *corePresenter; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXTabsViewController | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| - (id)init { | ||||
|     return [self initWithStyle:UITableViewStylePlain]; | ||||
| } | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|      | ||||
|     self.title = @"Open Tabs"; | ||||
|     self.navigationController.hidesBarsOnSwipe = NO; | ||||
|     self.tableView.allowsMultipleSelectionDuringEditing = YES; | ||||
|      | ||||
|     [self reloadData:NO]; | ||||
| } | ||||
|  | ||||
| - (void)viewWillAppear:(BOOL)animated { | ||||
|     [super viewWillAppear:animated]; | ||||
|     [self setupDefaultBarItems]; | ||||
| } | ||||
|  | ||||
| - (void)viewDidAppear:(BOOL)animated { | ||||
|     [super viewDidAppear:animated]; | ||||
|      | ||||
|     // Instead of updating the active snapshot before we present, | ||||
|     // we update it after we present to avoid pre-presenation latency | ||||
|     dispatch_async(dispatch_get_main_queue(), ^{ | ||||
|         [FLEXTabList.sharedList updateSnapshotForActiveTab]; | ||||
|         [self reloadData:NO]; | ||||
|         [self.tableView reloadData]; | ||||
|     }); | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Private | ||||
|  | ||||
| /// @param trackActiveTabDelta whether to check if the active | ||||
| /// tab changed and needs to be presented upon "Done" dismissal. | ||||
| /// @return whether the active tab changed or not (if there are any tabs left) | ||||
| - (BOOL)reloadData:(BOOL)trackActiveTabDelta { | ||||
|     BOOL activeTabDidChange = NO; | ||||
|     FLEXTabList *list = FLEXTabList.sharedList; | ||||
|      | ||||
|     // Flag to enable check to determine whether | ||||
|     if (trackActiveTabDelta) { | ||||
|         NSInteger oldActiveIndex = self.activeIndex; | ||||
|         if (oldActiveIndex != list.activeTabIndex && list.activeTabIndex != NSNotFound) { | ||||
|             self.presentNewActiveTabOnDismiss = YES; | ||||
|             activeTabDidChange = YES; | ||||
|         } else if (self.presentNewActiveTabOnDismiss) { | ||||
|             // If we had something to present before, now we don't | ||||
|             // (i.e. activeTabIndex == NSNotFound) | ||||
|             self.presentNewActiveTabOnDismiss = NO; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // We assume the tabs aren't going to change out from under us, since | ||||
|     // presenting any other tool via keyboard shortcuts should dismiss us first | ||||
|     self.openTabs = list.openTabs; | ||||
|     self.tabSnapshots = list.openTabSnapshots; | ||||
|     self.activeIndex = list.activeTabIndex; | ||||
|      | ||||
|     return activeTabDidChange; | ||||
| } | ||||
|  | ||||
| - (void)reloadActiveTabRowIfChanged:(BOOL)activeTabChanged { | ||||
|     // Refresh the newly active tab row if needed | ||||
|     if (activeTabChanged) { | ||||
|         NSIndexPath *active = [NSIndexPath | ||||
|            indexPathForRow:self.activeIndex inSection:0 | ||||
|         ]; | ||||
|         [self.tableView reloadRowsAtIndexPaths:@[active] withRowAnimation:UITableViewRowAnimationNone]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)setupDefaultBarItems { | ||||
|     self.navigationItem.rightBarButtonItem = FLEXBarButtonItemSystem(Done, self, @selector(dismissAnimated)); | ||||
|     self.toolbarItems = @[ | ||||
|         UIBarButtonItem.flex_fixedSpace, | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         FLEXBarButtonItemSystem(Add, self, @selector(addTabButtonPressed:)), | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         FLEXBarButtonItemSystem(Edit, self, @selector(toggleEditing)), | ||||
|     ]; | ||||
|      | ||||
|     // Disable editing if no tabs available | ||||
|     self.toolbarItems.lastObject.enabled = self.openTabs.count > 0; | ||||
| } | ||||
|  | ||||
| - (void)setupEditingBarItems { | ||||
|     self.navigationItem.rightBarButtonItem = nil; | ||||
|     self.toolbarItems = @[ | ||||
|         [UIBarButtonItem flex_itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed:)], | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         [UIBarButtonItem flex_disabledSystemItem:UIBarButtonSystemItemAdd], | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         // We use a non-system done item because we change its title dynamically | ||||
|         [UIBarButtonItem flex_doneStyleitemWithTitle:@"Done" target:self action:@selector(toggleEditing)] | ||||
|     ]; | ||||
|      | ||||
|     self.toolbarItems.firstObject.tintColor = FLEXColor.destructiveColor; | ||||
| } | ||||
|  | ||||
| - (FLEXExplorerViewController *)corePresenter { | ||||
|     // We must be presented by a FLEXExplorerViewController, or presented | ||||
|     // by another view controller that was presented by FLEXExplorerViewController | ||||
|     FLEXExplorerViewController *presenter = (id)self.presentingViewController; | ||||
|     presenter = (id)presenter.presentingViewController ?: presenter; | ||||
|     NSAssert( | ||||
|         [presenter isKindOfClass:[FLEXExplorerViewController class]], | ||||
|         @"The tabs view controller expects to be presented by the explorer controller" | ||||
|     ); | ||||
|     return presenter; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark Button Actions | ||||
|  | ||||
| - (void)dismissAnimated { | ||||
|     if (self.presentNewActiveTabOnDismiss) { | ||||
|         // The active tab was closed so we need to present the new one | ||||
|         UIViewController *activeTab = FLEXTabList.sharedList.activeTab; | ||||
|         FLEXExplorerViewController *presenter = self.corePresenter; | ||||
|         [presenter dismissViewControllerAnimated:YES completion:^{ | ||||
|             [presenter presentViewController:activeTab animated:YES completion:nil]; | ||||
|         }]; | ||||
|     } else if (self.activeIndex == NSNotFound) { | ||||
|         // The only tab was closed, so dismiss everything | ||||
|         [self.corePresenter dismissViewControllerAnimated:YES completion:nil]; | ||||
|     } else { | ||||
|         // Simple dismiss with the same active tab, only dismiss myself | ||||
|         [self dismissViewControllerAnimated:YES completion:nil]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)toggleEditing { | ||||
|     NSArray<NSIndexPath *> *selected = self.tableView.indexPathsForSelectedRows; | ||||
|     self.editing = !self.editing; | ||||
|      | ||||
|     if (self.isEditing) { | ||||
|         [self setupEditingBarItems]; | ||||
|     } else { | ||||
|         [self setupDefaultBarItems]; | ||||
|          | ||||
|         // Get index set of tabs to close | ||||
|         NSMutableIndexSet *indexes = [NSMutableIndexSet new]; | ||||
|         for (NSIndexPath *ip in selected) { | ||||
|             [indexes addIndex:ip.row]; | ||||
|         } | ||||
|          | ||||
|         if (selected.count) { | ||||
|             // Close tabs and update data source | ||||
|             [FLEXTabList.sharedList closeTabsAtIndexes:indexes]; | ||||
|             BOOL activeTabChanged = [self reloadData:YES]; | ||||
|              | ||||
|             // Remove deleted rows | ||||
|             [self.tableView deleteRowsAtIndexPaths:selected withRowAnimation:UITableViewRowAnimationAutomatic]; | ||||
|              | ||||
|             // Refresh the newly active tab row if needed | ||||
|             [self reloadActiveTabRowIfChanged:activeTabChanged]; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)addTabButtonPressed:(UIBarButtonItem *)sender { | ||||
|     if (FLEXBookmarkManager.bookmarks.count) { | ||||
|         [FLEXAlert makeSheet:^(FLEXAlert *make) { | ||||
|             make.title(@"New Tab"); | ||||
|             make.button(@"Main Menu").handler(^(NSArray<NSString *> *strings) { | ||||
|                 [self addTabAndDismiss:[FLEXNavigationController | ||||
|                     withRootViewController:[FLEXGlobalsViewController new] | ||||
|                 ]]; | ||||
|             }); | ||||
|             make.button(@"Choose from Bookmarks").handler(^(NSArray<NSString *> *strings) { | ||||
|                 [self presentViewController:[FLEXNavigationController | ||||
|                     withRootViewController:[FLEXBookmarksViewController new] | ||||
|                 ] animated:YES completion:nil]; | ||||
|             }); | ||||
|             make.button(@"Cancel").cancelStyle(); | ||||
|         } showFrom:self source:sender]; | ||||
|     } else { | ||||
|         // No bookmarks, just open the main menu | ||||
|         [self addTabAndDismiss:[FLEXNavigationController | ||||
|             withRootViewController:[FLEXGlobalsViewController new] | ||||
|         ]]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)addTabAndDismiss:(UINavigationController *)newTab { | ||||
|     FLEXExplorerViewController *presenter = self.corePresenter; | ||||
|     [presenter dismissViewControllerAnimated:YES completion:^{ | ||||
|         [presenter presentViewController:newTab animated:YES completion:nil]; | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| - (void)closeAllButtonPressed:(UIBarButtonItem *)sender { | ||||
|     [FLEXAlert makeSheet:^(FLEXAlert *make) { | ||||
|         NSInteger count = self.openTabs.count; | ||||
|         NSString *title = FLEXPluralFormatString(count, @"Close %@ tabs", @"Close %@ tab"); | ||||
|         make.button(title).destructiveStyle().handler(^(NSArray<NSString *> *strings) { | ||||
|             [self closeAll]; | ||||
|             [self toggleEditing]; | ||||
|         }); | ||||
|         make.button(@"Cancel").cancelStyle(); | ||||
|     } showFrom:self source:sender]; | ||||
| } | ||||
|  | ||||
| - (void)closeAll { | ||||
|     NSInteger rowCount = self.openTabs.count; | ||||
|      | ||||
|     // Close tabs and update data source | ||||
|     [FLEXTabList.sharedList closeAllTabs]; | ||||
|     [self reloadData:YES]; | ||||
|      | ||||
|     // Delete rows from table view | ||||
|     NSArray<NSIndexPath *> *allRows = [NSArray flex_forEachUpTo:rowCount map:^id(NSUInteger row) { | ||||
|         return [NSIndexPath indexPathForRow:row inSection:0]; | ||||
|     }]; | ||||
|     [self.tableView deleteRowsAtIndexPaths:allRows withRowAnimation:UITableViewRowAnimationAutomatic]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Table View Data Source | ||||
|  | ||||
| - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { | ||||
|     return self.openTabs.count; | ||||
| } | ||||
|  | ||||
| - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath]; | ||||
|      | ||||
|     UINavigationController *tab = self.openTabs[indexPath.row]; | ||||
|     cell.imageView.image = self.tabSnapshots[indexPath.row]; | ||||
|     cell.textLabel.text = tab.topViewController.title; | ||||
|     cell.detailTextLabel.text = FLEXPluralString(tab.viewControllers.count, @"pages", @"page"); | ||||
|      | ||||
|     if (!cell.tag) { | ||||
|         cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail; | ||||
|         cell.textLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]; | ||||
|         cell.detailTextLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline]; | ||||
|         cell.tag = 1; | ||||
|     } | ||||
|      | ||||
|     if (indexPath.row == self.activeIndex) { | ||||
|         cell.backgroundColor = FLEXColor.secondaryBackgroundColor; | ||||
|     } else { | ||||
|         cell.backgroundColor = FLEXColor.primaryBackgroundColor; | ||||
|     } | ||||
|      | ||||
|     return cell; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Table View Delegate | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     if (self.editing) { | ||||
|         // Case: editing with multi-select | ||||
|         self.toolbarItems.lastObject.title = @"Close Selected"; | ||||
|         self.toolbarItems.lastObject.tintColor = FLEXColor.destructiveColor; | ||||
|     } else { | ||||
|         if (self.activeIndex == indexPath.row && self.corePresenter != self.presentingViewController) { | ||||
|             // Case: selected the already active tab | ||||
|             [self dismissAnimated]; | ||||
|         } else { | ||||
|             // Case: selected a different tab, | ||||
|             // or selected a tab when presented from the FLEX toolbar | ||||
|             FLEXTabList.sharedList.activeTabIndex = indexPath.row; | ||||
|             self.presentNewActiveTabOnDismiss = YES; | ||||
|             [self dismissAnimated]; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     NSParameterAssert(self.editing); | ||||
|      | ||||
|     if (tableView.indexPathsForSelectedRows.count == 0) { | ||||
|         self.toolbarItems.lastObject.title = @"Done"; | ||||
|         self.toolbarItems.lastObject.tintColor = self.view.tintColor; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (void)tableView:(UITableView *)table | ||||
| commitEditingStyle:(UITableViewCellEditingStyle)edit | ||||
| forRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     NSParameterAssert(edit == UITableViewCellEditingStyleDelete); | ||||
|      | ||||
|     // Close tab and update data source | ||||
|     [FLEXTabList.sharedList closeTab:self.openTabs[indexPath.row]]; | ||||
|     BOOL activeTabChanged = [self reloadData:YES]; | ||||
|      | ||||
|     // Delete row from table view | ||||
|     [table deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; | ||||
|      | ||||
|     // Refresh the newly active tab row if needed | ||||
|     [self reloadActiveTabRowIfChanged:activeTabChanged]; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										20
									
								
								Tweaks/FLEX/FLEX-Categories.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								Tweaks/FLEX/FLEX-Categories.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| // | ||||
| //  FLEX-Categories.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/12/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "UIBarButtonItem+FLEX.h" | ||||
| #import "CALayer+FLEX.h" | ||||
| #import "UIFont+FLEX.h" | ||||
| #import "UIGestureRecognizer+Blocks.h" | ||||
| #import "UIPasteboard+FLEX.h" | ||||
| #import "UIMenu+FLEX.h" | ||||
| #import "UITextField+Range.h" | ||||
|  | ||||
| #import "NSObject+FLEX_Reflection.h" | ||||
| #import "NSArray+FLEX.h" | ||||
| #import "NSUserDefaults+FLEX.h" | ||||
| #import "NSTimer+FLEX.h" | ||||
							
								
								
									
										22
									
								
								Tweaks/FLEX/FLEX-Core.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								Tweaks/FLEX/FLEX-Core.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| // | ||||
| //  FLEX-Core.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/11/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXFilteringTableViewController.h" | ||||
| #import "FLEXNavigationController.h" | ||||
| #import "FLEXTableViewController.h" | ||||
| #import "FLEXTableView.h" | ||||
|  | ||||
| #import "FLEXSingleRowSection.h" | ||||
| #import "FLEXTableViewSection.h" | ||||
|  | ||||
| #import "FLEXCodeFontCell.h" | ||||
| #import "FLEXSubtitleTableViewCell.h" | ||||
| #import "FLEXTableViewCell.h" | ||||
| #import "FLEXMultilineTableViewCell.h" | ||||
| #import "FLEXKeyValueTableViewCell.h" | ||||
|  | ||||
							
								
								
									
										22
									
								
								Tweaks/FLEX/FLEX-ObjectExploring.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								Tweaks/FLEX/FLEX-ObjectExploring.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| // | ||||
| //  FLEX-ObjectExploring.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/11/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
| #import "FLEXObjectExplorerViewController.h" | ||||
|  | ||||
| #import "FLEXObjectExplorer.h" | ||||
|  | ||||
| #import "FLEXShortcut.h" | ||||
| #import "FLEXShortcutsSection.h" | ||||
|  | ||||
| #import "FLEXCollectionContentSection.h" | ||||
| #import "FLEXColorPreviewSection.h" | ||||
| #import "FLEXDefaultsContentSection.h" | ||||
| #import "FLEXMetadataSection.h" | ||||
| #import "FLEXMutableListSection.h" | ||||
| #import "FLEXObjectInfoSection.h" | ||||
							
								
								
									
										25
									
								
								Tweaks/FLEX/FLEX-Runtime.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								Tweaks/FLEX/FLEX-Runtime.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| // | ||||
| //  FLEX-Runtime.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/11/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXObjcInternal.h" | ||||
| #import "FLEXRuntimeSafety.h" | ||||
| #import "FLEXBlockDescription.h" | ||||
| #import "FLEXTypeEncodingParser.h" | ||||
|  | ||||
| #import "FLEXMirror.h" | ||||
| #import "FLEXProtocol.h" | ||||
| #import "FLEXProperty.h" | ||||
| #import "FLEXIvar.h" | ||||
| #import "FLEXMethodBase.h" | ||||
| #import "FLEXMethod.h" | ||||
| #import "FLEXPropertyAttributes.h" | ||||
| #import "FLEXRuntime+Compare.h" | ||||
| #import "FLEXRuntime+UIKitHelpers.h" | ||||
|  | ||||
| #import "FLEXProtocolBuilder.h" | ||||
| #import "FLEXClassBuilder.h" | ||||
							
								
								
									
										25
									
								
								Tweaks/FLEX/FLEX.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								Tweaks/FLEX/FLEX.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| // | ||||
| //  FLEX.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Eric Horacek on 7/18/15. | ||||
| //  Modified by Tanner Bennett on 3/12/20. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXManager.h" | ||||
| #import "FLEXManager+Extensibility.h" | ||||
| #import "FLEXManager+Networking.h" | ||||
|  | ||||
| #import "FLEXExplorerToolbar.h" | ||||
| #import "FLEXExplorerToolbarItem.h" | ||||
| #import "FLEXGlobalsEntry.h" | ||||
|  | ||||
| #import "FLEX-Core.h" | ||||
| #import "FLEX-Runtime.h" | ||||
| #import "FLEX-Categories.h" | ||||
| #import "FLEX-ObjectExploring.h" | ||||
|  | ||||
| #import "FLEXMacros.h" | ||||
| #import "FLEXAlert.h" | ||||
| #import "FLEXResources.h" | ||||
| @@ -0,0 +1,28 @@ | ||||
| // | ||||
| //  FLEXDBQueryRowCell.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/24. | ||||
| //  Copyright © 2015年 f. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| @class FLEXDBQueryRowCell; | ||||
|  | ||||
| extern NSString * const kFLEXDBQueryRowCellReuse; | ||||
|  | ||||
| @protocol FLEXDBQueryRowCellLayoutSource <NSObject> | ||||
|  | ||||
| - (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell minXForColumn:(NSUInteger)column; | ||||
| - (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell widthForColumn:(NSUInteger)column; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @interface FLEXDBQueryRowCell : UITableViewCell | ||||
|  | ||||
| /// An array of NSString, NSNumber, or NSData objects | ||||
| @property (nonatomic) NSArray *data; | ||||
| @property (nonatomic, weak) id<FLEXDBQueryRowCellLayoutSource> layoutSource; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,75 @@ | ||||
| // | ||||
| //  FLEXDBQueryRowCell.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/24. | ||||
| //  Copyright © 2015年 f. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXDBQueryRowCell.h" | ||||
| #import "FLEXMultiColumnTableView.h" | ||||
| #import "NSArray+FLEX.h" | ||||
| #import "UIFont+FLEX.h" | ||||
| #import "FLEXColor.h" | ||||
|  | ||||
| NSString * const kFLEXDBQueryRowCellReuse = @"kFLEXDBQueryRowCellReuse"; | ||||
|  | ||||
| @interface FLEXDBQueryRowCell () | ||||
| @property (nonatomic) NSInteger columnCount; | ||||
| @property (nonatomic) NSArray<UILabel *> *labels; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXDBQueryRowCell | ||||
|  | ||||
| - (void)setData:(NSArray *)data { | ||||
|     _data = data; | ||||
|     self.columnCount = data.count; | ||||
|      | ||||
|     [self.labels flex_forEach:^(UILabel *label, NSUInteger idx) { | ||||
|         id content = self.data[idx]; | ||||
|          | ||||
|         if ([content isKindOfClass:[NSString class]]) { | ||||
|             label.text = content; | ||||
|         } else if (content == NSNull.null) { | ||||
|             label.text = @"<null>"; | ||||
|             label.textColor = FLEXColor.deemphasizedTextColor; | ||||
|         } else { | ||||
|             label.text = [content description]; | ||||
|         } | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| - (void)setColumnCount:(NSInteger)columnCount { | ||||
|     if (columnCount != _columnCount) { | ||||
|         _columnCount = columnCount; | ||||
|          | ||||
|         // Remove existing labels | ||||
|         for (UILabel *l in self.labels) { | ||||
|             [l removeFromSuperview]; | ||||
|         } | ||||
|          | ||||
|         // Create new labels | ||||
|         self.labels = [NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) { | ||||
|             UILabel *label = [UILabel new]; | ||||
|             label.font = UIFont.flex_defaultTableCellFont; | ||||
|             label.textAlignment = NSTextAlignmentLeft; | ||||
|             [self.contentView addSubview:label]; | ||||
|              | ||||
|             return label; | ||||
|         }]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)layoutSubviews { | ||||
|     [super layoutSubviews]; | ||||
|      | ||||
|     CGFloat height = self.contentView.frame.size.height; | ||||
|      | ||||
|     [self.labels flex_forEach:^(UILabel *label, NSUInteger i) { | ||||
|         CGFloat width = [self.layoutSource dbQueryRowCell:self widthForColumn:i]; | ||||
|         CGFloat minX = [self.layoutSource dbQueryRowCell:self minXForColumn:i]; | ||||
|         label.frame = CGRectMake(minX + 5, 0, (width - 10), height); | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,35 @@ | ||||
| // | ||||
| //  PTDatabaseManager.h | ||||
| //  Derived from: | ||||
| // | ||||
| //  FMDatabase.h | ||||
| //  FMDB( https://github.com/ccgus/fmdb ) | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/23. | ||||
| // | ||||
| //  Licensed to Flying Meat Inc. under one or more contributor license agreements. | ||||
| //  See the LICENSE file distributed with this work for the terms under | ||||
| //  which Flying Meat Inc. licenses this file to you. | ||||
|  | ||||
| #import <Foundation/Foundation.h> | ||||
| #import "FLEXSQLResult.h" | ||||
|  | ||||
| /// Conformers should automatically open and close the database | ||||
| @protocol FLEXDatabaseManager <NSObject> | ||||
|  | ||||
| @required | ||||
|  | ||||
| /// @return \c nil if the database couldn't be opened | ||||
| + (instancetype)managerForDatabase:(NSString *)path; | ||||
|  | ||||
| /// @return a list of all table names | ||||
| - (NSArray<NSString *> *)queryAllTables; | ||||
| - (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName; | ||||
| - (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName; | ||||
|  | ||||
| @optional | ||||
|  | ||||
| - (NSArray<NSString *> *)queryRowIDsInTable:(NSString *)tableName; | ||||
| - (FLEXSQLResult *)executeStatement:(NSString *)SQLStatement; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,47 @@ | ||||
| // | ||||
| //  PTMultiColumnTableView.h | ||||
| //  PTMultiColumnTableViewDemo | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/16. | ||||
| //  Copyright © 2015年 Peng Tao. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
| #import "FLEXTableColumnHeader.h" | ||||
|  | ||||
| @class FLEXMultiColumnTableView; | ||||
|  | ||||
| @protocol FLEXMultiColumnTableViewDelegate <NSObject> | ||||
|  | ||||
| @required | ||||
| - (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didSelectRow:(NSInteger)row; | ||||
| - (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didSelectHeaderForColumn:(NSInteger)column sortType:(FLEXTableColumnHeaderSortType)sortType; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @protocol FLEXMultiColumnTableViewDataSource <NSObject> | ||||
|  | ||||
| @required | ||||
|  | ||||
| - (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView; | ||||
| - (NSInteger)numberOfRowsInTableView:(FLEXMultiColumnTableView *)tableView; | ||||
| - (NSString *)columnTitle:(NSInteger)column; | ||||
| - (NSString *)rowTitle:(NSInteger)row; | ||||
| - (NSArray<NSString *> *)contentForRow:(NSInteger)row; | ||||
|  | ||||
| - (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView minWidthForContentCellInColumn:(NSInteger)column; | ||||
| - (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView heightForContentCellInRow:(NSInteger)row; | ||||
| - (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView; | ||||
| - (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView; | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| @interface FLEXMultiColumnTableView : UIView | ||||
|  | ||||
| @property (nonatomic, weak) id<FLEXMultiColumnTableViewDataSource> dataSource; | ||||
| @property (nonatomic, weak) id<FLEXMultiColumnTableViewDelegate> delegate; | ||||
|  | ||||
| - (void)reloadData; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,339 @@ | ||||
| // | ||||
| //  PTMultiColumnTableView.m | ||||
| //  PTMultiColumnTableViewDemo | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/16. | ||||
| //  Copyright © 2015年 Peng Tao. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXMultiColumnTableView.h" | ||||
| #import "FLEXDBQueryRowCell.h" | ||||
| #import "FLEXTableLeftCell.h" | ||||
| #import "NSArray+FLEX.h" | ||||
| #import "FLEXColor.h" | ||||
|  | ||||
| @interface FLEXMultiColumnTableView () < | ||||
|     UITableViewDataSource, UITableViewDelegate, | ||||
|     UIScrollViewDelegate, FLEXDBQueryRowCellLayoutSource | ||||
| > | ||||
|  | ||||
| @property (nonatomic) UIScrollView *contentScrollView; | ||||
| @property (nonatomic) UIScrollView *headerScrollView; | ||||
| @property (nonatomic) UITableView  *leftTableView; | ||||
| @property (nonatomic) UITableView  *contentTableView; | ||||
| @property (nonatomic) UIView       *leftHeader; | ||||
|  | ||||
| @property (nonatomic) NSArray<UIView *> *headerViews; | ||||
|  | ||||
| /// \c NSNotFound if no column selected | ||||
| @property (nonatomic) NSInteger sortColumn; | ||||
| @property (nonatomic) FLEXTableColumnHeaderSortType sortType; | ||||
|  | ||||
| @property (nonatomic, readonly) NSInteger numberOfColumns; | ||||
| @property (nonatomic, readonly) NSInteger numberOfRows; | ||||
| @property (nonatomic, readonly) CGFloat topHeaderHeight; | ||||
| @property (nonatomic, readonly) CGFloat leftHeaderWidth; | ||||
| @property (nonatomic, readonly) CGFloat columnMargin; | ||||
|  | ||||
| @end | ||||
|  | ||||
| static const CGFloat kColumnMargin = 1; | ||||
|  | ||||
| @implementation FLEXMultiColumnTableView | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| - (instancetype)initWithFrame:(CGRect)frame { | ||||
|     self = [super initWithFrame:frame]; | ||||
|     if (self) { | ||||
|         self.autoresizingMask |= UIViewAutoresizingFlexibleWidth; | ||||
|         self.autoresizingMask |= UIViewAutoresizingFlexibleHeight; | ||||
|         self.autoresizingMask |= UIViewAutoresizingFlexibleTopMargin; | ||||
|         self.backgroundColor  = FLEXColor.groupedBackgroundColor; | ||||
|          | ||||
|         [self loadHeaderScrollView]; | ||||
|         [self loadContentScrollView]; | ||||
|         [self loadLeftView]; | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)layoutSubviews { | ||||
|     [super layoutSubviews]; | ||||
|      | ||||
|     CGFloat width  = self.frame.size.width; | ||||
|     CGFloat height = self.frame.size.height; | ||||
|     CGFloat topheaderHeight = self.topHeaderHeight; | ||||
|     CGFloat leftHeaderWidth = self.leftHeaderWidth; | ||||
|     CGFloat topInsets = 0.f; | ||||
|  | ||||
|     if (@available (iOS 11.0, *)) { | ||||
|         topInsets = self.safeAreaInsets.top; | ||||
|     } | ||||
|      | ||||
|     CGFloat contentWidth = 0.0; | ||||
|     NSInteger columnsCount = self.numberOfColumns; | ||||
|     for (int i = 0; i < columnsCount; i++) { | ||||
|         contentWidth += CGRectGetWidth(self.headerViews[i].bounds); | ||||
|     } | ||||
|      | ||||
|     CGFloat contentHeight = height - topheaderHeight - topInsets; | ||||
|      | ||||
|     self.leftHeader.frame = CGRectMake(0, topInsets, self.leftHeaderWidth, self.topHeaderHeight); | ||||
|     self.leftTableView.frame = CGRectMake( | ||||
|         0, topheaderHeight + topInsets, leftHeaderWidth, contentHeight | ||||
|     ); | ||||
|     self.headerScrollView.frame = CGRectMake( | ||||
|         leftHeaderWidth, topInsets, width - leftHeaderWidth, topheaderHeight | ||||
|     ); | ||||
|     self.headerScrollView.contentSize = CGSizeMake( | ||||
|         self.contentTableView.frame.size.width, self.headerScrollView.frame.size.height | ||||
|     ); | ||||
|     self.contentTableView.frame = CGRectMake( | ||||
|         0, 0, contentWidth + self.numberOfColumns * self.columnMargin , contentHeight | ||||
|     ); | ||||
|     self.contentScrollView.frame = CGRectMake( | ||||
|         leftHeaderWidth, topheaderHeight + topInsets, width - leftHeaderWidth, contentHeight | ||||
|     ); | ||||
|     self.contentScrollView.contentSize = self.contentTableView.frame.size; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - UI | ||||
|  | ||||
| - (void)loadHeaderScrollView { | ||||
|     UIScrollView *headerScrollView   = [UIScrollView new]; | ||||
|     headerScrollView.delegate        = self; | ||||
|     headerScrollView.backgroundColor = FLEXColor.secondaryGroupedBackgroundColor; | ||||
|     self.headerScrollView            = headerScrollView; | ||||
|      | ||||
|     [self addSubview:headerScrollView]; | ||||
| } | ||||
|  | ||||
| - (void)loadContentScrollView { | ||||
|     UIScrollView *scrollView = [UIScrollView new]; | ||||
|     scrollView.bounces       = NO; | ||||
|     scrollView.delegate      = self; | ||||
|      | ||||
|     UITableView *tableView   = [UITableView new]; | ||||
|     tableView.delegate       = self; | ||||
|     tableView.dataSource     = self; | ||||
|     tableView.separatorStyle = UITableViewCellSeparatorStyleNone; | ||||
|     [tableView registerClass:[FLEXDBQueryRowCell class] | ||||
|         forCellReuseIdentifier:kFLEXDBQueryRowCellReuse | ||||
|     ]; | ||||
|      | ||||
|     [scrollView addSubview:tableView]; | ||||
|     [self addSubview:scrollView]; | ||||
|      | ||||
|     self.contentScrollView = scrollView; | ||||
|     self.contentTableView  = tableView; | ||||
| } | ||||
|  | ||||
| - (void)loadLeftView { | ||||
|     UITableView *leftTableView   = [UITableView new]; | ||||
|     leftTableView.delegate       = self; | ||||
|     leftTableView.dataSource     = self; | ||||
|     leftTableView.separatorStyle = UITableViewCellSeparatorStyleNone; | ||||
|     self.leftTableView           = leftTableView; | ||||
|     [self addSubview:leftTableView]; | ||||
|      | ||||
|     UIView *leftHeader         = [UIView new]; | ||||
|     leftHeader.backgroundColor = FLEXColor.secondaryBackgroundColor; | ||||
|     self.leftHeader            = leftHeader; | ||||
|     [self addSubview:leftHeader]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Data | ||||
|  | ||||
| - (void)reloadData { | ||||
|     [self loadHeaderData]; | ||||
|     [self loadLeftViewData]; | ||||
|     [self loadContentData]; | ||||
| } | ||||
|  | ||||
| - (void)loadHeaderData { | ||||
|     // Remove existing headers, if any | ||||
|     for (UIView *subview in self.headerViews) { | ||||
|         [subview removeFromSuperview]; | ||||
|     } | ||||
|      | ||||
|     __block CGFloat xOffset = 0; | ||||
|      | ||||
|     self.headerViews = [NSArray flex_forEachUpTo:self.numberOfColumns map:^id(NSUInteger column) { | ||||
|         FLEXTableColumnHeader *header = [FLEXTableColumnHeader new]; | ||||
|         header.titleLabel.text = [self columnTitle:column]; | ||||
|          | ||||
|         CGSize fittingSize = CGSizeMake(CGFLOAT_MAX, self.topHeaderHeight - 1); | ||||
|         CGFloat width = self.columnMargin + MAX( | ||||
|             [self minContentWidthForColumn:column], | ||||
|             [header sizeThatFits:fittingSize].width | ||||
|         ); | ||||
|         header.frame = CGRectMake(xOffset, 0, width, self.topHeaderHeight - 1); | ||||
|  | ||||
|         if (column == self.sortColumn) { | ||||
|             header.sortType = self.sortType; | ||||
|         } | ||||
|          | ||||
|         // Header tap gesture | ||||
|         UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] | ||||
|             initWithTarget:self action:@selector(contentHeaderTap:) | ||||
|         ]; | ||||
|         [header addGestureRecognizer:gesture]; | ||||
|         header.userInteractionEnabled = YES; | ||||
|          | ||||
|         xOffset += width; | ||||
|         [self.headerScrollView addSubview:header]; | ||||
|         return header; | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| - (void)contentHeaderTap:(UIGestureRecognizer *)gesture { | ||||
|     NSInteger newSortColumn = [self.headerViews indexOfObject:gesture.view]; | ||||
|     FLEXTableColumnHeaderSortType newType = FLEXNextTableColumnHeaderSortType(self.sortType); | ||||
|      | ||||
|     // Reset old header | ||||
|     FLEXTableColumnHeader *oldHeader = (id)self.headerViews[self.sortColumn]; | ||||
|     oldHeader.sortType = FLEXTableColumnHeaderSortTypeNone; | ||||
|      | ||||
|     // Update new header | ||||
|     FLEXTableColumnHeader *newHeader = (id)self.headerViews[newSortColumn]; | ||||
|     newHeader.sortType = newType; | ||||
|      | ||||
|     // Update self | ||||
|     self.sortColumn = newSortColumn; | ||||
|     self.sortType = newType; | ||||
|  | ||||
|     // Notify delegate | ||||
|     [self.delegate multiColumnTableView:self didSelectHeaderForColumn:newSortColumn sortType:newType]; | ||||
| } | ||||
|  | ||||
| - (void)loadContentData { | ||||
|     [self.contentTableView reloadData]; | ||||
| } | ||||
|  | ||||
| - (void)loadLeftViewData { | ||||
|     [self.leftTableView reloadData]; | ||||
| } | ||||
|  | ||||
| - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     // Alternating background color | ||||
|     UIColor *backgroundColor = FLEXColor.primaryBackgroundColor; | ||||
|     if (indexPath.row % 2 != 0) { | ||||
|         backgroundColor = FLEXColor.secondaryBackgroundColor; | ||||
|     } | ||||
|      | ||||
|     // Left side table view for row numbers | ||||
|     if (tableView == self.leftTableView) { | ||||
|         FLEXTableLeftCell *cell = [FLEXTableLeftCell cellWithTableView:tableView]; | ||||
|         cell.contentView.backgroundColor = backgroundColor; | ||||
|         cell.titlelabel.text = [self rowTitle:indexPath.row]; | ||||
|         return cell; | ||||
|     } | ||||
|     // Right side table view for data | ||||
|     else { | ||||
|         FLEXDBQueryRowCell *cell = [tableView | ||||
|             dequeueReusableCellWithIdentifier:kFLEXDBQueryRowCellReuse forIndexPath:indexPath | ||||
|         ]; | ||||
|          | ||||
|         cell.contentView.backgroundColor = backgroundColor; | ||||
|         cell.data = [self.dataSource contentForRow:indexPath.row]; | ||||
|         cell.layoutSource = self; | ||||
|         NSAssert(cell.data.count == self.numberOfColumns, @"Count of data provided was incorrect"); | ||||
|         return cell; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { | ||||
|     return [self.dataSource numberOfRowsInTableView:self]; | ||||
| } | ||||
|  | ||||
| - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     return [self.dataSource multiColumnTableView:self heightForContentCellInRow:indexPath.row]; | ||||
| } | ||||
|  | ||||
| // Scroll all scroll views in sync | ||||
| - (void)scrollViewDidScroll:(UIScrollView *)scrollView { | ||||
|     if (scrollView == self.contentScrollView) { | ||||
|         self.headerScrollView.contentOffset = scrollView.contentOffset; | ||||
|     } | ||||
|     else if (scrollView == self.headerScrollView) { | ||||
|         self.contentScrollView.contentOffset = scrollView.contentOffset; | ||||
|     } | ||||
|     else if (scrollView == self.leftTableView) { | ||||
|         self.contentTableView.contentOffset = scrollView.contentOffset; | ||||
|     } | ||||
|     else if (scrollView == self.contentTableView) { | ||||
|         self.leftTableView.contentOffset = scrollView.contentOffset; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark UITableView Delegate | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     if (tableView == self.leftTableView) { | ||||
|         [self.contentTableView | ||||
|             selectRowAtIndexPath:indexPath | ||||
|             animated:NO | ||||
|             scrollPosition:UITableViewScrollPositionNone | ||||
|         ]; | ||||
|     } | ||||
|     else if (tableView == self.contentTableView) { | ||||
|         [self.delegate multiColumnTableView:self didSelectRow:indexPath.row]; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark FLEXDBQueryRowCellLayoutSource | ||||
|  | ||||
| - (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell minXForColumn:(NSUInteger)column { | ||||
|     return CGRectGetMinX(self.headerViews[column].frame); | ||||
| } | ||||
|  | ||||
| - (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell widthForColumn:(NSUInteger)column { | ||||
|     return CGRectGetWidth(self.headerViews[column].bounds); | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark DataSource Accessor | ||||
|  | ||||
| - (NSInteger)numberOfRows { | ||||
|     return [self.dataSource numberOfRowsInTableView:self]; | ||||
| } | ||||
|  | ||||
| - (NSInteger)numberOfColumns { | ||||
|     return [self.dataSource numberOfColumnsInTableView:self]; | ||||
| } | ||||
|  | ||||
| - (NSString *)columnTitle:(NSInteger)column { | ||||
|     return [self.dataSource columnTitle:column]; | ||||
| } | ||||
|  | ||||
| - (NSString *)rowTitle:(NSInteger)row { | ||||
|     return [self.dataSource rowTitle:row]; | ||||
| } | ||||
|  | ||||
| - (CGFloat)minContentWidthForColumn:(NSInteger)column { | ||||
|     return [self.dataSource multiColumnTableView:self minWidthForContentCellInColumn:column]; | ||||
| } | ||||
|  | ||||
| - (CGFloat)contentHeightForRow:(NSInteger)row { | ||||
|     return [self.dataSource multiColumnTableView:self heightForContentCellInRow:row]; | ||||
| } | ||||
|  | ||||
| - (CGFloat)topHeaderHeight { | ||||
|     return [self.dataSource heightForTopHeaderInTableView:self]; | ||||
| } | ||||
|  | ||||
| - (CGFloat)leftHeaderWidth { | ||||
|     return [self.dataSource widthForLeftHeaderInTableView:self]; | ||||
| } | ||||
|  | ||||
| - (CGFloat)columnMargin { | ||||
|     return kColumnMargin; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,14 @@ | ||||
| // | ||||
| //  FLEXRealmDatabaseManager.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tim Oliver on 28/01/2016. | ||||
| //  Copyright © 2016 Realm. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <Foundation/Foundation.h> | ||||
| #import "FLEXDatabaseManager.h" | ||||
|  | ||||
| @interface FLEXRealmDatabaseManager : NSObject <FLEXDatabaseManager> | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,102 @@ | ||||
| // | ||||
| //  FLEXRealmDatabaseManager.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tim Oliver on 28/01/2016. | ||||
| //  Copyright © 2016 Realm. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXRealmDatabaseManager.h" | ||||
| #import "NSArray+FLEX.h" | ||||
| #import "FLEXSQLResult.h" | ||||
|  | ||||
| #if __has_include(<Realm/Realm.h>) | ||||
| #import <Realm/Realm.h> | ||||
| #import <Realm/RLMRealm_Dynamic.h> | ||||
| #else | ||||
| #import "FLEXRealmDefines.h" | ||||
| #endif | ||||
|  | ||||
| @interface FLEXRealmDatabaseManager () | ||||
|  | ||||
| @property (nonatomic, copy) NSString *path; | ||||
| @property (nonatomic) RLMRealm *realm; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FLEXRealmDatabaseManager | ||||
| static Class RLMRealmClass = nil; | ||||
|  | ||||
| + (void)load { | ||||
|     RLMRealmClass = NSClassFromString(@"RLMRealm"); | ||||
| } | ||||
|  | ||||
| + (instancetype)managerForDatabase:(NSString *)path { | ||||
|     return [[self alloc] initWithPath:path]; | ||||
| } | ||||
|  | ||||
| - (instancetype)initWithPath:(NSString *)path { | ||||
|     if (!RLMRealmClass) { | ||||
|         return nil; | ||||
|     } | ||||
|      | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         _path = path; | ||||
|          | ||||
|         if (![self open]) { | ||||
|             return nil; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (BOOL)open { | ||||
|     Class configurationClass = NSClassFromString(@"RLMRealmConfiguration"); | ||||
|     if (!RLMRealmClass || !configurationClass) { | ||||
|         return NO; | ||||
|     } | ||||
|      | ||||
|     NSError *error = nil; | ||||
|     id configuration = [configurationClass new]; | ||||
|     [(RLMRealmConfiguration *)configuration setFileURL:[NSURL fileURLWithPath:self.path]]; | ||||
|     self.realm = [RLMRealmClass realmWithConfiguration:configuration error:&error]; | ||||
|      | ||||
|     return (error == nil); | ||||
| } | ||||
|  | ||||
| - (NSArray<NSString *> *)queryAllTables { | ||||
|     // Map each schema to its name | ||||
|     NSArray<NSString *> *tableNames = [self.realm.schema.objectSchema flex_mapped:^id(RLMObjectSchema *schema, NSUInteger idx) { | ||||
|         return schema.className ?: nil; | ||||
|     }]; | ||||
|  | ||||
|     return [tableNames sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; | ||||
| } | ||||
|  | ||||
| - (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName { | ||||
|     RLMObjectSchema *objectSchema = [self.realm.schema schemaForClassName:tableName]; | ||||
|     // Map each column to its name | ||||
|     return [objectSchema.properties flex_mapped:^id(RLMProperty *property, NSUInteger idx) { | ||||
|         return property.name; | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| - (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName { | ||||
|     RLMObjectSchema *objectSchema = [self.realm.schema schemaForClassName:tableName]; | ||||
|     RLMResults *results = [self.realm allObjects:tableName]; | ||||
|     if (results.count == 0 || !objectSchema) { | ||||
|         return nil; | ||||
|     } | ||||
|      | ||||
|     // Map results to an array of rows | ||||
|     return [NSArray flex_mapped:results block:^id(RLMObject *result, NSUInteger idx) { | ||||
|         // Map each row to an array of the values of its properties  | ||||
|         return [objectSchema.properties flex_mapped:^id(RLMProperty *property, NSUInteger idx) { | ||||
|             return [result valueForKey:property.name] ?: NSNull.null; | ||||
|         }]; | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,46 @@ | ||||
| // | ||||
| //  Realm.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tim Oliver on 16/02/2016. | ||||
| //  Copyright © 2016 Realm. All rights reserved. | ||||
| // | ||||
|  | ||||
| #if __has_include(<Realm/Realm.h>) | ||||
| #else | ||||
|  | ||||
| @class RLMObject, RLMResults, RLMRealm, RLMRealmConfiguration, RLMSchema, RLMObjectSchema, RLMProperty; | ||||
|  | ||||
| @interface RLMRealmConfiguration : NSObject | ||||
| @property (nonatomic, copy) NSURL *fileURL; | ||||
| @end | ||||
|  | ||||
| @interface RLMRealm : NSObject | ||||
| @property (nonatomic, readonly) RLMSchema *schema; | ||||
| + (RLMRealm *)realmWithConfiguration:(RLMRealmConfiguration *)configuration error:(NSError **)error; | ||||
| - (RLMResults *)allObjects:(NSString *)className; | ||||
| @end | ||||
|  | ||||
| @interface RLMSchema : NSObject | ||||
| @property (nonatomic, readonly) NSArray<RLMObjectSchema *> *objectSchema; | ||||
| - (RLMObjectSchema *)schemaForClassName:(NSString *)className; | ||||
| @end | ||||
|  | ||||
| @interface RLMObjectSchema : NSObject | ||||
| @property (nonatomic, readonly) NSString *className; | ||||
| @property (nonatomic, readonly) NSArray<RLMProperty *> *properties; | ||||
| @end | ||||
|  | ||||
| @interface RLMProperty : NSString | ||||
| @property (nonatomic, readonly) NSString *name; | ||||
| @end | ||||
|  | ||||
| @interface RLMResults : NSObject <NSFastEnumeration> | ||||
| @property (nonatomic, readonly) NSInteger count; | ||||
| @end | ||||
|  | ||||
| @interface RLMObject : NSObject | ||||
|  | ||||
| @end | ||||
|  | ||||
| #endif | ||||
| @@ -0,0 +1,48 @@ | ||||
| // | ||||
| //  FLEXSQLResult.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/3/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <Foundation/Foundation.h> | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FLEXSQLResult : NSObject | ||||
|  | ||||
| /// Describes the result of a non-select query, or an error of any kind of query | ||||
| + (instancetype)message:(NSString *)message; | ||||
| /// Describes the result of a known failed execution | ||||
| + (instancetype)error:(NSString *)message; | ||||
|  | ||||
| /// @param rowData A list of rows, where each element in the row | ||||
| /// corresponds to the column given in /c columnNames | ||||
| + (instancetype)columns:(NSArray<NSString *> *)columnNames | ||||
|                 rows:(NSArray<NSArray<NSString *> *> *)rowData; | ||||
|  | ||||
| @property (nonatomic, readonly, nullable) NSString *message; | ||||
|  | ||||
| /// A value of YES means this is surely an error, | ||||
| /// but it still might be an error even with a value of NO | ||||
| @property (nonatomic, readonly) BOOL isError; | ||||
|  | ||||
| /// A list of column names | ||||
| @property (nonatomic, readonly, nullable) NSArray<NSString *> *columns; | ||||
| /// A list of rows, where each element in the row corresponds | ||||
| /// to the value of the column at the same index in \c columns. | ||||
| /// | ||||
| /// That is, given a row, looping over the contents of the row and | ||||
| /// the contents of \c columns will give you key-value pairs of | ||||
| /// column names to column values for that row. | ||||
| @property (nonatomic, readonly, nullable) NSArray<NSArray<NSString *> *> *rows; | ||||
| /// A list of rows where the fields are paired to column names. | ||||
| /// | ||||
| /// This property is lazily constructed by looping over | ||||
| /// the rows and columns present in the other two properties. | ||||
| @property (nonatomic, readonly, nullable) NSArray<NSDictionary<NSString *, id> *> *keyedRows; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
| @@ -0,0 +1,53 @@ | ||||
| // | ||||
| //  FLEXSQLResult.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/3/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXSQLResult.h" | ||||
| #import "NSArray+FLEX.h" | ||||
|  | ||||
| @implementation FLEXSQLResult | ||||
| @synthesize keyedRows = _keyedRows; | ||||
|  | ||||
| + (instancetype)message:(NSString *)message { | ||||
|     return [[self alloc] initWithMessage:message columns:nil rows:nil]; | ||||
| } | ||||
|  | ||||
| + (instancetype)error:(NSString *)message { | ||||
|     FLEXSQLResult *result = [self message:message]; | ||||
|     result->_isError = YES; | ||||
|     return result; | ||||
| } | ||||
|  | ||||
| + (instancetype)columns:(NSArray<NSString *> *)columnNames rows:(NSArray<NSArray<NSString *> *> *)rowData { | ||||
|     return [[self alloc] initWithMessage:nil columns:columnNames rows:rowData]; | ||||
| } | ||||
|  | ||||
| - (instancetype)initWithMessage:(NSString *)message columns:(NSArray<NSString *> *)columns rows:(NSArray<NSArray<NSString *> *> *)rows { | ||||
|     NSParameterAssert(message || (columns && rows)); | ||||
|     NSParameterAssert(rows.count == 0 || columns.count == rows.firstObject.count); | ||||
|      | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         _message = message; | ||||
|         _columns = columns; | ||||
|         _rows = rows; | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (NSArray<NSDictionary<NSString *,id> *> *)keyedRows { | ||||
|     if (!_keyedRows) { | ||||
|         _keyedRows = [self.rows flex_mapped:^id(NSArray<NSString *> *row, NSUInteger idx) { | ||||
|             return [NSDictionary dictionaryWithObjects:row forKeys:self.columns]; | ||||
|         }]; | ||||
|     } | ||||
|      | ||||
|     return _keyedRows; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,32 @@ | ||||
| // | ||||
| //  PTDatabaseManager.h | ||||
| //  Derived from: | ||||
| // | ||||
| //  FMDatabase.h | ||||
| //  FMDB( https://github.com/ccgus/fmdb ) | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/23. | ||||
| // | ||||
| //  Licensed to Flying Meat Inc. under one or more contributor license agreements. | ||||
| //  See the LICENSE file distributed with this work for the terms under | ||||
| //  which Flying Meat Inc. licenses this file to you. | ||||
|  | ||||
| #import <Foundation/Foundation.h> | ||||
| #import "FLEXDatabaseManager.h" | ||||
| #import "FLEXSQLResult.h" | ||||
|  | ||||
| @interface FLEXSQLiteDatabaseManager : NSObject <FLEXDatabaseManager> | ||||
|  | ||||
| /// Contains the result of the last operation, which may be an error | ||||
| @property (nonatomic, readonly) FLEXSQLResult *lastResult; | ||||
| /// Calls into \c sqlite3_last_insert_rowid() | ||||
| @property (nonatomic, readonly) NSInteger lastRowID; | ||||
|  | ||||
| /// Given a statement like 'SELECT * from @table where @col = @val' and arguments | ||||
| /// like { @"table": @"Album", @"col": @"year", @"val" @1 } this method will | ||||
| /// invoke the statement and properly bind the given arguments to the statement. | ||||
| /// | ||||
| /// You may pass NSStrings, NSData, NSNumbers, or NSNulls as values. | ||||
| - (FLEXSQLResult *)executeStatement:(NSString *)statement arguments:(NSDictionary<NSString *, id> *)args; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,329 @@ | ||||
| // | ||||
| //  PTDatabaseManager.m | ||||
| //  PTDatabaseReader | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/23. | ||||
| //  Copyright © 2015年 Peng Tao. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXSQLiteDatabaseManager.h" | ||||
| #import "FLEXManager.h" | ||||
| #import "NSArray+FLEX.h" | ||||
| #import "FLEXRuntimeConstants.h" | ||||
| #import <sqlite3.h> | ||||
|  | ||||
| #define kQuery(name, str) static NSString * const QUERY_##name = str | ||||
|  | ||||
| kQuery(TABLENAMES, @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"); | ||||
| kQuery(ROWIDS, @"SELECT rowid FROM \"%@\" ORDER BY rowid ASC"); | ||||
|  | ||||
| @interface FLEXSQLiteDatabaseManager () | ||||
| @property (nonatomic) sqlite3 *db; | ||||
| @property (nonatomic, copy) NSString *path; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXSQLiteDatabaseManager | ||||
|  | ||||
| #pragma mark - FLEXDatabaseManager | ||||
|  | ||||
| + (instancetype)managerForDatabase:(NSString *)path { | ||||
|     return [[self alloc] initWithPath:path]; | ||||
| } | ||||
|  | ||||
| - (instancetype)initWithPath:(NSString *)path { | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         self.path = path; | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)dealloc { | ||||
|     [self close]; | ||||
| } | ||||
|  | ||||
| - (BOOL)open { | ||||
|     if (self.db) { | ||||
|         return YES; | ||||
|     } | ||||
|      | ||||
|     int err = sqlite3_open(self.path.UTF8String, &_db); | ||||
|  | ||||
| #if SQLITE_HAS_CODEC | ||||
|     NSString *defaultSqliteDatabasePassword = FLEXManager.sharedManager.defaultSqliteDatabasePassword; | ||||
|     if (defaultSqliteDatabasePassword) { | ||||
|         const char *key = defaultSqliteDatabasePassword.UTF8String; | ||||
|         sqlite3_key(_db, key, (int)strlen(key)); | ||||
|     } | ||||
| #endif | ||||
|  | ||||
|     if (err != SQLITE_OK) { | ||||
|         return [self storeErrorForLastTask:@"Open"]; | ||||
|     } | ||||
|      | ||||
|     return YES; | ||||
| } | ||||
|      | ||||
| - (BOOL)close { | ||||
|     if (!self.db) { | ||||
|         return YES; | ||||
|     } | ||||
|      | ||||
|     int  rc; | ||||
|     BOOL retry, triedFinalizingOpenStatements = NO; | ||||
|      | ||||
|     do { | ||||
|         retry = NO; | ||||
|         rc    = sqlite3_close(_db); | ||||
|         if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) { | ||||
|             if (!triedFinalizingOpenStatements) { | ||||
|                 triedFinalizingOpenStatements = YES; | ||||
|                 sqlite3_stmt *pStmt; | ||||
|                 while ((pStmt = sqlite3_next_stmt(_db, nil)) !=0) { | ||||
|                     NSLog(@"Closing leaked statement"); | ||||
|                     sqlite3_finalize(pStmt); | ||||
|                     retry = YES; | ||||
|                 } | ||||
|             } | ||||
|         } else if (SQLITE_OK != rc) { | ||||
|             [self storeErrorForLastTask:@"Close"]; | ||||
|             self.db = nil; | ||||
|             return NO; | ||||
|         } | ||||
|     } while (retry); | ||||
|      | ||||
|     self.db = nil; | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (NSInteger)lastRowID { | ||||
|     return (NSInteger)sqlite3_last_insert_rowid(self.db); | ||||
| } | ||||
|  | ||||
| - (NSArray<NSString *> *)queryAllTables { | ||||
|     return [[self executeStatement:QUERY_TABLENAMES].rows flex_mapped:^id(NSArray *table, NSUInteger idx) { | ||||
|         return table.firstObject; | ||||
|     }] ?: @[]; | ||||
| } | ||||
|  | ||||
| - (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName { | ||||
|     NSString *sql = [NSString stringWithFormat:@"PRAGMA table_info('%@')",tableName]; | ||||
|     FLEXSQLResult *results = [self executeStatement:sql]; | ||||
|      | ||||
|     // https://github.com/FLEXTool/FLEX/issues/554 | ||||
|     if (!results.keyedRows.count) { | ||||
|         sql = [NSString stringWithFormat:@"SELECT * FROM pragma_table_info('%@')", tableName]; | ||||
|         results = [self executeStatement:sql]; | ||||
|          | ||||
|         // Fallback to empty query | ||||
|         if (!results.keyedRows.count) { | ||||
|             sql = [NSString stringWithFormat:@"SELECT * FROM \"%@\" where 0=1", tableName]; | ||||
|             return [self executeStatement:sql].columns ?: @[]; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return [results.keyedRows flex_mapped:^id(NSDictionary *column, NSUInteger idx) { | ||||
|         return column[@"name"]; | ||||
|     }] ?: @[]; | ||||
| } | ||||
|  | ||||
| - (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName { | ||||
|     NSString *command = [NSString stringWithFormat:@"SELECT * FROM \"%@\"", tableName]; | ||||
|     return [self executeStatement:command].rows ?: @[]; | ||||
| } | ||||
|  | ||||
| - (NSArray<NSString *> *)queryRowIDsInTable:(NSString *)tableName { | ||||
|     NSString *command = [NSString stringWithFormat:QUERY_ROWIDS, tableName]; | ||||
|     NSArray<NSArray<NSString *> *> *data = [self executeStatement:command].rows ?: @[]; | ||||
|      | ||||
|     return [data flex_mapped:^id(NSArray<NSString *> *obj, NSUInteger idx) { | ||||
|         return obj.firstObject; | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| - (FLEXSQLResult *)executeStatement:(NSString *)sql { | ||||
|     return [self executeStatement:sql arguments:nil]; | ||||
| } | ||||
|  | ||||
| - (FLEXSQLResult *)executeStatement:(NSString *)sql arguments:(NSDictionary *)args { | ||||
|     [self open]; | ||||
|      | ||||
|     FLEXSQLResult *result = nil; | ||||
|      | ||||
|     sqlite3_stmt *pstmt; | ||||
|     int status; | ||||
|     if ((status = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &pstmt, 0)) == SQLITE_OK) { | ||||
|         NSMutableArray<NSArray *> *rows = [NSMutableArray new]; | ||||
|          | ||||
|         // Bind parameters, if any | ||||
|         if (![self bindParameters:args toStatement:pstmt]) { | ||||
|             return self.lastResult; | ||||
|         } | ||||
|          | ||||
|         // Grab columns (columnCount will be 0 for insert/update/delete)  | ||||
|         int columnCount = sqlite3_column_count(pstmt); | ||||
|         NSArray<NSString *> *columns = [NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) { | ||||
|             return @(sqlite3_column_name(pstmt, (int)i)); | ||||
|         }]; | ||||
|          | ||||
|         // Execute statement | ||||
|         while ((status = sqlite3_step(pstmt)) == SQLITE_ROW) { | ||||
|             // Grab rows if this is a selection query | ||||
|             int dataCount = sqlite3_data_count(pstmt); | ||||
|             if (dataCount > 0) { | ||||
|                 [rows addObject:[NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) { | ||||
|                     return [self objectForColumnIndex:(int)i stmt:pstmt]; | ||||
|                 }]]; | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         if (status == SQLITE_DONE) { | ||||
|             // columnCount will be 0 for insert/update/delete | ||||
|             if (rows.count || columnCount > 0) { | ||||
|                 // We executed a SELECT query | ||||
|                 result = _lastResult = [FLEXSQLResult columns:columns rows:rows]; | ||||
|             } else { | ||||
|                 // We executed a query like INSERT, UDPATE, or DELETE | ||||
|                 int rowsAffected = sqlite3_changes(_db); | ||||
|                 NSString *message = [NSString stringWithFormat:@"%d row(s) affected", rowsAffected]; | ||||
|                 result = _lastResult = [FLEXSQLResult message:message]; | ||||
|             } | ||||
|         } else { | ||||
|             // An error occured executing the query | ||||
|             result = _lastResult = [self errorResult:@"Execution"]; | ||||
|         } | ||||
|     } else { | ||||
|         // An error occurred creating the prepared statement | ||||
|         result = _lastResult = [self errorResult:@"Prepared statement"]; | ||||
|     } | ||||
|      | ||||
|     sqlite3_finalize(pstmt); | ||||
|     return result; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Private | ||||
|  | ||||
| /// @return YES on success, NO if an error was encountered and stored in \c lastResult | ||||
| - (BOOL)bindParameters:(NSDictionary *)args toStatement:(sqlite3_stmt *)pstmt { | ||||
|     for (NSString *param in args.allKeys) { | ||||
|         int status = SQLITE_OK, idx = sqlite3_bind_parameter_index(pstmt, param.UTF8String); | ||||
|         id value = args[param]; | ||||
|          | ||||
|         if (idx == 0) { | ||||
|             // No parameter matching that arg | ||||
|             @throw NSInternalInconsistencyException; | ||||
|         } | ||||
|          | ||||
|         // Null | ||||
|         if ([value isKindOfClass:[NSNull class]]) { | ||||
|             status = sqlite3_bind_null(pstmt, idx); | ||||
|         } | ||||
|         // String params | ||||
|         else if ([value isKindOfClass:[NSString class]]) { | ||||
|             const char *str = [value UTF8String]; | ||||
|             status = sqlite3_bind_text(pstmt, idx, str, (int)strlen(str), SQLITE_TRANSIENT); | ||||
|         } | ||||
|         // Data params | ||||
|         else if ([value isKindOfClass:[NSData class]]) { | ||||
|             const void *blob = [value bytes]; | ||||
|             status = sqlite3_bind_blob64(pstmt, idx, blob, [value length], SQLITE_TRANSIENT); | ||||
|         } | ||||
|         // Primitive params | ||||
|         else if ([value isKindOfClass:[NSNumber class]]) { | ||||
|             FLEXTypeEncoding type = [value objCType][0]; | ||||
|             switch (type) { | ||||
|                 case FLEXTypeEncodingCBool: | ||||
|                 case FLEXTypeEncodingChar: | ||||
|                 case FLEXTypeEncodingUnsignedChar: | ||||
|                 case FLEXTypeEncodingShort: | ||||
|                 case FLEXTypeEncodingUnsignedShort: | ||||
|                 case FLEXTypeEncodingInt: | ||||
|                 case FLEXTypeEncodingUnsignedInt: | ||||
|                 case FLEXTypeEncodingLong: | ||||
|                 case FLEXTypeEncodingUnsignedLong: | ||||
|                 case FLEXTypeEncodingLongLong: | ||||
|                 case FLEXTypeEncodingUnsignedLongLong: | ||||
|                     status = sqlite3_bind_int64(pstmt, idx, (sqlite3_int64)[value longValue]); | ||||
|                     break; | ||||
|                  | ||||
|                 case FLEXTypeEncodingFloat: | ||||
|                 case FLEXTypeEncodingDouble: | ||||
|                     status = sqlite3_bind_double(pstmt, idx, [value doubleValue]); | ||||
|                     break; | ||||
|                      | ||||
|                 default: | ||||
|                     @throw NSInternalInconsistencyException; | ||||
|                     break; | ||||
|             } | ||||
|         } | ||||
|         // Unsupported type | ||||
|         else { | ||||
|             @throw NSInternalInconsistencyException; | ||||
|         } | ||||
|          | ||||
|         if (status != SQLITE_OK) { | ||||
|             return [self storeErrorForLastTask: | ||||
|                 [NSString stringWithFormat:@"Binding param named '%@'", param] | ||||
|             ]; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (BOOL)storeErrorForLastTask:(NSString *)action { | ||||
|     _lastResult = [self errorResult:action]; | ||||
|     return NO; | ||||
| } | ||||
|  | ||||
| - (FLEXSQLResult *)errorResult:(NSString *)description { | ||||
|     const char *error = sqlite3_errmsg(_db); | ||||
|     NSString *message = error ? @(error) : [NSString | ||||
|         stringWithFormat:@"(%@: empty error)", description | ||||
|     ]; | ||||
|      | ||||
|     return [FLEXSQLResult error:message]; | ||||
| } | ||||
|  | ||||
| - (id)objectForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt*)stmt { | ||||
|     int columnType = sqlite3_column_type(stmt, columnIdx); | ||||
|      | ||||
|     switch (columnType) { | ||||
|         case SQLITE_INTEGER: | ||||
|             return @(sqlite3_column_int64(stmt, columnIdx)).stringValue; | ||||
|         case SQLITE_FLOAT: | ||||
|             return  @(sqlite3_column_double(stmt, columnIdx)).stringValue; | ||||
|         case SQLITE_BLOB: | ||||
|             return [NSString stringWithFormat:@"Data (%@ bytes)", | ||||
|                 @([self dataForColumnIndex:columnIdx stmt:stmt].length) | ||||
|             ]; | ||||
|              | ||||
|         default: | ||||
|             // Default to a string for everything else | ||||
|             return [self stringForColumnIndex:columnIdx stmt:stmt] ?: NSNull.null; | ||||
|     } | ||||
| } | ||||
|                  | ||||
| - (NSString *)stringForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt *)stmt { | ||||
|     if (sqlite3_column_type(stmt, columnIdx) == SQLITE_NULL || columnIdx < 0) { | ||||
|         return nil; | ||||
|     } | ||||
|      | ||||
|     const char *text = (const char *)sqlite3_column_text(stmt, columnIdx); | ||||
|     return text ? @(text) : nil; | ||||
| } | ||||
|  | ||||
| - (NSData *)dataForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt *)stmt { | ||||
|     if (sqlite3_column_type(stmt, columnIdx) == SQLITE_NULL || (columnIdx < 0)) { | ||||
|         return nil; | ||||
|     } | ||||
|      | ||||
|     const void *blob = sqlite3_column_blob(stmt, columnIdx); | ||||
|     NSInteger size = (NSInteger)sqlite3_column_bytes(stmt, columnIdx); | ||||
|      | ||||
|     return blob ? [NSData dataWithBytes:blob length:size] : nil; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,38 @@ | ||||
| // | ||||
| //  FLEXTableContentHeaderCell.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/26. | ||||
| //  Copyright © 2015年 f. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| typedef NS_ENUM(NSUInteger, FLEXTableColumnHeaderSortType) { | ||||
|     FLEXTableColumnHeaderSortTypeNone = 0, | ||||
|     FLEXTableColumnHeaderSortTypeAsc, | ||||
|     FLEXTableColumnHeaderSortTypeDesc, | ||||
| }; | ||||
|  | ||||
| NS_INLINE FLEXTableColumnHeaderSortType FLEXNextTableColumnHeaderSortType( | ||||
|     FLEXTableColumnHeaderSortType current) { | ||||
|     switch (current) { | ||||
|         case FLEXTableColumnHeaderSortTypeAsc: | ||||
|             return FLEXTableColumnHeaderSortTypeDesc; | ||||
|         case FLEXTableColumnHeaderSortTypeNone: | ||||
|         case FLEXTableColumnHeaderSortTypeDesc: | ||||
|             return FLEXTableColumnHeaderSortTypeAsc; | ||||
|     } | ||||
|      | ||||
|     return FLEXTableColumnHeaderSortTypeNone; | ||||
| } | ||||
|  | ||||
| @interface FLEXTableColumnHeader : UIView | ||||
|  | ||||
| @property (nonatomic) NSInteger index; | ||||
| @property (nonatomic, readonly) UILabel *titleLabel; | ||||
|  | ||||
| @property (nonatomic) FLEXTableColumnHeaderSortType sortType; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @@ -0,0 +1,78 @@ | ||||
| // | ||||
| //  FLEXTableContentHeaderCell.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/26. | ||||
| //  Copyright © 2015年 f. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableColumnHeader.h" | ||||
| #import "FLEXColor.h" | ||||
| #import "UIFont+FLEX.h" | ||||
| #import "FLEXUtility.h" | ||||
|  | ||||
| static const CGFloat kMargin = 5; | ||||
| static const CGFloat kArrowWidth = 20; | ||||
|  | ||||
| @interface FLEXTableColumnHeader () | ||||
| @property (nonatomic, readonly) UILabel *arrowLabel; | ||||
| @property (nonatomic, readonly) UIView *lineView; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXTableColumnHeader | ||||
|  | ||||
| - (instancetype)initWithFrame:(CGRect)frame { | ||||
|     self = [super initWithFrame:frame]; | ||||
|     if (self) { | ||||
|         self.backgroundColor = FLEXColor.secondaryBackgroundColor; | ||||
|          | ||||
|         _titleLabel = [UILabel new]; | ||||
|         _titleLabel.font = UIFont.flex_defaultTableCellFont; | ||||
|         [self addSubview:_titleLabel]; | ||||
|          | ||||
|         _arrowLabel = [UILabel new]; | ||||
|         _arrowLabel.font = UIFont.flex_defaultTableCellFont; | ||||
|         [self addSubview:_arrowLabel]; | ||||
|          | ||||
|         _lineView = [UIView new]; | ||||
|         _lineView.backgroundColor = FLEXColor.hairlineColor; | ||||
|         [self addSubview:_lineView]; | ||||
|          | ||||
|     } | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)setSortType:(FLEXTableColumnHeaderSortType)type { | ||||
|     _sortType = type; | ||||
|      | ||||
|     switch (type) { | ||||
|         case FLEXTableColumnHeaderSortTypeNone: | ||||
|             _arrowLabel.text = @""; | ||||
|             break; | ||||
|         case FLEXTableColumnHeaderSortTypeAsc: | ||||
|             _arrowLabel.text = @"⬆️"; | ||||
|             break; | ||||
|         case FLEXTableColumnHeaderSortTypeDesc: | ||||
|             _arrowLabel.text = @"⬇️"; | ||||
|             break; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)layoutSubviews { | ||||
|     [super layoutSubviews]; | ||||
|      | ||||
|     CGSize size = self.frame.size; | ||||
|      | ||||
|     self.titleLabel.frame = CGRectMake(kMargin, 0, size.width - kArrowWidth - kMargin, size.height); | ||||
|     self.arrowLabel.frame = CGRectMake(size.width - kArrowWidth, 0, kArrowWidth, size.height); | ||||
|     self.lineView.frame = CGRectMake(size.width - 1, 2, FLEXPointsToPixels(1), size.height - 4); | ||||
| } | ||||
|  | ||||
| - (CGSize)sizeThatFits:(CGSize)size { | ||||
|     CGFloat margins = kArrowWidth - 2 * kMargin; | ||||
|     size = CGSizeMake(size.width - margins, size.height); | ||||
|     CGFloat width = [_titleLabel sizeThatFits:size].width + margins; | ||||
|     return CGSizeMake(width, size.height); | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,36 @@ | ||||
| // | ||||
| //  PTTableContentViewController.h | ||||
| //  PTDatabaseReader | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/23. | ||||
| //  Copyright © 2015年 Peng Tao. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
| #import "FLEXDatabaseManager.h" | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FLEXTableContentViewController : UIViewController | ||||
|  | ||||
| /// Display a mutable table with the given columns, rows, and name. | ||||
| /// | ||||
| /// @param columnNames self explanatory. | ||||
| /// @param rowData an array of rows, where each row is an array of column data. | ||||
| /// @param rowIDs an array of stringy row IDs. Required for deleting rows. | ||||
| /// @param tableName an optional name of the table being viewed, if any. Enables adding rows. | ||||
| /// @param databaseManager an optional manager to allow modifying the table. | ||||
| ///        Required for deleting rows. Required for adding rows if \c tableName is supplied. | ||||
| + (instancetype)columns:(NSArray<NSString *> *)columnNames | ||||
|                    rows:(NSArray<NSArray<NSString *> *> *)rowData | ||||
|                  rowIDs:(NSArray<NSString *> *)rowIDs | ||||
|               tableName:(NSString *)tableName | ||||
|                database:(id<FLEXDatabaseManager>)databaseManager; | ||||
|  | ||||
| /// Display an immutable table with the given columns and rows. | ||||
| + (instancetype)columns:(NSArray<NSString *> *)columnNames | ||||
|                    rows:(NSArray<NSArray<NSString *> *> *)rowData; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
| @@ -0,0 +1,359 @@ | ||||
| // | ||||
| //  PTTableContentViewController.m | ||||
| //  PTDatabaseReader | ||||
| // | ||||
| //  Created by Peng Tao on 15/11/23. | ||||
| //  Copyright © 2015年 Peng Tao. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableContentViewController.h" | ||||
| #import "FLEXTableRowDataViewController.h" | ||||
| #import "FLEXMultiColumnTableView.h" | ||||
| #import "FLEXWebViewController.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "UIBarButtonItem+FLEX.h" | ||||
|  | ||||
| @interface FLEXTableContentViewController () < | ||||
|     FLEXMultiColumnTableViewDataSource, FLEXMultiColumnTableViewDelegate | ||||
| > | ||||
| @property (nonatomic, readonly) NSArray<NSString *> *columns; | ||||
| @property (nonatomic) NSMutableArray<NSArray *> *rows; | ||||
| @property (nonatomic, readonly) NSString *tableName; | ||||
| @property (nonatomic, nullable) NSMutableArray<NSString *> *rowIDs; | ||||
| @property (nonatomic, readonly, nullable) id<FLEXDatabaseManager> databaseManager; | ||||
|  | ||||
| @property (nonatomic, readonly) BOOL canRefresh; | ||||
|  | ||||
| @property (nonatomic) FLEXMultiColumnTableView *multiColumnView; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXTableContentViewController | ||||
|  | ||||
| + (instancetype)columns:(NSArray<NSString *> *)columnNames | ||||
|                    rows:(NSArray<NSArray<NSString *> *> *)rowData | ||||
|                  rowIDs:(NSArray<NSString *> *)rowIDs | ||||
|               tableName:(NSString *)tableName | ||||
|                database:(id<FLEXDatabaseManager>)databaseManager { | ||||
|     return [[self alloc] | ||||
|         initWithColumns:columnNames | ||||
|         rows:rowData | ||||
|         rowIDs:rowIDs | ||||
|         tableName:tableName | ||||
|         database:databaseManager | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| + (instancetype)columns:(NSArray<NSString *> *)cols | ||||
|                    rows:(NSArray<NSArray<NSString *> *> *)rowData { | ||||
|     return [[self alloc] initWithColumns:cols rows:rowData rowIDs:nil tableName:nil database:nil]; | ||||
| } | ||||
|  | ||||
| - (instancetype)initWithColumns:(NSArray<NSString *> *)columnNames | ||||
|                            rows:(NSArray<NSArray<NSString *> *> *)rowData | ||||
|                          rowIDs:(nullable NSArray<NSString *> *)rowIDs | ||||
|                       tableName:(nullable NSString *)tableName | ||||
|                        database:(nullable id<FLEXDatabaseManager>)databaseManager { | ||||
|     // Must supply all optional parameters as one, or none | ||||
|     BOOL all = rowIDs && tableName && databaseManager; | ||||
|     BOOL none = !rowIDs && !tableName && !databaseManager; | ||||
|     NSParameterAssert(all || none); | ||||
|  | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         self->_columns = columnNames.copy; | ||||
|         self->_rows = rowData.mutableCopy; | ||||
|         self->_rowIDs = rowIDs.mutableCopy; | ||||
|         self->_tableName = tableName.copy; | ||||
|         self->_databaseManager = databaseManager; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)loadView { | ||||
|     [super loadView]; | ||||
|      | ||||
|     [self.view addSubview:self.multiColumnView]; | ||||
| } | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|     self.title = self.tableName; | ||||
|     [self.multiColumnView reloadData]; | ||||
|     [self setupToolbarItems]; | ||||
| } | ||||
|  | ||||
| - (FLEXMultiColumnTableView *)multiColumnView { | ||||
|     if (!_multiColumnView) { | ||||
|         _multiColumnView = [[FLEXMultiColumnTableView alloc] | ||||
|             initWithFrame:FLEXRectSetSize(CGRectZero, self.view.frame.size) | ||||
|         ]; | ||||
|          | ||||
|         _multiColumnView.dataSource = self; | ||||
|         _multiColumnView.delegate   = self; | ||||
|     } | ||||
|      | ||||
|     return _multiColumnView; | ||||
| } | ||||
|  | ||||
| - (BOOL)canRefresh { | ||||
|     return self.databaseManager && self.tableName; | ||||
| } | ||||
|  | ||||
| #pragma mark MultiColumnTableView DataSource | ||||
|  | ||||
| - (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView { | ||||
|     return self.columns.count; | ||||
| } | ||||
|  | ||||
| - (NSInteger)numberOfRowsInTableView:(FLEXMultiColumnTableView *)tableView { | ||||
|     return self.rows.count; | ||||
| } | ||||
|  | ||||
| - (NSString *)columnTitle:(NSInteger)column { | ||||
|     return self.columns[column]; | ||||
| } | ||||
|  | ||||
| - (NSString *)rowTitle:(NSInteger)row { | ||||
|     return @(row).stringValue; | ||||
| } | ||||
|  | ||||
| - (NSArray *)contentForRow:(NSInteger)row { | ||||
|     return self.rows[row]; | ||||
| } | ||||
|  | ||||
| - (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView | ||||
|       heightForContentCellInRow:(NSInteger)row { | ||||
|     return 40; | ||||
| } | ||||
|  | ||||
| - (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView | ||||
|     minWidthForContentCellInColumn:(NSInteger)column { | ||||
|     return 100; | ||||
| } | ||||
|  | ||||
| - (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView { | ||||
|     return 40; | ||||
| } | ||||
|  | ||||
| - (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView { | ||||
|     NSString *str = [NSString stringWithFormat:@"%lu",(unsigned long)self.rows.count]; | ||||
|     NSDictionary *attrs = @{ NSFontAttributeName : [UIFont systemFontOfSize:17.0] }; | ||||
|     CGSize size = [str boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, 14) | ||||
|         options:NSStringDrawingUsesLineFragmentOrigin | ||||
|         attributes:attrs context:nil | ||||
|     ].size; | ||||
|      | ||||
|     return size.width + 20; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark MultiColumnTableView Delegate | ||||
|  | ||||
| - (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didSelectRow:(NSInteger)row { | ||||
|     NSArray<NSString *> *fields = [self.rows[row] flex_mapped:^id(NSString *field, NSUInteger idx) { | ||||
|         return [NSString stringWithFormat:@"%@:\n%@", self.columns[idx], field]; | ||||
|     }]; | ||||
|      | ||||
|     NSArray<NSString *> *values = [self.rows[row] flex_mapped:^id(NSString *value, NSUInteger idx) { | ||||
|         return [NSString stringWithFormat:@"'%@'", value]; | ||||
|     }]; | ||||
|      | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         make.title([@"Row " stringByAppendingString:@(row).stringValue]); | ||||
|         NSString *message = [fields componentsJoinedByString:@"\n\n"]; | ||||
|         make.message(message); | ||||
|         make.button(@"Copy").handler(^(NSArray<NSString *> *strings) { | ||||
|             UIPasteboard.generalPasteboard.string = message; | ||||
|         }); | ||||
|         make.button(@"Copy as CSV").handler(^(NSArray<NSString *> *strings) { | ||||
|             UIPasteboard.generalPasteboard.string = [values componentsJoinedByString:@", "]; | ||||
|         }); | ||||
|         make.button(@"Focus on Row").handler(^(NSArray<NSString *> *strings) { | ||||
|             UIViewController *focusedRow = [FLEXTableRowDataViewController | ||||
|                 rows:[NSDictionary dictionaryWithObjects:self.rows[row] forKeys:self.columns] | ||||
|             ]; | ||||
|             [self.navigationController pushViewController:focusedRow animated:YES]; | ||||
|         }); | ||||
|          | ||||
|         // Option to delete row | ||||
|         BOOL hasRowID = self.rows.count && row < self.rows.count; | ||||
|         if (hasRowID && self.canRefresh) { | ||||
|             make.button(@"Delete").destructiveStyle().handler(^(NSArray<NSString *> *strings) { | ||||
|                 NSString *deleteRow = [NSString stringWithFormat: | ||||
|                     @"DELETE FROM %@ WHERE rowid = %@", | ||||
|                     self.tableName, self.rowIDs[row] | ||||
|                 ]; | ||||
|                  | ||||
|                 [self executeStatementAndShowResult:deleteRow completion:^(BOOL success) { | ||||
|                     // Remove deleted row and reload view | ||||
|                     if (success) { | ||||
|                         [self reloadTableDataFromDB]; | ||||
|                     } | ||||
|                 }]; | ||||
|             }); | ||||
|         } | ||||
|          | ||||
|         make.button(@"Dismiss").cancelStyle(); | ||||
|     } showFrom:self]; | ||||
| } | ||||
|  | ||||
| - (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView | ||||
|     didSelectHeaderForColumn:(NSInteger)column | ||||
|                     sortType:(FLEXTableColumnHeaderSortType)sortType { | ||||
|      | ||||
|     NSArray<NSArray *> *sortContentData = [self.rows | ||||
|         sortedArrayWithOptions:NSSortStable | ||||
|         usingComparator:^NSComparisonResult(NSArray *obj1, NSArray *obj2) { | ||||
|             id a = obj1[column], b = obj2[column]; | ||||
|             if (a == NSNull.null) { | ||||
|                 return NSOrderedAscending; | ||||
|             } | ||||
|             if (b == NSNull.null) { | ||||
|                 return NSOrderedDescending; | ||||
|             } | ||||
|          | ||||
|             if ([a respondsToSelector:@selector(compare:options:)] && | ||||
|                 [b respondsToSelector:@selector(compare:options:)]) { | ||||
|                 return [a compare:b options:NSNumericSearch]; | ||||
|             } | ||||
|              | ||||
|             if ([a respondsToSelector:@selector(compare:)] && [b respondsToSelector:@selector(compare:)]) { | ||||
|                 return [a compare:b]; | ||||
|             } | ||||
|              | ||||
|             return NSOrderedSame; | ||||
|         } | ||||
|     ]; | ||||
|      | ||||
|     if (sortType == FLEXTableColumnHeaderSortTypeDesc) { | ||||
|         sortContentData = sortContentData.reverseObjectEnumerator.allObjects.copy; | ||||
|     } | ||||
|      | ||||
|     self.rows = sortContentData.mutableCopy; | ||||
|     [self.multiColumnView reloadData]; | ||||
| } | ||||
|  | ||||
| #pragma mark - About Transition | ||||
|  | ||||
| - (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection | ||||
|               withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator { | ||||
|     [super willTransitionToTraitCollection:newCollection withTransitionCoordinator:coordinator]; | ||||
|      | ||||
|     [coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) { | ||||
|         if (newCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) { | ||||
|             self.multiColumnView.frame = CGRectMake(0, 32, self.view.frame.size.width, self.view.frame.size.height - 32); | ||||
|         } | ||||
|         else { | ||||
|             self.multiColumnView.frame = CGRectMake(0, 64, self.view.frame.size.width, self.view.frame.size.height - 64); | ||||
|         } | ||||
|          | ||||
|         [self.view setNeedsLayout]; | ||||
|     } completion:nil]; | ||||
| } | ||||
|  | ||||
| #pragma mark - Toolbar | ||||
|  | ||||
| - (void)setupToolbarItems { | ||||
|     // We do not support modifying realm databases | ||||
|     if (![self.databaseManager respondsToSelector:@selector(executeStatement:)]) { | ||||
|         return; | ||||
|     } | ||||
|      | ||||
|     UIBarButtonItem *trashButton = FLEXBarButtonItemSystem(Trash, self, @selector(trashPressed)); | ||||
|     UIBarButtonItem *addButton = FLEXBarButtonItemSystem(Add, self, @selector(addPressed)); | ||||
|  | ||||
|     // Only allow adding rows or deleting rows if we have a table name | ||||
|     trashButton.enabled = self.canRefresh; | ||||
|     addButton.enabled = self.canRefresh; | ||||
|      | ||||
|     self.toolbarItems = @[ | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         addButton, | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         [trashButton flex_withTintColor:UIColor.redColor], | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| - (void)trashPressed { | ||||
|     NSParameterAssert(self.tableName); | ||||
|  | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         make.title(@"Delete All Rows"); | ||||
|         make.message(@"All rows in this table will be permanently deleted.\nDo you want to proceed?"); | ||||
|          | ||||
|         make.button(@"Yes, I'm sure").destructiveStyle().handler(^(NSArray<NSString *> *strings) { | ||||
|             NSString *deleteAll = [NSString stringWithFormat:@"DELETE FROM %@", self.tableName]; | ||||
|             [self executeStatementAndShowResult:deleteAll completion:^(BOOL success) { | ||||
|                 // Only dismiss on success | ||||
|                 if (success) { | ||||
|                     [self.navigationController popViewControllerAnimated:YES]; | ||||
|                 } | ||||
|             }]; | ||||
|         }); | ||||
|         make.button(@"Cancel").cancelStyle(); | ||||
|     } showFrom:self]; | ||||
| } | ||||
|  | ||||
| - (void)addPressed { | ||||
|     NSParameterAssert(self.tableName); | ||||
|  | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         make.title(@"Add a New Row"); | ||||
|         make.message(@"Comma separate values to use in an INSERT statement.\n\n"); | ||||
|         make.message(@"INSERT INTO [table] VALUES (your_input)"); | ||||
|         make.textField(@"5, 'John Smith', 14,..."); | ||||
|         make.button(@"Insert").handler(^(NSArray<NSString *> *strings) { | ||||
|             NSString *statement = [NSString stringWithFormat: | ||||
|                 @"INSERT INTO %@ VALUES (%@)", self.tableName, strings[0] | ||||
|             ]; | ||||
|  | ||||
|             [self executeStatementAndShowResult:statement completion:^(BOOL success) { | ||||
|                 if (success) { | ||||
|                     [self reloadTableDataFromDB]; | ||||
|                 } | ||||
|             }]; | ||||
|         }); | ||||
|         make.button(@"Cancel").cancelStyle(); | ||||
|     } showFrom:self]; | ||||
| } | ||||
|  | ||||
| #pragma mark - Helpers | ||||
|  | ||||
| - (void)executeStatementAndShowResult:(NSString *)statement | ||||
|                            completion:(void (^_Nullable)(BOOL success))completion { | ||||
|     NSParameterAssert(self.databaseManager); | ||||
|  | ||||
|     FLEXSQLResult *result = [self.databaseManager executeStatement:statement]; | ||||
|      | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         if (result.isError) { | ||||
|             make.title(@"Error"); | ||||
|         } | ||||
|          | ||||
|         make.message(result.message ?: @"<no output>"); | ||||
|         make.button(@"Dismiss").cancelStyle().handler(^(NSArray<NSString *> *_) { | ||||
|             if (completion) { | ||||
|                 completion(!result.isError); | ||||
|             } | ||||
|         }); | ||||
|     } showFrom:self]; | ||||
| } | ||||
|  | ||||
| - (void)reloadTableDataFromDB { | ||||
|     if (!self.canRefresh) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     NSArray<NSArray *> *rows = [self.databaseManager queryAllDataInTable:self.tableName]; | ||||
|     NSArray<NSString *> *rowIDs = nil; | ||||
|     if ([self.databaseManager respondsToSelector:@selector(queryRowIDsInTable:)]) { | ||||
|         rowIDs = [self.databaseManager queryRowIDsInTable:self.tableName]; | ||||
|     } | ||||
|  | ||||
|     self.rows = rows.mutableCopy; | ||||
|     self.rowIDs = rowIDs.mutableCopy; | ||||
|     [self.multiColumnView reloadData]; | ||||
| } | ||||
|  | ||||
| @end | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 Balackburn
					Balackburn