added files via upload

This commit is contained in:
Balackburn
2023-06-27 09:54:41 +02:00
commit 2ff6aac218
1420 changed files with 88898 additions and 0 deletions

1
Tweaks/FLEX/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.DS_Store

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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
View 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"

View 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"

View 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
View 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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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