// // 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 *columns; @property (nonatomic) NSMutableArray *rows; @property (nonatomic, readonly) NSString *tableName; @property (nonatomic, nullable) NSMutableArray *rowIDs; @property (nonatomic, readonly, nullable) id databaseManager; @property (nonatomic, readonly) BOOL canRefresh; @property (nonatomic) FLEXMultiColumnTableView *multiColumnView; @end @implementation FLEXTableContentViewController + (instancetype)columns:(NSArray *)columnNames rows:(NSArray *> *)rowData rowIDs:(NSArray *)rowIDs tableName:(NSString *)tableName database:(id)databaseManager { return [[self alloc] initWithColumns:columnNames rows:rowData rowIDs:rowIDs tableName:tableName database:databaseManager ]; } + (instancetype)columns:(NSArray *)cols rows:(NSArray *> *)rowData { return [[self alloc] initWithColumns:cols rows:rowData rowIDs:nil tableName:nil database:nil]; } - (instancetype)initWithColumns:(NSArray *)columnNames rows:(NSArray *> *)rowData rowIDs:(nullable NSArray *)rowIDs tableName:(nullable NSString *)tableName database:(nullable id)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 *fields = [self.rows[row] flex_mapped:^id(NSString *field, NSUInteger idx) { return [NSString stringWithFormat:@"%@:\n%@", self.columns[idx], field]; }]; NSArray *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 *strings) { UIPasteboard.generalPasteboard.string = message; }); make.button(@"Copy as CSV").handler(^(NSArray *strings) { UIPasteboard.generalPasteboard.string = [values componentsJoinedByString:@", "]; }); make.button(@"Focus on Row").handler(^(NSArray *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 *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 *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 )coordinator { [super willTransitionToTraitCollection:newCollection withTransitionCoordinator:coordinator]; [coordinator animateAlongsideTransition:^(id 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 *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 *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 ?: @""); make.button(@"Dismiss").cancelStyle().handler(^(NSArray *_) { if (completion) { completion(!result.isError); } }); } showFrom:self]; } - (void)reloadTableDataFromDB { if (!self.canRefresh) { return; } NSArray *rows = [self.databaseManager queryAllDataInTable:self.tableName]; NSArray *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