Compare commits
544 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c761865b9b | |||
| 700c50af5d | |||
| b38cca06b1 | |||
| 6429573918 | |||
| f77f5ccdc9 | |||
| 7aeddcdb2c | |||
| a25ef87a51 | |||
| fbeb1beca0 | |||
| 059bde9711 | |||
| 2ca563f570 | |||
| 88c7ca9373 | |||
| 83486641aa | |||
| 6bd0c87881 | |||
| 1a64da70c9 | |||
| 87ea2bb147 | |||
| d9e9be53d8 | |||
| 142f037497 | |||
| 6cdb626d78 | |||
| 6e81029b8b | |||
| 1c7048e710 | |||
| 23c7cfbe6e | |||
| b45750eb1b | |||
| 550c9c1120 | |||
| d5dfb23cf2 | |||
| 90ee4f8f38 | |||
| c037e703a6 | |||
| 6bf746d4aa | |||
| ca005fb4d0 | |||
| 5f2bac9c2f | |||
| dc0e7dc7d3 | |||
| 740daab55b | |||
| 1953089653 | |||
| 25e0af042e | |||
| 0265334976 | |||
| 6aa9ec9ec1 | |||
| 459aa9b6f5 | |||
| 06655dde6a | |||
| 5c153b0c89 | |||
| 29dd970ea7 | |||
| f6701f8ec9 | |||
| 7f119ba0cc | |||
| e7e5115fc0 | |||
| 0e0bd5a890 | |||
| 9cc2470901 | |||
| b07da3e11d | |||
| 36e0e9fb1e | |||
| 0e85162c11 | |||
| 7329fd9272 | |||
| 30fb8f077a | |||
| f2f66489d1 | |||
| 3cb1366966 | |||
| 907b315601 | |||
| 877a1db87b | |||
| 5b6b50bf6a | |||
| 6c8fbbeaa8 | |||
| da67902cf5 | |||
| 8533689ca7 | |||
| 8f85b22866 | |||
| fa8a4d61ea | |||
| 89010395de | |||
| 5b969b6438 | |||
| 2dee6901f7 | |||
| fced419509 | |||
| 09ff0482df | |||
| d9986d879b | |||
| 15d7d07809 | |||
| a556ece626 | |||
| 35ce037288 | |||
| a32b4074f8 | |||
| c3da7e10f7 | |||
| 135c8d05c1 | |||
| 8afd1a1975 | |||
| dcdd638719 | |||
| c661d491a5 | |||
| 82f104d682 | |||
| f9e42aed74 | |||
| c8343500af | |||
| 5a96ed6af5 | |||
| 84cdc6a8e4 | |||
| 2b6ccb23e4 | |||
| be02b89d9b | |||
| b4f07a0f92 | |||
| 2093913b17 | |||
| 7705dac42b | |||
| d23f01dd87 | |||
| ec2b73ceda | |||
| b8f226ce45 | |||
| b232ddb075 | |||
| 87a821903d | |||
| 9645811baa | |||
| 2e2a550b1f | |||
| 98dff514e8 | |||
| 9332a87d98 | |||
| d031dab174 | |||
| 7ddb46ff9f | |||
| 4a4a08df16 | |||
| 4830daeab3 | |||
| dd05a6652c | |||
| e78947260d | |||
| 5dd856070e | |||
| 2e868eba39 | |||
| 9ba80a53cf | |||
| 1780968f50 | |||
| 9abd9cc933 | |||
| 18f20fbf3b | |||
| 3e6d18dd8c | |||
| 99d2ddd001 | |||
| 1647f7ab9f | |||
| 5ae64e17e6 | |||
| c5d4e959cd | |||
| c056a29375 | |||
| 02409d8051 | |||
| 805434239a | |||
| 179c968443 | |||
| cfc68017e0 | |||
| 2300d68321 | |||
| 873e79d2c0 | |||
| 17e74a7b02 | |||
| 795cff68fd | |||
| 162bf48b5e | |||
| 239afdbd7c | |||
| e53e083507 | |||
| 1acfa52c64 | |||
| fa9eec08c1 | |||
| 2160fb3c46 | |||
| 43e7938281 | |||
| b7baa219a4 | |||
| 5d27613b38 | |||
| d8ff46853d | |||
| 804fd376b9 | |||
| 8ef3a78386 | |||
| 05bd084174 | |||
| 920727e375 | |||
| 3647c8deee | |||
| 43c15356d9 | |||
| 7c28466ec8 | |||
| edf5426e80 | |||
| 3fdaead2f7 | |||
| 1b2181d1f7 | |||
| ee2f5c6415 | |||
| 503ce499d1 | |||
| 1ac04cd52c | |||
| c3801f2366 | |||
| 7703757dab | |||
| 756b8210b8 | |||
| 30ce841030 | |||
| 3036676a93 | |||
| bf649ff1f6 | |||
| 9f68011207 | |||
| b5a95bacba | |||
| 6ec780b679 | |||
| fab67cb5cb | |||
| f24990d10f | |||
| 43a3f859e2 | |||
| f3129aaa90 | |||
| 3199063914 | |||
| 7d6cc33c2a | |||
| 0b914bb7c6 | |||
| 5cff9ce3e8 | |||
| 73bc5a4443 | |||
| 65e5bee4e3 | |||
| c02192cd71 | |||
| 37757d76e1 | |||
| 3226cbc8c4 | |||
| ab720971de | |||
| 613a886604 | |||
| 3581267072 | |||
| 7481f7e098 | |||
| c44b251413 | |||
| 2541c89b99 | |||
| bfacfcfe55 | |||
| b221da7021 | |||
| 8d381ea020 | |||
| 3bde42fd21 | |||
| 9790c7b0bc | |||
| b10807cd84 | |||
| 799e3b2a88 | |||
| a95a31cf74 | |||
| f43b438be0 | |||
| 6d4c7b5e0d | |||
| bbc64efb12 | |||
| c0cb5f6dcb | |||
| fc9874ece6 | |||
| c098bb2c5e | |||
| 605d35b364 | |||
| 10fcdfd886 | |||
| 44aac90d41 | |||
| f5cc3fd347 | |||
| b4e8574a2f | |||
| 191680a6b9 | |||
| 0f1134e25e | |||
| d506fee663 | |||
| 11544b77b6 | |||
| aa5b9d4e7f | |||
| c04e0b606d | |||
| eb1345dcb3 | |||
| 7186b58e80 | |||
| 5859017040 | |||
| d31e1aeb4e | |||
| 3d05e4fb6a | |||
| ba1de91f85 | |||
| 1e9379dc02 | |||
| 965419bd58 | |||
| b21fbabd67 | |||
| 31446c01be | |||
| 81a3336053 | |||
| 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 | |||
| 44e9d55fb8 | |||
| ab9515caaf | |||
| 0dd0fc9418 | |||
| 24d5f3e9b2 | |||
| 693f57eef7 | |||
| 7c17ce0787 | |||
| 400a3ccd1c | |||
| a8cdac1872 | |||
| dedac1f98d | |||
| efa317f0d1 | |||
| 9b55bb10de | |||
| 122fb41fa8 | |||
| d6b5e8c77d | |||
| a6ad98dd53 | |||
| cc35f2086a | |||
| 7038aae6db | |||
| f5433153d0 | |||
| 8b7c59d949 | |||
| faef524b6c | |||
| a2bdc03684 | |||
| bd5f9740b7 | |||
| 505bb2ca41 | |||
| 009711ab3f | |||
| 7ad7653cdf | |||
| e5f51e4dfa | |||
| 92029d2b43 | |||
| 7da059791e | |||
| af57527961 | |||
| 9a8f45663e | |||
| 8528c8a1f6 | |||
| d682fd0ace | |||
| 31af87a81e | |||
| 386d6ae06a | |||
| df79ae7971 | |||
| d30c642707 | |||
| f463e2b43e | |||
| ed49a4fc89 | |||
| b897250fde | |||
| 06709a5afe | |||
| 6eee9e6080 | |||
| c50e6e51c5 | |||
| d1e9248695 | |||
| a535f10d0c | |||
| 009aa5e3f9 | |||
| 675f03fc71 | |||
| 99eccdf4c3 | |||
| 29afa5e80f | |||
| bf26bc6539 | |||
| f7b40646e2 | |||
| 84c1fb159b | |||
| 62ef95ff93 | |||
| 731b729db7 | |||
| a1c464d1a7 | |||
| eb2ecbf9b3 | |||
| 16fab66f7b | |||
| b3e70ac491 | |||
| d5177bb049 | |||
| bb0faeb3cf | |||
| 761feef3c0 | |||
| 352bae03ea | |||
| b0085cae7d | |||
| b2f93f1752 | |||
| 354510f2c4 | |||
| b8c6175193 | |||
| d409b110f5 | |||
| 5c73220158 | |||
| 397721e7ea | |||
| 5714275bcd | |||
| 833c584e41 | |||
| 49b24487c5 | |||
| 6d4eb01a07 | |||
| a752203ff9 | |||
| c69427613d | |||
| 5d75a83568 | |||
| 7642a0632d | |||
| 841054a713 | |||
| 49f368fd63 | |||
| 1dc99250c8 | |||
| 00edccf326 | |||
| 7f3af90645 | |||
| 1a030f06cd | |||
| 0477858bed | |||
| 000e061d00 | |||
| 224978b31b | |||
| 7fd133f13b | |||
| e40054ba1a | |||
| 1761734447 | |||
| f23ee3cd95 | |||
| e455ac0c7d | |||
| 94f68c6dfe | |||
| a22f022014 | |||
| 22e7edb698 | |||
| 52fcda53c5 | |||
| be6c5d0e43 | |||
| 6ed0037f50 | |||
| 81e3a5ff47 | |||
| 2a6e28c9d0 | |||
| 9e928b0b09 | |||
| d5deaad628 | |||
| 232ae8a6fd | |||
| c766e5d94a | |||
| 5f27a2304b | |||
| 0cb0f44f18 | |||
| d1c1aa0a26 | |||
| 832957f621 | |||
| 24985ac984 | |||
| 0b652c2f2a | |||
| 98d83bb438 | |||
| e13717b056 | |||
| 552e687b9c | |||
| fb29421644 | |||
| f5d930bd58 | |||
| 26af4ef476 | |||
| ac8940da26 | |||
| f597152a62 | |||
| 58f94f108c | |||
| a5e0bbd50e | |||
| ac273fbfc9 | |||
| 548fd03bd5 | |||
| b564c25d2a | |||
| bd821dc553 | |||
| e46a33417b | |||
| a3419a841f | |||
| cafc1ba0bd | |||
| 0112c097d9 | |||
| 507d03fd90 | |||
| b6453ac360 | |||
| b693ceb20e | |||
| 31e81a616d | |||
| 928f60b56f | |||
| 12a1900d75 | |||
| c72b6f7e5b | |||
| 3cf9f72dcb | |||
| 9b1318e975 | |||
| d3d0f04c23 | |||
| b606d04944 | |||
| 7afd50d241 | |||
| 94f06d5ff8 | |||
| 85424fd15e | |||
| cff391f78c | |||
| 3e420bb747 | |||
| 8aece0a266 | |||
| 81b27b6918 | |||
| 727943c4b3 | |||
| 9f2c032157 | |||
| d6a5b1af8d | |||
| dda9dd5beb | |||
| 888887f09a | |||
| b70a1a2f48 | |||
| 54730c368c | |||
| 21672e6f8d | |||
| 4ffc992872 | |||
| 8eea2ec652 | |||
| 3df01ee7bb | |||
| d0ad6e4319 | |||
| 37aec6dacc | |||
| cdc5aae4b7 | |||
| fd2b89fd24 | |||
| f1683e54c3 | |||
| c66dd2e7d3 | |||
| 5a5b921bbf | |||
| 30cc65bd9d | |||
| 29a45aa02d | |||
| 08b25ea8d3 | |||
| 7ffcb83563 | |||
| b0b64c1ba9 | |||
| bc5dfa02ec | |||
| c69f5c220a | |||
| 7f28d430d0 | |||
| 30dc024903 | |||
| 6403053989 | |||
| ba352c15e8 | |||
| e9e084e6f1 | |||
| 26e92c2bd6 | |||
| 41d761f822 | |||
| 832a03bf27 | |||
| ebf5254629 | |||
| e199b529c8 | |||
| 06edea64ae | |||
| 6d3afe36d1 | |||
| b22d2b57a2 | |||
| b453086936 | |||
| 103489c566 | |||
| 3dd27557ea | |||
| a64188dd5e | |||
| 44e428655a |
@@ -17,3 +17,6 @@ DerivedData
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
.DS_Store
|
||||
/Example/Pods
|
||||
Podfile.lock
|
||||
IDEWorkspaceChecks.plist
|
||||
|
||||
+6
-2
@@ -1,8 +1,12 @@
|
||||
language: objective-c
|
||||
xcode_workspace: FLEX.xcworkspace
|
||||
xcode_sdk: iphonesimulator
|
||||
before_install:
|
||||
- gem install xcpretty
|
||||
matrix:
|
||||
include:
|
||||
- xcode_scheme: UICatalog
|
||||
xcode_sdk: iphonesimulator
|
||||
- xcode_scheme: FLEX
|
||||
xcode_sdk: iphonesimulator
|
||||
script:
|
||||
- set -o pipefail
|
||||
- xcodebuild -workspace $TRAVIS_XCODE_WORKSPACE -scheme $TRAVIS_XCODE_SCHEME -sdk $TRAVIS_XCODE_SDK build | xcpretty
|
||||
@@ -0,0 +1,3 @@
|
||||
# Contributing to FLEX #
|
||||
|
||||
We welcome contributions! Please open a pull request with your changes.
|
||||
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// FLEXFilteringTableViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/9/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
|
||||
#pragma mark - FLEXTableViewFiltering
|
||||
@protocol FLEXTableViewFiltering <FLEXSearchResultsUpdating>
|
||||
|
||||
/// An array of visible, "filtered" sections. For example,
|
||||
/// if you have 3 sections in \c allSections and the user searches
|
||||
/// for something that matches rows in only one section, then
|
||||
/// this property would only contain that on matching section.
|
||||
@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *sections;
|
||||
/// An array of all possible sections. Empty sections are to be removed
|
||||
/// and the resulting array stored in the \c section property. Setting
|
||||
/// this property should immediately set \c sections to \c nonemptySections
|
||||
///
|
||||
/// Do not manually initialize this property, it will be
|
||||
/// initialized for you using the result of \c makeSections.
|
||||
@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *allSections;
|
||||
|
||||
/// This computed property should filter \c allSections for assignment to \c sections
|
||||
@property (nonatomic, readonly) NSArray<FLEXTableViewSection *> *nonemptySections;
|
||||
|
||||
/// This should be able to re-initialize \c allSections
|
||||
- (NSArray<FLEXTableViewSection *> *)makeSections;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#pragma mark - FLEXFilteringTableViewController
|
||||
/// A table view which implements \c UITableView* methods using arrays of
|
||||
/// \c FLEXTableViewSection objects provied by a special delegate.
|
||||
@interface FLEXFilteringTableViewController : FLEXTableViewController <FLEXTableViewFiltering>
|
||||
|
||||
/// Stores the current search query.
|
||||
@property (nonatomic, copy) NSString *filterText;
|
||||
|
||||
/// This property is set to \c self by default.
|
||||
///
|
||||
/// This property is used to power almost all of the table view's data source
|
||||
/// and delegate methods automatically, including row and section filtering
|
||||
/// when the user searches, 3D Touch context menus, row selection, etc.
|
||||
///
|
||||
/// Setting this property will also set \c searchDelegate to that object.
|
||||
@property (nonatomic, weak) id<FLEXTableViewFiltering> filterDelegate;
|
||||
|
||||
/// Defaults to \c NO. If enabled, all filtering will be done by calling
|
||||
/// \c onBackgroundQueue:thenOnMainQueue: with the UI updated on the main queue.
|
||||
@property (nonatomic) BOOL filterInBackground;
|
||||
|
||||
/// Defaults to \c NO. If enabled, one • will be supplied as an index title for each section.
|
||||
@property (nonatomic) BOOL wantsSectionIndexTitles;
|
||||
|
||||
/// Recalculates the non-empty sections and reloads the table view.
|
||||
///
|
||||
/// Subclasses may override to perform additional reloading logic,
|
||||
/// such as calling \c -reloadSections if needed. Be sure to call
|
||||
/// \c super after any logic that would affect the appearance of
|
||||
/// the table view, since the table view is reloaded last.
|
||||
///
|
||||
/// Called at the end of this class's implementation of \c updateSearchResults:
|
||||
- (void)reloadData;
|
||||
|
||||
/// Invoke this method to call \c -reloadData on each section
|
||||
/// in \c self.filterDelegate.allSections.
|
||||
- (void)reloadSections;
|
||||
|
||||
#pragma mark FLEXTableViewFiltering
|
||||
|
||||
@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *sections;
|
||||
@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *allSections;
|
||||
|
||||
/// Subclasses can override to hide specific sections under certain conditions
|
||||
/// if using \c self as the \c filterDelegate, as is the default.
|
||||
///
|
||||
/// For example, the object explorer hides the description section when searching.
|
||||
@property (nonatomic, readonly) NSArray<FLEXTableViewSection *> *nonemptySections;
|
||||
|
||||
/// If using \c self as the \c filterDelegate, as is the default,
|
||||
/// subclasses should override to provide the sections for the table view.
|
||||
- (NSArray<FLEXTableViewSection *> *)makeSections;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,203 @@
|
||||
//
|
||||
// FLEXFilteringTableViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/9/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFilteringTableViewController.h"
|
||||
#import "FLEXTableViewSection.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
|
||||
@interface FLEXFilteringTableViewController ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXFilteringTableViewController
|
||||
@synthesize allSections = _allSections;
|
||||
|
||||
#pragma mark - View controller lifecycle
|
||||
|
||||
- (void)loadView {
|
||||
[super loadView];
|
||||
|
||||
if (!self.filterDelegate) {
|
||||
self.filterDelegate = self;
|
||||
} else {
|
||||
[self _registerCellsForReuse];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_registerCellsForReuse {
|
||||
for (FLEXTableViewSection *section in self.filterDelegate.allSections) {
|
||||
if (section.cellRegistrationMapping) {
|
||||
[self.tableView registerCells:section.cellRegistrationMapping];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)setFilterDelegate:(id<FLEXTableViewFiltering>)filterDelegate {
|
||||
_filterDelegate = filterDelegate;
|
||||
filterDelegate.allSections = [filterDelegate makeSections];
|
||||
|
||||
if (self.isViewLoaded) {
|
||||
[self _registerCellsForReuse];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reloadData {
|
||||
[self reloadData:self.nonemptySections];
|
||||
}
|
||||
|
||||
- (void)reloadData:(NSArray *)nonemptySections {
|
||||
// Recalculate displayed sections
|
||||
self.filterDelegate.sections = nonemptySections;
|
||||
|
||||
// Refresh table view
|
||||
if (self.isViewLoaded) {
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reloadSections {
|
||||
for (FLEXTableViewSection *section in self.filterDelegate.allSections) {
|
||||
[section reloadData];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Search
|
||||
|
||||
- (void)updateSearchResults:(NSString *)newText {
|
||||
NSArray *(^filter)() = ^NSArray *{
|
||||
self.filterText = newText;
|
||||
|
||||
// Sections will adjust data based on this property
|
||||
for (FLEXTableViewSection *section in self.filterDelegate.allSections) {
|
||||
section.filterText = newText;
|
||||
}
|
||||
|
||||
return nil;
|
||||
};
|
||||
|
||||
if (self.filterInBackground) {
|
||||
[self onBackgroundQueue:filter thenOnMainQueue:^(NSArray *unused) {
|
||||
if ([self.searchText isEqualToString:newText]) {
|
||||
[self reloadData];
|
||||
}
|
||||
}];
|
||||
} else {
|
||||
filter();
|
||||
[self reloadData];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Filtering
|
||||
|
||||
- (NSArray<FLEXTableViewSection *> *)nonemptySections {
|
||||
return [self.filterDelegate.allSections flex_filtered:^BOOL(FLEXTableViewSection *section, NSUInteger idx) {
|
||||
return section.numberOfRows > 0;
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSArray<FLEXTableViewSection *> *)makeSections {
|
||||
return @[];
|
||||
}
|
||||
|
||||
- (void)setAllSections:(NSArray<FLEXTableViewSection *> *)allSections {
|
||||
_allSections = allSections.copy;
|
||||
self.sections = self.nonemptySections;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UITableViewDataSource
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
||||
return self.filterDelegate.sections.count;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.filterDelegate.sections[section].numberOfRows;
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
return self.filterDelegate.sections[section].title;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSString *reuse = [self.filterDelegate.sections[indexPath.section] reuseIdentifierForRow:indexPath.row];
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuse forIndexPath:indexPath];
|
||||
[self.filterDelegate.sections[indexPath.section] configureCell:cell forRow:indexPath.row];
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return UITableViewAutomaticDimension;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView {
|
||||
if (self.wantsSectionIndexTitles) {
|
||||
return [NSArray flex_forEachUpTo:self.filterDelegate.sections.count map:^id(NSUInteger i) {
|
||||
return @"⦁";
|
||||
}];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UITableViewDelegate
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return [self.filterDelegate.sections[indexPath.section] canSelectRow:indexPath.row];
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
FLEXTableViewSection *section = self.filterDelegate.sections[indexPath.section];
|
||||
|
||||
void (^action)(UIViewController *) = [section didSelectRowAction:indexPath.row];
|
||||
UIViewController *details = [section viewControllerToPushForRow:indexPath.row];
|
||||
|
||||
if (action) {
|
||||
action(self);
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
} else if (details) {
|
||||
[self.navigationController pushViewController:details animated:YES];
|
||||
} else {
|
||||
[NSException raise:NSInternalInconsistencyException
|
||||
format:@"Row is selectable but has no action or view controller"];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath {
|
||||
[self.filterDelegate.sections[indexPath.section] didPressInfoButtonAction:indexPath.row](self);
|
||||
}
|
||||
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
|
||||
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) {
|
||||
FLEXTableViewSection *section = self.filterDelegate.sections[indexPath.section];
|
||||
NSString *title = [section menuTitleForRow:indexPath.row];
|
||||
NSArray<UIMenuElement *> *menuItems = [section menuItemsForRow:indexPath.row sender:self];
|
||||
|
||||
if (menuItems.count) {
|
||||
return [UIContextMenuConfiguration
|
||||
configurationWithIdentifier:nil
|
||||
previewProvider:nil
|
||||
actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
|
||||
return [UIMenu menuWithTitle:title children:menuItems];
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// FLEXNavigationController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 1/30/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXNavigationController : UINavigationController
|
||||
|
||||
+ (instancetype)withRootViewController:(UIViewController *)rootVC;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,178 @@
|
||||
//
|
||||
// FLEXNavigationController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 1/30/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXNavigationController.h"
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXTabList.h"
|
||||
|
||||
@interface UINavigationController (Private) <UIGestureRecognizerDelegate>
|
||||
- (void)_gestureRecognizedInteractiveHide:(UIGestureRecognizer *)sender;
|
||||
@end
|
||||
@interface UIPanGestureRecognizer (Private)
|
||||
- (void)_setDelegate:(id)delegate;
|
||||
@end
|
||||
|
||||
@interface FLEXNavigationController ()
|
||||
@property (nonatomic, readonly) BOOL toolbarWasHidden;
|
||||
@property (nonatomic) BOOL waitingToAddTab;
|
||||
@property (nonatomic) BOOL didSetupPendingDismissButtons;
|
||||
@property (nonatomic) UISwipeGestureRecognizer *navigationBarSwipeGesture;
|
||||
@end
|
||||
|
||||
@implementation FLEXNavigationController
|
||||
|
||||
+ (instancetype)withRootViewController:(UIViewController *)rootVC {
|
||||
return [[self alloc] initWithRootViewController:rootVC];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.waitingToAddTab = YES;
|
||||
|
||||
// Add gesture to reveal toolbar if hidden
|
||||
self.navigationBar.userInteractionEnabled = YES;
|
||||
[self.navigationBar addGestureRecognizer:[[UITapGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleNavigationBarTap:)
|
||||
]];
|
||||
|
||||
// Add gesture to dismiss if not presented with a sheet style
|
||||
if (@available(iOS 13, *)) {
|
||||
switch (self.modalPresentationStyle) {
|
||||
case UIModalPresentationAutomatic:
|
||||
case UIModalPresentationPageSheet:
|
||||
case UIModalPresentationFormSheet:
|
||||
break;
|
||||
|
||||
default:
|
||||
[self addNavigationBarSwipeGesture];
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
[self addNavigationBarSwipeGesture];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
if (self.beingPresented && !self.didSetupPendingDismissButtons) {
|
||||
for (UIViewController *vc in self.viewControllers) {
|
||||
[self addNavigationBarItemsToViewController:vc.navigationItem];
|
||||
}
|
||||
|
||||
self.didSetupPendingDismissButtons = YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
if (self.waitingToAddTab) {
|
||||
// Only add new tab if we're presented properly
|
||||
if ([self.presentingViewController isKindOfClass:[FLEXExplorerViewController class]]) {
|
||||
// New navigation controllers always add themselves as new tabs,
|
||||
// tabs are closed by FLEXExplorerViewController
|
||||
[FLEXTabList.sharedList addTab:self];
|
||||
self.waitingToAddTab = NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
|
||||
[super pushViewController:viewController animated:animated];
|
||||
[self addNavigationBarItemsToViewController:viewController.navigationItem];
|
||||
}
|
||||
|
||||
- (void)dismissAnimated {
|
||||
// Tabs are only closed if the done button is pressed; this
|
||||
// allows you to leave a tab open by dragging down to dismiss
|
||||
[FLEXTabList.sharedList closeTab:self];
|
||||
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)addNavigationBarItemsToViewController:(UINavigationItem *)navigationItem {
|
||||
if (!self.presentingViewController) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if a done item already exists
|
||||
for (UIBarButtonItem *item in navigationItem.rightBarButtonItems) {
|
||||
if (item.style == UIBarButtonItemStyleDone) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Give root view controllers a Done button if it does not already have one
|
||||
UIBarButtonItem *done = [[UIBarButtonItem alloc]
|
||||
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
|
||||
target:self
|
||||
action:@selector(dismissAnimated)
|
||||
];
|
||||
|
||||
// Prepend the button if other buttons exist already
|
||||
NSArray *existingItems = navigationItem.rightBarButtonItems;
|
||||
if (existingItems.count) {
|
||||
navigationItem.rightBarButtonItems = [@[done] arrayByAddingObjectsFromArray:existingItems];
|
||||
} else {
|
||||
navigationItem.rightBarButtonItem = done;
|
||||
}
|
||||
|
||||
// Keeps us from calling this method again on
|
||||
// the same view controllers in -viewWillAppear:
|
||||
self.didSetupPendingDismissButtons = YES;
|
||||
}
|
||||
|
||||
- (void)addNavigationBarSwipeGesture {
|
||||
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleNavigationBarSwipe:)
|
||||
];
|
||||
swipe.direction = UISwipeGestureRecognizerDirectionDown;
|
||||
swipe.delegate = self;
|
||||
self.navigationBarSwipeGesture = swipe;
|
||||
[self.navigationBar addGestureRecognizer:swipe];
|
||||
}
|
||||
|
||||
- (void)handleNavigationBarSwipe:(UISwipeGestureRecognizer *)sender {
|
||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
||||
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleNavigationBarTap:(UIGestureRecognizer *)sender {
|
||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
||||
if (self.toolbarHidden) {
|
||||
[self setToolbarHidden:NO animated:YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)g1 shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)g2 {
|
||||
if (g1 == self.navigationBarSwipeGesture && g2 == self.barHideOnSwipeGestureRecognizer) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)_gestureRecognizedInteractiveHide:(UIPanGestureRecognizer *)sender {
|
||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
||||
BOOL show = self.topViewController.toolbarItems.count;
|
||||
CGFloat yTranslation = [sender translationInView:self.view].y;
|
||||
CGFloat yVelocity = [sender velocityInView:self.view].y;
|
||||
if (yVelocity > 2000) {
|
||||
[self setToolbarHidden:YES animated:YES];
|
||||
} else if (show && yTranslation > 20 && yVelocity > 250) {
|
||||
[self setToolbarHidden:NO animated:YES];
|
||||
} else if (yTranslation < -20) {
|
||||
[self setToolbarHidden:YES animated:YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,149 @@
|
||||
//
|
||||
// FLEXTableViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 7/5/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXTableView.h"
|
||||
@class FLEXScopeCarousel, FLEXWindow, FLEXTableViewSection;
|
||||
|
||||
typedef CGFloat FLEXDebounceInterval;
|
||||
/// No delay, all events delivered
|
||||
extern CGFloat const kFLEXDebounceInstant;
|
||||
/// Small delay which makes UI seem smoother by avoiding rapid events
|
||||
extern CGFloat const kFLEXDebounceFast;
|
||||
/// Slower than Fast, faster than ExpensiveIO
|
||||
extern CGFloat const kFLEXDebounceForAsyncSearch;
|
||||
/// The least frequent, at just over once per second; for I/O or other expensive operations
|
||||
extern CGFloat const kFLEXDebounceForExpensiveIO;
|
||||
|
||||
@protocol FLEXSearchResultsUpdating <NSObject>
|
||||
/// A method to handle search query update events.
|
||||
///
|
||||
/// \c searchBarDebounceInterval is used to reduce the frequency at which this
|
||||
/// method is called. This method is also called when the search bar becomes
|
||||
/// the first responder, and when the selected search bar scope index changes.
|
||||
- (void)updateSearchResults:(NSString *)newText;
|
||||
@end
|
||||
|
||||
@interface FLEXTableViewController : UITableViewController <
|
||||
UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate
|
||||
>
|
||||
|
||||
/// A grouped table view. Inset on iOS 13.
|
||||
///
|
||||
/// Simply calls into \c initWithStyle:
|
||||
- (id)init;
|
||||
|
||||
/// Subclasses may override to configure the controller before \c viewDidLoad:
|
||||
- (id)initWithStyle:(UITableViewStyle)style;
|
||||
|
||||
@property (nonatomic) FLEXTableView *tableView;
|
||||
|
||||
/// If your subclass conforms to \c FLEXSearchResultsUpdating
|
||||
/// then this property is assigned to \c self automatically.
|
||||
///
|
||||
/// Setting \c filterDelegate will also set this property to that object.
|
||||
@property (nonatomic, weak) id<FLEXSearchResultsUpdating> searchDelegate;
|
||||
|
||||
/// Defaults to NO.
|
||||
///
|
||||
/// Setting this to YES will initialize the carousel and the view.
|
||||
@property (nonatomic) BOOL showsCarousel;
|
||||
/// A horizontally scrolling list with functionality similar to
|
||||
/// that of a search bar's scope bar. You'd want to use this when
|
||||
/// you have potentially more than 4 scope options.
|
||||
@property (nonatomic) FLEXScopeCarousel *carousel;
|
||||
|
||||
/// Defaults to NO.
|
||||
///
|
||||
/// Setting this to YES will initialize searchController and the view.
|
||||
@property (nonatomic) BOOL showsSearchBar;
|
||||
/// Defaults to NO.
|
||||
///
|
||||
/// Setting this to YES will make the search bar appear whenever the view appears.
|
||||
/// Otherwise, iOS will only show the search bar when you scroll up.
|
||||
@property (nonatomic) BOOL showSearchBarInitially;
|
||||
|
||||
/// nil unless showsSearchBar is set to YES.
|
||||
///
|
||||
/// self is used as the default search results updater and delegate.
|
||||
/// The search bar will not dim the background or hide the navigation bar by default.
|
||||
/// On iOS 11 and up, the search bar will appear in the navigation bar below the title.
|
||||
@property (nonatomic) UISearchController *searchController;
|
||||
/// Used to initialize the search controller. Defaults to nil.
|
||||
@property (nonatomic) UIViewController *searchResultsController;
|
||||
/// Defaults to "Fast"
|
||||
///
|
||||
/// Determines how often search bar results will be "debounced."
|
||||
/// Empty query events are always sent instantly. Query events will
|
||||
/// be sent when the user has not changed the query for this interval.
|
||||
@property (nonatomic) FLEXDebounceInterval searchBarDebounceInterval;
|
||||
/// Whether the search bar stays at the top of the view while scrolling.
|
||||
///
|
||||
/// Calls into self.navigationItem.hidesSearchBarWhenScrolling.
|
||||
/// Do not change self.navigationItem.hidesSearchBarWhenScrolling directly,
|
||||
/// or it will not be respsected. Use this instead.
|
||||
/// Defaults to NO.
|
||||
@property (nonatomic) BOOL pinSearchBar;
|
||||
/// By default, we will show the search bar's cancel button when
|
||||
/// search becomes active and hide it when search is dismissed.
|
||||
///
|
||||
/// Do not set the showsCancelButton property on the searchController's
|
||||
/// searchBar manually. Set this property after turning on showsSearchBar.
|
||||
///
|
||||
/// Does nothing pre-iOS 13, safe to call on any version.
|
||||
@property (nonatomic) BOOL automaticallyShowsSearchBarCancelButton;
|
||||
|
||||
/// If using the scope bar, self.searchController.searchBar.selectedScopeButtonIndex.
|
||||
/// Otherwise, this is the selected index of the carousel, or NSNotFound if using neither.
|
||||
@property (nonatomic) NSInteger selectedScope;
|
||||
/// self.searchController.searchBar.text
|
||||
@property (nonatomic, readonly) NSString *searchText;
|
||||
|
||||
/// A totally optional delegate to forward search results updater calls to.
|
||||
/// If a delegate is set, updateSearchResults: is not called on this view controller.
|
||||
@property (nonatomic, weak ) id<FLEXSearchResultsUpdating> searchResultsUpdater;
|
||||
|
||||
/// self.view.window as a \c FLEXWindow
|
||||
@property (nonatomic, readonly) FLEXWindow *window;
|
||||
|
||||
/// Convenient for doing some async processor-intensive searching
|
||||
/// in the background before updating the UI back on the main queue.
|
||||
- (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock;
|
||||
|
||||
/// Adds up to 3 additional items to the toolbar in right-to-left order.
|
||||
///
|
||||
/// That is, the first item in the given array will be the rightmost item behind
|
||||
/// any existing toolbar items. By default, buttons for bookmarks and tabs are shown.
|
||||
///
|
||||
/// If you wish to have more control over how the buttons are arranged or which
|
||||
/// buttons are displayed, you can access the properties for the pre-existing
|
||||
/// toolbar items directly and manually set \c self.toolbarItems by overriding
|
||||
/// the \c setupToolbarItems method below.
|
||||
- (void)addToolbarItems:(NSArray<UIBarButtonItem *> *)items;
|
||||
|
||||
/// Subclasses may override. You should not need to call this method directly.
|
||||
- (void)setupToolbarItems;
|
||||
|
||||
@property (nonatomic, readonly) UIBarButtonItem *shareToolbarItem;
|
||||
@property (nonatomic, readonly) UIBarButtonItem *bookmarksToolbarItem;
|
||||
@property (nonatomic, readonly) UIBarButtonItem *openTabsToolbarItem;
|
||||
|
||||
/// Whether or not to display the "share" icon in the middle of the toolbar. NO by default.
|
||||
///
|
||||
/// Turning this on after you have added custom toolbar items will
|
||||
/// push off the leftmost toolbar item and shift the others leftward.
|
||||
@property (nonatomic) BOOL showsShareToolbarItem;
|
||||
/// Called when the share button is pressed.
|
||||
/// Default implementation does nothign. Subclasses may override.
|
||||
- (void)shareButtonPressed:(UIBarButtonItem *)sender;
|
||||
|
||||
/// Subclasses may call this to opt-out of all toolbar related behavior.
|
||||
/// This is necessary if you want to disable the gesture which reveals the toolbar.
|
||||
- (void)disableToolbar;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,590 @@
|
||||
//
|
||||
// FLEXTableViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 7/5/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXBookmarksViewController.h"
|
||||
#import "FLEXTabsViewController.h"
|
||||
#import "FLEXScopeCarousel.h"
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXResources.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@interface Block : NSObject
|
||||
- (void)invoke;
|
||||
@end
|
||||
|
||||
CGFloat const kFLEXDebounceInstant = 0.f;
|
||||
CGFloat const kFLEXDebounceFast = 0.05;
|
||||
CGFloat const kFLEXDebounceForAsyncSearch = 0.15;
|
||||
CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
|
||||
@interface FLEXTableViewController ()
|
||||
@property (nonatomic) NSTimer *debounceTimer;
|
||||
@property (nonatomic) BOOL didInitiallyRevealSearchBar;
|
||||
@property (nonatomic) UITableViewStyle style;
|
||||
|
||||
@property (nonatomic) BOOL hasAppeared;
|
||||
@property (nonatomic, readonly) UIView *tableHeaderViewContainer;
|
||||
|
||||
@property (nonatomic, readonly) BOOL manuallyDeactivateSearchOnDisappear;
|
||||
|
||||
@property (nonatomic) UIBarButtonItem *middleToolbarItem;
|
||||
@property (nonatomic) UIBarButtonItem *middleLeftToolbarItem;
|
||||
@property (nonatomic) UIBarButtonItem *leftmostToolbarItem;
|
||||
@end
|
||||
|
||||
@implementation FLEXTableViewController
|
||||
@dynamic tableView;
|
||||
@synthesize showsShareToolbarItem = _showsShareToolbarItem;
|
||||
@synthesize tableHeaderViewContainer = _tableHeaderViewContainer;
|
||||
@synthesize automaticallyShowsSearchBarCancelButton = _automaticallyShowsSearchBarCancelButton;
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
- (id)init {
|
||||
#if 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;
|
||||
_style = style;
|
||||
_manuallyDeactivateSearchOnDisappear = ({
|
||||
NSProcessInfo.processInfo.operatingSystemVersion.majorVersion < 11;
|
||||
});
|
||||
|
||||
// We will be our own search delegate if we implement this method
|
||||
if ([self respondsToSelector:@selector(updateSearchResults:)]) {
|
||||
self.searchDelegate = (id)self;
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (FLEXWindow *)window {
|
||||
return (id)self.view.window;
|
||||
}
|
||||
|
||||
- (void)setShowsSearchBar:(BOOL)showsSearchBar {
|
||||
if (_showsSearchBar == showsSearchBar) return;
|
||||
_showsSearchBar = showsSearchBar;
|
||||
|
||||
if (showsSearchBar) {
|
||||
UIViewController *results = self.searchResultsController;
|
||||
self.searchController = [[UISearchController alloc] initWithSearchResultsController:results];
|
||||
self.searchController.searchBar.placeholder = @"Filter";
|
||||
self.searchController.searchResultsUpdater = (id)self;
|
||||
self.searchController.delegate = (id)self;
|
||||
self.searchController.dimsBackgroundDuringPresentation = NO;
|
||||
self.searchController.hidesNavigationBarDuringPresentation = NO;
|
||||
/// Not necessary in iOS 13; remove this when iOS 13 is the minimum deployment target
|
||||
self.searchController.searchBar.delegate = self;
|
||||
|
||||
self.automaticallyShowsSearchBarCancelButton = YES;
|
||||
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
if (@available(iOS 13, *)) {
|
||||
self.searchController.automaticallyShowsScopeBar = NO;
|
||||
}
|
||||
#endif
|
||||
|
||||
[self addSearchController:self.searchController];
|
||||
} else {
|
||||
// Search already shown and just set to NO, so remove it
|
||||
[self removeSearchController:self.searchController];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setShowsCarousel:(BOOL)showsCarousel {
|
||||
if (_showsCarousel == showsCarousel) return;
|
||||
_showsCarousel = showsCarousel;
|
||||
|
||||
if (showsCarousel) {
|
||||
_carousel = ({
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
|
||||
FLEXScopeCarousel *carousel = [FLEXScopeCarousel new];
|
||||
carousel.selectedIndexChangedAction = ^(NSInteger idx) {
|
||||
__typeof(self) self = weakSelf;
|
||||
[self.searchDelegate updateSearchResults:self.searchText];
|
||||
};
|
||||
|
||||
// UITableView won't update the header size unless you reset the header view
|
||||
[carousel registerBlockForDynamicTypeChanges:^(FLEXScopeCarousel *carousel) {
|
||||
__typeof(self) self = weakSelf;
|
||||
[self layoutTableHeaderIfNeeded];
|
||||
}];
|
||||
|
||||
carousel;
|
||||
});
|
||||
[self addCarousel:_carousel];
|
||||
} else {
|
||||
// Carousel already shown and just set to NO, so remove it
|
||||
[self removeCarousel:_carousel];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSInteger)selectedScope {
|
||||
if (self.searchController.searchBar.showsScopeBar) {
|
||||
return self.searchController.searchBar.selectedScopeButtonIndex;
|
||||
} else if (self.showsCarousel) {
|
||||
return self.carousel.selectedIndex;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setSelectedScope:(NSInteger)selectedScope {
|
||||
if (self.searchController.searchBar.showsScopeBar) {
|
||||
self.searchController.searchBar.selectedScopeButtonIndex = selectedScope;
|
||||
} else if (self.showsCarousel) {
|
||||
self.carousel.selectedIndex = selectedScope;
|
||||
}
|
||||
|
||||
[self.searchDelegate updateSearchResults:self.searchText];
|
||||
}
|
||||
|
||||
- (NSString *)searchText {
|
||||
return self.searchController.searchBar.text;
|
||||
}
|
||||
|
||||
- (BOOL)automaticallyShowsSearchBarCancelButton {
|
||||
#if 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)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSArray *items = backgroundBlock();
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
mainBlock(items);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (void)setsShowsShareToolbarItem:(BOOL)showsShareToolbarItem {
|
||||
_showsShareToolbarItem = showsShareToolbarItem;
|
||||
if (self.isViewLoaded) {
|
||||
[self setupToolbarItems];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)disableToolbar {
|
||||
self.navigationController.toolbarHidden = YES;
|
||||
self.navigationController.hidesBarsOnSwipe = NO;
|
||||
self.toolbarItems = nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - View Controller Lifecycle
|
||||
|
||||
- (void)loadView {
|
||||
self.view = [FLEXTableView style:self.style];
|
||||
self.tableView.dataSource = self;
|
||||
self.tableView.delegate = self;
|
||||
|
||||
_shareToolbarItem = FLEXBarButtonItemSystem(Action, self, @selector(shareButtonPressed:));
|
||||
_bookmarksToolbarItem = [UIBarButtonItem
|
||||
itemWithImage:FLEXResources.bookmarksIcon target:self action:@selector(showBookmarks)
|
||||
];
|
||||
_openTabsToolbarItem = [UIBarButtonItem
|
||||
itemWithImage:FLEXResources.openTabsIcon target:self action:@selector(showTabSwitcher)
|
||||
];
|
||||
|
||||
self.leftmostToolbarItem = UIBarButtonItem.flex_fixedSpace;
|
||||
self.middleLeftToolbarItem = UIBarButtonItem.flex_fixedSpace;
|
||||
self.middleToolbarItem = UIBarButtonItem.flex_fixedSpace;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
|
||||
|
||||
// Toolbar
|
||||
self.navigationController.toolbarHidden = NO;
|
||||
self.navigationController.hidesBarsOnSwipe = YES;
|
||||
|
||||
// On iOS 13, the root view controller shows it's search bar no matter what.
|
||||
// Turning this off avoids some weird flash the navigation bar does when we
|
||||
// toggle navigationItem.hidesSearchBarWhenScrolling on and off. The flash
|
||||
// will still happen on subsequent view controllers, but we can at least
|
||||
// avoid it for the root view controller
|
||||
if (@available(iOS 13, *)) {
|
||||
if (self.navigationController.viewControllers.firstObject == self) {
|
||||
_showSearchBarInitially = NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
[self setupToolbarItems];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
// Allow scrolling to collapse the search bar, only if we don't want it pinned
|
||||
if (@available(iOS 11.0, *)) {
|
||||
if (self.showSearchBarInitially && !self.pinSearchBar && !self.didInitiallyRevealSearchBar) {
|
||||
// All this mumbo jumbo is necessary to work around a bug in iOS 13 up to 13.2
|
||||
// wherein quickly toggling navigationItem.hidesSearchBarWhenScrolling to make
|
||||
// the search bar appear initially results in a bugged search bar that
|
||||
// becomes transparent and floats over the screen as you scroll
|
||||
[UIView animateWithDuration:0.2 animations:^{
|
||||
self.navigationItem.hidesSearchBarWhenScrolling = YES;
|
||||
[self.navigationController.view setNeedsLayout];
|
||||
[self.navigationController.view layoutIfNeeded];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
// We only want to reveal the search bar when the view controller first appears.
|
||||
self.didInitiallyRevealSearchBar = YES;
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
[super viewWillDisappear:animated];
|
||||
|
||||
if (self.manuallyDeactivateSearchOnDisappear && self.searchController.isActive) {
|
||||
self.searchController.active = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didMoveToParentViewController:(UIViewController *)parent {
|
||||
[super didMoveToParentViewController:parent];
|
||||
// Reset this since we are re-appearing under a new
|
||||
// parent view controller and need to show it again
|
||||
self.didInitiallyRevealSearchBar = NO;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Toolbar, Public
|
||||
|
||||
- (void)setupToolbarItems {
|
||||
if (!self.isViewLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.toolbarItems = @[
|
||||
self.leftmostToolbarItem,
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
self.middleLeftToolbarItem,
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
self.middleToolbarItem,
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
self.bookmarksToolbarItem,
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
self.openTabsToolbarItem,
|
||||
];
|
||||
|
||||
for (UIBarButtonItem *item in self.toolbarItems) {
|
||||
[item _setWidth:60];
|
||||
// This does not work for anything but fixed spaces for some reason
|
||||
// item.width = 60;
|
||||
}
|
||||
|
||||
// Disable tabs entirely when not presented by FLEXExplorerViewController
|
||||
UIViewController *presenter = self.navigationController.presentingViewController;
|
||||
if (![presenter isKindOfClass:[FLEXExplorerViewController class]]) {
|
||||
self.openTabsToolbarItem.enabled = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addToolbarItems:(NSArray<UIBarButtonItem *> *)items {
|
||||
if (self.showsShareToolbarItem) {
|
||||
// Share button is in the middle, skip middle button
|
||||
if (items.count > 0) {
|
||||
self.middleLeftToolbarItem = items[0];
|
||||
}
|
||||
if (items.count > 1) {
|
||||
self.leftmostToolbarItem = items[1];
|
||||
}
|
||||
} else {
|
||||
// Add buttons right-to-left
|
||||
if (items.count > 0) {
|
||||
self.middleToolbarItem = items[0];
|
||||
}
|
||||
if (items.count > 1) {
|
||||
self.middleLeftToolbarItem = items[1];
|
||||
}
|
||||
if (items.count > 2) {
|
||||
self.leftmostToolbarItem = items[2];
|
||||
}
|
||||
}
|
||||
|
||||
[self setupToolbarItems];
|
||||
}
|
||||
|
||||
- (void)setShowsShareToolbarItem:(BOOL)showShare {
|
||||
if (_showsShareToolbarItem != showShare) {
|
||||
_showsShareToolbarItem = showShare;
|
||||
|
||||
if (showShare) {
|
||||
// Push out leftmost item
|
||||
self.leftmostToolbarItem = self.middleLeftToolbarItem;
|
||||
self.middleLeftToolbarItem = self.middleToolbarItem;
|
||||
|
||||
// Use share for middle
|
||||
self.middleToolbarItem = self.shareToolbarItem;
|
||||
} else {
|
||||
// Remove share, shift custom items rightward
|
||||
self.middleToolbarItem = self.middleLeftToolbarItem;
|
||||
self.middleLeftToolbarItem = self.leftmostToolbarItem;
|
||||
self.leftmostToolbarItem = UIBarButtonItem.flex_fixedSpace;
|
||||
}
|
||||
}
|
||||
|
||||
[self setupToolbarItems];
|
||||
}
|
||||
|
||||
- (void)shareButtonPressed:(UIBarButtonItem *)sender {
|
||||
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)debounce:(void(^)(void))block {
|
||||
[self.debounceTimer invalidate];
|
||||
|
||||
self.debounceTimer = [NSTimer
|
||||
scheduledTimerWithTimeInterval:self.searchBarDebounceInterval
|
||||
target:block
|
||||
selector:@selector(invoke)
|
||||
userInfo:nil
|
||||
repeats:NO
|
||||
];
|
||||
}
|
||||
|
||||
- (void)layoutTableHeaderIfNeeded {
|
||||
if (self.showsCarousel) {
|
||||
self.carousel.frame = FLEXRectSetHeight(
|
||||
self.carousel.frame, self.carousel.intrinsicContentSize.height
|
||||
);
|
||||
}
|
||||
|
||||
self.tableView.tableHeaderView = self.tableView.tableHeaderView;
|
||||
}
|
||||
|
||||
- (void)addCarousel:(FLEXScopeCarousel *)carousel {
|
||||
if (@available(iOS 11.0, *)) {
|
||||
self.tableView.tableHeaderView = carousel;
|
||||
} else {
|
||||
carousel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
|
||||
|
||||
CGRect frame = self.tableHeaderViewContainer.frame;
|
||||
CGRect subviewFrame = carousel.frame;
|
||||
subviewFrame.origin.y = 0;
|
||||
|
||||
// Put the carousel below the search bar if it's already there
|
||||
if (self.showsSearchBar) {
|
||||
carousel.frame = subviewFrame = FLEXRectSetY(
|
||||
subviewFrame, self.searchController.searchBar.frame.size.height
|
||||
);
|
||||
frame.size.height += carousel.intrinsicContentSize.height;
|
||||
} else {
|
||||
frame.size.height = carousel.intrinsicContentSize.height;
|
||||
}
|
||||
|
||||
self.tableHeaderViewContainer.frame = frame;
|
||||
[self.tableHeaderViewContainer addSubview:carousel];
|
||||
}
|
||||
|
||||
[self layoutTableHeaderIfNeeded];
|
||||
}
|
||||
|
||||
- (void)removeCarousel:(FLEXScopeCarousel *)carousel {
|
||||
[carousel removeFromSuperview];
|
||||
|
||||
if (@available(iOS 11.0, *)) {
|
||||
self.tableView.tableHeaderView = nil;
|
||||
} else {
|
||||
if (self.showsSearchBar) {
|
||||
[self removeSearchController:self.searchController];
|
||||
[self addSearchController:self.searchController];
|
||||
} else {
|
||||
self.tableView.tableHeaderView = nil;
|
||||
_tableHeaderViewContainer = nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addSearchController:(UISearchController *)controller {
|
||||
if (@available(iOS 11.0, *)) {
|
||||
self.navigationItem.searchController = controller;
|
||||
} else {
|
||||
controller.searchBar.autoresizingMask |= UIViewAutoresizingFlexibleBottomMargin;
|
||||
[self.tableHeaderViewContainer addSubview:controller.searchBar];
|
||||
CGRect subviewFrame = controller.searchBar.frame;
|
||||
CGRect frame = self.tableHeaderViewContainer.frame;
|
||||
frame.size.width = MAX(frame.size.width, subviewFrame.size.width);
|
||||
frame.size.height = subviewFrame.size.height;
|
||||
|
||||
// Move the carousel down if it's already there
|
||||
if (self.showsCarousel) {
|
||||
self.carousel.frame = FLEXRectSetY(
|
||||
self.carousel.frame, subviewFrame.size.height
|
||||
);
|
||||
frame.size.height += self.carousel.frame.size.height;
|
||||
}
|
||||
|
||||
self.tableHeaderViewContainer.frame = frame;
|
||||
[self layoutTableHeaderIfNeeded];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeSearchController:(UISearchController *)controller {
|
||||
if (@available(iOS 11.0, *)) {
|
||||
self.navigationItem.searchController = nil;
|
||||
} else {
|
||||
[controller.searchBar removeFromSuperview];
|
||||
|
||||
if (self.showsCarousel) {
|
||||
// self.carousel.frame = FLEXRectRemake(CGPointZero, self.carousel.frame.size);
|
||||
[self removeCarousel:self.carousel];
|
||||
[self addCarousel:self.carousel];
|
||||
} else {
|
||||
self.tableView.tableHeaderView = nil;
|
||||
_tableHeaderViewContainer = nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (UIView *)tableHeaderViewContainer {
|
||||
if (!_tableHeaderViewContainer) {
|
||||
_tableHeaderViewContainer = [UIView new];
|
||||
self.tableView.tableHeaderView = self.tableHeaderViewContainer;
|
||||
}
|
||||
|
||||
return _tableHeaderViewContainer;
|
||||
}
|
||||
|
||||
- (void)showBookmarks {
|
||||
UINavigationController *nav = [[UINavigationController alloc]
|
||||
initWithRootViewController:[FLEXBookmarksViewController new]
|
||||
];
|
||||
[self presentViewController:nav animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)showTabSwitcher {
|
||||
UINavigationController *nav = [[UINavigationController alloc]
|
||||
initWithRootViewController:[FLEXTabsViewController new]
|
||||
];
|
||||
[self presentViewController:nav animated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Search Bar
|
||||
|
||||
#pragma mark UISearchResultsUpdating
|
||||
|
||||
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
|
||||
[self.debounceTimer invalidate];
|
||||
NSString *text = searchController.searchBar.text;
|
||||
|
||||
void (^updateSearchResults)() = ^{
|
||||
if (self.searchResultsUpdater) {
|
||||
[self.searchResultsUpdater updateSearchResults:text];
|
||||
} else {
|
||||
[self.searchDelegate updateSearchResults:text];
|
||||
}
|
||||
};
|
||||
|
||||
// Only debounce if we want to, and if we have a non-empty string
|
||||
// Empty string events are sent instantly
|
||||
if (text.length && self.searchBarDebounceInterval > kFLEXDebounceInstant) {
|
||||
[self debounce:updateSearchResults];
|
||||
} else {
|
||||
updateSearchResults();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark UISearchControllerDelegate
|
||||
|
||||
- (void)willPresentSearchController:(UISearchController *)searchController {
|
||||
// Manually show cancel button for < iOS 13
|
||||
if (!@available(iOS 13, *) && self.automaticallyShowsSearchBarCancelButton) {
|
||||
[searchController.searchBar setShowsCancelButton:YES animated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)willDismissSearchController:(UISearchController *)searchController {
|
||||
// Manually hide cancel button for < iOS 13
|
||||
if (!@available(iOS 13, *) && self.automaticallyShowsSearchBarCancelButton) {
|
||||
[searchController.searchBar setShowsCancelButton:NO animated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark UISearchBarDelegate
|
||||
|
||||
/// Not necessary in iOS 13; remove this when iOS 13 is the deployment target
|
||||
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope {
|
||||
[self updateSearchResultsForSearchController:self.searchController];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Table View
|
||||
|
||||
/// Not having a title in the first section looks weird with a rounded-corner table view style
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
if (@available(iOS 13, *)) {
|
||||
if (self.style == UITableViewStyleInsetGrouped) {
|
||||
return @" ";
|
||||
}
|
||||
}
|
||||
|
||||
return nil; // For plain/gropued style
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// FLEXSingleRowSection.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 9/25/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewSection.h"
|
||||
|
||||
/// A section providing a specific single row.
|
||||
///
|
||||
/// You may optionally provide a view controller to push when the row
|
||||
/// is selected, or an action to perform when it is selected.
|
||||
/// Which one is used first is up to the table view data source.
|
||||
@interface FLEXSingleRowSection : FLEXTableViewSection
|
||||
|
||||
/// @param reuseIdentifier if nil, kFLEXDefaultCell is used.
|
||||
+ (instancetype)title:(NSString *)sectionTitle
|
||||
reuse:(NSString *)reuseIdentifier
|
||||
cell:(void(^)(__kindof UITableViewCell *cell))cellConfiguration;
|
||||
|
||||
@property (nonatomic) UIViewController *pushOnSelection;
|
||||
@property (nonatomic) void (^selectionAction)(UIViewController *host);
|
||||
/// Called to determine whether the single row should display itself or not.
|
||||
@property (nonatomic) BOOL (^filterMatcher)(NSString *filterText);
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// FLEXSingleRowSection.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 9/25/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXSingleRowSection.h"
|
||||
#import "FLEXTableView.h"
|
||||
|
||||
@interface FLEXSingleRowSection ()
|
||||
@property (nonatomic, readonly) NSString *reuseIdentifier;
|
||||
@property (nonatomic, readonly) void (^cellConfiguration)(__kindof UITableViewCell *cell);
|
||||
|
||||
@property (nonatomic) NSString *lastTitle;
|
||||
@property (nonatomic) NSString *lastSubitle;
|
||||
@end
|
||||
|
||||
@implementation FLEXSingleRowSection
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
+ (instancetype)title:(NSString *)title
|
||||
reuse:(NSString *)reuse
|
||||
cell:(void (^)(__kindof UITableViewCell *))config {
|
||||
return [[self alloc] initWithTitle:title reuse:reuse cell:config];
|
||||
}
|
||||
|
||||
- (id)initWithTitle:(NSString *)sectionTitle
|
||||
reuse:(NSString *)reuseIdentifier
|
||||
cell:(void (^)(__kindof UITableViewCell *))cellConfiguration {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_title = sectionTitle;
|
||||
_reuseIdentifier = reuseIdentifier ?: kFLEXDefaultCell;
|
||||
_cellConfiguration = cellConfiguration;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Overrides
|
||||
|
||||
- (NSInteger)numberOfRows {
|
||||
if (self.filterMatcher && self.filterText.length) {
|
||||
return self.filterMatcher(self.filterText) ? 1 : 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (BOOL)canSelectRow:(NSInteger)row {
|
||||
return self.pushOnSelection || self.selectionAction;
|
||||
}
|
||||
|
||||
- (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row {
|
||||
return self.selectionAction;
|
||||
}
|
||||
|
||||
- (UIViewController *)viewControllerToPushForRow:(NSInteger)row {
|
||||
return self.pushOnSelection;
|
||||
}
|
||||
|
||||
- (NSString *)reuseIdentifierForRow:(NSInteger)row {
|
||||
return self.reuseIdentifier;
|
||||
}
|
||||
|
||||
- (void)configureCell:(__kindof UITableViewCell *)cell forRow:(NSInteger)row {
|
||||
cell.textLabel.text = nil;
|
||||
cell.detailTextLabel.text = nil;
|
||||
cell.accessoryType = UITableViewCellAccessoryNone;
|
||||
|
||||
self.cellConfiguration(cell);
|
||||
self.lastTitle = cell.textLabel.text;
|
||||
self.lastSubitle = cell.detailTextLabel.text;
|
||||
}
|
||||
|
||||
- (NSString *)titleForRow:(NSInteger)row {
|
||||
return self.lastTitle;
|
||||
}
|
||||
|
||||
- (NSString *)subtitleForRow:(NSInteger)row {
|
||||
return self.lastSubitle;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,134 @@
|
||||
//
|
||||
// FLEXTableViewSection.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 1/29/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXMacros.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
@class FLEXTableView;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#pragma mark FLEXTableViewSection
|
||||
|
||||
/// An abstract base class for table view sections.
|
||||
///
|
||||
/// Many properties or methods here return nil or some logical equivalent by default.
|
||||
/// Even so, most of the methods with defaults are intended to be overriden by subclasses.
|
||||
/// Some methods are not implemented at all and MUST be implemented by a subclass.
|
||||
@interface FLEXTableViewSection : NSObject {
|
||||
@protected
|
||||
/// Unused by default, use if you want
|
||||
NSString *_title;
|
||||
}
|
||||
|
||||
#pragma mark - Data
|
||||
|
||||
/// A title to be displayed for the custom section.
|
||||
/// Subclasses may override or use the \c _title ivar.
|
||||
@property (nonatomic, readonly, nullable) NSString *title;
|
||||
/// The number of rows in this section. Subclasses must override.
|
||||
/// This should not change until \c filterText is changed or \c reloadData is called.
|
||||
@property (nonatomic, readonly) NSInteger numberOfRows;
|
||||
/// A map of reuse identifiers to \c UITableViewCell (sub)class objects.
|
||||
/// Subclasses \e may override this as necessary, but are not required to.
|
||||
/// See \c FLEXTableView.h for more information.
|
||||
/// @return nil by default.
|
||||
@property (nonatomic, readonly, nullable) NSDictionary<NSString *, Class> *cellRegistrationMapping;
|
||||
|
||||
/// The section should filter itself based on the contents of this property
|
||||
/// as it is set. If it is set to nil or an empty string, it should not filter.
|
||||
/// Subclasses should override or observe this property and react to changes.
|
||||
///
|
||||
/// It is common practice to use two arrays for the underlying model:
|
||||
/// One to hold all rows, and one to hold unfiltered rows. When \c setFilterText:
|
||||
/// is called, call \c super to store the new value, and re-filter your model accordingly.
|
||||
@property (nonatomic, nullable) NSString *filterText;
|
||||
|
||||
/// Provides an avenue for the section to refresh data or change the number of rows.
|
||||
///
|
||||
/// This is called before reloading the table view itself. If your section pulls data
|
||||
/// from an external data source, this is a good place to refresh that data entirely.
|
||||
/// If your section does not, then it might be simpler for you to just override
|
||||
/// \c setFilterText: to call \c super and call \c reloadData.
|
||||
- (void)reloadData;
|
||||
|
||||
#pragma mark - Row Selection
|
||||
|
||||
/// Whether the given row should be selectable, such as if tapping the cell
|
||||
/// should take the user to a new screen or trigger an action.
|
||||
/// Subclasses \e may override this as necessary, but are not required to.
|
||||
/// @return \c NO by default
|
||||
- (BOOL)canSelectRow:(NSInteger)row;
|
||||
|
||||
/// An action "future" to be triggered when the row is selected, if the row
|
||||
/// supports being selected as indicated by \c canSelectRow:. Subclasses
|
||||
/// must implement this in accordance with how they implement \c canSelectRow:
|
||||
/// if they do not implement \c viewControllerToPushForRow:
|
||||
/// @return This returns \c nil if no view controller is provided by
|
||||
/// \c viewControllerToPushForRow: — otherwise it pushes that view controller
|
||||
/// onto \c host.navigationController
|
||||
- (nullable void(^)(__kindof UIViewController *host))didSelectRowAction:(NSInteger)row;
|
||||
|
||||
/// A view controller to display when the row is selected, if the row
|
||||
/// supports being selected as indicated by \c canSelectRow:. Subclasses
|
||||
/// must implement this in accordance with how they implement \c canSelectRow:
|
||||
/// if they do not implement \c didSelectRowAction:
|
||||
/// @return \c nil by default
|
||||
- (nullable UIViewController *)viewControllerToPushForRow:(NSInteger)row;
|
||||
|
||||
/// Called when the accessory view's detail button is pressed.
|
||||
/// @return \c nil by default.
|
||||
- (nullable void(^)(__kindof UIViewController *host))didPressInfoButtonAction:(NSInteger)row;
|
||||
|
||||
#pragma mark - Context Menus
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
|
||||
/// By default, this is the title of the row.
|
||||
/// @return The title of the context menu, if any.
|
||||
- (nullable NSString *)menuTitleForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
|
||||
/// Protected, not intended for public use. \c menuTitleForRow:
|
||||
/// already includes the value returned from this method.
|
||||
///
|
||||
/// By default, this returns \c @"". Subclasses may override to
|
||||
/// provide a detailed description of the target of the context menu.
|
||||
- (NSString *)menuSubtitleForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
|
||||
/// The context menu items, if any. Subclasses may override.
|
||||
/// By default, only inludes items for \c copyMenuItemsForRow:.
|
||||
- (nullable NSArray<UIMenuElement *> *)menuItemsForRow:(NSInteger)row sender:(UIViewController *)sender API_AVAILABLE(ios(13.0));
|
||||
/// Subclasses may override to return a list of copiable items.
|
||||
///
|
||||
/// Every two elements in the list compose a key-value pair, where the key
|
||||
/// should be a description of what will be copied, and the values should be
|
||||
/// the strings to copy. Return an empty string as a value to show a disabled action.
|
||||
- (nullable NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
|
||||
#endif
|
||||
|
||||
#pragma mark - Cell Configuration
|
||||
|
||||
/// Provide a reuse identifier for the given row. Subclasses should override.
|
||||
///
|
||||
/// Custom reuse identifiers should be specified in \c cellRegistrationMapping.
|
||||
/// You may return any of the identifiers in \c FLEXTableView.h
|
||||
/// without including them in the \c cellRegistrationMapping.
|
||||
/// @return \c kFLEXDefaultCell by default.
|
||||
- (NSString *)reuseIdentifierForRow:(NSInteger)row;
|
||||
/// Configure a cell for the given row. Subclasses must override.
|
||||
- (void)configureCell:(__kindof UITableViewCell *)cell forRow:(NSInteger)row;
|
||||
|
||||
#pragma mark - External Convenience
|
||||
|
||||
/// For use by whatever view controller uses your section. Not required.
|
||||
/// @return An optional title.
|
||||
- (nullable NSString *)titleForRow:(NSInteger)row;
|
||||
/// For use by whatever view controller uses your section. Not required.
|
||||
/// @return An optional subtitle.
|
||||
- (nullable NSString *)subtitleForRow:(NSInteger)row;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,128 @@
|
||||
//
|
||||
// FLEXTableViewSection.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 1/29/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewSection.h"
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "UIMenu+FLEX.h"
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wincomplete-implementation"
|
||||
|
||||
@implementation FLEXTableViewSection
|
||||
|
||||
- (NSInteger)numberOfRows {
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (void)reloadData { }
|
||||
|
||||
- (NSDictionary<NSString *,Class> *)cellRegistrationMapping {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)canSelectRow:(NSInteger)row { return NO; }
|
||||
|
||||
- (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row {
|
||||
UIViewController *toPush = [self viewControllerToPushForRow:row];
|
||||
if (toPush) {
|
||||
return ^(UIViewController *host) {
|
||||
[host.navigationController pushViewController:toPush animated:YES];
|
||||
};
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (UIViewController *)viewControllerToPushForRow:(NSInteger)row {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void (^)(__kindof UIViewController *))didPressInfoButtonAction:(NSInteger)row {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSString *)reuseIdentifierForRow:(NSInteger)row {
|
||||
return kFLEXDefaultCell;
|
||||
}
|
||||
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
|
||||
- (NSString *)menuTitleForRow:(NSInteger)row {
|
||||
NSString *title = [self titleForRow:row];
|
||||
NSString *subtitle = [self menuSubtitleForRow:row];
|
||||
|
||||
if (subtitle.length) {
|
||||
return [NSString stringWithFormat:@"%@\n\n%@", title, subtitle];
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
- (NSString *)menuSubtitleForRow:(NSInteger)row {
|
||||
return @"";
|
||||
}
|
||||
|
||||
- (NSArray<UIMenuElement *> *)menuItemsForRow:(NSInteger)row sender:(UIViewController *)sender API_AVAILABLE(ios(13)) {
|
||||
NSArray<NSString *> *copyItems = [self copyMenuItemsForRow:row];
|
||||
NSAssert(copyItems.count % 2 == 0, @"copyMenuItemsForRow: should return an even list");
|
||||
|
||||
if (copyItems.count) {
|
||||
NSInteger numberOfActions = copyItems.count / 2;
|
||||
BOOL collapseMenu = numberOfActions > 4;
|
||||
UIImage *copyIcon = [UIImage systemImageNamed:@"doc.on.doc"];
|
||||
|
||||
NSMutableArray *actions = [NSMutableArray new];
|
||||
|
||||
for (NSInteger i = 0; i < copyItems.count; i += 2) {
|
||||
NSString *key = copyItems[i], *value = copyItems[i+1];
|
||||
NSString *title = collapseMenu ? key : [@"Copy " stringByAppendingString:key];
|
||||
|
||||
UIAction *copy = [UIAction
|
||||
actionWithTitle:title
|
||||
image:copyIcon
|
||||
identifier:nil
|
||||
handler:^(__kindof UIAction *action) {
|
||||
UIPasteboard.generalPasteboard.string = value;
|
||||
}
|
||||
];
|
||||
if (!value.length) {
|
||||
copy.attributes = UIMenuElementAttributesDisabled;
|
||||
}
|
||||
|
||||
[actions addObject:copy];
|
||||
}
|
||||
|
||||
UIMenu *copyMenu = [UIMenu
|
||||
inlineMenuWithTitle:@"Copy…"
|
||||
image:copyIcon
|
||||
children:actions
|
||||
];
|
||||
|
||||
if (collapseMenu) {
|
||||
return @[[copyMenu collapsed]];
|
||||
} else {
|
||||
return @[copyMenu];
|
||||
}
|
||||
}
|
||||
|
||||
return @[];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
- (NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSString *)titleForRow:(NSInteger)row { return nil; }
|
||||
- (NSString *)subtitleForRow:(NSInteger)row { return nil; }
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
@@ -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,93 @@
|
||||
//
|
||||
// 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.selectionIndicatorStripe.backgroundColor = self.tintColor;
|
||||
if (@available(iOS 10, *)) {
|
||||
self.titleLabel.adjustsFontForContentSizeCategory = YES;
|
||||
}
|
||||
|
||||
[self.contentView addSubview:self.titleLabel];
|
||||
[self.contentView addSubview:self.selectionIndicatorStripe];
|
||||
|
||||
[self installConstraints];
|
||||
|
||||
[self updateAppearance];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)updateAppearance {
|
||||
self.selectionIndicatorStripe.hidden = !self.selected;
|
||||
|
||||
if (self.selected) {
|
||||
self.titleLabel.textColor = self.tintColor;
|
||||
} else {
|
||||
self.titleLabel.textColor = FLEXColor.deemphasizedTextColor;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Public
|
||||
|
||||
- (NSString *)title {
|
||||
return self.titleLabel.text;
|
||||
}
|
||||
|
||||
- (void)setTitle:(NSString *)title {
|
||||
self.titleLabel.text = title;
|
||||
[self.titleLabel sizeToFit];
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
#pragma mark Overrides
|
||||
|
||||
- (void)prepareForReuse {
|
||||
[super prepareForReuse];
|
||||
[self updateAppearance];
|
||||
}
|
||||
|
||||
- (void)installConstraints {
|
||||
CGFloat stripeHeight = 2;
|
||||
|
||||
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.selectionIndicatorStripe.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
UIView *superview = self.contentView;
|
||||
[self.titleLabel 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,204 @@
|
||||
//
|
||||
// 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) id dynamicTypeObserver;
|
||||
@property (nonatomic, readonly) NSMutableArray *dynamicTypeHandlers;
|
||||
|
||||
@property (nonatomic) BOOL constraintsInstalled;
|
||||
@end
|
||||
|
||||
@implementation FLEXScopeCarousel
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.backgroundColor = FLEXColor.primaryBackgroundColor;
|
||||
self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
self.translatesAutoresizingMaskIntoConstraints = YES;
|
||||
_dynamicTypeHandlers = [NSMutableArray new];
|
||||
|
||||
CGSize itemSize = CGSizeZero;
|
||||
if (@available(iOS 10.0, *)) {
|
||||
itemSize = UICollectionViewFlowLayoutAutomaticSize;
|
||||
}
|
||||
|
||||
// Collection view layout
|
||||
UICollectionViewFlowLayout *layout = ({
|
||||
UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new];
|
||||
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
|
||||
layout.sectionInset = UIEdgeInsetsZero;
|
||||
layout.minimumLineSpacing = kCarouselItemSpacing;
|
||||
layout.itemSize = itemSize;
|
||||
layout.estimatedItemSize = itemSize;
|
||||
layout;
|
||||
});
|
||||
|
||||
// Collection view
|
||||
_collectionView = ({
|
||||
UICollectionView *cv = [[UICollectionView alloc]
|
||||
initWithFrame:CGRectZero
|
||||
collectionViewLayout:layout
|
||||
];
|
||||
cv.showsHorizontalScrollIndicator = NO;
|
||||
cv.backgroundColor = UIColor.clearColor;
|
||||
cv.delegate = self;
|
||||
cv.dataSource = self;
|
||||
[cv registerClass:[FLEXCarouselCell class] forCellWithReuseIdentifier:kCarouselCellReuseIdentifier];
|
||||
|
||||
[self addSubview:cv];
|
||||
cv;
|
||||
});
|
||||
|
||||
|
||||
// Sizing cell
|
||||
_sizingCell = [FLEXCarouselCell new];
|
||||
self.sizingCell.title = @"NSObject";
|
||||
|
||||
// Dynamic type
|
||||
__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.collectionView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self.collectionView pinEdgesToSuperview];
|
||||
|
||||
self.constraintsInstalled = YES;
|
||||
}
|
||||
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
- (CGSize)intrinsicContentSize {
|
||||
return CGSizeMake(
|
||||
UIViewNoIntrinsicMetric,
|
||||
[self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height
|
||||
);
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)setItems:(NSArray<NSString *> *)items {
|
||||
NSParameterAssert(items.count);
|
||||
|
||||
_items = items.copy;
|
||||
|
||||
// Refresh list, select first item initially
|
||||
[self.collectionView reloadData];
|
||||
self.selectedIndex = 0;
|
||||
}
|
||||
|
||||
- (void)setSelectedIndex:(NSInteger)idx {
|
||||
NSParameterAssert(idx < self.items.count);
|
||||
|
||||
_selectedIndex = idx;
|
||||
NSIndexPath *path = [NSIndexPath indexPathForItem:idx inSection:0];
|
||||
[self.collectionView selectItemAtIndexPath:path
|
||||
animated:YES
|
||||
scrollPosition:UICollectionViewScrollPositionCenteredHorizontally];
|
||||
[self collectionView:self.collectionView didSelectItemAtIndexPath:path];
|
||||
}
|
||||
|
||||
- (void)registerBlockForDynamicTypeChanges:(void (^)(FLEXScopeCarousel *))handler {
|
||||
[self.dynamicTypeHandlers addObject:handler];
|
||||
}
|
||||
|
||||
#pragma mark - UICollectionView
|
||||
|
||||
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
// if (@available(iOS 10.0, *)) {
|
||||
// return UICollectionViewFlowLayoutAutomaticSize;
|
||||
// }
|
||||
|
||||
self.sizingCell.title = self.items[indexPath.item];
|
||||
return [self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
|
||||
}
|
||||
|
||||
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
||||
return self.items.count;
|
||||
}
|
||||
|
||||
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
|
||||
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
FLEXCarouselCell *cell = (id)[collectionView dequeueReusableCellWithReuseIdentifier:kCarouselCellReuseIdentifier
|
||||
forIndexPath:indexPath];
|
||||
cell.title = self.items[indexPath.row];
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
_selectedIndex = indexPath.item; // In case self.selectedIndex didn't trigger this call
|
||||
|
||||
if (self.selectedIndexChangedAction) {
|
||||
self.selectedIndexChangedAction(indexPath.row);
|
||||
}
|
||||
|
||||
// TODO: dynamically choose a scroll position. Very wide items should
|
||||
// get "Left" while smaller items should not scroll at all, unless
|
||||
// they are only partially on the screen, in which case they
|
||||
// should get "HorizontallyCentered" to bring them onto the screen.
|
||||
// For now, everything goes to the left, as this has a similar effect.
|
||||
[collectionView scrollToItemAtIndexPath:indexPath
|
||||
atScrollPosition:UICollectionViewScrollPositionLeft
|
||||
animated:YES];
|
||||
[self sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEXCodeFontCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 12/27/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXMultilineTableViewCell.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXCodeFontCell : FLEXMultilineDetailTableViewCell
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// FLEXCodeFontCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 12/27/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXCodeFontCell.h"
|
||||
#import "UIFont+FLEX.h"
|
||||
|
||||
@implementation FLEXCodeFontCell
|
||||
|
||||
- (void)postInit {
|
||||
[super postInit];
|
||||
|
||||
self.titleLabel.font = UIFont.flex_codeFont;
|
||||
self.subtitleLabel.font = UIFont.flex_codeFont;
|
||||
|
||||
self.titleLabel.adjustsFontSizeToFitWidth = YES;
|
||||
self.titleLabel.minimumScaleFactor = 0.9;
|
||||
self.subtitleLabel.adjustsFontSizeToFitWidth = YES;
|
||||
self.subtitleLabel.minimumScaleFactor = 0.75;
|
||||
|
||||
// Disable mutli-line pre iOS 11
|
||||
if (@available(iOS 11, *)) {
|
||||
self.subtitleLabel.numberOfLines = 5;
|
||||
} else {
|
||||
self.titleLabel.numberOfLines = 1;
|
||||
self.subtitleLabel.numberOfLines = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXKeyValueTableViewCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 1/23/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewCell.h"
|
||||
|
||||
@interface FLEXKeyValueTableViewCell : FLEXTableViewCell
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEXKeyValueTableViewCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 1/23/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXKeyValueTableViewCell.h"
|
||||
|
||||
@implementation FLEXKeyValueTableViewCell
|
||||
|
||||
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
return [super initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:reuseIdentifier];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// FLEXMultilineTableViewCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Ryan Olson on 2/13/15.
|
||||
// Copyright (c) 2015 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewCell.h"
|
||||
|
||||
/// A cell with both labels set to be multi-line capable.
|
||||
@interface FLEXMultilineTableViewCell : FLEXTableViewCell
|
||||
|
||||
+ (CGFloat)preferredHeightWithAttributedText:(NSAttributedString *)attributedText
|
||||
maxWidth:(CGFloat)contentViewWidth
|
||||
style:(UITableViewStyle)style
|
||||
showsAccessory:(BOOL)showsAccessory;
|
||||
|
||||
@end
|
||||
|
||||
/// A \c FLEXMultilineTableViewCell initialized with \c UITableViewCellStyleSubtitle
|
||||
@interface FLEXMultilineDetailTableViewCell : FLEXMultilineTableViewCell
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// FLEXMultilineTableViewCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Ryan Olson on 2/13/15.
|
||||
// Copyright (c) 2015 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXMultilineTableViewCell.h"
|
||||
#import "UIView+FLEX_Layout.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXMultilineTableViewCell ()
|
||||
@property (nonatomic, readonly) UILabel *_titleLabel;
|
||||
@property (nonatomic, readonly) UILabel *_subtitleLabel;
|
||||
@property (nonatomic) BOOL constraintsUpdated;
|
||||
@end
|
||||
|
||||
@implementation FLEXMultilineTableViewCell
|
||||
|
||||
- (void)postInit {
|
||||
[super postInit];
|
||||
|
||||
self.titleLabel.numberOfLines = 0;
|
||||
self.subtitleLabel.numberOfLines = 0;
|
||||
}
|
||||
|
||||
+ (UIEdgeInsets)labelInsets {
|
||||
return UIEdgeInsetsMake(10.0, 16.0, 10.0, 8.0);
|
||||
}
|
||||
|
||||
+ (CGFloat)preferredHeightWithAttributedText:(NSAttributedString *)attributedText
|
||||
maxWidth:(CGFloat)contentViewWidth
|
||||
style:(UITableViewStyle)style
|
||||
showsAccessory:(BOOL)showsAccessory {
|
||||
CGFloat labelWidth = contentViewWidth;
|
||||
|
||||
// Content view inset due to accessory view observed on iOS 8.1 iPhone 6.
|
||||
if (showsAccessory) {
|
||||
labelWidth -= 34.0;
|
||||
}
|
||||
|
||||
UIEdgeInsets labelInsets = [self labelInsets];
|
||||
labelWidth -= (labelInsets.left + labelInsets.right);
|
||||
|
||||
CGSize constrainSize = CGSizeMake(labelWidth, CGFLOAT_MAX);
|
||||
CGRect boundingBox = [attributedText
|
||||
boundingRectWithSize:constrainSize
|
||||
options:NSStringDrawingUsesLineFragmentOrigin
|
||||
context:nil
|
||||
];
|
||||
CGFloat preferredLabelHeight = FLEXFloor(boundingBox.size.height);
|
||||
CGFloat preferredCellHeight = preferredLabelHeight + labelInsets.top + labelInsets.bottom + 1.0;
|
||||
|
||||
return preferredCellHeight;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FLEXMultilineDetailTableViewCell
|
||||
|
||||
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
return [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// FLEXSubtitleTableViewCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 4/17/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewCell.h"
|
||||
|
||||
/// A cell initialized with \c UITableViewCellStyleSubtitle
|
||||
@interface FLEXSubtitleTableViewCell : FLEXTableViewCell
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEXSubtitleTableViewCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 4/17/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXSubtitleTableViewCell.h"
|
||||
|
||||
@implementation FLEXSubtitleTableViewCell
|
||||
|
||||
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
return [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// FLEXTableViewCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 4/17/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface FLEXTableViewCell : UITableViewCell
|
||||
|
||||
/// Use this instead of .textLabel
|
||||
@property (nonatomic, readonly) UILabel *titleLabel;
|
||||
/// Use this instead of .detailTextLabel
|
||||
@property (nonatomic, readonly) UILabel *subtitleLabel;
|
||||
|
||||
/// Subclasses can override this instead of initializers to
|
||||
/// perform additional initialization without lots of boilerplate.
|
||||
/// Remember to call super!
|
||||
- (void)postInit;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// FLEXTableViewCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 4/17/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewCell.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXTableView.h"
|
||||
|
||||
@interface UITableView (Internal)
|
||||
// Exists at least since iOS 5
|
||||
- (BOOL)_canPerformAction:(SEL)action forCell:(UITableViewCell *)cell sender:(id)sender;
|
||||
- (void)_performAction:(SEL)action forCell:(UITableViewCell *)cell sender:(id)sender;
|
||||
@end
|
||||
|
||||
@interface UITableViewCell (Internal)
|
||||
// Exists at least since iOS 5
|
||||
@property (nonatomic, readonly) FLEXTableView *_tableView;
|
||||
@end
|
||||
|
||||
@implementation FLEXTableViewCell
|
||||
|
||||
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
|
||||
if (self) {
|
||||
[self postInit];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)postInit {
|
||||
UIFont *cellFont = UIFont.flex_defaultTableCellFont;
|
||||
self.titleLabel.font = cellFont;
|
||||
self.subtitleLabel.font = cellFont;
|
||||
self.subtitleLabel.textColor = FLEXColor.deemphasizedTextColor;
|
||||
|
||||
self.titleLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
self.subtitleLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
|
||||
self.titleLabel.numberOfLines = 1;
|
||||
self.subtitleLabel.numberOfLines = 1;
|
||||
}
|
||||
|
||||
- (UILabel *)titleLabel {
|
||||
return self.textLabel;
|
||||
}
|
||||
|
||||
- (UILabel *)subtitleLabel {
|
||||
return self.detailTextLabel;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// FLEXTableView.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 4/17/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#pragma mark Reuse identifiers
|
||||
|
||||
typedef NSString * FLEXTableViewCellReuseIdentifier;
|
||||
|
||||
/// A regular \c FLEXTableViewCell initialized with \c UITableViewCellStyleDefault
|
||||
extern FLEXTableViewCellReuseIdentifier const kFLEXDefaultCell;
|
||||
/// A \c FLEXSubtitleTableViewCell initialized with \c UITableViewCellStyleSubtitle
|
||||
extern FLEXTableViewCellReuseIdentifier const kFLEXDetailCell;
|
||||
/// A \c FLEXMultilineTableViewCell initialized with \c UITableViewCellStyleDefault
|
||||
extern FLEXTableViewCellReuseIdentifier const kFLEXMultilineCell;
|
||||
/// A \c FLEXMultilineTableViewCell initialized with \c UITableViewCellStyleSubtitle
|
||||
extern FLEXTableViewCellReuseIdentifier const kFLEXMultilineDetailCell;
|
||||
/// A \c FLEXTableViewCell initialized with \c UITableViewCellStyleValue1
|
||||
extern FLEXTableViewCellReuseIdentifier const kFLEXKeyValueCell;
|
||||
/// A \c FLEXSubtitleTableViewCell which uses monospaced fonts for both labels
|
||||
extern FLEXTableViewCellReuseIdentifier const kFLEXCodeFontCell;
|
||||
|
||||
#pragma mark - FLEXTableView
|
||||
@interface FLEXTableView : UITableView
|
||||
|
||||
+ (instancetype)flexDefaultTableView;
|
||||
+ (instancetype)groupedTableView;
|
||||
+ (instancetype)plainTableView;
|
||||
+ (instancetype)style:(UITableViewStyle)style;
|
||||
|
||||
/// You do not need to register classes for any of the default reuse identifiers above
|
||||
/// (annotated as \c FLEXTableViewCellReuseIdentifier types) unless you wish to provide
|
||||
/// a custom cell for any of those reuse identifiers. By default, \c FLEXTableViewCell,
|
||||
/// \c FLEXSubtitleTableViewCell, and \c FLEXMultilineTableViewCell are used, respectively.
|
||||
///
|
||||
/// @param registrationMapping A map of reuse identifiers to \c UITableViewCell (sub)class objects.
|
||||
- (void)registerCells:(NSDictionary<NSString *, Class> *)registrationMapping;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,91 @@
|
||||
//
|
||||
// FLEXTableView.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 4/17/19.
|
||||
// Copyright © 2019 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXSubtitleTableViewCell.h"
|
||||
#import "FLEXMultilineTableViewCell.h"
|
||||
#import "FLEXKeyValueTableViewCell.h"
|
||||
#import "FLEXCodeFontCell.h"
|
||||
|
||||
FLEXTableViewCellReuseIdentifier const kFLEXDefaultCell = @"kFLEXDefaultCell";
|
||||
FLEXTableViewCellReuseIdentifier const kFLEXDetailCell = @"kFLEXDetailCell";
|
||||
FLEXTableViewCellReuseIdentifier const kFLEXMultilineCell = @"kFLEXMultilineCell";
|
||||
FLEXTableViewCellReuseIdentifier const kFLEXMultilineDetailCell = @"kFLEXMultilineDetailCell";
|
||||
FLEXTableViewCellReuseIdentifier const kFLEXKeyValueCell = @"kFLEXKeyValueCell";
|
||||
FLEXTableViewCellReuseIdentifier const kFLEXCodeFontCell = @"kFLEXCodeFontCell";
|
||||
|
||||
#pragma mark Private
|
||||
|
||||
@interface UITableView (Private)
|
||||
- (CGFloat)_heightForHeaderInSection:(NSInteger)section;
|
||||
- (NSString *)_titleForHeaderInSection:(NSInteger)section;
|
||||
@end
|
||||
|
||||
@implementation FLEXTableView
|
||||
|
||||
+ (instancetype)flexDefaultTableView {
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
if (@available(iOS 13.0, *)) {
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];
|
||||
} else {
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
|
||||
}
|
||||
#else
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
|
||||
#endif
|
||||
}
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (id)groupedTableView {
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
if (@available(iOS 13.0, *)) {
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];
|
||||
} else {
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
|
||||
}
|
||||
#else
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
|
||||
#endif
|
||||
}
|
||||
|
||||
+ (id)plainTableView {
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
|
||||
}
|
||||
|
||||
+ (id)style:(UITableViewStyle)style {
|
||||
return [[self alloc] initWithFrame:CGRectZero style:style];
|
||||
}
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame style:(UITableViewStyle)style {
|
||||
self = [super initWithFrame:frame style:style];
|
||||
if (self) {
|
||||
[self registerCells:@{
|
||||
kFLEXDefaultCell : [FLEXTableViewCell class],
|
||||
kFLEXDetailCell : [FLEXSubtitleTableViewCell class],
|
||||
kFLEXMultilineCell : [FLEXMultilineTableViewCell class],
|
||||
kFLEXMultilineDetailCell : [FLEXMultilineDetailTableViewCell class],
|
||||
kFLEXKeyValueCell : [FLEXKeyValueTableViewCell class],
|
||||
kFLEXCodeFontCell : [FLEXCodeFontCell class],
|
||||
}];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)registerCells:(NSDictionary<NSString*, Class> *)registrationMapping {
|
||||
[registrationMapping enumerateKeysAndObjectsUsingBlock:^(NSString *identifier, Class cellClass, BOOL *stop) {
|
||||
[self registerClass:cellClass forCellReuseIdentifier:identifier];
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/30/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/30/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputColorView.h"
|
||||
@@ -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;
|
||||
|
||||
@@ -30,18 +30,16 @@
|
||||
|
||||
@implementation FLEXColorComponentInputView
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
- (id)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.slider = [[UISlider alloc] init];
|
||||
self.slider.backgroundColor = self.backgroundColor;
|
||||
self.slider = [UISlider new];
|
||||
[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.font = [UIFont systemFontOfSize:14.0];
|
||||
self.valueLabel.textAlignment = NSTextAlignmentRight;
|
||||
[self addSubview:self.valueLabel];
|
||||
|
||||
@@ -50,15 +48,13 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||
[super setBackgroundColor:backgroundColor];
|
||||
self.slider.backgroundColor = backgroundColor;
|
||||
self.valueLabel.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
const CGFloat kValueLabelWidth = 50.0;
|
||||
@@ -73,19 +69,16 @@
|
||||
self.valueLabel.frame = CGRectMake(valueLabelOriginX, valueLabelOriginY, kValueLabelWidth, self.valueLabel.frame.size.height);
|
||||
}
|
||||
|
||||
- (void)sliderChanged:(id)sender
|
||||
{
|
||||
- (void)sliderChanged:(id)sender {
|
||||
[self.delegate colorComponentInputViewValueDidChange:self];
|
||||
[self updateValueLabel];
|
||||
}
|
||||
|
||||
- (void)updateValueLabel
|
||||
{
|
||||
- (void)updateValueLabel {
|
||||
self.valueLabel.text = [NSString stringWithFormat:@"%.3f", self.slider.value];
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGFloat height = [self.slider sizeThatFits:size].height;
|
||||
return CGSizeMake(size.width, height);
|
||||
}
|
||||
@@ -94,52 +87,48 @@
|
||||
|
||||
@interface FLEXColorPreviewBox : UIView
|
||||
|
||||
@property (nonatomic, strong) UIColor *color;
|
||||
@property (nonatomic) UIColor *color;
|
||||
|
||||
@property (nonatomic, strong) UIView *colorOverlayView;
|
||||
@property (nonatomic) UIView *colorOverlayView;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXColorPreviewBox
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
- (id)initWithFrame:(CGRect)frame {
|
||||
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;
|
||||
}
|
||||
|
||||
- (void)setColor:(UIColor *)color
|
||||
{
|
||||
- (void)setColor:(UIColor *)color {
|
||||
self.colorOverlayView.backgroundColor = color;
|
||||
}
|
||||
|
||||
- (UIColor *)color
|
||||
{
|
||||
- (UIColor *)color {
|
||||
return self.colorOverlayView.backgroundColor;
|
||||
}
|
||||
|
||||
+ (UIImage *)backgroundPatternImage
|
||||
{
|
||||
+ (UIImage *)backgroundPatternImage {
|
||||
const CGFloat kSquareDimension = 5.0;
|
||||
CGSize squareSize = CGSizeMake(kSquareDimension, kSquareDimension);
|
||||
CGSize imageSize = CGSizeMake(2.0 * kSquareDimension, 2.0 * kSquareDimension);
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(imageSize, YES, [[UIScreen mainScreen] scale]);
|
||||
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,55 +142,53 @@
|
||||
|
||||
@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
|
||||
|
||||
@implementation FLEXArgumentInputColorView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
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.hexLabel.font = [UIFont systemFontOfSize: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];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||
[super setBackgroundColor:backgroundColor];
|
||||
self.alphaInput.backgroundColor = backgroundColor;
|
||||
self.redInput.backgroundColor = backgroundColor;
|
||||
@@ -209,8 +196,7 @@
|
||||
self.blueInput.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
CGFloat runningOriginY = 0;
|
||||
@@ -221,14 +207,14 @@
|
||||
|
||||
[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;
|
||||
self.hexLabel.frame = CGRectMake(hexLabelOriginX, hexLabelOriginY, self.hexLabel.frame.size.width, self.hexLabel.frame.size.height);
|
||||
|
||||
NSArray *colorComponentInputViews = @[self.alphaInput, self.redInput, self.greenInput, self.blueInput];
|
||||
NSArray<FLEXColorComponentInputView *> *colorComponentInputViews = @[self.alphaInput, self.redInput, self.greenInput, self.blueInput];
|
||||
for (FLEXColorComponentInputView *inputView in colorComponentInputViews) {
|
||||
CGSize fitSize = [inputView sizeThatFits:constrainSize];
|
||||
inputView.frame = CGRectMake(0, runningOriginY, fitSize.width, fitSize.height);
|
||||
@@ -236,8 +222,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
if ([inputValue isKindOfClass:[UIColor class]]) {
|
||||
[self updateWithColor:inputValue];
|
||||
} else if ([inputValue isKindOfClass:[NSValue class]]) {
|
||||
@@ -248,21 +233,20 @@
|
||||
UIColor *color = [[UIColor alloc] initWithCGColor:colorRef];
|
||||
[self updateWithColor:color];
|
||||
}
|
||||
} else {
|
||||
[self updateWithColor:UIColor.clearColor];
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
- (id)inputValue {
|
||||
return [UIColor colorWithRed:self.redInput.slider.value green:self.greenInput.slider.value blue:self.blueInput.slider.value alpha:self.alphaInput.slider.value];
|
||||
}
|
||||
|
||||
- (void)colorComponentInputViewValueDidChange:(FLEXColorComponentInputView *)colorComponentInputView
|
||||
{
|
||||
- (void)colorComponentInputViewValueDidChange:(FLEXColorComponentInputView *)colorComponentInputView {
|
||||
[self updateColorPreview];
|
||||
}
|
||||
|
||||
- (void)updateWithColor:(UIColor *)color
|
||||
{
|
||||
- (void)updateWithColor:(UIColor *)color {
|
||||
CGFloat red, green, blue, white, alpha;
|
||||
if ([color getRed:&red green:&green blue:&blue alpha:&alpha]) {
|
||||
self.alphaInput.slider.value = alpha;
|
||||
@@ -286,8 +270,7 @@
|
||||
[self updateColorPreview];
|
||||
}
|
||||
|
||||
- (void)updateColorPreview
|
||||
{
|
||||
- (void)updateColorPreview {
|
||||
self.colorPreviewBox.color = self.inputValue;
|
||||
unsigned char redByte = self.redInput.slider.value * 255;
|
||||
unsigned char greenByte = self.greenInput.slider.value * 255;
|
||||
@@ -296,8 +279,7 @@
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGFloat height = 0;
|
||||
height += [[self class] colorPreviewBoxHeight];
|
||||
height += [[self class] inputViewVerticalPadding];
|
||||
@@ -311,19 +293,19 @@
|
||||
return CGSizeMake(size.width, height);
|
||||
}
|
||||
|
||||
+ (CGFloat)inputViewVerticalPadding
|
||||
{
|
||||
+ (CGFloat)inputViewVerticalPadding {
|
||||
return 10.0;
|
||||
}
|
||||
|
||||
+ (CGFloat)colorPreviewBoxHeight
|
||||
{
|
||||
+ (CGFloat)colorPreviewBoxHeight {
|
||||
return 40.0;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
return (type && (strcmp(type, @encode(CGColorRef)) == 0 || strcmp(type, FLEXEncodeClass(UIColor)) == 0)) || [value isKindOfClass:[UIColor class]];
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
|
||||
// We don't care if currentValue is a color or not; we will default to +clearColor
|
||||
return (strcmp(type, @encode(CGColorRef)) == 0) || (strcmp(type, FLEXEncodeClass(UIColor)) == 0);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Daniel Rodriguez Troitino on 2/14/15.
|
||||
// Copyright (c) 2015 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Daniel Rodriguez Troitino on 2/14/15.
|
||||
// Copyright (c) 2015 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputDateView.h"
|
||||
@@ -11,17 +11,16 @@
|
||||
|
||||
@interface FLEXArgumentInputDateView ()
|
||||
|
||||
@property (nonatomic, strong) UIDatePicker *datePicker;
|
||||
@property (nonatomic) UIDatePicker *datePicker;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputDateView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
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];
|
||||
@@ -31,33 +30,29 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
if ([inputValue isKindOfClass:[NSDate class]]) {
|
||||
self.datePicker.date = inputValue;
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
- (id)inputValue {
|
||||
return self.datePicker.date;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
self.datePicker.frame = self.bounds;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGFloat height = [self.datePicker sizeThatFits:size].height;
|
||||
return CGSizeMake(size.width, height);
|
||||
}
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
return (type && (strcmp(type, FLEXEncodeClass(NSDate)) == 0)) || [value isKindOfClass:[NSDate class]];
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
return strcmp(type, FLEXEncodeClass(NSDate)) == 0;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/28/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/28/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputFontView.h"
|
||||
@@ -13,25 +13,22 @@
|
||||
|
||||
@interface FLEXArgumentInputFontView ()
|
||||
|
||||
@property (nonatomic, strong) FLEXArgumentInputView *fontNameInput;
|
||||
@property (nonatomic, strong) FLEXArgumentInputView *pointSizeInput;
|
||||
@property (nonatomic) FLEXArgumentInputView *fontNameInput;
|
||||
@property (nonatomic) FLEXArgumentInputView *pointSizeInput;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputFontView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.fontNameInput = [[FLEXArgumentInputFontsPickerView alloc] initWithArgumentTypeEncoding:FLEXEncodeClass(NSString)];
|
||||
self.fontNameInput.backgroundColor = self.backgroundColor;
|
||||
self.fontNameInput.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
self.fontNameInput.title = @"Font Name:";
|
||||
[self addSubview:self.fontNameInput];
|
||||
|
||||
self.pointSizeInput = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:@encode(CGFloat)];
|
||||
self.pointSizeInput.backgroundColor = self.backgroundColor;
|
||||
self.pointSizeInput.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
self.pointSizeInput.title = @"Point Size:";
|
||||
[self addSubview:self.pointSizeInput];
|
||||
@@ -39,15 +36,13 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||
[super setBackgroundColor:backgroundColor];
|
||||
self.fontNameInput.backgroundColor = backgroundColor;
|
||||
self.pointSizeInput.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
if ([inputValue isKindOfClass:[UIFont class]]) {
|
||||
UIFont *font = (UIFont *)inputValue;
|
||||
self.fontNameInput.inputValue = font.fontName;
|
||||
@@ -55,8 +50,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
- (id)inputValue {
|
||||
CGFloat pointSize = 0;
|
||||
if ([self.pointSizeInput.inputValue isKindOfClass:[NSValue class]]) {
|
||||
NSValue *pointSizeValue = (NSValue *)self.pointSizeInput.inputValue;
|
||||
@@ -67,16 +61,14 @@
|
||||
return [UIFont fontWithName:self.fontNameInput.inputValue size:pointSize];
|
||||
}
|
||||
|
||||
- (BOOL)inputViewIsFirstResponder
|
||||
{
|
||||
- (BOOL)inputViewIsFirstResponder {
|
||||
return [self.fontNameInput inputViewIsFirstResponder] || [self.pointSizeInput inputViewIsFirstResponder];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Layout and Sizing
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
CGFloat runningOriginY = self.topInputFieldVerticalLayoutGuide;
|
||||
@@ -89,13 +81,11 @@
|
||||
self.pointSizeInput.frame = CGRectMake(0, runningOriginY, pointSizeFitSize.width, pointSizeFitSize.height);
|
||||
}
|
||||
|
||||
+ (CGFloat)verticalPaddingBetweenFields
|
||||
{
|
||||
+ (CGFloat)verticalPaddingBetweenFields {
|
||||
return 10.0;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGSize fitSize = [super sizeThatFits:size];
|
||||
|
||||
CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX);
|
||||
@@ -111,11 +101,9 @@
|
||||
|
||||
#pragma mark -
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
BOOL supported = type && strcmp(type, FLEXEncodeClass(UIFont)) == 0;
|
||||
supported = supported || (value && [value isKindOfClass:[UIFont class]]);
|
||||
return supported;
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
return strcmp(type, FLEXEncodeClass(UIFont)) == 0;
|
||||
}
|
||||
|
||||
@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,15 +11,14 @@
|
||||
|
||||
@interface FLEXArgumentInputFontsPickerView ()
|
||||
|
||||
@property (nonatomic, strong) NSMutableArray *availableFonts;
|
||||
@property (nonatomic) NSMutableArray<NSString *> *availableFonts;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FLEXArgumentInputFontsPickerView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
@@ -29,24 +28,21 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
self.inputTextView.text = inputValue;
|
||||
if ([self.availableFonts indexOfObject:inputValue] == NSNotFound) {
|
||||
[self.availableFonts insertObject:inputValue atIndex:0];
|
||||
}
|
||||
[(UIPickerView*)self.inputTextView.inputView selectRow:[self.availableFonts indexOfObject:inputValue] inComponent:0 animated:NO];
|
||||
[(UIPickerView *)self.inputTextView.inputView selectRow:[self.availableFonts indexOfObject:inputValue] inComponent:0 animated:NO];
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
return [self.inputTextView.text length] > 0 ? [self.inputTextView.text copy] : nil;
|
||||
- (id)inputValue {
|
||||
return self.inputTextView.text.length > 0 ? [self.inputTextView.text copy] : nil;
|
||||
}
|
||||
|
||||
#pragma mark - private
|
||||
|
||||
- (UIPickerView*)createFontsPicker
|
||||
{
|
||||
- (UIPickerView*)createFontsPicker {
|
||||
UIPickerView *fontsPicker = [UIPickerView new];
|
||||
fontsPicker.dataSource = self;
|
||||
fontsPicker.delegate = self;
|
||||
@@ -54,10 +50,9 @@
|
||||
return fontsPicker;
|
||||
}
|
||||
|
||||
- (void)createAvailableFonts
|
||||
{
|
||||
NSMutableArray *unsortedFontsArray = [NSMutableArray array];
|
||||
for (NSString *eachFontFamily in [UIFont familyNames]) {
|
||||
- (void)createAvailableFonts {
|
||||
NSMutableArray<NSString *> *unsortedFontsArray = [NSMutableArray new];
|
||||
for (NSString *eachFontFamily in UIFont.familyNames) {
|
||||
for (NSString *eachFontName in [UIFont fontNamesForFamilyName:eachFontFamily]) {
|
||||
[unsortedFontsArray addObject:eachFontName];
|
||||
}
|
||||
@@ -67,38 +62,34 @@
|
||||
|
||||
#pragma mark - UIPickerViewDataSource
|
||||
|
||||
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
|
||||
{
|
||||
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
|
||||
{
|
||||
return [self.availableFonts count];
|
||||
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
|
||||
return self.availableFonts.count;
|
||||
}
|
||||
|
||||
#pragma mark - UIPickerViewDelegate
|
||||
|
||||
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view
|
||||
{
|
||||
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
|
||||
UILabel *fontLabel;
|
||||
if (!view) {
|
||||
fontLabel = [UILabel new];
|
||||
fontLabel.backgroundColor = [UIColor clearColor];
|
||||
fontLabel.backgroundColor = UIColor.clearColor;
|
||||
fontLabel.textAlignment = NSTextAlignmentCenter;
|
||||
} else {
|
||||
fontLabel = (UILabel*)view;
|
||||
}
|
||||
UIFont *font = [UIFont fontWithName:self.availableFonts[row] size:15.0];
|
||||
NSDictionary *attributesDictionary = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName];
|
||||
NSDictionary<NSString *, id> *attributesDictionary = [NSDictionary<NSString *, id> dictionaryWithObject:font forKey:NSFontAttributeName];
|
||||
NSAttributedString *attributesString = [[NSAttributedString alloc] initWithString:self.availableFonts[row] attributes:attributesDictionary];
|
||||
fontLabel.attributedText = attributesString;
|
||||
[fontLabel sizeToFit];
|
||||
return fontLabel;
|
||||
}
|
||||
|
||||
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
|
||||
{
|
||||
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
|
||||
self.inputTextView.text = self.availableFonts[row];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// FLEXArgumentInputJSONObjectView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
|
||||
@interface FLEXArgumentInputJSONObjectView : FLEXArgumentInputTextView
|
||||
|
||||
@end
|
||||
@@ -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
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/18/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/18/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputNotSupportedView.h"
|
||||
#import "FLEXColor.h"
|
||||
|
||||
@implementation FLEXArgumentInputNotSupportedView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.inputTextView.userInteractionEnabled = NO;
|
||||
self.inputTextView.backgroundColor = [UIColor colorWithWhite:0.8 alpha:1.0];
|
||||
self.inputTextView.text = @"nil";
|
||||
self.inputTextView.backgroundColor = [FLEXColor secondaryGroupedBackgroundColorWithAlpha:0.5];
|
||||
self.inputPlaceholderText = @"nil (type not supported)";
|
||||
self.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
}
|
||||
return self;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputNumberView.h"
|
||||
@@ -11,47 +11,52 @@
|
||||
|
||||
@implementation FLEXArgumentInputNumberView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
|
||||
self.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
if ([inputValue respondsToSelector:@selector(stringValue)]) {
|
||||
self.inputTextView.text = [inputValue stringValue];
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
return [FLEXRuntimeUtility valueForNumberWithObjCType:[self.typeEncoding UTF8String] fromInputString:self.inputTextView.text];
|
||||
- (id)inputValue {
|
||||
return [FLEXRuntimeUtility valueForNumberWithObjCType:self.typeEncoding.UTF8String fromInputString:self.inputTextView.text];
|
||||
}
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
static NSArray *primitiveTypes = nil;
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
|
||||
static NSArray<NSString *> *supportedTypes = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
primitiveTypes = @[@(@encode(char)),
|
||||
@(@encode(int)),
|
||||
@(@encode(short)),
|
||||
@(@encode(long)),
|
||||
@(@encode(long long)),
|
||||
@(@encode(unsigned char)),
|
||||
@(@encode(unsigned int)),
|
||||
@(@encode(unsigned short)),
|
||||
@(@encode(unsigned long)),
|
||||
@(@encode(unsigned long long)),
|
||||
@(@encode(float)),
|
||||
@(@encode(double))];
|
||||
supportedTypes = @[
|
||||
@FLEXEncodeClass(NSNumber),
|
||||
@FLEXEncodeClass(NSDecimalNumber),
|
||||
@(@encode(char)),
|
||||
@(@encode(int)),
|
||||
@(@encode(short)),
|
||||
@(@encode(long)),
|
||||
@(@encode(long long)),
|
||||
@(@encode(unsigned char)),
|
||||
@(@encode(unsigned int)),
|
||||
@(@encode(unsigned short)),
|
||||
@(@encode(unsigned long)),
|
||||
@(@encode(unsigned long long)),
|
||||
@(@encode(float)),
|
||||
@(@encode(double)),
|
||||
@(@encode(long double))
|
||||
];
|
||||
});
|
||||
return type && [primitiveTypes containsObject:@(type)];
|
||||
|
||||
return type && [supportedTypes containsObject:@(type)];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXArgumentInputObjectView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
|
||||
@interface FLEXArgumentInputObjectView : FLEXArgumentInputTextView
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,232 @@
|
||||
//
|
||||
// FLEXArgumentInputJSONObjectView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2020 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] == FLEXTypeEncodingObjcObject || type[0] == FLEXTypeEncodingObjcClass;
|
||||
}
|
||||
|
||||
+ (FLEXArgInputObjectType)preferredDefaultTypeForObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type[0] == FLEXTypeEncodingObjcObject || type[0] == FLEXTypeEncodingObjcClass);
|
||||
|
||||
if (value) {
|
||||
// If there's a current value, it must be serializable to JSON
|
||||
// to display the JSON editor. Otherwise display the address field.
|
||||
if ([FLEXRuntimeUtility editableJSONStringForObject:value]) {
|
||||
return FLEXArgInputObjectTypeJSON;
|
||||
} else {
|
||||
return FLEXArgInputObjectTypeAddress;
|
||||
}
|
||||
} else {
|
||||
// Otherwise, see if we have more type information than just 'id'.
|
||||
// If we do, make sure the encoding is something serializable to JSON.
|
||||
// Properties and ivars keep more detailed type encoding information than method arguments.
|
||||
if (strcmp(type, @encode(id)) != 0) {
|
||||
BOOL isJSONSerializableType = NO;
|
||||
|
||||
// Parse class name out of the string,
|
||||
// which is in the form `@"ClassName"`
|
||||
Class cls = NSClassFromString(({
|
||||
NSString *className = nil;
|
||||
NSScanner *scan = [NSScanner scannerWithString:@(type)];
|
||||
NSCharacterSet *allowed = [NSCharacterSet
|
||||
characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$"
|
||||
];
|
||||
|
||||
// Skip over the @" then scan the name
|
||||
if ([scan scanString:@"@\"" intoString:nil]) {
|
||||
[scan scanCharactersFromSet:allowed intoString:&className];
|
||||
}
|
||||
|
||||
className;
|
||||
}));
|
||||
|
||||
// Note: we can't use @encode(NSString) here because that drops
|
||||
// the class information and just goes to @encode(id).
|
||||
NSArray<Class> *jsonTypes = @[
|
||||
[NSString class],
|
||||
[NSNumber class],
|
||||
[NSArray class],
|
||||
[NSDictionary class],
|
||||
];
|
||||
|
||||
// Look for matching types
|
||||
for (Class jsonClass in jsonTypes) {
|
||||
if ([cls isSubclassOfClass:jsonClass]) {
|
||||
isJSONSerializableType = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isJSONSerializableType) {
|
||||
return FLEXArgInputObjectTypeJSON;
|
||||
} else {
|
||||
return FLEXArgInputObjectTypeAddress;
|
||||
}
|
||||
} else {
|
||||
return FLEXArgInputObjectTypeAddress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/28/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/28/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputStringView.h"
|
||||
@@ -11,35 +11,119 @@
|
||||
|
||||
@implementation FLEXArgumentInputStringView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.targetSize = FLEXArgumentInputViewSizeLarge;
|
||||
FLEXTypeEncoding type = typeEncoding[0];
|
||||
if (type == FLEXTypeEncodingConst) {
|
||||
// A crash here would mean an invalid type encoding string
|
||||
type = typeEncoding[1];
|
||||
}
|
||||
|
||||
// Selectors don't need a multi-line text box
|
||||
if (type == FLEXTypeEncodingSelector) {
|
||||
self.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
} else {
|
||||
self.targetSize = FLEXArgumentInputViewSizeLarge;
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
self.inputTextView.text = inputValue;
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
if ([inputValue isKindOfClass:[NSString class]]) {
|
||||
self.inputTextView.text = inputValue;
|
||||
} else if ([inputValue isKindOfClass:[NSValue class]]) {
|
||||
NSValue *value = (id)inputValue;
|
||||
NSParameterAssert(strlen(value.objCType) == 1);
|
||||
|
||||
// C-String or SEL from NSValue
|
||||
FLEXTypeEncoding type = value.objCType[0];
|
||||
if (type == FLEXTypeEncodingConst) {
|
||||
// A crash here would mean an invalid type encoding string
|
||||
type = value.objCType[1];
|
||||
}
|
||||
|
||||
if (type == FLEXTypeEncodingCString) {
|
||||
self.inputTextView.text = @((const char *)value.pointerValue);
|
||||
} else if (type == FLEXTypeEncodingSelector) {
|
||||
self.inputTextView.text = NSStringFromSelector((SEL)value.pointerValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
// Interpret empty string as nil. We loose the ablitiy to set empty string as a string value,
|
||||
- (id)inputValue {
|
||||
NSString *text = self.inputTextView.text;
|
||||
// Interpret empty string as nil. We loose the ability to set empty string as a string value,
|
||||
// but we accept that tradeoff in exchange for not having to type quotes for every string.
|
||||
return [self.inputTextView.text length] > 0 ? [self.inputTextView.text copy] : nil;
|
||||
if (!text.length) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Case: C-strings and SELs
|
||||
if (self.typeEncoding.length <= 2) {
|
||||
FLEXTypeEncoding type = [self.typeEncoding characterAtIndex:0];
|
||||
if (type == FLEXTypeEncodingConst) {
|
||||
// A crash here would mean an invalid type encoding string
|
||||
type = [self.typeEncoding characterAtIndex:1];
|
||||
}
|
||||
|
||||
if (type == FLEXTypeEncodingCString || type == FLEXTypeEncodingSelector) {
|
||||
const char *encoding = self.typeEncoding.UTF8String;
|
||||
SEL selector = NSSelectorFromString(text);
|
||||
return [NSValue valueWithBytes:&selector objCType:encoding];
|
||||
}
|
||||
}
|
||||
|
||||
// Case: NSStrings
|
||||
return self.inputTextView.text.copy;
|
||||
}
|
||||
|
||||
// TODO: Support using object address for strings, as in the object arg view.
|
||||
|
||||
#pragma mark -
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
unsigned long len = strlen(type);
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
BOOL supported = type && strcmp(type, FLEXEncodeClass(NSString)) == 0;
|
||||
supported = supported || (value && [value isKindOfClass:[NSString class]]);
|
||||
return supported;
|
||||
BOOL isConst = type[0] == FLEXTypeEncodingConst;
|
||||
NSInteger i = isConst ? 1 : 0;
|
||||
|
||||
BOOL typeIsString = strcmp(type, FLEXEncodeClass(NSString)) == 0;
|
||||
BOOL typeIsCString = len <= 2 && type[i] == FLEXTypeEncodingCString;
|
||||
BOOL typeIsSEL = len <= 2 && type[i] == FLEXTypeEncodingSelector;
|
||||
BOOL valueIsString = [value isKindOfClass:[NSString class]];
|
||||
|
||||
BOOL typeIsPrimitiveString = typeIsSEL || typeIsCString;
|
||||
BOOL typeIsSupported = typeIsString || typeIsCString || typeIsSEL;
|
||||
|
||||
BOOL valueIsNSValueWithCorrectType = NO;
|
||||
if ([value isKindOfClass:[NSValue class]]) {
|
||||
NSValue *v = (id)value;
|
||||
len = strlen(v.objCType);
|
||||
if (len == 1) {
|
||||
FLEXTypeEncoding type = v.objCType[i];
|
||||
if (type == FLEXTypeEncodingCString && typeIsCString) {
|
||||
valueIsNSValueWithCorrectType = YES;
|
||||
} else if (type == FLEXTypeEncodingSelector && typeIsSEL) {
|
||||
valueIsNSValueWithCorrectType = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!value && typeIsSupported) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (typeIsString && valueIsString) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Primitive strings can be input as NSStrings or NSValues
|
||||
if (typeIsPrimitiveString && (valueIsString || valueIsNSValueWithCorrectType)) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@@ -3,37 +3,42 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputStructView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXTypeEncodingParser.h"
|
||||
|
||||
@interface FLEXArgumentInputStructView ()
|
||||
|
||||
@property (nonatomic, strong) NSArray *argumentInputViews;
|
||||
@property (nonatomic) NSArray<FLEXArgumentInputView *> *argumentInputViews;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputStructView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
NSMutableArray *inputViews = [NSMutableArray array];
|
||||
NSArray *customTitles = [[self class] customFieldTitlesForTypeEncoding:typeEncoding];
|
||||
[FLEXRuntimeUtility enumerateTypesInStructEncoding:typeEncoding usingBlock:^(NSString *structName, const char *fieldTypeEncoding, NSString *prettyTypeEncoding, NSUInteger fieldIndex, NSUInteger fieldOffset) {
|
||||
NSMutableArray<FLEXArgumentInputView *> *inputViews = [NSMutableArray new];
|
||||
NSArray<NSString *> *customTitles = [[self class] customFieldTitlesForTypeEncoding:typeEncoding];
|
||||
[FLEXRuntimeUtility enumerateTypesInStructEncoding:typeEncoding usingBlock:^(NSString *structName,
|
||||
const char *fieldTypeEncoding,
|
||||
NSString *prettyTypeEncoding,
|
||||
NSUInteger fieldIndex,
|
||||
NSUInteger fieldOffset) {
|
||||
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:fieldTypeEncoding];
|
||||
inputView.backgroundColor = self.backgroundColor;
|
||||
inputView.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
|
||||
if (fieldIndex < [customTitles count]) {
|
||||
inputView.title = [customTitles objectAtIndex:fieldIndex];
|
||||
if (fieldIndex < customTitles.count) {
|
||||
inputView.title = customTitles[fieldIndex];
|
||||
} else {
|
||||
inputView.title = [NSString stringWithFormat:@"%@ field %lu (%@)", structName, (unsigned long)fieldIndex, prettyTypeEncoding];
|
||||
inputView.title = [NSString stringWithFormat:@"%@ field %lu (%@)",
|
||||
structName, (unsigned long)fieldIndex, prettyTypeEncoding
|
||||
];
|
||||
}
|
||||
|
||||
[inputViews addObject:inputView];
|
||||
@@ -47,34 +52,32 @@
|
||||
|
||||
#pragma mark - Superclass Overrides
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||
[super setBackgroundColor:backgroundColor];
|
||||
for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
|
||||
inputView.backgroundColor = backgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
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.
|
||||
NSGetSizeAndAlignment(structTypeEncoding, &valueSize, NULL);
|
||||
} @catch (NSException *exception) { }
|
||||
|
||||
if (valueSize > 0) {
|
||||
if (FLEXGetSizeAndAlignment(structTypeEncoding, &valueSize, NULL)) {
|
||||
void *unboxedValue = malloc(valueSize);
|
||||
[inputValue getValue:unboxedValue];
|
||||
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName, const char *fieldTypeEncoding, NSString *prettyTypeEncoding, NSUInteger fieldIndex, NSUInteger fieldOffset) {
|
||||
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName,
|
||||
const char *fieldTypeEncoding,
|
||||
NSString *prettyTypeEncoding,
|
||||
NSUInteger fieldIndex,
|
||||
NSUInteger fieldOffset) {
|
||||
|
||||
void *fieldPointer = unboxedValue + fieldOffset;
|
||||
FLEXArgumentInputView *inputView = [self.argumentInputViews objectAtIndex:fieldIndex];
|
||||
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];
|
||||
@@ -87,24 +90,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
- (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.
|
||||
NSGetSizeAndAlignment(structTypeEncoding, &structSize, NULL);
|
||||
} @catch (NSException *exception) { }
|
||||
|
||||
if (structSize > 0) {
|
||||
if (FLEXGetSizeAndAlignment(structTypeEncoding, &structSize, NULL)) {
|
||||
void *unboxedStruct = malloc(structSize);
|
||||
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName, const char *fieldTypeEncoding, NSString *prettyTypeEncoding, NSUInteger fieldIndex, NSUInteger fieldOffset) {
|
||||
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName,
|
||||
const char *fieldTypeEncoding,
|
||||
NSString *prettyTypeEncoding,
|
||||
NSUInteger fieldIndex,
|
||||
NSUInteger fieldOffset) {
|
||||
|
||||
void *fieldPointer = unboxedStruct + fieldOffset;
|
||||
FLEXArgumentInputView *inputView = [self.argumentInputViews objectAtIndex:fieldIndex];
|
||||
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 {
|
||||
@@ -123,8 +125,7 @@
|
||||
return boxedStruct;
|
||||
}
|
||||
|
||||
- (BOOL)inputViewIsFirstResponder
|
||||
{
|
||||
- (BOOL)inputViewIsFirstResponder {
|
||||
BOOL isFirstResponder = NO;
|
||||
for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
|
||||
if ([inputView inputViewIsFirstResponder]) {
|
||||
@@ -138,8 +139,7 @@
|
||||
|
||||
#pragma mark - Layout and Sizing
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
CGFloat runningOriginY = self.topInputFieldVerticalLayoutGuide;
|
||||
@@ -151,13 +151,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
+ (CGFloat)verticalPaddingBetweenFields
|
||||
{
|
||||
+ (CGFloat)verticalPaddingBetweenFields {
|
||||
return 10.0;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGSize fitSize = [super sizeThatFits:size];
|
||||
|
||||
CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX);
|
||||
@@ -174,20 +172,25 @@
|
||||
|
||||
#pragma mark - Class Helpers
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
return type && type[0] == '{';
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
if (type[0] == FLEXTypeEncodingStructBegin) {
|
||||
return FLEXGetSizeAndAlignment(type, nil, nil);
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (NSArray *)customFieldTitlesForTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
NSArray *customTitles = nil;
|
||||
+ (NSArray<NSString *> *)customFieldTitlesForTypeEncoding:(const char *)typeEncoding {
|
||||
NSArray<NSString *> *customTitles = nil;
|
||||
if (strcmp(typeEncoding, @encode(CGRect)) == 0) {
|
||||
customTitles = @[@"CGPoint origin", @"CGSize size"];
|
||||
} else if (strcmp(typeEncoding, @encode(CGPoint)) == 0) {
|
||||
customTitles = @[@"CGFloat x", @"CGFloat y"];
|
||||
} else if (strcmp(typeEncoding, @encode(CGSize)) == 0) {
|
||||
customTitles = @[@"CGFloat width", @"CGFloat height"];
|
||||
} else if (strcmp(typeEncoding, @encode(CGVector)) == 0) {
|
||||
customTitles = @[@"CGFloat dx", @"CGFloat dy"];
|
||||
} else if (strcmp(typeEncoding, @encode(UIEdgeInsets)) == 0) {
|
||||
customTitles = @[@"CGFloat top", @"CGFloat left", @"CGFloat bottom", @"CGFloat right"];
|
||||
} else if (strcmp(typeEncoding, @encode(UIOffset)) == 0) {
|
||||
@@ -203,6 +206,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;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@@ -3,24 +3,23 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputSwitchView.h"
|
||||
|
||||
@interface FLEXArgumentInputSwitchView ()
|
||||
|
||||
@property (nonatomic, strong) UISwitch *inputSwitch;
|
||||
@property (nonatomic) UISwitch *inputSwitch;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputSwitchView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
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];
|
||||
@@ -31,8 +30,7 @@
|
||||
|
||||
#pragma mark Input/Output
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
BOOL on = NO;
|
||||
if ([inputValue isKindOfClass:[NSNumber class]]) {
|
||||
NSNumber *number = (NSNumber *)inputValue;
|
||||
@@ -46,30 +44,26 @@
|
||||
self.inputSwitch.on = on;
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
- (id)inputValue {
|
||||
BOOL isOn = [self.inputSwitch isOn];
|
||||
NSValue *boxedBool = [NSValue value:&isOn withObjCType:@encode(BOOL)];
|
||||
return boxedBool;
|
||||
}
|
||||
|
||||
- (void)switchValueDidChange:(id)sender
|
||||
{
|
||||
- (void)switchValueDidChange:(id)sender {
|
||||
[self.delegate argumentInputViewValueDidChange:self];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Layout and Sizing
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
self.inputSwitch.frame = CGRectMake(0, self.topInputFieldVerticalLayoutGuide, self.inputSwitch.frame.size.width, self.inputSwitch.frame.size.height);
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGSize fitSize = [super sizeThatFits:size];
|
||||
fitSize.height += self.inputSwitch.frame.size.height;
|
||||
return fitSize;
|
||||
@@ -78,10 +72,10 @@
|
||||
|
||||
#pragma mark - Class Helpers
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
// Only BOOLs. Current value is irrelevant.
|
||||
return type && strcmp(type, @encode(BOOL)) == 0;
|
||||
return strcmp(type, @encode(BOOL)) == 0;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -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,105 +6,131 @@
|
||||
//
|
||||
//
|
||||
|
||||
#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
|
||||
|
||||
@implementation FLEXArgumentInputTextView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
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.secondaryGroupedBackgroundColor;
|
||||
self.inputTextView.layer.cornerRadius = 10.f;
|
||||
self.inputTextView.contentInset = UIEdgeInsetsMake(0, 5, 0, 0);
|
||||
self.inputTextView.autocapitalizationType = UITextAutocapitalizationTypeNone;
|
||||
self.inputTextView.autocorrectionType = UITextAutocorrectionTypeNo;
|
||||
self.inputTextView.delegate = self;
|
||||
self.inputTextView.inputAccessoryView = [self createToolBar];
|
||||
if (@available(iOS 11, *)) {
|
||||
[self.inputTextView.layer setValue:@YES forKey:@"continuousCorners"];
|
||||
} else {
|
||||
self.inputTextView.layer.borderWidth = 1.f;
|
||||
self.inputTextView.layer.borderColor = FLEXColor.borderColor.CGColor;
|
||||
}
|
||||
|
||||
self.placeholderLabel = [UILabel new];
|
||||
self.placeholderLabel.font = self.inputTextView.font;
|
||||
self.placeholderLabel.textColor = FLEXColor.deemphasizedTextColor;
|
||||
self.placeholderLabel.numberOfLines = 0;
|
||||
|
||||
[self addSubview:self.inputTextView];
|
||||
[self.inputTextView addSubview:self.placeholderLabel];
|
||||
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - private
|
||||
#pragma mark - Private
|
||||
|
||||
- (UIToolbar*)createToolBar
|
||||
{
|
||||
- (UIToolbar *)createToolBar {
|
||||
UIToolbar *toolBar = [UIToolbar new];
|
||||
[toolBar sizeToFit];
|
||||
UIBarButtonItem *spaceItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
|
||||
UIBarButtonItem *doneItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(textViewDone)];
|
||||
toolBar.items = @[spaceItem, doneItem];
|
||||
UIBarButtonItem *spaceItem = [[UIBarButtonItem alloc]
|
||||
initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
|
||||
target:nil action:nil
|
||||
];
|
||||
UIBarButtonItem *pasteItem = [[UIBarButtonItem alloc]
|
||||
initWithTitle:@"Paste" style:UIBarButtonItemStyleDone
|
||||
target:self.inputTextView action:@selector(paste:)
|
||||
];
|
||||
UIBarButtonItem *doneItem = [[UIBarButtonItem alloc]
|
||||
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
|
||||
target:self.inputTextView action:@selector(resignFirstResponder)
|
||||
];
|
||||
toolBar.items = @[spaceItem, pasteItem, doneItem];
|
||||
return toolBar;
|
||||
}
|
||||
|
||||
- (void)textViewDone
|
||||
{
|
||||
[self.inputTextView resignFirstResponder];
|
||||
- (void)setInputPlaceholderText:(NSString *)placeholder {
|
||||
self.placeholderLabel.text = placeholder;
|
||||
if (placeholder.length) {
|
||||
if (!self.inputTextView.text.length) {
|
||||
self.placeholderLabel.hidden = NO;
|
||||
} else {
|
||||
self.placeholderLabel.hidden = YES;
|
||||
}
|
||||
} else {
|
||||
self.placeholderLabel.hidden = YES;
|
||||
}
|
||||
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Text View Changes
|
||||
|
||||
- (void)textViewDidChange:(UITextView *)textView
|
||||
{
|
||||
[self.delegate argumentInputViewValueDidChange:self];
|
||||
- (NSString *)inputPlaceholderText {
|
||||
return self.placeholderLabel.text;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Superclass Overrides
|
||||
|
||||
- (BOOL)inputViewIsFirstResponder
|
||||
{
|
||||
- (BOOL)inputViewIsFirstResponder {
|
||||
return self.inputTextView.isFirstResponder;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Layout and Sizing
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
self.inputTextView.frame = CGRectMake(0, self.topInputFieldVerticalLayoutGuide, self.bounds.size.width, [self inputTextViewHeight]);
|
||||
// Placeholder label is positioned by insetting then origin
|
||||
// by the content inset then the text container inset
|
||||
CGSize s = self.inputTextView.frame.size;
|
||||
self.placeholderLabel.frame = CGRectMake(0, 0, s.width, s.height);
|
||||
self.placeholderLabel.frame = UIEdgeInsetsInsetRect(
|
||||
UIEdgeInsetsInsetRect(self.placeholderLabel.frame, self.inputTextView.contentInset),
|
||||
self.inputTextView.textContainerInset
|
||||
);
|
||||
}
|
||||
|
||||
- (NSUInteger)numberOfInputLines
|
||||
{
|
||||
NSUInteger numberOfInputLines = 0;
|
||||
- (NSUInteger)numberOfInputLines {
|
||||
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
|
||||
{
|
||||
- (CGFloat)inputTextViewHeight {
|
||||
return ceil([[self class] inputFont].lineHeight * self.numberOfInputLines) + 16.0;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGSize fitSize = [super sizeThatFits:size];
|
||||
fitSize.height += [self inputTextViewHeight];
|
||||
return fitSize;
|
||||
@@ -113,9 +139,16 @@
|
||||
|
||||
#pragma mark - Class Helpers
|
||||
|
||||
+ (UIFont *)inputFont
|
||||
{
|
||||
return [FLEXUtility defaultFontOfSize:14.0];
|
||||
+ (UIFont *)inputFont {
|
||||
return [UIFont systemFontOfSize:14.0];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UITextViewDelegate
|
||||
|
||||
- (void)textViewDidChange:(UITextView *)textView {
|
||||
[self.delegate argumentInputViewValueDidChange:self];
|
||||
self.placeholderLabel.hidden = !(self.inputPlaceholderText.length && !textView.text.length);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -3,14 +3,17 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/30/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) {
|
||||
/// 2 lines, medium-sized
|
||||
FLEXArgumentInputViewSizeDefault = 0,
|
||||
/// One line
|
||||
FLEXArgumentInputViewSizeSmall,
|
||||
/// Several lines
|
||||
FLEXArgumentInputViewSizeLarge
|
||||
};
|
||||
|
||||
@@ -26,12 +29,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 +51,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
|
||||
|
||||
@@ -3,32 +3,31 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/30/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXColor.h"
|
||||
|
||||
@interface FLEXArgumentInputView ()
|
||||
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@property (nonatomic, strong) NSString *typeEncoding;
|
||||
@property (nonatomic) UILabel *titleLabel;
|
||||
@property (nonatomic) NSString *typeEncoding;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithFrame:CGRectZero];
|
||||
if (self) {
|
||||
self.typeEncoding = @(typeEncoding);
|
||||
self.typeEncoding = typeEncoding != NULL ? @(typeEncoding) : nil;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
if (self.showsTitle) {
|
||||
@@ -38,14 +37,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||
[super setBackgroundColor:backgroundColor];
|
||||
self.titleLabel.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
- (void)setTitle:(NSString *)title
|
||||
{
|
||||
- (void)setTitle:(NSString *)title {
|
||||
if (![_title isEqual:title]) {
|
||||
_title = title;
|
||||
self.titleLabel.text = title;
|
||||
@@ -53,26 +50,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (UILabel *)titleLabel
|
||||
{
|
||||
- (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];
|
||||
_titleLabel.textColor = FLEXColor.primaryTextColor;
|
||||
_titleLabel.numberOfLines = 0;
|
||||
[self addSubview:_titleLabel];
|
||||
}
|
||||
return _titleLabel;
|
||||
}
|
||||
|
||||
- (BOOL)showsTitle
|
||||
{
|
||||
return [self.title length] > 0;
|
||||
- (BOOL)showsTitle {
|
||||
return self.title.length > 0;
|
||||
}
|
||||
|
||||
- (CGFloat)topInputFieldVerticalLayoutGuide
|
||||
{
|
||||
- (CGFloat)topInputFieldVerticalLayoutGuide {
|
||||
CGFloat verticalLayoutGuide = 0;
|
||||
if (self.showsTitle) {
|
||||
CGFloat titleHeight = [self.titleLabel sizeThatFits:self.bounds.size].height;
|
||||
@@ -84,48 +77,32 @@
|
||||
|
||||
#pragma mark - Subclasses Can Override
|
||||
|
||||
- (BOOL)inputViewIsFirstResponder
|
||||
{
|
||||
- (BOOL)inputViewIsFirstResponder {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
// Subclasses should override.
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
// Subclasses should override.
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Class Helpers
|
||||
|
||||
+ (UIFont *)titleFont
|
||||
{
|
||||
return [FLEXUtility defaultFontOfSize:12.0];
|
||||
+ (UIFont *)titleFont {
|
||||
return [UIFont systemFontOfSize:12.0];
|
||||
}
|
||||
|
||||
+ (CGFloat)titleBottomPadding
|
||||
{
|
||||
+ (CGFloat)titleBottomPadding {
|
||||
return 4.0;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Sizing
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
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];
|
||||
|
||||
+1
-2
@@ -7,8 +7,7 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class FLEXArgumentInputView;
|
||||
#import "FLEXArgumentInputSwitchView.h"
|
||||
|
||||
@interface FLEXArgumentInputViewFactory : NSObject
|
||||
|
||||
+26
-28
@@ -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,55 +17,53 @@
|
||||
#import "FLEXArgumentInputFontView.h"
|
||||
#import "FLEXArgumentInputColorView.h"
|
||||
#import "FLEXArgumentInputDateView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
@implementation FLEXArgumentInputViewFactory
|
||||
|
||||
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding {
|
||||
return [self argumentInputViewForTypeEncoding:typeEncoding currentValue:nil];
|
||||
}
|
||||
|
||||
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue
|
||||
{
|
||||
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue {
|
||||
Class subclass = [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:currentValue];
|
||||
if (!subclass) {
|
||||
// Fall back to a FLEXArgumentInputNotSupportedView if we can't find a subclass that fits the type encoding.
|
||||
// The unsupported view shows "nil" and does not allow user input.
|
||||
subclass = [FLEXArgumentInputNotSupportedView class];
|
||||
}
|
||||
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
|
||||
{
|
||||
+ (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;
|
||||
}
|
||||
|
||||
+ (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue
|
||||
{
|
||||
+ (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue {
|
||||
return [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:currentValue] != nil;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/23/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/23/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXDefaultEditorViewController.h"
|
||||
@@ -15,14 +15,13 @@
|
||||
@interface FLEXDefaultEditorViewController ()
|
||||
|
||||
@property (nonatomic, readonly) NSUserDefaults *defaults;
|
||||
@property (nonatomic, strong) NSString *key;
|
||||
@property (nonatomic) NSString *key;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXDefaultEditorViewController
|
||||
|
||||
- (id)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key
|
||||
{
|
||||
- (id)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key {
|
||||
self = [super initWithTarget:defaults];
|
||||
if (self) {
|
||||
self.key = key;
|
||||
@@ -31,26 +30,26 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSUserDefaults *)defaults
|
||||
{
|
||||
- (NSUserDefaults *)defaults {
|
||||
return [self.target isKindOfClass:[NSUserDefaults class]] ? self.target : nil;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
- (void)actionButtonPressed:(id)sender
|
||||
{
|
||||
- (void)actionButtonPressed:(id)sender {
|
||||
[super actionButtonPressed:sender];
|
||||
|
||||
id value = self.firstInputView.inputValue;
|
||||
@@ -64,9 +63,17 @@
|
||||
self.firstInputView.inputValue = [self.defaults objectForKey:self.key];
|
||||
}
|
||||
|
||||
+ (BOOL)canEditDefaultWithValue:(id)currentValue
|
||||
{
|
||||
return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:@encode(id) currentValue:currentValue];
|
||||
- (void)getterButtonPressed:(id)sender {
|
||||
[super getterButtonPressed:sender];
|
||||
id returnedObject = [self.defaults objectForKey:self.key];
|
||||
[self exploreObjectOrPopViewController:returnedObject];
|
||||
}
|
||||
|
||||
+ (BOOL)canEditDefaultWithValue:(id)currentValue {
|
||||
return [FLEXArgumentInputViewFactory
|
||||
canEditFieldWithTypeEncoding:FLEXEncodeObject(currentValue)
|
||||
currentValue:currentValue
|
||||
];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -3,16 +3,18 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class FLEXArgumentInputView;
|
||||
|
||||
@interface FLEXFieldEditorView : UIView
|
||||
|
||||
@property (nonatomic, copy) NSString *targetDescription;
|
||||
@property (nonatomic, copy) NSString *fieldDescription;
|
||||
|
||||
@property (nonatomic, strong) NSArray *argumentInputViews;
|
||||
@property (nonatomic, copy) NSArray<FLEXArgumentInputView *> *argumentInputViews;
|
||||
|
||||
@end
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorView.h"
|
||||
@@ -12,20 +12,19 @@
|
||||
|
||||
@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
|
||||
|
||||
@implementation FLEXFieldEditorView
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
- (id)initWithFrame:(CGRect)frame {
|
||||
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 +32,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];
|
||||
@@ -44,8 +43,7 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
CGFloat horizontalPadding = [[self class] horizontalPadding];
|
||||
@@ -78,15 +76,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||
[super setBackgroundColor:backgroundColor];
|
||||
self.targetDescriptionLabel.backgroundColor = backgroundColor;
|
||||
self.fieldDescriptionLabel.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
- (void)setTargetDescription:(NSString *)targetDescription
|
||||
{
|
||||
- (void)setTargetDescription:(NSString *)targetDescription {
|
||||
if (![_targetDescription isEqual:targetDescription]) {
|
||||
_targetDescription = targetDescription;
|
||||
self.targetDescriptionLabel.text = targetDescription;
|
||||
@@ -94,8 +90,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setFieldDescription:(NSString *)fieldDescription
|
||||
{
|
||||
- (void)setFieldDescription:(NSString *)fieldDescription {
|
||||
if (![_fieldDescription isEqual:fieldDescription]) {
|
||||
_fieldDescription = fieldDescription;
|
||||
self.fieldDescriptionLabel.text = fieldDescription;
|
||||
@@ -103,8 +98,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setArgumentInputViews:(NSArray *)argumentInputViews
|
||||
{
|
||||
- (void)setArgumentInputViews:(NSArray<FLEXArgumentInputView *> *)argumentInputViews {
|
||||
if (![_argumentInputViews isEqual:argumentInputViews]) {
|
||||
|
||||
for (FLEXArgumentInputView *inputView in _argumentInputViews) {
|
||||
@@ -121,40 +115,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
+ (UIView *)dividerView
|
||||
{
|
||||
UIView *dividerView = [[UIView alloc] init];
|
||||
+ (UIView *)dividerView {
|
||||
UIView *dividerView = [UIView new];
|
||||
dividerView.backgroundColor = [self dividerColor];
|
||||
return dividerView;
|
||||
}
|
||||
|
||||
+ (UIColor *)dividerColor
|
||||
{
|
||||
return [UIColor lightGrayColor];
|
||||
+ (UIColor *)dividerColor {
|
||||
return UIColor.lightGrayColor;
|
||||
}
|
||||
|
||||
+ (CGFloat)horizontalPadding
|
||||
{
|
||||
+ (CGFloat)horizontalPadding {
|
||||
return 10.0;
|
||||
}
|
||||
|
||||
+ (CGFloat)verticalPadding
|
||||
{
|
||||
+ (CGFloat)verticalPadding {
|
||||
return 20.0;
|
||||
}
|
||||
|
||||
+ (UIFont *)labelFont
|
||||
{
|
||||
return [FLEXUtility defaultFontOfSize:14.0];
|
||||
+ (UIFont *)labelFont {
|
||||
return [UIFont systemFontOfSize:14.0];
|
||||
}
|
||||
|
||||
+ (CGFloat)dividerLineHeight
|
||||
{
|
||||
+ (CGFloat)dividerLineHeight {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGFloat horizontalPadding = [[self class] horizontalPadding];
|
||||
CGFloat verticalPadding = [[self class] verticalPadding];
|
||||
CGFloat dividerLineHeight = [[self class] dividerLineHeight];
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
//
|
||||
// FLEXFieldEditorViewController.h
|
||||
// Flipboard
|
||||
// FLEX
|
||||
//
|
||||
// Created by Ryan Olson on 5/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Created by Tanner on 11/22/18.
|
||||
// Copyright © 2018 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXVariableEditorViewController.h"
|
||||
#import "FLEXProperty.h"
|
||||
#import "FLEXIvar.h"
|
||||
|
||||
@class FLEXFieldEditorView;
|
||||
@class FLEXArgumentInputView;
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXFieldEditorViewController : UIViewController
|
||||
@interface FLEXFieldEditorViewController : FLEXVariableEditorViewController
|
||||
|
||||
- (id)initWithTarget:(id)target;
|
||||
/// @return nil if the property is readonly or if the type is unsupported
|
||||
+ (nullable instancetype)target:(id)target property:(FLEXProperty *)property;
|
||||
/// @return nil if the ivar type is unsupported
|
||||
+ (nullable instancetype)target:(id)target ivar:(FLEXIvar *)ivar;
|
||||
|
||||
// Convenience accessor since many subclasses only use one input view
|
||||
@property (nonatomic, readonly) FLEXArgumentInputView *firstInputView;
|
||||
/// Subclasses can change the button title via the \c title property
|
||||
@property (nonatomic, readonly) UIBarButtonItem *getterButton;
|
||||
|
||||
// For subclass use only.
|
||||
@property (nonatomic, strong, readonly) id target;
|
||||
@property (nonatomic, strong, readonly) FLEXFieldEditorView *fieldEditorView;
|
||||
@property (nonatomic, strong, readonly) UIBarButtonItem *setterButton;
|
||||
- (void)actionButtonPressed:(id)sender;
|
||||
- (NSString *)titleForActionButton;
|
||||
- (void)getterButtonPressed:(id)sender;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -1,117 +1,163 @@
|
||||
//
|
||||
// FLEXFieldEditorViewController.m
|
||||
// Flipboard
|
||||
// FLEX
|
||||
//
|
||||
// Created by Ryan Olson on 5/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Created by Tanner on 11/22/18.
|
||||
// Copyright © 2018 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
#import "FLEXFieldEditorView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXPropertyAttributes.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
|
||||
@interface FLEXFieldEditorViewController () <UIScrollViewDelegate>
|
||||
@interface FLEXFieldEditorViewController () <FLEXArgumentInputViewDelegate>
|
||||
|
||||
@property (nonatomic, strong) UIScrollView *scrollView;
|
||||
@property (nonatomic) FLEXProperty *property;
|
||||
@property (nonatomic) FLEXIvar *ivar;
|
||||
|
||||
@property (nonatomic, strong, readwrite) id target;
|
||||
@property (nonatomic, strong, readwrite) FLEXFieldEditorView *fieldEditorView;
|
||||
@property (nonatomic, strong, readwrite) UIBarButtonItem *setterButton;
|
||||
@property (nonatomic, readonly) id currentValue;
|
||||
@property (nonatomic, readonly) const FLEXTypeEncoding *typeEncoding;
|
||||
@property (nonatomic, readonly) NSString *fieldDescription;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXFieldEditorViewController
|
||||
|
||||
- (id)initWithTarget:(id)target
|
||||
{
|
||||
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];
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (instancetype)target:(id)target property:(FLEXProperty *)property {
|
||||
id value = [property getValue:target];
|
||||
if (![self canEditProperty:property onObject:target currentValue:value]) {
|
||||
return nil;
|
||||
}
|
||||
return self;
|
||||
|
||||
FLEXFieldEditorViewController *editor = [self target:target];
|
||||
editor.title = [@"Property: " stringByAppendingString:property.name];
|
||||
editor.property = property;
|
||||
return editor;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
+ (instancetype)target:(id)target ivar:(nonnull FLEXIvar *)ivar {
|
||||
FLEXFieldEditorViewController *editor = [self target:target];
|
||||
editor.title = [@"Ivar: " stringByAppendingString:ivar.name];
|
||||
editor.ivar = ivar;
|
||||
return editor;
|
||||
}
|
||||
|
||||
- (void)keyboardDidShow:(NSNotification *)notification
|
||||
{
|
||||
CGRect keyboardRectInWindow = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
||||
CGSize keyboardSize = [self.view convertRect:keyboardRectInWindow fromView:nil].size;
|
||||
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
|
||||
scrollInsets.bottom = keyboardSize.height;
|
||||
self.scrollView.contentInset = scrollInsets;
|
||||
self.scrollView.scrollIndicatorInsets = scrollInsets;
|
||||
|
||||
// Find the active input view and scroll to make sure it's visible.
|
||||
for (FLEXArgumentInputView *argumentInputView in self.fieldEditorView.argumentInputViews) {
|
||||
if (argumentInputView.inputViewIsFirstResponder) {
|
||||
CGRect scrollToVisibleRect = [self.scrollView convertRect:argumentInputView.bounds fromView:argumentInputView];
|
||||
[self.scrollView scrollRectToVisible:scrollToVisibleRect animated:YES];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma mark - Overrides
|
||||
|
||||
- (void)keyboardWillHide:(NSNotification *)notification
|
||||
{
|
||||
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
|
||||
scrollInsets.bottom = 0.0;
|
||||
self.scrollView.contentInset = scrollInsets;
|
||||
self.scrollView.scrollIndicatorInsets = scrollInsets;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.view.backgroundColor = [FLEXUtility scrollViewGrayColor];
|
||||
|
||||
self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
|
||||
self.scrollView.backgroundColor = self.view.backgroundColor;
|
||||
self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
self.scrollView.delegate = self;
|
||||
[self.view addSubview:self.scrollView];
|
||||
|
||||
self.fieldEditorView = [[FLEXFieldEditorView alloc] init];
|
||||
self.fieldEditorView.backgroundColor = self.view.backgroundColor;
|
||||
self.fieldEditorView.targetDescription = [NSString stringWithFormat:@"%@ %p", [self.target class], self.target];
|
||||
[self.scrollView addSubview:self.fieldEditorView];
|
||||
|
||||
self.setterButton = [[UIBarButtonItem alloc] initWithTitle:[self titleForActionButton] style:UIBarButtonItemStyleDone target:self action:@selector(actionButtonPressed:)];
|
||||
self.navigationItem.rightBarButtonItem = self.setterButton;
|
||||
|
||||
self.view.backgroundColor = FLEXColor.groupedBackgroundColor;
|
||||
|
||||
// Create getter button
|
||||
_getterButton = [[UIBarButtonItem alloc]
|
||||
initWithTitle:@"Get"
|
||||
style:UIBarButtonItemStyleDone
|
||||
target:self
|
||||
action:@selector(getterButtonPressed:)
|
||||
];
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_flexibleSpace, self.getterButton, self.actionButton
|
||||
];
|
||||
|
||||
// Configure input view
|
||||
self.fieldEditorView.fieldDescription = self.fieldDescription;
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:self.typeEncoding];
|
||||
inputView.inputValue = self.currentValue;
|
||||
inputView.delegate = self;
|
||||
self.fieldEditorView.argumentInputViews = @[inputView];
|
||||
|
||||
// Don't show a "set" button for switches; we mutate when the switch is flipped
|
||||
if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
|
||||
self.actionButton.enabled = NO;
|
||||
self.actionButton.title = @"Flip the switch to call the setter";
|
||||
// Put getter button before setter button
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_flexibleSpace, self.actionButton, self.getterButton
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewWillLayoutSubviews
|
||||
{
|
||||
CGSize constrainSize = CGSizeMake(self.scrollView.bounds.size.width, CGFLOAT_MAX);
|
||||
CGSize fieldEditorSize = [self.fieldEditorView sizeThatFits:constrainSize];
|
||||
self.fieldEditorView.frame = CGRectMake(0, 0, fieldEditorSize.width, fieldEditorSize.height);
|
||||
self.scrollView.contentSize = fieldEditorSize;
|
||||
- (void)actionButtonPressed:(id)sender {
|
||||
[super actionButtonPressed:sender];
|
||||
|
||||
if (self.property) {
|
||||
id userInputObject = self.firstInputView.inputValue;
|
||||
NSArray *arguments = userInputObject ? @[userInputObject] : nil;
|
||||
SEL setterSelector = self.property.likelySetter;
|
||||
NSError *error = nil;
|
||||
[FLEXRuntimeUtility performSelector:setterSelector onObject:self.target withArguments:arguments error:&error];
|
||||
if (error) {
|
||||
[FLEXAlert showAlert:@"Property Setter Failed" message:error.localizedDescription from:self];
|
||||
sender = nil; // Don't pop back
|
||||
}
|
||||
} else {
|
||||
// TODO: check mutability and use mutableCopy if necessary;
|
||||
// this currently could and would assign NSArray to NSMutableArray
|
||||
[self.ivar setValue:self.firstInputView.inputValue onObject:self.target];
|
||||
}
|
||||
|
||||
// Go back after setting, but not for switches.
|
||||
if (sender) {
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
} else {
|
||||
self.firstInputView.inputValue = self.currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
- (FLEXArgumentInputView *)firstInputView
|
||||
{
|
||||
return [[self.fieldEditorView argumentInputViews] firstObject];
|
||||
}
|
||||
|
||||
- (void)actionButtonPressed:(id)sender
|
||||
{
|
||||
// Subclasses can override
|
||||
- (void)getterButtonPressed:(id)sender {
|
||||
[self.fieldEditorView endEditing:YES];
|
||||
|
||||
[self exploreObjectOrPopViewController:self.currentValue];
|
||||
}
|
||||
|
||||
- (NSString *)titleForActionButton
|
||||
{
|
||||
// Subclasses can override.
|
||||
return @"Set";
|
||||
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView {
|
||||
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
|
||||
[self actionButtonPressed:nil];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (id)currentValue {
|
||||
if (self.property) {
|
||||
return [self.property getValue:self.target];
|
||||
} else {
|
||||
return [self.ivar getValue:self.target];
|
||||
}
|
||||
}
|
||||
|
||||
- (const FLEXTypeEncoding *)typeEncoding {
|
||||
if (self.property) {
|
||||
return self.property.attributes.typeEncoding.UTF8String;
|
||||
} else {
|
||||
return self.ivar.typeEncoding.UTF8String;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)fieldDescription {
|
||||
if (self.property) {
|
||||
return self.property.fullDescription;
|
||||
} else {
|
||||
return self.ivar.description;
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)canEditProperty:(FLEXProperty *)property onObject:(id)object currentValue:(id)value {
|
||||
const FLEXTypeEncoding *typeEncoding = property.attributes.typeEncoding.UTF8String;
|
||||
BOOL canEditType = [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:typeEncoding currentValue:value];
|
||||
return canEditType && [object respondsToSelector:property.likelySetter];
|
||||
}
|
||||
|
||||
+ (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value {
|
||||
return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:ivar_getTypeEncoding(ivar) currentValue:value];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// FLEXIvarEditorViewController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/23/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@interface FLEXIvarEditorViewController : FLEXFieldEditorViewController
|
||||
|
||||
- (id)initWithTarget:(id)target ivar:(Ivar)ivar;
|
||||
|
||||
+ (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value;
|
||||
|
||||
@end
|
||||
@@ -1,72 +0,0 @@
|
||||
//
|
||||
// FLEXIvarEditorViewController.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/23/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXIvarEditorViewController.h"
|
||||
#import "FLEXFieldEditorView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXArgumentInputSwitchView.h"
|
||||
|
||||
@interface FLEXIvarEditorViewController () <FLEXArgumentInputViewDelegate>
|
||||
|
||||
@property (nonatomic, assign) Ivar ivar;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXIvarEditorViewController
|
||||
|
||||
- (id)initWithTarget:(id)target ivar:(Ivar)ivar
|
||||
{
|
||||
self = [super initWithTarget:target];
|
||||
if (self) {
|
||||
self.ivar = ivar;
|
||||
self.title = @"Instance Variable";
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility prettyNameForIvar:self.ivar];
|
||||
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:ivar_getTypeEncoding(self.ivar)];
|
||||
inputView.backgroundColor = self.view.backgroundColor;
|
||||
inputView.inputValue = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
|
||||
inputView.delegate = self;
|
||||
self.fieldEditorView.argumentInputViews = @[inputView];
|
||||
|
||||
// Don't show a "set" button for switches. Set the ivar when the switch toggles.
|
||||
if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
|
||||
self.navigationItem.rightBarButtonItem = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)actionButtonPressed:(id)sender
|
||||
{
|
||||
[super actionButtonPressed:sender];
|
||||
|
||||
[FLEXRuntimeUtility setValue:self.firstInputView.inputValue forIvar:self.ivar onObject:self.target];
|
||||
self.firstInputView.inputValue = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
|
||||
}
|
||||
|
||||
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView
|
||||
{
|
||||
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
|
||||
[self actionButtonPressed:nil];
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value
|
||||
{
|
||||
return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:ivar_getTypeEncoding(ivar) currentValue:value];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -3,14 +3,14 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/23/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
#import <objc/runtime.h>
|
||||
#import "FLEXVariableEditorViewController.h"
|
||||
#import "FLEXMethod.h"
|
||||
|
||||
@interface FLEXMethodCallingViewController : FLEXFieldEditorViewController
|
||||
@interface FLEXMethodCallingViewController : FLEXVariableEditorViewController
|
||||
|
||||
- (id)initWithTarget:(id)target method:(Method)method;
|
||||
+ (instancetype)target:(id)target method:(FLEXMethod *)method;
|
||||
|
||||
@end
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/23/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXMethodCallingViewController.h"
|
||||
@@ -13,87 +13,93 @@
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXMethodCallingViewController ()
|
||||
|
||||
@property (nonatomic, assign) Method method;
|
||||
|
||||
@property (nonatomic) FLEXMethod *method;
|
||||
@end
|
||||
|
||||
@implementation FLEXMethodCallingViewController
|
||||
|
||||
- (id)initWithTarget:(id)target method:(Method)method
|
||||
{
|
||||
+ (instancetype)target:(id)target method:(FLEXMethod *)method {
|
||||
return [[self alloc] initWithTarget:target method:method];
|
||||
}
|
||||
|
||||
- (id)initWithTarget:(id)target method:(FLEXMethod *)method {
|
||||
NSParameterAssert(method.isInstanceMethod == !object_isClass(target));
|
||||
|
||||
self = [super initWithTarget:target];
|
||||
if (self) {
|
||||
self.method = method;
|
||||
self.title = [self isClassMethod] ? @"Class Method" : @"Method";
|
||||
self.title = method.isInstanceMethod ? @"Method: " : @"Class Method: ";
|
||||
self.title = [self.title stringByAppendingString:method.selectorString];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility prettyNameForMethod:self.method isClassMethod:[self isClassMethod]];
|
||||
|
||||
NSArray *methodComponents = [FLEXRuntimeUtility prettyArgumentComponentsForMethod:self.method];
|
||||
NSMutableArray *argumentInputViews = [NSMutableArray array];
|
||||
|
||||
self.actionButton.title = @"Call";
|
||||
|
||||
// Configure field editor view
|
||||
self.fieldEditorView.argumentInputViews = [self argumentInputViews];
|
||||
self.fieldEditorView.fieldDescription = [NSString stringWithFormat:
|
||||
@"Signature:\n%@\n\nReturn Type:\n%s",
|
||||
self.method.description, (char *)self.method.returnType
|
||||
];
|
||||
}
|
||||
|
||||
- (NSArray<FLEXArgumentInputView *> *)argumentInputViews {
|
||||
Method method = self.method.objc_method;
|
||||
NSArray *methodComponents = [FLEXRuntimeUtility prettyArgumentComponentsForMethod:method];
|
||||
NSMutableArray<FLEXArgumentInputView *> *argumentInputViews = [NSMutableArray new];
|
||||
unsigned int argumentIndex = kFLEXNumberOfImplicitArgs;
|
||||
|
||||
for (NSString *methodComponent in methodComponents) {
|
||||
char *argumentTypeEncoding = method_copyArgumentType(self.method, argumentIndex);
|
||||
char *argumentTypeEncoding = method_copyArgumentType(method, argumentIndex);
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:argumentTypeEncoding];
|
||||
free(argumentTypeEncoding);
|
||||
|
||||
|
||||
inputView.backgroundColor = self.view.backgroundColor;
|
||||
inputView.title = methodComponent;
|
||||
[argumentInputViews addObject:inputView];
|
||||
argumentIndex++;
|
||||
}
|
||||
self.fieldEditorView.argumentInputViews = argumentInputViews;
|
||||
|
||||
return argumentInputViews;
|
||||
}
|
||||
|
||||
- (BOOL)isClassMethod
|
||||
{
|
||||
return self.target && self.target == [self.target class];
|
||||
}
|
||||
|
||||
- (NSString *)titleForActionButton
|
||||
{
|
||||
return @"Call";
|
||||
}
|
||||
|
||||
- (void)actionButtonPressed:(id)sender
|
||||
{
|
||||
- (void)actionButtonPressed:(id)sender {
|
||||
[super actionButtonPressed:sender];
|
||||
|
||||
NSMutableArray *arguments = [NSMutableArray array];
|
||||
|
||||
// Gather arguments
|
||||
NSMutableArray *arguments = [NSMutableArray new];
|
||||
for (FLEXArgumentInputView *inputView in self.fieldEditorView.argumentInputViews) {
|
||||
id argumentValue = inputView.inputValue;
|
||||
if (!argumentValue) {
|
||||
// Use NSNulls as placeholders in the array. They will be interpreted as nil arguments.
|
||||
argumentValue = [NSNull null];
|
||||
}
|
||||
[arguments addObject:argumentValue];
|
||||
// Use NSNull as a nil placeholder; it will be interpreted as nil
|
||||
[arguments addObject:inputView.inputValue ?: NSNull.null];
|
||||
}
|
||||
|
||||
|
||||
// Call method
|
||||
NSError *error = nil;
|
||||
id returnedObject = [FLEXRuntimeUtility performSelector:method_getName(self.method) onObject:self.target withArguments:arguments error:&error];
|
||||
|
||||
id returnValue = [FLEXRuntimeUtility
|
||||
performSelector:self.method.selector
|
||||
onObject:self.target
|
||||
withArguments:arguments
|
||||
error:&error
|
||||
];
|
||||
|
||||
// Display return value or 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];
|
||||
} else if (returnedObject) {
|
||||
[FLEXAlert showAlert:@"Method Call Failed" message:error.localizedDescription from:self];
|
||||
} else if (returnValue) {
|
||||
// For non-nil (or void) return types, push an explorer view controller to display the returned object
|
||||
FLEXObjectExplorerViewController *explorerViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:returnedObject];
|
||||
[self.navigationController pushViewController:explorerViewController animated:YES];
|
||||
returnValue = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:returnValue type:self.method.returnType];
|
||||
FLEXObjectExplorerViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:returnValue];
|
||||
[self.navigationController pushViewController:explorer 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:returnValue];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// FLEXPropertyEditorViewController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/20/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@interface FLEXPropertyEditorViewController : FLEXFieldEditorViewController
|
||||
|
||||
- (id)initWithTarget:(id)target property:(objc_property_t)property;
|
||||
|
||||
+ (BOOL)canEditProperty:(objc_property_t)property currentValue:(id)value;
|
||||
|
||||
@end
|
||||
@@ -1,94 +0,0 @@
|
||||
//
|
||||
// FLEXPropertyEditorViewController.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/20/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXPropertyEditorViewController.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXFieldEditorView.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXArgumentInputSwitchView.h"
|
||||
|
||||
@interface FLEXPropertyEditorViewController () <FLEXArgumentInputViewDelegate>
|
||||
|
||||
@property (nonatomic, assign) objc_property_t property;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXPropertyEditorViewController
|
||||
|
||||
- (id)initWithTarget:(id)target property:(objc_property_t)property
|
||||
{
|
||||
self = [super initWithTarget:target];
|
||||
if (self) {
|
||||
self.property = property;
|
||||
self.title = @"Property";
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
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];
|
||||
|
||||
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];
|
||||
inputView.delegate = self;
|
||||
self.fieldEditorView.argumentInputViews = @[inputView];
|
||||
|
||||
// Don't show a "set" button for switches - just call the setter immediately after the switch toggles.
|
||||
if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
|
||||
self.navigationItem.rightBarButtonItem = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)actionButtonPressed:(id)sender
|
||||
{
|
||||
[super actionButtonPressed:sender];
|
||||
|
||||
id userInputObject = self.firstInputView.inputValue;
|
||||
NSArray *arguments = userInputObject ? @[userInputObject] : nil;
|
||||
SEL setterSelector = [FLEXRuntimeUtility setterSelectorForProperty:self.property];
|
||||
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];
|
||||
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.
|
||||
// Don't do this for simulated taps on the action button (i.e. from switch/BOOL editors). The experience is weird there.
|
||||
if (sender) {
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView
|
||||
{
|
||||
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
|
||||
[self actionButtonPressed:nil];
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)canEditProperty:(objc_property_t)property currentValue:(id)value
|
||||
{
|
||||
const char *typeEncoding = [[FLEXRuntimeUtility typeEncodingForProperty:property] UTF8String];
|
||||
BOOL canEditType = [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:typeEncoding currentValue:value];
|
||||
BOOL isReadonly = [FLEXRuntimeUtility isReadonlyProperty:property];
|
||||
return canEditType && !isReadonly;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// FLEXVariableEditorViewController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/16/14.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class FLEXFieldEditorView;
|
||||
@class FLEXArgumentInputView;
|
||||
|
||||
/// Provides a screen for editing or configuring one or more variables.
|
||||
@interface FLEXVariableEditorViewController : UIViewController
|
||||
|
||||
+ (instancetype)target:(id)target;
|
||||
- (id)initWithTarget:(id)target;
|
||||
|
||||
// Convenience accessor since many subclasses only use one input view
|
||||
@property (nonatomic, readonly) FLEXArgumentInputView *firstInputView;
|
||||
|
||||
// For subclass use only.
|
||||
@property (nonatomic, readonly) id target;
|
||||
@property (nonatomic, readonly) FLEXFieldEditorView *fieldEditorView;
|
||||
/// Subclasses can change the button title via the button's \c title property
|
||||
@property (nonatomic, readonly) UIBarButtonItem *actionButton;
|
||||
|
||||
- (void)actionButtonPressed:(id)sender;
|
||||
|
||||
/// Pushes an explorer view controller for the given object
|
||||
/// or pops the current view controller.
|
||||
- (void)exploreObjectOrPopViewController:(id)objectOrNil;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,137 @@
|
||||
//
|
||||
// FLEXVariableEditorViewController.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/16/14.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXVariableEditorViewController.h"
|
||||
#import "FLEXFieldEditorView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
|
||||
@interface FLEXVariableEditorViewController () <UIScrollViewDelegate>
|
||||
@property (nonatomic) UIScrollView *scrollView;
|
||||
@property (nonatomic) id target;
|
||||
@end
|
||||
|
||||
@implementation FLEXVariableEditorViewController
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (instancetype)target:(id)target {
|
||||
return [[self alloc] initWithTarget:target];
|
||||
}
|
||||
|
||||
- (id)initWithTarget:(id)target {
|
||||
self = [super init];
|
||||
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
|
||||
];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[NSNotificationCenter.defaultCenter removeObserver:self];
|
||||
}
|
||||
|
||||
#pragma mark - UIViewController methods
|
||||
|
||||
- (void)keyboardDidShow:(NSNotification *)notification {
|
||||
CGRect keyboardRectInWindow = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
||||
CGSize keyboardSize = [self.view convertRect:keyboardRectInWindow fromView:nil].size;
|
||||
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
|
||||
scrollInsets.bottom = keyboardSize.height;
|
||||
self.scrollView.contentInset = scrollInsets;
|
||||
self.scrollView.scrollIndicatorInsets = scrollInsets;
|
||||
|
||||
// Find the active input view and scroll to make sure it's visible.
|
||||
for (FLEXArgumentInputView *argumentInputView in self.fieldEditorView.argumentInputViews) {
|
||||
if (argumentInputView.inputViewIsFirstResponder) {
|
||||
CGRect scrollToVisibleRect = [self.scrollView convertRect:argumentInputView.bounds fromView:argumentInputView];
|
||||
[self.scrollView scrollRectToVisible:scrollToVisibleRect animated:YES];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)keyboardWillHide:(NSNotification *)notification {
|
||||
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
|
||||
scrollInsets.bottom = 0.0;
|
||||
self.scrollView.contentInset = scrollInsets;
|
||||
self.scrollView.scrollIndicatorInsets = scrollInsets;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.view.backgroundColor = FLEXColor.scrollViewBackgroundColor;
|
||||
|
||||
self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
|
||||
self.scrollView.backgroundColor = self.view.backgroundColor;
|
||||
self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
self.scrollView.delegate = self;
|
||||
[self.view addSubview:self.scrollView];
|
||||
|
||||
_fieldEditorView = [FLEXFieldEditorView new];
|
||||
self.fieldEditorView.targetDescription = [NSString stringWithFormat:@"%@ %p", [self.target class], self.target];
|
||||
[self.scrollView addSubview:self.fieldEditorView];
|
||||
|
||||
_actionButton = [[UIBarButtonItem alloc]
|
||||
initWithTitle:@"Set"
|
||||
style:UIBarButtonItemStyleDone
|
||||
target:self
|
||||
action:@selector(actionButtonPressed:)
|
||||
];
|
||||
|
||||
self.navigationController.toolbarHidden = NO;
|
||||
self.toolbarItems = @[UIBarButtonItem.flex_flexibleSpace, self.actionButton];
|
||||
}
|
||||
|
||||
- (void)viewWillLayoutSubviews {
|
||||
CGSize constrainSize = CGSizeMake(self.scrollView.bounds.size.width, CGFLOAT_MAX);
|
||||
CGSize fieldEditorSize = [self.fieldEditorView sizeThatFits:constrainSize];
|
||||
self.fieldEditorView.frame = CGRectMake(0, 0, fieldEditorSize.width, fieldEditorSize.height);
|
||||
self.scrollView.contentSize = fieldEditorSize;
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (FLEXArgumentInputView *)firstInputView {
|
||||
return [self.fieldEditorView argumentInputViews].firstObject;
|
||||
}
|
||||
|
||||
- (void)actionButtonPressed:(id)sender {
|
||||
// Subclasses can override
|
||||
[self.fieldEditorView endEditing:YES];
|
||||
}
|
||||
|
||||
- (void)exploreObjectOrPopViewController:(id)objectOrNil {
|
||||
if (objectOrNil) {
|
||||
// For non-nil (or void) return types, push an explorer view controller to display the object
|
||||
FLEXObjectExplorerViewController *explorerViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:objectOrNil];
|
||||
[self.navigationController pushViewController:explorerViewController animated:YES];
|
||||
} else {
|
||||
// If we didn't get a returned object but the method call succeeded,
|
||||
// pop this view controller off the stack to indicate that the call went through.
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// FLEXBookmarkManager.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/6/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXBookmarkManager : NSObject
|
||||
|
||||
@property (nonatomic, readonly, class) NSMutableArray *bookmarks;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// FLEXBookmarkManager.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/6/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXBookmarkManager.h"
|
||||
|
||||
static NSMutableArray *kFLEXBookmarkManagerBookmarks = nil;
|
||||
|
||||
@implementation FLEXBookmarkManager
|
||||
|
||||
+ (void)initialize {
|
||||
if (self == [FLEXBookmarkManager class]) {
|
||||
kFLEXBookmarkManagerBookmarks = [NSMutableArray new];
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSMutableArray *)bookmarks {
|
||||
return kFLEXBookmarkManagerBookmarks;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEXBookmarksViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/6/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXBookmarksViewController : FLEXTableViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,235 @@
|
||||
//
|
||||
// FLEXBookmarksViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/6/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXBookmarksViewController.h"
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXNavigationController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXBookmarkManager.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXTableView.h"
|
||||
|
||||
@interface FLEXBookmarksViewController ()
|
||||
@property (nonatomic, copy) NSArray *bookmarks;
|
||||
@property (nonatomic, readonly) FLEXExplorerViewController *corePresenter;
|
||||
@end
|
||||
|
||||
@implementation FLEXBookmarksViewController
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
- (id)init {
|
||||
return [self initWithStyle:UITableViewStylePlain];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.navigationController.hidesBarsOnSwipe = NO;
|
||||
self.tableView.allowsMultipleSelectionDuringEditing = YES;
|
||||
|
||||
[self reloadData];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
[self setupDefaultBarItems];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)reloadData {
|
||||
// We assume the bookmarks aren't going to change out from under us, since
|
||||
// presenting any other tool via keyboard shortcuts should dismiss us first
|
||||
self.bookmarks = FLEXBookmarkManager.bookmarks;
|
||||
self.title = [NSString stringWithFormat:@"Bookmarks (%@)", @(self.bookmarks.count)];
|
||||
}
|
||||
|
||||
- (void)setupDefaultBarItems {
|
||||
self.navigationItem.rightBarButtonItem = FLEXBarButtonItemSystem(Done, self, @selector(dismissAnimated));
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
FLEXBarButtonItemSystem(Edit, self, @selector(toggleEditing)),
|
||||
];
|
||||
|
||||
// Disable editing if no bookmarks available
|
||||
self.toolbarItems.lastObject.enabled = self.bookmarks.count > 0;
|
||||
}
|
||||
|
||||
- (void)setupEditingBarItems {
|
||||
self.navigationItem.rightBarButtonItem = nil;
|
||||
self.toolbarItems = @[
|
||||
[UIBarButtonItem itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed:)],
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
// We use a non-system done item because we change its title dynamically
|
||||
[UIBarButtonItem doneStyleitemWithTitle:@"Done" target:self action:@selector(toggleEditing)]
|
||||
];
|
||||
|
||||
self.toolbarItems.firstObject.tintColor = FLEXColor.destructiveColor;
|
||||
}
|
||||
|
||||
- (FLEXExplorerViewController *)corePresenter {
|
||||
// We must be presented by a FLEXExplorerViewController, or presented
|
||||
// by another view controller that was presented by FLEXExplorerViewController
|
||||
FLEXExplorerViewController *presenter = (id)self.presentingViewController;
|
||||
presenter = (id)presenter.presentingViewController ?: presenter;
|
||||
presenter = (id)presenter.presentingViewController ?: presenter;
|
||||
NSAssert(
|
||||
[presenter isKindOfClass:[FLEXExplorerViewController class]],
|
||||
@"The bookmarks view controller expects to be presented by the explorer controller"
|
||||
);
|
||||
return presenter;
|
||||
}
|
||||
|
||||
#pragma mark Button Actions
|
||||
|
||||
- (void)dismissAnimated {
|
||||
[self dismissAnimated:nil];
|
||||
}
|
||||
|
||||
- (void)dismissAnimated:(id)selectedObject {
|
||||
if (selectedObject) {
|
||||
UIViewController *explorer = [FLEXObjectExplorerFactory
|
||||
explorerViewControllerForObject:selectedObject
|
||||
];
|
||||
if ([self.presentingViewController isKindOfClass:[FLEXNavigationController class]]) {
|
||||
// I am presented on an existing navigation stack, so
|
||||
// dismiss myself and push the bookmark there
|
||||
UINavigationController *presenter = (id)self.presentingViewController;
|
||||
[presenter dismissViewControllerAnimated:YES completion:^{
|
||||
[presenter pushViewController:explorer animated:YES];
|
||||
}];
|
||||
} else {
|
||||
// Dismiss myself and present explorer
|
||||
UIViewController *presenter = self.corePresenter;
|
||||
[presenter dismissViewControllerAnimated:YES completion:^{
|
||||
[presenter presentViewController:[FLEXNavigationController
|
||||
withRootViewController:explorer
|
||||
] animated:YES completion:nil];
|
||||
}];
|
||||
}
|
||||
} else {
|
||||
// Just dismiss myself
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)toggleEditing {
|
||||
NSArray<NSIndexPath *> *selected = self.tableView.indexPathsForSelectedRows;
|
||||
self.editing = !self.editing;
|
||||
|
||||
if (self.isEditing) {
|
||||
[self setupEditingBarItems];
|
||||
} else {
|
||||
[self setupDefaultBarItems];
|
||||
|
||||
// Get index set of bookmarks to close
|
||||
NSMutableIndexSet *indexes = [NSMutableIndexSet new];
|
||||
for (NSIndexPath *ip in selected) {
|
||||
[indexes addIndex:ip.row];
|
||||
}
|
||||
|
||||
if (selected.count) {
|
||||
// Close bookmarks and update data source
|
||||
[FLEXBookmarkManager.bookmarks removeObjectsAtIndexes:indexes];
|
||||
[self reloadData];
|
||||
|
||||
// Remove deleted rows
|
||||
[self.tableView deleteRowsAtIndexPaths:selected withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)closeAllButtonPressed:(UIBarButtonItem *)sender {
|
||||
[FLEXAlert makeSheet:^(FLEXAlert *make) {
|
||||
NSInteger count = self.bookmarks.count;
|
||||
NSString *title = FLEXPluralFormatString(count, @"Remove %@ bookmarks", @"Remove %@ bookmark");
|
||||
make.button(title).destructiveStyle().handler(^(NSArray<NSString *> *strings) {
|
||||
[self closeAll];
|
||||
[self toggleEditing];
|
||||
});
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:self source:sender];
|
||||
}
|
||||
|
||||
- (void)closeAll {
|
||||
NSInteger rowCount = self.bookmarks.count;
|
||||
|
||||
// Close bookmarks and update data source
|
||||
[FLEXBookmarkManager.bookmarks removeAllObjects];
|
||||
[self reloadData];
|
||||
|
||||
// Delete rows from table view
|
||||
NSArray<NSIndexPath *> *allRows = [NSArray flex_forEachUpTo:rowCount map:^id(NSUInteger row) {
|
||||
return [NSIndexPath indexPathForRow:row inSection:0];
|
||||
}];
|
||||
[self.tableView deleteRowsAtIndexPaths:allRows withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Data Source
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.bookmarks.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(FLEXTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath];
|
||||
|
||||
id object = self.bookmarks[indexPath.row];
|
||||
cell.textLabel.text = [FLEXRuntimeUtility safeDescriptionForObject:object];
|
||||
cell.detailTextLabel.text = [NSString stringWithFormat:@"%@ — %p", [object class], object];
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Delegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (self.editing) {
|
||||
// Case: editing with multi-select
|
||||
self.toolbarItems.lastObject.title = @"Remove Selected";
|
||||
self.toolbarItems.lastObject.tintColor = FLEXColor.destructiveColor;
|
||||
} else {
|
||||
// Case: selected a bookmark
|
||||
[self dismissAnimated:self.bookmarks[indexPath.row]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSParameterAssert(self.editing);
|
||||
|
||||
if (tableView.indexPathsForSelectedRows.count == 0) {
|
||||
self.toolbarItems.lastObject.title = @"Done";
|
||||
self.toolbarItems.lastObject.tintColor = self.view.tintColor;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)table
|
||||
commitEditingStyle:(UITableViewCellEditingStyle)edit
|
||||
forRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSParameterAssert(edit == UITableViewCellEditingStyleDelete);
|
||||
|
||||
// Remove bookmark and update data source
|
||||
[FLEXBookmarkManager.bookmarks removeObjectAtIndex:indexPath.row];
|
||||
[self reloadData];
|
||||
|
||||
// Delete row from table view
|
||||
[table deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// FLEXExplorerViewController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXExplorerToolbar.h"
|
||||
|
||||
@class FLEXWindow;
|
||||
@protocol FLEXExplorerViewControllerDelegate;
|
||||
|
||||
/// A view controller that manages the FLEX toolbar.
|
||||
@interface FLEXExplorerViewController : UIViewController
|
||||
|
||||
@property (nonatomic, weak) id <FLEXExplorerViewControllerDelegate> delegate;
|
||||
@property (nonatomic, readonly) BOOL wantsWindowToBecomeKey;
|
||||
|
||||
@property (nonatomic, readonly) FLEXExplorerToolbar *explorerToolbar;
|
||||
|
||||
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates;
|
||||
|
||||
/// @brief Used to present (or dismiss) a modal view controller ("tool"), typically triggered by pressing a button in the toolbar.
|
||||
///
|
||||
/// If a tool is already presented, this method simply dismisses it and calls the completion block.
|
||||
/// If no tool is presented, @code future() @endcode is presented and the completion block is called.
|
||||
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future completion:(void(^)(void))completion;
|
||||
|
||||
// Keyboard shortcut helpers
|
||||
|
||||
- (void)toggleSelectTool;
|
||||
- (void)toggleMoveTool;
|
||||
- (void)toggleViewsTool;
|
||||
- (void)toggleMenuTool;
|
||||
|
||||
/// @return YES if the explorer used the key press to perform an action, NO otherwise
|
||||
- (BOOL)handleDownArrowKeyPressed;
|
||||
/// @return YES if the explorer used the key press to perform an action, NO otherwise
|
||||
- (BOOL)handleUpArrowKeyPressed;
|
||||
/// @return YES if the explorer used the key press to perform an action, NO otherwise
|
||||
- (BOOL)handleRightArrowKeyPressed;
|
||||
/// @return YES if the explorer used the key press to perform an action, NO otherwise
|
||||
- (BOOL)handleLeftArrowKeyPressed;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
@protocol FLEXExplorerViewControllerDelegate <NSObject>
|
||||
- (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController;
|
||||
@end
|
||||
@@ -0,0 +1,996 @@
|
||||
//
|
||||
// FLEXExplorerViewController.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXExplorerToolbarItem.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXWindow.h"
|
||||
#import "FLEXTabList.h"
|
||||
#import "FLEXNavigationController.h"
|
||||
#import "FLEXHierarchyViewController.h"
|
||||
#import "FLEXGlobalsViewController.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXNetworkMITMViewController.h"
|
||||
#import "FLEXTabsViewController.h"
|
||||
#import "FLEXWindowManagerController.h"
|
||||
#import "FLEXViewControllersViewController.h"
|
||||
#import "NSUserDefaults+FLEX.h"
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
FLEXExplorerModeDefault,
|
||||
FLEXExplorerModeSelect,
|
||||
FLEXExplorerModeMove
|
||||
};
|
||||
|
||||
@interface FLEXExplorerViewController () <FLEXHierarchyDelegate, UIAdaptivePresentationControllerDelegate>
|
||||
|
||||
/// Tracks the currently active tool/mode
|
||||
@property (nonatomic) FLEXExplorerMode currentMode;
|
||||
|
||||
/// Gesture recognizer for dragging a view in move mode
|
||||
@property (nonatomic) UIPanGestureRecognizer *movePanGR;
|
||||
|
||||
/// Gesture recognizer for showing additional details on the selected view
|
||||
@property (nonatomic) UITapGestureRecognizer *detailsTapGR;
|
||||
|
||||
/// Only valid while a move pan gesture is in progress.
|
||||
@property (nonatomic) CGRect selectedViewFrameBeforeDragging;
|
||||
|
||||
/// Only valid while a toolbar drag pan gesture is in progress.
|
||||
@property (nonatomic) CGRect toolbarFrameBeforeDragging;
|
||||
|
||||
/// Borders of all the visible views in the hierarchy at the selection point.
|
||||
/// 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) NSArray<UIView *> *viewsAtTapPoint;
|
||||
|
||||
/// The view that we're currently highlighting with an overlay and displaying details for.
|
||||
@property (nonatomic) UIView *selectedView;
|
||||
|
||||
/// A colored transparent overlay to indicate that the view is selected.
|
||||
@property (nonatomic) UIView *selectedViewOverlay;
|
||||
|
||||
/// self.view.window as a \c FLEXWindow
|
||||
@property (nonatomic, readonly) FLEXWindow *window;
|
||||
|
||||
/// All views that we're KVOing. Used to help us clean up properly.
|
||||
@property (nonatomic) NSMutableSet<UIView *> *observedViews;
|
||||
|
||||
/// Used to preserve the target app's UIMenuController items.
|
||||
@property (nonatomic) NSArray<UIMenuItem *> *appMenuItems;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXExplorerViewController
|
||||
|
||||
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
|
||||
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
|
||||
if (self) {
|
||||
self.observedViews = [NSMutableSet new];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
for (UIView *view in _observedViews) {
|
||||
[self stopObservingView:view];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
// Toolbar
|
||||
_explorerToolbar = [FLEXExplorerToolbar new];
|
||||
|
||||
// Start the toolbar off below any bars that may be at the top of the view.
|
||||
CGFloat toolbarOriginY = NSUserDefaults.standardUserDefaults.flex_toolbarTopMargin;
|
||||
|
||||
CGRect safeArea = [self viewSafeArea];
|
||||
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:CGSizeMake(
|
||||
CGRectGetWidth(self.view.bounds), CGRectGetHeight(safeArea)
|
||||
)];
|
||||
[self updateToolbarPositionWithUnconstrainedFrame:CGRectMake(
|
||||
CGRectGetMinX(safeArea), toolbarOriginY, toolbarSize.width, toolbarSize.height
|
||||
)];
|
||||
self.explorerToolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth |
|
||||
UIViewAutoresizingFlexibleBottomMargin |
|
||||
UIViewAutoresizingFlexibleTopMargin;
|
||||
[self.view addSubview:self.explorerToolbar];
|
||||
[self setupToolbarActions];
|
||||
[self setupToolbarGestures];
|
||||
|
||||
// View selection
|
||||
UITapGestureRecognizer *selectionTapGR = [[UITapGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleSelectionTap:)
|
||||
];
|
||||
[self.view addGestureRecognizer:selectionTapGR];
|
||||
|
||||
// View moving
|
||||
self.movePanGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleMovePan:)];
|
||||
self.movePanGR.enabled = self.currentMode == FLEXExplorerModeMove;
|
||||
[self.view addGestureRecognizer:self.movePanGR];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
[self updateButtonStates];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Rotation
|
||||
|
||||
- (UIViewController *)viewControllerForRotationAndOrientation {
|
||||
UIViewController *viewController = FLEXUtility.appKeyWindow.rootViewController;
|
||||
// Obfuscating selector _viewControllerForSupportedInterfaceOrientations
|
||||
NSString *viewControllerSelectorString = [@[
|
||||
@"_vie", @"wContro", @"llerFor", @"Supported", @"Interface", @"Orientations"
|
||||
] componentsJoinedByString:@""];
|
||||
SEL viewControllerSelector = NSSelectorFromString(viewControllerSelectorString);
|
||||
if ([viewController respondsToSelector:viewControllerSelector]) {
|
||||
viewController = [viewController valueForKey:viewControllerSelectorString];
|
||||
}
|
||||
|
||||
return viewController;
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
|
||||
// Commenting this out until I can figure out a better way to solve this
|
||||
// if (self.window.isKeyWindow) {
|
||||
// [self.window resignKeyWindow];
|
||||
// }
|
||||
|
||||
UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
|
||||
UIInterfaceOrientationMask supportedOrientations = FLEXUtility.infoPlistSupportedInterfaceOrientationsMask;
|
||||
if (viewControllerToAsk && ![viewControllerToAsk isKindOfClass:[self class]]) {
|
||||
supportedOrientations = [viewControllerToAsk supportedInterfaceOrientations];
|
||||
}
|
||||
|
||||
// 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 (supportedOrientations == 0) {
|
||||
supportedOrientations = UIInterfaceOrientationMaskAll;
|
||||
}
|
||||
|
||||
return supportedOrientations;
|
||||
}
|
||||
|
||||
- (BOOL)shouldAutorotate {
|
||||
UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
|
||||
BOOL shouldAutorotate = YES;
|
||||
if (viewControllerToAsk && viewControllerToAsk != self) {
|
||||
shouldAutorotate = [viewControllerToAsk shouldAutorotate];
|
||||
}
|
||||
return shouldAutorotate;
|
||||
}
|
||||
|
||||
- (void)viewWillTransitionToSize:(CGSize)size
|
||||
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
|
||||
[super viewWillTransitionToSize:size withTransitionCoordinator: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;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.selectedView) {
|
||||
self.selectedViewOverlay.frame = [self frameInLocalCoordinatesForView:self.selectedView];
|
||||
self.selectedViewOverlay.hidden = NO;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Setter Overrides
|
||||
|
||||
- (void)setSelectedView:(UIView *)selectedView {
|
||||
if (![_selectedView isEqual:selectedView]) {
|
||||
if (![self.viewsAtTapPoint containsObject:_selectedView]) {
|
||||
[self stopObservingView:_selectedView];
|
||||
}
|
||||
|
||||
_selectedView = selectedView;
|
||||
|
||||
[self beginObservingView:selectedView];
|
||||
|
||||
// Update the toolbar and selected overlay
|
||||
self.explorerToolbar.selectedViewDescription = [FLEXUtility
|
||||
descriptionForView:selectedView includingFrame:YES
|
||||
];
|
||||
self.explorerToolbar.selectedViewOverlayColor = [FLEXUtility
|
||||
consistentRandomColorForObject:selectedView
|
||||
];
|
||||
|
||||
if (selectedView) {
|
||||
if (!self.selectedViewOverlay) {
|
||||
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.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.
|
||||
[self.view bringSubviewToFront:self.selectedViewOverlay];
|
||||
[self.view bringSubviewToFront:self.explorerToolbar];
|
||||
} else {
|
||||
[self.selectedViewOverlay removeFromSuperview];
|
||||
self.selectedViewOverlay = nil;
|
||||
}
|
||||
|
||||
// Some of the button states depend on whether we have a selected view.
|
||||
[self updateButtonStates];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setViewsAtTapPoint:(NSArray<UIView *> *)viewsAtTapPoint {
|
||||
if (![_viewsAtTapPoint isEqual:viewsAtTapPoint]) {
|
||||
for (UIView *view in _viewsAtTapPoint) {
|
||||
if (view != self.selectedView) {
|
||||
[self stopObservingView:view];
|
||||
}
|
||||
}
|
||||
|
||||
_viewsAtTapPoint = viewsAtTapPoint;
|
||||
|
||||
for (UIView *view in viewsAtTapPoint) {
|
||||
[self beginObservingView:view];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setCurrentMode:(FLEXExplorerMode)currentMode {
|
||||
if (_currentMode != currentMode) {
|
||||
_currentMode = currentMode;
|
||||
switch (currentMode) {
|
||||
case FLEXExplorerModeDefault:
|
||||
[self removeAndClearOutlineViews];
|
||||
self.viewsAtTapPoint = nil;
|
||||
self.selectedView = nil;
|
||||
break;
|
||||
|
||||
case FLEXExplorerModeSelect:
|
||||
// Make sure the outline views are unhidden in case we came from the move mode.
|
||||
for (NSValue *key in self.outlineViewsForVisibleViews) {
|
||||
UIView *outlineView = self.outlineViewsForVisibleViews[key];
|
||||
outlineView.hidden = NO;
|
||||
}
|
||||
break;
|
||||
|
||||
case FLEXExplorerModeMove:
|
||||
// Hide all the outline views to focus on the selected view,
|
||||
// which is the only one that will move.
|
||||
for (NSValue *key in self.outlineViewsForVisibleViews) {
|
||||
UIView *outlineView = self.outlineViewsForVisibleViews[key];
|
||||
outlineView.hidden = YES;
|
||||
}
|
||||
break;
|
||||
}
|
||||
self.movePanGR.enabled = currentMode == FLEXExplorerModeMove;
|
||||
[self updateButtonStates];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - View Tracking
|
||||
|
||||
- (void)beginObservingView:(UIView *)view {
|
||||
// Bail if we're already observing this view or if there's nothing to observe.
|
||||
if (!view || [self.observedViews containsObject:view]) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (NSString *keyPath in self.viewKeyPathsToTrack) {
|
||||
[view addObserver:self forKeyPath:keyPath options:0 context:NULL];
|
||||
}
|
||||
|
||||
[self.observedViews addObject:view];
|
||||
}
|
||||
|
||||
- (void)stopObservingView:(UIView *)view {
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (NSString *keyPath in self.viewKeyPathsToTrack) {
|
||||
[view removeObserver:self forKeyPath:keyPath];
|
||||
}
|
||||
|
||||
[self.observedViews removeObject:view];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)viewKeyPathsToTrack {
|
||||
static NSArray<NSString *> *trackedViewKeyPaths = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
NSString *frameKeyPath = NSStringFromSelector(@selector(frame));
|
||||
trackedViewKeyPaths = @[frameKeyPath];
|
||||
});
|
||||
return trackedViewKeyPaths;
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
|
||||
change:(NSDictionary<NSString *, id> *)change
|
||||
context:(void *)context {
|
||||
[self updateOverlayAndDescriptionForObjectIfNeeded:object];
|
||||
}
|
||||
|
||||
- (void)updateOverlayAndDescriptionForObjectIfNeeded:(id)object {
|
||||
NSUInteger indexOfView = [self.viewsAtTapPoint indexOfObject:object];
|
||||
if (indexOfView != NSNotFound) {
|
||||
UIView *view = self.viewsAtTapPoint[indexOfView];
|
||||
NSValue *key = [NSValue valueWithNonretainedObject:view];
|
||||
UIView *outline = self.outlineViewsForVisibleViews[key];
|
||||
if (outline) {
|
||||
outline.frame = [self frameInLocalCoordinatesForView:view];
|
||||
}
|
||||
}
|
||||
if (object == self.selectedView) {
|
||||
// Update the selected view description since we show the frame value there.
|
||||
self.explorerToolbar.selectedViewDescription = [FLEXUtility
|
||||
descriptionForView:self.selectedView includingFrame:YES
|
||||
];
|
||||
CGRect selectedViewOutlineFrame = [self frameInLocalCoordinatesForView:self.selectedView];
|
||||
self.selectedViewOverlay.frame = selectedViewOutlineFrame;
|
||||
}
|
||||
}
|
||||
|
||||
- (CGRect)frameInLocalCoordinatesForView:(UIView *)view {
|
||||
// Convert to window coordinates since the view may be in a different window than our view
|
||||
CGRect frameInWindow = [view convertRect:view.bounds toView:nil];
|
||||
// Convert from the window to our view's coordinate space
|
||||
return [self.view convertRect:frameInWindow fromView:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Toolbar Buttons
|
||||
|
||||
- (void)setupToolbarActions {
|
||||
FLEXExplorerToolbar *toolbar = self.explorerToolbar;
|
||||
NSDictionary<NSString *, FLEXExplorerToolbarItem *> *actionsToItems = @{
|
||||
NSStringFromSelector(@selector(selectButtonTapped:)): toolbar.selectItem,
|
||||
NSStringFromSelector(@selector(hierarchyButtonTapped:)): toolbar.hierarchyItem,
|
||||
NSStringFromSelector(@selector(recentButtonTapped:)): toolbar.recentItem,
|
||||
NSStringFromSelector(@selector(moveButtonTapped:)): toolbar.moveItem,
|
||||
NSStringFromSelector(@selector(globalsButtonTapped:)): toolbar.globalsItem,
|
||||
NSStringFromSelector(@selector(closeButtonTapped:)): toolbar.closeItem,
|
||||
};
|
||||
|
||||
[actionsToItems enumerateKeysAndObjectsUsingBlock:^(NSString *sel, FLEXExplorerToolbarItem *item, BOOL *stop) {
|
||||
[item addTarget:self action:NSSelectorFromString(sel) forControlEvents:UIControlEventTouchUpInside];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)selectButtonTapped:(FLEXExplorerToolbarItem *)sender {
|
||||
[self toggleSelectTool];
|
||||
}
|
||||
|
||||
- (void)hierarchyButtonTapped:(FLEXExplorerToolbarItem *)sender {
|
||||
[self toggleViewsTool];
|
||||
}
|
||||
|
||||
- (UIWindow *)statusWindow {
|
||||
NSString *statusBarString = [NSString stringWithFormat:@"%@arWindow", @"_statusB"];
|
||||
return [UIApplication.sharedApplication valueForKey:statusBarString];
|
||||
}
|
||||
|
||||
- (void)recentButtonTapped:(FLEXExplorerToolbarItem *)sender {
|
||||
NSAssert(FLEXTabList.sharedList.activeTab, @"Must have active tab");
|
||||
[self presentViewController:FLEXTabList.sharedList.activeTab animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)moveButtonTapped:(FLEXExplorerToolbarItem *)sender {
|
||||
[self toggleMoveTool];
|
||||
}
|
||||
|
||||
- (void)globalsButtonTapped:(FLEXExplorerToolbarItem *)sender {
|
||||
[self toggleMenuTool];
|
||||
}
|
||||
|
||||
- (void)closeButtonTapped:(FLEXExplorerToolbarItem *)sender {
|
||||
self.currentMode = FLEXExplorerModeDefault;
|
||||
[self.delegate explorerViewControllerDidFinish:self];
|
||||
}
|
||||
|
||||
- (void)updateButtonStates {
|
||||
FLEXExplorerToolbar *toolbar = self.explorerToolbar;
|
||||
|
||||
toolbar.selectItem.selected = self.currentMode == FLEXExplorerModeSelect;
|
||||
|
||||
// Move only enabled when an object is selected.
|
||||
BOOL hasSelectedObject = self.selectedView != nil;
|
||||
toolbar.moveItem.enabled = hasSelectedObject;
|
||||
toolbar.moveItem.selected = self.currentMode == FLEXExplorerModeMove;
|
||||
|
||||
// Recent only enabled when we have a last active tab
|
||||
toolbar.recentItem.enabled = FLEXTabList.sharedList.activeTab != nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Toolbar Dragging
|
||||
|
||||
- (void)setupToolbarGestures {
|
||||
FLEXExplorerToolbar *toolbar = self.explorerToolbar;
|
||||
|
||||
// Pan gesture for dragging.
|
||||
[toolbar.dragHandle addGestureRecognizer:[[UIPanGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleToolbarPanGesture:)
|
||||
]];
|
||||
|
||||
// Tap gesture for hinting.
|
||||
[toolbar.dragHandle addGestureRecognizer:[[UITapGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleToolbarHintTapGesture:)
|
||||
]];
|
||||
|
||||
// Tap gesture for showing additional details
|
||||
self.detailsTapGR = [[UITapGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleToolbarDetailsTapGesture:)
|
||||
];
|
||||
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:self.detailsTapGR];
|
||||
// Swipe gestures for selecting deeper / higher views at a point
|
||||
UISwipeGestureRecognizer *leftSwipe = [[UISwipeGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleChangeViewAtPointGesture:)
|
||||
];
|
||||
UISwipeGestureRecognizer *rightSwipe = [[UISwipeGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleChangeViewAtPointGesture:)
|
||||
];
|
||||
leftSwipe.direction = UISwipeGestureRecognizerDirectionLeft;
|
||||
rightSwipe.direction = UISwipeGestureRecognizerDirectionRight;
|
||||
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:leftSwipe];
|
||||
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:rightSwipe];
|
||||
|
||||
// Long press gesture to present tabs manager
|
||||
[toolbar.globalsItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleToolbarShowTabsGesture:)
|
||||
]];
|
||||
|
||||
// Long press gesture to present window manager
|
||||
[toolbar.selectItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleToolbarWindowManagerGesture:)
|
||||
]];
|
||||
|
||||
// Long press gesture to present view controllers at tap
|
||||
[toolbar.hierarchyItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleToolbarShowViewControllersGesture:)
|
||||
]];
|
||||
}
|
||||
|
||||
- (void)handleToolbarPanGesture:(UIPanGestureRecognizer *)panGR {
|
||||
switch (panGR.state) {
|
||||
case UIGestureRecognizerStateBegan:
|
||||
self.toolbarFrameBeforeDragging = self.explorerToolbar.frame;
|
||||
[self updateToolbarPositionWithDragGesture:panGR];
|
||||
break;
|
||||
|
||||
case UIGestureRecognizerStateChanged:
|
||||
case UIGestureRecognizerStateEnded:
|
||||
[self updateToolbarPositionWithDragGesture:panGR];
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateToolbarPositionWithDragGesture:(UIPanGestureRecognizer *)panGR {
|
||||
CGPoint translation = [panGR translationInView:self.view];
|
||||
CGRect newToolbarFrame = self.toolbarFrameBeforeDragging;
|
||||
newToolbarFrame.origin.y += translation.y;
|
||||
|
||||
[self updateToolbarPositionWithUnconstrainedFrame:newToolbarFrame];
|
||||
}
|
||||
|
||||
- (void)updateToolbarPositionWithUnconstrainedFrame:(CGRect)unconstrainedFrame {
|
||||
CGRect safeArea = [self viewSafeArea];
|
||||
// We only constrain the Y-axis because we want the toolbar
|
||||
// to handle the X-axis safeArea layout by itself
|
||||
CGFloat minY = CGRectGetMinY(safeArea);
|
||||
CGFloat maxY = CGRectGetMaxY(safeArea) - unconstrainedFrame.size.height;
|
||||
if (unconstrainedFrame.origin.y < minY) {
|
||||
unconstrainedFrame.origin.y = minY;
|
||||
} else if (unconstrainedFrame.origin.y > maxY) {
|
||||
unconstrainedFrame.origin.y = maxY;
|
||||
}
|
||||
|
||||
self.explorerToolbar.frame = unconstrainedFrame;
|
||||
NSUserDefaults.standardUserDefaults.flex_toolbarTopMargin = unconstrainedFrame.origin.y;
|
||||
}
|
||||
|
||||
- (void)handleToolbarHintTapGesture:(UITapGestureRecognizer *)tapGR {
|
||||
// Bounce the toolbar to indicate that it is draggable.
|
||||
// TODO: make it bouncier.
|
||||
if (tapGR.state == UIGestureRecognizerStateRecognized) {
|
||||
CGRect originalToolbarFrame = self.explorerToolbar.frame;
|
||||
const NSTimeInterval kHalfwayDuration = 0.2;
|
||||
const CGFloat kVerticalOffset = 30.0;
|
||||
[UIView animateWithDuration:kHalfwayDuration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
|
||||
CGRect newToolbarFrame = self.explorerToolbar.frame;
|
||||
newToolbarFrame.origin.y += kVerticalOffset;
|
||||
self.explorerToolbar.frame = newToolbarFrame;
|
||||
} completion:^(BOOL finished) {
|
||||
[UIView animateWithDuration:kHalfwayDuration delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
|
||||
self.explorerToolbar.frame = originalToolbarFrame;
|
||||
} completion:nil];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleToolbarDetailsTapGesture:(UITapGestureRecognizer *)tapGR {
|
||||
if (tapGR.state == UIGestureRecognizerStateRecognized && self.selectedView) {
|
||||
UIViewController *topStackVC = [FLEXObjectExplorerFactory explorerViewControllerForObject:self.selectedView];
|
||||
[self presentViewController:
|
||||
[FLEXNavigationController withRootViewController:topStackVC]
|
||||
animated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleToolbarShowTabsGesture:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state == UIGestureRecognizerStateBegan) {
|
||||
// Back up the UIMenuController items since dismissViewController: will attempt to replace them
|
||||
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
|
||||
|
||||
// Don't use FLEXNavigationController because the tab viewer itself is not a tab
|
||||
[super presentViewController:[[UINavigationController alloc]
|
||||
initWithRootViewController:[FLEXTabsViewController new]
|
||||
] animated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleToolbarWindowManagerGesture:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state == UIGestureRecognizerStateBegan) {
|
||||
// Back up the UIMenuController items since dismissViewController: will attempt to replace them
|
||||
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
|
||||
|
||||
[super presentViewController:[FLEXNavigationController
|
||||
withRootViewController:[FLEXWindowManagerController new]
|
||||
] animated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleToolbarShowViewControllersGesture:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state == UIGestureRecognizerStateBegan && self.viewsAtTapPoint.count) {
|
||||
// Back up the UIMenuController items since dismissViewController: will attempt to replace them
|
||||
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
|
||||
|
||||
UIViewController *list = [FLEXViewControllersViewController
|
||||
controllersForViews:self.viewsAtTapPoint
|
||||
];
|
||||
[self presentViewController:
|
||||
[FLEXNavigationController withRootViewController:list
|
||||
] animated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - View Selection
|
||||
|
||||
- (void)handleSelectionTap:(UITapGestureRecognizer *)tapGR {
|
||||
// Only if we're in selection mode
|
||||
if (self.currentMode == FLEXExplorerModeSelect && tapGR.state == UIGestureRecognizerStateRecognized) {
|
||||
// Note that [tapGR locationInView:nil] is broken in iOS 8,
|
||||
// so we have to do a two step conversion to window coordinates.
|
||||
// Thanks to @lascorbe for finding this: https://github.com/Flipboard/FLEX/pull/31
|
||||
CGPoint tapPointInView = [tapGR locationInView:self.view];
|
||||
CGPoint tapPointInWindow = [self.view convertPoint:tapPointInView toView:nil];
|
||||
[self updateOutlineViewsForSelectionPoint:tapPointInWindow];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleChangeViewAtPointGesture:(UISwipeGestureRecognizer *)sender {
|
||||
NSInteger max = self.viewsAtTapPoint.count - 1;
|
||||
NSInteger currentIdx = [self.viewsAtTapPoint indexOfObject:self.selectedView];
|
||||
switch (sender.direction) {
|
||||
case UISwipeGestureRecognizerDirectionLeft:
|
||||
self.selectedView = self.viewsAtTapPoint[MIN(max, currentIdx + 1)];
|
||||
break;
|
||||
case UISwipeGestureRecognizerDirectionRight:
|
||||
self.selectedView = self.viewsAtTapPoint[MAX(0, currentIdx - 1)];
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateOutlineViewsForSelectionPoint:(CGPoint)selectionPointInWindow {
|
||||
[self removeAndClearOutlineViews];
|
||||
|
||||
// Include hidden views in the "viewsAtTapPoint" array so we can show them in the hierarchy list.
|
||||
self.viewsAtTapPoint = [self viewsAtPoint:selectionPointInWindow skipHiddenViews:NO];
|
||||
|
||||
// For outlined views and the selected view, only use visible views.
|
||||
// Outlining hidden views adds clutter and makes the selection behavior confusing.
|
||||
NSArray<UIView *> *visibleViewsAtTapPoint = [self viewsAtPoint:selectionPointInWindow skipHiddenViews:YES];
|
||||
NSMutableDictionary<NSValue *, UIView *> *newOutlineViewsForVisibleViews = [NSMutableDictionary new];
|
||||
for (UIView *view in visibleViewsAtTapPoint) {
|
||||
UIView *outlineView = [self outlineViewForView:view];
|
||||
[self.view addSubview:outlineView];
|
||||
NSValue *key = [NSValue valueWithNonretainedObject:view];
|
||||
[newOutlineViewsForVisibleViews setObject:outlineView forKey:key];
|
||||
}
|
||||
self.outlineViewsForVisibleViews = newOutlineViewsForVisibleViews;
|
||||
self.selectedView = [self viewForSelectionAtPoint:selectionPointInWindow];
|
||||
|
||||
// Make sure the explorer toolbar doesn't end up behind the newly added outline views.
|
||||
[self.view bringSubviewToFront:self.explorerToolbar];
|
||||
|
||||
[self updateButtonStates];
|
||||
}
|
||||
|
||||
- (UIView *)outlineViewForView:(UIView *)view {
|
||||
CGRect outlineFrame = [self frameInLocalCoordinatesForView:view];
|
||||
UIView *outlineView = [[UIView alloc] initWithFrame:outlineFrame];
|
||||
outlineView.backgroundColor = UIColor.clearColor;
|
||||
outlineView.layer.borderColor = [FLEXUtility consistentRandomColorForObject:view].CGColor;
|
||||
outlineView.layer.borderWidth = 1.0;
|
||||
return outlineView;
|
||||
}
|
||||
|
||||
- (void)removeAndClearOutlineViews {
|
||||
for (NSValue *key in self.outlineViewsForVisibleViews) {
|
||||
UIView *outlineView = self.outlineViewsForVisibleViews[key];
|
||||
[outlineView removeFromSuperview];
|
||||
}
|
||||
self.outlineViewsForVisibleViews = nil;
|
||||
}
|
||||
|
||||
- (NSArray<UIView *> *)viewsAtPoint:(CGPoint)tapPointInWindow skipHiddenViews:(BOOL)skipHidden {
|
||||
NSMutableArray<UIView *> *views = [NSMutableArray new];
|
||||
for (UIWindow *window in FLEXUtility.allWindows) {
|
||||
// Don't include the explorer's own window or subviews.
|
||||
if (window != self.view.window && [window pointInside:tapPointInWindow withEvent:nil]) {
|
||||
[views addObject:window];
|
||||
[views addObjectsFromArray:[self
|
||||
recursiveSubviewsAtPoint:tapPointInWindow inView:window skipHiddenViews:skipHidden
|
||||
]];
|
||||
}
|
||||
}
|
||||
return views;
|
||||
}
|
||||
|
||||
- (UIView *)viewForSelectionAtPoint:(CGPoint)tapPointInWindow {
|
||||
// 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) {
|
||||
// Ignore the explorer's own window.
|
||||
if (window != self.view.window) {
|
||||
if ([window hitTest:tapPointInWindow withEvent:nil]) {
|
||||
windowForSelection = window;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
- (NSArray<UIView *> *)recursiveSubviewsAtPoint:(CGPoint)pointInView
|
||||
inView:(UIView *)view
|
||||
skipHiddenViews:(BOOL)skipHidden {
|
||||
NSMutableArray<UIView *> *subviewsAtPoint = [NSMutableArray new];
|
||||
for (UIView *subview in view.subviews) {
|
||||
BOOL isHidden = subview.hidden || subview.alpha < 0.01;
|
||||
if (skipHidden && isHidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
BOOL subviewContainsPoint = CGRectContainsPoint(subview.frame, pointInView);
|
||||
if (subviewContainsPoint) {
|
||||
[subviewsAtPoint addObject:subview];
|
||||
}
|
||||
|
||||
// If this view doesn't clip to its bounds, we need to check its subviews even if it
|
||||
// doesn't contain the selection point. They may be visible and contain the selection point.
|
||||
if (subviewContainsPoint || !subview.clipsToBounds) {
|
||||
CGPoint pointInSubview = [view convertPoint:pointInView toView:subview];
|
||||
[subviewsAtPoint addObjectsFromArray:[self
|
||||
recursiveSubviewsAtPoint:pointInSubview inView:subview skipHiddenViews:skipHidden
|
||||
]];
|
||||
}
|
||||
}
|
||||
return subviewsAtPoint;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Selected View Moving
|
||||
|
||||
- (void)handleMovePan:(UIPanGestureRecognizer *)movePanGR {
|
||||
switch (movePanGR.state) {
|
||||
case UIGestureRecognizerStateBegan:
|
||||
self.selectedViewFrameBeforeDragging = self.selectedView.frame;
|
||||
[self updateSelectedViewPositionWithDragGesture:movePanGR];
|
||||
break;
|
||||
|
||||
case UIGestureRecognizerStateChanged:
|
||||
case UIGestureRecognizerStateEnded:
|
||||
[self updateSelectedViewPositionWithDragGesture:movePanGR];
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateSelectedViewPositionWithDragGesture:(UIPanGestureRecognizer *)movePanGR {
|
||||
CGPoint translation = [movePanGR translationInView:self.selectedView.superview];
|
||||
CGRect newSelectedViewFrame = self.selectedViewFrameBeforeDragging;
|
||||
newSelectedViewFrame.origin.x = FLEXFloor(newSelectedViewFrame.origin.x + translation.x);
|
||||
newSelectedViewFrame.origin.y = FLEXFloor(newSelectedViewFrame.origin.y + translation.y);
|
||||
self.selectedView.frame = newSelectedViewFrame;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Safe Area Handling
|
||||
|
||||
- (CGRect)viewSafeArea {
|
||||
CGRect safeArea = self.view.bounds;
|
||||
if (@available(iOS 11.0, *)) {
|
||||
safeArea = UIEdgeInsetsInsetRect(self.view.bounds, self.view.safeAreaInsets);
|
||||
}
|
||||
|
||||
return safeArea;
|
||||
}
|
||||
|
||||
- (void)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)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Touch Handling
|
||||
|
||||
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates {
|
||||
BOOL shouldReceiveTouch = NO;
|
||||
|
||||
CGPoint pointInLocalCoordinates = [self.view convertPoint:pointInWindowCoordinates fromView:nil];
|
||||
|
||||
// Always if it's on the toolbar
|
||||
if (CGRectContainsPoint(self.explorerToolbar.frame, pointInLocalCoordinates)) {
|
||||
shouldReceiveTouch = YES;
|
||||
}
|
||||
|
||||
// Always if we're in selection mode
|
||||
if (!shouldReceiveTouch && self.currentMode == FLEXExplorerModeSelect) {
|
||||
shouldReceiveTouch = YES;
|
||||
}
|
||||
|
||||
// Always in move mode too
|
||||
if (!shouldReceiveTouch && self.currentMode == FLEXExplorerModeMove) {
|
||||
shouldReceiveTouch = YES;
|
||||
}
|
||||
|
||||
// Always if we have a modal presented
|
||||
if (!shouldReceiveTouch && self.presentedViewController) {
|
||||
shouldReceiveTouch = YES;
|
||||
}
|
||||
|
||||
return shouldReceiveTouch;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - FLEXHierarchyDelegate
|
||||
|
||||
- (void)viewHierarchyDidDismiss:(UIView *)selectedView {
|
||||
// Note that we need to wait until the view controller is dismissed to calculate the frame
|
||||
// of the outline view, otherwise the coordinate conversion doesn't give the correct result.
|
||||
[self toggleViewsToolWithCompletion:^{
|
||||
// If the selected view is outside of the tap point array (selected from "Full Hierarchy"),
|
||||
// then clear out the tap point array and remove all the outline views.
|
||||
if (![self.viewsAtTapPoint containsObject:selectedView]) {
|
||||
self.viewsAtTapPoint = nil;
|
||||
[self removeAndClearOutlineViews];
|
||||
}
|
||||
|
||||
// If we now have a selected view and we didn't have one previously, go to "select" mode.
|
||||
if (self.currentMode == FLEXExplorerModeDefault && selectedView) {
|
||||
self.currentMode = FLEXExplorerModeSelect;
|
||||
}
|
||||
|
||||
// The selected view setter will also update the selected view overlay appropriately.
|
||||
self.selectedView = selectedView;
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Modal Presentation and Window Management
|
||||
|
||||
- (void)presentViewController:(UIViewController *)toPresent
|
||||
animated:(BOOL)animated
|
||||
completion:(void (^)(void))completion {
|
||||
// Make our window key to correctly handle input.
|
||||
[self.view.window makeKeyWindow];
|
||||
|
||||
// Move the status bar on top of FLEX so we can get scroll to top behavior for taps.
|
||||
if (!@available(iOS 13, *)) {
|
||||
[self statusWindow].windowLevel = self.view.window.windowLevel + 1.0;
|
||||
}
|
||||
|
||||
// Back up and replace the UIMenuController items
|
||||
// Edit: no longer replacing the items, but still backing them
|
||||
// up in case we start replacing them again in the future
|
||||
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
|
||||
|
||||
// Show the view controller
|
||||
[super presentViewController:toPresent animated:animated completion:completion];
|
||||
}
|
||||
|
||||
- (void)dismissViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion {
|
||||
UIWindow *appWindow = self.window.previousKeyWindow;
|
||||
[appWindow makeKeyWindow];
|
||||
[appWindow.rootViewController setNeedsStatusBarAppearanceUpdate];
|
||||
|
||||
// Restore previous UIMenuController items
|
||||
// Back up and replace the UIMenuController items
|
||||
UIMenuController.sharedMenuController.menuItems = self.appMenuItems;
|
||||
[UIMenuController.sharedMenuController update];
|
||||
self.appMenuItems = nil;
|
||||
|
||||
// Restore the status bar window's normal window level.
|
||||
// We want it above FLEX while a modal is presented for
|
||||
// scroll to top, but below FLEX otherwise for exploration.
|
||||
[self statusWindow].windowLevel = UIWindowLevelStatusBar;
|
||||
|
||||
[self updateButtonStates];
|
||||
|
||||
[super dismissViewControllerAnimated:animated completion:completion];
|
||||
}
|
||||
|
||||
- (BOOL)wantsWindowToBecomeKey
|
||||
{
|
||||
return self.window.previousKeyWindow != nil;
|
||||
}
|
||||
|
||||
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future
|
||||
completion:(void(^)(void))completion {
|
||||
if (self.presentedViewController) {
|
||||
[self dismissViewControllerAnimated:YES completion:completion];
|
||||
} else if (future) {
|
||||
[self presentViewController:future() animated:YES completion:completion];
|
||||
}
|
||||
}
|
||||
|
||||
- (FLEXWindow *)window {
|
||||
return (id)self.view.window;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Keyboard Shortcut Helpers
|
||||
|
||||
- (void)toggleSelectTool {
|
||||
if (self.currentMode == FLEXExplorerModeSelect) {
|
||||
self.currentMode = FLEXExplorerModeDefault;
|
||||
} else {
|
||||
self.currentMode = FLEXExplorerModeSelect;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)toggleMoveTool {
|
||||
if (self.currentMode == FLEXExplorerModeMove) {
|
||||
self.currentMode = FLEXExplorerModeSelect;
|
||||
} else if (self.currentMode == FLEXExplorerModeSelect && self.selectedView) {
|
||||
self.currentMode = FLEXExplorerModeMove;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)toggleViewsTool {
|
||||
[self toggleViewsToolWithCompletion:nil];
|
||||
}
|
||||
|
||||
- (void)toggleViewsToolWithCompletion:(void(^)(void))completion {
|
||||
[self toggleToolWithViewControllerProvider:^UINavigationController *{
|
||||
if (self.selectedView) {
|
||||
return [FLEXHierarchyViewController
|
||||
delegate:self
|
||||
viewsAtTap:self.viewsAtTapPoint
|
||||
selectedView:self.selectedView
|
||||
];
|
||||
} else {
|
||||
return [FLEXHierarchyViewController delegate:self];
|
||||
}
|
||||
} completion:^{
|
||||
if (completion) {
|
||||
completion();
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)toggleMenuTool {
|
||||
[self toggleToolWithViewControllerProvider:^UINavigationController *{
|
||||
return [FLEXNavigationController withRootViewController:[FLEXGlobalsViewController new]];
|
||||
} completion:nil];
|
||||
}
|
||||
|
||||
- (BOOL)handleDownArrowKeyPressed {
|
||||
if (self.currentMode == FLEXExplorerModeMove) {
|
||||
CGRect frame = self.selectedView.frame;
|
||||
frame.origin.y += 1.0 / UIScreen.mainScreen.scale;
|
||||
self.selectedView.frame = frame;
|
||||
} 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];
|
||||
}
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)handleUpArrowKeyPressed {
|
||||
if (self.currentMode == FLEXExplorerModeMove) {
|
||||
CGRect frame = self.selectedView.frame;
|
||||
frame.origin.y -= 1.0 / UIScreen.mainScreen.scale;
|
||||
self.selectedView.frame = frame;
|
||||
} else if (self.currentMode == FLEXExplorerModeSelect && self.viewsAtTapPoint.count > 0) {
|
||||
NSInteger selectedViewIndex = [self.viewsAtTapPoint indexOfObject:self.selectedView];
|
||||
if (selectedViewIndex < self.viewsAtTapPoint.count - 1) {
|
||||
self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex + 1];
|
||||
}
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)handleRightArrowKeyPressed {
|
||||
if (self.currentMode == FLEXExplorerModeMove) {
|
||||
CGRect frame = self.selectedView.frame;
|
||||
frame.origin.x += 1.0 / UIScreen.mainScreen.scale;
|
||||
self.selectedView.frame = frame;
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)handleLeftArrowKeyPressed {
|
||||
if (self.currentMode == FLEXExplorerModeMove) {
|
||||
CGRect frame = self.selectedView.frame;
|
||||
frame.origin.x -= 1.0 / UIScreen.mainScreen.scale;
|
||||
self.selectedView.frame = frame;
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// FLEXViewControllersViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 2/13/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFilteringTableViewController.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXViewControllersViewController : FLEXFilteringTableViewController
|
||||
|
||||
+ (instancetype)controllersForViews:(NSArray<UIView *> *)views;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// FLEXViewControllersViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 2/13/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXViewControllersViewController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXMutableListSection.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXViewControllersViewController ()
|
||||
@property (nonatomic, readonly) FLEXMutableListSection *section;
|
||||
@property (nonatomic, readonly) NSArray<UIViewController *> *controllers;
|
||||
@end
|
||||
|
||||
@implementation FLEXViewControllersViewController
|
||||
@dynamic sections, allSections;
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (instancetype)controllersForViews:(NSArray<UIView *> *)views {
|
||||
return [[self alloc] initWithViews:views];
|
||||
}
|
||||
|
||||
- (id)initWithViews:(NSArray<UIView *> *)views {
|
||||
NSParameterAssert(views.count);
|
||||
|
||||
self = [self initWithStyle:UITableViewStylePlain];
|
||||
if (self) {
|
||||
_controllers = [views flex_mapped:^id(UIView *view, NSUInteger idx) {
|
||||
return [FLEXUtility viewControllerForView:view];
|
||||
}];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.title = @"View Controllers at Tap";
|
||||
self.showsSearchBar = YES;
|
||||
[self disableToolbar];
|
||||
}
|
||||
|
||||
- (NSArray<FLEXTableViewSection *> *)makeSections {
|
||||
_section = [FLEXMutableListSection list:self.controllers
|
||||
cellConfiguration:^(UITableViewCell *cell, UIViewController *controller, NSInteger row) {
|
||||
cell.textLabel.text = [NSString
|
||||
stringWithFormat:@"%@ — %p", NSStringFromClass(controller.class), controller
|
||||
];
|
||||
cell.detailTextLabel.text = controller.view.description;
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
} filterMatcher:^BOOL(NSString *filterText, UIViewController *controller) {
|
||||
return [NSStringFromClass(controller.class) localizedCaseInsensitiveContainsString:filterText];
|
||||
}];
|
||||
|
||||
self.section.selectionHandler = ^(UIViewController *host, UIViewController *controller) {
|
||||
[host.navigationController pushViewController:
|
||||
[FLEXObjectExplorerFactory explorerViewControllerForObject:controller]
|
||||
animated:YES];
|
||||
};
|
||||
|
||||
self.section.customTitle = @"View Controllers";
|
||||
return @[self.section];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)dismissAnimated {
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// FLEXWindow.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/13/14.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@protocol FLEXWindowEventDelegate <NSObject>
|
||||
|
||||
- (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow;
|
||||
- (BOOL)canBecomeKeyWindow;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
@interface FLEXWindow : UIWindow
|
||||
|
||||
@property (nonatomic, weak) id <FLEXWindowEventDelegate> eventDelegate;
|
||||
|
||||
/// Tracked so we can restore the key window after dismissing a modal.
|
||||
/// We need to become key after modal presentation so we can correctly capture input.
|
||||
/// If we're just showing the toolbar, we want the main app's window to remain key
|
||||
/// so that we don't interfere with input, status bar, etc.
|
||||
@property (nonatomic, readonly) UIWindow *previousKeyWindow;
|
||||
|
||||
@end
|
||||
@@ -3,19 +3,18 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/13/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXWindow.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@implementation FLEXWindow
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
- (id)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
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.
|
||||
@@ -25,8 +24,7 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
|
||||
{
|
||||
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
|
||||
BOOL pointInside = NO;
|
||||
if ([self.eventDelegate shouldHandleTouchAtPoint:point]) {
|
||||
pointInside = [super pointInside:point withEvent:event];
|
||||
@@ -34,22 +32,29 @@
|
||||
return pointInside;
|
||||
}
|
||||
|
||||
- (BOOL)shouldAffectStatusBarAppearance
|
||||
{
|
||||
- (BOOL)shouldAffectStatusBarAppearance {
|
||||
return [self isKeyWindow];
|
||||
}
|
||||
|
||||
- (BOOL)canBecomeKeyWindow
|
||||
{
|
||||
- (BOOL)canBecomeKeyWindow {
|
||||
return [self.eventDelegate canBecomeKeyWindow];
|
||||
}
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
- (void)makeKeyWindow {
|
||||
_previousKeyWindow = FLEXUtility.appKeyWindow;
|
||||
[super makeKeyWindow];
|
||||
}
|
||||
|
||||
- (void)resignKeyWindow {
|
||||
[super resignKeyWindow];
|
||||
_previousKeyWindow = nil;
|
||||
}
|
||||
|
||||
+ (void)initialize {
|
||||
// This adds a method (superclass override) at runtime which gives us the status bar behavior we want.
|
||||
// The FLEX window is intended to be an overlay that generally doesn't affect the app underneath.
|
||||
// Most of the time, we want the app's main window(s) to be in control of status bar behavior.
|
||||
// Done at runtime with an obfuscated selector because it is private API. But you 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));
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEXWindowManagerController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/6/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXWindowManagerController : FLEXTableViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,302 @@
|
||||
//
|
||||
// FLEXWindowManagerController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/6/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXWindowManagerController.h"
|
||||
#import "FLEXManager+Private.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
|
||||
@interface FLEXWindowManagerController ()
|
||||
@property (nonatomic) UIWindow *keyWindow;
|
||||
@property (nonatomic, copy) NSString *keyWindowSubtitle;
|
||||
@property (nonatomic, copy) NSArray<UIWindow *> *windows;
|
||||
@property (nonatomic, copy) NSArray<NSString *> *windowSubtitles;
|
||||
@property (nonatomic, copy) NSArray<UIScene *> *scenes API_AVAILABLE(ios(13));
|
||||
@property (nonatomic, copy) NSArray<NSString *> *sceneSubtitles;
|
||||
@property (nonatomic, copy) NSArray<NSArray *> *sections;
|
||||
@end
|
||||
|
||||
@implementation FLEXWindowManagerController
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
- (id)init {
|
||||
return [self initWithStyle:UITableViewStylePlain];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.title = @"Windows";
|
||||
if (@available(iOS 13, *)) {
|
||||
self.title = @"Windows and Scenes";
|
||||
}
|
||||
|
||||
[self disableToolbar];
|
||||
[self reloadData];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)reloadData {
|
||||
self.keyWindow = UIApplication.sharedApplication.keyWindow;
|
||||
self.windows = UIApplication.sharedApplication.windows;
|
||||
self.keyWindowSubtitle = self.windowSubtitles[[self.windows indexOfObject:self.keyWindow]];
|
||||
self.windowSubtitles = [self.windows flex_mapped:^id(UIWindow *window, NSUInteger idx) {
|
||||
return [NSString stringWithFormat:@"Level: %@ — Root: %@",
|
||||
@(window.windowLevel), window.rootViewController
|
||||
];
|
||||
}];
|
||||
|
||||
if (@available(iOS 13, *)) {
|
||||
self.scenes = UIApplication.sharedApplication.connectedScenes.allObjects;
|
||||
self.sceneSubtitles = [self.scenes flex_mapped:^id(UIScene *scene, NSUInteger idx) {
|
||||
return [self sceneDescription:scene];
|
||||
}];
|
||||
|
||||
self.sections = @[@[self.keyWindow], self.windows, self.scenes];
|
||||
} else {
|
||||
self.sections = @[@[self.keyWindow], self.windows];
|
||||
}
|
||||
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
- (void)dismissAnimated {
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)showRevertOrDismissAlert:(void(^)())revertBlock {
|
||||
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
|
||||
[self reloadData];
|
||||
[self.tableView reloadData];
|
||||
|
||||
UIWindow *highestWindow = UIApplication.sharedApplication.keyWindow;
|
||||
UIWindowLevel maxLevel = 0;
|
||||
for (UIWindow *window in UIApplication.sharedApplication.windows) {
|
||||
if (window.windowLevel > maxLevel) {
|
||||
maxLevel = window.windowLevel;
|
||||
highestWindow = window;
|
||||
}
|
||||
}
|
||||
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Keep Changes?");
|
||||
make.message(@"If you do not wish to keep these settings, choose 'Revert Changes' below.");
|
||||
|
||||
make.button(@"Keep Changes").destructiveStyle();
|
||||
make.button(@"Keep Changes and Dismiss").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
|
||||
[self dismissAnimated];
|
||||
});
|
||||
make.button(@"Revert Changes").cancelStyle().handler(^(NSArray<NSString *> *strings) {
|
||||
revertBlock();
|
||||
[self reloadData];
|
||||
[self.tableView reloadData];
|
||||
});
|
||||
} showFrom:[FLEXUtility topViewControllerInWindow:highestWindow]];
|
||||
}
|
||||
|
||||
- (NSString *)sceneDescription:(UIScene *)scene API_AVAILABLE(ios(13)) {
|
||||
NSString *state = [self stringFromSceneState:scene.activationState];
|
||||
NSString *title = scene.title.length ? scene.title : nil;
|
||||
NSString *suffix = nil;
|
||||
|
||||
if ([scene isKindOfClass:[UIWindowScene class]]) {
|
||||
UIWindowScene *windowScene = (id)scene;
|
||||
suffix = FLEXPluralString(windowScene.windows.count, @"windows", @"window");
|
||||
}
|
||||
|
||||
NSMutableString *description = state.mutableCopy;
|
||||
if (title) {
|
||||
[description appendFormat:@" — %@", title];
|
||||
}
|
||||
if (suffix) {
|
||||
[description appendFormat:@" — %@", suffix];
|
||||
}
|
||||
|
||||
return description.copy;
|
||||
}
|
||||
|
||||
- (NSString *)stringFromSceneState:(UISceneActivationState)state API_AVAILABLE(ios(13)) {
|
||||
switch (state) {
|
||||
case UISceneActivationStateUnattached:
|
||||
return @"Unattached";
|
||||
case UISceneActivationStateForegroundActive:
|
||||
return @"Active";
|
||||
case UISceneActivationStateForegroundInactive:
|
||||
return @"Inactive";
|
||||
case UISceneActivationStateBackground:
|
||||
return @"Backgrounded";
|
||||
}
|
||||
|
||||
return [NSString stringWithFormat:@"Unknown state: %@", @(state)];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Data Source
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
||||
return self.sections.count;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.sections[section].count;
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
switch (section) {
|
||||
case 0: return @"Key Window";
|
||||
case 1: return @"Windows";
|
||||
case 2: return @"Connected Scenes";
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath];
|
||||
cell.accessoryType = UITableViewCellAccessoryDetailButton;
|
||||
cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
|
||||
UIWindow *window = nil;
|
||||
NSString *subtitle = nil;
|
||||
|
||||
switch (indexPath.section) {
|
||||
case 0:
|
||||
window = self.keyWindow;
|
||||
subtitle = self.keyWindowSubtitle;
|
||||
break;
|
||||
case 1:
|
||||
window = self.windows[indexPath.row];
|
||||
subtitle = self.windowSubtitles[indexPath.row];
|
||||
break;
|
||||
case 2:
|
||||
if (@available(iOS 13, *)) {
|
||||
UIScene *scene = self.scenes[indexPath.row];
|
||||
cell.textLabel.text = scene.description;
|
||||
cell.detailTextLabel.text = self.sceneSubtitles[indexPath.row];
|
||||
return cell;
|
||||
}
|
||||
}
|
||||
|
||||
cell.textLabel.text = window.description;
|
||||
cell.detailTextLabel.text = [NSString
|
||||
stringWithFormat:@"Level: %@ — Root: %@",
|
||||
@((NSInteger)window.windowLevel), window.rootViewController.class
|
||||
];
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Delegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
UIWindow *window = nil;
|
||||
NSString *subtitle = nil;
|
||||
FLEXWindow *flex = FLEXManager.sharedManager.explorerWindow;
|
||||
|
||||
id cancelHandler = ^{
|
||||
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
|
||||
};
|
||||
|
||||
switch (indexPath.section) {
|
||||
case 0:
|
||||
window = self.keyWindow;
|
||||
subtitle = self.keyWindowSubtitle;
|
||||
break;
|
||||
case 1:
|
||||
window = self.windows[indexPath.row];
|
||||
subtitle = self.windowSubtitles[indexPath.row];
|
||||
break;
|
||||
case 2:
|
||||
if (@available(iOS 13, *)) {
|
||||
UIScene *scene = self.scenes[indexPath.row];
|
||||
UIWindowScene *oldScene = flex.windowScene;
|
||||
BOOL isWindowScene = [scene isKindOfClass:[UIWindowScene class]];
|
||||
BOOL isFLEXScene = isWindowScene ? flex.windowScene == scene : NO;
|
||||
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(NSStringFromClass(scene.class));
|
||||
|
||||
if (isWindowScene) {
|
||||
if (isFLEXScene) {
|
||||
make.message(@"Already the FLEX window scene");
|
||||
}
|
||||
|
||||
make.button(@"Set as FLEX Window Scene")
|
||||
.handler(^(NSArray<NSString *> *strings) {
|
||||
flex.windowScene = (id)scene;
|
||||
[self showRevertOrDismissAlert:^{
|
||||
flex.windowScene = oldScene;
|
||||
}];
|
||||
}).enabled(!isFLEXScene);
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} else {
|
||||
make.message(@"Not a UIWindowScene");
|
||||
make.button(@"Dismiss").cancelStyle().handler(cancelHandler);
|
||||
}
|
||||
} showFrom:self];
|
||||
}
|
||||
}
|
||||
|
||||
__block UIWindow *targetWindow = nil, *oldKeyWindow = nil;
|
||||
__block UIWindowLevel oldLevel;
|
||||
__block BOOL wasVisible;
|
||||
|
||||
subtitle = [subtitle stringByAppendingString:
|
||||
@"\n\n1) Adjust the FLEX window level relative to this window,\n"
|
||||
"2) adjust this window's level relative to the FLEX window,\n"
|
||||
"3) set this window's level to a specific value, or\n"
|
||||
"4) make this window the key window if it isn't already."
|
||||
];
|
||||
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(NSStringFromClass(window.class)).message(subtitle);
|
||||
make.button(@"Adjust FLEX Window Level").handler(^(NSArray<NSString *> *strings) {
|
||||
targetWindow = flex; oldLevel = flex.windowLevel;
|
||||
flex.windowLevel = window.windowLevel + strings.firstObject.integerValue;
|
||||
|
||||
[self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }];
|
||||
});
|
||||
make.button(@"Adjust This Window's Level").handler(^(NSArray<NSString *> *strings) {
|
||||
targetWindow = window; oldLevel = window.windowLevel;
|
||||
window.windowLevel = flex.windowLevel + strings.firstObject.integerValue;
|
||||
|
||||
[self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }];
|
||||
});
|
||||
make.button(@"Set This Window's Level").handler(^(NSArray<NSString *> *strings) {
|
||||
targetWindow = window; oldLevel = window.windowLevel;
|
||||
window.windowLevel = strings.firstObject.integerValue;
|
||||
|
||||
[self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }];
|
||||
});
|
||||
make.button(@"Make Key And Visible").handler(^(NSArray<NSString *> *strings) {
|
||||
oldKeyWindow = UIApplication.sharedApplication.keyWindow;
|
||||
wasVisible = window.hidden;
|
||||
[window makeKeyAndVisible];
|
||||
|
||||
[self showRevertOrDismissAlert:^{
|
||||
window.hidden = wasVisible;
|
||||
[oldKeyWindow makeKeyWindow];
|
||||
}];
|
||||
}).enabled(!window.isKeyWindow && !window.hidden);
|
||||
make.button(@"Cancel").cancelStyle().handler(cancelHandler);
|
||||
|
||||
make.textField(@"+/- window level, i.e. 5 or -10");
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)ip {
|
||||
[self.navigationController pushViewController:
|
||||
[FLEXObjectExplorerFactory explorerViewControllerForObject:self.sections[ip.section][ip.row]]
|
||||
animated:YES];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// FLEXTabList.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/1/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXTabList : NSObject
|
||||
|
||||
@property (nonatomic, readonly, class) FLEXTabList *sharedList;
|
||||
|
||||
@property (nonatomic, readonly, nullable) UINavigationController *activeTab;
|
||||
@property (nonatomic, readonly) NSArray<UINavigationController *> *openTabs;
|
||||
/// Snapshots of each tab when they were last active.
|
||||
@property (nonatomic, readonly) NSArray<UIImage *> *openTabSnapshots;
|
||||
/// \c NSNotFound if no tabs are present.
|
||||
/// Setting this property changes the active tab to one of the already open tabs.
|
||||
@property (nonatomic) NSInteger activeTabIndex;
|
||||
|
||||
/// Adds a new tab and sets the new tab as the active tab.
|
||||
- (void)addTab:(UINavigationController *)newTab;
|
||||
/// Closes the given tab. If this tab was the active tab,
|
||||
/// the most recent tab before that becomes the active tab.
|
||||
- (void)closeTab:(UINavigationController *)tab;
|
||||
/// Closes a tab at the given index. If this tab was the active tab,
|
||||
/// the most recent tab before that becomes the active tab.
|
||||
- (void)closeTabAtIndex:(NSInteger)idx;
|
||||
/// Closes all of the tabs at the given indexes. If the active tab
|
||||
/// is included, the most recent still-open tab becomes the active tab.
|
||||
- (void)closeTabsAtIndexes:(NSIndexSet *)indexes;
|
||||
/// A shortcut to close the active tab.
|
||||
- (void)closeActiveTab;
|
||||
/// A shortcut to close \e every tab.
|
||||
- (void)closeAllTabs;
|
||||
|
||||
- (void)updateSnapshotForActiveTab;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,133 @@
|
||||
//
|
||||
// FLEXTabList.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/1/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTabList.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXTabList () {
|
||||
NSMutableArray *_openTabs;
|
||||
NSMutableArray *_openTabSnapshots;
|
||||
}
|
||||
@end
|
||||
#pragma mark -
|
||||
@implementation FLEXTabList
|
||||
|
||||
#pragma mark Initialization
|
||||
|
||||
+ (FLEXTabList *)sharedList {
|
||||
static FLEXTabList *sharedList = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedList = [self new];
|
||||
});
|
||||
|
||||
return sharedList;
|
||||
}
|
||||
|
||||
- (id)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_openTabs = [NSMutableArray new];
|
||||
_openTabSnapshots = [NSMutableArray new];
|
||||
_activeTabIndex = NSNotFound;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Private
|
||||
|
||||
- (void)chooseNewActiveTab {
|
||||
if (self.openTabs.count) {
|
||||
self.activeTabIndex = self.openTabs.count - 1;
|
||||
} else {
|
||||
self.activeTabIndex = NSNotFound;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Public
|
||||
|
||||
- (void)setActiveTabIndex:(NSInteger)idx {
|
||||
NSParameterAssert(idx < self.openTabs.count || idx == NSNotFound);
|
||||
if (_activeTabIndex == idx) return;
|
||||
|
||||
_activeTabIndex = idx;
|
||||
_activeTab = (idx == NSNotFound) ? nil : self.openTabs[idx];
|
||||
}
|
||||
|
||||
- (void)addTab:(UINavigationController *)newTab {
|
||||
NSParameterAssert(newTab);
|
||||
|
||||
// Update snapshot of the last active tab
|
||||
if (self.activeTab) {
|
||||
[self updateSnapshotForActiveTab];
|
||||
}
|
||||
|
||||
// Add new tab and snapshot,
|
||||
// update active tab and index
|
||||
[_openTabs addObject:newTab];
|
||||
[_openTabSnapshots addObject:[FLEXUtility previewImageForView:newTab.view]];
|
||||
_activeTab = newTab;
|
||||
_activeTabIndex = self.openTabs.count - 1;
|
||||
}
|
||||
|
||||
- (void)closeTab:(UINavigationController *)tab {
|
||||
NSParameterAssert(tab);
|
||||
NSParameterAssert([self.openTabs containsObject:tab]);
|
||||
NSInteger idx = [self.openTabs indexOfObject:tab];
|
||||
|
||||
[self closeTabAtIndex:idx];
|
||||
}
|
||||
|
||||
- (void)closeTabAtIndex:(NSInteger)idx {
|
||||
NSParameterAssert(idx < self.openTabs.count);
|
||||
|
||||
// Remove old tab and snapshot
|
||||
[_openTabs removeObjectAtIndex:idx];
|
||||
[_openTabSnapshots removeObjectAtIndex:idx];
|
||||
|
||||
// Update active tab and index if needed
|
||||
if (self.activeTabIndex == idx) {
|
||||
[self chooseNewActiveTab];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)closeTabsAtIndexes:(NSIndexSet *)indexes {
|
||||
// Remove old tabs and snapshot
|
||||
[_openTabs removeObjectsAtIndexes:indexes];
|
||||
[_openTabSnapshots removeObjectsAtIndexes:indexes];
|
||||
|
||||
// Update active tab and index if needed
|
||||
if ([indexes containsIndex:self.activeTabIndex]) {
|
||||
[self chooseNewActiveTab];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)closeActiveTab {
|
||||
[self closeTab:self.activeTab];
|
||||
}
|
||||
|
||||
- (void)closeAllTabs {
|
||||
// Remove tabs and snapshots
|
||||
[_openTabs removeAllObjects];
|
||||
[_openTabSnapshots removeAllObjects];
|
||||
|
||||
// Update active tab index
|
||||
self.activeTabIndex = NSNotFound;
|
||||
}
|
||||
|
||||
- (void)updateSnapshotForActiveTab {
|
||||
if (self.activeTabIndex != NSNotFound) {
|
||||
UIImage *newSnapshot = [FLEXUtility previewImageForView:self.activeTab.view];
|
||||
[_openTabSnapshots replaceObjectAtIndex:self.activeTabIndex withObject:newSnapshot];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXTabsViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/4/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
|
||||
@interface FLEXTabsViewController : FLEXTableViewController
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,335 @@
|
||||
//
|
||||
// FLEXTabsViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/4/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTabsViewController.h"
|
||||
#import "FLEXNavigationController.h"
|
||||
#import "FLEXTabList.h"
|
||||
#import "FLEXBookmarkManager.h"
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXGlobalsViewController.h"
|
||||
#import "FLEXBookmarksViewController.h"
|
||||
|
||||
@interface FLEXTabsViewController ()
|
||||
@property (nonatomic, copy) NSArray<UINavigationController *> *openTabs;
|
||||
@property (nonatomic, copy) NSArray<UIImage *> *tabSnapshots;
|
||||
@property (nonatomic) NSInteger activeIndex;
|
||||
@property (nonatomic) BOOL presentNewActiveTabOnDismiss;
|
||||
|
||||
@property (nonatomic, readonly) FLEXExplorerViewController *corePresenter;
|
||||
@end
|
||||
|
||||
@implementation FLEXTabsViewController
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
- (id)init {
|
||||
return [self initWithStyle:UITableViewStylePlain];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.title = @"Open Tabs";
|
||||
self.navigationController.hidesBarsOnSwipe = NO;
|
||||
self.tableView.allowsMultipleSelectionDuringEditing = YES;
|
||||
|
||||
[self reloadData:NO];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
[self setupDefaultBarItems];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
// Instead of updating the active snapshot before we present,
|
||||
// we update it after we present to avoid pre-presenation latency
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[FLEXTabList.sharedList updateSnapshotForActiveTab];
|
||||
[self reloadData:NO];
|
||||
[self.tableView reloadData];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
/// @param trackActiveTabDelta whether to check if the active
|
||||
/// tab changed and needs to be presented upon "Done" dismissal.
|
||||
/// @return whether the active tab changed or not (if there are any tabs left)
|
||||
- (BOOL)reloadData:(BOOL)trackActiveTabDelta {
|
||||
BOOL activeTabDidChange = NO;
|
||||
FLEXTabList *list = FLEXTabList.sharedList;
|
||||
|
||||
// Flag to enable check to determine whether
|
||||
if (trackActiveTabDelta) {
|
||||
NSInteger oldActiveIndex = self.activeIndex;
|
||||
if (oldActiveIndex != list.activeTabIndex && list.activeTabIndex != NSNotFound) {
|
||||
self.presentNewActiveTabOnDismiss = YES;
|
||||
activeTabDidChange = YES;
|
||||
} else if (self.presentNewActiveTabOnDismiss) {
|
||||
// If we had something to present before, now we don't
|
||||
// (i.e. activeTabIndex == NSNotFound)
|
||||
self.presentNewActiveTabOnDismiss = NO;
|
||||
}
|
||||
}
|
||||
|
||||
// We assume the tabs aren't going to change out from under us, since
|
||||
// presenting any other tool via keyboard shortcuts should dismiss us first
|
||||
self.openTabs = list.openTabs;
|
||||
self.tabSnapshots = list.openTabSnapshots;
|
||||
self.activeIndex = list.activeTabIndex;
|
||||
|
||||
return activeTabDidChange;
|
||||
}
|
||||
|
||||
- (void)reloadActiveTabRowIfChanged:(BOOL)activeTabChanged {
|
||||
// Refresh the newly active tab row if needed
|
||||
if (activeTabChanged) {
|
||||
NSIndexPath *active = [NSIndexPath
|
||||
indexPathForRow:self.activeIndex inSection:0
|
||||
];
|
||||
[self.tableView reloadRowsAtIndexPaths:@[active] withRowAnimation:UITableViewRowAnimationNone];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setupDefaultBarItems {
|
||||
self.navigationItem.rightBarButtonItem = FLEXBarButtonItemSystem(Done, self, @selector(dismissAnimated));
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_fixedSpace,
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
FLEXBarButtonItemSystem(Add, self, @selector(addTabButtonPressed:)),
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
FLEXBarButtonItemSystem(Edit, self, @selector(toggleEditing)),
|
||||
];
|
||||
|
||||
// Disable editing if no tabs available
|
||||
self.toolbarItems.lastObject.enabled = self.openTabs.count > 0;
|
||||
}
|
||||
|
||||
- (void)setupEditingBarItems {
|
||||
self.navigationItem.rightBarButtonItem = nil;
|
||||
self.toolbarItems = @[
|
||||
[UIBarButtonItem itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed:)],
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
[UIBarButtonItem disabledSystemItem:UIBarButtonSystemItemAdd],
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
// We use a non-system done item because we change its title dynamically
|
||||
[UIBarButtonItem doneStyleitemWithTitle:@"Done" target:self action:@selector(toggleEditing)]
|
||||
];
|
||||
|
||||
self.toolbarItems.firstObject.tintColor = FLEXColor.destructiveColor;
|
||||
}
|
||||
|
||||
- (FLEXExplorerViewController *)corePresenter {
|
||||
// We must be presented by a FLEXExplorerViewController, or presented
|
||||
// by another view controller that was presented by FLEXExplorerViewController
|
||||
FLEXExplorerViewController *presenter = (id)self.presentingViewController;
|
||||
presenter = (id)presenter.presentingViewController ?: presenter;
|
||||
NSAssert(
|
||||
[presenter isKindOfClass:[FLEXExplorerViewController class]],
|
||||
@"The tabs view controller expects to be presented by the explorer controller"
|
||||
);
|
||||
return presenter;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Button Actions
|
||||
|
||||
- (void)dismissAnimated {
|
||||
if (self.presentNewActiveTabOnDismiss) {
|
||||
// The active tab was closed so we need to present the new one
|
||||
UIViewController *activeTab = FLEXTabList.sharedList.activeTab;
|
||||
FLEXExplorerViewController *presenter = self.corePresenter;
|
||||
[presenter dismissViewControllerAnimated:YES completion:^{
|
||||
[presenter presentViewController:activeTab animated:YES completion:nil];
|
||||
}];
|
||||
} else if (self.activeIndex == NSNotFound) {
|
||||
// The only tab was closed, so dismiss everything
|
||||
[self.corePresenter dismissViewControllerAnimated:YES completion:nil];
|
||||
} else {
|
||||
// Simple dismiss with the same active tab, only dismiss myself
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)toggleEditing {
|
||||
NSArray<NSIndexPath *> *selected = self.tableView.indexPathsForSelectedRows;
|
||||
self.editing = !self.editing;
|
||||
|
||||
if (self.isEditing) {
|
||||
[self setupEditingBarItems];
|
||||
} else {
|
||||
[self setupDefaultBarItems];
|
||||
|
||||
// Get index set of tabs to close
|
||||
NSMutableIndexSet *indexes = [NSMutableIndexSet new];
|
||||
for (NSIndexPath *ip in selected) {
|
||||
[indexes addIndex:ip.row];
|
||||
}
|
||||
|
||||
if (selected.count) {
|
||||
// Close tabs and update data source
|
||||
[FLEXTabList.sharedList closeTabsAtIndexes:indexes];
|
||||
BOOL activeTabChanged = [self reloadData:YES];
|
||||
|
||||
// Remove deleted rows
|
||||
[self.tableView deleteRowsAtIndexPaths:selected withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
|
||||
// Refresh the newly active tab row if needed
|
||||
[self reloadActiveTabRowIfChanged:activeTabChanged];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addTabButtonPressed:(UIBarButtonItem *)sender {
|
||||
if (FLEXBookmarkManager.bookmarks.count) {
|
||||
[FLEXAlert makeSheet:^(FLEXAlert *make) {
|
||||
make.title(@"New Tab");
|
||||
make.button(@"Main Menu").handler(^(NSArray<NSString *> *strings) {
|
||||
[self addTabAndDismiss:[FLEXNavigationController
|
||||
withRootViewController:[FLEXGlobalsViewController new]
|
||||
]];
|
||||
});
|
||||
make.button(@"Choose from Bookmarks").handler(^(NSArray<NSString *> *strings) {
|
||||
[self presentViewController:[FLEXNavigationController
|
||||
withRootViewController:[FLEXBookmarksViewController new]
|
||||
] animated:YES completion:nil];
|
||||
});
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:self source:sender];
|
||||
} else {
|
||||
// No bookmarks, just open the main menu
|
||||
[self addTabAndDismiss:[FLEXNavigationController
|
||||
withRootViewController:[FLEXGlobalsViewController new]
|
||||
]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addTabAndDismiss:(UINavigationController *)newTab {
|
||||
FLEXExplorerViewController *presenter = self.corePresenter;
|
||||
[presenter dismissViewControllerAnimated:YES completion:^{
|
||||
[presenter presentViewController:newTab animated:YES completion:nil];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)closeAllButtonPressed:(UIBarButtonItem *)sender {
|
||||
[FLEXAlert makeSheet:^(FLEXAlert *make) {
|
||||
NSInteger count = self.openTabs.count;
|
||||
NSString *title = FLEXPluralFormatString(count, @"Close %@ tabs", @"Close %@ tab");
|
||||
make.button(title).destructiveStyle().handler(^(NSArray<NSString *> *strings) {
|
||||
[self closeAll];
|
||||
[self toggleEditing];
|
||||
});
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:self source:sender];
|
||||
}
|
||||
|
||||
- (void)closeAll {
|
||||
NSInteger rowCount = self.openTabs.count;
|
||||
|
||||
// Close tabs and update data source
|
||||
[FLEXTabList.sharedList closeAllTabs];
|
||||
[self reloadData:YES];
|
||||
|
||||
// Delete rows from table view
|
||||
NSArray<NSIndexPath *> *allRows = [NSArray flex_forEachUpTo:rowCount map:^id(NSUInteger row) {
|
||||
return [NSIndexPath indexPathForRow:row inSection:0];
|
||||
}];
|
||||
[self.tableView deleteRowsAtIndexPaths:allRows withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Data Source
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.openTabs.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath];
|
||||
|
||||
UINavigationController *tab = self.openTabs[indexPath.row];
|
||||
cell.imageView.image = self.tabSnapshots[indexPath.row];
|
||||
cell.textLabel.text = tab.topViewController.title;
|
||||
cell.detailTextLabel.text = FLEXPluralString(tab.viewControllers.count, @"pages", @"page");
|
||||
|
||||
if (!cell.tag) {
|
||||
cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
cell.textLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
|
||||
cell.detailTextLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline];
|
||||
cell.tag = 1;
|
||||
}
|
||||
|
||||
if (indexPath.row == self.activeIndex) {
|
||||
cell.backgroundColor = FLEXColor.secondaryBackgroundColor;
|
||||
} else {
|
||||
cell.backgroundColor = FLEXColor.primaryBackgroundColor;
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Delegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (self.editing) {
|
||||
// Case: editing with multi-select
|
||||
self.toolbarItems.lastObject.title = @"Close Selected";
|
||||
self.toolbarItems.lastObject.tintColor = FLEXColor.destructiveColor;
|
||||
} else {
|
||||
if (self.activeIndex == indexPath.row && self.corePresenter != self.presentingViewController) {
|
||||
// Case: selected the already active tab
|
||||
[self dismissAnimated];
|
||||
} else {
|
||||
// Case: selected a different tab,
|
||||
// or selected a tab when presented from the FLEX toolbar
|
||||
FLEXTabList.sharedList.activeTabIndex = indexPath.row;
|
||||
self.presentNewActiveTabOnDismiss = YES;
|
||||
[self dismissAnimated];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSParameterAssert(self.editing);
|
||||
|
||||
if (tableView.indexPathsForSelectedRows.count == 0) {
|
||||
self.toolbarItems.lastObject.title = @"Done";
|
||||
self.toolbarItems.lastObject.tintColor = self.view.tintColor;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)table
|
||||
commitEditingStyle:(UITableViewCellEditingStyle)edit
|
||||
forRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSParameterAssert(edit == UITableViewCellEditingStyleDelete);
|
||||
|
||||
// Close tab and update data source
|
||||
[FLEXTabList.sharedList closeTab:self.openTabs[indexPath.row]];
|
||||
BOOL activeTabChanged = [self reloadData:YES];
|
||||
|
||||
// Delete row from table view
|
||||
[table deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
|
||||
// Refresh the newly active tab row if needed
|
||||
[self reloadActiveTabRowIfChanged:activeTabChanged];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,49 +0,0 @@
|
||||
//
|
||||
// FLEXExplorerToolbar.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class FLEXToolbarItem;
|
||||
|
||||
@interface FLEXExplorerToolbar : UIView
|
||||
|
||||
/// Toolbar item for selecting views.
|
||||
/// Users of the toolbar can configure the enabled/selected state and event targets/actions.
|
||||
@property (nonatomic, strong, readonly) FLEXToolbarItem *selectItem;
|
||||
|
||||
/// Toolbar item for presenting a list with the view hierarchy.
|
||||
/// Users of the toolbar can configure the enabled state and event targets/actions.
|
||||
@property (nonatomic, strong, readonly) FLEXToolbarItem *hierarchyItem;
|
||||
|
||||
/// Toolbar item for moving views.
|
||||
/// Users of the toolbar can configure the enabled/selected state and event targets/actions.
|
||||
@property (nonatomic, strong, readonly) FLEXToolbarItem *moveItem;
|
||||
|
||||
/// Toolbar item for inspecting details of the selected view.
|
||||
/// Users of the toolbar can configure the enabled state and event targets/actions.
|
||||
@property (nonatomic, strong, readonly) FLEXToolbarItem *globalsItem;
|
||||
|
||||
/// Toolbar item for hiding the explorer.
|
||||
/// Users of the toolbar can configure the event targets/actions.
|
||||
@property (nonatomic, strong, readonly) FLEXToolbarItem *closeItem;
|
||||
|
||||
/// A view for moving the entire toolbar.
|
||||
/// Users of the toolbar can attach a pan gesture recognizer to decide how to reposition the toolbar.
|
||||
@property (nonatomic, strong, readonly) UIView *dragHandle;
|
||||
|
||||
/// A color matching the overlay on color on the selected view.
|
||||
@property (nonatomic, strong) UIColor *selectedViewOverlayColor;
|
||||
|
||||
/// Description text for the selected view displayed below the toolbar items.
|
||||
@property (nonatomic, copy) NSString *selectedViewDescription;
|
||||
|
||||
/// Area where details of the selected view are shown
|
||||
/// Users of the toolbar can attach a tap gesture recognizer to show additional details.
|
||||
@property (nonatomic, strong, readonly) UIView *selectedViewDescriptionContainer;
|
||||
|
||||
@end
|
||||
@@ -1,226 +0,0 @@
|
||||
//
|
||||
// FLEXExplorerToolbar.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXExplorerToolbar.h"
|
||||
#import "FLEXToolbarItem.h"
|
||||
#import "FLEXResources.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXExplorerToolbar ()
|
||||
|
||||
@property (nonatomic, strong, readwrite) FLEXToolbarItem *selectItem;
|
||||
@property (nonatomic, strong, readwrite) FLEXToolbarItem *moveItem;
|
||||
@property (nonatomic, strong, readwrite) FLEXToolbarItem *globalsItem;
|
||||
@property (nonatomic, strong, readwrite) FLEXToolbarItem *closeItem;
|
||||
@property (nonatomic, strong, readwrite) FLEXToolbarItem *hierarchyItem;
|
||||
@property (nonatomic, strong, readwrite) UIView *dragHandle;
|
||||
|
||||
@property (nonatomic, strong) UIImageView *dragHandleImageView;
|
||||
|
||||
@property (nonatomic, strong) NSArray *toolbarItems;
|
||||
|
||||
@property (nonatomic, strong) UIView *selectedViewDescriptionContainer;
|
||||
@property (nonatomic, strong) UIView *selectedViewColorIndicator;
|
||||
@property (nonatomic, strong) UILabel *selectedViewDescriptionLabel;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXExplorerToolbar
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
NSMutableArray *toolbarItems = [NSMutableArray array];
|
||||
|
||||
self.dragHandle = [[UIView alloc] init];
|
||||
self.dragHandle.backgroundColor = [FLEXToolbarItem defaultBackgroundColor];
|
||||
[self addSubview:self.dragHandle];
|
||||
|
||||
UIImage *dragHandle = [FLEXResources dragHandle];
|
||||
self.dragHandleImageView = [[UIImageView alloc] initWithImage:dragHandle];
|
||||
[self.dragHandle addSubview:self.dragHandleImageView];
|
||||
|
||||
UIImage *globalsIcon = [FLEXResources globeIcon];
|
||||
self.globalsItem = [FLEXToolbarItem toolbarItemWithTitle:@"menu" image:globalsIcon];
|
||||
[self addSubview:self.globalsItem];
|
||||
[toolbarItems addObject:self.globalsItem];
|
||||
|
||||
UIImage *listIcon = [FLEXResources listIcon];
|
||||
self.hierarchyItem = [FLEXToolbarItem toolbarItemWithTitle:@"views" image:listIcon];
|
||||
[self addSubview:self.hierarchyItem];
|
||||
[toolbarItems addObject:self.hierarchyItem];
|
||||
|
||||
UIImage *selectIcon = [FLEXResources selectIcon];
|
||||
self.selectItem = [FLEXToolbarItem toolbarItemWithTitle:@"select" image:selectIcon];
|
||||
[self addSubview:self.selectItem];
|
||||
[toolbarItems addObject:self.selectItem];
|
||||
|
||||
UIImage *moveIcon = [FLEXResources moveIcon];
|
||||
self.moveItem = [FLEXToolbarItem toolbarItemWithTitle:@"move" image:moveIcon];
|
||||
[self addSubview:self.moveItem];
|
||||
[toolbarItems addObject:self.moveItem];
|
||||
|
||||
UIImage *closeIcon = [FLEXResources closeIcon];
|
||||
self.closeItem = [FLEXToolbarItem toolbarItemWithTitle:@"close" image:closeIcon];
|
||||
[self addSubview:self.closeItem];
|
||||
[toolbarItems addObject:self.closeItem];
|
||||
|
||||
self.toolbarItems = toolbarItems;
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
|
||||
self.selectedViewDescriptionContainer = [[UIView alloc] init];
|
||||
self.selectedViewDescriptionContainer.backgroundColor = [UIColor colorWithWhite:0.9 alpha:0.95];
|
||||
self.selectedViewDescriptionContainer.hidden = YES;
|
||||
[self addSubview:self.selectedViewDescriptionContainer];
|
||||
|
||||
self.selectedViewColorIndicator = [[UIView alloc] init];
|
||||
self.selectedViewColorIndicator.backgroundColor = [UIColor redColor];
|
||||
[self.selectedViewDescriptionContainer addSubview:self.selectedViewColorIndicator];
|
||||
|
||||
self.selectedViewDescriptionLabel = [[UILabel alloc] init];
|
||||
self.selectedViewDescriptionLabel.backgroundColor = [UIColor clearColor];
|
||||
self.selectedViewDescriptionLabel.font = [[self class] descriptionLabelFont];
|
||||
[self.selectedViewDescriptionContainer addSubview:self.selectedViewDescriptionLabel];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
// Drag Handle
|
||||
const CGFloat kToolbarItemHeight = [[self class] toolbarItemHeight];
|
||||
self.dragHandle.frame = CGRectMake(self.bounds.origin.x, self.bounds.origin.y, [[self class] dragHandleWidth], kToolbarItemHeight);
|
||||
CGRect dragHandleImageFrame = self.dragHandleImageView.frame;
|
||||
dragHandleImageFrame.origin.x = FLEXFloor((self.dragHandle.frame.size.width - dragHandleImageFrame.size.width) / 2.0);
|
||||
dragHandleImageFrame.origin.y = FLEXFloor((self.dragHandle.frame.size.height - dragHandleImageFrame.size.height) / 2.0);
|
||||
self.dragHandleImageView.frame = dragHandleImageFrame;
|
||||
|
||||
|
||||
// Toolbar Items
|
||||
CGFloat originX = CGRectGetMaxX(self.dragHandle.frame);
|
||||
CGFloat originY = self.bounds.origin.y;
|
||||
CGFloat height = kToolbarItemHeight;
|
||||
CGFloat width = FLEXFloor((CGRectGetMaxX(self.bounds) - originX) / [self.toolbarItems count]);
|
||||
for (UIView *toolbarItem in self.toolbarItems) {
|
||||
toolbarItem.frame = CGRectMake(originX, originY, width, height);
|
||||
originX = CGRectGetMaxX(toolbarItem.frame);
|
||||
}
|
||||
|
||||
// Make sure the last toolbar item goes to the edge to account for any accumulated rounding effects.
|
||||
UIView *lastToolbarItem = [self.toolbarItems lastObject];
|
||||
CGRect lastToolbarItemFrame = lastToolbarItem.frame;
|
||||
lastToolbarItemFrame.size.width = CGRectGetMaxX(self.bounds) - lastToolbarItemFrame.origin.x;
|
||||
lastToolbarItem.frame = lastToolbarItemFrame;
|
||||
|
||||
const CGFloat kSelectedViewColorDiameter = [[self class] selectedViewColorIndicatorDiameter];
|
||||
const CGFloat kDescriptionLabelHeight = [[self class] descriptionLabelHeight];
|
||||
const CGFloat kHorizontalPadding = [[self class] horizontalPadding];
|
||||
const CGFloat kDescriptionVerticalPadding = [[self class] descriptionVerticalPadding];
|
||||
const CGFloat kDescriptionContainerHeight = [[self class] descriptionContainerHeight];
|
||||
|
||||
CGRect descriptionContainerFrame = CGRectZero;
|
||||
descriptionContainerFrame.size.height = kDescriptionContainerHeight;
|
||||
descriptionContainerFrame.origin.y = CGRectGetMaxY(self.bounds) - kDescriptionContainerHeight;
|
||||
descriptionContainerFrame.size.width = self.bounds.size.width;
|
||||
self.selectedViewDescriptionContainer.frame = descriptionContainerFrame;
|
||||
|
||||
// Selected View Color
|
||||
CGRect selectedViewColorFrame = CGRectZero;
|
||||
selectedViewColorFrame.size.width = kSelectedViewColorDiameter;
|
||||
selectedViewColorFrame.size.height = kSelectedViewColorDiameter;
|
||||
selectedViewColorFrame.origin.x = kHorizontalPadding;
|
||||
selectedViewColorFrame.origin.y = FLEXFloor((kDescriptionContainerHeight - kSelectedViewColorDiameter) / 2.0);
|
||||
self.selectedViewColorIndicator.frame = selectedViewColorFrame;
|
||||
self.selectedViewColorIndicator.layer.cornerRadius = ceil(selectedViewColorFrame.size.height / 2.0);
|
||||
|
||||
// Selected View Description
|
||||
CGRect descriptionLabelFrame = CGRectZero;
|
||||
CGFloat descriptionOriginX = CGRectGetMaxX(selectedViewColorFrame) + kHorizontalPadding;
|
||||
descriptionLabelFrame.size.height = kDescriptionLabelHeight;
|
||||
descriptionLabelFrame.origin.x = descriptionOriginX;
|
||||
descriptionLabelFrame.origin.y = kDescriptionVerticalPadding;
|
||||
descriptionLabelFrame.size.width = CGRectGetMaxX(self.selectedViewDescriptionContainer.bounds) - kHorizontalPadding - descriptionOriginX;
|
||||
self.selectedViewDescriptionLabel.frame = descriptionLabelFrame;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Setter Overrides
|
||||
|
||||
- (void)setSelectedViewOverlayColor:(UIColor *)selectedViewOverlayColor
|
||||
{
|
||||
if (![_selectedViewOverlayColor isEqual:selectedViewOverlayColor]) {
|
||||
_selectedViewOverlayColor = selectedViewOverlayColor;
|
||||
self.selectedViewColorIndicator.backgroundColor = selectedViewOverlayColor;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setSelectedViewDescription:(NSString *)selectedViewDescription
|
||||
{
|
||||
if (![_selectedViewDescription isEqual:selectedViewDescription]) {
|
||||
_selectedViewDescription = selectedViewDescription;
|
||||
self.selectedViewDescriptionLabel.text = selectedViewDescription;
|
||||
BOOL showDescription = [selectedViewDescription length] > 0;
|
||||
self.selectedViewDescriptionContainer.hidden = !showDescription;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Sizing Convenience Methods
|
||||
|
||||
+ (UIFont *)descriptionLabelFont
|
||||
{
|
||||
return [UIFont systemFontOfSize:12.0];
|
||||
}
|
||||
|
||||
+ (CGFloat)toolbarItemHeight
|
||||
{
|
||||
return 44.0;
|
||||
}
|
||||
|
||||
+ (CGFloat)dragHandleWidth
|
||||
{
|
||||
return 30.0;
|
||||
}
|
||||
|
||||
+ (CGFloat)descriptionLabelHeight
|
||||
{
|
||||
return ceil([[self descriptionLabelFont] lineHeight]);
|
||||
}
|
||||
|
||||
+ (CGFloat)descriptionVerticalPadding
|
||||
{
|
||||
return 2.0;
|
||||
}
|
||||
|
||||
+ (CGFloat)descriptionContainerHeight
|
||||
{
|
||||
return [self descriptionVerticalPadding] * 2.0 + [self descriptionLabelHeight];
|
||||
}
|
||||
|
||||
+ (CGFloat)selectedViewColorIndicatorDiameter
|
||||
{
|
||||
return ceil([self descriptionLabelHeight] / 2.0);
|
||||
}
|
||||
|
||||
+ (CGFloat)horizontalPadding
|
||||
{
|
||||
return 11.0;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
CGFloat height = 0.0;
|
||||
height += [[self class] toolbarItemHeight];
|
||||
height += [[self class] descriptionContainerHeight];
|
||||
return CGSizeMake(size.width, height);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,26 +0,0 @@
|
||||
//
|
||||
// FLEXExplorerViewController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@protocol FLEXExplorerViewControllerDelegate;
|
||||
|
||||
@interface FLEXExplorerViewController : UIViewController
|
||||
|
||||
@property (nonatomic, weak) id <FLEXExplorerViewControllerDelegate> delegate;
|
||||
|
||||
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates;
|
||||
- (BOOL)wantsWindowToBecomeKey;
|
||||
|
||||
@end
|
||||
|
||||
@protocol FLEXExplorerViewControllerDelegate <NSObject>
|
||||
|
||||
- (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController;
|
||||
|
||||
@end
|
||||
@@ -1,833 +0,0 @@
|
||||
//
|
||||
// FLEXExplorerViewController.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXExplorerToolbar.h"
|
||||
#import "FLEXToolbarItem.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXHierarchyTableViewController.h"
|
||||
#import "FLEXGlobalsTableViewController.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
FLEXExplorerModeDefault,
|
||||
FLEXExplorerModeSelect,
|
||||
FLEXExplorerModeMove
|
||||
};
|
||||
|
||||
@interface FLEXExplorerViewController () <FLEXHierarchyTableViewControllerDelegate, FLEXGlobalsTableViewControllerDelegate>
|
||||
|
||||
@property (nonatomic, strong) FLEXExplorerToolbar *explorerToolbar;
|
||||
|
||||
/// Tracks the currently active tool/mode
|
||||
@property (nonatomic, assign) FLEXExplorerMode currentMode;
|
||||
|
||||
/// Gesture recognizer for dragging a view in move mode
|
||||
@property (nonatomic, strong) UIPanGestureRecognizer *movePanGR;
|
||||
|
||||
/// Gesture recognizer for showing additional details on the selected view
|
||||
@property (nonatomic, strong) UITapGestureRecognizer *detailsTapGR;
|
||||
|
||||
/// Only valid while a move pan gesture is in progress.
|
||||
@property (nonatomic, assign) CGRect selectedViewFrameBeforeDragging;
|
||||
|
||||
/// Only valid while a toolbar drag pan gesture is in progress.
|
||||
@property (nonatomic, assign) 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 *outlineViewsForVisibleViews;
|
||||
|
||||
/// The actual views at the selection point with the deepest view last.
|
||||
@property (nonatomic, strong) NSArray *viewsAtTapPoint;
|
||||
|
||||
/// The view that we're currently highlighting with an overlay and displaying details for.
|
||||
@property (nonatomic, strong) UIView *selectedView;
|
||||
|
||||
/// A colored transparent overlay to indicate that the view is selected.
|
||||
@property (nonatomic, strong) 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.
|
||||
/// 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;
|
||||
|
||||
/// All views that we're KVOing. Used to help us clean up properly.
|
||||
@property (nonatomic, strong) NSMutableSet *observedViews;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXExplorerViewController
|
||||
|
||||
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
|
||||
{
|
||||
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
|
||||
if (self) {
|
||||
self.observedViews = [NSMutableSet set];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
-(void)dealloc
|
||||
{
|
||||
for (UIView *view in _observedViews) {
|
||||
[self stopObservingView:view];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
// Toolbar
|
||||
self.explorerToolbar = [[FLEXExplorerToolbar alloc] init];
|
||||
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:self.view.bounds.size];
|
||||
// Start the toolbar off below any bars that may be at the top of the view.
|
||||
CGFloat toolbarOriginY = 100.0;
|
||||
self.explorerToolbar.frame = CGRectMake(0.0, toolbarOriginY, toolbarSize.width, toolbarSize.height);
|
||||
self.explorerToolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleTopMargin;
|
||||
[self.view addSubview:self.explorerToolbar];
|
||||
[self setupToolbarActions];
|
||||
[self setupToolbarGestures];
|
||||
|
||||
// View selection
|
||||
UITapGestureRecognizer *selectionTapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSelectionTap:)];
|
||||
[self.view addGestureRecognizer:selectionTapGR];
|
||||
|
||||
// View moving
|
||||
self.movePanGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleMovePan:)];
|
||||
self.movePanGR.enabled = self.currentMode == FLEXExplorerModeMove;
|
||||
[self.view addGestureRecognizer:self.movePanGR];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
[self updateButtonStates];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Rotation
|
||||
|
||||
- (UIViewController *)viewControllerForRotationAndOrientation
|
||||
{
|
||||
UIWindow *window = self.previousKeyWindow ?: [[UIApplication sharedApplication] keyWindow];
|
||||
UIViewController *viewController = window.rootViewController;
|
||||
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
|
||||
}
|
||||
return viewController;
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
|
||||
{
|
||||
UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
|
||||
UIInterfaceOrientationMask supportedOrientations = [FLEXUtility infoPlistSupportedInterfaceOrientationsMask];
|
||||
if (viewControllerToAsk && viewControllerToAsk != self) {
|
||||
supportedOrientations = [viewControllerToAsk supportedInterfaceOrientations];
|
||||
}
|
||||
|
||||
// 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 (supportedOrientations == 0) {
|
||||
supportedOrientations = UIInterfaceOrientationMaskAll;
|
||||
}
|
||||
|
||||
return supportedOrientations;
|
||||
}
|
||||
|
||||
- (BOOL)shouldAutorotate
|
||||
{
|
||||
UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
|
||||
BOOL shouldAutorotate = YES;
|
||||
if (viewControllerToAsk && viewControllerToAsk != self) {
|
||||
shouldAutorotate = [viewControllerToAsk shouldAutorotate];
|
||||
}
|
||||
return shouldAutorotate;
|
||||
}
|
||||
|
||||
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
|
||||
{
|
||||
for (UIView *outlineView in [self.outlineViewsForVisibleViews allValues]) {
|
||||
outlineView.hidden = YES;
|
||||
}
|
||||
self.selectedViewOverlay.hidden = YES;
|
||||
}
|
||||
|
||||
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Setter Overrides
|
||||
|
||||
- (void)setSelectedView:(UIView *)selectedView
|
||||
{
|
||||
if (![_selectedView isEqual:selectedView]) {
|
||||
if (![self.viewsAtTapPoint containsObject:_selectedView]) {
|
||||
[self stopObservingView:_selectedView];
|
||||
}
|
||||
|
||||
_selectedView = selectedView;
|
||||
|
||||
[self beginObservingView:selectedView];
|
||||
|
||||
// Update the toolbar and selected overlay
|
||||
self.explorerToolbar.selectedViewDescription = [FLEXUtility descriptionForView:selectedView includingFrame:YES];
|
||||
self.explorerToolbar.selectedViewOverlayColor = [FLEXUtility consistentRandomColorForObject:selectedView];;
|
||||
|
||||
if (selectedView) {
|
||||
if (!self.selectedViewOverlay) {
|
||||
self.selectedViewOverlay = [[UIView alloc] init];
|
||||
[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.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.
|
||||
[self.view bringSubviewToFront:self.selectedViewOverlay];
|
||||
[self.view bringSubviewToFront:self.explorerToolbar];
|
||||
} else {
|
||||
[self.selectedViewOverlay removeFromSuperview];
|
||||
self.selectedViewOverlay = nil;
|
||||
}
|
||||
|
||||
// Some of the button states depend on whether we have a selected view.
|
||||
[self updateButtonStates];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setViewsAtTapPoint:(NSArray *)viewsAtTapPoint
|
||||
{
|
||||
if (![_viewsAtTapPoint isEqual:viewsAtTapPoint]) {
|
||||
for (UIView *view in _viewsAtTapPoint) {
|
||||
if (view != self.selectedView) {
|
||||
[self stopObservingView:view];
|
||||
}
|
||||
}
|
||||
|
||||
_viewsAtTapPoint = viewsAtTapPoint;
|
||||
|
||||
for (UIView *view in viewsAtTapPoint) {
|
||||
[self beginObservingView:view];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setCurrentMode:(FLEXExplorerMode)currentMode
|
||||
{
|
||||
if (_currentMode != currentMode) {
|
||||
_currentMode = currentMode;
|
||||
switch (currentMode) {
|
||||
case FLEXExplorerModeDefault:
|
||||
[self removeAndClearOutlineViews];
|
||||
self.viewsAtTapPoint = nil;
|
||||
self.selectedView = nil;
|
||||
break;
|
||||
|
||||
case FLEXExplorerModeSelect:
|
||||
// Make sure the outline views are unhidden in case we came from the move mode.
|
||||
for (id key in self.outlineViewsForVisibleViews) {
|
||||
UIView *outlineView = self.outlineViewsForVisibleViews[key];
|
||||
outlineView.hidden = NO;
|
||||
}
|
||||
break;
|
||||
|
||||
case FLEXExplorerModeMove:
|
||||
// Hide all the outline views to focus on the selected view, which is the only one that will move.
|
||||
for (id key in self.outlineViewsForVisibleViews) {
|
||||
UIView *outlineView = self.outlineViewsForVisibleViews[key];
|
||||
outlineView.hidden = YES;
|
||||
}
|
||||
break;
|
||||
}
|
||||
self.movePanGR.enabled = currentMode == FLEXExplorerModeMove;
|
||||
[self updateButtonStates];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - View Tracking
|
||||
|
||||
- (void)beginObservingView:(UIView *)view
|
||||
{
|
||||
// Bail if we're already observing this view or if there's nothing to observe.
|
||||
if (!view || [self.observedViews containsObject:view]) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (NSString *keyPath in [[self class] viewKeyPathsToTrack]) {
|
||||
[view addObserver:self forKeyPath:keyPath options:0 context:NULL];
|
||||
}
|
||||
|
||||
[self.observedViews addObject:view];
|
||||
}
|
||||
|
||||
- (void)stopObservingView:(UIView *)view
|
||||
{
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (NSString *keyPath in [[self class] viewKeyPathsToTrack]) {
|
||||
[view removeObserver:self forKeyPath:keyPath];
|
||||
}
|
||||
|
||||
[self.observedViews removeObject:view];
|
||||
}
|
||||
|
||||
+ (NSArray *)viewKeyPathsToTrack
|
||||
{
|
||||
static NSArray *trackedViewKeyPaths = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
NSString *frameKeyPath = NSStringFromSelector(@selector(frame));
|
||||
trackedViewKeyPaths = @[frameKeyPath];
|
||||
});
|
||||
return trackedViewKeyPaths;
|
||||
}
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
|
||||
{
|
||||
[self updateOverlayAndDescriptionForObjectIfNeeded:object];
|
||||
}
|
||||
|
||||
- (void)updateOverlayAndDescriptionForObjectIfNeeded:(id)object
|
||||
{
|
||||
NSUInteger indexOfView = [self.viewsAtTapPoint indexOfObject:object];
|
||||
if (indexOfView != NSNotFound) {
|
||||
UIView *view = [self.viewsAtTapPoint objectAtIndex:indexOfView];
|
||||
NSValue *key = [NSValue valueWithNonretainedObject:view];
|
||||
UIView *outline = [self.outlineViewsForVisibleViews objectForKey:key];
|
||||
if (outline) {
|
||||
outline.frame = [self frameInLocalCoordinatesForView:view];
|
||||
}
|
||||
}
|
||||
if (object == self.selectedView) {
|
||||
// Update the selected view description since we show the frame value there.
|
||||
self.explorerToolbar.selectedViewDescription = [FLEXUtility descriptionForView:self.selectedView includingFrame:YES];
|
||||
CGRect selectedViewOutlineFrame = [self frameInLocalCoordinatesForView:self.selectedView];
|
||||
self.selectedViewOverlay.frame = selectedViewOutlineFrame;
|
||||
}
|
||||
}
|
||||
|
||||
- (CGRect)frameInLocalCoordinatesForView:(UIView *)view
|
||||
{
|
||||
// First convert to window coordinates since the view may be in a different window than our view.
|
||||
CGRect frameInWindow = [view convertRect:view.bounds toView:nil];
|
||||
// Then convert from the window to our view's coordinate space.
|
||||
return [self.view convertRect:frameInWindow fromView:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Toolbar Buttons
|
||||
|
||||
- (void)setupToolbarActions
|
||||
{
|
||||
[self.explorerToolbar.selectItem addTarget:self action:@selector(selectButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.explorerToolbar.hierarchyItem addTarget:self action:@selector(hierarchyButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.explorerToolbar.moveItem addTarget:self action:@selector(moveButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.explorerToolbar.globalsItem addTarget:self action:@selector(globalsButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self.explorerToolbar.closeItem addTarget:self action:@selector(closeButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
|
||||
- (void)selectButtonTapped:(FLEXToolbarItem *)sender
|
||||
{
|
||||
if (self.currentMode == FLEXExplorerModeSelect) {
|
||||
self.currentMode = FLEXExplorerModeDefault;
|
||||
} else {
|
||||
self.currentMode = FLEXExplorerModeSelect;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)hierarchyButtonTapped:(FLEXToolbarItem *)sender
|
||||
{
|
||||
NSArray *allViews = [self allViewsInHierarchy];
|
||||
NSDictionary *depthsForViews = [self hierarchyDepthsForViews:allViews];
|
||||
FLEXHierarchyTableViewController *hierarchyTVC = [[FLEXHierarchyTableViewController alloc] initWithViews:allViews viewsAtTap:self.viewsAtTapPoint selectedView:self.selectedView depths:depthsForViews];
|
||||
hierarchyTVC.delegate = self;
|
||||
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:hierarchyTVC];
|
||||
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (NSArray *)allViewsInHierarchy
|
||||
{
|
||||
NSMutableArray *allViews = [NSMutableArray array];
|
||||
NSArray *windows = [self allWindows];
|
||||
for (UIWindow *window in windows) {
|
||||
if (window != self.view.window) {
|
||||
[allViews addObject:window];
|
||||
[allViews addObjectsFromArray:[self allRecursiveSubviewsInView:window]];
|
||||
}
|
||||
}
|
||||
return allViews;
|
||||
}
|
||||
|
||||
- (NSArray *)allWindows
|
||||
{
|
||||
BOOL includeInternalWindows = YES;
|
||||
BOOL onlyVisibleWindows = NO;
|
||||
|
||||
NSArray *allWindowsComponents = @[@"al", @"lWindo", @"wsIncl", @"udingInt", @"ernalWin", @"dows:o", @"nlyVisi", @"bleWin", @"dows:"];
|
||||
SEL allWindowsSelector = NSSelectorFromString([allWindowsComponents componentsJoinedByString:@""]);
|
||||
|
||||
NSMethodSignature *methodSignature = [[UIWindow class] methodSignatureForSelector:allWindowsSelector];
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
|
||||
|
||||
invocation.target = [UIWindow class];
|
||||
invocation.selector = allWindowsSelector;
|
||||
[invocation setArgument:&includeInternalWindows atIndex:2];
|
||||
[invocation setArgument:&onlyVisibleWindows atIndex:3];
|
||||
[invocation invoke];
|
||||
|
||||
__unsafe_unretained NSArray *windows = nil;
|
||||
[invocation getReturnValue:&windows];
|
||||
return windows;
|
||||
}
|
||||
|
||||
- (UIWindow *)statusWindow
|
||||
{
|
||||
NSString *statusBarString = [NSString stringWithFormat:@"%@arWindow", @"_statusB"];
|
||||
return [[UIApplication sharedApplication] valueForKey:statusBarString];
|
||||
}
|
||||
|
||||
- (void)moveButtonTapped:(FLEXToolbarItem *)sender
|
||||
{
|
||||
if (self.currentMode == FLEXExplorerModeMove) {
|
||||
self.currentMode = FLEXExplorerModeDefault;
|
||||
} else {
|
||||
self.currentMode = FLEXExplorerModeMove;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)globalsButtonTapped:(FLEXToolbarItem *)sender
|
||||
{
|
||||
FLEXGlobalsTableViewController *globalsViewController = [[FLEXGlobalsTableViewController alloc] init];
|
||||
globalsViewController.delegate = self;
|
||||
[FLEXGlobalsTableViewController setApplicationWindow:[[UIApplication sharedApplication] keyWindow]];
|
||||
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:globalsViewController];
|
||||
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)closeButtonTapped:(FLEXToolbarItem *)sender
|
||||
{
|
||||
self.currentMode = FLEXExplorerModeDefault;
|
||||
[self.delegate explorerViewControllerDidFinish:self];
|
||||
}
|
||||
|
||||
- (void)updateButtonStates
|
||||
{
|
||||
// Move and details only active when an object is selected.
|
||||
BOOL hasSelectedObject = self.selectedView != nil;
|
||||
self.explorerToolbar.moveItem.enabled = hasSelectedObject;
|
||||
self.explorerToolbar.selectItem.selected = self.currentMode == FLEXExplorerModeSelect;
|
||||
self.explorerToolbar.moveItem.selected = self.currentMode == FLEXExplorerModeMove;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Toolbar Dragging
|
||||
|
||||
- (void)setupToolbarGestures
|
||||
{
|
||||
// Pan gesture for dragging.
|
||||
UIPanGestureRecognizer *panGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleToolbarPanGesture:)];
|
||||
[self.explorerToolbar.dragHandle addGestureRecognizer:panGR];
|
||||
|
||||
// Tap gesture for hinting.
|
||||
UITapGestureRecognizer *hintTapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleToolbarHintTapGesture:)];
|
||||
[self.explorerToolbar.dragHandle addGestureRecognizer:hintTapGR];
|
||||
|
||||
// Tap gesture for showing additional details
|
||||
self.detailsTapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleToolbarDetailsTapGesture:)];
|
||||
[self.explorerToolbar.selectedViewDescriptionContainer addGestureRecognizer:self.detailsTapGR];
|
||||
}
|
||||
|
||||
- (void)handleToolbarPanGesture:(UIPanGestureRecognizer *)panGR
|
||||
{
|
||||
switch (panGR.state) {
|
||||
case UIGestureRecognizerStateBegan:
|
||||
self.toolbarFrameBeforeDragging = self.explorerToolbar.frame;
|
||||
[self updateToolbarPostionWithDragGesture:panGR];
|
||||
break;
|
||||
|
||||
case UIGestureRecognizerStateChanged:
|
||||
case UIGestureRecognizerStateEnded:
|
||||
[self updateToolbarPostionWithDragGesture:panGR];
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateToolbarPostionWithDragGesture:(UIPanGestureRecognizer *)panGR
|
||||
{
|
||||
CGPoint translation = [panGR translationInView:self.view];
|
||||
CGRect newToolbarFrame = self.toolbarFrameBeforeDragging;
|
||||
newToolbarFrame.origin.y += translation.y;
|
||||
|
||||
CGFloat maxY = CGRectGetMaxY(self.view.bounds) - newToolbarFrame.size.height;
|
||||
if (newToolbarFrame.origin.y < 0.0) {
|
||||
newToolbarFrame.origin.y = 0.0;
|
||||
} else if (newToolbarFrame.origin.y > maxY) {
|
||||
newToolbarFrame.origin.y = maxY;
|
||||
}
|
||||
|
||||
self.explorerToolbar.frame = newToolbarFrame;
|
||||
}
|
||||
|
||||
- (void)handleToolbarHintTapGesture:(UITapGestureRecognizer *)tapGR
|
||||
{
|
||||
// Bounce the toolbar to indicate that it is draggable.
|
||||
// TODO: make it bouncier.
|
||||
if (tapGR.state == UIGestureRecognizerStateRecognized) {
|
||||
CGRect originalToolbarFrame = self.explorerToolbar.frame;
|
||||
const NSTimeInterval kHalfwayDuration = 0.2;
|
||||
const CGFloat kVerticalOffset = 30.0;
|
||||
[UIView animateWithDuration:kHalfwayDuration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
|
||||
CGRect newToolbarFrame = self.explorerToolbar.frame;
|
||||
newToolbarFrame.origin.y += kVerticalOffset;
|
||||
self.explorerToolbar.frame = newToolbarFrame;
|
||||
} completion:^(BOOL finished) {
|
||||
[UIView animateWithDuration:kHalfwayDuration delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
|
||||
self.explorerToolbar.frame = originalToolbarFrame;
|
||||
} completion:nil];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleToolbarDetailsTapGesture:(UITapGestureRecognizer *)tapGR
|
||||
{
|
||||
if (tapGR.state == UIGestureRecognizerStateRecognized && self.selectedView) {
|
||||
FLEXObjectExplorerViewController *selectedViewExplorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:self.selectedView];
|
||||
selectedViewExplorer.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(selectedViewExplorerFinished:)];
|
||||
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:selectedViewExplorer];
|
||||
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - View Selection
|
||||
|
||||
- (void)handleSelectionTap:(UITapGestureRecognizer *)tapGR
|
||||
{
|
||||
// Only if we're in selection mode
|
||||
if (self.currentMode == FLEXExplorerModeSelect && tapGR.state == UIGestureRecognizerStateRecognized) {
|
||||
// Note that [tapGR locationInView:nil] is broken in iOS 8, so we have to do a two step conversion to window coordinates.
|
||||
// Thanks to @lascorbe for finding this: https://github.com/Flipboard/FLEX/pull/31
|
||||
CGPoint tapPointInView = [tapGR locationInView:self.view];
|
||||
CGPoint tapPointInWindow = [self.view convertPoint:tapPointInView toView:nil];
|
||||
[self updateOutlineViewsForSelectionPoint:tapPointInWindow];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateOutlineViewsForSelectionPoint:(CGPoint)selectionPointInWindow
|
||||
{
|
||||
[self removeAndClearOutlineViews];
|
||||
|
||||
// Include hidden views in the "viewsAtTapPoint" array so we can show them in the hierarchy list.
|
||||
self.viewsAtTapPoint = [self viewsAtPoint:selectionPointInWindow skipHiddenViews:NO];
|
||||
|
||||
// For outlined views and the selected view, only use visible views.
|
||||
// Outlining hidden views adds clutter and makes the selection behavior confusing.
|
||||
NSArray *visibleViewsAtTapPoint = [self viewsAtPoint:selectionPointInWindow skipHiddenViews:YES];
|
||||
NSMutableDictionary *newOutlineViewsForVisibleViews = [NSMutableDictionary dictionary];
|
||||
for (UIView *view in visibleViewsAtTapPoint) {
|
||||
UIView *outlineView = [self outlineViewForView:view];
|
||||
[self.view addSubview:outlineView];
|
||||
NSValue *key = [NSValue valueWithNonretainedObject:view];
|
||||
[newOutlineViewsForVisibleViews setObject:outlineView forKey:key];
|
||||
}
|
||||
self.outlineViewsForVisibleViews = newOutlineViewsForVisibleViews;
|
||||
self.selectedView = [self viewForSelectionAtPoint:selectionPointInWindow];
|
||||
|
||||
// Make sure the explorer toolbar doesn't end up behind the newly added outline views.
|
||||
[self.view bringSubviewToFront:self.explorerToolbar];
|
||||
|
||||
[self updateButtonStates];
|
||||
}
|
||||
|
||||
- (UIView *)outlineViewForView:(UIView *)view
|
||||
{
|
||||
CGRect outlineFrame = [self frameInLocalCoordinatesForView:view];
|
||||
UIView *outlineView = [[UIView alloc] initWithFrame:outlineFrame];
|
||||
outlineView.backgroundColor = [UIColor clearColor];
|
||||
outlineView.layer.borderColor = [[FLEXUtility consistentRandomColorForObject:view] CGColor];
|
||||
outlineView.layer.borderWidth = 1.0;
|
||||
return outlineView;
|
||||
}
|
||||
|
||||
- (void)removeAndClearOutlineViews
|
||||
{
|
||||
for (id key in self.outlineViewsForVisibleViews) {
|
||||
UIView *outlineView = self.outlineViewsForVisibleViews[key];
|
||||
[outlineView removeFromSuperview];
|
||||
}
|
||||
self.outlineViewsForVisibleViews = nil;
|
||||
}
|
||||
|
||||
- (NSArray *)viewsAtPoint:(CGPoint)tapPointInWindow skipHiddenViews:(BOOL)skipHidden
|
||||
{
|
||||
NSMutableArray *views = [NSMutableArray array];
|
||||
for (UIWindow *window in [self allWindows]) {
|
||||
// Don't include the explorer's own window or subviews.
|
||||
if (window != self.view.window && [window pointInside:tapPointInWindow withEvent:nil]) {
|
||||
[views addObject:window];
|
||||
[views addObjectsFromArray:[self recursiveSubviewsAtPoint:tapPointInWindow inView:window skipHiddenViews:skipHidden]];
|
||||
}
|
||||
}
|
||||
return views;
|
||||
}
|
||||
|
||||
- (UIView *)viewForSelectionAtPoint:(CGPoint)tapPointInWindow
|
||||
{
|
||||
// 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 [[self allWindows] reverseObjectEnumerator]) {
|
||||
// Ignore the explorer's own window.
|
||||
if (window != self.view.window) {
|
||||
if ([window hitTest:tapPointInWindow withEvent:nil]) {
|
||||
windowForSelection = window;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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];
|
||||
}
|
||||
|
||||
- (NSArray *)recursiveSubviewsAtPoint:(CGPoint)pointInView inView:(UIView *)view skipHiddenViews:(BOOL)skipHidden
|
||||
{
|
||||
NSMutableArray *subviewsAtPoint = [NSMutableArray array];
|
||||
for (UIView *subview in view.subviews) {
|
||||
BOOL isHidden = subview.hidden || subview.alpha < 0.01;
|
||||
if (skipHidden && isHidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
BOOL subviewContainsPoint = CGRectContainsPoint(subview.frame, pointInView);
|
||||
if (subviewContainsPoint) {
|
||||
[subviewsAtPoint addObject:subview];
|
||||
}
|
||||
|
||||
// If this view doesn't clip to its bounds, we need to check its subviews even if it doesn't contain the selection point.
|
||||
// They may be visible and contain the selection point.
|
||||
if (subviewContainsPoint || !subview.clipsToBounds) {
|
||||
CGPoint pointInSubview = [view convertPoint:pointInView toView:subview];
|
||||
[subviewsAtPoint addObjectsFromArray:[self recursiveSubviewsAtPoint:pointInSubview inView:subview skipHiddenViews:skipHidden]];
|
||||
}
|
||||
}
|
||||
return subviewsAtPoint;
|
||||
}
|
||||
|
||||
- (NSArray *)allRecursiveSubviewsInView:(UIView *)view
|
||||
{
|
||||
NSMutableArray *subviews = [NSMutableArray array];
|
||||
for (UIView *subview in view.subviews) {
|
||||
[subviews addObject:subview];
|
||||
[subviews addObjectsFromArray:[self allRecursiveSubviewsInView:subview]];
|
||||
}
|
||||
return subviews;
|
||||
}
|
||||
|
||||
- (NSDictionary *)hierarchyDepthsForViews:(NSArray *)views
|
||||
{
|
||||
NSMutableDictionary *hierarchyDepths = [NSMutableDictionary dictionary];
|
||||
for (UIView *view in views) {
|
||||
NSInteger depth = 0;
|
||||
UIView *tryView = view;
|
||||
while (tryView.superview) {
|
||||
tryView = tryView.superview;
|
||||
depth++;
|
||||
}
|
||||
[hierarchyDepths setObject:@(depth) forKey:[NSValue valueWithNonretainedObject:view]];
|
||||
}
|
||||
return hierarchyDepths;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Selected View Moving
|
||||
|
||||
- (void)handleMovePan:(UIPanGestureRecognizer *)movePanGR
|
||||
{
|
||||
switch (movePanGR.state) {
|
||||
case UIGestureRecognizerStateBegan:
|
||||
self.selectedViewFrameBeforeDragging = self.selectedView.frame;
|
||||
[self updateSelectedViewPositionWithDragGesture:movePanGR];
|
||||
break;
|
||||
|
||||
case UIGestureRecognizerStateChanged:
|
||||
case UIGestureRecognizerStateEnded:
|
||||
[self updateSelectedViewPositionWithDragGesture:movePanGR];
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateSelectedViewPositionWithDragGesture:(UIPanGestureRecognizer *)movePanGR
|
||||
{
|
||||
CGPoint translation = [movePanGR translationInView:self.selectedView.superview];
|
||||
CGRect newSelectedViewFrame = self.selectedViewFrameBeforeDragging;
|
||||
newSelectedViewFrame.origin.x = FLEXFloor(newSelectedViewFrame.origin.x + translation.x);
|
||||
newSelectedViewFrame.origin.y = FLEXFloor(newSelectedViewFrame.origin.y + translation.y);
|
||||
self.selectedView.frame = newSelectedViewFrame;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Touch Handling
|
||||
|
||||
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates
|
||||
{
|
||||
BOOL shouldReceiveTouch = NO;
|
||||
|
||||
CGPoint pointInLocalCoordinates = [self.view convertPoint:pointInWindowCoordinates fromView:nil];
|
||||
|
||||
// Always if it's on the toolbar
|
||||
if (CGRectContainsPoint(self.explorerToolbar.frame, pointInLocalCoordinates)) {
|
||||
shouldReceiveTouch = YES;
|
||||
}
|
||||
|
||||
// Always if we're in selection mode
|
||||
if (!shouldReceiveTouch && self.currentMode == FLEXExplorerModeSelect) {
|
||||
shouldReceiveTouch = YES;
|
||||
}
|
||||
|
||||
// Always in move mode too
|
||||
if (!shouldReceiveTouch && self.currentMode == FLEXExplorerModeMove) {
|
||||
shouldReceiveTouch = YES;
|
||||
}
|
||||
|
||||
// Always if we have a modal presented
|
||||
if (!shouldReceiveTouch && self.presentedViewController) {
|
||||
shouldReceiveTouch = YES;
|
||||
}
|
||||
|
||||
return shouldReceiveTouch;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - FLEXHierarchyTableViewControllerDelegate
|
||||
|
||||
- (void)hierarchyViewController:(FLEXHierarchyTableViewController *)hierarchyViewController didFinishWithSelectedView:(UIView *)selectedView
|
||||
{
|
||||
// Note that we need to wait until the view controller is dismissed to calculated the frame of the outline view.
|
||||
// Otherwise the coordinate conversion doesn't give the correct result.
|
||||
[self resignKeyAndDismissViewControllerAnimated:YES completion:^{
|
||||
// If the selected view is outside of the tap point array (selected from "Full Hierarchy"),
|
||||
// then clear out the tap point array and remove all the outline views.
|
||||
if (![self.viewsAtTapPoint containsObject:selectedView]) {
|
||||
self.viewsAtTapPoint = nil;
|
||||
[self removeAndClearOutlineViews];
|
||||
}
|
||||
|
||||
// If we now have a selected view and we didn't have one previously, go to "select" mode.
|
||||
if (self.currentMode == FLEXExplorerModeDefault && selectedView) {
|
||||
self.currentMode = FLEXExplorerModeSelect;
|
||||
}
|
||||
|
||||
// The selected view setter will also update the selected view overlay appropriately.
|
||||
self.selectedView = selectedView;
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - FLEXGlobalsViewControllerDelegate
|
||||
|
||||
- (void)globalsViewControllerDidFinish:(FLEXGlobalsTableViewController *)globalsViewController
|
||||
{
|
||||
[self resignKeyAndDismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - FLEXObjectExplorerViewController Done Action
|
||||
|
||||
- (void)selectedViewExplorerFinished:(id)sender
|
||||
{
|
||||
[self resignKeyAndDismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Modal Presentation and Window Management
|
||||
|
||||
- (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];
|
||||
|
||||
// Make our window key to correctly handle input.
|
||||
[self.view.window makeKeyWindow];
|
||||
|
||||
// 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];
|
||||
}
|
||||
|
||||
- (void)resignKeyAndDismissViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion
|
||||
{
|
||||
UIWindow *previousKeyWindow = self.previousKeyWindow;
|
||||
self.previousKeyWindow = nil;
|
||||
[previousKeyWindow makeKeyWindow];
|
||||
[[previousKeyWindow rootViewController] setNeedsStatusBarAppearanceUpdate];
|
||||
|
||||
// Restore the status bar window's normal window level.
|
||||
// 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];
|
||||
}
|
||||
|
||||
- (BOOL)wantsWindowToBecomeKey
|
||||
{
|
||||
return self.previousKeyWindow != nil;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,16 +0,0 @@
|
||||
//
|
||||
// FLEXManager+Private.h
|
||||
// PebbleApp
|
||||
//
|
||||
// Created by Javier Soto on 7/26/14.
|
||||
// Copyright (c) 2014 Pebble Technology. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXManager.h"
|
||||
|
||||
@interface FLEXManager ()
|
||||
|
||||
/// An array of FLEXGlobalsTableViewControllerEntry objects that have been registered by the user.
|
||||
@property (nonatomic, readonly, strong) NSArray *userGlobalEntries;
|
||||
|
||||
@end
|
||||
@@ -1,50 +0,0 @@
|
||||
//
|
||||
// FLEXManager.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface FLEXManager : NSObject
|
||||
|
||||
+ (instancetype)sharedManager;
|
||||
|
||||
@property (nonatomic, readonly) BOOL isHidden;
|
||||
|
||||
- (void)showExplorer;
|
||||
- (void)hideExplorer;
|
||||
|
||||
/// 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 pruged under memory pressure.
|
||||
@property (nonatomic, assign, getter=isNetworkDebuggingEnabled) BOOL networkDebuggingEnabled;
|
||||
|
||||
/// Defaults to 50 MB if never set. Values set here are presisted 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;
|
||||
|
||||
#pragma mark - Extensions
|
||||
|
||||
/// Adds an entry at the bottom of the list of Global State items. Call this method before this view controller is displayed.
|
||||
/// @param entryName The string to be displayed in the cell.
|
||||
/// @param objectFutureBlock When you tap on the row, information about the object returned by this block will be displayed.
|
||||
/// Passing a block that returns an object allows you to display information about an object whose actual pointer may change at runtime (e.g. +currentUser)
|
||||
/// @note This method must be called from the main thread.
|
||||
/// The objectFutureBlock will be invoked from the main thread and may return nil.
|
||||
/// @note The passed block will be copied and retain for the duration of the application, you may want to use __weak references.
|
||||
- (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id (^)(void))objectFutureBlock;
|
||||
|
||||
/// Adds an entry at the bottom of the list of Global State items. Call this method before this view controller is displayed.
|
||||
/// @param entryName The string to be displayed in the cell.
|
||||
/// @param viewControllerFutureBlock When you tap on the row, view controller returned by this block will be pushed on the navigation controller stack.
|
||||
/// @note This method must be called from the main thread.
|
||||
/// The viewControllerFutureBlock will be invoked from the main thread and may not return nil.
|
||||
/// @note The passed block will be copied and retain for the duration of the application, you may want to use __weak references.
|
||||
- (void)registerGlobalEntryWithName:(NSString *)entryName
|
||||
viewControllerFutureBlock:(UIViewController * (^)(void))viewControllerFutureBlock;
|
||||
|
||||
@end
|
||||
@@ -1,165 +0,0 @@
|
||||
//
|
||||
// FLEXManager.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXManager.h"
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXWindow.h"
|
||||
#import "FLEXGlobalsTableViewControllerEntry.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "FLEXNetworkObserver.h"
|
||||
#import "FLEXNetworkRecorder.h"
|
||||
|
||||
@interface FLEXManager () <FLEXWindowEventDelegate, FLEXExplorerViewControllerDelegate>
|
||||
|
||||
@property (nonatomic, strong) FLEXWindow *explorerWindow;
|
||||
@property (nonatomic, strong) FLEXExplorerViewController *explorerViewController;
|
||||
|
||||
@property (nonatomic, readonly, strong) NSMutableArray *userGlobalEntries;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXManager
|
||||
|
||||
+ (instancetype)sharedManager
|
||||
{
|
||||
static FLEXManager *sharedManager = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedManager = [[[self class] alloc] init];
|
||||
});
|
||||
return sharedManager;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_userGlobalEntries = [[NSMutableArray alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (FLEXWindow *)explorerWindow
|
||||
{
|
||||
NSAssert([NSThread isMainThread], @"You must use %@ from the main thread only.", NSStringFromClass([self class]));
|
||||
|
||||
if (!_explorerWindow) {
|
||||
_explorerWindow = [[FLEXWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
|
||||
_explorerWindow.eventDelegate = self;
|
||||
_explorerWindow.rootViewController = self.explorerViewController;
|
||||
}
|
||||
|
||||
return _explorerWindow;
|
||||
}
|
||||
|
||||
- (FLEXExplorerViewController *)explorerViewController
|
||||
{
|
||||
if (!_explorerViewController) {
|
||||
_explorerViewController = [[FLEXExplorerViewController alloc] init];
|
||||
_explorerViewController.delegate = self;
|
||||
}
|
||||
|
||||
return _explorerViewController;
|
||||
}
|
||||
|
||||
- (void)showExplorer
|
||||
{
|
||||
self.explorerWindow.hidden = NO;
|
||||
}
|
||||
|
||||
- (void)hideExplorer
|
||||
{
|
||||
self.explorerWindow.hidden = YES;
|
||||
}
|
||||
|
||||
- (BOOL)isHidden
|
||||
{
|
||||
return self.explorerWindow.isHidden;
|
||||
}
|
||||
|
||||
- (BOOL)isNetworkDebuggingEnabled
|
||||
{
|
||||
return [FLEXNetworkObserver isEnabled];
|
||||
}
|
||||
|
||||
- (void)setNetworkDebuggingEnabled:(BOOL)networkDebuggingEnabled
|
||||
{
|
||||
[FLEXNetworkObserver setEnabled:networkDebuggingEnabled];
|
||||
}
|
||||
|
||||
- (NSUInteger)networkResponseCacheByteLimit
|
||||
{
|
||||
return [[FLEXNetworkRecorder defaultRecorder] responseCacheByteLimit];
|
||||
}
|
||||
|
||||
- (void)setNetworkResponseCacheByteLimit:(NSUInteger)networkResponseCacheByteLimit
|
||||
{
|
||||
[[FLEXNetworkRecorder defaultRecorder] setResponseCacheByteLimit:networkResponseCacheByteLimit];
|
||||
}
|
||||
|
||||
#pragma mark - FLEXWindowEventDelegate
|
||||
|
||||
- (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow
|
||||
{
|
||||
// Ask the explorer view controller
|
||||
return [self.explorerViewController shouldReceiveTouchAtWindowPoint:pointInWindow];
|
||||
}
|
||||
|
||||
- (BOOL)canBecomeKeyWindow
|
||||
{
|
||||
// Only when the explorer view controller wants it because it needs to accept key input & affect the status bar.
|
||||
return [self.explorerViewController wantsWindowToBecomeKey];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - FLEXExplorerViewControllerDelegate
|
||||
|
||||
- (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController
|
||||
{
|
||||
[self hideExplorer];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Extensions
|
||||
|
||||
- (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id (^)(void))objectFutureBlock
|
||||
{
|
||||
NSParameterAssert(entryName);
|
||||
NSParameterAssert(objectFutureBlock);
|
||||
NSAssert([NSThread isMainThread], @"This method must be called from the main thread.");
|
||||
|
||||
entryName = entryName.copy;
|
||||
FLEXGlobalsTableViewControllerEntry *entry = [FLEXGlobalsTableViewControllerEntry entryWithNameFuture:^NSString *{
|
||||
return entryName;
|
||||
} viewControllerFuture:^UIViewController *{
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:objectFutureBlock()];
|
||||
}];
|
||||
|
||||
[self.userGlobalEntries addObject:entry];
|
||||
}
|
||||
|
||||
- (void)registerGlobalEntryWithName:(NSString *)entryName viewControllerFutureBlock:(UIViewController * (^)(void))viewControllerFutureBlock
|
||||
{
|
||||
NSParameterAssert(entryName);
|
||||
NSParameterAssert(viewControllerFutureBlock);
|
||||
NSAssert([NSThread isMainThread], @"This method must be called from the main thread.");
|
||||
|
||||
entryName = entryName.copy;
|
||||
FLEXGlobalsTableViewControllerEntry *entry = [FLEXGlobalsTableViewControllerEntry entryWithNameFuture:^NSString *{
|
||||
return entryName;
|
||||
} viewControllerFuture:^UIViewController *{
|
||||
UIViewController *viewController = viewControllerFutureBlock();
|
||||
NSCAssert(viewController, @"'%@' entry returned nil viewController. viewControllerFutureBlock should never return nil.", entryName);
|
||||
return viewController;
|
||||
}];
|
||||
|
||||
[self.userGlobalEntries addObject:entry];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// FLEXToolbarItem.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface FLEXToolbarItem : UIButton
|
||||
|
||||
+ (instancetype)toolbarItemWithTitle:(NSString *)title image:(UIImage *)image;
|
||||
|
||||
+ (UIColor *)defaultBackgroundColor;
|
||||
|
||||
@end
|
||||
@@ -1,133 +0,0 @@
|
||||
//
|
||||
// FLEXToolbarItem.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXToolbarItem.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXToolbarItem ()
|
||||
|
||||
@property (nonatomic, copy) NSAttributedString *attributedTitle;
|
||||
@property (nonatomic, strong) UIImage *image;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXToolbarItem
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.backgroundColor = [[self class] defaultBackgroundColor];
|
||||
[self setTitleColor:[[self class] defaultTitleColor] forState:UIControlStateNormal];
|
||||
[self setTitleColor:[[self class] disabledTitleColor] forState:UIControlStateDisabled];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype)toolbarItemWithTitle:(NSString *)title image:(UIImage *)image
|
||||
{
|
||||
FLEXToolbarItem *toolbarItem = [self buttonWithType:UIButtonTypeCustom];
|
||||
NSAttributedString *attributedTitle = [[NSAttributedString alloc] initWithString:title attributes:[self titleAttributes]];
|
||||
toolbarItem.attributedTitle = attributedTitle;
|
||||
toolbarItem.image = image;
|
||||
[toolbarItem setAttributedTitle:attributedTitle forState:UIControlStateNormal];
|
||||
[toolbarItem setImage:image forState:UIControlStateNormal];
|
||||
return toolbarItem;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Display Defaults
|
||||
|
||||
+ (NSDictionary *)titleAttributes
|
||||
{
|
||||
return @{NSFontAttributeName : [FLEXUtility defaultFontOfSize:12.0]};
|
||||
}
|
||||
|
||||
+ (UIColor *)defaultTitleColor
|
||||
{
|
||||
return [UIColor blackColor];
|
||||
}
|
||||
|
||||
+ (UIColor *)disabledTitleColor
|
||||
{
|
||||
return [UIColor colorWithWhite:121.0/255.0 alpha:1.0];
|
||||
}
|
||||
|
||||
+ (UIColor *)highlightedBackgroundColor
|
||||
{
|
||||
return [UIColor colorWithWhite:0.9 alpha:1.0];
|
||||
}
|
||||
|
||||
+ (UIColor *)selectedBackgroundColor
|
||||
{
|
||||
return [UIColor colorWithRed:199.0/255.0 green:199.0/255.0 blue:255.0/255.0 alpha:1.0];
|
||||
}
|
||||
|
||||
+ (UIColor *)defaultBackgroundColor
|
||||
{
|
||||
return [UIColor colorWithWhite:1.0 alpha:0.95];
|
||||
}
|
||||
|
||||
+ (CGFloat)topMargin
|
||||
{
|
||||
return 2.0;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - State Changes
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted
|
||||
{
|
||||
[super setHighlighted:highlighted];
|
||||
[self updateBackgroundColor];
|
||||
}
|
||||
|
||||
- (void)setSelected:(BOOL)selected
|
||||
{
|
||||
[super setSelected:selected];
|
||||
[self updateBackgroundColor];
|
||||
}
|
||||
|
||||
- (void)updateBackgroundColor
|
||||
{
|
||||
if (self.highlighted) {
|
||||
self.backgroundColor = [[self class] highlightedBackgroundColor];
|
||||
} else if (self.selected) {
|
||||
self.backgroundColor = [[self class] selectedBackgroundColor];
|
||||
} else {
|
||||
self.backgroundColor = [[self class] defaultBackgroundColor];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UIButton Layout Overrides
|
||||
|
||||
- (CGRect)titleRectForContentRect:(CGRect)contentRect
|
||||
{
|
||||
// Bottom aligned and centered.
|
||||
CGRect titleRect = CGRectZero;
|
||||
CGSize titleSize = [self.attributedTitle boundingRectWithSize:contentRect.size options:0 context:nil].size;
|
||||
titleSize = CGSizeMake(ceil(titleSize.width), ceil(titleSize.height));
|
||||
titleRect.size = titleSize;
|
||||
titleRect.origin.y = contentRect.origin.y + CGRectGetMaxY(contentRect) - titleSize.height;
|
||||
titleRect.origin.x = contentRect.origin.x + FLEXFloor((contentRect.size.width - titleSize.width) / 2.0);
|
||||
return titleRect;
|
||||
}
|
||||
|
||||
- (CGRect)imageRectForContentRect:(CGRect)contentRect
|
||||
{
|
||||
CGSize imageSize = self.image.size;
|
||||
CGRect titleRect = [self titleRectForContentRect:contentRect];
|
||||
CGFloat availableHeight = contentRect.size.height - titleRect.size.height - [[self class] topMargin];
|
||||
CGFloat originY = [[self class] topMargin] + FLEXFloor((availableHeight - imageSize.height) / 2.0);
|
||||
CGFloat originX = FLEXFloor((contentRect.size.width - imageSize.width) / 2.0);
|
||||
CGRect imageRect = CGRectMake(originX, originY, imageSize.width, imageSize.height);
|
||||
return imageRect;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,24 +0,0 @@
|
||||
//
|
||||
// FLEXWindow.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/13/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@protocol FLEXWindowEventDelegate;
|
||||
|
||||
@interface FLEXWindow : UIWindow
|
||||
|
||||
@property (nonatomic, weak) id <FLEXWindowEventDelegate> eventDelegate;
|
||||
|
||||
@end
|
||||
|
||||
@protocol FLEXWindowEventDelegate <NSObject>
|
||||
|
||||
- (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow;
|
||||
- (BOOL)canBecomeKeyWindow;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// FLEX-Categories.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/12/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
|
||||
#import <FLEX/UIBarButtonItem+FLEX.h>
|
||||
#import <FLEX/CALayer+FLEX.h>
|
||||
#import <FLEX/UIFont+FLEX.h>
|
||||
#import <FLEX/UIGestureRecognizer+Blocks.h>
|
||||
#import <FLEX/UIView+FLEX_Layout.h>
|
||||
#import <FLEX/UIPasteboard+FLEX.h>
|
||||
#import <FLEX/UIMenu+FLEX.h>
|
||||
#import <FLEX/UITextField+Range.h>
|
||||
|
||||
#import <FLEX/NSObject+FLEX_Reflection.h>
|
||||
#import <FLEX/NSArray+FLEX.h>
|
||||
#import <FLEX/NSDictionary+ObjcRuntime.h>
|
||||
#import <FLEX/NSString+ObjcRuntime.h>
|
||||
#import <FLEX/NSString+FLEX.h>
|
||||
#import <FLEX/NSUserDefaults+FLEX.h>
|
||||
#import <FLEX/NSMapTable+FLEX_Subscripting.h>
|
||||
#import <FLEX/NSTimer+FLEX.h>
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// FLEX-Core.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/11/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <FLEX/FLEXFilteringTableViewController.h>
|
||||
#import <FLEX/FLEXNavigationController.h>
|
||||
#import <FLEX/FLEXTableViewController.h>
|
||||
#import <FLEX/FLEXTableView.h>
|
||||
|
||||
#import <FLEX/FLEXSingleRowSection.h>
|
||||
#import <FLEX/FLEXTableViewSection.h>
|
||||
|
||||
#import <FLEX/FLEXCodeFontCell.h>
|
||||
#import <FLEX/FLEXSubtitleTableViewCell.h>
|
||||
#import <FLEX/FLEXTableViewCell.h>
|
||||
#import <FLEX/FLEXMultilineTableViewCell.h>
|
||||
#import <FLEX/FLEXKeyValueTableViewCell.h>
|
||||
|
||||
#import <FLEX/FLEXScopeCarousel.h>
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// FLEX-ObjectExploring.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/11/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <FLEX/FLEXObjectExplorerFactory.h>
|
||||
#import <FLEX/FLEXObjectExplorerViewController.h>
|
||||
|
||||
#import <FLEX/FLEXObjectExplorer.h>
|
||||
|
||||
#import <FLEX/FLEXShortcut.h>
|
||||
#import <FLEX/FLEXShortcutsFactory+Defaults.h>
|
||||
#import <FLEX/FLEXShortcutsSection.h>
|
||||
#import <FLEX/FLEXBlockShortcuts.h>
|
||||
#import <FLEX/FLEXBundleShortcuts.h>
|
||||
#import <FLEX/FLEXClassShortcuts.h>
|
||||
#import <FLEX/FLEXImageShortcuts.h>
|
||||
#import <FLEX/FLEXLayerShortcuts.h>
|
||||
#import <FLEX/FLEXViewControllerShortcuts.h>
|
||||
#import <FLEX/FLEXViewShortcuts.h>
|
||||
|
||||
#import <FLEX/FLEXCollectionContentSection.h>
|
||||
#import <FLEX/FLEXColorPreviewSection.h>
|
||||
#import <FLEX/FLEXDefaultsContentSection.h>
|
||||
#import <FLEX/FLEXMetadataSection.h>
|
||||
#import <FLEX/FLEXMutableListSection.h>
|
||||
#import <FLEX/FLEXObjectInfoSection.h>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user