Compare commits
152 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8367342b25 | |||
| 2df073a792 | |||
| 8236fc97cc | |||
| 0364de36bd | |||
| 12195eb879 | |||
| acdc46c43f | |||
| 52eed1b6f9 | |||
| a91d1de9ad | |||
| 492d2e49fe | |||
| 49bc439000 | |||
| 8c919cc26c | |||
| 8e86ffccd6 | |||
| 228de102e7 | |||
| 37b5d1be2a | |||
| ef8f866330 | |||
| e862b81734 | |||
| a3f66b3f87 | |||
| 3e12ad9887 | |||
| fe36b59b4c | |||
| b735a69c1b | |||
| 7b1b6f9e24 | |||
| c047fbc581 | |||
| 40239524d1 | |||
| 2f93050e2e | |||
| dc8ac6c195 | |||
| 8edf6b4ad6 | |||
| 4d019046bc | |||
| 67359023f4 | |||
| a6cbfbd3fd | |||
| b7cac1fe48 | |||
| 226e0cd803 | |||
| 53538bfead | |||
| a78bf1b22f | |||
| 0803b46f9d | |||
| f64e6ec3c9 | |||
| 46652ac73d | |||
| e30b1854fc | |||
| 8a5e57c1d2 | |||
| f582c9ae0d | |||
| 8a762a66ae | |||
| d537d3c79e | |||
| 07f0b07a91 | |||
| 555b57941d | |||
| 4be2b119d1 | |||
| 8255c7fe79 | |||
| a21e5ea158 | |||
| ac4c50b62c | |||
| ca919a4188 | |||
| b9c9af5509 | |||
| 6cbfa63d48 | |||
| c3066a7847 | |||
| 16fbab783e | |||
| a58314a825 | |||
| 89b9ece45d | |||
| b5d5867bc1 | |||
| 5a54f5808d | |||
| 664a39e0f1 | |||
| 9935860efb | |||
| 25a05eec1a | |||
| 32c0983bb7 | |||
| 3650da6c12 | |||
| 40b52120d0 | |||
| 74eb6e180b | |||
| 4c2e921f9e | |||
| bba5b8b72c | |||
| e7290bc84f | |||
| f7619cdbf2 | |||
| bff9f1dd89 | |||
| 81b7ccea22 | |||
| f2c8ede0e0 | |||
| adf2fc56e8 | |||
| 78a34a8437 | |||
| aa6bbfb7e7 | |||
| c907a98099 | |||
| d3ae20bebe | |||
| b69560e62e | |||
| b01309678c | |||
| eb4160636f | |||
| 6d489e72c5 | |||
| 156eb8cbe1 | |||
| e98abb1e23 | |||
| e88c286f9e | |||
| 0de3a9d65c | |||
| f98e0622b5 | |||
| 85cc51bfd3 | |||
| c3bb4ff0d3 | |||
| 55b579a34e | |||
| 3a5a242346 | |||
| e3ac0d6ecc | |||
| 211e2956ca | |||
| efed18c386 | |||
| 3dd9a5705d | |||
| 462b38a473 | |||
| 7979fcd896 | |||
| 642b1810c5 | |||
| e0b6eec03c | |||
| 9c4ff2ddd8 | |||
| 0ddd202852 | |||
| 81be8e1316 | |||
| 4e2e05b451 | |||
| 4262074948 | |||
| 0f98a35643 | |||
| 4c1fceac54 | |||
| 45996df0c2 | |||
| 1669b205da | |||
| 9f1d988651 | |||
| 5541e3c683 | |||
| 6632703b70 | |||
| eebfffe4a6 | |||
| 70264c1cd5 | |||
| 0a124a2424 | |||
| 2a34f21667 | |||
| 62b26036ed | |||
| 611b861678 | |||
| c7bc875b0e | |||
| 841f3f9775 | |||
| 8b225d2046 | |||
| cf9bd2335b | |||
| e07bfa8d5f | |||
| 17e194b69d | |||
| f86cb8a81f | |||
| 739c28cf81 | |||
| fcdb33fce2 | |||
| 1eb8e4f430 | |||
| 1d937777c0 | |||
| 308afda5c2 | |||
| f7b00e02ee | |||
| 0ff29e1f90 | |||
| 3fe31e8628 | |||
| 33263bfcfa | |||
| d6caab29dc | |||
| 5d181adcb8 | |||
| 4ba2fdc289 | |||
| 140dc32775 | |||
| 78568cd5be | |||
| b010cdb072 | |||
| 37e299733b | |||
| f3a1587cf1 | |||
| 821ca1683b | |||
| 7e13ca2757 | |||
| 129c91c876 | |||
| d2f6ff0b40 | |||
| 69414e4174 | |||
| 0654fb4b5f | |||
| d7d40e6d27 | |||
| bec7e0c229 | |||
| 82a19e41e7 | |||
| 867ae614e5 | |||
| 22b7c6ccc7 | |||
| 9e9704580a | |||
| 1ef608cf8a | |||
| b64cd37ec6 |
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// FLEXCarouselCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 7/17/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface FLEXCarouselCell : UICollectionViewCell
|
||||
|
||||
@property (nonatomic, copy) NSString *title;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// FLEXCarouselCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 7/17/19.
|
||||
// Copyright © 2019 Flipboard. 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.titleLabel.adjustsFontForContentSizeCategory = YES;
|
||||
self.selectionIndicatorStripe.backgroundColor = self.tintColor;
|
||||
|
||||
[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 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
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// FLEXScopeCarousel.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 7/17/19.
|
||||
// Copyright © 2019 Flipboard. 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
|
||||
@@ -0,0 +1,207 @@
|
||||
//
|
||||
// FLEXScopeCarousel.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 7/17/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXScopeCarousel.h"
|
||||
#import "FLEXCarouselCell.h"
|
||||
#import "FLEXColor.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) NSLayoutConstraint *heightConstraint;
|
||||
|
||||
@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;
|
||||
_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
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
_dynamicTypeObserver = [NSNotificationCenter.defaultCenter
|
||||
addObserverForName:UIContentSizeCategoryDidChangeNotification
|
||||
object:nil queue:nil usingBlock:^(NSNotification *note) {
|
||||
[self.collectionView setNeedsLayout];
|
||||
[self setNeedsUpdateConstraints];
|
||||
|
||||
// Notify observers
|
||||
__typeof(self) self = weakSelf;
|
||||
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.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.collectionView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
[self.centerXAnchor constraintEqualToAnchor:self.superview.centerXAnchor].active = YES;
|
||||
[self.widthAnchor constraintEqualToAnchor:self.superview.widthAnchor].active = YES;
|
||||
[self.topAnchor constraintEqualToAnchor:self.superview.topAnchor].active = YES;
|
||||
|
||||
[self.collectionView pinEdgesToSuperview];
|
||||
_heightConstraint = [self.heightAnchor constraintEqualToConstant:100];
|
||||
self.heightConstraint.active = YES;
|
||||
|
||||
self.constraintsInstalled = YES;
|
||||
}
|
||||
|
||||
self.heightConstraint.constant = [self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
|
||||
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// FLEXTableViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 7/5/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
@class FLEXScopeCarousel;
|
||||
|
||||
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;
|
||||
|
||||
@interface FLEXTableViewController : UITableViewController <UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate>
|
||||
|
||||
/// A grouped table view. Inset on iOS 13.
|
||||
///
|
||||
/// Simply calls into initWithStyle:
|
||||
- (id)init;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// nil unless showsSearchBar is set to YES.
|
||||
///
|
||||
/// self is used as the default search results updater and delegate.
|
||||
/// Make sure your subclass conforms to UISearchControllerDelegate.
|
||||
/// 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, readonly) NSInteger selectedScope;
|
||||
/// self.searchController.searchBar.text
|
||||
@property (nonatomic, readonly) NSString *searchText;
|
||||
|
||||
/// Subclasses should override to handle search query update events.
|
||||
///
|
||||
/// 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;
|
||||
|
||||
/// 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;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,285 @@
|
||||
//
|
||||
// FLEXTableViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 7/5/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
#import "FLEXScopeCarousel.h"
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXUtility.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;
|
||||
@end
|
||||
|
||||
@implementation FLEXTableViewController
|
||||
@synthesize automaticallyShowsSearchBarCancelButton = _automaticallyShowsSearchBarCancelButton;
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (id)init {
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
if (@available(iOS 13.0, *)) {
|
||||
self = [self initWithStyle:UITableViewStyleInsetGrouped];
|
||||
} else {
|
||||
self = [self initWithStyle:UITableViewStyleGrouped];
|
||||
}
|
||||
#else
|
||||
self = [self initWithStyle:UITableViewStyleGrouped];
|
||||
#endif
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithStyle:(UITableViewStyle)style {
|
||||
self = [super initWithStyle:style];
|
||||
|
||||
if (self) {
|
||||
_searchBarDebounceInterval = kFLEXDebounceFast;
|
||||
_showSearchBarInitially = YES;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setShowsSearchBar:(BOOL)showsSearchBar {
|
||||
if (_showsSearchBar == showsSearchBar) return;
|
||||
_showsSearchBar = 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 FLEX_AT_LEAST_IOS13_SDK
|
||||
if (@available(iOS 13, *)) {
|
||||
self.searchController.automaticallyShowsScopeBar = NO;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (@available(iOS 11.0, *)) {
|
||||
self.navigationItem.searchController = self.searchController;
|
||||
} else {
|
||||
self.tableView.tableHeaderView = self.searchController.searchBar;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setShowsCarousel:(BOOL)showsCarousel {
|
||||
if (_showsCarousel == showsCarousel) return;
|
||||
_showsCarousel = showsCarousel;
|
||||
|
||||
_carousel = ({
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
|
||||
FLEXScopeCarousel *carousel = [FLEXScopeCarousel new];
|
||||
carousel.selectedIndexChangedAction = ^(NSInteger idx) {
|
||||
__typeof(self) self = weakSelf;
|
||||
[self updateSearchResults:self.searchText];
|
||||
};
|
||||
|
||||
self.tableView.tableHeaderView = carousel;
|
||||
[self.tableView layoutIfNeeded];
|
||||
// UITableView won't update the header size unless you reset the header view
|
||||
[carousel registerBlockForDynamicTypeChanges:^(FLEXScopeCarousel *carousel) {
|
||||
__typeof(self) self = weakSelf;
|
||||
self.tableView.tableHeaderView = carousel;
|
||||
[self.tableView layoutIfNeeded];
|
||||
}];
|
||||
|
||||
carousel;
|
||||
});
|
||||
}
|
||||
|
||||
- (NSInteger)selectedScope {
|
||||
if (self.searchController.searchBar.showsScopeBar) {
|
||||
return self.searchController.searchBar.selectedScopeButtonIndex;
|
||||
} else if (self.showsCarousel) {
|
||||
return self.carousel.selectedIndex;
|
||||
} else {
|
||||
return NSNotFound;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)searchText {
|
||||
return self.searchController.searchBar.text;
|
||||
}
|
||||
|
||||
- (BOOL)automaticallyShowsSearchBarCancelButton {
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
if (@available(iOS 13, *)) {
|
||||
return self.searchController.automaticallyShowsCancelButton;
|
||||
}
|
||||
#endif
|
||||
|
||||
return _automaticallyShowsSearchBarCancelButton;
|
||||
}
|
||||
|
||||
- (void)setAutomaticallyShowsSearchBarCancelButton:(BOOL)value {
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
if (@available(iOS 13, *)) {
|
||||
self.searchController.automaticallyShowsCancelButton = value;
|
||||
}
|
||||
#endif
|
||||
|
||||
_automaticallyShowsSearchBarCancelButton = value;
|
||||
}
|
||||
|
||||
- (void)updateSearchResults:(NSString *)newText { }
|
||||
|
||||
- (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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - View Controller Lifecycle
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
|
||||
|
||||
// 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];
|
||||
|
||||
// When going back, make the search bar reappear instead of hiding
|
||||
if (@available(iOS 11.0, *)) {
|
||||
if ((self.pinSearchBar || self.showSearchBarInitially) && !self.didInitiallyRevealSearchBar) {
|
||||
self.navigationItem.hidesSearchBarWhenScrolling = NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (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];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
// We only want to reveal the search bar when the view controller first appears.
|
||||
self.didInitiallyRevealSearchBar = YES;
|
||||
}
|
||||
|
||||
- (void)willMoveToParentViewController:(UIViewController *)parent {
|
||||
[super willMoveToParentViewController: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 - Private
|
||||
|
||||
- (void)debounce:(void(^)(void))block {
|
||||
[self.debounceTimer invalidate];
|
||||
|
||||
self.debounceTimer = [NSTimer
|
||||
scheduledTimerWithTimeInterval:self.searchBarDebounceInterval
|
||||
target:block
|
||||
selector:@selector(invoke)
|
||||
userInfo:nil
|
||||
repeats:NO
|
||||
];
|
||||
}
|
||||
|
||||
#pragma mark - Search Bar
|
||||
|
||||
#pragma mark UISearchResultsUpdating
|
||||
|
||||
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
|
||||
{
|
||||
[self.debounceTimer invalidate];
|
||||
NSString *text = searchController.searchBar.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:^{
|
||||
[self updateSearchResults:text];
|
||||
}];
|
||||
} else {
|
||||
[self updateSearchResults:text];
|
||||
}
|
||||
}
|
||||
|
||||
#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, *)) {
|
||||
return @" "; // For inset grouped style
|
||||
}
|
||||
|
||||
return nil; // For plain/gropued style
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// FLEXTableViewSection.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 7/11/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// A protocol for arbitrary case-insensitive pattern matching
|
||||
@protocol FLEXPatternMatching <NSObject>
|
||||
/// @return YES if the receiver matches the query, case-insensitive
|
||||
- (BOOL)matches:(NSString *)query;
|
||||
@end
|
||||
|
||||
@interface FLEXTableViewSection<__covariant ObjectType> : NSObject
|
||||
|
||||
+ (instancetype)section:(NSInteger)section title:(NSString *)title rows:(NSArray<ObjectType<FLEXPatternMatching>> *)rows;
|
||||
|
||||
@property (nonatomic, readonly) NSInteger section;
|
||||
@property (nonatomic, readonly) NSString *title;
|
||||
@property (nonatomic, readonly) NSArray<ObjectType<FLEXPatternMatching>> *rows;
|
||||
|
||||
@property (nonatomic, readonly) NSInteger count;
|
||||
|
||||
/// @return A new section containing only rows that match the string,
|
||||
/// or nil if the section was empty and no rows matched the string.
|
||||
- (nullable instancetype)newSectionWithRowsMatchingQuery:(NSString *)query;
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXTableViewSection<__covariant ObjectType> (Subscripting)
|
||||
- (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// FLEXTableViewSection.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 7/11/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewSection.h"
|
||||
|
||||
@implementation FLEXTableViewSection
|
||||
|
||||
+ (instancetype)section:(NSInteger)section title:(NSString *)title rows:(NSArray *)rows {
|
||||
FLEXTableViewSection *s = [self new];
|
||||
s->_section = section;
|
||||
s->_title = title;
|
||||
s->_rows = rows.copy;
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
- (instancetype)newSectionWithRowsMatchingQuery:(NSString *)query {
|
||||
// Find rows containing the search string
|
||||
NSPredicate *containsString = [NSPredicate predicateWithBlock:^BOOL(id<FLEXPatternMatching> obj, NSDictionary *bindings) {
|
||||
return [obj matches:query];
|
||||
}];
|
||||
NSArray *filteredRows = [self.rows filteredArrayUsingPredicate:containsString];
|
||||
|
||||
// Only return new section if not empty
|
||||
if (filteredRows.count) {
|
||||
return [[self class] section:self.section title:self.title rows:filteredRows];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSInteger)count {
|
||||
return self.rows.count;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXTableViewSection (Subscripting)
|
||||
|
||||
- (id)objectAtIndexedSubscript:(NSUInteger)idx {
|
||||
return self.rows[idx];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
@interface FLEXColorComponentInputView : UIView
|
||||
|
||||
@property (nonatomic, strong) UISlider *slider;
|
||||
@property (nonatomic, strong) UILabel *valueLabel;
|
||||
@property (nonatomic) UISlider *slider;
|
||||
@property (nonatomic) UILabel *valueLabel;
|
||||
|
||||
@property (nonatomic, weak) id <FLEXColorComponentInputViewDelegate> delegate;
|
||||
|
||||
@@ -34,12 +34,12 @@
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.slider = [[UISlider alloc] init];
|
||||
self.slider = [UISlider new];
|
||||
self.slider.backgroundColor = self.backgroundColor;
|
||||
[self.slider addTarget:self action:@selector(sliderChanged:) forControlEvents:UIControlEventValueChanged];
|
||||
[self addSubview:self.slider];
|
||||
|
||||
self.valueLabel = [[UILabel alloc] init];
|
||||
self.valueLabel = [UILabel new];
|
||||
self.valueLabel.backgroundColor = self.backgroundColor;
|
||||
self.valueLabel.font = [FLEXUtility defaultFontOfSize:14.0];
|
||||
self.valueLabel.textAlignment = NSTextAlignmentRight;
|
||||
@@ -94,9 +94,9 @@
|
||||
|
||||
@interface FLEXColorPreviewBox : UIView
|
||||
|
||||
@property (nonatomic, strong) UIColor *color;
|
||||
@property (nonatomic) UIColor *color;
|
||||
|
||||
@property (nonatomic, strong) UIView *colorOverlayView;
|
||||
@property (nonatomic) UIView *colorOverlayView;
|
||||
|
||||
@end
|
||||
|
||||
@@ -107,12 +107,12 @@
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.layer.borderWidth = 1.0;
|
||||
self.layer.borderColor = [[UIColor blackColor] CGColor];
|
||||
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.colorOverlayView.backgroundColor = UIColor.clearColor;
|
||||
[self addSubview:self.colorOverlayView];
|
||||
}
|
||||
return self;
|
||||
@@ -134,12 +134,12 @@
|
||||
CGSize squareSize = CGSizeMake(kSquareDimension, kSquareDimension);
|
||||
CGSize imageSize = CGSizeMake(2.0 * kSquareDimension, 2.0 * kSquareDimension);
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(imageSize, YES, [[UIScreen mainScreen] scale]);
|
||||
UIGraphicsBeginImageContextWithOptions(imageSize, YES, UIScreen.mainScreen.scale);
|
||||
|
||||
[[UIColor whiteColor] setFill];
|
||||
[UIColor.whiteColor setFill];
|
||||
UIRectFill(CGRectMake(0, 0, imageSize.width, imageSize.height));
|
||||
|
||||
[[UIColor grayColor] setFill];
|
||||
[UIColor.grayColor setFill];
|
||||
UIRectFill(CGRectMake(squareSize.width, 0, squareSize.width, squareSize.height));
|
||||
UIRectFill(CGRectMake(0, squareSize.height, squareSize.width, squareSize.height));
|
||||
|
||||
@@ -153,12 +153,12 @@
|
||||
|
||||
@interface FLEXArgumentInputColorView () <FLEXColorComponentInputViewDelegate>
|
||||
|
||||
@property (nonatomic, strong) FLEXColorPreviewBox *colorPreviewBox;
|
||||
@property (nonatomic, strong) UILabel *hexLabel;
|
||||
@property (nonatomic, strong) FLEXColorComponentInputView *alphaInput;
|
||||
@property (nonatomic, strong) FLEXColorComponentInputView *redInput;
|
||||
@property (nonatomic, strong) FLEXColorComponentInputView *greenInput;
|
||||
@property (nonatomic, strong) FLEXColorComponentInputView *blueInput;
|
||||
@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
|
||||
|
||||
@@ -168,32 +168,32 @@
|
||||
{
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.colorPreviewBox = [[FLEXColorPreviewBox alloc] init];
|
||||
self.colorPreviewBox = [FLEXColorPreviewBox new];
|
||||
[self addSubview:self.colorPreviewBox];
|
||||
|
||||
self.hexLabel = [[UILabel alloc] init];
|
||||
self.hexLabel = [UILabel new];
|
||||
self.hexLabel.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.9];
|
||||
self.hexLabel.textAlignment = NSTextAlignmentCenter;
|
||||
self.hexLabel.font = [FLEXUtility defaultFontOfSize:12.0];
|
||||
[self addSubview:self.hexLabel];
|
||||
|
||||
self.alphaInput = [[FLEXColorComponentInputView alloc] init];
|
||||
self.alphaInput.slider.minimumTrackTintColor = [UIColor blackColor];
|
||||
self.alphaInput = [FLEXColorComponentInputView new];
|
||||
self.alphaInput.slider.minimumTrackTintColor = UIColor.blackColor;
|
||||
self.alphaInput.delegate = self;
|
||||
[self addSubview:self.alphaInput];
|
||||
|
||||
self.redInput = [[FLEXColorComponentInputView alloc] init];
|
||||
self.redInput.slider.minimumTrackTintColor = [UIColor redColor];
|
||||
self.redInput = [FLEXColorComponentInputView new];
|
||||
self.redInput.slider.minimumTrackTintColor = UIColor.redColor;
|
||||
self.redInput.delegate = self;
|
||||
[self addSubview:self.redInput];
|
||||
|
||||
self.greenInput = [[FLEXColorComponentInputView alloc] init];
|
||||
self.greenInput.slider.minimumTrackTintColor = [UIColor greenColor];
|
||||
self.greenInput = [FLEXColorComponentInputView new];
|
||||
self.greenInput.slider.minimumTrackTintColor = UIColor.greenColor;
|
||||
self.greenInput.delegate = self;
|
||||
[self addSubview:self.greenInput];
|
||||
|
||||
self.blueInput = [[FLEXColorComponentInputView alloc] init];
|
||||
self.blueInput.slider.minimumTrackTintColor = [UIColor blueColor];
|
||||
self.blueInput = [FLEXColorComponentInputView new];
|
||||
self.blueInput.slider.minimumTrackTintColor = UIColor.blueColor;
|
||||
self.blueInput.delegate = self;
|
||||
[self addSubview:self.blueInput];
|
||||
}
|
||||
@@ -221,8 +221,8 @@
|
||||
|
||||
[self.hexLabel sizeToFit];
|
||||
const CGFloat kLabelVerticalOutsetAmount = 0.0;
|
||||
const CGFloat kLabelHorizonalOutsetAmount = 2.0;
|
||||
UIEdgeInsets labelOutset = UIEdgeInsetsMake(-kLabelVerticalOutsetAmount, -kLabelHorizonalOutsetAmount, -kLabelVerticalOutsetAmount, -kLabelHorizonalOutsetAmount);
|
||||
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;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
@interface FLEXArgumentInputDateView ()
|
||||
|
||||
@property (nonatomic, strong) UIDatePicker *datePicker;
|
||||
@property (nonatomic) UIDatePicker *datePicker;
|
||||
|
||||
@end
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
{
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.datePicker = [[UIDatePicker alloc] init];
|
||||
self.datePicker = [UIDatePicker new];
|
||||
self.datePicker.datePickerMode = UIDatePickerModeDateAndTime;
|
||||
// Using UTC, because that's what the NSDate description prints
|
||||
self.datePicker.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
|
||||
@interface FLEXArgumentInputFontView ()
|
||||
|
||||
@property (nonatomic, strong) FLEXArgumentInputView *fontNameInput;
|
||||
@property (nonatomic, strong) FLEXArgumentInputView *pointSizeInput;
|
||||
@property (nonatomic) FLEXArgumentInputView *fontNameInput;
|
||||
@property (nonatomic) FLEXArgumentInputView *pointSizeInput;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// FLEXArgumentInputFontsPickerView.h
|
||||
// UICatalog
|
||||
// FLEX
|
||||
//
|
||||
// Created by 啟倫 陳 on 2014/7/27.
|
||||
// Copyright (c) 2014年 f. All rights reserved.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// FLEXArgumentInputFontsPickerView.m
|
||||
// UICatalog
|
||||
// FLEX
|
||||
//
|
||||
// Created by 啟倫 陳 on 2014/7/27.
|
||||
// Copyright (c) 2014年 f. All rights reserved.
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
@interface FLEXArgumentInputFontsPickerView ()
|
||||
|
||||
@property (nonatomic, strong) NSMutableArray<NSString *> *availableFonts;
|
||||
@property (nonatomic) NSMutableArray<NSString *> *availableFonts;
|
||||
|
||||
@end
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
return [self.inputTextView.text length] > 0 ? [self.inputTextView.text copy] : nil;
|
||||
return self.inputTextView.text.length > 0 ? [self.inputTextView.text copy] : nil;
|
||||
}
|
||||
|
||||
#pragma mark - private
|
||||
@@ -74,7 +74,7 @@
|
||||
|
||||
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
|
||||
{
|
||||
return [self.availableFonts count];
|
||||
return self.availableFonts.count;
|
||||
}
|
||||
|
||||
#pragma mark - UIPickerViewDelegate
|
||||
@@ -84,7 +84,7 @@
|
||||
UILabel *fontLabel;
|
||||
if (!view) {
|
||||
fontLabel = [UILabel new];
|
||||
fontLabel.backgroundColor = [UIColor clearColor];
|
||||
fontLabel.backgroundColor = UIColor.clearColor;
|
||||
fontLabel.textAlignment = NSTextAlignmentCenter;
|
||||
} else {
|
||||
fontLabel = (UILabel*)view;
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
//
|
||||
// FLEXArgumentInputJSONObjectView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputJSONObjectView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
@implementation FLEXArgumentInputJSONObjectView
|
||||
|
||||
- (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;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
self.inputTextView.text = [FLEXRuntimeUtility editableJSONStringForObject:inputValue];
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
return [FLEXRuntimeUtility objectValueFromEditableJSONString:self.inputTextView.text];
|
||||
}
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
// Must be object type.
|
||||
BOOL supported = type && type[0] == '@';
|
||||
|
||||
if (supported) {
|
||||
if (value) {
|
||||
// If there's a current value, it must be serializable to JSON
|
||||
supported = [FLEXRuntimeUtility editableJSONStringForObject:value] != nil;
|
||||
} 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;
|
||||
// Note: we can't use @encode(NSString) here because that drops the string information and just goes to @encode(id).
|
||||
isJSONSerializableType = isJSONSerializableType || strcmp(type, FLEXEncodeClass(NSString)) == 0;
|
||||
isJSONSerializableType = isJSONSerializableType || strcmp(type, FLEXEncodeClass(NSNumber)) == 0;
|
||||
isJSONSerializableType = isJSONSerializableType || strcmp(type, FLEXEncodeClass(NSArray)) == 0;
|
||||
isJSONSerializableType = isJSONSerializableType || strcmp(type, FLEXEncodeClass(NSDictionary)) == 0;
|
||||
|
||||
supported = isJSONSerializableType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return supported;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
return [FLEXRuntimeUtility valueForNumberWithObjCType:[self.typeEncoding UTF8String] fromInputString:self.inputTextView.text];
|
||||
return [FLEXRuntimeUtility valueForNumberWithObjCType:self.typeEncoding.UTF8String fromInputString:self.inputTextView.text];
|
||||
}
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
@@ -49,7 +49,8 @@
|
||||
@(@encode(unsigned long)),
|
||||
@(@encode(unsigned long long)),
|
||||
@(@encode(float)),
|
||||
@(@encode(double))];
|
||||
@(@encode(double)),
|
||||
@(@encode(long double))];
|
||||
});
|
||||
return type && [primitiveTypes containsObject:@(type)];
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// FLEXArgumentInputJSONObjectView.h
|
||||
// FLEXArgumentInputObjectView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
@@ -8,6 +8,6 @@
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
|
||||
@interface FLEXArgumentInputJSONObjectView : FLEXArgumentInputTextView
|
||||
@interface FLEXArgumentInputObjectView : FLEXArgumentInputTextView
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,243 @@
|
||||
//
|
||||
// FLEXArgumentInputJSONObjectView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2014 Flipboard. 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] == '@';
|
||||
}
|
||||
|
||||
+ (FLEXArgInputObjectType)preferredDefaultTypeForObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
NSParameterAssert(type[0] == '@');
|
||||
|
||||
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
|
||||
@@ -27,11 +27,12 @@
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
// Interpret empty string as nil. We loose the ablitiy to set empty string as a string value,
|
||||
// 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.
|
||||
return [self.inputTextView.text length] > 0 ? [self.inputTextView.text copy] : nil;
|
||||
return self.inputTextView.text.length > 0 ? [self.inputTextView.text copy] : nil;
|
||||
}
|
||||
|
||||
// TODO: Support using object address for strings, as in the object arg view.
|
||||
|
||||
#pragma mark -
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
@interface FLEXArgumentInputStructView ()
|
||||
|
||||
@property (nonatomic, strong) NSArray<FLEXArgumentInputView *> *argumentInputViews;
|
||||
@property (nonatomic) NSArray<FLEXArgumentInputView *> *argumentInputViews;
|
||||
|
||||
@end
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
inputView.backgroundColor = self.backgroundColor;
|
||||
inputView.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
|
||||
if (fieldIndex < [customTitles count]) {
|
||||
if (fieldIndex < customTitles.count) {
|
||||
inputView.title = customTitles[fieldIndex];
|
||||
} else {
|
||||
inputView.title = [NSString stringWithFormat:@"%@ field %lu (%@)", structName, (unsigned long)fieldIndex, prettyTypeEncoding];
|
||||
@@ -59,7 +59,7 @@
|
||||
{
|
||||
if ([inputValue isKindOfClass:[NSValue class]]) {
|
||||
const char *structTypeEncoding = [inputValue objCType];
|
||||
if (strcmp([self.typeEncoding UTF8String], structTypeEncoding) == 0) {
|
||||
if (strcmp(self.typeEncoding.UTF8String, structTypeEncoding) == 0) {
|
||||
NSUInteger valueSize = 0;
|
||||
@try {
|
||||
// NSGetSizeAndAlignment barfs on type encoding for bitfields.
|
||||
@@ -74,7 +74,7 @@
|
||||
void *fieldPointer = unboxedValue + fieldOffset;
|
||||
FLEXArgumentInputView *inputView = self.argumentInputViews[fieldIndex];
|
||||
|
||||
if (fieldTypeEncoding[0] == @encode(id)[0] || fieldTypeEncoding[0] == @encode(Class)[0]) {
|
||||
if (fieldTypeEncoding[0] == FLEXTypeEncodingObjcObject || fieldTypeEncoding[0] == FLEXTypeEncodingObjcClass) {
|
||||
inputView.inputValue = (__bridge id)fieldPointer;
|
||||
} else {
|
||||
NSValue *boxedField = [FLEXRuntimeUtility valueForPrimitivePointer:fieldPointer objCType:fieldTypeEncoding];
|
||||
@@ -90,7 +90,7 @@
|
||||
- (id)inputValue
|
||||
{
|
||||
NSValue *boxedStruct = nil;
|
||||
const char *structTypeEncoding = [self.typeEncoding UTF8String];
|
||||
const char *structTypeEncoding = self.typeEncoding.UTF8String;
|
||||
NSUInteger structSize = 0;
|
||||
@try {
|
||||
// NSGetSizeAndAlignment barfs on type encoding for bitfields.
|
||||
@@ -104,7 +104,7 @@
|
||||
void *fieldPointer = unboxedStruct + fieldOffset;
|
||||
FLEXArgumentInputView *inputView = self.argumentInputViews[fieldIndex];
|
||||
|
||||
if (fieldTypeEncoding[0] == @encode(id)[0] || fieldTypeEncoding[0] == @encode(Class)[0]) {
|
||||
if (fieldTypeEncoding[0] == FLEXTypeEncodingObjcObject || fieldTypeEncoding[0] == FLEXTypeEncodingObjcClass) {
|
||||
// Object fields
|
||||
memcpy(fieldPointer, (__bridge void *)inputView.inputValue, sizeof(id));
|
||||
} else {
|
||||
@@ -176,7 +176,7 @@
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
return type && type[0] == '{';
|
||||
return type && type[0] == FLEXTypeEncodingStructBegin;
|
||||
}
|
||||
|
||||
+ (NSArray<NSString *> *)customFieldTitlesForTypeEncoding:(const char *)typeEncoding
|
||||
@@ -188,6 +188,8 @@
|
||||
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) {
|
||||
@@ -203,6 +205,13 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
@interface FLEXArgumentInputSwitchView ()
|
||||
|
||||
@property (nonatomic, strong) UISwitch *inputSwitch;
|
||||
@property (nonatomic) UISwitch *inputSwitch;
|
||||
|
||||
@end
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.inputSwitch = [[UISwitch alloc] init];
|
||||
self.inputSwitch = [UISwitch new];
|
||||
[self.inputSwitch addTarget:self action:@selector(switchValueDidChange:) forControlEvents:UIControlEventValueChanged];
|
||||
[self.inputSwitch sizeToFit];
|
||||
[self addSubview:self.inputSwitch];
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@interface FLEXArgumentInputTextView : FLEXArgumentInputView
|
||||
@interface FLEXArgumentInputTextView : FLEXArgumentInputView <UITextViewDelegate>
|
||||
|
||||
// For subclass eyes only
|
||||
|
||||
@property (nonatomic, strong, readonly) UITextView *inputTextView;
|
||||
@property (nonatomic, readonly) UITextView *inputTextView;
|
||||
@property (nonatomic) NSString *inputPlaceholderText;
|
||||
|
||||
@end
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
//
|
||||
//
|
||||
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXArgumentInputTextView () <UITextViewDelegate>
|
||||
@interface FLEXArgumentInputTextView ()
|
||||
|
||||
@property (nonatomic, strong) UITextView *inputTextView;
|
||||
@property (nonatomic) UITextView *inputTextView;
|
||||
@property (nonatomic) UILabel *placeholderLabel;
|
||||
@property (nonatomic, readonly) NSUInteger numberOfInputLines;
|
||||
|
||||
@end
|
||||
@@ -22,23 +24,31 @@
|
||||
{
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.inputTextView = [[UITextView alloc] init];
|
||||
self.inputTextView = [UITextView new];
|
||||
self.inputTextView.font = [[self class] inputFont];
|
||||
self.inputTextView.backgroundColor = [UIColor whiteColor];
|
||||
self.inputTextView.layer.borderColor = [[UIColor blackColor] CGColor];
|
||||
self.inputTextView.layer.borderWidth = 1.0;
|
||||
self.inputTextView.backgroundColor = [FLEXColor primaryBackgroundColor];
|
||||
self.inputTextView.layer.borderColor = [FLEXColor borderColor].CGColor;
|
||||
self.inputTextView.layer.borderWidth = 1.f;
|
||||
self.inputTextView.layer.cornerRadius = 5.f;
|
||||
self.inputTextView.autocapitalizationType = UITextAutocapitalizationTypeNone;
|
||||
self.inputTextView.autocorrectionType = UITextAutocorrectionTypeNo;
|
||||
self.inputTextView.delegate = self;
|
||||
self.inputTextView.inputAccessoryView = [self createToolBar];
|
||||
[self addSubview:self.inputTextView];
|
||||
|
||||
self.placeholderLabel = [UILabel new];
|
||||
self.placeholderLabel.font = self.inputTextView.font;
|
||||
self.placeholderLabel.textColor = [FLEXColor deemphasizedTextColor];
|
||||
self.placeholderLabel.numberOfLines = 0;
|
||||
[self.inputTextView addSubview:self.placeholderLabel];
|
||||
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - private
|
||||
#pragma mark - Private
|
||||
|
||||
- (UIToolbar*)createToolBar
|
||||
- (UIToolbar *)createToolBar
|
||||
{
|
||||
UIToolbar *toolBar = [UIToolbar new];
|
||||
[toolBar sizeToFit];
|
||||
@@ -53,12 +63,25 @@
|
||||
[self.inputTextView resignFirstResponder];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Text View Changes
|
||||
|
||||
- (void)textViewDidChange:(UITextView *)textView
|
||||
- (void)setInputPlaceholderText:(NSString *)placeholder
|
||||
{
|
||||
[self.delegate argumentInputViewValueDidChange:self];
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -77,25 +100,28 @@
|
||||
[super layoutSubviews];
|
||||
|
||||
self.inputTextView.frame = CGRectMake(0, self.topInputFieldVerticalLayoutGuide, self.bounds.size.width, [self inputTextViewHeight]);
|
||||
// Placeholder label is positioned by insetting origin,
|
||||
// which is the line fragment padding for X and 0 for Y,
|
||||
// by the content inset then the text container inset
|
||||
CGFloat leading = self.inputTextView.textContainer.lineFragmentPadding;
|
||||
CGSize s = self.inputTextView.frame.size;
|
||||
self.placeholderLabel.frame = CGRectMake(leading, 0, s.width, s.height);
|
||||
self.placeholderLabel.frame = UIEdgeInsetsInsetRect(
|
||||
UIEdgeInsetsInsetRect(self.placeholderLabel.frame, self.inputTextView.contentInset),
|
||||
self.inputTextView.textContainerInset
|
||||
);
|
||||
}
|
||||
|
||||
- (NSUInteger)numberOfInputLines
|
||||
{
|
||||
NSUInteger numberOfInputLines = 0;
|
||||
switch (self.targetSize) {
|
||||
case FLEXArgumentInputViewSizeDefault:
|
||||
numberOfInputLines = 2;
|
||||
break;
|
||||
|
||||
return 2;
|
||||
case FLEXArgumentInputViewSizeSmall:
|
||||
numberOfInputLines = 1;
|
||||
break;
|
||||
|
||||
return 1;
|
||||
case FLEXArgumentInputViewSizeLarge:
|
||||
numberOfInputLines = 8;
|
||||
break;
|
||||
return 8;
|
||||
}
|
||||
return numberOfInputLines;
|
||||
}
|
||||
|
||||
- (CGFloat)inputTextViewHeight
|
||||
@@ -111,6 +137,20 @@
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Trait collection changes
|
||||
|
||||
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
|
||||
{
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
if (@available(iOS 13.0, *)) {
|
||||
if (previousTraitCollection.userInterfaceStyle != self.traitCollection.userInterfaceStyle) {
|
||||
self.inputTextView.layer.borderColor = [FLEXColor borderColor].CGColor;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Class Helpers
|
||||
|
||||
+ (UIFont *)inputFont
|
||||
@@ -118,4 +158,13 @@
|
||||
return [FLEXUtility defaultFontOfSize:14.0];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UITextViewDelegate
|
||||
|
||||
- (void)textViewDidChange:(UITextView *)textView
|
||||
{
|
||||
[self.delegate argumentInputViewValueDidChange:self];
|
||||
self.placeholderLabel.hidden = !(self.inputPlaceholderText.length && !textView.text.length);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -26,12 +26,13 @@ typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) {
|
||||
/// 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 *must* override both the setter and getter for this property.
|
||||
/// 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, assign) FLEXArgumentInputViewSize targetSize;
|
||||
@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;
|
||||
@@ -47,8 +48,8 @@ typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) {
|
||||
|
||||
// For subclass eyes only
|
||||
|
||||
@property (nonatomic, strong, readonly) UILabel *titleLabel;
|
||||
@property (nonatomic, strong, readonly) NSString *typeEncoding;
|
||||
@property (nonatomic, readonly) UILabel *titleLabel;
|
||||
@property (nonatomic, readonly) NSString *typeEncoding;
|
||||
@property (nonatomic, readonly) CGFloat topInputFieldVerticalLayoutGuide;
|
||||
|
||||
@end
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
|
||||
@interface FLEXArgumentInputView ()
|
||||
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@property (nonatomic, strong) NSString *typeEncoding;
|
||||
@property (nonatomic) UILabel *titleLabel;
|
||||
@property (nonatomic) NSString *typeEncoding;
|
||||
|
||||
@end
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
- (UILabel *)titleLabel
|
||||
{
|
||||
if (!_titleLabel) {
|
||||
_titleLabel = [[UILabel alloc] init];
|
||||
_titleLabel = [UILabel new];
|
||||
_titleLabel.font = [[self class] titleFont];
|
||||
_titleLabel.backgroundColor = self.backgroundColor;
|
||||
_titleLabel.textColor = [UIColor colorWithWhite:0.3 alpha:1.0];
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
- (BOOL)showsTitle
|
||||
{
|
||||
return [self.title length] > 0;
|
||||
return self.title.length > 0;
|
||||
}
|
||||
|
||||
- (CGFloat)topInputFieldVerticalLayoutGuide
|
||||
@@ -89,17 +89,6 @@
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
// Subclasses should override.
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
// Subclasses should override.
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
return NO;
|
||||
@@ -125,7 +114,7 @@
|
||||
{
|
||||
CGFloat height = 0;
|
||||
|
||||
if ([self.title length] > 0) {
|
||||
if (self.title.length > 0) {
|
||||
CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX);
|
||||
height += ceil([self.titleLabel sizeThatFits:constrainSize].height);
|
||||
height += [[self class] titleBottomPadding];
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputJSONObjectView.h"
|
||||
#import "FLEXArgumentInputObjectView.h"
|
||||
#import "FLEXArgumentInputNumberView.h"
|
||||
#import "FLEXArgumentInputSwitchView.h"
|
||||
#import "FLEXArgumentInputStructView.h"
|
||||
@@ -17,6 +17,7 @@
|
||||
#import "FLEXArgumentInputFontView.h"
|
||||
#import "FLEXArgumentInputColorView.h"
|
||||
#import "FLEXArgumentInputDateView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
@implementation FLEXArgumentInputViewFactory
|
||||
|
||||
@@ -33,34 +34,35 @@
|
||||
// The unsupported view shows "nil" and does not allow user input.
|
||||
subclass = [FLEXArgumentInputNotSupportedView class];
|
||||
}
|
||||
return [[subclass alloc] initWithArgumentTypeEncoding:typeEncoding];
|
||||
// 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.
|
||||
if ([FLEXArgumentInputColorView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = [FLEXArgumentInputColorView class];
|
||||
} else if ([FLEXArgumentInputFontView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = [FLEXArgumentInputFontView class];
|
||||
} else if ([FLEXArgumentInputStringView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = [FLEXArgumentInputStringView class];
|
||||
} else if ([FLEXArgumentInputStructView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = [FLEXArgumentInputStructView class];
|
||||
} else if ([FLEXArgumentInputSwitchView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = [FLEXArgumentInputSwitchView class];
|
||||
} else if ([FLEXArgumentInputDateView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = [FLEXArgumentInputDateView class];
|
||||
} else if ([FLEXArgumentInputNumberView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = [FLEXArgumentInputNumberView class];
|
||||
} else if ([FLEXArgumentInputJSONObjectView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = [FLEXArgumentInputJSONObjectView class];
|
||||
for (Class inputViewClass in inputViewClasses) {
|
||||
if ([inputViewClass supportsObjCType:typeEncoding + fieldNameOffset withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = inputViewClass;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return argumentInputViewSubclass;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
#import "FLEXMutableFieldEditorViewController.h"
|
||||
|
||||
@interface FLEXDefaultEditorViewController : FLEXFieldEditorViewController
|
||||
@interface FLEXDefaultEditorViewController : FLEXMutableFieldEditorViewController
|
||||
|
||||
- (id)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
@interface FLEXDefaultEditorViewController ()
|
||||
|
||||
@property (nonatomic, readonly) NSUserDefaults *defaults;
|
||||
@property (nonatomic, strong) NSString *key;
|
||||
@property (nonatomic) NSString *key;
|
||||
|
||||
@end
|
||||
|
||||
@@ -43,7 +43,10 @@
|
||||
self.fieldEditorView.fieldDescription = self.key;
|
||||
|
||||
id currentValue = [self.defaults objectForKey:self.key];
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:@encode(id) currentValue:currentValue];
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory
|
||||
argumentInputViewForTypeEncoding:FLEXEncodeObject(currentValue)
|
||||
currentValue:currentValue
|
||||
];
|
||||
inputView.backgroundColor = self.view.backgroundColor;
|
||||
inputView.inputValue = currentValue;
|
||||
self.fieldEditorView.argumentInputViews = @[inputView];
|
||||
@@ -64,9 +67,19 @@
|
||||
self.firstInputView.inputValue = [self.defaults objectForKey:self.key];
|
||||
}
|
||||
|
||||
- (void)getterButtonPressed:(id)sender
|
||||
{
|
||||
[super getterButtonPressed:sender];
|
||||
id returnedObject = [self.defaults objectForKey:self.key];
|
||||
[self exploreObjectOrPopViewController:returnedObject];
|
||||
}
|
||||
|
||||
+ (BOOL)canEditDefaultWithValue:(id)currentValue
|
||||
{
|
||||
return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:@encode(id) currentValue:currentValue];
|
||||
return [FLEXArgumentInputViewFactory
|
||||
canEditFieldWithTypeEncoding:FLEXEncodeObject(currentValue)
|
||||
currentValue:currentValue
|
||||
];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -15,6 +15,6 @@
|
||||
@property (nonatomic, copy) NSString *targetDescription;
|
||||
@property (nonatomic, copy) NSString *fieldDescription;
|
||||
|
||||
@property (nonatomic, strong) NSArray<FLEXArgumentInputView *> *argumentInputViews;
|
||||
@property (nonatomic) NSArray<FLEXArgumentInputView *> *argumentInputViews;
|
||||
|
||||
@end
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
|
||||
@interface FLEXFieldEditorView ()
|
||||
|
||||
@property (nonatomic, strong) UILabel *targetDescriptionLabel;
|
||||
@property (nonatomic, strong) UIView *targetDescriptionDivider;
|
||||
@property (nonatomic, strong) UILabel *fieldDescriptionLabel;
|
||||
@property (nonatomic, strong) UIView *fieldDescriptionDivider;
|
||||
@property (nonatomic) UILabel *targetDescriptionLabel;
|
||||
@property (nonatomic) UIView *targetDescriptionDivider;
|
||||
@property (nonatomic) UILabel *fieldDescriptionLabel;
|
||||
@property (nonatomic) UIView *fieldDescriptionDivider;
|
||||
|
||||
@end
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.targetDescriptionLabel = [[UILabel alloc] init];
|
||||
self.targetDescriptionLabel = [UILabel new];
|
||||
self.targetDescriptionLabel.numberOfLines = 0;
|
||||
self.targetDescriptionLabel.font = [[self class] labelFont];
|
||||
[self addSubview:self.targetDescriptionLabel];
|
||||
@@ -33,7 +33,7 @@
|
||||
self.targetDescriptionDivider = [[self class] dividerView];
|
||||
[self addSubview:self.targetDescriptionDivider];
|
||||
|
||||
self.fieldDescriptionLabel = [[UILabel alloc] init];
|
||||
self.fieldDescriptionLabel = [UILabel new];
|
||||
self.fieldDescriptionLabel.numberOfLines = 0;
|
||||
self.fieldDescriptionLabel.font = [[self class] labelFont];
|
||||
[self addSubview:self.fieldDescriptionLabel];
|
||||
@@ -123,14 +123,14 @@
|
||||
|
||||
+ (UIView *)dividerView
|
||||
{
|
||||
UIView *dividerView = [[UIView alloc] init];
|
||||
UIView *dividerView = [UIView new];
|
||||
dividerView.backgroundColor = [self dividerColor];
|
||||
return dividerView;
|
||||
}
|
||||
|
||||
+ (UIColor *)dividerColor
|
||||
{
|
||||
return [UIColor lightGrayColor];
|
||||
return UIColor.lightGrayColor;
|
||||
}
|
||||
|
||||
+ (CGFloat)horizontalPadding
|
||||
|
||||
@@ -19,10 +19,14 @@
|
||||
@property (nonatomic, readonly) FLEXArgumentInputView *firstInputView;
|
||||
|
||||
// For subclass use only.
|
||||
@property (nonatomic, strong, readonly) id target;
|
||||
@property (nonatomic, strong, readonly) FLEXFieldEditorView *fieldEditorView;
|
||||
@property (nonatomic, strong, readonly) UIBarButtonItem *setterButton;
|
||||
@property (nonatomic, readonly) id target;
|
||||
@property (nonatomic, readonly) FLEXFieldEditorView *fieldEditorView;
|
||||
@property (nonatomic, readonly) UIBarButtonItem *setterButton;
|
||||
|
||||
- (void)actionButtonPressed:(id)sender;
|
||||
- (NSString *)titleForActionButton;
|
||||
/// Pushes an explorer view controller for the given object
|
||||
/// or pops the current view controller.
|
||||
- (void)exploreObjectOrPopViewController:(id)objectOrNil;
|
||||
|
||||
@end
|
||||
|
||||
@@ -6,20 +6,23 @@
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
#import "FLEXFieldEditorView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
|
||||
@interface FLEXFieldEditorViewController () <UIScrollViewDelegate>
|
||||
|
||||
@property (nonatomic, strong) UIScrollView *scrollView;
|
||||
@property (nonatomic) UIScrollView *scrollView;
|
||||
|
||||
@property (nonatomic, strong, readwrite) id target;
|
||||
@property (nonatomic, strong, readwrite) FLEXFieldEditorView *fieldEditorView;
|
||||
@property (nonatomic, strong, readwrite) UIBarButtonItem *setterButton;
|
||||
@property (nonatomic, readwrite) id target;
|
||||
@property (nonatomic, readwrite) FLEXFieldEditorView *fieldEditorView;
|
||||
@property (nonatomic, readwrite) UIBarButtonItem *setterButton;
|
||||
|
||||
@end
|
||||
|
||||
@@ -30,15 +33,15 @@
|
||||
self = [super initWithNibName:nil bundle:nil];
|
||||
if (self) {
|
||||
self.target = target;
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
|
||||
[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];
|
||||
[NSNotificationCenter.defaultCenter removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)keyboardDidShow:(NSNotification *)notification
|
||||
@@ -72,7 +75,7 @@
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.view.backgroundColor = [FLEXUtility scrollViewGrayColor];
|
||||
self.view.backgroundColor = [FLEXColor scrollViewBackgroundColor];
|
||||
|
||||
self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
|
||||
self.scrollView.backgroundColor = self.view.backgroundColor;
|
||||
@@ -80,7 +83,7 @@
|
||||
self.scrollView.delegate = self;
|
||||
[self.view addSubview:self.scrollView];
|
||||
|
||||
self.fieldEditorView = [[FLEXFieldEditorView alloc] init];
|
||||
self.fieldEditorView = [FLEXFieldEditorView new];
|
||||
self.fieldEditorView.backgroundColor = self.view.backgroundColor;
|
||||
self.fieldEditorView.targetDescription = [NSString stringWithFormat:@"%@ %p", [self.target class], self.target];
|
||||
[self.scrollView addSubview:self.fieldEditorView];
|
||||
@@ -99,7 +102,7 @@
|
||||
|
||||
- (FLEXArgumentInputView *)firstInputView
|
||||
{
|
||||
return [[self.fieldEditorView argumentInputViews] firstObject];
|
||||
return [self.fieldEditorView argumentInputViews].firstObject;
|
||||
}
|
||||
|
||||
- (void)actionButtonPressed:(id)sender
|
||||
@@ -114,4 +117,16 @@
|
||||
return @"Set";
|
||||
}
|
||||
|
||||
- (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
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
#import "FLEXMutableFieldEditorViewController.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@interface FLEXIvarEditorViewController : FLEXFieldEditorViewController
|
||||
@interface FLEXIvarEditorViewController : FLEXMutableFieldEditorViewController
|
||||
|
||||
- (id)initWithTarget:(id)target ivar:(Ivar)ivar;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
@interface FLEXIvarEditorViewController () <FLEXArgumentInputViewDelegate>
|
||||
|
||||
@property (nonatomic, assign) Ivar ivar;
|
||||
@property (nonatomic) Ivar ivar;
|
||||
|
||||
@end
|
||||
|
||||
@@ -52,9 +52,23 @@
|
||||
- (void)actionButtonPressed:(id)sender
|
||||
{
|
||||
[super actionButtonPressed:sender];
|
||||
|
||||
// TODO: check mutability and use mutableCopy if necessary;
|
||||
// this currently could and would assign NSArray to NSMutableArray
|
||||
|
||||
[FLEXRuntimeUtility setValue:self.firstInputView.inputValue forIvar:self.ivar onObject:self.target];
|
||||
self.firstInputView.inputValue = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
|
||||
|
||||
// Pop view controller for consistency;
|
||||
// property setters and method calls also pop on success.
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
|
||||
- (void)getterButtonPressed:(id)sender
|
||||
{
|
||||
[super getterButtonPressed:sender];
|
||||
id returnedObject = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
|
||||
[self exploreObjectOrPopViewController:returnedObject];
|
||||
}
|
||||
|
||||
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXMethodCallingViewController ()
|
||||
|
||||
@property (nonatomic, assign) Method method;
|
||||
@property (nonatomic) Method method;
|
||||
@property (nonatomic) FLEXTypeEncoding *returnType;
|
||||
|
||||
@end
|
||||
|
||||
@@ -27,6 +29,7 @@
|
||||
self = [super initWithTarget:target];
|
||||
if (self) {
|
||||
self.method = method;
|
||||
self.returnType = [FLEXRuntimeUtility returnTypeForMethod:method];
|
||||
self.title = [self isClassMethod] ? @"Class Method" : @"Method";
|
||||
}
|
||||
return self;
|
||||
@@ -36,7 +39,11 @@
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility prettyNameForMethod:self.method isClassMethod:[self isClassMethod]];
|
||||
NSString *returnType = @((const char *)self.returnType);
|
||||
NSString *methodDescription = [FLEXRuntimeUtility prettyNameForMethod:self.method isClassMethod:[self isClassMethod]];
|
||||
NSString *format = @"Signature:\n%@\n\nReturn Type:\n%@";
|
||||
NSString *info = [NSString stringWithFormat:format, methodDescription, returnType];
|
||||
self.fieldEditorView.fieldDescription = info;
|
||||
|
||||
NSArray<NSString *> *methodComponents = [FLEXRuntimeUtility prettyArgumentComponentsForMethod:self.method];
|
||||
NSMutableArray<FLEXArgumentInputView *> *argumentInputViews = [NSMutableArray array];
|
||||
@@ -54,6 +61,12 @@
|
||||
self.fieldEditorView.argumentInputViews = argumentInputViews;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
free(self.returnType);
|
||||
self.returnType = NULL;
|
||||
}
|
||||
|
||||
- (BOOL)isClassMethod
|
||||
{
|
||||
return self.target && self.target == [self.target class];
|
||||
@@ -82,18 +95,14 @@
|
||||
id returnedObject = [FLEXRuntimeUtility performSelector:method_getName(self.method) onObject:self.target withArguments:arguments error:&error];
|
||||
|
||||
if (error) {
|
||||
NSString *title = @"Method Call Failed";
|
||||
NSString *message = [error localizedDescription];
|
||||
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
|
||||
[alert show];
|
||||
[FLEXAlert showAlert:@"Method Call Failed" message:[error localizedDescription] from:self];
|
||||
} else if (returnedObject) {
|
||||
// For non-nil (or void) return types, push an explorer view controller to display the returned object
|
||||
returnedObject = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:returnedObject type:self.returnType];
|
||||
FLEXObjectExplorerViewController *explorerViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:returnedObject];
|
||||
[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];
|
||||
[self exploreObjectOrPopViewController:returnedObject];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// FLEXMutableFieldEditorViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 11/22/18.
|
||||
// Copyright © 2018 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
|
||||
@interface FLEXMutableFieldEditorViewController : FLEXFieldEditorViewController
|
||||
|
||||
@property (nonatomic, readonly) UIBarButtonItem *getterButton;
|
||||
|
||||
- (void)getterButtonPressed:(id)sender;
|
||||
- (NSString *)titleForGetterButton;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// FLEXMutableFieldEditorViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 11/22/18.
|
||||
// Copyright © 2018 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXMutableFieldEditorViewController.h"
|
||||
#import "FLEXFieldEditorView.h"
|
||||
|
||||
@interface FLEXMutableFieldEditorViewController ()
|
||||
|
||||
@property (nonatomic, readwrite) UIBarButtonItem *getterButton;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXMutableFieldEditorViewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.getterButton = [[UIBarButtonItem alloc] initWithTitle:[self titleForGetterButton] style:UIBarButtonItemStyleDone target:self action:@selector(getterButtonPressed:)];
|
||||
self.navigationItem.rightBarButtonItems = @[self.setterButton, self.getterButton];
|
||||
}
|
||||
|
||||
- (void)getterButtonPressed:(id)sender {
|
||||
// Subclasses can override
|
||||
[self.fieldEditorView endEditing:YES];
|
||||
}
|
||||
|
||||
- (NSString *)titleForGetterButton {
|
||||
return @"Get";
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -6,13 +6,13 @@
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
#import "FLEXMutableFieldEditorViewController.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@interface FLEXPropertyEditorViewController : FLEXFieldEditorViewController
|
||||
@interface FLEXPropertyEditorViewController : FLEXMutableFieldEditorViewController
|
||||
|
||||
- (id)initWithTarget:(id)target property:(objc_property_t)property;
|
||||
|
||||
+ (BOOL)canEditProperty:(objc_property_t)property currentValue:(id)value;
|
||||
+ (BOOL)canEditProperty:(objc_property_t)property onObject:(id)object currentValue:(id)value;
|
||||
|
||||
@end
|
||||
|
||||
@@ -12,10 +12,11 @@
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXArgumentInputSwitchView.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXPropertyEditorViewController () <FLEXArgumentInputViewDelegate>
|
||||
|
||||
@property (nonatomic, assign) objc_property_t property;
|
||||
@property (nonatomic) objc_property_t property;
|
||||
|
||||
@end
|
||||
|
||||
@@ -37,9 +38,9 @@
|
||||
|
||||
self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility fullDescriptionForProperty:self.property];
|
||||
id currentValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
|
||||
self.setterButton.enabled = [[self class] canEditProperty:self.property currentValue:currentValue];
|
||||
self.setterButton.enabled = [[self class] canEditProperty:self.property onObject:self.target currentValue:currentValue];
|
||||
|
||||
const char *typeEncoding = [[FLEXRuntimeUtility typeEncodingForProperty:self.property] UTF8String];
|
||||
const char *typeEncoding = [FLEXRuntimeUtility typeEncodingForProperty:self.property].UTF8String;
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:typeEncoding];
|
||||
inputView.backgroundColor = self.view.backgroundColor;
|
||||
inputView.inputValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
|
||||
@@ -62,10 +63,7 @@
|
||||
NSError *error = nil;
|
||||
[FLEXRuntimeUtility performSelector:setterSelector onObject:self.target withArguments:arguments error:&error];
|
||||
if (error) {
|
||||
NSString *title = @"Property Setter Failed";
|
||||
NSString *message = [error localizedDescription];
|
||||
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
|
||||
[alert show];
|
||||
[FLEXAlert showAlert:@"Property Setter Failed" message:[error localizedDescription] from:self];
|
||||
self.firstInputView.inputValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
|
||||
} else {
|
||||
// If the setter was called without error, pop the view controller to indicate that and make the user's life easier.
|
||||
@@ -76,6 +74,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)getterButtonPressed:(id)sender
|
||||
{
|
||||
[super getterButtonPressed:sender];
|
||||
id returnedObject = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
|
||||
[self exploreObjectOrPopViewController:returnedObject];
|
||||
}
|
||||
|
||||
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView
|
||||
{
|
||||
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
|
||||
@@ -83,11 +88,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)canEditProperty:(objc_property_t)property currentValue:(id)value
|
||||
+ (BOOL)canEditProperty:(objc_property_t)property onObject:(id)object currentValue:(id)value
|
||||
{
|
||||
const char *typeEncoding = [[FLEXRuntimeUtility typeEncodingForProperty:property] UTF8String];
|
||||
const char *typeEncoding = [FLEXRuntimeUtility typeEncodingForProperty:property].UTF8String;
|
||||
BOOL canEditType = [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:typeEncoding currentValue:value];
|
||||
BOOL isReadonly = [FLEXRuntimeUtility isReadonlyProperty:property];
|
||||
SEL setterSelector = [FLEXRuntimeUtility setterSelectorForProperty:property];
|
||||
BOOL isReadonly = [FLEXRuntimeUtility isReadonlyProperty:property] && (!setterSelector || ![object respondsToSelector:setterSelector]);
|
||||
return canEditType && !isReadonly;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
@protocol FLEXExplorerViewControllerDelegate;
|
||||
|
||||
/// A view controller that manages the FLEX toolbar.
|
||||
@interface FLEXExplorerViewController : UIViewController
|
||||
|
||||
@property (nonatomic, weak) id <FLEXExplorerViewControllerDelegate> delegate;
|
||||
|
||||
@@ -26,49 +26,43 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
|
||||
@interface FLEXExplorerViewController () <FLEXHierarchyTableViewControllerDelegate, FLEXGlobalsTableViewControllerDelegate>
|
||||
|
||||
@property (nonatomic, strong) FLEXExplorerToolbar *explorerToolbar;
|
||||
@property (nonatomic) FLEXExplorerToolbar *explorerToolbar;
|
||||
|
||||
/// Tracks the currently active tool/mode
|
||||
@property (nonatomic, assign) FLEXExplorerMode currentMode;
|
||||
@property (nonatomic) FLEXExplorerMode currentMode;
|
||||
|
||||
/// Gesture recognizer for dragging a view in move mode
|
||||
@property (nonatomic, strong) UIPanGestureRecognizer *movePanGR;
|
||||
@property (nonatomic) UIPanGestureRecognizer *movePanGR;
|
||||
|
||||
/// Gesture recognizer for showing additional details on the selected view
|
||||
@property (nonatomic, strong) UITapGestureRecognizer *detailsTapGR;
|
||||
@property (nonatomic) UITapGestureRecognizer *detailsTapGR;
|
||||
|
||||
/// Only valid while a move pan gesture is in progress.
|
||||
@property (nonatomic, assign) CGRect selectedViewFrameBeforeDragging;
|
||||
@property (nonatomic) CGRect selectedViewFrameBeforeDragging;
|
||||
|
||||
/// Only valid while a toolbar drag pan gesture is in progress.
|
||||
@property (nonatomic, assign) CGRect toolbarFrameBeforeDragging;
|
||||
@property (nonatomic) CGRect toolbarFrameBeforeDragging;
|
||||
|
||||
/// Borders of all the visible views in the hierarchy at the selection point.
|
||||
/// The keys are NSValues with the correponding view (nonretained).
|
||||
@property (nonatomic, strong) NSDictionary<NSValue *, UIView *> *outlineViewsForVisibleViews;
|
||||
/// The keys are NSValues with the corresponding view (nonretained).
|
||||
@property (nonatomic) NSDictionary<NSValue *, UIView *> *outlineViewsForVisibleViews;
|
||||
|
||||
/// The actual views at the selection point with the deepest view last.
|
||||
@property (nonatomic, strong) NSArray<UIView *> *viewsAtTapPoint;
|
||||
@property (nonatomic) NSArray<UIView *> *viewsAtTapPoint;
|
||||
|
||||
/// The view that we're currently highlighting with an overlay and displaying details for.
|
||||
@property (nonatomic, strong) UIView *selectedView;
|
||||
@property (nonatomic) UIView *selectedView;
|
||||
|
||||
/// A colored transparent overlay to indicate that the view is selected.
|
||||
@property (nonatomic, strong) UIView *selectedViewOverlay;
|
||||
@property (nonatomic) UIView *selectedViewOverlay;
|
||||
|
||||
/// 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 intput.
|
||||
/// 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, strong) UIWindow *previousKeyWindow;
|
||||
|
||||
/// Similar to the previousKeyWindow property above, we need to track status bar styling if
|
||||
/// the app doesn't use view controller based status bar management. When we present a modal,
|
||||
/// we want to change the status bar style to UIStausBarStyleDefault. Before changing, we stash
|
||||
/// the current style. On dismissal, we return the staus bar to the style that the app was using previously.
|
||||
@property (nonatomic, assign) UIStatusBarStyle previousStatusBarStyle;
|
||||
@property (nonatomic) UIWindow *previousKeyWindow;
|
||||
|
||||
/// All views that we're KVOing. Used to help us clean up properly.
|
||||
@property (nonatomic, strong) NSMutableSet<UIView *> *observedViews;
|
||||
@property (nonatomic) NSMutableSet<UIView *> *observedViews;
|
||||
|
||||
@end
|
||||
|
||||
@@ -93,9 +87,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
|
||||
// Toolbar
|
||||
self.explorerToolbar = [[FLEXExplorerToolbar alloc] init];
|
||||
self.explorerToolbar = [FLEXExplorerToolbar new];
|
||||
|
||||
// Start the toolbar off below any bars that may be at the top of the view.
|
||||
id toolbarOriginYDefault = [[NSUserDefaults standardUserDefaults] objectForKey:kFLEXToolbarTopMarginDefaultsKey];
|
||||
@@ -131,16 +125,15 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
|
||||
- (UIViewController *)viewControllerForRotationAndOrientation
|
||||
{
|
||||
UIWindow *window = self.previousKeyWindow ?: [[UIApplication sharedApplication] keyWindow];
|
||||
UIWindow *window = self.previousKeyWindow ?: [UIApplication.sharedApplication keyWindow];
|
||||
UIViewController *viewController = window.rootViewController;
|
||||
// Obfuscating selector _viewControllerForSupportedInterfaceOrientations
|
||||
NSString *viewControllerSelectorString = [@[@"_vie", @"wContro", @"llerFor", @"Supported", @"Interface", @"Orientations"] componentsJoinedByString:@""];
|
||||
SEL viewControllerSelector = NSSelectorFromString(viewControllerSelectorString);
|
||||
if ([viewController respondsToSelector:viewControllerSelector]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
viewController = [viewController performSelector:viewControllerSelector];
|
||||
#pragma clang diagnostic pop
|
||||
viewController = [viewController valueForKey:viewControllerSelectorString];
|
||||
}
|
||||
|
||||
return viewController;
|
||||
}
|
||||
|
||||
@@ -153,7 +146,8 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
}
|
||||
|
||||
// The UIViewController docs state that this method must not return zero.
|
||||
// If we weren't able to get a valid value for the supported interface orientations, default to all supported.
|
||||
// If we weren't able to get a valid value for the supported interface
|
||||
// orientations, default to all supported.
|
||||
if (supportedOrientations == 0) {
|
||||
supportedOrientations = UIInterfaceOrientationMaskAll;
|
||||
}
|
||||
@@ -174,25 +168,25 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
|
||||
{
|
||||
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
||||
for (UIView *outlineView in [self.outlineViewsForVisibleViews allValues]) {
|
||||
outlineView.hidden = YES;
|
||||
}
|
||||
self.selectedViewOverlay.hidden = YES;
|
||||
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
||||
for (UIView *view in self.viewsAtTapPoint) {
|
||||
NSValue *key = [NSValue valueWithNonretainedObject:view];
|
||||
UIView *outlineView = self.outlineViewsForVisibleViews[key];
|
||||
outlineView.frame = [self frameInLocalCoordinatesForView:view];
|
||||
if (self.currentMode == FLEXExplorerModeSelect) {
|
||||
outlineView.hidden = NO;
|
||||
}
|
||||
}
|
||||
for (UIView *outlineView in self.outlineViewsForVisibleViews.allValues) {
|
||||
outlineView.hidden = YES;
|
||||
}
|
||||
self.selectedViewOverlay.hidden = YES;
|
||||
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
||||
for (UIView *view in self.viewsAtTapPoint) {
|
||||
NSValue *key = [NSValue valueWithNonretainedObject:view];
|
||||
UIView *outlineView = self.outlineViewsForVisibleViews[key];
|
||||
outlineView.frame = [self frameInLocalCoordinatesForView:view];
|
||||
if (self.currentMode == FLEXExplorerModeSelect) {
|
||||
outlineView.hidden = NO;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.selectedView) {
|
||||
self.selectedViewOverlay.frame = [self frameInLocalCoordinatesForView:self.selectedView];
|
||||
self.selectedViewOverlay.hidden = NO;
|
||||
}
|
||||
}];
|
||||
if (self.selectedView) {
|
||||
self.selectedViewOverlay.frame = [self frameInLocalCoordinatesForView:self.selectedView];
|
||||
self.selectedViewOverlay.hidden = NO;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Setter Overrides
|
||||
@@ -214,13 +208,13 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
|
||||
if (selectedView) {
|
||||
if (!self.selectedViewOverlay) {
|
||||
self.selectedViewOverlay = [[UIView alloc] init];
|
||||
self.selectedViewOverlay = [UIView new];
|
||||
[self.view addSubview:self.selectedViewOverlay];
|
||||
self.selectedViewOverlay.layer.borderWidth = 1.0;
|
||||
}
|
||||
UIColor *outlineColor = [FLEXUtility consistentRandomColorForObject:selectedView];
|
||||
self.selectedViewOverlay.backgroundColor = [outlineColor colorWithAlphaComponent:0.2];
|
||||
self.selectedViewOverlay.layer.borderColor = [outlineColor CGColor];
|
||||
self.selectedViewOverlay.layer.borderColor = outlineColor.CGColor;
|
||||
self.selectedViewOverlay.frame = [self.view convertRect:selectedView.bounds fromView:selectedView];
|
||||
|
||||
// Make sure the selected overlay is in front of all the other subviews except the toolbar, which should always stay on top.
|
||||
@@ -396,7 +390,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
- (UIWindow *)statusWindow
|
||||
{
|
||||
NSString *statusBarString = [NSString stringWithFormat:@"%@arWindow", @"_statusB"];
|
||||
return [[UIApplication sharedApplication] valueForKey:statusBarString];
|
||||
return [UIApplication.sharedApplication valueForKey:statusBarString];
|
||||
}
|
||||
|
||||
- (void)moveButtonTapped:(FLEXToolbarItem *)sender
|
||||
@@ -447,12 +441,12 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
switch (panGR.state) {
|
||||
case UIGestureRecognizerStateBegan:
|
||||
self.toolbarFrameBeforeDragging = self.explorerToolbar.frame;
|
||||
[self updateToolbarPostionWithDragGesture:panGR];
|
||||
[self updateToolbarPositionWithDragGesture:panGR];
|
||||
break;
|
||||
|
||||
case UIGestureRecognizerStateChanged:
|
||||
case UIGestureRecognizerStateEnded:
|
||||
[self updateToolbarPostionWithDragGesture:panGR];
|
||||
[self updateToolbarPositionWithDragGesture:panGR];
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -460,7 +454,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateToolbarPostionWithDragGesture:(UIPanGestureRecognizer *)panGR
|
||||
- (void)updateToolbarPositionWithDragGesture:(UIPanGestureRecognizer *)panGR
|
||||
{
|
||||
CGPoint translation = [panGR translationInView:self.view];
|
||||
CGRect newToolbarFrame = self.toolbarFrameBeforeDragging;
|
||||
@@ -561,8 +555,8 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
{
|
||||
CGRect outlineFrame = [self frameInLocalCoordinatesForView:view];
|
||||
UIView *outlineView = [[UIView alloc] initWithFrame:outlineFrame];
|
||||
outlineView.backgroundColor = [UIColor clearColor];
|
||||
outlineView.layer.borderColor = [[FLEXUtility consistentRandomColorForObject:view] CGColor];
|
||||
outlineView.backgroundColor = UIColor.clearColor;
|
||||
outlineView.layer.borderColor = [FLEXUtility consistentRandomColorForObject:view].CGColor;
|
||||
outlineView.layer.borderWidth = 1.0;
|
||||
return outlineView;
|
||||
}
|
||||
@@ -593,8 +587,8 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
{
|
||||
// Select in the window that would handle the touch, but don't just use the result of hitTest:withEvent: so we can still select views with interaction disabled.
|
||||
// Default to the the application's key window if none of the windows want the touch.
|
||||
UIWindow *windowForSelection = [[UIApplication sharedApplication] keyWindow];
|
||||
for (UIWindow *window in [[FLEXUtility allWindows] reverseObjectEnumerator]) {
|
||||
UIWindow *windowForSelection = [UIApplication.sharedApplication keyWindow];
|
||||
for (UIWindow *window in [FLEXUtility allWindows].reverseObjectEnumerator) {
|
||||
// Ignore the explorer's own window.
|
||||
if (window != self.view.window) {
|
||||
if ([window hitTest:tapPointInWindow withEvent:nil]) {
|
||||
@@ -605,7 +599,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
}
|
||||
|
||||
// Select the deepest visible view at the tap point. This generally corresponds to what the user wants to select.
|
||||
return [[self recursiveSubviewsAtPoint:tapPointInWindow inView:windowForSelection skipHiddenViews:YES] lastObject];
|
||||
return [self recursiveSubviewsAtPoint:tapPointInWindow inView:windowForSelection skipHiddenViews:YES].lastObject;
|
||||
}
|
||||
|
||||
- (NSArray<UIView *> *)recursiveSubviewsAtPoint:(CGPoint)pointInView inView:(UIView *)view skipHiddenViews:(BOOL)skipHidden
|
||||
@@ -693,26 +687,23 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
- (CGRect)viewSafeArea
|
||||
{
|
||||
CGRect safeArea = self.view.bounds;
|
||||
#if FLEX_AT_LEAST_IOS11_SDK
|
||||
if (@available(iOS 11, *)) {
|
||||
if (@available(iOS 11.0, *)) {
|
||||
safeArea = UIEdgeInsetsInsetRect(self.view.bounds, self.view.safeAreaInsets);
|
||||
}
|
||||
#endif
|
||||
|
||||
return safeArea;
|
||||
}
|
||||
|
||||
#if FLEX_AT_LEAST_IOS11_SDK
|
||||
- (void)viewSafeAreaInsetsDidChange
|
||||
{
|
||||
if (@available(iOS 11, *)) {
|
||||
[super viewSafeAreaInsetsDidChange];
|
||||
}
|
||||
if (@available(iOS 11.0, *)) {
|
||||
[super viewSafeAreaInsetsDidChange];
|
||||
|
||||
CGRect safeArea = [self viewSafeArea];
|
||||
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:CGSizeMake(CGRectGetWidth(self.view.bounds), CGRectGetHeight(safeArea))];
|
||||
[self updateToolbarPositionWithUnconstrainedFrame:CGRectMake(CGRectGetMinX(self.explorerToolbar.frame), CGRectGetMinY(self.explorerToolbar.frame), toolbarSize.width, toolbarSize.height)];
|
||||
CGRect safeArea = [self viewSafeArea];
|
||||
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:CGSizeMake(CGRectGetWidth(self.view.bounds), CGRectGetHeight(safeArea))];
|
||||
[self updateToolbarPositionWithUnconstrainedFrame:CGRectMake(CGRectGetMinX(self.explorerToolbar.frame), CGRectGetMinY(self.explorerToolbar.frame), toolbarSize.width, toolbarSize.height)];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#pragma mark - Touch Handling
|
||||
@@ -793,20 +784,28 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
- (void)makeKeyAndPresentViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(void (^)(void))completion
|
||||
{
|
||||
// Save the current key window so we can restore it following dismissal.
|
||||
self.previousKeyWindow = [[UIApplication sharedApplication] keyWindow];
|
||||
|
||||
self.previousKeyWindow = UIApplication.sharedApplication.keyWindow;
|
||||
|
||||
// Make our window key to correctly handle input.
|
||||
[self.view.window makeKeyWindow];
|
||||
|
||||
|
||||
// Fix for iOS 13, regarding custom UIMenu callouts not appearing because
|
||||
// the UITextEffectsWindow has a lower level than the FLEX window by default
|
||||
// until a text field is activated, bringing it above the FLEX window.
|
||||
if (@available(iOS 13, *)) {
|
||||
for (UIWindow *window in UIApplication.sharedApplication.windows) {
|
||||
if ([window isKindOfClass:NSClassFromString(@"UITextEffectsWindow")]) {
|
||||
if (window.windowLevel <= self.view.window.windowLevel) {
|
||||
window.windowLevel = self.view.window.windowLevel + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move the status bar on top of FLEX so we can get scroll to top behavior for taps.
|
||||
[[self statusWindow] setWindowLevel:self.view.window.windowLevel + 1.0];
|
||||
|
||||
// If this app doesn't use view controller based status bar management and we're on iOS 7+,
|
||||
// make sure the status bar style is UIStatusBarStyleDefault. We don't actully have to check
|
||||
// for view controller based management because the global methods no-op if that is turned on.
|
||||
self.previousStatusBarStyle = [[UIApplication sharedApplication] statusBarStyle];
|
||||
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault];
|
||||
|
||||
// Show the view controller.
|
||||
[self presentViewController:viewController animated:animated completion:completion];
|
||||
}
|
||||
@@ -822,9 +821,6 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
// We want it above FLEX while a modal is presented for scroll to top, but below FLEX otherwise for exploration.
|
||||
[[self statusWindow] setWindowLevel:UIWindowLevelStatusBar];
|
||||
|
||||
// Restore the stauts bar style if the app is using global status bar management.
|
||||
[[UIApplication sharedApplication] setStatusBarStyle:self.previousStatusBarStyle];
|
||||
|
||||
[self dismissViewControllerAnimated:animated completion:completion];
|
||||
}
|
||||
|
||||
@@ -837,7 +833,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
{
|
||||
if (self.presentedViewController) {
|
||||
[self resignKeyAndDismissViewControllerAnimated:YES completion:completion];
|
||||
} else {
|
||||
} else if (future) {
|
||||
[self makeKeyAndPresentViewController:future() animated:YES completion:completion];
|
||||
}
|
||||
}
|
||||
@@ -885,9 +881,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
- (void)toggleMenuTool
|
||||
{
|
||||
[self toggleToolWithViewControllerProvider:^UIViewController *{
|
||||
FLEXGlobalsTableViewController *globalsViewController = [[FLEXGlobalsTableViewController alloc] init];
|
||||
FLEXGlobalsTableViewController *globalsViewController = [FLEXGlobalsTableViewController new];
|
||||
globalsViewController.delegate = self;
|
||||
[FLEXGlobalsTableViewController setApplicationWindow:[[UIApplication sharedApplication] keyWindow]];
|
||||
[FLEXGlobalsTableViewController setApplicationWindow:[UIApplication.sharedApplication keyWindow]];
|
||||
return [[UINavigationController alloc] initWithRootViewController:globalsViewController];
|
||||
} completion:nil];
|
||||
}
|
||||
@@ -896,9 +892,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
{
|
||||
if (self.currentMode == FLEXExplorerModeMove) {
|
||||
CGRect frame = self.selectedView.frame;
|
||||
frame.origin.y += 1.0 / [[UIScreen mainScreen] scale];
|
||||
frame.origin.y += 1.0 / UIScreen.mainScreen.scale;
|
||||
self.selectedView.frame = frame;
|
||||
} else if (self.currentMode == FLEXExplorerModeSelect && [self.viewsAtTapPoint count] > 0) {
|
||||
} else if (self.currentMode == FLEXExplorerModeSelect && self.viewsAtTapPoint.count > 0) {
|
||||
NSInteger selectedViewIndex = [self.viewsAtTapPoint indexOfObject:self.selectedView];
|
||||
if (selectedViewIndex > 0) {
|
||||
self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex - 1];
|
||||
@@ -910,11 +906,11 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
{
|
||||
if (self.currentMode == FLEXExplorerModeMove) {
|
||||
CGRect frame = self.selectedView.frame;
|
||||
frame.origin.y -= 1.0 / [[UIScreen mainScreen] scale];
|
||||
frame.origin.y -= 1.0 / UIScreen.mainScreen.scale;
|
||||
self.selectedView.frame = frame;
|
||||
} else if (self.currentMode == FLEXExplorerModeSelect && [self.viewsAtTapPoint count] > 0) {
|
||||
} else if (self.currentMode == FLEXExplorerModeSelect && self.viewsAtTapPoint.count > 0) {
|
||||
NSInteger selectedViewIndex = [self.viewsAtTapPoint indexOfObject:self.selectedView];
|
||||
if (selectedViewIndex < [self.viewsAtTapPoint count] - 1) {
|
||||
if (selectedViewIndex < self.viewsAtTapPoint.count - 1) {
|
||||
self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex + 1];
|
||||
}
|
||||
}
|
||||
@@ -924,7 +920,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
{
|
||||
if (self.currentMode == FLEXExplorerModeMove) {
|
||||
CGRect frame = self.selectedView.frame;
|
||||
frame.origin.x += 1.0 / [[UIScreen mainScreen] scale];
|
||||
frame.origin.x += 1.0 / UIScreen.mainScreen.scale;
|
||||
self.selectedView.frame = frame;
|
||||
}
|
||||
}
|
||||
@@ -933,7 +929,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
{
|
||||
if (self.currentMode == FLEXExplorerModeMove) {
|
||||
CGRect frame = self.selectedView.frame;
|
||||
frame.origin.x -= 1.0 / [[UIScreen mainScreen] scale];
|
||||
frame.origin.x -= 1.0 / UIScreen.mainScreen.scale;
|
||||
self.selectedView.frame = frame;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
self.backgroundColor = UIColor.clearColor;
|
||||
// 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.
|
||||
@@ -49,7 +49,7 @@
|
||||
// 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 shoudn't ship this to the App Store anyways...
|
||||
// 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));
|
||||
|
||||
+12
-4
@@ -9,6 +9,10 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#if !FLEX_AT_LEAST_IOS13_SDK
|
||||
@class UIWindowScene;
|
||||
#endif
|
||||
|
||||
typedef UIViewController *(^FLEXCustomContentViewerFuture)(NSData *data);
|
||||
|
||||
@interface FLEXManager : NSObject
|
||||
@@ -21,16 +25,20 @@ typedef UIViewController *(^FLEXCustomContentViewerFuture)(NSData *data);
|
||||
- (void)hideExplorer;
|
||||
- (void)toggleExplorer;
|
||||
|
||||
/// Use this to present the explorer in a specific scene when the one
|
||||
/// it chooses by default is not the one you wish to display it in.
|
||||
- (void)showExplorerFromScene:(UIWindowScene *)scene API_AVAILABLE(ios(13.0));
|
||||
|
||||
#pragma mark - Network Debugging
|
||||
|
||||
/// If this property is set to YES, FLEX will swizzle NSURLConnection*Delegate and NSURLSession*Delegate methods
|
||||
/// on classes that conform to the protocols. This allows you to view network activity history from the main FLEX menu.
|
||||
/// Full responses are kept temporarily in a size-limited cache and may be pruned under memory pressure.
|
||||
@property (nonatomic, assign, getter=isNetworkDebuggingEnabled) BOOL networkDebuggingEnabled;
|
||||
@property (nonatomic, getter=isNetworkDebuggingEnabled) BOOL networkDebuggingEnabled;
|
||||
|
||||
/// Defaults to 25 MB if never set. Values set here are presisted across launches of the app.
|
||||
/// Defaults to 25 MB if never set. Values set here are persisted across launches of the app.
|
||||
/// The response cache uses an NSCache, so it may purge prior to hitting the limit when the app is under memory pressure.
|
||||
@property (nonatomic, assign) NSUInteger networkResponseCacheByteLimit;
|
||||
@property (nonatomic) NSUInteger networkResponseCacheByteLimit;
|
||||
|
||||
/// Requests whose host ends with one of the blacklisted entries in this array will be not be recorded (eg. google.com).
|
||||
/// Wildcard or subdomain entries are not required (eg. google.com will match any subdomain under google.com).
|
||||
@@ -44,7 +52,7 @@ typedef UIViewController *(^FLEXCustomContentViewerFuture)(NSData *data);
|
||||
/// The shortcuts will not fire when there is an active text field, text view, or other responder accepting key input.
|
||||
/// You can disable keyboard shortcuts if you have existing keyboard shortcuts that conflict with FLEX, or if you like doing things the hard way ;)
|
||||
/// Keyboard shortcuts are always disabled (and support is compiled out) in non-simulator builds
|
||||
@property (nonatomic, assign) BOOL simulatorShortcutsEnabled;
|
||||
@property (nonatomic) BOOL simulatorShortcutsEnabled;
|
||||
|
||||
/// Adds an action to run when the specified key & modifier combination is pressed
|
||||
/// @param key A single character string matching a key on the keyboard
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
@interface FLEXMultiColumnTableView ()
|
||||
<UITableViewDataSource, UITableViewDelegate,UIScrollViewDelegate, FLEXTableContentCellDelegate>
|
||||
|
||||
@property (nonatomic, strong) UIScrollView *contentScrollView;
|
||||
@property (nonatomic, strong) UIScrollView *headerScrollView;
|
||||
@property (nonatomic, strong) UITableView *leftTableView;
|
||||
@property (nonatomic, strong) UITableView *contentTableView;
|
||||
@property (nonatomic, strong) UIView *leftHeader;
|
||||
@property (nonatomic) UIScrollView *contentScrollView;
|
||||
@property (nonatomic) UIScrollView *headerScrollView;
|
||||
@property (nonatomic) UITableView *leftTableView;
|
||||
@property (nonatomic) UITableView *contentTableView;
|
||||
@property (nonatomic) UIView *leftHeader;
|
||||
|
||||
@property (nonatomic, strong) NSDictionary<NSString *, NSNumber *> *sortStatusDict;
|
||||
@property (nonatomic, strong) NSArray *rowData;
|
||||
@property (nonatomic) NSDictionary<NSString *, NSNumber *> *sortStatusDict;
|
||||
@property (nonatomic) NSArray *rowData;
|
||||
@end
|
||||
|
||||
static const CGFloat kColumnMargin = 1;
|
||||
@@ -51,6 +51,11 @@ static const CGFloat kColumnMargin = 1;
|
||||
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 rowsCount = [self numberOfColumns];
|
||||
@@ -58,13 +63,13 @@ static const CGFloat kColumnMargin = 1;
|
||||
contentWidth += [self contentWidthForColumn:i];
|
||||
}
|
||||
|
||||
self.leftTableView.frame = CGRectMake(0, topheaderHeight, leftHeaderWidth, height - topheaderHeight);
|
||||
self.headerScrollView.frame = CGRectMake(leftHeaderWidth, 0, width - leftHeaderWidth, topheaderHeight);
|
||||
self.leftTableView.frame = CGRectMake(0, topheaderHeight + topInsets, leftHeaderWidth, height - topheaderHeight - topInsets);
|
||||
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] , height - topheaderHeight);
|
||||
self.contentScrollView.frame = CGRectMake(leftHeaderWidth, topheaderHeight, width - leftHeaderWidth, height - topheaderHeight);
|
||||
self.contentTableView.frame = CGRectMake(0, 0, contentWidth + [self numberOfColumns] * [self columnMargin] , height - topheaderHeight - topInsets);
|
||||
self.contentScrollView.frame = CGRectMake(leftHeaderWidth, topheaderHeight + topInsets, width - leftHeaderWidth, height - topheaderHeight - topInsets);
|
||||
self.contentScrollView.contentSize = self.contentTableView.frame.size;
|
||||
self.leftHeader.frame = CGRectMake(0, 0, [self leftHeaderWidth], [self topHeaderHeight]);
|
||||
self.leftHeader.frame = CGRectMake(0, topInsets, [self leftHeaderWidth], [self topHeaderHeight]);
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +91,7 @@ static const CGFloat kColumnMargin = 1;
|
||||
|
||||
- (void)loadHeaderScrollView
|
||||
{
|
||||
UIScrollView *headerScrollView = [[UIScrollView alloc] init];
|
||||
UIScrollView *headerScrollView = [UIScrollView new];
|
||||
headerScrollView.delegate = self;
|
||||
self.headerScrollView = headerScrollView;
|
||||
self.headerScrollView.backgroundColor = [UIColor colorWithWhite:0.803 alpha:0.850];
|
||||
@@ -97,11 +102,11 @@ static const CGFloat kColumnMargin = 1;
|
||||
- (void)loadContentScrollView
|
||||
{
|
||||
|
||||
UIScrollView *scrollView = [[UIScrollView alloc] init];
|
||||
UIScrollView *scrollView = [UIScrollView new];
|
||||
scrollView.bounces = NO;
|
||||
scrollView.delegate = self;
|
||||
|
||||
UITableView *tableView = [[UITableView alloc] init];
|
||||
UITableView *tableView = [UITableView new];
|
||||
tableView.delegate = self;
|
||||
tableView.dataSource = self;
|
||||
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
@@ -116,14 +121,14 @@ static const CGFloat kColumnMargin = 1;
|
||||
|
||||
- (void)loadLeftView
|
||||
{
|
||||
UITableView *leftTableView = [[UITableView alloc] init];
|
||||
UITableView *leftTableView = [UITableView new];
|
||||
leftTableView.delegate = self;
|
||||
leftTableView.dataSource = self;
|
||||
leftTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
self.leftTableView = leftTableView;
|
||||
[self addSubview:leftTableView];
|
||||
|
||||
UIView *leftHeader = [[UIView alloc] init];
|
||||
UIView *leftHeader = [UIView new];
|
||||
leftHeader.backgroundColor = [UIColor colorWithWhite:0.950 alpha:0.668];
|
||||
self.leftHeader = leftHeader;
|
||||
[self addSubview:leftHeader];
|
||||
@@ -199,7 +204,7 @@ static const CGFloat kColumnMargin = 1;
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView
|
||||
cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
UIColor *backgroundColor = [UIColor whiteColor];
|
||||
UIColor *backgroundColor = UIColor.whiteColor;
|
||||
if (indexPath.row % 2 != 0) {
|
||||
backgroundColor = [UIColor colorWithWhite:0.950 alpha:0.750];
|
||||
}
|
||||
@@ -214,11 +219,11 @@ static const CGFloat kColumnMargin = 1;
|
||||
for (int i = 0 ; i < cell.labels.count; i++) {
|
||||
|
||||
UILabel *label = cell.labels[i];
|
||||
label.textColor = [UIColor blackColor];
|
||||
label.textColor = UIColor.blackColor;
|
||||
|
||||
NSString *content = [NSString stringWithFormat:@"%@",self.rowData[i]];
|
||||
if ([content isEqualToString:@"<null>"]) {
|
||||
label.textColor = [UIColor lightGrayColor];
|
||||
label.textColor = UIColor.lightGrayColor;
|
||||
content = @"NULL";
|
||||
}
|
||||
label.text = content;
|
||||
@@ -282,7 +287,7 @@ static const CGFloat kColumnMargin = 1;
|
||||
#pragma mark -
|
||||
#pragma mark DataSource Accessor
|
||||
|
||||
- (NSInteger)numberOfrows
|
||||
- (NSInteger)numberOfRows
|
||||
{
|
||||
return [self.dataSource numberOfRowsInTableView:self];
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
@interface FLEXRealmDatabaseManager ()
|
||||
|
||||
@property (nonatomic, copy) NSString *path;
|
||||
@property (nonatomic, strong) RLMRealm * realm;
|
||||
@property (nonatomic) RLMRealm * realm;
|
||||
|
||||
@end
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
id configuration = [[configurationClass alloc] init];
|
||||
id configuration = [configurationClass new];
|
||||
[(RLMRealmConfiguration *)configuration setFileURL:[NSURL fileURLWithPath:self.path]];
|
||||
self.realm = [realmClass realmWithConfiguration:configuration error:&error];
|
||||
return (error == nil);
|
||||
|
||||
@@ -33,7 +33,7 @@ static NSString *const QUERY_TABLENAMES_SQL = @"SELECT name FROM sqlite_master W
|
||||
if (_db) {
|
||||
return YES;
|
||||
}
|
||||
int err = sqlite3_open([_databasePath UTF8String], &_db);
|
||||
int err = sqlite3_open(_databasePath.UTF8String, &_db);
|
||||
|
||||
#if SQLITE_HAS_CODEC
|
||||
NSString *defaultSqliteDatabasePassword = [FLEXManager sharedManager].defaultSqliteDatabasePassword;
|
||||
@@ -117,7 +117,7 @@ static NSString *const QUERY_TABLENAMES_SQL = @"SELECT name FROM sqlite_master W
|
||||
[self open];
|
||||
NSMutableArray<NSDictionary<NSString *, id> *> *resultArray = [NSMutableArray array];
|
||||
sqlite3_stmt *pstmt;
|
||||
if (sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pstmt, 0) == SQLITE_OK) {
|
||||
if (sqlite3_prepare_v2(_db, sql.UTF8String, -1, &pstmt, 0) == SQLITE_OK) {
|
||||
while (sqlite3_step(pstmt) == SQLITE_ROW) {
|
||||
NSUInteger num_cols = (NSUInteger)sqlite3_data_count(pstmt);
|
||||
if (num_cols > 0) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// FLEXTableContentHeaderCell.h
|
||||
// UICatalog
|
||||
// FLEX
|
||||
//
|
||||
// Created by Peng Tao on 15/11/26.
|
||||
// Copyright © 2015年 f. All rights reserved.
|
||||
@@ -16,7 +16,7 @@ typedef NS_ENUM(NSUInteger, FLEXTableColumnHeaderSortType) {
|
||||
|
||||
@interface FLEXTableColumnHeader : UIView
|
||||
|
||||
@property (nonatomic, strong) UILabel *label;
|
||||
@property (nonatomic) UILabel *label;
|
||||
|
||||
- (void)changeSortStatusWithType:(FLEXTableColumnHeaderSortType)type;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// FLEXTableContentHeaderCell.m
|
||||
// UICatalog
|
||||
// FLEX
|
||||
//
|
||||
// Created by Peng Tao on 15/11/26.
|
||||
// Copyright © 2015年 f. All rights reserved.
|
||||
@@ -18,7 +18,7 @@
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.backgroundColor = [UIColor whiteColor];
|
||||
self.backgroundColor = UIColor.whiteColor;
|
||||
|
||||
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(5, 0, frame.size.width - 25, frame.size.height)];
|
||||
label.font = [UIFont systemFontOfSize:13.0];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// FLEXTableContentCell.h
|
||||
// UICatalog
|
||||
// FLEX
|
||||
//
|
||||
// Created by Peng Tao on 15/11/24.
|
||||
// Copyright © 2015年 f. All rights reserved.
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
@interface FLEXTableContentCell : UITableViewCell
|
||||
|
||||
@property (nonatomic, strong) NSArray<UILabel *> *labels;
|
||||
@property (nonatomic) NSArray<UILabel *> *labels;
|
||||
|
||||
@property (nonatomic, weak) id<FLEXTableContentCellDelegate> delegate;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// FLEXTableContentCell.m
|
||||
// UICatalog
|
||||
// FLEX
|
||||
//
|
||||
// Created by Peng Tao on 15/11/24.
|
||||
// Copyright © 2015年 f. All rights reserved.
|
||||
@@ -24,10 +24,10 @@
|
||||
NSMutableArray<UILabel *> *labels = [NSMutableArray array];
|
||||
for (int i = 0; i < number ; i++) {
|
||||
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
|
||||
label.backgroundColor = [UIColor whiteColor];
|
||||
label.backgroundColor = UIColor.whiteColor;
|
||||
label.font = [UIFont systemFontOfSize:13.0];
|
||||
label.textAlignment = NSTextAlignmentLeft;
|
||||
label.backgroundColor = [UIColor greenColor];
|
||||
label.backgroundColor = UIColor.greenColor;
|
||||
[labels addObject:label];
|
||||
|
||||
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:cell
|
||||
@@ -36,7 +36,7 @@
|
||||
label.userInteractionEnabled = YES;
|
||||
|
||||
[cell.contentView addSubview:label];
|
||||
cell.contentView.backgroundColor = [UIColor whiteColor];
|
||||
cell.contentView.backgroundColor = UIColor.whiteColor;
|
||||
}
|
||||
cell.labels = labels;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
@interface FLEXTableContentViewController : UIViewController
|
||||
|
||||
@property (nonatomic, strong) NSArray<NSString *> *columnsArray;
|
||||
@property (nonatomic, strong) NSArray<NSDictionary<NSString *, id> *> *contentsArray;
|
||||
@property (nonatomic) NSArray<NSString *> *columnsArray;
|
||||
@property (nonatomic) NSArray<NSDictionary<NSString *, id> *> *contentsArray;
|
||||
|
||||
@end
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
@interface FLEXTableContentViewController ()<FLEXMultiColumnTableViewDataSource, FLEXMultiColumnTableViewDelegate>
|
||||
|
||||
@property (nonatomic, strong) FLEXMultiColumnTableView *multiColumView;
|
||||
@property (nonatomic) FLEXMultiColumnTableView *multiColumnView;
|
||||
|
||||
@end
|
||||
|
||||
@@ -22,29 +22,29 @@
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.edgesForExtendedLayout = UIRectEdgeNone;
|
||||
[self.view addSubview:self.multiColumView];
|
||||
[self.view addSubview:self.multiColumnView];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
[self.multiColumView reloadData];
|
||||
[self.multiColumnView reloadData];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
#pragma mark init SubView
|
||||
- (FLEXMultiColumnTableView *)multiColumView {
|
||||
if (!_multiColumView) {
|
||||
_multiColumView = [[FLEXMultiColumnTableView alloc] initWithFrame:
|
||||
- (FLEXMultiColumnTableView *)multiColumnView {
|
||||
if (!_multiColumnView) {
|
||||
_multiColumnView = [[FLEXMultiColumnTableView alloc] initWithFrame:
|
||||
CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
|
||||
|
||||
_multiColumView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleTopMargin;
|
||||
_multiColumView.backgroundColor = [UIColor whiteColor];
|
||||
_multiColumView.dataSource = self;
|
||||
_multiColumView.delegate = self;
|
||||
_multiColumnView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleTopMargin;
|
||||
_multiColumnView.backgroundColor = UIColor.whiteColor;
|
||||
_multiColumnView.dataSource = self;
|
||||
_multiColumnView.delegate = self;
|
||||
}
|
||||
return _multiColumView;
|
||||
return _multiColumnView;
|
||||
}
|
||||
#pragma mark MultiColumnTableView DataSource
|
||||
|
||||
@@ -151,12 +151,12 @@
|
||||
return result;
|
||||
}];
|
||||
if (sortType == FLEXTableColumnHeaderSortTypeDesc) {
|
||||
NSEnumerator *contentReverseEvumerator = [sortContentData reverseObjectEnumerator];
|
||||
sortContentData = [NSArray arrayWithArray:[contentReverseEvumerator allObjects]];
|
||||
NSEnumerator *contentReverseEnumerator = sortContentData.reverseObjectEnumerator;
|
||||
sortContentData = [NSArray arrayWithArray:contentReverseEnumerator.allObjects];
|
||||
}
|
||||
|
||||
self.contentsArray = sortContentData;
|
||||
[self.multiColumView reloadData];
|
||||
[self.multiColumnView reloadData];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
@@ -170,10 +170,10 @@
|
||||
[coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
|
||||
if (newCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
|
||||
|
||||
self->_multiColumView.frame = CGRectMake(0, 32, self.view.frame.size.width, self.view.frame.size.height - 32);
|
||||
self->_multiColumnView.frame = CGRectMake(0, 32, self.view.frame.size.width, self.view.frame.size.height - 32);
|
||||
}
|
||||
else {
|
||||
self->_multiColumView.frame = CGRectMake(0, 64, self.view.frame.size.width, self.view.frame.size.height - 64);
|
||||
self->_multiColumnView.frame = CGRectMake(0, 64, self.view.frame.size.width, self.view.frame.size.height - 64);
|
||||
}
|
||||
[self.view setNeedsLayout];
|
||||
} completion:nil];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// FLEXTableLeftCell.h
|
||||
// UICatalog
|
||||
// FLEX
|
||||
//
|
||||
// Created by Peng Tao on 15/11/24.
|
||||
// Copyright © 2015年 f. All rights reserved.
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
@interface FLEXTableLeftCell : UITableViewCell
|
||||
|
||||
@property (nonatomic, strong) UILabel *titlelabel;
|
||||
@property (nonatomic) UILabel *titlelabel;
|
||||
|
||||
+ (instancetype)cellWithTableView:(UITableView *)tableView;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// FLEXTableLeftCell.m
|
||||
// UICatalog
|
||||
// FLEX
|
||||
//
|
||||
// Created by Peng Tao on 15/11/24.
|
||||
// Copyright © 2015年 f. All rights reserved.
|
||||
@@ -20,7 +20,7 @@
|
||||
UILabel *textLabel = [[UILabel alloc] initWithFrame:CGRectZero];
|
||||
textLabel.textAlignment = NSTextAlignmentCenter;
|
||||
textLabel.font = [UIFont systemFontOfSize:13.0];
|
||||
textLabel.backgroundColor = [UIColor clearColor];
|
||||
textLabel.backgroundColor = UIColor.clearColor;
|
||||
[cell.contentView addSubview:textLabel];
|
||||
cell.titlelabel = textLabel;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
// Copyright © 2015年 Peng Tao. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXTableViewController.h"
|
||||
|
||||
@interface FLEXTableListViewController : UITableViewController
|
||||
@interface FLEXTableListViewController : FLEXTableViewController
|
||||
|
||||
+ (BOOL)supportsExtension:(NSString *)extension;
|
||||
- (instancetype)initWithPath:(NSString *)path;
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
NSString *_databasePath;
|
||||
}
|
||||
|
||||
@property (nonatomic, strong) NSArray<NSString *> *tables;
|
||||
@property (nonatomic) NSArray<NSString *> *tables;
|
||||
@property (nonatomic) NSArray<NSString *> *filteredTables;
|
||||
|
||||
+ (NSArray<NSString *> *)supportedSQLiteExtensions;
|
||||
+ (NSArray<NSString *> *)supportedRealmExtensions;
|
||||
@@ -67,11 +68,35 @@
|
||||
[array addObject:columnName];
|
||||
}
|
||||
self.tables = array;
|
||||
self.filteredTables = array;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.showsSearchBar = YES;
|
||||
}
|
||||
|
||||
#pragma mark - Search bar
|
||||
|
||||
- (void)updateSearchResults:(NSString *)searchText
|
||||
{
|
||||
if (searchText.length > 0) {
|
||||
NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", searchText];
|
||||
self.filteredTables = [self.tables filteredArrayUsingPredicate:searchPredicate];
|
||||
} else {
|
||||
self.filteredTables = self.tables;
|
||||
}
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Data Source
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
||||
{
|
||||
return self.tables.count;
|
||||
return self.filteredTables.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
@@ -81,28 +106,29 @@
|
||||
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
|
||||
reuseIdentifier:@"FLEXTableListViewControllerCell"];
|
||||
}
|
||||
cell.textLabel.text = self.tables[indexPath.row];
|
||||
cell.textLabel.text = self.filteredTables[indexPath.row];
|
||||
return cell;
|
||||
}
|
||||
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
FLEXTableContentViewController *contentViewController = [[FLEXTableContentViewController alloc] init];
|
||||
FLEXTableContentViewController *contentViewController = [FLEXTableContentViewController new];
|
||||
|
||||
contentViewController.contentsArray = [_dbm queryAllDataWithTableName:self.tables[indexPath.row]];
|
||||
contentViewController.columnsArray = [_dbm queryAllColumnsWithTableName:self.tables[indexPath.row]];
|
||||
contentViewController.contentsArray = [_dbm queryAllDataWithTableName:self.filteredTables[indexPath.row]];
|
||||
contentViewController.columnsArray = [_dbm queryAllColumnsWithTableName:self.filteredTables[indexPath.row]];
|
||||
|
||||
contentViewController.title = self.tables[indexPath.row];
|
||||
contentViewController.title = self.filteredTables[indexPath.row];
|
||||
[self.navigationController pushViewController:contentViewController animated:YES];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
|
||||
{
|
||||
return [NSString stringWithFormat:@"%lu tables", (unsigned long)self.tables.count];
|
||||
return [NSString stringWithFormat:@"Tables (%lu)", (unsigned long)self.filteredTables.count];
|
||||
}
|
||||
|
||||
#pragma mark - FLEXTableListViewController
|
||||
|
||||
+ (BOOL)supportsExtension:(NSString *)extension
|
||||
{
|
||||
extension = extension.lowercaseString;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXAddressExplorerCoordinator.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 7/10/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
|
||||
@interface FLEXAddressExplorerCoordinator : NSObject <FLEXGlobalsEntry>
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,95 @@
|
||||
//
|
||||
// FLEXAddressExplorerCoordinator.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 7/10/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXAddressExplorerCoordinator.h"
|
||||
#import "FLEXGlobalsTableViewController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXGlobalsTableViewController (FLEXAddressExploration)
|
||||
- (void)deselectSelectedRow;
|
||||
- (void)tryExploreAddress:(NSString *)addressString safely:(BOOL)safely;
|
||||
@end
|
||||
|
||||
@implementation FLEXAddressExplorerCoordinator
|
||||
|
||||
#pragma mark - FLEXGlobalsEntry
|
||||
|
||||
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
|
||||
return @"🔎 Address Explorer";
|
||||
}
|
||||
|
||||
+ (FLEXGlobalsTableViewControllerRowAction)globalsEntryRowAction:(FLEXGlobalsRow)row {
|
||||
return ^(FLEXGlobalsTableViewController *host) {
|
||||
|
||||
NSString *title = @"Explore Object at Address";
|
||||
NSString *message = @"Paste a hexadecimal address below, starting with '0x'. "
|
||||
"Use the unsafe option if you need to bypass pointer validation, "
|
||||
"but know that it may crash the app if the address is invalid.";
|
||||
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(title).message(message);
|
||||
make.configuredTextField(^(UITextField *textField) {
|
||||
NSString *copied = UIPasteboard.generalPasteboard.string;
|
||||
textField.placeholder = @"0x00000070deadbeef";
|
||||
// Go ahead and paste our clipboard if we have an address copied
|
||||
if ([copied hasPrefix:@"0x"]) {
|
||||
textField.text = copied;
|
||||
[textField selectAll:nil];
|
||||
}
|
||||
});
|
||||
make.button(@"Explore").handler(^(NSArray<NSString *> *strings) {
|
||||
[host tryExploreAddress:strings.firstObject safely:YES];
|
||||
});
|
||||
make.button(@"Unsafe Explore").destructiveStyle().handler(^(NSArray *strings) {
|
||||
[host tryExploreAddress:strings.firstObject safely:NO];
|
||||
});
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:host];
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXGlobalsTableViewController (FLEXAddressExploration)
|
||||
|
||||
- (void)deselectSelectedRow {
|
||||
NSIndexPath *selected = self.tableView.indexPathForSelectedRow;
|
||||
[self.tableView deselectRowAtIndexPath:selected animated:YES];
|
||||
}
|
||||
|
||||
- (void)tryExploreAddress:(NSString *)addressString safely:(BOOL)safely {
|
||||
NSScanner *scanner = [NSScanner scannerWithString:addressString];
|
||||
unsigned long long hexValue = 0;
|
||||
BOOL didParseAddress = [scanner scanHexLongLong:&hexValue];
|
||||
const void *pointerValue = (void *)hexValue;
|
||||
|
||||
NSString *error = nil;
|
||||
|
||||
if (didParseAddress) {
|
||||
if (safely && ![FLEXRuntimeUtility pointerIsValidObjcObject:pointerValue]) {
|
||||
error = @"The given address is unlikely to be a valid object.";
|
||||
}
|
||||
} else {
|
||||
error = @"Malformed address. Make sure it's not too long and starts with '0x'.";
|
||||
}
|
||||
|
||||
if (!error) {
|
||||
id object = (__bridge id)pointerValue;
|
||||
FLEXObjectExplorerViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:object];
|
||||
[self.navigationController pushViewController:explorer animated:YES];
|
||||
} else {
|
||||
[FLEXAlert showAlert:@"Uh-oh" message:error from:self];
|
||||
[self deselectSelectedRow];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -6,10 +6,11 @@
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXTableViewController.h"
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
|
||||
@interface FLEXClassesTableViewController : UITableViewController
|
||||
@interface FLEXClassesTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
|
||||
|
||||
@property (nonatomic, copy) NSString *binaryImageName;
|
||||
+ (instancetype)binaryImageName:(NSString *)binaryImageName;
|
||||
|
||||
@end
|
||||
|
||||
@@ -12,46 +12,64 @@
|
||||
#import "FLEXUtility.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@interface FLEXClassesTableViewController () <UISearchBarDelegate>
|
||||
@interface FLEXClassesTableViewController ()
|
||||
|
||||
@property (nonatomic, strong) NSArray<NSString *> *classNames;
|
||||
@property (nonatomic, strong) NSArray<NSString *> *filteredClassNames;
|
||||
@property (nonatomic, strong) UISearchBar *searchBar;
|
||||
@property (nonatomic, copy) NSArray<NSString *> *classNames;
|
||||
@property (nonatomic, copy) NSArray<NSString *> *filteredClassNames;
|
||||
@property (nonatomic, copy) NSString *binaryImageName;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXClassesTableViewController
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (instancetype)binaryImageName:(NSString *)binaryImageName
|
||||
{
|
||||
return [[self alloc] initWithBinaryImageName:binaryImageName];
|
||||
}
|
||||
|
||||
- (id)initWithBinaryImageName:(NSString *)binaryImageName
|
||||
{
|
||||
NSParameterAssert(binaryImageName);
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.binaryImageName = binaryImageName;
|
||||
[self loadClassNames];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Internal
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.searchBar = [[UISearchBar alloc] init];
|
||||
self.searchBar.placeholder = [FLEXUtility searchBarPlaceholderText];
|
||||
self.searchBar.delegate = self;
|
||||
[self.searchBar sizeToFit];
|
||||
self.tableView.tableHeaderView = self.searchBar;
|
||||
self.showsSearchBar = YES;
|
||||
[self updateTitle];
|
||||
}
|
||||
|
||||
- (void)setBinaryImageName:(NSString *)binaryImageName
|
||||
- (void)updateTitle
|
||||
{
|
||||
if (![_binaryImageName isEqual:binaryImageName]) {
|
||||
_binaryImageName = binaryImageName;
|
||||
[self loadClassNames];
|
||||
[self updateTitle];
|
||||
}
|
||||
NSString *shortImageName = self.binaryImageName.lastPathComponent;
|
||||
self.title = [NSString stringWithFormat:@"%@ Classes (%lu)",
|
||||
shortImageName, (unsigned long)self.filteredClassNames.count
|
||||
];
|
||||
}
|
||||
|
||||
- (void)setClassNames:(NSArray<NSString *> *)classNames
|
||||
{
|
||||
_classNames = classNames;
|
||||
self.filteredClassNames = classNames;
|
||||
_classNames = self.filteredClassNames = classNames.copy;
|
||||
}
|
||||
|
||||
- (void)loadClassNames
|
||||
{
|
||||
unsigned int classNamesCount = 0;
|
||||
const char **classNames = objc_copyClassNamesForImage([self.binaryImageName UTF8String], &classNamesCount);
|
||||
const char **classNames = objc_copyClassNamesForImage(self.binaryImageName.UTF8String, &classNamesCount);
|
||||
if (classNames) {
|
||||
NSMutableArray<NSString *> *classNameStrings = [NSMutableArray array];
|
||||
for (unsigned int i = 0; i < classNamesCount; i++) {
|
||||
@@ -66,20 +84,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateTitle
|
||||
{
|
||||
NSString *shortImageName = self.binaryImageName.lastPathComponent;
|
||||
self.title = [NSString stringWithFormat:@"%@ Classes (%lu)", shortImageName, (unsigned long)[self.filteredClassNames count]];
|
||||
|
||||
#pragma mark - FLEXGlobalsEntry
|
||||
|
||||
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
|
||||
return [NSString stringWithFormat:@"📕 %@ Classes", [FLEXUtility applicationName]];
|
||||
}
|
||||
|
||||
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
|
||||
return [self binaryImageName:[FLEXUtility applicationImageName]];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Search
|
||||
#pragma mark - Search bar
|
||||
|
||||
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
|
||||
- (void)updateSearchResults:(NSString *)searchText
|
||||
{
|
||||
if ([searchText length] > 0) {
|
||||
NSPredicate *searchPreidcate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", searchText];
|
||||
self.filteredClassNames = [self.classNames filteredArrayUsingPredicate:searchPreidcate];
|
||||
if (searchText.length > 0) {
|
||||
NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", searchText];
|
||||
self.filteredClassNames = [self.classNames filteredArrayUsingPredicate:searchPredicate].reverseObjectEnumerator.allObjects;
|
||||
} else {
|
||||
self.filteredClassNames = self.classNames;
|
||||
}
|
||||
@@ -87,17 +110,6 @@
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
|
||||
{
|
||||
[searchBar resignFirstResponder];
|
||||
}
|
||||
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
||||
{
|
||||
// Dismiss the keyboard when interacting with filtered results.
|
||||
[self.searchBar endEditing:YES];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Data Source
|
||||
|
||||
@@ -108,7 +120,7 @@
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
||||
{
|
||||
return [self.filteredClassNames count];
|
||||
return self.filteredClassNames.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
@@ -132,7 +144,7 @@
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
NSString *className = self.filteredClassNames[indexPath.row];
|
||||
Class selectedClass = objc_getClass([className UTF8String]);
|
||||
Class selectedClass = objc_getClass(className.UTF8String);
|
||||
FLEXObjectExplorerViewController *objectExplorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:selectedClass];
|
||||
[self.navigationController pushViewController:objectExplorer animated:YES];
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
// Copyright © 2015 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
#import "FLEXTableViewController.h"
|
||||
|
||||
@interface FLEXCookiesTableViewController : UITableViewController
|
||||
@interface FLEXCookiesTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
|
||||
|
||||
@end
|
||||
|
||||
@@ -11,33 +11,37 @@
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXCookiesTableViewController ()
|
||||
|
||||
@property (nonatomic, strong) NSArray<NSHTTPCookie *> *cookies;
|
||||
|
||||
@property (nonatomic, readonly) NSArray<NSHTTPCookie *> *cookies;
|
||||
@property (nonatomic) NSString *headerTitle;
|
||||
@end
|
||||
|
||||
@implementation FLEXCookiesTableViewController
|
||||
|
||||
- (id)initWithStyle:(UITableViewStyle)style {
|
||||
self = [super initWithStyle:style];
|
||||
|
||||
if (self) {
|
||||
self.title = @"Cookies";
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
NSSortDescriptor *nameSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES selector:@selector(caseInsensitiveCompare:)];
|
||||
_cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies sortedArrayUsingDescriptors:@[nameSortDescriptor]];
|
||||
}
|
||||
|
||||
return self;
|
||||
NSSortDescriptor *nameSortDescriptor = [[NSSortDescriptor alloc]
|
||||
initWithKey:@"name" ascending:YES selector:@selector(caseInsensitiveCompare:)
|
||||
];
|
||||
_cookies = [NSHTTPCookieStorage.sharedHTTPCookieStorage.cookies
|
||||
sortedArrayUsingDescriptors:@[nameSortDescriptor]
|
||||
];
|
||||
|
||||
self.title = @"Cookies";
|
||||
[self updateHeaderTitle];
|
||||
}
|
||||
|
||||
- (void)updateHeaderTitle {
|
||||
self.headerTitle = [NSString stringWithFormat:@"%@ cookies", @(self.cookies.count)];
|
||||
// TODO update header title here when we can search cookies
|
||||
}
|
||||
|
||||
- (NSHTTPCookie *)cookieForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return self.cookies[indexPath.row];
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
||||
return 1;
|
||||
}
|
||||
|
||||
#pragma mark - Table View Data Source
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.cookies.count;
|
||||
@@ -50,7 +54,7 @@
|
||||
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
|
||||
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
|
||||
cell.detailTextLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
|
||||
cell.detailTextLabel.textColor = [UIColor grayColor];
|
||||
cell.detailTextLabel.textColor = UIColor.grayColor;
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
}
|
||||
|
||||
@@ -61,6 +65,13 @@
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
return self.headerTitle;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Delegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSHTTPCookie *cookie = [self cookieForRowAtIndexPath:indexPath];
|
||||
UIViewController *cookieViewController = (UIViewController *)[FLEXObjectExplorerFactory explorerViewControllerForObject:cookie];
|
||||
@@ -68,4 +79,15 @@
|
||||
[self.navigationController pushViewController:cookieViewController animated:YES];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - FLEXGlobalsEntry
|
||||
|
||||
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
|
||||
return @"🍪 Cookies";
|
||||
}
|
||||
|
||||
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
|
||||
return [self new];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
//
|
||||
// FLEXFileBrowserFileOperationController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Daniel Rodriguez Troitino on 2/13/15.
|
||||
// Copyright (c) 2015 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@protocol FLEXFileBrowserFileOperationController;
|
||||
|
||||
@protocol FLEXFileBrowserFileOperationControllerDelegate <NSObject>
|
||||
|
||||
- (void)fileOperationControllerDidDismiss:(id<FLEXFileBrowserFileOperationController>)controller;
|
||||
|
||||
@end
|
||||
|
||||
@protocol FLEXFileBrowserFileOperationController <NSObject>
|
||||
|
||||
@property (nonatomic, weak) id<FLEXFileBrowserFileOperationControllerDelegate> delegate;
|
||||
|
||||
- (instancetype)initWithPath:(NSString *)path;
|
||||
|
||||
- (void)show;
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXFileBrowserFileDeleteOperationController : NSObject <FLEXFileBrowserFileOperationController>
|
||||
@end
|
||||
|
||||
@interface FLEXFileBrowserFileRenameOperationController : NSObject <FLEXFileBrowserFileOperationController>
|
||||
@end
|
||||
@@ -1,142 +0,0 @@
|
||||
//
|
||||
// FLEXFileBrowserFileOperationController.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Daniel Rodriguez Troitino on 2/13/15.
|
||||
// Copyright (c) 2015 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFileBrowserFileOperationController.h"
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface FLEXFileBrowserFileDeleteOperationController () <UIAlertViewDelegate>
|
||||
|
||||
@property (nonatomic, copy, readonly) NSString *path;
|
||||
|
||||
- (instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXFileBrowserFileDeleteOperationController
|
||||
|
||||
@synthesize delegate = _delegate;
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
return [self initWithPath:nil];
|
||||
}
|
||||
|
||||
- (instancetype)initWithPath:(NSString *)path
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_path = path;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)show
|
||||
{
|
||||
BOOL isDirectory = NO;
|
||||
BOOL stillExists = [[NSFileManager defaultManager] fileExistsAtPath:self.path isDirectory:&isDirectory];
|
||||
|
||||
if (stillExists) {
|
||||
UIAlertView *deleteWarning = [[UIAlertView alloc]
|
||||
initWithTitle:[NSString stringWithFormat:@"Delete %@?", self.path.lastPathComponent]
|
||||
message:[NSString stringWithFormat:@"The %@ will be deleted. This operation cannot be undone", isDirectory ? @"directory" : @"file"]
|
||||
delegate:self
|
||||
cancelButtonTitle:@"Cancel"
|
||||
otherButtonTitles:@"Delete", nil];
|
||||
[deleteWarning show];
|
||||
} else {
|
||||
[[[UIAlertView alloc] initWithTitle:@"File Removed" message:@"The file at the specified path no longer exists." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIAlertViewDelegate
|
||||
|
||||
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
|
||||
{
|
||||
if (buttonIndex == alertView.cancelButtonIndex) {
|
||||
// Nothing, just cancel
|
||||
} else if (buttonIndex == alertView.firstOtherButtonIndex) {
|
||||
[[NSFileManager defaultManager] removeItemAtPath:self.path error:NULL];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
|
||||
{
|
||||
[self.delegate fileOperationControllerDidDismiss:self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXFileBrowserFileRenameOperationController () <UIAlertViewDelegate>
|
||||
|
||||
@property (nonatomic, copy, readonly) NSString *path;
|
||||
|
||||
- (instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXFileBrowserFileRenameOperationController
|
||||
|
||||
@synthesize delegate = _delegate;
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
return [self initWithPath:nil];
|
||||
}
|
||||
|
||||
- (instancetype)initWithPath:(NSString *)path
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_path = path;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)show
|
||||
{
|
||||
BOOL isDirectory = NO;
|
||||
BOOL stillExists = [[NSFileManager defaultManager] fileExistsAtPath:self.path isDirectory:&isDirectory];
|
||||
|
||||
if (stillExists) {
|
||||
UIAlertView *renameDialog = [[UIAlertView alloc]
|
||||
initWithTitle:[NSString stringWithFormat:@"Rename %@?", self.path.lastPathComponent]
|
||||
message:nil
|
||||
delegate:self
|
||||
cancelButtonTitle:@"Cancel"
|
||||
otherButtonTitles:@"Rename", nil];
|
||||
renameDialog.alertViewStyle = UIAlertViewStylePlainTextInput;
|
||||
UITextField *textField = [renameDialog textFieldAtIndex:0];
|
||||
textField.placeholder = @"New file name";
|
||||
textField.text = self.path.lastPathComponent;
|
||||
[renameDialog show];
|
||||
} else {
|
||||
[[[UIAlertView alloc] initWithTitle:@"File Removed" message:@"The file at the specified path no longer exists." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIAlertViewDelegate
|
||||
|
||||
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
|
||||
{
|
||||
if (buttonIndex == alertView.cancelButtonIndex) {
|
||||
// Nothing, just cancel
|
||||
} else if (buttonIndex == alertView.firstOtherButtonIndex) {
|
||||
NSString *newFileName = [alertView textFieldAtIndex:0].text;
|
||||
NSString *newPath = [[self.path stringByDeletingLastPathComponent] stringByAppendingPathComponent:newFileName];
|
||||
[[NSFileManager defaultManager] moveItemAtPath:self.path toPath:newPath error:NULL];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
|
||||
{
|
||||
[self.delegate fileOperationControllerDidDismiss:self];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,376 +0,0 @@
|
||||
//
|
||||
// FLEXFileBrowserTableViewController.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/9/14.
|
||||
//
|
||||
//
|
||||
|
||||
#import "FLEXFileBrowserTableViewController.h"
|
||||
#import "FLEXFileBrowserFileOperationController.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXWebViewController.h"
|
||||
#import "FLEXImagePreviewViewController.h"
|
||||
#import "FLEXTableListViewController.h"
|
||||
|
||||
@interface FLEXFileBrowserTableViewCell : UITableViewCell
|
||||
@end
|
||||
|
||||
@interface FLEXFileBrowserTableViewController () <FLEXFileBrowserFileOperationControllerDelegate, FLEXFileBrowserSearchOperationDelegate, UISearchResultsUpdating, UISearchControllerDelegate>
|
||||
|
||||
@property (nonatomic, copy) NSString *path;
|
||||
@property (nonatomic, copy) NSArray<NSString *> *childPaths;
|
||||
@property (nonatomic, strong) NSArray<NSString *> *searchPaths;
|
||||
@property (nonatomic, strong) NSNumber *recursiveSize;
|
||||
@property (nonatomic, strong) NSNumber *searchPathsSize;
|
||||
@property (nonatomic, strong) UISearchController *searchController;
|
||||
@property (nonatomic) NSOperationQueue *operationQueue;
|
||||
@property (nonatomic, strong) UIDocumentInteractionController *documentController;
|
||||
@property (nonatomic, strong) id<FLEXFileBrowserFileOperationController> fileOperationController;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXFileBrowserTableViewController
|
||||
|
||||
- (id)initWithStyle:(UITableViewStyle)style
|
||||
{
|
||||
return [self initWithPath:NSHomeDirectory()];
|
||||
}
|
||||
|
||||
- (id)initWithPath:(NSString *)path
|
||||
{
|
||||
self = [super initWithStyle:UITableViewStyleGrouped];
|
||||
if (self) {
|
||||
self.path = path;
|
||||
self.title = [path lastPathComponent];
|
||||
self.operationQueue = [NSOperationQueue new];
|
||||
|
||||
self.searchController = [[UISearchController alloc] initWithSearchResultsController:nil];
|
||||
self.searchController.searchResultsUpdater = self;
|
||||
self.searchController.delegate = self;
|
||||
self.searchController.dimsBackgroundDuringPresentation = NO;
|
||||
self.tableView.tableHeaderView = self.searchController.searchBar;
|
||||
|
||||
//computing path size
|
||||
FLEXFileBrowserTableViewController *__weak weakSelf = self;
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
NSDictionary<NSString *, id> *attributes = [fileManager attributesOfItemAtPath:path error:NULL];
|
||||
uint64_t totalSize = [attributes fileSize];
|
||||
|
||||
for (NSString *fileName in [fileManager enumeratorAtPath:path]) {
|
||||
attributes = [fileManager attributesOfItemAtPath:[path stringByAppendingPathComponent:fileName] error:NULL];
|
||||
totalSize += [attributes fileSize];
|
||||
|
||||
// Bail if the interested view controller has gone away.
|
||||
if (!weakSelf) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
FLEXFileBrowserTableViewController *__strong strongSelf = weakSelf;
|
||||
strongSelf.recursiveSize = @(totalSize);
|
||||
[strongSelf.tableView reloadData];
|
||||
});
|
||||
});
|
||||
|
||||
[self reloadChildPaths];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - UIViewController
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
}
|
||||
|
||||
#pragma mark - FLEXFileBrowserSearchOperationDelegate
|
||||
|
||||
- (void)fileBrowserSearchOperationResult:(NSArray<NSString *> *)searchResult size:(uint64_t)size
|
||||
{
|
||||
self.searchPaths = searchResult;
|
||||
self.searchPathsSize = @(size);
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
#pragma mark - UISearchResultsUpdating
|
||||
|
||||
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
|
||||
{
|
||||
[self reloadDisplayedPaths];
|
||||
}
|
||||
|
||||
#pragma mark - UISearchControllerDelegate
|
||||
|
||||
- (void)willDismissSearchController:(UISearchController *)searchController
|
||||
{
|
||||
[self.operationQueue cancelAllOperations];
|
||||
[self reloadChildPaths];
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table view data source
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
||||
{
|
||||
return self.searchController.isActive ? [self.searchPaths count] : [self.childPaths count];
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
|
||||
{
|
||||
BOOL isSearchActive = self.searchController.isActive;
|
||||
NSNumber *currentSize = isSearchActive ? self.searchPathsSize : self.recursiveSize;
|
||||
NSArray<NSString *> *currentPaths = isSearchActive ? self.searchPaths : self.childPaths;
|
||||
|
||||
NSString *sizeString = nil;
|
||||
if (!currentSize) {
|
||||
sizeString = @"Computing size…";
|
||||
} else {
|
||||
sizeString = [NSByteCountFormatter stringFromByteCount:[currentSize longLongValue] countStyle:NSByteCountFormatterCountStyleFile];
|
||||
}
|
||||
|
||||
return [NSString stringWithFormat:@"%lu files (%@)", (unsigned long)[currentPaths count], sizeString];
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
NSString *fullPath = [self filePathAtIndexPath:indexPath];
|
||||
NSDictionary<NSString *, id> *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:fullPath error:NULL];
|
||||
BOOL isDirectory = [[attributes fileType] isEqual:NSFileTypeDirectory];
|
||||
NSString *subtitle = nil;
|
||||
if (isDirectory) {
|
||||
NSUInteger count = [[[NSFileManager defaultManager] contentsOfDirectoryAtPath:fullPath error:NULL] count];
|
||||
subtitle = [NSString stringWithFormat:@"%lu file%@", (unsigned long)count, (count == 1 ? @"" : @"s")];
|
||||
} else {
|
||||
NSString *sizeString = [NSByteCountFormatter stringFromByteCount:[attributes fileSize] countStyle:NSByteCountFormatterCountStyleFile];
|
||||
subtitle = [NSString stringWithFormat:@"%@ - %@", sizeString, [attributes fileModificationDate]];
|
||||
}
|
||||
|
||||
static NSString *textCellIdentifier = @"textCell";
|
||||
static NSString *imageCellIdentifier = @"imageCell";
|
||||
UITableViewCell *cell = nil;
|
||||
|
||||
// Separate image and text only cells because otherwise the separator lines get out-of-whack on image cells reused with text only.
|
||||
BOOL showImagePreview = [FLEXUtility isImagePathExtension:[fullPath pathExtension]];
|
||||
NSString *cellIdentifier = showImagePreview ? imageCellIdentifier : textCellIdentifier;
|
||||
|
||||
if (!cell) {
|
||||
cell = [[FLEXFileBrowserTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
|
||||
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
|
||||
cell.detailTextLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
|
||||
cell.detailTextLabel.textColor = [UIColor grayColor];
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
}
|
||||
NSString *cellTitle = [fullPath lastPathComponent];
|
||||
cell.textLabel.text = cellTitle;
|
||||
cell.detailTextLabel.text = subtitle;
|
||||
|
||||
if (showImagePreview) {
|
||||
cell.imageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
cell.imageView.image = [UIImage imageWithContentsOfFile:fullPath];
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
NSString *fullPath = [self filePathAtIndexPath:indexPath];
|
||||
NSString *subpath = [fullPath lastPathComponent];
|
||||
NSString *pathExtension = [subpath pathExtension];
|
||||
|
||||
BOOL isDirectory = NO;
|
||||
BOOL stillExists = [[NSFileManager defaultManager] fileExistsAtPath:fullPath isDirectory:&isDirectory];
|
||||
if (stillExists) {
|
||||
UIViewController *drillInViewController = nil;
|
||||
if (isDirectory) {
|
||||
drillInViewController = [[[self class] alloc] initWithPath:fullPath];
|
||||
} else if ([FLEXUtility isImagePathExtension:pathExtension]) {
|
||||
UIImage *image = [UIImage imageWithContentsOfFile:fullPath];
|
||||
drillInViewController = [[FLEXImagePreviewViewController alloc] initWithImage:image];
|
||||
} else {
|
||||
// Special case keyed archives, json, and plists to get more readable data.
|
||||
NSString *prettyString = nil;
|
||||
if ([pathExtension isEqual:@"archive"] || [pathExtension isEqual:@"coded"]) {
|
||||
prettyString = [[NSKeyedUnarchiver unarchiveObjectWithFile:fullPath] description];
|
||||
} else if ([pathExtension isEqualToString:@"json"]) {
|
||||
prettyString = [FLEXUtility prettyJSONStringFromData:[NSData dataWithContentsOfFile:fullPath]];
|
||||
} else if ([pathExtension isEqualToString:@"plist"]) {
|
||||
NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
|
||||
prettyString = [[NSPropertyListSerialization propertyListWithData:fileData options:0 format:NULL error:NULL] description];
|
||||
}
|
||||
|
||||
if ([prettyString length] > 0) {
|
||||
drillInViewController = [[FLEXWebViewController alloc] initWithText:prettyString];
|
||||
} else if ([FLEXWebViewController supportsPathExtension:pathExtension]) {
|
||||
drillInViewController = [[FLEXWebViewController alloc] initWithURL:[NSURL fileURLWithPath:fullPath]];
|
||||
} else if ([FLEXTableListViewController supportsExtension:subpath.pathExtension]) {
|
||||
drillInViewController = [[FLEXTableListViewController alloc] initWithPath:fullPath];
|
||||
}
|
||||
else {
|
||||
NSString *fileString = [NSString stringWithContentsOfFile:fullPath encoding:NSUTF8StringEncoding error:NULL];
|
||||
if ([fileString length] > 0) {
|
||||
drillInViewController = [[FLEXWebViewController alloc] initWithText:fileString];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (drillInViewController) {
|
||||
drillInViewController.title = [subpath lastPathComponent];
|
||||
[self.navigationController pushViewController:drillInViewController animated:YES];
|
||||
} else {
|
||||
[self openFileController:fullPath];
|
||||
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
}
|
||||
} else {
|
||||
[[[UIAlertView alloc] initWithTitle:@"File Removed" message:@"The file at the specified path no longer exists." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
|
||||
[self reloadDisplayedPaths];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
UIMenuItem *renameMenuItem = [[UIMenuItem alloc] initWithTitle:@"Rename" action:@selector(fileBrowserRename:)];
|
||||
UIMenuItem *deleteMenuItem = [[UIMenuItem alloc] initWithTitle:@"Delete" action:@selector(fileBrowserDelete:)];
|
||||
NSMutableArray *menus = [NSMutableArray arrayWithObjects:renameMenuItem, deleteMenuItem, nil];
|
||||
|
||||
NSString *fullPath = [self filePathAtIndexPath:indexPath];
|
||||
NSError *error = nil;
|
||||
NSDictionary *attributes = [NSFileManager.defaultManager attributesOfItemAtPath:fullPath error:&error];
|
||||
if (error == nil && [attributes fileType] != NSFileTypeDirectory) {
|
||||
UIMenuItem *shareMenuItem = [[UIMenuItem alloc] initWithTitle:@"Share" action:@selector(fileBrowserShare:)];
|
||||
[menus addObject:shareMenuItem];
|
||||
}
|
||||
[UIMenuController sharedMenuController].menuItems = menus;
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
|
||||
{
|
||||
return action == @selector(fileBrowserDelete:) || action == @selector(fileBrowserRename:) || action == @selector(fileBrowserShare:);
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
|
||||
{
|
||||
// Empty, but has to exist for the menu to show
|
||||
// The table view only calls this method for actions in the UIResponderStandardEditActions informal protocol.
|
||||
// Since our actions are outside of that protocol, we need to manually handle the action forwarding from the cells.
|
||||
}
|
||||
|
||||
#pragma mark - FLEXFileBrowserFileOperationControllerDelegate
|
||||
|
||||
- (void)fileOperationControllerDidDismiss:(id<FLEXFileBrowserFileOperationController>)controller
|
||||
{
|
||||
[self reloadDisplayedPaths];
|
||||
}
|
||||
|
||||
- (void)openFileController:(NSString *)fullPath
|
||||
{
|
||||
UIDocumentInteractionController *controller = [UIDocumentInteractionController new];
|
||||
controller.URL = [[NSURL alloc] initFileURLWithPath:fullPath];
|
||||
|
||||
[controller presentOptionsMenuFromRect:self.view.bounds inView:self.view animated:YES];
|
||||
self.documentController = controller;
|
||||
}
|
||||
|
||||
- (void)fileBrowserRename:(UITableViewCell *)sender
|
||||
{
|
||||
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
|
||||
NSString *fullPath = [self filePathAtIndexPath:indexPath];
|
||||
|
||||
self.fileOperationController = [[FLEXFileBrowserFileRenameOperationController alloc] initWithPath:fullPath];
|
||||
self.fileOperationController.delegate = self;
|
||||
[self.fileOperationController show];
|
||||
}
|
||||
|
||||
- (void)fileBrowserDelete:(UITableViewCell *)sender
|
||||
{
|
||||
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
|
||||
NSString *fullPath = [self filePathAtIndexPath:indexPath];
|
||||
|
||||
self.fileOperationController = [[FLEXFileBrowserFileDeleteOperationController alloc] initWithPath:fullPath];
|
||||
self.fileOperationController.delegate = self;
|
||||
[self.fileOperationController show];
|
||||
}
|
||||
|
||||
- (void)fileBrowserShare:(UITableViewCell *)sender
|
||||
{
|
||||
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
|
||||
NSString *fullPath = [self filePathAtIndexPath:indexPath];
|
||||
|
||||
UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:@[fullPath] applicationActivities:nil];
|
||||
[self presentViewController:activityViewController animated:true completion:nil];
|
||||
}
|
||||
|
||||
- (void)reloadDisplayedPaths
|
||||
{
|
||||
if (self.searchController.isActive) {
|
||||
[self reloadSearchPaths];
|
||||
} else {
|
||||
[self reloadChildPaths];
|
||||
}
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
- (void)reloadChildPaths
|
||||
{
|
||||
NSMutableArray<NSString *> *childPaths = [NSMutableArray array];
|
||||
NSArray<NSString *> *subpaths = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.path error:NULL];
|
||||
for (NSString *subpath in subpaths) {
|
||||
[childPaths addObject:[self.path stringByAppendingPathComponent:subpath]];
|
||||
}
|
||||
self.childPaths = childPaths;
|
||||
}
|
||||
|
||||
- (void)reloadSearchPaths
|
||||
{
|
||||
self.searchPaths = nil;
|
||||
self.searchPathsSize = nil;
|
||||
|
||||
//clear pre search request and start a new one
|
||||
[self.operationQueue cancelAllOperations];
|
||||
FLEXFileBrowserSearchOperation *newOperation = [[FLEXFileBrowserSearchOperation alloc] initWithPath:self.path searchString:self.searchController.searchBar.text];
|
||||
newOperation.delegate = self;
|
||||
[self.operationQueue addOperation:newOperation];
|
||||
}
|
||||
|
||||
- (NSString *)filePathAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
return self.searchController.isActive ? self.searchPaths[indexPath.row] : self.childPaths[indexPath.row];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FLEXFileBrowserTableViewCell
|
||||
|
||||
- (void)fileBrowserRename:(UIMenuController *)sender
|
||||
{
|
||||
id target = [self.nextResponder targetForAction:_cmd withSender:sender];
|
||||
[[UIApplication sharedApplication] sendAction:_cmd to:target from:self forEvent:nil];
|
||||
}
|
||||
|
||||
- (void)fileBrowserDelete:(UIMenuController *)sender
|
||||
{
|
||||
id target = [self.nextResponder targetForAction:_cmd withSender:sender];
|
||||
[[UIApplication sharedApplication] sendAction:_cmd to:target from:self forEvent:nil];
|
||||
}
|
||||
|
||||
- (void)fileBrowserShare:(UIMenuController *)sender
|
||||
{
|
||||
id target = [self.nextResponder targetForAction:_cmd withSender:sender];
|
||||
[[UIApplication sharedApplication] sendAction:_cmd to:target from:self forEvent:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,315 +0,0 @@
|
||||
//
|
||||
// FLEXGlobalsTableViewController.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 2014-05-03.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXGlobalsTableViewController.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXLibrariesTableViewController.h"
|
||||
#import "FLEXClassesTableViewController.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXLiveObjectsTableViewController.h"
|
||||
#import "FLEXFileBrowserTableViewController.h"
|
||||
#import "FLEXCookiesTableViewController.h"
|
||||
#import "FLEXGlobalsTableViewControllerEntry.h"
|
||||
#import "FLEXManager+Private.h"
|
||||
#import "FLEXSystemLogTableViewController.h"
|
||||
#import "FLEXNetworkHistoryTableViewController.h"
|
||||
|
||||
static __weak UIWindow *s_applicationWindow = nil;
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
|
||||
FLEXGlobalsRowNetworkHistory,
|
||||
FLEXGlobalsRowSystemLog,
|
||||
FLEXGlobalsRowLiveObjects,
|
||||
FLEXGlobalsRowFileBrowser,
|
||||
FLEXGlobalsCookies,
|
||||
FLEXGlobalsRowSystemLibraries,
|
||||
FLEXGlobalsRowAppClasses,
|
||||
FLEXGlobalsRowAppDelegate,
|
||||
FLEXGlobalsRowRootViewController,
|
||||
FLEXGlobalsRowUserDefaults,
|
||||
FLEXGlobalsRowMainBundle,
|
||||
FLEXGlobalsRowApplication,
|
||||
FLEXGlobalsRowKeyWindow,
|
||||
FLEXGlobalsRowMainScreen,
|
||||
FLEXGlobalsRowCurrentDevice,
|
||||
FLEXGlobalsRowCount
|
||||
};
|
||||
|
||||
@interface FLEXGlobalsTableViewController ()
|
||||
|
||||
@property (nonatomic, readonly, copy) NSArray<FLEXGlobalsTableViewControllerEntry *> *entries;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXGlobalsTableViewController
|
||||
|
||||
+ (NSArray<FLEXGlobalsTableViewControllerEntry *> *)defaultGlobalEntries
|
||||
{
|
||||
NSMutableArray<FLEXGlobalsTableViewControllerEntry *> *defaultGlobalEntries = [NSMutableArray array];
|
||||
|
||||
for (FLEXGlobalsRow defaultRowIndex = 0; defaultRowIndex < FLEXGlobalsRowCount; defaultRowIndex++) {
|
||||
FLEXGlobalsTableViewControllerEntryNameFuture titleFuture = nil;
|
||||
FLEXGlobalsTableViewControllerViewControllerFuture viewControllerFuture = nil;
|
||||
|
||||
switch (defaultRowIndex) {
|
||||
case FLEXGlobalsRowAppClasses:
|
||||
titleFuture = ^NSString *{
|
||||
return [NSString stringWithFormat:@"📕 %@ Classes", [FLEXUtility applicationName]];
|
||||
};
|
||||
viewControllerFuture = ^UIViewController *{
|
||||
FLEXClassesTableViewController *classesViewController = [[FLEXClassesTableViewController alloc] init];
|
||||
classesViewController.binaryImageName = [FLEXUtility applicationImageName];
|
||||
|
||||
return classesViewController;
|
||||
};
|
||||
break;
|
||||
|
||||
case FLEXGlobalsRowSystemLibraries: {
|
||||
NSString *titleString = @"📚 System Libraries";
|
||||
titleFuture = ^NSString *{
|
||||
return titleString;
|
||||
};
|
||||
viewControllerFuture = ^UIViewController *{
|
||||
FLEXLibrariesTableViewController *librariesViewController = [[FLEXLibrariesTableViewController alloc] init];
|
||||
librariesViewController.title = titleString;
|
||||
|
||||
return librariesViewController;
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
case FLEXGlobalsRowLiveObjects:
|
||||
titleFuture = ^NSString *{
|
||||
return @"💩 Heap Objects";
|
||||
};
|
||||
viewControllerFuture = ^UIViewController *{
|
||||
return [[FLEXLiveObjectsTableViewController alloc] init];
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case FLEXGlobalsRowAppDelegate:
|
||||
titleFuture = ^NSString *{
|
||||
return [NSString stringWithFormat:@"👉 %@", [[[UIApplication sharedApplication] delegate] class]];
|
||||
};
|
||||
viewControllerFuture = ^UIViewController *{
|
||||
id <UIApplicationDelegate> appDelegate = [[UIApplication sharedApplication] delegate];
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:appDelegate];
|
||||
};
|
||||
break;
|
||||
|
||||
case FLEXGlobalsRowRootViewController:
|
||||
titleFuture = ^NSString *{
|
||||
return [NSString stringWithFormat:@"🌴 %@", [[s_applicationWindow rootViewController] class]];
|
||||
};
|
||||
viewControllerFuture = ^UIViewController *{
|
||||
UIViewController *rootViewController = [s_applicationWindow rootViewController];
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:rootViewController];
|
||||
};
|
||||
break;
|
||||
|
||||
case FLEXGlobalsRowUserDefaults:
|
||||
titleFuture = ^NSString *{
|
||||
return @"🚶 +[NSUserDefaults standardUserDefaults]";
|
||||
};
|
||||
viewControllerFuture = ^UIViewController *{
|
||||
NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:standardUserDefaults];
|
||||
};
|
||||
break;
|
||||
|
||||
case FLEXGlobalsRowMainBundle:
|
||||
titleFuture = ^NSString *{
|
||||
return @"📦 +[NSBundle mainBundle]";
|
||||
};
|
||||
viewControllerFuture = ^UIViewController *{
|
||||
NSBundle *mainBundle = [NSBundle mainBundle];
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:mainBundle];
|
||||
};
|
||||
break;
|
||||
|
||||
case FLEXGlobalsRowApplication:
|
||||
titleFuture = ^NSString *{
|
||||
return @"💾 +[UIApplication sharedApplication]";
|
||||
};
|
||||
viewControllerFuture = ^UIViewController *{
|
||||
UIApplication *sharedApplication = [UIApplication sharedApplication];
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:sharedApplication];
|
||||
};
|
||||
break;
|
||||
|
||||
case FLEXGlobalsRowKeyWindow:
|
||||
titleFuture = ^NSString *{
|
||||
return @"🔑 -[UIApplication keyWindow]";
|
||||
};
|
||||
viewControllerFuture = ^UIViewController *{
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:s_applicationWindow];
|
||||
};
|
||||
break;
|
||||
|
||||
case FLEXGlobalsRowMainScreen:
|
||||
titleFuture = ^NSString *{
|
||||
return @"💻 +[UIScreen mainScreen]";
|
||||
};
|
||||
viewControllerFuture = ^UIViewController *{
|
||||
UIScreen *mainScreen = [UIScreen mainScreen];
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:mainScreen];
|
||||
};
|
||||
break;
|
||||
|
||||
case FLEXGlobalsRowCurrentDevice:
|
||||
titleFuture = ^NSString *{
|
||||
return @"📱 +[UIDevice currentDevice]";
|
||||
};
|
||||
viewControllerFuture = ^UIViewController *{
|
||||
UIDevice *currentDevice = [UIDevice currentDevice];
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:currentDevice];
|
||||
};
|
||||
break;
|
||||
|
||||
case FLEXGlobalsCookies:
|
||||
titleFuture = ^NSString *{
|
||||
return @"🍪 Cookies";
|
||||
};
|
||||
viewControllerFuture = ^UIViewController *{
|
||||
return [[FLEXCookiesTableViewController alloc] init];
|
||||
};
|
||||
break;
|
||||
|
||||
case FLEXGlobalsRowFileBrowser:
|
||||
titleFuture = ^NSString *{
|
||||
return @"📁 File Browser";
|
||||
};
|
||||
viewControllerFuture = ^UIViewController *{
|
||||
return [[FLEXFileBrowserTableViewController alloc] init];
|
||||
};
|
||||
break;
|
||||
|
||||
case FLEXGlobalsRowSystemLog:
|
||||
titleFuture = ^{
|
||||
return @"⚠️ System Log";
|
||||
};
|
||||
viewControllerFuture = ^{
|
||||
return [[FLEXSystemLogTableViewController alloc] init];
|
||||
};
|
||||
break;
|
||||
|
||||
case FLEXGlobalsRowNetworkHistory:
|
||||
titleFuture = ^{
|
||||
return @"📡 Network History";
|
||||
};
|
||||
viewControllerFuture = ^{
|
||||
return [[FLEXNetworkHistoryTableViewController alloc] init];
|
||||
};
|
||||
break;
|
||||
case FLEXGlobalsRowCount:
|
||||
break;
|
||||
}
|
||||
|
||||
NSParameterAssert(titleFuture);
|
||||
NSParameterAssert(viewControllerFuture);
|
||||
|
||||
[defaultGlobalEntries addObject:[FLEXGlobalsTableViewControllerEntry entryWithNameFuture:titleFuture viewControllerFuture:viewControllerFuture]];
|
||||
}
|
||||
|
||||
return defaultGlobalEntries;
|
||||
}
|
||||
|
||||
- (id)initWithStyle:(UITableViewStyle)style
|
||||
{
|
||||
self = [super initWithStyle:style];
|
||||
if (self) {
|
||||
self.title = @"💪 FLEX";
|
||||
_entries = [[[self class] defaultGlobalEntries] arrayByAddingObjectsFromArray:[FLEXManager sharedManager].userGlobalEntries];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
+ (void)setApplicationWindow:(UIWindow *)applicationWindow
|
||||
{
|
||||
s_applicationWindow = applicationWindow;
|
||||
}
|
||||
|
||||
#pragma mark - UIViewController
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(donePressed:)];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)donePressed:(id)sender
|
||||
{
|
||||
[self.delegate globalsViewControllerDidFinish:self];
|
||||
}
|
||||
|
||||
#pragma mark - Table Data Helpers
|
||||
|
||||
- (FLEXGlobalsTableViewControllerEntry *)globalEntryAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
return self.entries[indexPath.row];
|
||||
}
|
||||
|
||||
- (NSString *)titleForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
FLEXGlobalsTableViewControllerEntry *entry = [self globalEntryAtIndexPath:indexPath];
|
||||
|
||||
return entry.entryNameFuture();
|
||||
}
|
||||
|
||||
- (UIViewController *)viewControllerToPushForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
FLEXGlobalsTableViewControllerEntry *entry = [self globalEntryAtIndexPath:indexPath];
|
||||
|
||||
return entry.viewControllerFuture();
|
||||
}
|
||||
|
||||
#pragma mark - Table View Data Source
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
||||
{
|
||||
return [self.entries count];
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
static NSString *CellIdentifier = @"Cell";
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
|
||||
if (!cell) {
|
||||
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
cell.textLabel.font = [FLEXUtility defaultFontOfSize:14.0];
|
||||
}
|
||||
|
||||
cell.textLabel.text = [self titleForRowAtIndexPath:indexPath];
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Delegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
UIViewController *viewControllerToPush = [self viewControllerToPushForRowAtIndexPath:indexPath];
|
||||
|
||||
[self.navigationController pushViewController:viewControllerToPush animated:YES];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -57,11 +57,12 @@
|
||||
|
||||
+ (instancetype)instancesTableViewControllerForClassName:(NSString *)className
|
||||
{
|
||||
const char *classNameCString = [className UTF8String];
|
||||
const char *classNameCString = className.UTF8String;
|
||||
NSMutableArray *instances = [NSMutableArray array];
|
||||
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id object, __unsafe_unretained Class actualClass) {
|
||||
if (strcmp(classNameCString, class_getName(actualClass)) == 0) {
|
||||
// Note: objects of certain classes crash when retain is called. It is up to the user to avoid tapping into instance lists for these classes.
|
||||
// Note: objects of certain classes crash when retain is called.
|
||||
// It is up to the user to avoid tapping into instance lists for these classes.
|
||||
// Ex. OS_dispatch_queue_specific_queue
|
||||
// In the future, we could provide some kind of warning for classes that are known to be problematic.
|
||||
if (malloc_size((__bridge const void *)(object)) > 0) {
|
||||
@@ -71,7 +72,7 @@
|
||||
}];
|
||||
NSArray<FLEXObjectRef *> *references = [FLEXObjectRef referencingAll:instances];
|
||||
FLEXInstancesTableViewController *viewController = [[self alloc] initWithReferences:references];
|
||||
viewController.title = [NSString stringWithFormat:@"%@ (%lu)", className, (unsigned long)[instances count]];
|
||||
viewController.title = [NSString stringWithFormat:@"%@ (%lu)", className, (unsigned long)instances.count];
|
||||
return viewController;
|
||||
}
|
||||
|
||||
@@ -93,7 +94,7 @@
|
||||
for (unsigned int ivarIndex = 0; ivarIndex < ivarCount; ivarIndex++) {
|
||||
Ivar ivar = ivars[ivarIndex];
|
||||
const char *typeEncoding = ivar_getTypeEncoding(ivar);
|
||||
if (typeEncoding[0] == @encode(id)[0] || typeEncoding[0] == @encode(Class)[0]) {
|
||||
if (typeEncoding[0] == FLEXTypeEncodingObjcObject || typeEncoding[0] == FLEXTypeEncodingObjcClass) {
|
||||
ptrdiff_t offset = ivar_getOffset(ivar);
|
||||
uintptr_t *fieldPointer = (__bridge void *)tryObject + offset;
|
||||
if (*fieldPointer == (uintptr_t)(__bridge void *)object) {
|
||||
@@ -209,7 +210,7 @@
|
||||
UIFont *cellFont = [FLEXUtility defaultTableViewCellLabelFont];
|
||||
cell.textLabel.font = cellFont;
|
||||
cell.detailTextLabel.font = cellFont;
|
||||
cell.detailTextLabel.textColor = [UIColor grayColor];
|
||||
cell.detailTextLabel.textColor = UIColor.grayColor;
|
||||
}
|
||||
|
||||
FLEXObjectRef *row = self.sections[indexPath.section][indexPath.row];
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXTableViewController.h"
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
|
||||
@interface FLEXLibrariesTableViewController : UITableViewController
|
||||
@interface FLEXLibrariesTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
|
||||
|
||||
@end
|
||||
|
||||
@@ -9,16 +9,16 @@
|
||||
#import "FLEXLibrariesTableViewController.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXClassesTableViewController.h"
|
||||
#import "FLEXClassExplorerViewController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@interface FLEXLibrariesTableViewController () <UISearchBarDelegate>
|
||||
@interface FLEXLibrariesTableViewController ()
|
||||
|
||||
@property (nonatomic, strong) NSArray<NSString *> *imageNames;
|
||||
@property (nonatomic, strong) NSArray<NSString *> *filteredImageNames;
|
||||
@property (nonatomic) NSArray<NSString *> *imageNames;
|
||||
@property (nonatomic) NSArray<NSString *> *filteredImageNames;
|
||||
@property (nonatomic) NSString *headerTitle;
|
||||
|
||||
@property (nonatomic, strong) UISearchBar *searchBar;
|
||||
@property (nonatomic, strong) Class foundClass;
|
||||
@property (nonatomic) Class foundClass;
|
||||
|
||||
@end
|
||||
|
||||
@@ -37,11 +37,37 @@
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.searchBar = [[UISearchBar alloc] init];
|
||||
self.searchBar.delegate = self;
|
||||
self.searchBar.placeholder = [FLEXUtility searchBarPlaceholderText];
|
||||
[self.searchBar sizeToFit];
|
||||
self.tableView.tableHeaderView = self.searchBar;
|
||||
self.showsSearchBar = YES;
|
||||
[self updateHeaderTitle];
|
||||
}
|
||||
|
||||
- (void)updateHeaderTitle
|
||||
{
|
||||
if (self.foundClass) {
|
||||
self.headerTitle = @"Looking for this?";
|
||||
} else if (self.imageNames.count == self.filteredImageNames.count) {
|
||||
// Unfiltered
|
||||
self.headerTitle = [NSString stringWithFormat:@"%@ libraries", @(self.imageNames.count)];
|
||||
} else {
|
||||
self.headerTitle = [NSString
|
||||
stringWithFormat:@"%@ of %@ libraries",
|
||||
@(self.filteredImageNames.count), @(self.imageNames.count)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - FLEXGlobalsEntry
|
||||
|
||||
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
|
||||
return @"📚 System Libraries";
|
||||
}
|
||||
|
||||
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
|
||||
FLEXLibrariesTableViewController *librariesViewController = [self new];
|
||||
librariesViewController.title = [self globalsEntryTitle:row];
|
||||
|
||||
return librariesViewController;
|
||||
}
|
||||
|
||||
|
||||
@@ -94,10 +120,10 @@
|
||||
|
||||
#pragma mark - Filtering
|
||||
|
||||
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
|
||||
- (void)updateSearchResults:(NSString *)searchText
|
||||
{
|
||||
if ([searchText length] > 0) {
|
||||
NSPredicate *searchPreidcate = [NSPredicate predicateWithBlock:^BOOL(NSString *evaluatedObject, NSDictionary<NSString *, id> *bindings) {
|
||||
if (searchText.length) {
|
||||
NSPredicate *searchPredicate = [NSPredicate predicateWithBlock:^BOOL(NSString *evaluatedObject, NSDictionary<NSString *, id> *bindings) {
|
||||
BOOL matches = NO;
|
||||
NSString *shortName = [self shortNameForImageName:evaluatedObject];
|
||||
if ([shortName rangeOfString:searchText options:NSCaseInsensitiveSearch].length > 0) {
|
||||
@@ -105,28 +131,19 @@
|
||||
}
|
||||
return matches;
|
||||
}];
|
||||
self.filteredImageNames = [self.imageNames filteredArrayUsingPredicate:searchPreidcate];
|
||||
self.filteredImageNames = [self.imageNames filteredArrayUsingPredicate:searchPredicate];
|
||||
} else {
|
||||
self.filteredImageNames = self.imageNames;
|
||||
}
|
||||
|
||||
self.foundClass = NSClassFromString(searchText);
|
||||
[self updateHeaderTitle];
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
|
||||
{
|
||||
[searchBar resignFirstResponder];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Data Source
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
||||
{
|
||||
return self.filteredImageNames.count + (self.foundClass ? 1 : 0);
|
||||
@@ -145,7 +162,7 @@
|
||||
NSString *executablePath;
|
||||
if (self.foundClass) {
|
||||
if (indexPath.row == 0) {
|
||||
cell.textLabel.text = [NSString stringWithFormat:@"Class \"%@\"", self.searchBar.text];
|
||||
cell.textLabel.text = [NSString stringWithFormat:@"Class \"%@\"", self.searchText];
|
||||
return cell;
|
||||
} else {
|
||||
executablePath = self.filteredImageNames[indexPath.row-1];
|
||||
@@ -158,19 +175,24 @@
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
|
||||
{
|
||||
return self.headerTitle;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Delegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
if (indexPath.row == 0 && self.foundClass) {
|
||||
FLEXClassExplorerViewController *objectExplorer = [FLEXClassExplorerViewController new];
|
||||
objectExplorer.object = self.foundClass;
|
||||
[self.navigationController pushViewController:objectExplorer animated:YES];
|
||||
[self.navigationController pushViewController:[FLEXObjectExplorerFactory
|
||||
explorerViewControllerForObject:self.foundClass
|
||||
] animated:YES];
|
||||
} else {
|
||||
FLEXClassesTableViewController *classesViewController = [[FLEXClassesTableViewController alloc] init];
|
||||
classesViewController.binaryImageName = self.filteredImageNames[self.foundClass ? indexPath.row-1 : indexPath.row];
|
||||
[self.navigationController pushViewController:classesViewController animated:YES];
|
||||
[self.navigationController pushViewController:[FLEXClassesTableViewController
|
||||
binaryImageName:self.filteredImageNames[self.foundClass ? 0 : indexPath.row]
|
||||
] animated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXTableViewController.h"
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
|
||||
@interface FLEXLiveObjectsTableViewController : UITableViewController
|
||||
@interface FLEXLiveObjectsTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
|
||||
|
||||
@end
|
||||
|
||||
@@ -10,37 +10,42 @@
|
||||
#import "FLEXHeapEnumerator.h"
|
||||
#import "FLEXInstancesTableViewController.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXScopeCarousel.h"
|
||||
#import "FLEXTableView.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
static const NSInteger kFLEXLiveObjectsSortAlphabeticallyIndex = 0;
|
||||
static const NSInteger kFLEXLiveObjectsSortByCountIndex = 1;
|
||||
static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
|
||||
|
||||
@interface FLEXLiveObjectsTableViewController () <UISearchBarDelegate>
|
||||
@interface FLEXLiveObjectsTableViewController ()
|
||||
|
||||
@property (nonatomic, strong) NSDictionary<NSString *, NSNumber *> *instanceCountsForClassNames;
|
||||
@property (nonatomic, strong) NSDictionary<NSString *, NSNumber *> *instanceSizesForClassNames;
|
||||
@property (nonatomic) NSDictionary<NSString *, NSNumber *> *instanceCountsForClassNames;
|
||||
@property (nonatomic) NSDictionary<NSString *, NSNumber *> *instanceSizesForClassNames;
|
||||
@property (nonatomic, readonly) NSArray<NSString *> *allClassNames;
|
||||
@property (nonatomic, strong) NSArray<NSString *> *filteredClassNames;
|
||||
@property (nonatomic, strong) UISearchBar *searchBar;
|
||||
@property (nonatomic) NSArray<NSString *> *filteredClassNames;
|
||||
@property (nonatomic) NSString *headerTitle;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXLiveObjectsTableViewController
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
self.tableView = [FLEXTableView flexDefaultTableView];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
// self.title = @"Live Objects";
|
||||
self.showsSearchBar = YES;
|
||||
self.searchBarDebounceInterval = kFLEXDebounceInstant;
|
||||
self.showsCarousel = YES;
|
||||
self.carousel.items = @[@"A→Z", @"Count", @"Size"];
|
||||
|
||||
self.searchBar = [[UISearchBar alloc] init];
|
||||
self.searchBar.placeholder = [FLEXUtility searchBarPlaceholderText];
|
||||
self.searchBar.delegate = self;
|
||||
self.searchBar.showsScopeBar = YES;
|
||||
self.searchBar.scopeButtonTitles = @[@"Sort Alphabetically", @"Sort by Count", @"Sort by Size"];
|
||||
[self.searchBar sizeToFit];
|
||||
self.tableView.tableHeaderView = self.searchBar;
|
||||
|
||||
self.refreshControl = [[UIRefreshControl alloc] init];
|
||||
self.refreshControl = [UIRefreshControl new];
|
||||
[self.refreshControl addTarget:self action:@selector(refreshControlDidRefresh:) forControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self reloadTableData];
|
||||
@@ -48,7 +53,7 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
|
||||
|
||||
- (NSArray<NSString *> *)allClassNames
|
||||
{
|
||||
return [self.instanceCountsForClassNames allKeys];
|
||||
return self.instanceCountsForClassNames.allKeys;
|
||||
}
|
||||
|
||||
- (void)reloadTableData
|
||||
@@ -90,7 +95,7 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
|
||||
self.instanceCountsForClassNames = mutableCountsForClassNames;
|
||||
self.instanceSizesForClassNames = mutableSizesForClassNames;
|
||||
|
||||
[self updateTableDataForSearchFilter];
|
||||
[self updateSearchResults:nil];
|
||||
}
|
||||
|
||||
- (void)refreshControlDidRefresh:(id)sender
|
||||
@@ -99,80 +104,82 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
|
||||
[self.refreshControl endRefreshing];
|
||||
}
|
||||
|
||||
- (void)updateTitle
|
||||
- (void)updateHeaderTitle
|
||||
{
|
||||
NSString *title = @"Live Objects";
|
||||
|
||||
NSUInteger totalCount = 0;
|
||||
NSUInteger totalSize = 0;
|
||||
for (NSString *className in self.allClassNames) {
|
||||
NSUInteger count = [self.instanceCountsForClassNames[className] unsignedIntegerValue];
|
||||
NSUInteger count = self.instanceCountsForClassNames[className].unsignedIntegerValue;
|
||||
totalCount += count;
|
||||
totalSize += count * [self.instanceSizesForClassNames[className] unsignedIntegerValue];
|
||||
totalSize += count * self.instanceSizesForClassNames[className].unsignedIntegerValue;
|
||||
}
|
||||
|
||||
NSUInteger filteredCount = 0;
|
||||
NSUInteger filteredSize = 0;
|
||||
for (NSString *className in self.filteredClassNames) {
|
||||
NSUInteger count = [self.instanceCountsForClassNames[className] unsignedIntegerValue];
|
||||
NSUInteger count = self.instanceCountsForClassNames[className].unsignedIntegerValue;
|
||||
filteredCount += count;
|
||||
filteredSize += count * [self.instanceSizesForClassNames[className] unsignedIntegerValue];
|
||||
filteredSize += count * self.instanceSizesForClassNames[className].unsignedIntegerValue;
|
||||
}
|
||||
|
||||
if (filteredCount == totalCount) {
|
||||
// Unfiltered
|
||||
title = [title stringByAppendingFormat:@" (%lu, %@)", (unsigned long)totalCount,
|
||||
[NSByteCountFormatter stringFromByteCount:totalSize countStyle:NSByteCountFormatterCountStyleFile]];
|
||||
self.headerTitle = [NSString
|
||||
stringWithFormat:@"%@ objects, %@",
|
||||
@(totalCount), [NSByteCountFormatter
|
||||
stringFromByteCount:totalSize
|
||||
countStyle:NSByteCountFormatterCountStyleFile
|
||||
]
|
||||
];
|
||||
} else {
|
||||
title = [title stringByAppendingFormat:@" (filtered, %lu, %@)", (unsigned long)filteredCount,
|
||||
[NSByteCountFormatter stringFromByteCount:filteredSize countStyle:NSByteCountFormatterCountStyleFile]];
|
||||
self.headerTitle = [NSString
|
||||
stringWithFormat:@"%@ of %@ objects, %@",
|
||||
@(filteredCount), @(totalCount), [NSByteCountFormatter
|
||||
stringFromByteCount:filteredSize
|
||||
countStyle:NSByteCountFormatterCountStyleFile
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - FLEXGlobalsEntry
|
||||
|
||||
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
|
||||
return @"💩 Heap Objects";
|
||||
}
|
||||
|
||||
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
|
||||
FLEXLiveObjectsTableViewController *liveObjectsViewController = [self new];
|
||||
liveObjectsViewController.title = [self globalsEntryTitle:row];
|
||||
|
||||
return liveObjectsViewController;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Search bar
|
||||
|
||||
- (void)updateSearchResults:(NSString *)filter
|
||||
{
|
||||
NSInteger selectedScope = self.selectedScope;
|
||||
|
||||
self.title = title;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Search
|
||||
|
||||
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
|
||||
{
|
||||
[self updateTableDataForSearchFilter];
|
||||
}
|
||||
|
||||
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
|
||||
{
|
||||
[searchBar resignFirstResponder];
|
||||
}
|
||||
|
||||
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope
|
||||
{
|
||||
[self updateTableDataForSearchFilter];
|
||||
}
|
||||
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
||||
{
|
||||
// Dismiss the keyboard when interacting with filtered results.
|
||||
[self.searchBar endEditing:YES];
|
||||
}
|
||||
|
||||
- (void)updateTableDataForSearchFilter
|
||||
{
|
||||
if ([self.searchBar.text length] > 0) {
|
||||
NSPredicate *searchPreidcate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", self.searchBar.text];
|
||||
self.filteredClassNames = [self.allClassNames filteredArrayUsingPredicate:searchPreidcate];
|
||||
if (filter.length) {
|
||||
NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", filter];
|
||||
self.filteredClassNames = [self.allClassNames filteredArrayUsingPredicate:searchPredicate];
|
||||
} else {
|
||||
self.filteredClassNames = self.allClassNames;
|
||||
}
|
||||
|
||||
if (self.searchBar.selectedScopeButtonIndex == kFLEXLiveObjectsSortAlphabeticallyIndex) {
|
||||
if (selectedScope == kFLEXLiveObjectsSortAlphabeticallyIndex) {
|
||||
self.filteredClassNames = [self.filteredClassNames sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
|
||||
} else if (self.searchBar.selectedScopeButtonIndex == kFLEXLiveObjectsSortByCountIndex) {
|
||||
} else if (selectedScope == kFLEXLiveObjectsSortByCountIndex) {
|
||||
self.filteredClassNames = [self.filteredClassNames sortedArrayUsingComparator:^NSComparisonResult(NSString *className1, NSString *className2) {
|
||||
NSNumber *count1 = self.instanceCountsForClassNames[className1];
|
||||
NSNumber *count2 = self.instanceCountsForClassNames[className2];
|
||||
// Reversed for descending counts.
|
||||
return [count2 compare:count1];
|
||||
}];
|
||||
} else if (self.searchBar.selectedScopeButtonIndex == kFLEXLiveObjectsSortBySizeIndex) {
|
||||
} else if (selectedScope == kFLEXLiveObjectsSortBySizeIndex) {
|
||||
self.filteredClassNames = [self.filteredClassNames sortedArrayUsingComparator:^NSComparisonResult(NSString *className1, NSString *className2) {
|
||||
NSNumber *count1 = self.instanceCountsForClassNames[className1];
|
||||
NSNumber *count2 = self.instanceCountsForClassNames[className2];
|
||||
@@ -183,7 +190,7 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
|
||||
}];
|
||||
}
|
||||
|
||||
[self updateTitle];
|
||||
[self updateHeaderTitle];
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
@@ -197,29 +204,37 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
||||
{
|
||||
return [self.filteredClassNames count];
|
||||
return self.filteredClassNames.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
- (UITableViewCell *)tableView:(__kindof UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
static NSString *CellIdentifier = @"Cell";
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
|
||||
if (!cell) {
|
||||
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
|
||||
}
|
||||
|
||||
UITableViewCell *cell = [tableView
|
||||
dequeueReusableCellWithIdentifier:kFLEXDefaultCell
|
||||
forIndexPath:indexPath
|
||||
];
|
||||
|
||||
NSString *className = self.filteredClassNames[indexPath.row];
|
||||
NSNumber *count = self.instanceCountsForClassNames[className];
|
||||
NSNumber *size = self.instanceSizesForClassNames[className];
|
||||
unsigned long totalSize = count.unsignedIntegerValue * size.unsignedIntegerValue;
|
||||
cell.textLabel.text = [NSString stringWithFormat:@"%@ (%ld, %@)", className, (long)[count integerValue],
|
||||
[NSByteCountFormatter stringFromByteCount:totalSize countStyle:NSByteCountFormatterCountStyleFile]];
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
cell.textLabel.text = [NSString stringWithFormat:@"%@ (%ld, %@)",
|
||||
className, (long)[count integerValue],
|
||||
[NSByteCountFormatter
|
||||
stringFromByteCount:totalSize
|
||||
countStyle:NSByteCountFormatterCountStyleFile
|
||||
]
|
||||
];
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
|
||||
{
|
||||
return self.headerTitle;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table view delegate
|
||||
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
|
||||
#import "FLEXWebViewController.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import <WebKit/WebKit.h>
|
||||
|
||||
@interface FLEXWebViewController () <UIWebViewDelegate>
|
||||
@interface FLEXWebViewController () <WKNavigationDelegate>
|
||||
|
||||
@property (nonatomic, strong) UIWebView *webView;
|
||||
@property (nonatomic, strong) NSString *originalText;
|
||||
@property (nonatomic) WKWebView *webView;
|
||||
@property (nonatomic) NSString *originalText;
|
||||
|
||||
@end
|
||||
|
||||
@@ -22,10 +23,14 @@
|
||||
{
|
||||
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
|
||||
if (self) {
|
||||
self.webView = [[UIWebView alloc] init];
|
||||
self.webView.delegate = self;
|
||||
self.webView.dataDetectorTypes = UIDataDetectorTypeLink;
|
||||
self.webView.scalesPageToFit = YES;
|
||||
WKWebViewConfiguration *configuration = [WKWebViewConfiguration new];
|
||||
|
||||
if (@available(iOS 10.0, *)) {
|
||||
configuration.dataDetectorTypes = UIDataDetectorTypeLink;
|
||||
}
|
||||
|
||||
self.webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration];
|
||||
self.webView.navigationDelegate = self;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -53,9 +58,9 @@
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
// UIWebView's delegate is assign so we need to clear it manually.
|
||||
if (_webView.delegate == self) {
|
||||
_webView.delegate = nil;
|
||||
// WKWebView's delegate is assigned so we need to clear it manually.
|
||||
if (_webView.navigationDelegate == self) {
|
||||
_webView.navigationDelegate = nil;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,33 +72,34 @@
|
||||
self.webView.frame = self.view.bounds;
|
||||
self.webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
|
||||
if ([self.originalText length] > 0) {
|
||||
if (self.originalText.length > 0) {
|
||||
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Copy" style:UIBarButtonItemStylePlain target:self action:@selector(copyButtonTapped:)];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)copyButtonTapped:(id)sender
|
||||
{
|
||||
[[UIPasteboard generalPasteboard] setString:self.originalText];
|
||||
[UIPasteboard.generalPasteboard setString:self.originalText];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UIWebView Delegate
|
||||
#pragma mark - WKWebView Delegate
|
||||
|
||||
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
|
||||
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
|
||||
{
|
||||
BOOL shouldStart = NO;
|
||||
if (navigationType == UIWebViewNavigationTypeOther) {
|
||||
WKNavigationActionPolicy policy = WKNavigationActionPolicyCancel;
|
||||
if (navigationAction.navigationType == WKNavigationTypeOther) {
|
||||
// Allow the initial load
|
||||
shouldStart = YES;
|
||||
policy = WKNavigationActionPolicyAllow;
|
||||
} else {
|
||||
// For clicked links, push another web view controller onto the navigation stack so that hitting the back button works as expected.
|
||||
// Don't allow the current web view do handle the navigation.
|
||||
// Don't allow the current web view to handle the navigation.
|
||||
NSURLRequest *request = navigationAction.request;
|
||||
FLEXWebViewController *webVC = [[[self class] alloc] initWithURL:[request URL]];
|
||||
webVC.title = [[request URL] absoluteString];
|
||||
[self.navigationController pushViewController:webVC animated:YES];
|
||||
}
|
||||
return shouldStart;
|
||||
decisionHandler(policy);
|
||||
}
|
||||
|
||||
|
||||
@@ -111,17 +117,17 @@
|
||||
|
||||
+ (NSSet<NSString *> *)webViewSupportedPathExtensions
|
||||
{
|
||||
static NSSet<NSString *> *pathExtenstions = nil;
|
||||
static NSSet<NSString *> *pathExtensions = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
// Note that this is not exhaustive, but all these extensions should work well in the web view.
|
||||
// See https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/CreatingContentforSafarioniPhone/CreatingContentforSafarioniPhone.html#//apple_ref/doc/uid/TP40006482-SW7
|
||||
pathExtenstions = [NSSet<NSString *> setWithArray:@[@"jpg", @"jpeg", @"png", @"gif", @"pdf", @"svg", @"tiff", @"3gp", @"3gpp", @"3g2",
|
||||
@"3gp2", @"aiff", @"aif", @"aifc", @"cdda", @"amr", @"mp3", @"swa", @"mp4", @"mpeg",
|
||||
@"mpg", @"mp3", @"wav", @"bwf", @"m4a", @"m4b", @"m4p", @"mov", @"qt", @"mqv", @"m4v"]];
|
||||
// See https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/CreatingContentforSafarioniPhone/CreatingContentforSafarioniPhone.html#//apple_ref/doc/uid/TP40006482-SW7
|
||||
pathExtensions = [NSSet<NSString *> setWithArray:@[@"jpg", @"jpeg", @"png", @"gif", @"pdf", @"svg", @"tiff", @"3gp", @"3gpp", @"3g2",
|
||||
@"3gp2", @"aiff", @"aif", @"aifc", @"cdda", @"amr", @"mp3", @"swa", @"mp4", @"mpeg",
|
||||
@"mpg", @"mp3", @"wav", @"bwf", @"m4a", @"m4b", @"m4p", @"mov", @"qt", @"mqv", @"m4v"]];
|
||||
|
||||
});
|
||||
return pathExtenstions;
|
||||
return pathExtensions;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// FLEXFileBrowserSearchOperation.h
|
||||
// UICatalog
|
||||
// FLEX
|
||||
//
|
||||
// Created by 啟倫 陳 on 2014/8/4.
|
||||
// Copyright (c) 2014年 f. All rights reserved.
|
||||
+7
-7
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// FLEXFileBrowserSearchOperation.m
|
||||
// UICatalog
|
||||
// FLEX
|
||||
//
|
||||
// Created by 啟倫 陳 on 2014/8/4.
|
||||
// Copyright (c) 2014年 f. All rights reserved.
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
- (id)flex_pop
|
||||
{
|
||||
id anObject = [self lastObject];
|
||||
id anObject = self.lastObject;
|
||||
[self removeLastObject];
|
||||
return anObject;
|
||||
}
|
||||
@@ -26,8 +26,8 @@
|
||||
|
||||
@interface FLEXFileBrowserSearchOperation ()
|
||||
|
||||
@property (nonatomic, strong) NSString *path;
|
||||
@property (nonatomic, strong) NSString *searchString;
|
||||
@property (nonatomic) NSString *path;
|
||||
@property (nonatomic) NSString *searchString;
|
||||
|
||||
@end
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
- (uint64_t)totalSizeAtPath:(NSString *)path
|
||||
{
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
NSFileManager *fileManager = NSFileManager.defaultManager;
|
||||
NSDictionary<NSString *, id> *attributes = [fileManager attributesOfItemAtPath:path error:NULL];
|
||||
uint64_t totalSize = [attributes fileSize];
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
|
||||
- (void)main
|
||||
{
|
||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||
NSFileManager *fileManager = NSFileManager.defaultManager;
|
||||
NSMutableArray<NSString *> *searchPaths = [NSMutableArray array];
|
||||
NSMutableDictionary<NSString *, NSNumber *> *sizeMapping = [NSMutableDictionary dictionary];
|
||||
uint64_t totalSize = 0;
|
||||
@@ -72,7 +72,7 @@
|
||||
[stack flex_push:self.path];
|
||||
|
||||
//recursive found all match searchString paths, and precomputing there size
|
||||
while ([stack count]) {
|
||||
while (stack.count) {
|
||||
NSString *currentPath = [stack flex_pop];
|
||||
NSArray<NSString *> *directoryPath = [fileManager contentsOfDirectoryAtPath:currentPath error:nil];
|
||||
|
||||
+4
-3
@@ -6,12 +6,13 @@
|
||||
// Based on previous work by Evan Doll
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
#import "FLEXFileBrowserSearchOperation.h"
|
||||
|
||||
@interface FLEXFileBrowserTableViewController : UITableViewController
|
||||
@interface FLEXFileBrowserTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
|
||||
|
||||
+ (instancetype)path:(NSString *)path;
|
||||
- (id)initWithPath:(NSString *)path;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,506 @@
|
||||
//
|
||||
// FLEXFileBrowserTableViewController.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/9/14.
|
||||
//
|
||||
//
|
||||
|
||||
#import "FLEXFileBrowserTableViewController.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXWebViewController.h"
|
||||
#import "FLEXImagePreviewViewController.h"
|
||||
#import "FLEXTableListViewController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
|
||||
@interface FLEXFileBrowserTableViewCell : UITableViewCell
|
||||
@end
|
||||
|
||||
@interface FLEXFileBrowserTableViewController () <FLEXFileBrowserSearchOperationDelegate>
|
||||
|
||||
@property (nonatomic, copy) NSString *path;
|
||||
@property (nonatomic, copy) NSArray<NSString *> *childPaths;
|
||||
@property (nonatomic) NSArray<NSString *> *searchPaths;
|
||||
@property (nonatomic) NSNumber *recursiveSize;
|
||||
@property (nonatomic) NSNumber *searchPathsSize;
|
||||
@property (nonatomic) NSOperationQueue *operationQueue;
|
||||
@property (nonatomic) UIDocumentInteractionController *documentController;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXFileBrowserTableViewController
|
||||
|
||||
+ (instancetype)path:(NSString *)path
|
||||
{
|
||||
return [[self alloc] initWithPath:path];
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
return [self initWithPath:NSHomeDirectory()];
|
||||
}
|
||||
|
||||
- (id)initWithPath:(NSString *)path
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.path = path;
|
||||
self.title = [path lastPathComponent];
|
||||
self.operationQueue = [NSOperationQueue new];
|
||||
|
||||
|
||||
//computing path size
|
||||
FLEXFileBrowserTableViewController *__weak weakSelf = self;
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSFileManager *fileManager = NSFileManager.defaultManager;
|
||||
NSDictionary<NSString *, id> *attributes = [fileManager attributesOfItemAtPath:path error:NULL];
|
||||
uint64_t totalSize = [attributes fileSize];
|
||||
|
||||
for (NSString *fileName in [fileManager enumeratorAtPath:path]) {
|
||||
attributes = [fileManager attributesOfItemAtPath:[path stringByAppendingPathComponent:fileName] error:NULL];
|
||||
totalSize += [attributes fileSize];
|
||||
|
||||
// Bail if the interested view controller has gone away.
|
||||
if (!weakSelf) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
FLEXFileBrowserTableViewController *__strong strongSelf = weakSelf;
|
||||
strongSelf.recursiveSize = @(totalSize);
|
||||
[strongSelf.tableView reloadData];
|
||||
});
|
||||
});
|
||||
|
||||
[self reloadCurrentPath];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - UIViewController
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.showsSearchBar = YES;
|
||||
self.searchBarDebounceInterval = kFLEXDebounceForAsyncSearch;
|
||||
}
|
||||
|
||||
#pragma mark - FLEXGlobalsEntry
|
||||
|
||||
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
|
||||
switch (row) {
|
||||
case FLEXGlobalsRowBrowseBundle: return @"📁 Browse Bundle Directory";
|
||||
case FLEXGlobalsRowBrowseContainer: return @"📁 Browse Container Directory";
|
||||
default: return nil;
|
||||
}
|
||||
}
|
||||
|
||||
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
|
||||
switch (row) {
|
||||
case FLEXGlobalsRowBrowseBundle: return [[self alloc] initWithPath:NSBundle.mainBundle.bundlePath];
|
||||
case FLEXGlobalsRowBrowseContainer: return [[self alloc] initWithPath:NSHomeDirectory()];
|
||||
default: return [self new];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - FLEXFileBrowserSearchOperationDelegate
|
||||
|
||||
- (void)fileBrowserSearchOperationResult:(NSArray<NSString *> *)searchResult size:(uint64_t)size
|
||||
{
|
||||
self.searchPaths = searchResult;
|
||||
self.searchPathsSize = @(size);
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
#pragma mark - Search bar
|
||||
|
||||
- (void)updateSearchResults:(NSString *)newText
|
||||
{
|
||||
[self reloadDisplayedPaths];
|
||||
}
|
||||
|
||||
#pragma mark UISearchControllerDelegate
|
||||
|
||||
- (void)willDismissSearchController:(UISearchController *)searchController
|
||||
{
|
||||
[self.operationQueue cancelAllOperations];
|
||||
[self reloadCurrentPath];
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
#pragma mark - Table view data source
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
||||
{
|
||||
return self.searchController.isActive ? self.searchPaths.count : self.childPaths.count;
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
|
||||
{
|
||||
BOOL isSearchActive = self.searchController.isActive;
|
||||
NSNumber *currentSize = isSearchActive ? self.searchPathsSize : self.recursiveSize;
|
||||
NSArray<NSString *> *currentPaths = isSearchActive ? self.searchPaths : self.childPaths;
|
||||
|
||||
NSString *sizeString = nil;
|
||||
if (!currentSize) {
|
||||
sizeString = @"Computing size…";
|
||||
} else {
|
||||
sizeString = [NSByteCountFormatter stringFromByteCount:[currentSize longLongValue] countStyle:NSByteCountFormatterCountStyleFile];
|
||||
}
|
||||
|
||||
return [NSString stringWithFormat:@"%lu files (%@)", (unsigned long)currentPaths.count, sizeString];
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
NSString *fullPath = [self filePathAtIndexPath:indexPath];
|
||||
NSDictionary<NSString *, id> *attributes = [NSFileManager.defaultManager attributesOfItemAtPath:fullPath error:NULL];
|
||||
BOOL isDirectory = [attributes.fileType isEqual:NSFileTypeDirectory];
|
||||
NSString *subtitle = nil;
|
||||
if (isDirectory) {
|
||||
NSUInteger count = [NSFileManager.defaultManager contentsOfDirectoryAtPath:fullPath error:NULL].count;
|
||||
subtitle = [NSString stringWithFormat:@"%lu item%@", (unsigned long)count, (count == 1 ? @"" : @"s")];
|
||||
} else {
|
||||
NSString *sizeString = [NSByteCountFormatter stringFromByteCount:attributes.fileSize countStyle:NSByteCountFormatterCountStyleFile];
|
||||
subtitle = [NSString stringWithFormat:@"%@ - %@", sizeString, attributes.fileModificationDate ?: @"Never modified"];
|
||||
}
|
||||
|
||||
static NSString *textCellIdentifier = @"textCell";
|
||||
static NSString *imageCellIdentifier = @"imageCell";
|
||||
UITableViewCell *cell = nil;
|
||||
|
||||
// Separate image and text only cells because otherwise the separator lines get out-of-whack on image cells reused with text only.
|
||||
UIImage *image = [UIImage imageWithContentsOfFile:fullPath];
|
||||
NSString *cellIdentifier = image ? imageCellIdentifier : textCellIdentifier;
|
||||
|
||||
if (!cell) {
|
||||
cell = [[FLEXFileBrowserTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
|
||||
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
|
||||
cell.detailTextLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
|
||||
cell.detailTextLabel.textColor = UIColor.grayColor;
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
}
|
||||
NSString *cellTitle = [fullPath lastPathComponent];
|
||||
cell.textLabel.text = cellTitle;
|
||||
cell.detailTextLabel.text = subtitle;
|
||||
|
||||
if (image) {
|
||||
cell.imageView.contentMode = UIViewContentModeScaleAspectFit;
|
||||
cell.imageView.image = image;
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
|
||||
NSString *fullPath = [self filePathAtIndexPath:indexPath];
|
||||
NSString *subpath = fullPath.lastPathComponent;
|
||||
NSString *pathExtension = subpath.pathExtension;
|
||||
|
||||
BOOL isDirectory = NO;
|
||||
BOOL stillExists = [NSFileManager.defaultManager fileExistsAtPath:fullPath isDirectory:&isDirectory];
|
||||
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
|
||||
UIImage *image = cell.imageView.image;
|
||||
|
||||
if (!stillExists) {
|
||||
[FLEXAlert showAlert:@"File Not Found" message:@"The file at the specified path no longer exists." from:self];
|
||||
[self reloadDisplayedPaths];
|
||||
return;
|
||||
}
|
||||
|
||||
UIViewController *drillInViewController = nil;
|
||||
if (isDirectory) {
|
||||
drillInViewController = [[[self class] alloc] initWithPath:fullPath];
|
||||
} else if (image) {
|
||||
drillInViewController = [[FLEXImagePreviewViewController alloc] initWithImage:image];
|
||||
} else {
|
||||
NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
|
||||
if (!fileData.length) {
|
||||
[FLEXAlert showAlert:@"Empty File" message:@"No data returned from the file." from:self];
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case keyed archives, json, and plists to get more readable data.
|
||||
NSString *prettyString = nil;
|
||||
if ([pathExtension isEqualToString:@"json"]) {
|
||||
prettyString = [FLEXUtility prettyJSONStringFromData:fileData];
|
||||
} else {
|
||||
// Regardless of file extension...
|
||||
|
||||
id object = nil;
|
||||
@try {
|
||||
// Try to decode an archived object regardless of file extension
|
||||
object = [NSKeyedUnarchiver unarchiveObjectWithData:fileData];
|
||||
} @catch (NSException *e) { }
|
||||
|
||||
// Try to decode other things instead
|
||||
object = object
|
||||
?: [NSPropertyListSerialization propertyListWithData:fileData
|
||||
options:0
|
||||
format:NULL
|
||||
error:NULL]
|
||||
?: [NSDictionary dictionaryWithContentsOfFile:fullPath]
|
||||
?: [NSArray arrayWithContentsOfFile:fullPath];
|
||||
|
||||
if (object) {
|
||||
drillInViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:object];
|
||||
}
|
||||
}
|
||||
|
||||
if (prettyString.length) {
|
||||
drillInViewController = [[FLEXWebViewController alloc] initWithText:prettyString];
|
||||
} else if ([FLEXWebViewController supportsPathExtension:pathExtension]) {
|
||||
drillInViewController = [[FLEXWebViewController alloc] initWithURL:[NSURL fileURLWithPath:fullPath]];
|
||||
} else if ([FLEXTableListViewController supportsExtension:pathExtension]) {
|
||||
drillInViewController = [[FLEXTableListViewController alloc] initWithPath:fullPath];
|
||||
}
|
||||
else if (!drillInViewController) {
|
||||
NSString *fileString = [NSString stringWithUTF8String:fileData.bytes];
|
||||
if (fileString.length) {
|
||||
drillInViewController = [[FLEXWebViewController alloc] initWithText:fileString];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (drillInViewController) {
|
||||
drillInViewController.title = subpath.lastPathComponent;
|
||||
[self.navigationController pushViewController:drillInViewController animated:YES];
|
||||
} else {
|
||||
// Share the file otherwise
|
||||
[self openFileController:fullPath];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
UIMenuItem *rename = [[UIMenuItem alloc] initWithTitle:@"Rename" action:@selector(fileBrowserRename:)];
|
||||
UIMenuItem *delete = [[UIMenuItem alloc] initWithTitle:@"Delete" action:@selector(fileBrowserDelete:)];
|
||||
UIMenuItem *copyPath = [[UIMenuItem alloc] initWithTitle:@"Copy Path" action:@selector(fileBrowserCopyPath:)];
|
||||
UIMenuItem *share = [[UIMenuItem alloc] initWithTitle:@"Share" action:@selector(fileBrowserShare:)];
|
||||
|
||||
UIMenuController.sharedMenuController.menuItems = @[rename, delete, copyPath, share];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
|
||||
{
|
||||
return action == @selector(fileBrowserDelete:)
|
||||
|| action == @selector(fileBrowserRename:)
|
||||
|| action == @selector(fileBrowserCopyPath:)
|
||||
|| action == @selector(fileBrowserShare:);
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
|
||||
{
|
||||
// Empty, but has to exist for the menu to show
|
||||
// The table view only calls this method for actions in the UIResponderStandardEditActions informal protocol.
|
||||
// Since our actions are outside of that protocol, we need to manually handle the action forwarding from the cells.
|
||||
}
|
||||
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
|
||||
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0)
|
||||
{
|
||||
__weak typeof(self) weakSelf = self;
|
||||
return [UIContextMenuConfiguration configurationWithIdentifier:nil
|
||||
previewProvider:nil
|
||||
actionProvider:^UIMenu * _Nullable(NSArray<UIMenuElement *> * _Nonnull suggestedActions) {
|
||||
UITableViewCell * const cell = [tableView cellForRowAtIndexPath:indexPath];
|
||||
UIAction *rename = [UIAction actionWithTitle:@"Rename"
|
||||
image:nil
|
||||
identifier:@"Rename"
|
||||
handler:^(__kindof UIAction * _Nonnull action) {
|
||||
[weakSelf fileBrowserRename:cell];
|
||||
}];
|
||||
UIAction *delete = [UIAction actionWithTitle:@"Delete"
|
||||
image:nil
|
||||
identifier:@"Delete"
|
||||
handler:^(__kindof UIAction * _Nonnull action) {
|
||||
[weakSelf fileBrowserDelete:cell];
|
||||
}];
|
||||
UIAction *copyPath = [UIAction actionWithTitle:@"Copy Path"
|
||||
image:nil
|
||||
identifier:@"Copy Path"
|
||||
handler:^(__kindof UIAction * _Nonnull action) {
|
||||
[weakSelf fileBrowserCopyPath:cell];
|
||||
}];
|
||||
UIAction *share = [UIAction actionWithTitle:@"Share"
|
||||
image:nil
|
||||
identifier:@"Share"
|
||||
handler:^(__kindof UIAction * _Nonnull action) {
|
||||
[weakSelf fileBrowserShare:cell];
|
||||
}];
|
||||
return [UIMenu menuWithTitle:@"Manage File" image:nil identifier:@"Manage File" options:UIMenuOptionsDisplayInline children:@[rename, delete, copyPath, share]];
|
||||
}];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
- (void)openFileController:(NSString *)fullPath
|
||||
{
|
||||
UIDocumentInteractionController *controller = [UIDocumentInteractionController new];
|
||||
controller.URL = [NSURL fileURLWithPath:fullPath];
|
||||
|
||||
[controller presentOptionsMenuFromRect:self.view.bounds inView:self.view animated:YES];
|
||||
self.documentController = controller;
|
||||
}
|
||||
|
||||
- (void)fileBrowserRename:(UITableViewCell *)sender
|
||||
{
|
||||
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
|
||||
NSString *fullPath = [self filePathAtIndexPath:indexPath];
|
||||
|
||||
BOOL stillExists = [NSFileManager.defaultManager fileExistsAtPath:self.path isDirectory:NULL];
|
||||
if (stillExists) {
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title([NSString stringWithFormat:@"Rename %@?", fullPath.lastPathComponent]);
|
||||
make.configuredTextField(^(UITextField *textField) {
|
||||
textField.placeholder = @"New file name";
|
||||
textField.text = fullPath.lastPathComponent;
|
||||
});
|
||||
make.button(@"Rename").handler(^(NSArray<NSString *> *strings) {
|
||||
NSString *newFileName = strings.firstObject;
|
||||
NSString *newPath = [fullPath.stringByDeletingLastPathComponent stringByAppendingPathComponent:newFileName];
|
||||
[NSFileManager.defaultManager moveItemAtPath:fullPath toPath:newPath error:NULL];
|
||||
[self reloadDisplayedPaths];
|
||||
});
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:self];
|
||||
} else {
|
||||
[FLEXAlert showAlert:@"File Removed" message:@"The file at the specified path no longer exists." from:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)fileBrowserDelete:(UITableViewCell *)sender
|
||||
{
|
||||
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
|
||||
NSString *fullPath = [self filePathAtIndexPath:indexPath];
|
||||
|
||||
BOOL isDirectory = NO;
|
||||
BOOL stillExists = [NSFileManager.defaultManager fileExistsAtPath:fullPath isDirectory:&isDirectory];
|
||||
if (stillExists) {
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Confirm Deletion");
|
||||
make.message([NSString stringWithFormat:
|
||||
@"The %@ '%@' will be deleted. This operation cannot be undone",
|
||||
(isDirectory ? @"directory" : @"file"), fullPath.lastPathComponent
|
||||
]);
|
||||
make.button(@"Delete").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
|
||||
[NSFileManager.defaultManager removeItemAtPath:fullPath error:NULL];
|
||||
[self reloadDisplayedPaths];
|
||||
});
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:self];
|
||||
} else {
|
||||
[FLEXAlert showAlert:@"File Removed" message:@"The file at the specified path no longer exists." from:self];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)fileBrowserCopyPath:(UITableViewCell *)sender
|
||||
{
|
||||
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
|
||||
NSString *fullPath = [self filePathAtIndexPath:indexPath];
|
||||
UIPasteboard.generalPasteboard.string = fullPath;
|
||||
}
|
||||
|
||||
- (void)fileBrowserShare:(UITableViewCell *)sender
|
||||
{
|
||||
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
|
||||
NSString *pathString = [self filePathAtIndexPath:indexPath];
|
||||
NSURL *filePath = [NSURL fileURLWithPath:pathString];
|
||||
|
||||
BOOL isDirectory = NO;
|
||||
[NSFileManager.defaultManager fileExistsAtPath:pathString isDirectory:&isDirectory];
|
||||
|
||||
if (isDirectory) {
|
||||
// UIDocumentInteractionController for folders
|
||||
[self openFileController:pathString];
|
||||
} else {
|
||||
// Share sheet for files
|
||||
UIActivityViewController *shareSheet = [[UIActivityViewController alloc] initWithActivityItems:@[filePath] applicationActivities:nil];
|
||||
[self presentViewController:shareSheet animated:true completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reloadDisplayedPaths
|
||||
{
|
||||
if (self.searchController.isActive) {
|
||||
[self updateSearchPaths];
|
||||
} else {
|
||||
[self reloadCurrentPath];
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reloadCurrentPath
|
||||
{
|
||||
NSMutableArray<NSString *> *childPaths = [NSMutableArray array];
|
||||
NSArray<NSString *> *subpaths = [NSFileManager.defaultManager contentsOfDirectoryAtPath:self.path error:NULL];
|
||||
for (NSString *subpath in subpaths) {
|
||||
[childPaths addObject:[self.path stringByAppendingPathComponent:subpath]];
|
||||
}
|
||||
self.childPaths = childPaths;
|
||||
}
|
||||
|
||||
- (void)updateSearchPaths
|
||||
{
|
||||
self.searchPaths = nil;
|
||||
self.searchPathsSize = nil;
|
||||
|
||||
//clear pre search request and start a new one
|
||||
[self.operationQueue cancelAllOperations];
|
||||
FLEXFileBrowserSearchOperation *newOperation = [[FLEXFileBrowserSearchOperation alloc] initWithPath:self.path searchString:self.searchText];
|
||||
newOperation.delegate = self;
|
||||
[self.operationQueue addOperation:newOperation];
|
||||
}
|
||||
|
||||
- (NSString *)filePathAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
return self.searchController.isActive ? self.searchPaths[indexPath.row] : self.childPaths[indexPath.row];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FLEXFileBrowserTableViewCell
|
||||
|
||||
- (void)forwardAction:(SEL)action withSender:(id)sender
|
||||
{
|
||||
id target = [self.nextResponder targetForAction:action withSender:sender];
|
||||
[UIApplication.sharedApplication sendAction:action to:target from:self forEvent:nil];
|
||||
}
|
||||
|
||||
- (void)fileBrowserRename:(UIMenuController *)sender
|
||||
{
|
||||
[self forwardAction:_cmd withSender:sender];
|
||||
}
|
||||
|
||||
- (void)fileBrowserDelete:(UIMenuController *)sender
|
||||
{
|
||||
[self forwardAction:_cmd withSender:sender];
|
||||
}
|
||||
|
||||
- (void)fileBrowserCopyPath:(UIMenuController *)sender
|
||||
{
|
||||
[self forwardAction:_cmd withSender:sender];
|
||||
}
|
||||
|
||||
- (void)fileBrowserShare:(UIMenuController *)sender
|
||||
{
|
||||
[self forwardAction:_cmd withSender:sender];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// FLEXGlobalsEntry.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Javier Soto on 7/26/14.
|
||||
// Copyright (c) 2014 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXTableViewSection.h"
|
||||
@class FLEXGlobalsTableViewController;
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
|
||||
FLEXGlobalsRowProcessInfo,
|
||||
FLEXGlobalsRowNetworkHistory,
|
||||
FLEXGlobalsRowSystemLog,
|
||||
FLEXGlobalsRowLiveObjects,
|
||||
FLEXGlobalsRowAddressInspector,
|
||||
FLEXGlobalsRowCookies,
|
||||
FLEXGlobalsRowSystemLibraries,
|
||||
FLEXGlobalsRowAppClasses,
|
||||
FLEXGlobalsRowAppKeychainItems,
|
||||
FLEXGlobalsRowAppDelegate,
|
||||
FLEXGlobalsRowRootViewController,
|
||||
FLEXGlobalsRowUserDefaults,
|
||||
FLEXGlobalsRowMainBundle,
|
||||
FLEXGlobalsRowBrowseBundle,
|
||||
FLEXGlobalsRowBrowseContainer,
|
||||
FLEXGlobalsRowApplication,
|
||||
FLEXGlobalsRowKeyWindow,
|
||||
FLEXGlobalsRowMainScreen,
|
||||
FLEXGlobalsRowCurrentDevice,
|
||||
FLEXGlobalsRowPasteboard,
|
||||
FLEXGlobalsRowCount
|
||||
};
|
||||
|
||||
typedef NSString *(^FLEXGlobalsEntryNameFuture)(void);
|
||||
/// Simply return a view controller to be pushed on the navigation stack
|
||||
typedef UIViewController *(^FLEXGlobalsTableViewControllerViewControllerFuture)(void);
|
||||
/// Do something like present an alert, then use the host
|
||||
/// view controller to present or push another view controller.
|
||||
typedef void (^FLEXGlobalsTableViewControllerRowAction)(FLEXGlobalsTableViewController *host);
|
||||
|
||||
/// For view controllers to conform to to indicate they support being used
|
||||
/// in the globals table view controller. These methods help create concrete entries.
|
||||
///
|
||||
/// Previously, the concrete entries relied on "futures" for the view controller and title.
|
||||
/// With this protocol, the conforming class itself can act as a future, since the methods
|
||||
/// will not be invoked until the title and view controller / row action are needed.
|
||||
@protocol FLEXGlobalsEntry <NSObject>
|
||||
|
||||
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row;
|
||||
|
||||
// Must respond to at least one of the below
|
||||
@optional
|
||||
|
||||
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row;
|
||||
+ (FLEXGlobalsTableViewControllerRowAction)globalsEntryRowAction:(FLEXGlobalsRow)row;
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXGlobalsEntry : NSObject <FLEXPatternMatching>
|
||||
|
||||
@property (nonatomic, readonly) FLEXGlobalsEntryNameFuture entryNameFuture;
|
||||
@property (nonatomic, readonly) FLEXGlobalsTableViewControllerViewControllerFuture viewControllerFuture;
|
||||
@property (nonatomic, readonly) FLEXGlobalsTableViewControllerRowAction rowAction;
|
||||
|
||||
+ (instancetype)entryWithEntry:(Class<FLEXGlobalsEntry>)entry row:(FLEXGlobalsRow)row;
|
||||
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture viewControllerFuture:(FLEXGlobalsTableViewControllerViewControllerFuture)viewControllerFuture;
|
||||
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture action:(FLEXGlobalsTableViewControllerRowAction)rowSelectedAction;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface NSObject (FLEXGlobalsEntry)
|
||||
|
||||
/// @return The result of passing self to +[FLEXGlobalsEntry entryWithEntry:]
|
||||
/// if the class conforms to FLEXGlobalsEntry, else, nil.
|
||||
+ (FLEXGlobalsEntry *)flex_concreteGlobalsEntry:(FLEXGlobalsRow)row;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// FLEXGlobalsEntry.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Javier Soto on 7/26/14.
|
||||
// Copyright (c) 2014 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
|
||||
@implementation FLEXGlobalsEntry
|
||||
|
||||
+ (instancetype)entryWithEntry:(Class<FLEXGlobalsEntry>)cls row:(FLEXGlobalsRow)row
|
||||
{
|
||||
NSParameterAssert(cls);
|
||||
NSParameterAssert(
|
||||
[cls respondsToSelector:@selector(globalsEntryViewController:)] ||
|
||||
[cls respondsToSelector:@selector(globalsEntryRowAction:)]
|
||||
);
|
||||
|
||||
FLEXGlobalsEntry *entry = [self new];
|
||||
entry->_entryNameFuture = ^{ return [cls globalsEntryTitle:row]; };
|
||||
|
||||
if ([cls respondsToSelector:@selector(globalsEntryViewController:)]) {
|
||||
entry->_viewControllerFuture = ^{ return [cls globalsEntryViewController:row]; };
|
||||
} else {
|
||||
entry->_rowAction = [cls globalsEntryRowAction:row];
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture
|
||||
viewControllerFuture:(FLEXGlobalsTableViewControllerViewControllerFuture)viewControllerFuture
|
||||
{
|
||||
NSParameterAssert(nameFuture);
|
||||
NSParameterAssert(viewControllerFuture);
|
||||
|
||||
FLEXGlobalsEntry *entry = [self new];
|
||||
entry->_entryNameFuture = [nameFuture copy];
|
||||
entry->_viewControllerFuture = [viewControllerFuture copy];
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture
|
||||
action:(FLEXGlobalsTableViewControllerRowAction)rowSelectedAction
|
||||
{
|
||||
NSParameterAssert(nameFuture);
|
||||
NSParameterAssert(rowSelectedAction);
|
||||
|
||||
FLEXGlobalsEntry *entry = [self new];
|
||||
entry->_entryNameFuture = [nameFuture copy];
|
||||
entry->_rowAction = [rowSelectedAction copy];
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
#pragma mark FLEXPatternMatching
|
||||
|
||||
- (BOOL)matches:(NSString *)query
|
||||
{
|
||||
return [self.entryNameFuture() localizedCaseInsensitiveContainsString:query];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - flex_concreteGlobalsEntry
|
||||
|
||||
@implementation NSObject (FLEXGlobalsEntry)
|
||||
|
||||
+ (FLEXGlobalsEntry *)flex_concreteGlobalsEntry:(FLEXGlobalsRow)row {
|
||||
if ([self conformsToProtocol:@protocol(FLEXGlobalsEntry)]) {
|
||||
return [FLEXGlobalsEntry entryWithEntry:self row:row];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
+16
-3
@@ -6,11 +6,24 @@
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
@protocol FLEXGlobalsTableViewControllerDelegate;
|
||||
|
||||
@interface FLEXGlobalsTableViewController : UITableViewController
|
||||
typedef NS_ENUM(NSUInteger, FLEXGlobalsSection) {
|
||||
/// NSProcessInfo, Network history, system log,
|
||||
/// heap, address explorer, libraries, app classes
|
||||
FLEXGlobalsSectionProcessAndEvents,
|
||||
/// Browse container, browse bundle, NSBundle.main,
|
||||
/// NSUserDefaults.standard, UIApplication,
|
||||
/// app delegate, key window, root VC, cookies
|
||||
FLEXGlobalsSectionAppShortcuts,
|
||||
/// UIPasteBoard.general, UIScreen, UIDevice
|
||||
FLEXGlobalsSectionMisc,
|
||||
FLEXGlobalsSectionCustom,
|
||||
FLEXGlobalsSectionCount
|
||||
};
|
||||
|
||||
@interface FLEXGlobalsTableViewController : FLEXTableViewController
|
||||
|
||||
@property (nonatomic, weak) id <FLEXGlobalsTableViewControllerDelegate> delegate;
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
//
|
||||
// FLEXGlobalsTableViewController.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 2014-05-03.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXGlobalsTableViewController.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXLibrariesTableViewController.h"
|
||||
#import "FLEXClassesTableViewController.h"
|
||||
#import "FLEXKeychainTableViewController.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXLiveObjectsTableViewController.h"
|
||||
#import "FLEXFileBrowserTableViewController.h"
|
||||
#import "FLEXCookiesTableViewController.h"
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
#import "FLEXManager+Private.h"
|
||||
#import "FLEXSystemLogTableViewController.h"
|
||||
#import "FLEXNetworkHistoryTableViewController.h"
|
||||
#import "FLEXAddressExplorerCoordinator.h"
|
||||
#import "FLEXTableViewSection.h"
|
||||
|
||||
static __weak UIWindow *s_applicationWindow = nil;
|
||||
|
||||
@interface FLEXGlobalsTableViewController ()
|
||||
|
||||
@property (nonatomic, readonly) NSArray<FLEXTableViewSection<FLEXGlobalsEntry *> *> *sections;
|
||||
@property (nonatomic, copy) NSArray<FLEXTableViewSection<FLEXGlobalsEntry *> *> *filteredSections;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXGlobalsTableViewController
|
||||
|
||||
+ (NSString *)globalsTitleForSection:(FLEXGlobalsSection)section
|
||||
{
|
||||
switch (section) {
|
||||
case FLEXGlobalsSectionProcessAndEvents:
|
||||
return @"Process and Events";
|
||||
case FLEXGlobalsSectionAppShortcuts:
|
||||
return @"App Shortcuts";
|
||||
case FLEXGlobalsSectionMisc:
|
||||
return @"Miscellaneous";
|
||||
case FLEXGlobalsSectionCustom:
|
||||
return @"Custom Additions";
|
||||
|
||||
default:
|
||||
@throw NSInternalInconsistencyException;
|
||||
}
|
||||
}
|
||||
|
||||
+ (FLEXGlobalsEntry *)globalsEntryForRow:(FLEXGlobalsRow)row
|
||||
{
|
||||
switch (row) {
|
||||
case FLEXGlobalsRowAppClasses:
|
||||
return [FLEXClassesTableViewController flex_concreteGlobalsEntry:row];
|
||||
case FLEXGlobalsRowAppKeychainItems:
|
||||
return [FLEXKeychainTableViewController flex_concreteGlobalsEntry:row];
|
||||
case FLEXGlobalsRowAddressInspector:
|
||||
return [FLEXAddressExplorerCoordinator flex_concreteGlobalsEntry:row];
|
||||
case FLEXGlobalsRowSystemLibraries:
|
||||
return [FLEXLibrariesTableViewController flex_concreteGlobalsEntry:row];
|
||||
case FLEXGlobalsRowLiveObjects:
|
||||
return [FLEXLiveObjectsTableViewController flex_concreteGlobalsEntry:row];
|
||||
case FLEXGlobalsRowCookies:
|
||||
return [FLEXCookiesTableViewController flex_concreteGlobalsEntry:row];
|
||||
case FLEXGlobalsRowBrowseBundle:
|
||||
case FLEXGlobalsRowBrowseContainer:
|
||||
return [FLEXFileBrowserTableViewController flex_concreteGlobalsEntry:row];
|
||||
case FLEXGlobalsRowSystemLog:
|
||||
return [FLEXSystemLogTableViewController flex_concreteGlobalsEntry:row];
|
||||
case FLEXGlobalsRowNetworkHistory:
|
||||
return [FLEXNetworkHistoryTableViewController flex_concreteGlobalsEntry:row];
|
||||
case FLEXGlobalsRowKeyWindow:
|
||||
return [FLEXGlobalsEntry
|
||||
entryWithNameFuture:^NSString *{
|
||||
return @"🔑 -[UIApplication keyWindow]";
|
||||
} viewControllerFuture:^UIViewController *{
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:s_applicationWindow];
|
||||
}
|
||||
];
|
||||
case FLEXGlobalsRowRootViewController:
|
||||
case FLEXGlobalsRowProcessInfo:
|
||||
case FLEXGlobalsRowAppDelegate:
|
||||
case FLEXGlobalsRowUserDefaults:
|
||||
case FLEXGlobalsRowMainBundle:
|
||||
case FLEXGlobalsRowApplication:
|
||||
case FLEXGlobalsRowMainScreen:
|
||||
case FLEXGlobalsRowCurrentDevice:
|
||||
case FLEXGlobalsRowPasteboard:
|
||||
return [FLEXObjectExplorerFactory flex_concreteGlobalsEntry:row];
|
||||
|
||||
default:
|
||||
@throw [NSException
|
||||
exceptionWithName:NSInternalInconsistencyException
|
||||
reason:@"Missing globals case in switch" userInfo:nil
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSArray<FLEXTableViewSection<FLEXGlobalsEntry *> *> *)defaultGlobalSections
|
||||
{
|
||||
static NSArray<FLEXTableViewSection<FLEXGlobalsEntry *> *> *sections = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
NSArray *rows = @[
|
||||
@[
|
||||
[self globalsEntryForRow:FLEXGlobalsRowNetworkHistory],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowSystemLog],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowProcessInfo],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowLiveObjects],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowAddressInspector],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowSystemLibraries],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowAppClasses],
|
||||
],
|
||||
@[ // FLEXGlobalsSectionAppShortcuts
|
||||
[self globalsEntryForRow:FLEXGlobalsRowBrowseBundle],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowBrowseContainer],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowMainBundle],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowUserDefaults],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowAppKeychainItems],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowApplication],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowAppDelegate],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowKeyWindow],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowRootViewController],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowCookies],
|
||||
],
|
||||
@[ // FLEXGlobalsSectionMisc
|
||||
[self globalsEntryForRow:FLEXGlobalsRowPasteboard],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowMainScreen],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowCurrentDevice],
|
||||
]
|
||||
];
|
||||
|
||||
NSMutableArray *tmp = [NSMutableArray array];
|
||||
for (NSInteger i = 0; i < FLEXGlobalsSectionCount - 1; i++) { // Skip custom
|
||||
NSString *title = [self globalsTitleForSection:i];
|
||||
[tmp addObject:[FLEXTableViewSection section:i title:title rows:rows[i]]];
|
||||
}
|
||||
|
||||
sections = tmp.copy;
|
||||
});
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
+ (void)setApplicationWindow:(UIWindow *)applicationWindow
|
||||
{
|
||||
s_applicationWindow = applicationWindow;
|
||||
}
|
||||
|
||||
#pragma mark - UIViewController
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.title = @"💪 FLEX";
|
||||
self.showsSearchBar = YES;
|
||||
self.searchBarDebounceInterval = kFLEXDebounceInstant;
|
||||
|
||||
// Table view data
|
||||
_sections = [[self class] defaultGlobalSections];
|
||||
if ([FLEXManager sharedManager].userGlobalEntries.count) {
|
||||
// Make custom section
|
||||
NSString *title = [[self class] globalsTitleForSection:FLEXGlobalsSectionCustom];
|
||||
FLEXTableViewSection *custom = [FLEXTableViewSection
|
||||
section:FLEXGlobalsSectionCustom
|
||||
title:title
|
||||
rows:[FLEXManager sharedManager].userGlobalEntries
|
||||
];
|
||||
_sections = [_sections arrayByAddingObject:custom];
|
||||
}
|
||||
|
||||
// Done button
|
||||
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
|
||||
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
|
||||
target:self
|
||||
action:@selector(donePressed:)
|
||||
];
|
||||
}
|
||||
|
||||
#pragma mark - Search Bar
|
||||
|
||||
- (void)updateSearchResults:(NSString *)newText {
|
||||
if (!newText.length) {
|
||||
self.filteredSections = nil;
|
||||
[self.tableView reloadData];
|
||||
return;
|
||||
}
|
||||
|
||||
// Sections are a map of index to rows, since empty sections are omitted
|
||||
NSMutableArray *filteredSections = [NSMutableArray array];
|
||||
|
||||
[self.sections enumerateObjectsUsingBlock:^(FLEXTableViewSection<FLEXGlobalsEntry *> *section, NSUInteger idx, BOOL *stop) {
|
||||
section = [section newSectionWithRowsMatchingQuery:newText];
|
||||
if (section) {
|
||||
[filteredSections addObject:section];
|
||||
}
|
||||
}];
|
||||
|
||||
self.filteredSections = filteredSections.copy;
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
#pragma mark - Misc
|
||||
|
||||
- (void)donePressed:(id)sender
|
||||
{
|
||||
[self.delegate globalsViewControllerDidFinish:self];
|
||||
}
|
||||
|
||||
#pragma mark - Table Data Helpers
|
||||
|
||||
- (FLEXGlobalsEntry *)globalEntryAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
if (self.filteredSections) {
|
||||
return self.filteredSections[indexPath.section][indexPath.row];
|
||||
} else {
|
||||
return self.sections[indexPath.section][indexPath.row];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)titleForSection:(NSInteger)section
|
||||
{
|
||||
if (self.filteredSections) {
|
||||
return self.filteredSections[section].title;
|
||||
} else {
|
||||
return self.sections[section].title;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)titleForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
FLEXGlobalsEntry *entry = [self globalEntryAtIndexPath:indexPath];
|
||||
return entry.entryNameFuture();
|
||||
}
|
||||
|
||||
#pragma mark - Table View Data Source
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
|
||||
{
|
||||
return self.filteredSections ? self.filteredSections.count : self.sections.count;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
||||
{
|
||||
if (self.filteredSections) {
|
||||
return self.filteredSections[section].count;
|
||||
} else {
|
||||
return self.sections[section].count;
|
||||
}
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
static NSString *CellIdentifier = @"Cell";
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
|
||||
if (!cell) {
|
||||
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
cell.textLabel.font = [FLEXUtility defaultFontOfSize:14.0];
|
||||
}
|
||||
|
||||
cell.textLabel.text = [self titleForRowAtIndexPath:indexPath];
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
|
||||
{
|
||||
return [self titleForSection:section];
|
||||
}
|
||||
|
||||
#pragma mark - Table View Delegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
FLEXGlobalsEntry *entry = [self globalEntryAtIndexPath:indexPath];
|
||||
if (entry.viewControllerFuture) {
|
||||
[self.navigationController pushViewController:entry.viewControllerFuture() animated:YES];
|
||||
} else {
|
||||
entry.rowAction(self);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,141 @@
|
||||
//
|
||||
// FLEXKeychain.h
|
||||
//
|
||||
// Derived from:
|
||||
// SSKeychain.h in SSKeychain
|
||||
// Created by Sam Soffes on 5/19/10.
|
||||
// Copyright (c) 2010-2014 Sam Soffes. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/// Error code specific to FLEXKeychain that can be returned in NSError objects.
|
||||
/// For codes returned by the operating system, refer to SecBase.h for your
|
||||
/// platform.
|
||||
typedef NS_ENUM(OSStatus, FLEXKeychainErrorCode) {
|
||||
/// Some of the arguments were invalid.
|
||||
FLEXKeychainErrorBadArguments = -1001,
|
||||
};
|
||||
|
||||
/// FLEXKeychain error domain
|
||||
extern NSString *const kFLEXKeychainErrorDomain;
|
||||
|
||||
/// Account name.
|
||||
extern NSString *const kFLEXKeychainAccountKey;
|
||||
|
||||
/// Time the item was created.
|
||||
///
|
||||
/// The value will be a string.
|
||||
extern NSString *const kFLEXKeychainCreatedAtKey;
|
||||
|
||||
/// Item class.
|
||||
extern NSString *const kFLEXKeychainClassKey;
|
||||
|
||||
/// Item description.
|
||||
extern NSString *const kFLEXKeychainDescriptionKey;
|
||||
|
||||
/// Item label.
|
||||
extern NSString *const kFLEXKeychainLabelKey;
|
||||
|
||||
/// Time the item was last modified.
|
||||
///
|
||||
/// The value will be a string.
|
||||
extern NSString *const kFLEXKeychainLastModifiedKey;
|
||||
|
||||
/// Where the item was created.
|
||||
extern NSString *const kFLEXKeychainWhereKey;
|
||||
|
||||
/// A simple wrapper for accessing accounts, getting passwords,
|
||||
/// setting passwords, and deleting passwords using the system Keychain.
|
||||
@interface FLEXKeychain : NSObject
|
||||
|
||||
#pragma mark - Classic methods
|
||||
|
||||
/// @param serviceName The service for which to return the corresponding password.
|
||||
/// @param account The account for which to return the corresponding password.
|
||||
/// @return Returns a string containing the password for a given account and service,
|
||||
/// or `nil` if the Keychain doesn't have a password for the given parameters.
|
||||
+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account;
|
||||
+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
|
||||
|
||||
/// Returns a nsdata containing the password for a given account and service,
|
||||
/// or `nil` if the Keychain doesn't have a password for the given parameters.
|
||||
///
|
||||
/// @param serviceName The service for which to return the corresponding password.
|
||||
/// @param account The account for which to return the corresponding password.
|
||||
/// @return Returns a nsdata containing the password for a given account and service,
|
||||
/// or `nil` if the Keychain doesn't have a password for the given parameters.
|
||||
+ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account;
|
||||
+ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
|
||||
|
||||
|
||||
/// Deletes a password from the Keychain.
|
||||
///
|
||||
/// @param serviceName The service for which to delete the corresponding password.
|
||||
/// @param account The account for which to delete the corresponding password.
|
||||
/// @return Returns `YES` on success, or `NO` on failure.
|
||||
+ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account;
|
||||
+ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
|
||||
|
||||
|
||||
/// Sets a password in the Keychain.
|
||||
///
|
||||
/// @param password The password to store in the Keychain.
|
||||
/// @param serviceName The service for which to set the corresponding password.
|
||||
/// @param account The account for which to set the corresponding password.
|
||||
/// @return Returns `YES` on success, or `NO` on failure.
|
||||
+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account;
|
||||
+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
|
||||
|
||||
/// Sets a password in the Keychain.
|
||||
///
|
||||
/// @param password The password to store in the Keychain.
|
||||
/// @param serviceName The service for which to set the corresponding password.
|
||||
/// @param account The account for which to set the corresponding password.
|
||||
/// @return Returns `YES` on success, or `NO` on failure.
|
||||
+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account;
|
||||
+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
|
||||
|
||||
/// @return An array of dictionaries containing the Keychain's accounts, or `nil` if
|
||||
/// the Keychain doesn't have any accounts. The order of the objects in the array isn't defined.
|
||||
///
|
||||
/// @note See the `NSString` constants declared in FLEXKeychain.h for a list of keys that
|
||||
/// can be used when accessing the dictionaries returned by this method.
|
||||
+ (NSArray<NSDictionary<NSString *, id> *> *)allAccounts;
|
||||
+ (NSArray<NSDictionary<NSString *, id> *> *)allAccounts:(NSError *__autoreleasing *)error;
|
||||
|
||||
/// @param serviceName The service for which to return the corresponding accounts.
|
||||
/// @return An array of dictionaries containing the Keychain's accounts for a given `serviceName`,
|
||||
/// or `nil` if the Keychain doesn't have any accounts for the given `serviceName`.
|
||||
/// The order of the objects in the array isn't defined.
|
||||
///
|
||||
/// @note See the `NSString` constants declared in FLEXKeychain.h for a list of keys that
|
||||
/// can be used when accessing the dictionaries returned by this method.
|
||||
+ (NSArray<NSDictionary<NSString *, id> *> *)accountsForService:(NSString *)serviceName;
|
||||
+ (NSArray<NSDictionary<NSString *, id> *> *)accountsForService:(NSString *)serviceName error:(NSError *__autoreleasing *)error;
|
||||
|
||||
|
||||
#pragma mark - Configuration
|
||||
|
||||
#if __IPHONE_4_0 && TARGET_OS_IPHONE
|
||||
/// Returns the accessibility type for all future passwords saved to the Keychain.
|
||||
///
|
||||
/// @return `NULL` or one of the "Keychain Item Accessibility
|
||||
/// Constants" used for determining when a keychain item should be readable.
|
||||
+ (CFTypeRef)accessibilityType;
|
||||
|
||||
/// Sets the accessibility type for all future passwords saved to the Keychain.
|
||||
///
|
||||
/// @param accessibilityType One of the "Keychain Item Accessibility Constants"
|
||||
/// used for determining when a keychain item should be readable.
|
||||
/// If the value is `NULL` (the default), the Keychain default will be used which
|
||||
/// is highly insecure. You really should use at least `kSecAttrAccessibleAfterFirstUnlock`
|
||||
/// for background applications or `kSecAttrAccessibleWhenUnlocked` for all
|
||||
/// other applications.
|
||||
///
|
||||
/// @note See Security/SecItem.h
|
||||
+ (void)setAccessibilityType:(CFTypeRef)accessibilityType;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
//
|
||||
// FLEXKeychain.m
|
||||
//
|
||||
// Forked from:
|
||||
// SSKeychain.m in SSKeychain
|
||||
// Created by Sam Soffes on 5/19/10.
|
||||
// Copyright (c) 2010-2014 Sam Soffes. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXKeychain.h"
|
||||
#import "FLEXKeychainQuery.h"
|
||||
|
||||
NSString * const kFLEXKeychainErrorDomain = @"com.flipboard.flex";
|
||||
NSString * const kFLEXKeychainAccountKey = @"acct";
|
||||
NSString * const kFLEXKeychainCreatedAtKey = @"cdat";
|
||||
NSString * const kFLEXKeychainClassKey = @"labl";
|
||||
NSString * const kFLEXKeychainDescriptionKey = @"desc";
|
||||
NSString * const kFLEXKeychainLabelKey = @"labl";
|
||||
NSString * const kFLEXKeychainLastModifiedKey = @"mdat";
|
||||
NSString * const kFLEXKeychainWhereKey = @"svce";
|
||||
|
||||
#if __IPHONE_4_0 && TARGET_OS_IPHONE
|
||||
static CFTypeRef FLEXKeychainAccessibilityType = NULL;
|
||||
#endif
|
||||
|
||||
@implementation FLEXKeychain
|
||||
|
||||
+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account {
|
||||
return [self passwordForService:serviceName account:account error:nil];
|
||||
}
|
||||
|
||||
+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account error:(NSError *__autoreleasing *)error {
|
||||
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
|
||||
query.service = serviceName;
|
||||
query.account = account;
|
||||
[query fetch:error];
|
||||
return query.password;
|
||||
}
|
||||
|
||||
+ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account {
|
||||
return [self passwordDataForService:serviceName account:account error:nil];
|
||||
}
|
||||
|
||||
+ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error {
|
||||
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
|
||||
query.service = serviceName;
|
||||
query.account = account;
|
||||
[query fetch:error];
|
||||
|
||||
return query.passwordData;
|
||||
}
|
||||
|
||||
+ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account {
|
||||
return [self deletePasswordForService:serviceName account:account error:nil];
|
||||
}
|
||||
|
||||
+ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account error:(NSError *__autoreleasing *)error {
|
||||
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
|
||||
query.service = serviceName;
|
||||
query.account = account;
|
||||
return [query deleteItem:error];
|
||||
}
|
||||
|
||||
+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account {
|
||||
return [self setPassword:password forService:serviceName account:account error:nil];
|
||||
}
|
||||
|
||||
+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError *__autoreleasing *)error {
|
||||
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
|
||||
query.service = serviceName;
|
||||
query.account = account;
|
||||
query.password = password;
|
||||
return [query save:error];
|
||||
}
|
||||
|
||||
+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account {
|
||||
return [self setPasswordData:password forService:serviceName account:account error:nil];
|
||||
}
|
||||
|
||||
+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error {
|
||||
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
|
||||
query.service = serviceName;
|
||||
query.account = account;
|
||||
query.passwordData = password;
|
||||
return [query save:error];
|
||||
}
|
||||
|
||||
+ (NSArray *)allAccounts {
|
||||
return [self allAccounts:nil];
|
||||
}
|
||||
|
||||
+ (NSArray *)allAccounts:(NSError *__autoreleasing *)error {
|
||||
return [self accountsForService:nil error:error];
|
||||
}
|
||||
|
||||
+ (NSArray *)accountsForService:(NSString *)serviceName {
|
||||
return [self accountsForService:serviceName error:nil];
|
||||
}
|
||||
|
||||
+ (NSArray *)accountsForService:(NSString *)serviceName error:(NSError *__autoreleasing *)error {
|
||||
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
|
||||
query.service = serviceName;
|
||||
return [query fetchAll:error];
|
||||
}
|
||||
|
||||
#if __IPHONE_4_0 && TARGET_OS_IPHONE
|
||||
+ (CFTypeRef)accessibilityType {
|
||||
return FLEXKeychainAccessibilityType;
|
||||
}
|
||||
|
||||
+ (void)setAccessibilityType:(CFTypeRef)accessibilityType {
|
||||
CFRetain(accessibilityType);
|
||||
if (FLEXKeychainAccessibilityType) {
|
||||
CFRelease(FLEXKeychainAccessibilityType);
|
||||
}
|
||||
FLEXKeychainAccessibilityType = accessibilityType;
|
||||
}
|
||||
#endif
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,112 @@
|
||||
//
|
||||
// FLEXKeychainQuery.h
|
||||
//
|
||||
// Derived from:
|
||||
// SSKeychainQuery.h in SSKeychain
|
||||
// Created by Caleb Davenport on 3/19/13.
|
||||
// Copyright (c) 2010-2014 Sam Soffes. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Security/Security.h>
|
||||
|
||||
#if __IPHONE_7_0 || __MAC_10_9
|
||||
// Keychain synchronization available at compile time
|
||||
#define FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE 1
|
||||
#endif
|
||||
|
||||
#if __IPHONE_3_0 || __MAC_10_9
|
||||
// Keychain access group available at compile time
|
||||
#define FLEXKEYCHAIN_ACCESS_GROUP_AVAILABLE 1
|
||||
#endif
|
||||
|
||||
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
|
||||
typedef NS_ENUM(NSUInteger, FLEXKeychainQuerySynchronizationMode) {
|
||||
FLEXKeychainQuerySynchronizationModeAny,
|
||||
FLEXKeychainQuerySynchronizationModeNo,
|
||||
FLEXKeychainQuerySynchronizationModeYes
|
||||
};
|
||||
#endif
|
||||
|
||||
/// Simple interface for querying or modifying keychain items.
|
||||
@interface FLEXKeychainQuery : NSObject
|
||||
|
||||
/// kSecAttrAccount
|
||||
@property (nonatomic, copy) NSString *account;
|
||||
|
||||
/// kSecAttrService
|
||||
@property (nonatomic, copy) NSString *service;
|
||||
|
||||
/// kSecAttrLabel
|
||||
@property (nonatomic, copy) NSString *label;
|
||||
|
||||
#ifdef FLEXKEYCHAIN_ACCESS_GROUP_AVAILABLE
|
||||
/// kSecAttrAccessGroup (only used on iOS)
|
||||
@property (nonatomic, copy) NSString *accessGroup;
|
||||
#endif
|
||||
|
||||
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
|
||||
/// kSecAttrSynchronizable
|
||||
@property (nonatomic) FLEXKeychainQuerySynchronizationMode synchronizationMode;
|
||||
#endif
|
||||
|
||||
/// Root storage for password information
|
||||
@property (nonatomic, copy) NSData *passwordData;
|
||||
|
||||
/// This property automatically transitions between an object and the value of
|
||||
/// `passwordData` using NSKeyedArchiver and NSKeyedUnarchiver.
|
||||
@property (nonatomic, copy) id<NSCoding> passwordObject;
|
||||
|
||||
/// Convenience accessor for setting and getting a password string. Passes through
|
||||
/// to `passwordData` using UTF-8 string encoding.
|
||||
@property (nonatomic, copy) NSString *password;
|
||||
|
||||
|
||||
#pragma mark Saving & Deleting
|
||||
|
||||
/// Save the receiver's attributes as a keychain item. Existing items with the
|
||||
/// given account, service, and access group will first be deleted.
|
||||
///
|
||||
/// @param error Populated should an error occur.
|
||||
/// @return `YES` if saving was successful, `NO` otherwise.
|
||||
- (BOOL)save:(NSError **)error;
|
||||
|
||||
/// Delete keychain items that match the given account, service, and access group.
|
||||
///
|
||||
/// @param error Populated should an error occur.
|
||||
/// @return `YES` if saving was successful, `NO` otherwise.
|
||||
- (BOOL)deleteItem:(NSError **)error;
|
||||
|
||||
|
||||
#pragma mark Fetching
|
||||
|
||||
/// Fetch all keychain items that match the given account, service, and access
|
||||
/// group. The values of `password` and `passwordData` are ignored when fetching.
|
||||
///
|
||||
/// @param error Populated should an error occur.
|
||||
/// @return An array of dictionaries that represent all matching keychain items,
|
||||
/// or `nil` should an error occur. The order of the items is not determined.
|
||||
- (NSArray<NSDictionary<NSString *, id> *> *)fetchAll:(NSError **)error;
|
||||
|
||||
/// Fetch the keychain item that matches the given account, service, and access
|
||||
/// group. The `password` and `passwordData` properties will be populated unless
|
||||
/// an error occurs. The values of `password` and `passwordData` are ignored when
|
||||
/// fetching.
|
||||
///
|
||||
/// @param error Populated should an error occur.
|
||||
/// @return `YES` if fetching was successful, `NO` otherwise.
|
||||
- (BOOL)fetch:(NSError **)error;
|
||||
|
||||
|
||||
#pragma mark Synchronization Status
|
||||
|
||||
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
|
||||
/// Returns a boolean indicating if keychain synchronization is available on the device at runtime.
|
||||
/// The #define FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE is only for compile time.
|
||||
/// If you are checking for the presence of synchronization, you should use this method.
|
||||
///
|
||||
/// @return A value indicating if keychain synchronization is available
|
||||
+ (BOOL)isSynchronizationAvailable;
|
||||
#endif
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,304 @@
|
||||
//
|
||||
// FLEXKeychainQuery.m
|
||||
// FLEXKeychain
|
||||
//
|
||||
// Created by Caleb Davenport on 3/19/13.
|
||||
// Copyright (c) 2013-2014 Sam Soffes. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXKeychainQuery.h"
|
||||
#import "FLEXKeychain.h"
|
||||
|
||||
@implementation FLEXKeychainQuery
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (BOOL)save:(NSError *__autoreleasing *)error {
|
||||
OSStatus status = FLEXKeychainErrorBadArguments;
|
||||
if (!self.service || !self.account || !self.passwordData) {
|
||||
if (error) {
|
||||
*error = [self errorWithCode:status];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSMutableDictionary *query = nil;
|
||||
NSMutableDictionary * searchQuery = [self query];
|
||||
status = SecItemCopyMatching((__bridge CFDictionaryRef)searchQuery, nil);
|
||||
if (status == errSecSuccess) {//item already exists, update it!
|
||||
query = [[NSMutableDictionary alloc]init];
|
||||
query[(__bridge id)kSecValueData] = self.passwordData;
|
||||
#if __IPHONE_4_0 && TARGET_OS_IPHONE
|
||||
CFTypeRef accessibilityType = [FLEXKeychain accessibilityType];
|
||||
if (accessibilityType) {
|
||||
query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType;
|
||||
}
|
||||
#endif
|
||||
status = SecItemUpdate((__bridge CFDictionaryRef)(searchQuery), (__bridge CFDictionaryRef)(query));
|
||||
}else if (status == errSecItemNotFound){//item not found, create it!
|
||||
query = [self query];
|
||||
if (self.label) {
|
||||
query[(__bridge id)kSecAttrLabel] = self.label;
|
||||
}
|
||||
query[(__bridge id)kSecValueData] = self.passwordData;
|
||||
#if __IPHONE_4_0 && TARGET_OS_IPHONE
|
||||
CFTypeRef accessibilityType = [FLEXKeychain accessibilityType];
|
||||
if (accessibilityType) {
|
||||
query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType;
|
||||
}
|
||||
#endif
|
||||
status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
|
||||
}
|
||||
|
||||
if (status != errSecSuccess && error != NULL) {
|
||||
*error = [self errorWithCode:status];
|
||||
}
|
||||
|
||||
return (status == errSecSuccess);
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)deleteItem:(NSError *__autoreleasing *)error {
|
||||
OSStatus status = FLEXKeychainErrorBadArguments;
|
||||
if (!self.service || !self.account) {
|
||||
if (error) {
|
||||
*error = [self errorWithCode:status];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSMutableDictionary *query = [self query];
|
||||
#if TARGET_OS_IPHONE
|
||||
status = SecItemDelete((__bridge CFDictionaryRef)query);
|
||||
#else
|
||||
// On Mac OS, SecItemDelete will not delete a key created in a different
|
||||
// app, nor in a different version of the same app.
|
||||
//
|
||||
// To replicate the issue, save a password, change to the code and
|
||||
// rebuild the app, and then attempt to delete that password.
|
||||
//
|
||||
// This was true in OS X 10.6 and probably later versions as well.
|
||||
//
|
||||
// Work around it by using SecItemCopyMatching and SecKeychainItemDelete.
|
||||
CFTypeRef result = NULL;
|
||||
query[(__bridge id)kSecReturnRef] = @YES;
|
||||
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
|
||||
if (status == errSecSuccess) {
|
||||
status = SecKeychainItemDelete((SecKeychainItemRef)result);
|
||||
CFRelease(result);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (status != errSecSuccess && error != NULL) {
|
||||
*error = [self errorWithCode:status];
|
||||
}
|
||||
|
||||
return (status == errSecSuccess);
|
||||
}
|
||||
|
||||
|
||||
- (NSArray *)fetchAll:(NSError *__autoreleasing *)error {
|
||||
NSMutableDictionary *query = [self query];
|
||||
query[(__bridge id)kSecReturnAttributes] = @YES;
|
||||
query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitAll;
|
||||
#if __IPHONE_4_0 && TARGET_OS_IPHONE
|
||||
CFTypeRef accessibilityType = [FLEXKeychain accessibilityType];
|
||||
if (accessibilityType) {
|
||||
query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType;
|
||||
}
|
||||
#endif
|
||||
|
||||
CFTypeRef result = NULL;
|
||||
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
|
||||
if (status != errSecSuccess && error != NULL) {
|
||||
*error = [self errorWithCode:status];
|
||||
return nil;
|
||||
}
|
||||
|
||||
return (__bridge_transfer NSArray *)result;
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)fetch:(NSError *__autoreleasing *)error {
|
||||
OSStatus status = FLEXKeychainErrorBadArguments;
|
||||
if (!self.service || !self.account) {
|
||||
if (error) {
|
||||
*error = [self errorWithCode:status];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
CFTypeRef result = NULL;
|
||||
NSMutableDictionary *query = [self query];
|
||||
query[(__bridge id)kSecReturnData] = @YES;
|
||||
query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
|
||||
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
|
||||
|
||||
if (status != errSecSuccess) {
|
||||
if (error) {
|
||||
*error = [self errorWithCode:status];
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
self.passwordData = (__bridge_transfer NSData *)result;
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Accessors
|
||||
|
||||
- (void)setPasswordObject:(id<NSCoding>)object {
|
||||
self.passwordData = [NSKeyedArchiver archivedDataWithRootObject:object];
|
||||
}
|
||||
|
||||
|
||||
- (id<NSCoding>)passwordObject {
|
||||
if (self.passwordData.length) {
|
||||
return [NSKeyedUnarchiver unarchiveObjectWithData:self.passwordData];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
- (void)setPassword:(NSString *)password {
|
||||
self.passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)password {
|
||||
if (self.passwordData.length) {
|
||||
return [NSString stringWithCString:self.passwordData.bytes encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Synchronization Status
|
||||
|
||||
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
|
||||
+ (BOOL)isSynchronizationAvailable {
|
||||
#if TARGET_OS_IPHONE
|
||||
return YES;
|
||||
#else
|
||||
return floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_8_4;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (NSMutableDictionary *)query {
|
||||
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
|
||||
dictionary[(__bridge id)kSecClass] = (__bridge id)kSecClassGenericPassword;
|
||||
|
||||
if (self.service) {
|
||||
dictionary[(__bridge id)kSecAttrService] = self.service;
|
||||
}
|
||||
|
||||
if (self.account) {
|
||||
dictionary[(__bridge id)kSecAttrAccount] = self.account;
|
||||
}
|
||||
|
||||
#ifdef FLEXKEYCHAIN_ACCESS_GROUP_AVAILABLE
|
||||
#if !TARGET_IPHONE_SIMULATOR
|
||||
if (self.accessGroup) {
|
||||
dictionary[(__bridge id)kSecAttrAccessGroup] = self.accessGroup;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
|
||||
if ([[self class] isSynchronizationAvailable]) {
|
||||
id value;
|
||||
|
||||
switch (self.synchronizationMode) {
|
||||
case FLEXKeychainQuerySynchronizationModeNo: {
|
||||
value = @NO;
|
||||
break;
|
||||
}
|
||||
case FLEXKeychainQuerySynchronizationModeYes: {
|
||||
value = @YES;
|
||||
break;
|
||||
}
|
||||
case FLEXKeychainQuerySynchronizationModeAny: {
|
||||
value = (__bridge id)(kSecAttrSynchronizableAny);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dictionary[(__bridge id)(kSecAttrSynchronizable)] = value;
|
||||
}
|
||||
#endif
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
- (NSError *)errorWithCode:(OSStatus)code {
|
||||
static dispatch_once_t onceToken;
|
||||
static NSBundle *resourcesBundle = nil;
|
||||
dispatch_once(&onceToken, ^{
|
||||
NSURL *url = [[NSBundle bundleForClass:[self class]] URLForResource:@"FLEXKeychain" withExtension:@"bundle"];
|
||||
resourcesBundle = [NSBundle bundleWithURL:url];
|
||||
});
|
||||
|
||||
NSString *message = nil;
|
||||
switch (code) {
|
||||
case errSecSuccess: return nil;
|
||||
case FLEXKeychainErrorBadArguments: message = NSLocalizedStringFromTableInBundle(@"FLEXKeychainErrorBadArguments", @"FLEXKeychain", resourcesBundle, nil); break;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
case errSecUnimplemented: {
|
||||
message = NSLocalizedStringFromTableInBundle(@"errSecUnimplemented", @"FLEXKeychain", resourcesBundle, nil);
|
||||
break;
|
||||
}
|
||||
case errSecParam: {
|
||||
message = NSLocalizedStringFromTableInBundle(@"errSecParam", @"FLEXKeychain", resourcesBundle, nil);
|
||||
break;
|
||||
}
|
||||
case errSecAllocate: {
|
||||
message = NSLocalizedStringFromTableInBundle(@"errSecAllocate", @"FLEXKeychain", resourcesBundle, nil);
|
||||
break;
|
||||
}
|
||||
case errSecNotAvailable: {
|
||||
message = NSLocalizedStringFromTableInBundle(@"errSecNotAvailable", @"FLEXKeychain", resourcesBundle, nil);
|
||||
break;
|
||||
}
|
||||
case errSecDuplicateItem: {
|
||||
message = NSLocalizedStringFromTableInBundle(@"errSecDuplicateItem", @"FLEXKeychain", resourcesBundle, nil);
|
||||
break;
|
||||
}
|
||||
case errSecItemNotFound: {
|
||||
message = NSLocalizedStringFromTableInBundle(@"errSecItemNotFound", @"FLEXKeychain", resourcesBundle, nil);
|
||||
break;
|
||||
}
|
||||
case errSecInteractionNotAllowed: {
|
||||
message = NSLocalizedStringFromTableInBundle(@"errSecInteractionNotAllowed", @"FLEXKeychain", resourcesBundle, nil);
|
||||
break;
|
||||
}
|
||||
case errSecDecode: {
|
||||
message = NSLocalizedStringFromTableInBundle(@"errSecDecode", @"FLEXKeychain", resourcesBundle, nil);
|
||||
break;
|
||||
}
|
||||
case errSecAuthFailed: {
|
||||
message = NSLocalizedStringFromTableInBundle(@"errSecAuthFailed", @"FLEXKeychain", resourcesBundle, nil);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
message = NSLocalizedStringFromTableInBundle(@"errSecDefault", @"FLEXKeychain", resourcesBundle, nil);
|
||||
}
|
||||
#else
|
||||
default:
|
||||
message = (__bridge_transfer NSString *)SecCopyErrorMessageString(code, NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
NSDictionary *userInfo = message ? @{ NSLocalizedDescriptionKey : message } : nil;
|
||||
return [NSError errorWithDomain:kFLEXKeychainErrorDomain code:code userInfo:userInfo];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// FLEXKeychainTableViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by ray on 2019/8/17.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
#import "FLEXTableViewController.h"
|
||||
|
||||
@interface FLEXKeychainTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,213 @@
|
||||
//
|
||||
// FLEXKeychainTableViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by ray on 2019/8/17.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXKeychain.h"
|
||||
#import "FLEXKeychainQuery.h"
|
||||
#import "FLEXKeychainTableViewController.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "UIPasteboard+FLEX.h"
|
||||
|
||||
@interface FLEXKeychainTableViewController ()
|
||||
|
||||
@property (nonatomic) NSMutableArray<NSDictionary *> *keychainItems;
|
||||
@property (nonatomic) NSString *headerTitle;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXKeychainTableViewController
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.navigationItem.rightBarButtonItems = @[
|
||||
[[UIBarButtonItem alloc]
|
||||
initWithBarButtonSystemItem:UIBarButtonSystemItemTrash target:self action:@selector(trashPressed)
|
||||
],
|
||||
[[UIBarButtonItem alloc]
|
||||
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addPressed)
|
||||
],
|
||||
];
|
||||
|
||||
[self refreshkeychainItems];
|
||||
[self updateHeaderTitle];
|
||||
}
|
||||
|
||||
- (void)refreshkeychainItems
|
||||
{
|
||||
self.keychainItems = [FLEXKeychain allAccounts].mutableCopy;
|
||||
}
|
||||
|
||||
- (void)updateHeaderTitle
|
||||
{
|
||||
self.headerTitle = [NSString stringWithFormat:@"%@ items", @(self.keychainItems.count)];
|
||||
}
|
||||
|
||||
- (FLEXKeychainQuery *)queryForItemAtIndex:(NSInteger)idx
|
||||
{
|
||||
NSDictionary *item = self.keychainItems[idx];
|
||||
|
||||
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
|
||||
query.service = item[kFLEXKeychainWhereKey];
|
||||
query.account = item[kFLEXKeychainAccountKey];
|
||||
[query fetch:nil];
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
- (void)deleteItem:(NSDictionary *)item
|
||||
{
|
||||
NSError *error = nil;
|
||||
BOOL success = [FLEXKeychain
|
||||
deletePasswordForService:item[kFLEXKeychainWhereKey]
|
||||
account:item[kFLEXKeychainAccountKey]
|
||||
error:&error
|
||||
];
|
||||
|
||||
if (!success) {
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Error Deleting Item");
|
||||
make.message(error.localizedDescription);
|
||||
} showFrom:self];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Buttons
|
||||
|
||||
- (void)trashPressed
|
||||
{
|
||||
[FLEXAlert makeSheet:^(FLEXAlert *make) {
|
||||
make.title(@"Clear Keychain");
|
||||
make.message(@"This will remove all keychain items for this app.\n");
|
||||
make.message(@"This action cannot be undone. Are you sure?");
|
||||
make.button(@"Yes, clear the keychain").destructiveStyle().handler(^(NSArray *strings) {
|
||||
for (id account in self.keychainItems) {
|
||||
[self deleteItem:account];
|
||||
}
|
||||
|
||||
[self refreshkeychainItems];
|
||||
[self.tableView reloadData];
|
||||
});
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
- (void)addPressed
|
||||
{
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Add Keychain Item");
|
||||
make.textField(@"Service name, i.e. Instagram");
|
||||
make.textField(@"Account, i.e. username@example.com");
|
||||
make.textField(@"Password");
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
make.button(@"Save").handler(^(NSArray<NSString *> *strings) {
|
||||
// Display errors
|
||||
NSError *error = nil;
|
||||
if (![FLEXKeychain setPassword:strings[2] forService:strings[0] account:strings[1] error:&error]) {
|
||||
[FLEXAlert showAlert:@"Error" message:error.localizedDescription from:self];
|
||||
}
|
||||
|
||||
[self refreshkeychainItems];
|
||||
[self.tableView reloadData];
|
||||
});
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - FLEXGlobalsEntry
|
||||
|
||||
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row
|
||||
{
|
||||
return @"🔑 Keychain";
|
||||
}
|
||||
|
||||
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
|
||||
FLEXKeychainTableViewController *viewController = [self new];
|
||||
viewController.title = [self globalsEntryTitle:row];
|
||||
|
||||
return viewController;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Data Source
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
||||
{
|
||||
return self.keychainItems.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
static NSString *CellIdentifier = @"Cell";
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
|
||||
if (!cell) {
|
||||
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
|
||||
}
|
||||
|
||||
NSDictionary *item = self.keychainItems[indexPath.row];
|
||||
id account = item[kFLEXKeychainAccountKey];
|
||||
if ([account isKindOfClass:[NSString class]]) {
|
||||
cell.textLabel.text = account;
|
||||
} else {
|
||||
cell.textLabel.text = [NSString stringWithFormat:
|
||||
@"[%@]\n\n%@",
|
||||
NSStringFromClass([account class]),
|
||||
[account description]
|
||||
];
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
|
||||
{
|
||||
return self.headerTitle;
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tv commitEditingStyle:(UITableViewCellEditingStyle)style forRowAtIndexPath:(NSIndexPath *)ip
|
||||
{
|
||||
if (style == UITableViewCellEditingStyleDelete) {
|
||||
[self deleteItem:self.keychainItems[ip.row]];
|
||||
[self.keychainItems removeObjectAtIndex:ip.row];
|
||||
[tv deleteRowsAtIndexPaths:@[ip] withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Delegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
FLEXKeychainQuery *query = [self queryForItemAtIndex:indexPath.row];
|
||||
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(query.service);
|
||||
make.message(@"Service: ").message(query.service);
|
||||
make.message(@"\nAccount: ").message(query.account);
|
||||
make.message(@"\nPassword: ").message(query.password);
|
||||
|
||||
make.button(@"Copy Service").handler(^(NSArray<NSString *> *strings) {
|
||||
[UIPasteboard.generalPasteboard flex_copy:query.service];
|
||||
});
|
||||
make.button(@"Copy Account").handler(^(NSArray<NSString *> *strings) {
|
||||
[UIPasteboard.generalPasteboard flex_copy:query.account];
|
||||
});
|
||||
make.button(@"Copy Password").handler(^(NSArray<NSString *> *strings) {
|
||||
[UIPasteboard.generalPasteboard flex_copy:query.password];
|
||||
});
|
||||
make.button(@"Dismiss").cancelStyle();
|
||||
|
||||
} showFrom:self];
|
||||
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2010-2012 Sam Soffes.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,207 @@
|
||||
//
|
||||
// Taken from https://github.com/llvm-mirror/lldb/blob/master/tools/debugserver/source/MacOSX/DarwinLog/ActivityStreamSPI.h
|
||||
// by Tanner Bennett on 03/03/2019 with minimal modifications.
|
||||
//
|
||||
|
||||
//===-- ActivityStreamAPI.h -------------------------------------*- C++ -*-===//
|
||||
//
|
||||
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
||||
// See https://llvm.org/LICENSE.txt for license information.
|
||||
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#ifndef ActivityStreamSPI_h
|
||||
#define ActivityStreamSPI_h
|
||||
|
||||
#include <sys/time.h>
|
||||
// #include <xpc/xpc.h>
|
||||
|
||||
/* By default, XPC objects are declared as Objective-C types when building with
|
||||
* an Objective-C compiler. This allows them to participate in ARC, in RR
|
||||
* management by the Blocks runtime and in leaks checking by the static
|
||||
* analyzer, and enables them to be added to Cocoa collections.
|
||||
*
|
||||
* See <os/object.h> for details.
|
||||
*/
|
||||
#if !TARGET_OS_MACCATALYST
|
||||
#if OS_OBJECT_USE_OBJC
|
||||
OS_OBJECT_DECL(xpc_object);
|
||||
#else
|
||||
typedef void * xpc_object_t;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#define OS_ACTIVITY_MAX_CALLSTACK 32
|
||||
|
||||
// Enums
|
||||
|
||||
typedef NS_ENUM(uint32_t, os_activity_stream_flag_t) {
|
||||
OS_ACTIVITY_STREAM_PROCESS_ONLY = 0x00000001,
|
||||
OS_ACTIVITY_STREAM_SKIP_DECODE = 0x00000002,
|
||||
OS_ACTIVITY_STREAM_PAYLOAD = 0x00000004,
|
||||
OS_ACTIVITY_STREAM_HISTORICAL = 0x00000008,
|
||||
OS_ACTIVITY_STREAM_CALLSTACK = 0x00000010,
|
||||
OS_ACTIVITY_STREAM_DEBUG = 0x00000020,
|
||||
OS_ACTIVITY_STREAM_BUFFERED = 0x00000040,
|
||||
OS_ACTIVITY_STREAM_NO_SENSITIVE = 0x00000080,
|
||||
OS_ACTIVITY_STREAM_INFO = 0x00000100,
|
||||
OS_ACTIVITY_STREAM_PROMISCUOUS = 0x00000200,
|
||||
OS_ACTIVITY_STREAM_PRECISE_TIMESTAMPS = 0x00000200
|
||||
};
|
||||
|
||||
typedef NS_ENUM(uint32_t, os_activity_stream_type_t) {
|
||||
OS_ACTIVITY_STREAM_TYPE_ACTIVITY_CREATE = 0x0201,
|
||||
OS_ACTIVITY_STREAM_TYPE_ACTIVITY_TRANSITION = 0x0202,
|
||||
OS_ACTIVITY_STREAM_TYPE_ACTIVITY_USERACTION = 0x0203,
|
||||
|
||||
OS_ACTIVITY_STREAM_TYPE_TRACE_MESSAGE = 0x0300,
|
||||
|
||||
OS_ACTIVITY_STREAM_TYPE_LOG_MESSAGE = 0x0400,
|
||||
OS_ACTIVITY_STREAM_TYPE_LEGACY_LOG_MESSAGE = 0x0480,
|
||||
|
||||
OS_ACTIVITY_STREAM_TYPE_SIGNPOST_BEGIN = 0x0601,
|
||||
OS_ACTIVITY_STREAM_TYPE_SIGNPOST_END = 0x0602,
|
||||
OS_ACTIVITY_STREAM_TYPE_SIGNPOST_EVENT = 0x0603,
|
||||
|
||||
OS_ACTIVITY_STREAM_TYPE_STATEDUMP_EVENT = 0x0A00,
|
||||
};
|
||||
|
||||
typedef NS_ENUM(uint32_t, os_activity_stream_event_t) {
|
||||
OS_ACTIVITY_STREAM_EVENT_STARTED = 1,
|
||||
OS_ACTIVITY_STREAM_EVENT_STOPPED = 2,
|
||||
OS_ACTIVITY_STREAM_EVENT_FAILED = 3,
|
||||
OS_ACTIVITY_STREAM_EVENT_CHUNK_STARTED = 4,
|
||||
OS_ACTIVITY_STREAM_EVENT_CHUNK_FINISHED = 5,
|
||||
};
|
||||
|
||||
// Types
|
||||
|
||||
typedef uint64_t os_activity_id_t;
|
||||
typedef struct os_activity_stream_s *os_activity_stream_t;
|
||||
typedef struct os_activity_stream_entry_s *os_activity_stream_entry_t;
|
||||
|
||||
#define OS_ACTIVITY_STREAM_COMMON() \
|
||||
uint64_t trace_id; \
|
||||
uint64_t timestamp; \
|
||||
uint64_t thread; \
|
||||
const uint8_t *image_uuid; \
|
||||
const char *image_path; \
|
||||
struct timeval tv_gmt; \
|
||||
struct timezone tz; \
|
||||
uint32_t offset
|
||||
|
||||
typedef struct os_activity_stream_common_s {
|
||||
OS_ACTIVITY_STREAM_COMMON();
|
||||
} * os_activity_stream_common_t;
|
||||
|
||||
struct os_activity_create_s {
|
||||
OS_ACTIVITY_STREAM_COMMON();
|
||||
const char *name;
|
||||
os_activity_id_t creator_aid;
|
||||
uint64_t unique_pid;
|
||||
};
|
||||
|
||||
struct os_activity_transition_s {
|
||||
OS_ACTIVITY_STREAM_COMMON();
|
||||
os_activity_id_t transition_id;
|
||||
};
|
||||
|
||||
typedef struct os_log_message_s {
|
||||
OS_ACTIVITY_STREAM_COMMON();
|
||||
const char *format;
|
||||
const uint8_t *buffer;
|
||||
size_t buffer_sz;
|
||||
const uint8_t *privdata;
|
||||
size_t privdata_sz;
|
||||
const char *subsystem;
|
||||
const char *category;
|
||||
uint32_t oversize_id;
|
||||
uint8_t ttl;
|
||||
bool persisted;
|
||||
} * os_log_message_t;
|
||||
|
||||
typedef struct os_trace_message_v2_s {
|
||||
OS_ACTIVITY_STREAM_COMMON();
|
||||
const char *format;
|
||||
const void *buffer;
|
||||
size_t bufferLen;
|
||||
xpc_object_t __unsafe_unretained payload;
|
||||
} * os_trace_message_v2_t;
|
||||
|
||||
typedef struct os_activity_useraction_s {
|
||||
OS_ACTIVITY_STREAM_COMMON();
|
||||
const char *action;
|
||||
bool persisted;
|
||||
} * os_activity_useraction_t;
|
||||
|
||||
typedef struct os_signpost_s {
|
||||
OS_ACTIVITY_STREAM_COMMON();
|
||||
const char *format;
|
||||
const uint8_t *buffer;
|
||||
size_t buffer_sz;
|
||||
const uint8_t *privdata;
|
||||
size_t privdata_sz;
|
||||
const char *subsystem;
|
||||
const char *category;
|
||||
uint64_t duration_nsec;
|
||||
uint32_t callstack_depth;
|
||||
uint64_t callstack[OS_ACTIVITY_MAX_CALLSTACK];
|
||||
} * os_signpost_t;
|
||||
|
||||
typedef struct os_activity_statedump_s {
|
||||
OS_ACTIVITY_STREAM_COMMON();
|
||||
char *message;
|
||||
size_t message_size;
|
||||
char image_path_buffer[PATH_MAX];
|
||||
} * os_activity_statedump_t;
|
||||
|
||||
struct os_activity_stream_entry_s {
|
||||
os_activity_stream_type_t type;
|
||||
|
||||
// information about the process streaming the data
|
||||
pid_t pid;
|
||||
uint64_t proc_id;
|
||||
const uint8_t *proc_imageuuid;
|
||||
const char *proc_imagepath;
|
||||
|
||||
// the activity associated with this streamed event
|
||||
os_activity_id_t activity_id;
|
||||
os_activity_id_t parent_id;
|
||||
|
||||
union {
|
||||
struct os_activity_stream_common_s common;
|
||||
struct os_activity_create_s activity_create;
|
||||
struct os_activity_transition_s activity_transition;
|
||||
struct os_log_message_s log_message;
|
||||
struct os_trace_message_v2_s trace_message;
|
||||
struct os_activity_useraction_s useraction;
|
||||
struct os_signpost_s signpost;
|
||||
struct os_activity_statedump_s statedump;
|
||||
};
|
||||
};
|
||||
|
||||
// Blocks
|
||||
|
||||
typedef bool (^os_activity_stream_block_t)(os_activity_stream_entry_t entry,
|
||||
int error);
|
||||
|
||||
typedef void (^os_activity_stream_event_block_t)(
|
||||
os_activity_stream_t stream, os_activity_stream_event_t event);
|
||||
|
||||
// SPI entry point prototypes
|
||||
|
||||
typedef os_activity_stream_t (*os_activity_stream_for_pid_t)(
|
||||
pid_t pid, os_activity_stream_flag_t flags,
|
||||
os_activity_stream_block_t stream_block);
|
||||
|
||||
typedef void (*os_activity_stream_resume_t)(os_activity_stream_t stream);
|
||||
|
||||
typedef void (*os_activity_stream_cancel_t)(os_activity_stream_t stream);
|
||||
|
||||
typedef char *(*os_log_copy_formatted_message_t)(os_log_message_t log_message);
|
||||
|
||||
typedef void (*os_activity_stream_set_event_handler_t)(
|
||||
os_activity_stream_t stream, os_activity_stream_event_block_t block);
|
||||
|
||||
#endif /* ActivityStreamSPI_h */
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// FLEXASLLogController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/14/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXLogController.h"
|
||||
|
||||
@interface FLEXASLLogController : NSObject <FLEXLogController>
|
||||
|
||||
/// Guaranteed to call back on the main thread.
|
||||
+ (instancetype)withUpdateHandler:(void(^)(NSArray<FLEXSystemLogMessage *> *newMessages))newMessagesHandler;
|
||||
|
||||
- (BOOL)startMonitoring;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,154 @@
|
||||
//
|
||||
// FLEXASLLogController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/14/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXASLLogController.h"
|
||||
#import <asl.h>
|
||||
|
||||
// Querying the ASL is much slower in the simulator. We need a longer polling interval to keep things responsive.
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
#define updateInterval 5.0
|
||||
#else
|
||||
#define updateInterval 1.0
|
||||
#endif
|
||||
|
||||
@interface FLEXASLLogController ()
|
||||
|
||||
@property (nonatomic, readonly) void (^updateHandler)(NSArray<FLEXSystemLogMessage *> *);
|
||||
|
||||
@property (nonatomic) NSTimer *logUpdateTimer;
|
||||
@property (nonatomic, readonly) NSMutableIndexSet *logMessageIdentifiers;
|
||||
|
||||
// ASL stuff
|
||||
|
||||
@property (nonatomic) NSUInteger heapSize;
|
||||
@property (nonatomic) dispatch_queue_t logQueue;
|
||||
@property (nonatomic) dispatch_io_t io;
|
||||
@property (nonatomic) NSString *remaining;
|
||||
@property (nonatomic) int stderror;
|
||||
@property (nonatomic) NSString *lastTimestamp;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXASLLogController
|
||||
|
||||
+ (instancetype)withUpdateHandler:(void(^)(NSArray<FLEXSystemLogMessage *> *newMessages))newMessagesHandler
|
||||
{
|
||||
return [[self alloc] initWithUpdateHandler:newMessagesHandler];
|
||||
}
|
||||
|
||||
- (id)initWithUpdateHandler:(void(^)(NSArray<FLEXSystemLogMessage *> *newMessages))newMessagesHandler
|
||||
{
|
||||
NSParameterAssert(newMessagesHandler);
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_updateHandler = newMessagesHandler;
|
||||
_logMessageIdentifiers = [NSMutableIndexSet indexSet];
|
||||
self.logUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:updateInterval
|
||||
target:self
|
||||
selector:@selector(updateLogMessages)
|
||||
userInfo:nil
|
||||
repeats:YES];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self.logUpdateTimer invalidate];
|
||||
}
|
||||
|
||||
- (BOOL)startMonitoring {
|
||||
[self.logUpdateTimer fire];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)updateLogMessages
|
||||
{
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSArray<FLEXSystemLogMessage *> *newMessages;
|
||||
@synchronized (self) {
|
||||
newMessages = [self newLogMessagesForCurrentProcess];
|
||||
if (!newMessages.count) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (FLEXSystemLogMessage *message in newMessages) {
|
||||
[self.logMessageIdentifiers addIndex:(NSUInteger)message.messageID];
|
||||
}
|
||||
|
||||
self.lastTimestamp = @(asl_get(newMessages.lastObject.aslMessage, ASL_KEY_TIME) ?: "null");
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.updateHandler(newMessages);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Log Message Fetching
|
||||
|
||||
- (NSArray<FLEXSystemLogMessage *> *)newLogMessagesForCurrentProcess
|
||||
{
|
||||
if (!self.logMessageIdentifiers.count) {
|
||||
return [self allLogMessagesForCurrentProcess];
|
||||
}
|
||||
|
||||
aslresponse response = [self ASLMessageListForCurrentProcess];
|
||||
aslmsg aslMessage = NULL;
|
||||
|
||||
NSMutableArray<FLEXSystemLogMessage *> *newMessages = [NSMutableArray array];
|
||||
|
||||
while ((aslMessage = asl_next(response))) {
|
||||
NSUInteger messageID = (NSUInteger)atoll(asl_get(aslMessage, ASL_KEY_MSG_ID));
|
||||
if (![self.logMessageIdentifiers containsIndex:messageID]) {
|
||||
[newMessages addObject:[FLEXSystemLogMessage logMessageFromASLMessage:aslMessage]];
|
||||
}
|
||||
}
|
||||
|
||||
asl_release(response);
|
||||
return newMessages;
|
||||
}
|
||||
|
||||
- (aslresponse)ASLMessageListForCurrentProcess
|
||||
{
|
||||
static NSString *pidString = nil;
|
||||
if (!pidString) {
|
||||
pidString = @([NSProcessInfo.processInfo processIdentifier]).stringValue;
|
||||
}
|
||||
|
||||
// Create system log query object.
|
||||
asl_object_t query = asl_new(ASL_TYPE_QUERY);
|
||||
|
||||
// Filter for messages from the current process.
|
||||
// Note that this appears to happen by default on device, but is required in the simulator.
|
||||
asl_set_query(query, ASL_KEY_PID, pidString.UTF8String, ASL_QUERY_OP_EQUAL);
|
||||
// Filter for messages after the last retrieved message.
|
||||
if (self.lastTimestamp) {
|
||||
asl_set_query(query, ASL_KEY_TIME, self.lastTimestamp.UTF8String, ASL_QUERY_OP_GREATER);
|
||||
}
|
||||
|
||||
return asl_search(NULL, query);
|
||||
}
|
||||
|
||||
- (NSArray<FLEXSystemLogMessage *> *)allLogMessagesForCurrentProcess
|
||||
{
|
||||
aslresponse response = [self ASLMessageListForCurrentProcess];
|
||||
aslmsg aslMessage = NULL;
|
||||
|
||||
NSMutableArray<FLEXSystemLogMessage *> *logMessages = [NSMutableArray array];
|
||||
while ((aslMessage = asl_next(response))) {
|
||||
[logMessages addObject:[FLEXSystemLogMessage logMessageFromASLMessage:aslMessage]];
|
||||
}
|
||||
asl_release(response);
|
||||
|
||||
return logMessages;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// FLEXLogController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/17/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "FLEXSystemLogMessage.h"
|
||||
|
||||
@protocol FLEXLogController <NSObject>
|
||||
|
||||
/// Guaranteed to call back on the main thread.
|
||||
+ (instancetype)withUpdateHandler:(void(^)(NSArray<FLEXSystemLogMessage *> *newMessages))newMessagesHandler;
|
||||
|
||||
- (BOOL)startMonitoring;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// FLEXOSLogController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 12/19/18.
|
||||
// Copyright © 2018 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXLogController.h"
|
||||
|
||||
#define FLEXOSLogAvailable() (NSProcessInfo.processInfo.operatingSystemVersion.majorVersion >= 10)
|
||||
|
||||
extern NSString * const kFLEXiOSPersistentOSLogKey;
|
||||
|
||||
/// The log controller used for iOS 10 and up.
|
||||
@interface FLEXOSLogController : NSObject <FLEXLogController>
|
||||
|
||||
+ (instancetype)withUpdateHandler:(void(^)(NSArray<FLEXSystemLogMessage *> *newMessages))newMessagesHandler;
|
||||
|
||||
- (BOOL)startMonitoring;
|
||||
|
||||
/// Whether log messages are to be recorded and kept in-memory in the background.
|
||||
/// You do not need to initialize this value, only change it.
|
||||
@property (nonatomic) BOOL persistent;
|
||||
/// Used mostly internally, but also used by the log VC to persist messages
|
||||
/// that were created prior to enabling persistence.
|
||||
@property (nonatomic) NSMutableArray<FLEXSystemLogMessage *> *messages;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,220 @@
|
||||
//
|
||||
// FLEXOSLogController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 12/19/18.
|
||||
// Copyright © 2018 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXOSLogController.h"
|
||||
#include <dlfcn.h>
|
||||
#include "ActivityStreamAPI.h"
|
||||
|
||||
NSString * const kFLEXiOSPersistentOSLogKey = @"com.flex.enablePersistentOSLogLogging";
|
||||
|
||||
static os_activity_stream_for_pid_t OSActivityStreamForPID;
|
||||
static os_activity_stream_resume_t OSActivityStreamResume;
|
||||
static os_activity_stream_cancel_t OSActivityStreamCancel;
|
||||
static os_log_copy_formatted_message_t OSLogCopyFormattedMessage;
|
||||
static os_activity_stream_set_event_handler_t OSActivityStreamSetEventHandler;
|
||||
static int (*proc_name)(int, char *, unsigned int);
|
||||
static int (*proc_listpids)(uint32_t, uint32_t, void*, int);
|
||||
static uint8_t (*OSLogGetType)(void *);
|
||||
|
||||
@interface FLEXOSLogController ()
|
||||
|
||||
+ (FLEXOSLogController *)sharedLogController;
|
||||
|
||||
@property (nonatomic) void (^updateHandler)(NSArray<FLEXSystemLogMessage *> *);
|
||||
|
||||
@property (nonatomic) BOOL canPrint;
|
||||
@property (nonatomic) int filterPid;
|
||||
@property (nonatomic) BOOL levelInfo;
|
||||
@property (nonatomic) BOOL subsystemInfo;
|
||||
|
||||
@property (nonatomic) os_activity_stream_t stream;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXOSLogController
|
||||
|
||||
+ (void)load
|
||||
{
|
||||
// Persist logs when the app launches on iOS 10 if we have persistent logs turned on
|
||||
if (FLEXOSLogAvailable()) {
|
||||
BOOL persistent = [[NSUserDefaults standardUserDefaults] boolForKey:kFLEXiOSPersistentOSLogKey];
|
||||
if (persistent) {
|
||||
[self sharedLogController].persistent = YES;
|
||||
[[self sharedLogController] startMonitoring];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+ (instancetype)sharedLogController {
|
||||
static FLEXOSLogController *shared = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
shared = [self new];
|
||||
});
|
||||
|
||||
return shared;
|
||||
}
|
||||
|
||||
+ (instancetype)withUpdateHandler:(void(^)(NSArray<FLEXSystemLogMessage *> *newMessages))newMessagesHandler
|
||||
{
|
||||
FLEXOSLogController *shared = [self sharedLogController];
|
||||
shared.updateHandler = newMessagesHandler;
|
||||
return shared;
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
NSAssert(FLEXOSLogAvailable(), @"os_log is only available on iOS 10 and up");
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_filterPid = NSProcessInfo.processInfo.processIdentifier;
|
||||
_levelInfo = NO;
|
||||
_subsystemInfo = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
OSActivityStreamCancel(self.stream);
|
||||
_stream = nil;
|
||||
}
|
||||
|
||||
- (void)setPersistent:(BOOL)persistent {
|
||||
if (_persistent == persistent) return;
|
||||
|
||||
_persistent = persistent;
|
||||
self.messages = persistent ? [NSMutableArray array] : nil;
|
||||
}
|
||||
|
||||
- (BOOL)startMonitoring {
|
||||
if (![self lookupSPICalls]) {
|
||||
// >= iOS 10 is required
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Are we already monitoring?
|
||||
if (self.stream) {
|
||||
// Should we send out the "persisted" messages?
|
||||
if (self.updateHandler && self.messages.count) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.updateHandler(self.messages);
|
||||
});
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Stream entry handler
|
||||
os_activity_stream_block_t block = ^bool(os_activity_stream_entry_t entry, int error) {
|
||||
return [self handleStreamEntry:entry error:error];
|
||||
};
|
||||
|
||||
// Controls which types of messages we see
|
||||
// 'Historical' appears to just show NSLog stuff
|
||||
uint32_t activity_stream_flags = OS_ACTIVITY_STREAM_HISTORICAL;
|
||||
activity_stream_flags |= OS_ACTIVITY_STREAM_PROCESS_ONLY;
|
||||
// activity_stream_flags |= OS_ACTIVITY_STREAM_PROCESS_ONLY;
|
||||
|
||||
self.stream = OSActivityStreamForPID(self.filterPid, activity_stream_flags, block);
|
||||
|
||||
// Specify the stream-related event handler
|
||||
OSActivityStreamSetEventHandler(self.stream, [self streamEventHandlerBlock]);
|
||||
// Start the stream
|
||||
OSActivityStreamResume(self.stream);
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)lookupSPICalls {
|
||||
static BOOL hasSPI = NO;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
void *handle = dlopen("/System/Library/PrivateFrameworks/LoggingSupport.framework/LoggingSupport", RTLD_NOW);
|
||||
|
||||
OSActivityStreamForPID = (os_activity_stream_for_pid_t)dlsym(handle, "os_activity_stream_for_pid");
|
||||
OSActivityStreamResume = (os_activity_stream_resume_t)dlsym(handle, "os_activity_stream_resume");
|
||||
OSActivityStreamCancel = (os_activity_stream_cancel_t)dlsym(handle, "os_activity_stream_cancel");
|
||||
OSLogCopyFormattedMessage = (os_log_copy_formatted_message_t)dlsym(handle, "os_log_copy_formatted_message");
|
||||
OSActivityStreamSetEventHandler = (os_activity_stream_set_event_handler_t)dlsym(handle, "os_activity_stream_set_event_handler");
|
||||
proc_name = (int(*)(int, char *, unsigned int))dlsym(handle, "proc_name");
|
||||
proc_listpids = (int(*)(uint32_t, uint32_t, void*, int))dlsym(handle, "proc_listpids");
|
||||
OSLogGetType = (uint8_t(*)(void *))dlsym(handle, "os_log_get_type");
|
||||
|
||||
hasSPI = (OSActivityStreamForPID != NULL) &&
|
||||
(OSActivityStreamResume != NULL) &&
|
||||
(OSActivityStreamCancel != NULL) &&
|
||||
(OSLogCopyFormattedMessage != NULL) &&
|
||||
(OSActivityStreamSetEventHandler != NULL) &&
|
||||
(OSLogGetType != NULL) &&
|
||||
(proc_name != NULL);
|
||||
});
|
||||
|
||||
return hasSPI;
|
||||
}
|
||||
|
||||
- (BOOL)handleStreamEntry:(os_activity_stream_entry_t)entry error:(int)error {
|
||||
if (!self.canPrint || (self.filterPid != -1 && entry->pid != self.filterPid)) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (!error && entry) {
|
||||
if (entry->type == OS_ACTIVITY_STREAM_TYPE_LOG_MESSAGE ||
|
||||
entry->type == OS_ACTIVITY_STREAM_TYPE_LEGACY_LOG_MESSAGE) {
|
||||
os_log_message_t log_message = &entry->log_message;
|
||||
|
||||
// Get date
|
||||
NSDate *date = [NSDate dateWithTimeIntervalSince1970:log_message->tv_gmt.tv_sec];
|
||||
|
||||
// Get log message text
|
||||
const char *messageText = OSLogCopyFormattedMessage(log_message);
|
||||
// https://github.com/limneos/oslog/issues/1
|
||||
if (entry->log_message.format && !(strcmp(entry->log_message.format, messageText))) {
|
||||
messageText = (char *)entry->log_message.format;
|
||||
}
|
||||
// move messageText from stack to heap
|
||||
NSString *msg = [NSString stringWithUTF8String:messageText];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
FLEXSystemLogMessage *message = [FLEXSystemLogMessage logMessageFromDate:date text:msg];
|
||||
if (self.persistent) {
|
||||
[self.messages addObject:message];
|
||||
}
|
||||
if (self.updateHandler) {
|
||||
self.updateHandler(@[message]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (os_activity_stream_event_block_t)streamEventHandlerBlock {
|
||||
return [^void(os_activity_stream_t stream, os_activity_stream_event_t event) {
|
||||
switch (event) {
|
||||
case OS_ACTIVITY_STREAM_EVENT_STARTED:
|
||||
self.canPrint = YES;
|
||||
break;
|
||||
case OS_ACTIVITY_STREAM_EVENT_STOPPED:
|
||||
break;
|
||||
case OS_ACTIVITY_STREAM_EVENT_FAILED:
|
||||
break;
|
||||
case OS_ACTIVITY_STREAM_EVENT_CHUNK_STARTED:
|
||||
break;
|
||||
case OS_ACTIVITY_STREAM_EVENT_CHUNK_FINISHED:
|
||||
break;
|
||||
default:
|
||||
printf("=== Unhandled case ===\n");
|
||||
break;
|
||||
}
|
||||
} copy];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// FLEXSystemLogMessage.h
|
||||
// UICatalog
|
||||
// FLEX
|
||||
//
|
||||
// Created by Ryan Olson on 1/25/15.
|
||||
// Copyright (c) 2015 f. All rights reserved.
|
||||
@@ -8,14 +8,23 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <asl.h>
|
||||
#import "ActivityStreamAPI.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXSystemLogMessage : NSObject
|
||||
|
||||
+ (instancetype)logMessageFromASLMessage:(aslmsg)aslMessage;
|
||||
+ (instancetype)logMessageFromDate:(NSDate *)date text:(NSString *)text;
|
||||
|
||||
@property (nonatomic, strong) NSDate *date;
|
||||
@property (nonatomic, copy) NSString *sender;
|
||||
@property (nonatomic, copy) NSString *messageText;
|
||||
@property (nonatomic, assign) long long messageID;
|
||||
// ASL specific properties
|
||||
@property (nonatomic, readonly, nullable) NSString *sender;
|
||||
@property (nonatomic, readonly, nullable) aslmsg aslMessage;
|
||||
|
||||
@property (nonatomic, readonly) NSDate *date;
|
||||
@property (nonatomic, readonly) NSString *messageText;
|
||||
@property (nonatomic, readonly) long long messageID;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// FLEXSystemLogMessage.m
|
||||
// UICatalog
|
||||
// FLEX
|
||||
//
|
||||
// Created by Ryan Olson on 1/25/15.
|
||||
// Copyright (c) 2015 f. All rights reserved.
|
||||
@@ -12,7 +12,9 @@
|
||||
|
||||
+ (instancetype)logMessageFromASLMessage:(aslmsg)aslMessage
|
||||
{
|
||||
FLEXSystemLogMessage *logMessage = [[FLEXSystemLogMessage alloc] init];
|
||||
NSDate *date = nil;
|
||||
NSString *sender = nil, *text = nil;
|
||||
long long identifier = 0;
|
||||
|
||||
const char *timestamp = asl_get(aslMessage, ASL_KEY_TIME);
|
||||
if (timestamp) {
|
||||
@@ -21,30 +23,61 @@
|
||||
if (nanoseconds) {
|
||||
timeInterval += [@(nanoseconds) doubleValue] / NSEC_PER_SEC;
|
||||
}
|
||||
logMessage.date = [NSDate dateWithTimeIntervalSince1970:timeInterval];
|
||||
date = [NSDate dateWithTimeIntervalSince1970:timeInterval];
|
||||
}
|
||||
|
||||
const char *sender = asl_get(aslMessage, ASL_KEY_SENDER);
|
||||
if (sender) {
|
||||
logMessage.sender = @(sender);
|
||||
const char *s = asl_get(aslMessage, ASL_KEY_SENDER);
|
||||
if (s) {
|
||||
sender = @(s);
|
||||
}
|
||||
|
||||
const char *messageText = asl_get(aslMessage, ASL_KEY_MSG);
|
||||
if (messageText) {
|
||||
logMessage.messageText = @(messageText);
|
||||
text = @(messageText);
|
||||
}
|
||||
|
||||
const char *messageID = asl_get(aslMessage, ASL_KEY_MSG_ID);
|
||||
if (messageID) {
|
||||
logMessage.messageID = [@(messageID) longLongValue];
|
||||
identifier = [@(messageID) longLongValue];
|
||||
}
|
||||
|
||||
return logMessage;
|
||||
FLEXSystemLogMessage *message = [[self alloc] initWithDate:date sender:sender text:text messageID:identifier];
|
||||
message->_aslMessage = aslMessage;
|
||||
return message;
|
||||
}
|
||||
|
||||
+ (instancetype)logMessageFromDate:(NSDate *)date text:(NSString *)text
|
||||
{
|
||||
return [[self alloc] initWithDate:date sender:nil text:text messageID:0];
|
||||
}
|
||||
|
||||
- (id)initWithDate:(NSDate *)date sender:(NSString *)sender text:(NSString *)text messageID:(long long)identifier
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_date = date;
|
||||
_sender = sender;
|
||||
_messageText = text;
|
||||
_messageID = identifier;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object
|
||||
{
|
||||
return [object isKindOfClass:[FLEXSystemLogMessage class]] && self.messageID == [object messageID];
|
||||
if ([object isKindOfClass:[self class]]) {
|
||||
if (self.messageID) {
|
||||
// Only ASL uses messageID, otherwise it is 0
|
||||
return self.messageID == [object messageID];
|
||||
} else {
|
||||
// Test message texts and dates for OS Log
|
||||
return [self.messageText isEqual:[object messageText]] &&
|
||||
[self.date isEqualToDate:[object date]];
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSUInteger)hash
|
||||
@@ -52,4 +85,10 @@
|
||||
return (NSUInteger)self.messageID;
|
||||
}
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
NSString *escaped = [self.messageText stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
|
||||
return [NSString stringWithFormat:@"(%@) %@", @(self.messageText.length), escaped];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// FLEXSystemLogTableViewCell.h
|
||||
// UICatalog
|
||||
// FLEX
|
||||
//
|
||||
// Created by Ryan Olson on 1/25/15.
|
||||
// Copyright (c) 2015 f. All rights reserved.
|
||||
@@ -14,7 +14,7 @@ extern NSString *const kFLEXSystemLogTableViewCellIdentifier;
|
||||
|
||||
@interface FLEXSystemLogTableViewCell : UITableViewCell
|
||||
|
||||
@property (nonatomic, strong) FLEXSystemLogMessage *logMessage;
|
||||
@property (nonatomic) FLEXSystemLogMessage *logMessage;
|
||||
@property (nonatomic, copy) NSString *highlightedText;
|
||||
|
||||
+ (NSString *)displayedTextForLogMessage:(FLEXSystemLogMessage *)logMessage;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// FLEXSystemLogTableViewCell.m
|
||||
// UICatalog
|
||||
// FLEX
|
||||
//
|
||||
// Created by Ryan Olson on 1/25/15.
|
||||
// Copyright (c) 2015 f. All rights reserved.
|
||||
@@ -13,8 +13,8 @@ NSString *const kFLEXSystemLogTableViewCellIdentifier = @"FLEXSystemLogTableView
|
||||
|
||||
@interface FLEXSystemLogTableViewCell ()
|
||||
|
||||
@property (nonatomic, strong) UILabel *logMessageLabel;
|
||||
@property (nonatomic, strong) NSAttributedString *logMessageAttributedText;
|
||||
@property (nonatomic) UILabel *logMessageLabel;
|
||||
@property (nonatomic) NSAttributedString *logMessageAttributedText;
|
||||
|
||||
@end
|
||||
|
||||
@@ -24,7 +24,7 @@ NSString *const kFLEXSystemLogTableViewCellIdentifier = @"FLEXSystemLogTableView
|
||||
{
|
||||
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
|
||||
if (self) {
|
||||
self.logMessageLabel = [[UILabel alloc] init];
|
||||
self.logMessageLabel = [UILabel new];
|
||||
self.logMessageLabel.numberOfLines = 0;
|
||||
self.separatorInset = UIEdgeInsetsZero;
|
||||
self.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
@@ -77,9 +77,9 @@ static const UIEdgeInsets kFLEXLogMessageCellInsets = {10.0, 10.0, 10.0, 10.0};
|
||||
NSDictionary<NSString *, id> *attributes = @{ NSFontAttributeName : [UIFont fontWithName:@"CourierNewPSMT" size:12.0] };
|
||||
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:attributes];
|
||||
|
||||
if ([highlightedText length] > 0) {
|
||||
if (highlightedText.length > 0) {
|
||||
NSMutableAttributedString *mutableAttributedText = [attributedText mutableCopy];
|
||||
NSMutableDictionary<NSString *, id> *highlightAttributes = [@{ NSBackgroundColorAttributeName : [UIColor yellowColor] } mutableCopy];
|
||||
NSMutableDictionary<NSString *, id> *highlightAttributes = [@{ NSBackgroundColorAttributeName : UIColor.yellowColor } mutableCopy];
|
||||
[highlightAttributes addEntriesFromDictionary:attributes];
|
||||
|
||||
NSRange remainingSearchRange = NSMakeRange(0, text.length);
|
||||
@@ -118,7 +118,7 @@ static const UIEdgeInsets kFLEXLogMessageCellInsets = {10.0, 10.0, 10.0, 10.0};
|
||||
static NSDateFormatter *formatter = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
formatter = [[NSDateFormatter alloc] init];
|
||||
formatter = [NSDateFormatter new];
|
||||
formatter.dateFormat = @"yyyy-MM-dd HH:mm:ss.SSS";
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
//
|
||||
// FLEXSystemLogTableViewController.h
|
||||
// UICatalog
|
||||
// FLEX
|
||||
//
|
||||
// Created by Ryan Olson on 1/19/15.
|
||||
// Copyright (c) 2015 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXTableViewController.h"
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
|
||||
@interface FLEXSystemLogTableViewController : UITableViewController
|
||||
@interface FLEXSystemLogTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// FLEXSystemLogTableViewController.m
|
||||
// UICatalog
|
||||
// FLEX
|
||||
//
|
||||
// Created by Ryan Olson on 1/19/15.
|
||||
// Copyright (c) 2015 f. All rights reserved.
|
||||
@@ -8,88 +8,82 @@
|
||||
|
||||
#import "FLEXSystemLogTableViewController.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXSystemLogMessage.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXASLLogController.h"
|
||||
#import "FLEXOSLogController.h"
|
||||
#import "FLEXSystemLogTableViewCell.h"
|
||||
#import <asl.h>
|
||||
|
||||
@interface FLEXSystemLogTableViewController () <UISearchResultsUpdating, UISearchControllerDelegate>
|
||||
@interface FLEXSystemLogTableViewController ()
|
||||
|
||||
@property (nonatomic, strong) UISearchController *searchController;
|
||||
@property (nonatomic, readonly) id<FLEXLogController> logController;
|
||||
@property (nonatomic, readonly) NSMutableArray<FLEXSystemLogMessage *> *logMessages;
|
||||
@property (nonatomic, copy) NSArray<FLEXSystemLogMessage *> *filteredLogMessages;
|
||||
@property (nonatomic, strong) NSTimer *logUpdateTimer;
|
||||
@property (nonatomic, readonly) NSMutableIndexSet *logMessageIdentifiers;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXSystemLogTableViewController
|
||||
|
||||
- (id)init {
|
||||
return [super initWithStyle:UITableViewStylePlain];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.showsSearchBar = YES;
|
||||
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
id logHandler = ^(NSArray<FLEXSystemLogMessage *> *newMessages) {
|
||||
__strong __typeof(weakSelf) self = weakSelf;
|
||||
[self handleUpdateWithNewMessages:newMessages];
|
||||
};
|
||||
|
||||
_logMessages = [NSMutableArray array];
|
||||
_logMessageIdentifiers = [NSMutableIndexSet indexSet];
|
||||
if (FLEXOSLogAvailable()) {
|
||||
_logController = [FLEXOSLogController withUpdateHandler:logHandler];
|
||||
} else {
|
||||
_logController = [FLEXASLLogController withUpdateHandler:logHandler];
|
||||
}
|
||||
|
||||
[self.tableView registerClass:[FLEXSystemLogTableViewCell class] forCellReuseIdentifier:kFLEXSystemLogTableViewCellIdentifier];
|
||||
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
self.title = @"Loading...";
|
||||
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@" ⬇︎ " style:UIBarButtonItemStylePlain target:self action:@selector(scrollToLastRow)];
|
||||
|
||||
UIBarButtonItem *scrollDown = [[UIBarButtonItem alloc] initWithTitle:@" ⬇︎ "
|
||||
style:UIBarButtonItemStylePlain
|
||||
target:self
|
||||
action:@selector(scrollToLastRow)];
|
||||
UIBarButtonItem *settings = [[UIBarButtonItem alloc] initWithTitle:@"Settings"
|
||||
style:UIBarButtonItemStylePlain
|
||||
target:self
|
||||
action:@selector(showLogSettings)];
|
||||
if (FLEXOSLogAvailable()) {
|
||||
self.navigationItem.rightBarButtonItems = @[scrollDown, settings];
|
||||
} else {
|
||||
self.navigationItem.rightBarButtonItem = scrollDown;
|
||||
}
|
||||
}
|
||||
|
||||
self.searchController = [[UISearchController alloc] initWithSearchResultsController:nil];
|
||||
self.searchController.delegate = self;
|
||||
self.searchController.searchResultsUpdater = self;
|
||||
self.searchController.dimsBackgroundDuringPresentation = NO;
|
||||
self.tableView.tableHeaderView = self.searchController.searchBar;
|
||||
- (void)handleUpdateWithNewMessages:(NSArray<FLEXSystemLogMessage *> *)newMessages
|
||||
{
|
||||
self.title = @"System Log";
|
||||
|
||||
[self updateLogMessages];
|
||||
[self.logMessages addObjectsFromArray:newMessages];
|
||||
|
||||
// "Follow" the log as new messages stream in if we were previously near the bottom.
|
||||
BOOL wasNearBottom = self.tableView.contentOffset.y >= self.tableView.contentSize.height - self.tableView.frame.size.height - 100.0;
|
||||
[self.tableView reloadData];
|
||||
if (wasNearBottom) {
|
||||
[self scrollToLastRow];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
NSTimeInterval updateInterval = 1.0;
|
||||
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
// Querrying the ASL is much slower in the simulator. We need a longer polling interval to keep things repsonsive.
|
||||
updateInterval = 5.0;
|
||||
#endif
|
||||
|
||||
self.logUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:updateInterval target:self selector:@selector(updateLogMessages) userInfo:nil repeats:YES];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewWillDisappear:animated];
|
||||
|
||||
[self.logUpdateTimer invalidate];
|
||||
}
|
||||
|
||||
- (void)updateLogMessages
|
||||
{
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSArray<FLEXSystemLogMessage *> *newMessages = [self newLogMessagesForCurrentProcess];
|
||||
if (!newMessages.count) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.title = @"System Log";
|
||||
|
||||
[self.logMessages addObjectsFromArray:newMessages];
|
||||
for (FLEXSystemLogMessage *message in newMessages) {
|
||||
[self.logMessageIdentifiers addIndex:(NSUInteger)message.messageID];
|
||||
}
|
||||
|
||||
// "Follow" the log as new messages stream in if we were previously near the bottom.
|
||||
BOOL wasNearBottom = self.tableView.contentOffset.y >= self.tableView.contentSize.height - self.tableView.frame.size.height - 100.0;
|
||||
[self.tableView reloadData];
|
||||
if (wasNearBottom) {
|
||||
[self scrollToLastRow];
|
||||
}
|
||||
});
|
||||
});
|
||||
[self.logController startMonitoring];
|
||||
}
|
||||
|
||||
- (void)scrollToLastRow
|
||||
@@ -101,6 +95,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showLogSettings
|
||||
{
|
||||
FLEXOSLogController *logController = (FLEXOSLogController *)self.logController;
|
||||
BOOL persistent = [[NSUserDefaults standardUserDefaults] boolForKey:kFLEXiOSPersistentOSLogKey];
|
||||
NSString *toggle = persistent ? @"Disable" : @"Enable";
|
||||
NSString *title = [@"Persistent logging: " stringByAppendingString:persistent ? @"ON" : @"OFF"];
|
||||
NSString *body = @"In iOS 10 and up, ASL is gone. The OS Log API is much more limited. "
|
||||
"To get as close to the old behavior as possible, logs must be collected manually at launch and stored.\n\n"
|
||||
"Turn this feature on only when you need it.";
|
||||
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(title).message(body).button(toggle).handler(^(NSArray<NSString *> *strings) {
|
||||
[[NSUserDefaults standardUserDefaults] setBool:!persistent forKey:kFLEXiOSPersistentOSLogKey];
|
||||
logController.persistent = !persistent;
|
||||
[logController.messages addObjectsFromArray:self.logMessages];
|
||||
});
|
||||
make.button(@"Dismiss").cancelStyle();
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
#pragma mark - FLEXGlobalsEntry
|
||||
|
||||
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
|
||||
return @"⚠️ System Log";
|
||||
}
|
||||
|
||||
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
|
||||
return [self new];
|
||||
}
|
||||
|
||||
#pragma mark - Table view data source
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
|
||||
@@ -110,19 +134,19 @@
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
||||
{
|
||||
return self.searchController.isActive ? [self.filteredLogMessages count] : [self.logMessages count];
|
||||
return self.searchController.isActive ? self.filteredLogMessages.count : self.logMessages.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
FLEXSystemLogTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXSystemLogTableViewCellIdentifier forIndexPath:indexPath];
|
||||
cell.logMessage = [self logMessageAtIndexPath:indexPath];
|
||||
cell.highlightedText = self.searchController.searchBar.text;
|
||||
cell.highlightedText = self.searchText;
|
||||
|
||||
if (indexPath.row % 2 == 0) {
|
||||
cell.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
|
||||
cell.backgroundColor = [FLEXColor primaryBackgroundColor];
|
||||
} else {
|
||||
cell.backgroundColor = [UIColor whiteColor];
|
||||
cell.backgroundColor = [FLEXColor secondaryBackgroundColor];
|
||||
}
|
||||
|
||||
return cell;
|
||||
@@ -149,9 +173,8 @@
|
||||
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
|
||||
{
|
||||
if (action == @selector(copy:)) {
|
||||
FLEXSystemLogMessage *logMessage = [self logMessageAtIndexPath:indexPath];
|
||||
NSString *stringToCopy = [FLEXSystemLogTableViewCell displayedTextForLogMessage:logMessage] ?: @"";
|
||||
[[UIPasteboard generalPasteboard] setString:stringToCopy];
|
||||
// We usually only want to copy the log message itself, not any metadata associated with it.
|
||||
UIPasteboard.generalPasteboard.string = [self logMessageAtIndexPath:indexPath].messageText;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,78 +183,21 @@
|
||||
return self.searchController.isActive ? self.filteredLogMessages[indexPath.row] : self.logMessages[indexPath.row];
|
||||
}
|
||||
|
||||
#pragma mark - UISearchResultsUpdating
|
||||
#pragma mark - Search bar
|
||||
|
||||
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
|
||||
- (void)updateSearchResults:(NSString *)searchString
|
||||
{
|
||||
NSString *searchString = searchController.searchBar.text;
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSArray<FLEXSystemLogMessage *> *filteredLogMessages = [self.logMessages filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(FLEXSystemLogMessage *logMessage, NSDictionary<NSString *, id> *bindings) {
|
||||
[self onBackgroundQueue:^NSArray *{
|
||||
return [self.logMessages filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(FLEXSystemLogMessage *logMessage, NSDictionary<NSString *, id> *bindings) {
|
||||
NSString *displayedText = [FLEXSystemLogTableViewCell displayedTextForLogMessage:logMessage];
|
||||
return [displayedText rangeOfString:searchString options:NSCaseInsensitiveSearch].length > 0;
|
||||
}]];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if ([searchController.searchBar.text isEqual:searchString]) {
|
||||
self.filteredLogMessages = filteredLogMessages;
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Log Message Fetching
|
||||
|
||||
- (NSArray<FLEXSystemLogMessage *> *)newLogMessagesForCurrentProcess
|
||||
{
|
||||
if (!self.logMessages.count) {
|
||||
return [[self class] allLogMessagesForCurrentProcess];
|
||||
}
|
||||
|
||||
aslresponse response = [FLEXSystemLogTableViewController ASLMessageListForCurrentProcess];
|
||||
aslmsg aslMessage = NULL;
|
||||
|
||||
NSMutableArray<FLEXSystemLogMessage *> *newMessages = [NSMutableArray array];
|
||||
|
||||
while ((aslMessage = asl_next(response))) {
|
||||
NSUInteger messageID = (NSUInteger)atoll(asl_get(aslMessage, ASL_KEY_MSG_ID));
|
||||
if (![self.logMessageIdentifiers containsIndex:messageID]) {
|
||||
[newMessages addObject:[FLEXSystemLogMessage logMessageFromASLMessage:aslMessage]];
|
||||
} thenOnMainQueue:^(NSArray *filteredLogMessages) {
|
||||
if ([self.searchText isEqual:searchString]) {
|
||||
self.filteredLogMessages = filteredLogMessages;
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
}
|
||||
|
||||
asl_release(response);
|
||||
return newMessages;
|
||||
}
|
||||
|
||||
+ (aslresponse)ASLMessageListForCurrentProcess
|
||||
{
|
||||
static NSString *pidString = nil;
|
||||
if (!pidString) {
|
||||
pidString = @([[NSProcessInfo processInfo] processIdentifier]).stringValue;
|
||||
}
|
||||
|
||||
// Create system log query object.
|
||||
asl_object_t query = asl_new(ASL_TYPE_QUERY);
|
||||
|
||||
// Filter for messages from the current process.
|
||||
// Note that this appears to happen by default on device, but is required in the simulator.
|
||||
asl_set_query(query, ASL_KEY_PID, pidString.UTF8String, ASL_QUERY_OP_EQUAL);
|
||||
|
||||
return asl_search(NULL, query);
|
||||
}
|
||||
|
||||
+ (NSArray<FLEXSystemLogMessage *> *)allLogMessagesForCurrentProcess
|
||||
{
|
||||
aslresponse response = [self ASLMessageListForCurrentProcess];
|
||||
aslmsg aslMessage = NULL;
|
||||
|
||||
NSMutableArray<FLEXSystemLogMessage *> *logMessages = [NSMutableArray array];
|
||||
while ((aslMessage = asl_next(response))) {
|
||||
[logMessages addObject:[FLEXSystemLogMessage logMessageFromASLMessage:aslMessage]];
|
||||
}
|
||||
asl_release(response);
|
||||
|
||||
return logMessages;
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
==============================================================================
|
||||
The LLVM Project is under the Apache License v2.0 with LLVM Exceptions:
|
||||
==============================================================================
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
---- LLVM Exceptions to the Apache 2.0 License ----
|
||||
|
||||
As an exception, if, as a result of your compiling your source code, portions
|
||||
of this Software are embedded into an Object form of such source code, you
|
||||
may redistribute such embedded portions in such Object form without complying
|
||||
with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
|
||||
|
||||
In addition, if you combine or link compiled forms of this Software with
|
||||
software that is licensed under the GPLv2 ("Combined Software") and if a
|
||||
court of competent jurisdiction determines that the patent provision (Section
|
||||
3), the indemnity provision (Section 9) or other Section of the License
|
||||
conflicts with the conditions of the GPLv2, you may retroactively and
|
||||
prospectively choose to deem waived or otherwise exclude such Section(s) of
|
||||
the License, but only in their entirety and only with respect to the Combined
|
||||
Software.
|
||||
|
||||
==============================================================================
|
||||
Software from third parties included in the LLVM Project:
|
||||
==============================================================================
|
||||
The LLVM Project contains third party software which is under different license
|
||||
terms. All such code will be identified clearly using at least one of two
|
||||
mechanisms:
|
||||
1) It will be in a separate directory tree with its own `LICENSE.txt` or
|
||||
`LICENSE` file at the top containing the specific license and restrictions
|
||||
which apply to that software, or
|
||||
2) It will contain specific license and restriction terms at the top of every
|
||||
file.
|
||||
|
||||
==============================================================================
|
||||
Legacy LLVM License (https://llvm.org/docs/DeveloperPolicy.html#legacy):
|
||||
==============================================================================
|
||||
University of Illinois/NCSA
|
||||
Open Source License
|
||||
|
||||
Copyright (c) 2010 Apple Inc.
|
||||
All rights reserved.
|
||||
|
||||
Developed by:
|
||||
|
||||
LLDB Team
|
||||
|
||||
http://lldb.llvm.org/
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal with
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimers.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimers in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the names of the LLDB Team, copyright holders, nor the names of
|
||||
its contributors may be used to endorse or promote products derived from
|
||||
this Software without specific prior written permission.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
|
||||
#import "FLEXManager.h"
|
||||
|
||||
@class FLEXGlobalsTableViewControllerEntry;
|
||||
@class FLEXGlobalsEntry;
|
||||
|
||||
@interface FLEXManager ()
|
||||
|
||||
/// An array of FLEXGlobalsTableViewControllerEntry objects that have been registered by the user.
|
||||
@property (nonatomic, readonly, strong) NSArray<FLEXGlobalsTableViewControllerEntry *> *userGlobalEntries;
|
||||
/// An array of FLEXGlobalsEntry objects that have been registered by the user.
|
||||
@property (nonatomic, readonly) NSArray<FLEXGlobalsEntry *> *userGlobalEntries;
|
||||
|
||||
@property (nonatomic, readonly, strong) NSDictionary<NSString *, FLEXCustomContentViewerFuture> *customContentTypeViewers;
|
||||
@property (nonatomic, readonly) NSDictionary<NSString *, FLEXCustomContentViewerFuture> *customContentTypeViewers;
|
||||
|
||||
@end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user