Repository: CosmicMind/Material Branch: development Commit: ea59f7c4d72a Files: 185 Total size: 854.8 KB Directory structure: gitextract_djfw36z0/ ├── .gitignore ├── .gitmodules ├── .swift-version ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cartfile ├── LICENSE.md ├── Material.podspec ├── Material.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── Material.xcscmblueprint │ └── xcshareddata/ │ └── xcschemes/ │ └── Material.xcscheme ├── Package.swift ├── README.md └── Sources/ ├── Assets.xcassets/ │ ├── Contents.json │ ├── cm_add_white.imageset/ │ │ └── Contents.json │ ├── cm_arrow_back_white.imageset/ │ │ └── Contents.json │ ├── cm_arrow_downward_white.imageset/ │ │ └── Contents.json │ ├── cm_audio_library_white.imageset/ │ │ └── Contents.json │ ├── cm_audio_white.imageset/ │ │ └── Contents.json │ ├── cm_bell_white.imageset/ │ │ └── Contents.json │ ├── cm_check_white.imageset/ │ │ └── Contents.json │ ├── cm_close_white.imageset/ │ │ └── Contents.json │ ├── cm_image_white.imageset/ │ │ └── Contents.json │ ├── cm_menu_white.imageset/ │ │ └── Contents.json │ ├── cm_microphone_white.imageset/ │ │ └── Contents.json │ ├── cm_more_horiz_white.imageset/ │ │ └── Contents.json │ ├── cm_more_vert_white.imageset/ │ │ └── Contents.json │ ├── cm_movie_white.imageset/ │ │ └── Contents.json │ ├── cm_pause_white.imageset/ │ │ └── Contents.json │ ├── cm_pen_white.imageset/ │ │ └── Contents.json │ ├── cm_photo_camera_white.imageset/ │ │ └── Contents.json │ ├── cm_photo_library_white.imageset/ │ │ └── Contents.json │ ├── cm_play_white.imageset/ │ │ └── Contents.json │ ├── cm_search_white.imageset/ │ │ └── Contents.json │ ├── cm_settings_white.imageset/ │ │ └── Contents.json │ ├── cm_share_white.imageset/ │ │ └── Contents.json │ ├── cm_shuffle_white.imageset/ │ │ └── Contents.json │ ├── cm_skip_backward_white.imageset/ │ │ └── Contents.json │ ├── cm_skip_forward_white.imageset/ │ │ └── Contents.json │ ├── cm_star_white.imageset/ │ │ └── Contents.json │ ├── cm_videocam_white.imageset/ │ │ └── Contents.json │ ├── cm_volume_high_white.imageset/ │ │ └── Contents.json │ ├── cm_volume_medium_white.imageset/ │ │ └── Contents.json │ ├── cm_volume_off_white.imageset/ │ │ └── Contents.json │ ├── ic_add_circle_outline_white.imageset/ │ │ └── Contents.json │ ├── ic_add_circle_white.imageset/ │ │ └── Contents.json │ ├── ic_add_white.imageset/ │ │ └── Contents.json │ ├── ic_arrow_back_white.imageset/ │ │ └── Contents.json │ ├── ic_arrow_downward_white.imageset/ │ │ └── Contents.json │ ├── ic_audiotrack_white.imageset/ │ │ └── Contents.json │ ├── ic_camera_front_white.imageset/ │ │ └── Contents.json │ ├── ic_camera_rear_white.imageset/ │ │ └── Contents.json │ ├── ic_check_white.imageset/ │ │ └── Contents.json │ ├── ic_close_white.imageset/ │ │ └── Contents.json │ ├── ic_edit_white.imageset/ │ │ └── Contents.json │ ├── ic_email_white.imageset/ │ │ └── Contents.json │ ├── ic_favorite_border_white.imageset/ │ │ └── Contents.json │ ├── ic_favorite_white.imageset/ │ │ └── Contents.json │ ├── ic_flash_auto_white.imageset/ │ │ └── Contents.json │ ├── ic_flash_off_white.imageset/ │ │ └── Contents.json │ ├── ic_flash_on_white.imageset/ │ │ └── Contents.json │ ├── ic_history_white.imageset/ │ │ └── Contents.json │ ├── ic_home_white.imageset/ │ │ └── Contents.json │ ├── ic_image_white.imageset/ │ │ └── Contents.json │ ├── ic_menu_white.imageset/ │ │ └── Contents.json │ ├── ic_more_horiz_white.imageset/ │ │ └── Contents.json │ ├── ic_more_vert_white.imageset/ │ │ └── Contents.json │ ├── ic_movie_white.imageset/ │ │ └── Contents.json │ ├── ic_phone_white.imageset/ │ │ └── Contents.json │ ├── ic_photo_camera_white.imageset/ │ │ └── Contents.json │ ├── ic_photo_library_white.imageset/ │ │ └── Contents.json │ ├── ic_place_white.imageset/ │ │ └── Contents.json │ ├── ic_search_white.imageset/ │ │ └── Contents.json │ ├── ic_settings_white.imageset/ │ │ └── Contents.json │ ├── ic_share_white.imageset/ │ │ └── Contents.json │ ├── ic_star_border_white.imageset/ │ │ └── Contents.json │ ├── ic_star_half_white.imageset/ │ │ └── Contents.json │ ├── ic_star_white.imageset/ │ │ └── Contents.json │ ├── ic_videocam_white.imageset/ │ │ └── Contents.json │ ├── ic_visibility_off_white.imageset/ │ │ └── Contents.json │ ├── ic_visibility_white.imageset/ │ │ └── Contents.json │ └── ic_work_white.imageset/ │ └── Contents.json ├── Info.plist ├── LICENSE ├── Material.h └── iOS/ ├── Animation/ │ ├── PulseAnimation.swift │ └── SpringAnimation.swift ├── Application/ │ └── Application.swift ├── Bar/ │ └── Bar.swift ├── BottomTabBar/ │ └── BottomNavigationController.swift ├── Button/ │ ├── BaseIconLayerButton.swift │ ├── Button.swift │ ├── CheckButton.swift │ ├── FABButton.swift │ ├── FlatButton.swift │ ├── IconButton.swift │ ├── RadioButton.swift │ └── RaisedButton.swift ├── ButtonGroup/ │ ├── BaseButtonGroup.swift │ ├── CheckButtonGroup.swift │ └── RadioButtonGroup.swift ├── Card/ │ ├── Card.swift │ ├── ImageCard.swift │ └── PresenterCard.swift ├── Chip/ │ ├── ChipBar.swift │ └── ChipBarController.swift ├── Collection/ │ ├── CardCollectionView/ │ │ ├── CardCollectionViewCell.swift │ │ └── CardCollectionViewController.swift │ ├── CollectionReusableView.swift │ ├── CollectionView.swift │ ├── CollectionViewCell.swift │ ├── CollectionViewController.swift │ └── CollectionViewLayout.swift ├── Color/ │ └── Color.swift ├── Data/ │ └── DataSourceItem.swift ├── Device/ │ └── Device.swift ├── Dialogs/ │ ├── Dialog.swift │ ├── DialogController.swift │ └── DialogView.swift ├── Divider/ │ └── Divider.swift ├── Extension/ │ ├── Material+Array.swift │ ├── Material+CALayer.swift │ ├── Material+MotionAnimation.swift │ ├── Material+NSMutableAttributedString.swift │ ├── Material+String.swift │ ├── Material+UIButton.swift │ ├── Material+UIColor.swift │ ├── Material+UIFont.swift │ ├── Material+UIImage.swift │ ├── Material+UILabel.swift │ ├── Material+UIView.swift │ ├── Material+UIViewController.swift │ └── Material+UIWindow.swift ├── FABMenu/ │ ├── FABMenu.swift │ └── FABMenuController.swift ├── Font/ │ ├── DynamicFontType.swift │ ├── Font.swift │ └── RobotoFont.swift ├── Grid/ │ └── Grid.swift ├── Height/ │ └── HeightPreset.swift ├── Icon/ │ └── Icon.swift ├── Layer/ │ └── Layer.swift ├── Layout/ │ ├── Layout.swift │ ├── LayoutAnchor.swift │ ├── LayoutAttribute.swift │ └── LayoutConstraint.swift ├── Navigation/ │ ├── NavigationBar.swift │ ├── NavigationController.swift │ └── NavigationItem.swift ├── NavigationDrawer/ │ └── NavigationDrawerController.swift ├── Screen/ │ └── Screen.swift ├── SearchBar/ │ ├── SearchBar.swift │ └── SearchBarController.swift ├── Snackbar/ │ ├── Snackbar.swift │ └── SnackbarController.swift ├── StatusBar/ │ └── StatusBarController.swift ├── Switch/ │ └── Switch.swift ├── Tab/ │ ├── TabBar.swift │ └── TabsController.swift ├── Table/ │ ├── TableView.swift │ ├── TableViewCell.swift │ └── TableViewController.swift ├── Text/ │ ├── Editor.swift │ ├── ErrorTextField.swift │ ├── ErrorTextFieldValidator.swift │ ├── TextField.swift │ ├── TextStorage.swift │ └── TextView.swift ├── Theme/ │ └── Theme.swift ├── Toolbar/ │ ├── Toolbar.swift │ └── ToolbarController.swift ├── Transition/ │ ├── DisplayStyle.swift │ └── TransitionController.swift ├── Type/ │ ├── Border.swift │ ├── CornerRadius.swift │ ├── Depth.swift │ ├── EdgeInsets.swift │ ├── InterimSpace.swift │ ├── Offset.swift │ └── Shape.swift └── View/ ├── PulseView.swift ├── View.swift └── ViewController.swift ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ #OS X .DS_Store # Xcode build/* *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata profile *.moved-aside *.playground *.framework DerivedData Index Build Checkouts ================================================ FILE: .gitmodules ================================================ [submodule "Sources/Frameworks/Motion"] path = Sources/Frameworks/Motion url = https://github.com/CosmicMind/Motion.git ================================================ FILE: .swift-version ================================================ 5.0 ================================================ FILE: CHANGELOG.md ================================================ ## 3.1.8 - [pr-1269](https://github.com/CosmicMind/Material/pull/1269): Fixed Xcode 11 crash, where layoutMargins are not available before iOS 13. - [pr-1270](https://github.com/CosmicMind/Material/pull/1270): Fixed missing argument in Swift Package Manager. * Updated to [Motion 3.1.3](https://github.com/CosmicMind/Motion/releases/tag/3.1.3). ## 3.1.7 * Fixed Grid issues, where the layout calculations were being deferred and causing inconsistencies in layouts. * Updated to [Motion 3.1.2](https://github.com/CosmicMind/Motion/releases/tag/3.1.2). ## 3.1.6 - [issue-1245](https://github.com/CosmicMind/Material/issues/1245): Fixed issue where completion block was not executed when calling Switch.toggle. ## 3.1.5 - [pr-1248](https://github.com/CosmicMind/Material/pull/1248): Exposed Obj-C methods for NavigationDrawerController. - [issue-1247](https://github.com/CosmicMind/Material/issues/1247): Several methods in NavigationDrawerController not visible in Obj-C. ## 3.1.4 - [pr-1239](https://github.com/CosmicMind/Material/pull/1239): Fixed regression with intrinsic content sizing in Switch control. - [pr-1240](https://github.com/CosmicMind/Material/pull/1240): Fixed prepare method called twice. - [issue-1215](https://github.com/CosmicMind/Material/issues/1215): prepare() is called twice on NavigationDrawerController. ## 3.1.3 * Added installation instructions to README. - [pr-1236](https://github.com/CosmicMind/Material/pull/1236): Added Layout relations. - [issue-1220](https://github.com/CosmicMind/Material/issues/1220): Support all relations for Layout constraints. ## 3.1.2 - [pr-1233](https://github.com/CosmicMind/Material/pull/1233): Fixed Layout breaks - subview ordering. - [issue-1232](https://github.com/CosmicMind/Material/issues/1232): Layout breaks view arrangements. ## 3.1.1 - [pr-2131](https://github.com/CosmicMind/Material/pull/2131): Storyboard TextField fixes. - [issue-1229](https://github.com/CosmicMind/Material/issues/1229): TextField's tintColor doesn't support user setting. - [issue-1230](https://github.com/CosmicMind/Material/issues/1230): Button's title font doesn't support user setting. ## 3.1.0 - Updated to swift 5. ## 3.0.0 - Updated to swift 4.2. - [pr-1124](https://github.com/CosmicMind/Material/pull/1124): Fixed issue-1123, TextField is not scrolling. - [issue-1123](https://github.com/CosmicMind/Material/issues/1123): TextField is not scrolling when inputing characters and using a large Font size. - [pr-1126](https://github.com/CosmicMind/Material/pull/1126): Cleaned up TextField. - [pr-1130](https://github.com/CosmicMind/Material/pull/1130): Addressed multiple issues. - [issue-1125](https://github.com/CosmicMind/Material/issues/1125): TextView with animated placeholder. - [issue-1127](https://github.com/CosmicMind/Material/issues/1127): TextView auto-adjust height based on text lines. - [issue-1128](https://github.com/CosmicMind/Material/issues/1128): TextField animates weird when text alignment is .right and we have textInset. - Removed `textInset: CGFloat` and added `textInsets: EdgeInsets` to `TextField`. - [pr-1134](https://github.com/CosmicMind/Material/pull/1134): Added swipe feature to BottomNavigationController. - [issue-1132](https://github.com/CosmicMind/Material/issues/1132): BottomNavigationController same swipe behaviour as TabsController. - [pr-1147](https://github.com/CosmicMind/Material/pull/1147): Allow framework to be linked from extensions. - [pr-1151](https://github.com/CosmicMind/Material/pull/1151): New features. - Added `left/right/above/below` directions to `DepthPreset`. - Added `.custom(x)` case for `HeightPreset`. - Added support for `heightPreset` in `BottomNavigationController`. [issue-1150](https://github.com/CosmicMind/Material/issues/1150) - [pr-1165](https://github.com/CosmicMind/Material/pull/1165): Added interactive swipe. - [issue-1135](https://github.com/CosmicMind/Material/issues/1135): Convert swiping in TabsController and BottomNavigationController to interactive. - [pr-1115](https://github.com/CosmicMind/Material/pull/1115): Introducing Theming to Material. - [pr-1173](https://github.com/CosmicMind/Material/pull/1173): Added dialogs. - [pr-1174](https://github.com/CosmicMind/Material/pull/1174): Added disabling theming globally and per-class. - [pr-1183](https://github.com/CosmicMind/Material/pull/1183): Added global theme font. - [pr-1185](https://github.com/CosmicMind/Material/pull/1185): Reworked layout system. - [pr-1186](https://github.com/CosmicMind/Material/pull/1186): Fixed SnackBar laid out incorrectly. - [pr-1187](https://github.com/CosmicMind/Material/pull/1187): Added option for disabling snackbar layout edge inset calculation ## 2.17.0 * Updated for Swift 4.2. * Updated to [Motion 1.5.0](https://github.com/CosmicMind/Motion/releases/tag/1.5.0) ## 2.16.4 * [pr-1120](https://github.com/CosmicMind/Material/pull/1120): Fixed issue where TextField cursor was not being repositioned correctly. * [issue-1119](https://github.com/CosmicMind/Material/issues/1119): Cursor position was incorrectly being positioned when toggling security entry. ## 2.16.3 * Updated to [Motion 1.4.3](https://github.com/CosmicMind/Motion/releases/tag/1.4.3) * [pr-1116](https://github.com/CosmicMind/Material/pull/1116): ViewController-oriented clean up. * [pr-1117](https://github.com/CosmicMind/Material/pull/1117): Fixed TextView font issue with emojis. * [issue-838](https://github.com/CosmicMind/Material/issues/838): TextView's font breaks when you type emoji. ## 2.16.2 * [pr-1113](https://github.com/CosmicMind/Material/pull/1113): Added update() to Grid. * [pr-1112](https://github.com/CosmicMind/Material/pull/1112): Added tab bar centering. * [issue-926](https://github.com/CosmicMind/Material/issues/926): TabsController - centering TabItem after selection. * [pr-1114](https://github.com/CosmicMind/Material/pull/1114): Added option to adjust tabBar line width. * [issue-1109](https://github.com/CosmicMind/Material/issues/1109): Want to change TabBar line width. ## 2.16.1 * [issue-1110](https://github.com/CosmicMind/Material/issues/1110): Fixed an issue where the depth of a view was being clipped incorrectly. * [pr-1111](https://github.com/CosmicMind/Material/pull/1111): Fixed TabItem - was not being changed on swipe. * [pr-1106](https://github.com/CosmicMind/Material/pull/1106): Added ability to show visibility and clear button at the same time. * [issue-992](https://github.com/CosmicMind/Material/issues/992): Visibility & Clear Button can't be shown in TextField at the same time. * [pr-1104](https://github.com/CosmicMind/Material/pull/1104): Added missing devices. * [pr-1101](https://github.com/CosmicMind/Material/pull/1101): Enum for support iPhoneX. ## 2.16.0 * Updated to [Motion 1.4.2](https://github.com/CosmicMind/Motion/releases/tag/1.4.2). * [pr-1004](https://github.com/CosmicMind/Material/pull/1004): Added RadioButton/CheckButton and RadioButtonGroup/CheckButtonGroup. * [issue-505](https://github.com/CosmicMind/Material/issues/505): Add RadioButton and Checkbox. * Updated to [Motion 1.4.0](https://github.com/CosmicMind/Motion/releases/tag/1.4.0). * [issue-1078](https://github.com/CosmicMind/Material/issues/1078): Update Motion Dependency. * [pr-1047](https://github.com/CosmicMind/Material/pull/1047): Document material color codes. * [issue-1000](https://github.com/CosmicMind/Material/issues/1000): Color: Document mapping from codes (e.g. a400) to names (e.g. accent1). * [pr-1055](https://github.com/CosmicMind/Material/pull/1055): Open up FABMenu a little bit. * Updated Copyright years. * [pr-1079](https://github.com/CosmicMind/Material/pull/1079): Added custom navigationBarClass support to NavigationController. * [issue-1074](https://github.com/CosmicMind/Material/issues/1074): Need to use a NavigationBar subclass with NavigationController. * [pr-1080](https://github.com/CosmicMind/Material/pull/1080): Fixed license badge href. * [pr-1046](https://github.com/CosmicMind/Material/pull/1046): Added ShouldOpen and ShouldClose delegate methods to FABMenuDelegate. * [issue-1043](https://github.com/CosmicMind/Material/issues/1043): ShouldOpen and ShouldClose delegate methods FABMenu. * [pr-1086](https://github.com/CosmicMind/Material/pull/1086): Fix delegations never fired on tab swipe. * [issue-1087](https://github.com/CosmicMind/Material/issues/1087): TabBar item is selected even though TabsController delegate shouldSelect always returns false. * [issue-1056](https://github.com/CosmicMind/Material/issues/1056): Delegation methods never fired on Tab swipe. * [pr-1088](https://github.com/CosmicMind/Material/pull/1088): Removed unnecessary convenience initializers. * [issue-1085](https://github.com/CosmicMind/Material/issues/1085): `convenience init()` across the framework prevents generic initialization of the components. * [pr-1082](https://github.com/CosmicMind/Material/pull/1082): Added ErrorTextField validation. * [issue-1017](https://github.com/CosmicMind/Material/issues/1017): Can we make the error detail for textfields dynamic? * [issue-1053](https://github.com/CosmicMind/Material/issues/1053): TextField Detail Label not Layed-Out correctly with Left-Image. * [pr-1103](https://github.com/CosmicMind/Material/pull/1103): Added ability to change password visibility icons. * [pr-1097](https://github.com/CosmicMind/Material/pull/1097):: Added new extensions: UIColor(argb:), UIColor(rgb:), UIButton.fontSize, UILabel.fontSize. * [pr-1093](https://github.com/CosmicMind/Material/pull/1093):: Fix TextField placeholderLabel position. * [issue-1092](https://github.com/CosmicMind/Material/issues/1092): TextField.placeholderLabel is positioned higher than before in version 2.x.x. * [pr-1103](https://github.com/CosmicMind/Material/pull/1103): Added ability to change password visibility icons. * [issue-1012](https://github.com/CosmicMind/Material/issues/1012): Can we set visibility icon custom for password textfield. ## 2.15.0 * [issue-1057](https://github.com/CosmicMind/Material/issues/1057): Added image states for TabItems used in TabBar and TabsController. ## 2.14.0 * [issue-995](https://github.com/CosmicMind/Material/issues/995): Updated iOS 11 layout margins for NavigationBar. * [pr-1038](https://github.com/CosmicMind/Material/pull/1038): Merged PR for iOS 11 layout margins fix. ## 2.13.7 * Updated TabsController to no longer force the default animation to change between tabs and not return to normal behavior. * [issue-1044](https://github.com/CosmicMind/Material/issues/1044): Fixed issue where TabBar items were not correctly laying out. ## 2.13.6 * [issue-841](https://github.com/CosmicMind/Material/issues/841): Adjusted default sizing for Switch to be more like the original sizing. * [pr-1030](https://github.com/CosmicMind/Material/pull/1032): Added workaround for known issue where trailing whitespace is apparent in UITextField. * Updated to [Motion 1.3.5](https://github.com/CosmicMind/Motion/releases/tag/1.3.5). ## 2.13.5 * [pr-1019](https://github.com/CosmicMind/Material/pull/1019): Added swipe gesture handling to TabsController. * Updated to [Motion 1.3.4](https://github.com/CosmicMind/Motion/releases/tag/1.3.4). ## 2.13.4 * [issue-1016](https://github.com/CosmicMind/Material/issues/1016): Updated hierarchy traversal for TransitionController types to no longer skip over non TransitionController types. ## 2.13.3 * [issue-1015](https://github.com/CosmicMind/Material/issues/1015): Fixed regression where view lifecycle functions were not being called. * Motion disabled by default for NavigationController to avoid unbalanced calls to view lifecycle when presenting a NavigationController modally. * Updated to [Motion 1.3.3](https://github.com/CosmicMind/Motion/releases/tag/1.3.3). ## 2.13.2 * Updated to [Motion 1.3.2](https://github.com/CosmicMind/Motion/releases/tag/1.3.2). * Fixed unbalanced calls in Motion transitions. ## 2.13.1 * Updated to [Motion 1.3.1](https://github.com/CosmicMind/Motion/releases/tag/1.3.1). ## 2.13.0 * Updated to [Motion 1.3.0](https://github.com/CosmicMind/Motion/releases/tag/1.3.0). ## 2.12.19 * [issue-997](https://github.com/CosmicMind/Material/issues/977): Fixed NavigationDrawerController where swiping off device caused a partial correct state. ## 2.12.18 * Fixed layout issues in CollectionView, where the sizing was not correctlly being initialized. * [issue-495](https://github.com/CosmicMind/Material/issues/495): Made TextField.textInset available in Obj-C. ## 2.12.17 * [pr-979](https://github.com/CosmicMind/Material/pull/979): Added `visibilityOff` icon and updated `TextField` to utilize it. * [issue-982](https://github.com/CosmicMind/Material/issues/982): Updated Icon let declarations to var declarations to allow custom icon sets. * [issue-980](https://github.com/CosmicMind/Material/issues/980): Added `@objc` to extension properties in Material+UIView. * [issue-650](https://github.com/CosmicMind/Material/issues/650): Fixed issue where `NavigationBar.backButton` would incorrectly be laid out when caching view controllers. * [issue-973](https://github.com/CosmicMind/Material/issues/973): Fixed issue where `Button.prepare` was not being called in the correct order. ## 2.12.16 * [issue-965](https://github.com/CosmicMind/Material/issues/965): Removed duplicate `prepare` call in initializer. * Rework of Layout's internal process - removed an async call to layout views. * Updated the layout calls for FABMenu's fabButton. ## 2.12.15 * [issue-957](https://github.com/CosmicMind/Material/issues/957): Fixed StatusBar height issue in iOS 9 and iOS 10. ## 2.12.14 * [samples issue-95](https://github.com/CosmicMind/Samples/issues/95): Fixed TabBar image colors that were not correctly being set for a given state. * Fixed layout issue, where the calculation of the layout item was not being set in the current render cycle. ## 2.12.13 * Fixed issue where sizing of pulse was incorrectly animating when using the NavigationController on iOS 11. ## 2.12.12 * [issue-924](https://github.com/CosmicMind/Material/issues/924): Fixed NavigationController display in iOS 10. ## 2.12.11 * Fixed iPhoneX topLayoutGuide constraints not properly being set for StatusBarController types. * Fixed iOS 11 layout issues for NavigationController. * [pr-945](https://github.com/CosmicMind/Material/pull/945): iPhoneX update for TabBar bottom line alignment. ## 2.12.10 * [samples-issue-78](https://github.com/CosmicMind/Samples/issues/78): Fixed iPhoneX bottomLayoutGuide constraints not properly being set. ## 2.12.9 * Fixed breaking change to loading the TabsController. ## 2.12.8 * [issue-933](https://github.com/CosmicMind/Material/issues/933): Fixed issue where `NavigationDrawerController` was not displaying the `leftViewController` and `rightViewController`. * [issue-940](https://github.com/CosmicMind/Material/issues/940): Fixed an issue where the `TransitionController` was not executing the lifecycle functions for the initial `rootViewController`. ## 2.12.7 * [pr-938](https://github.com/CosmicMind/Material/pull/938): An expansion on this PR to fix the lifecycle issues with transitions. ## 2.12.6 * Fixed issue where TabBar.lineColor was incorrectly being updated. ## 2.12.5 * Updated to [Motion 1.2.4](https://github.com/CosmicMind/Motion/releases/tag/1.2.4). * [issue-937](https://github.com/CosmicMind/Material/issues/937): Added @objc to `TabBar.lineColor` for access availability. * [pr-934](https://github.com/CosmicMind/Material/pull/934): Added access to the `TabBar.line` view. ## 2.12.4 * Updated to [Motion 1.2.3](https://github.com/CosmicMind/Motion/releases/tag/1.2.3). * [issue-919](https://github.com/CosmicMind/Material/issues/919): Fixed issue where lifecycle methods were being called on tab item view controllers prematurely. * [pr-923](https://github.com/CosmicMind/Material/pull/923): Merge PR that fixes [issue-919](https://github.com/CosmicMind/Material/issues/919). * [issue-931](https://github.com/CosmicMind/Material/issues/931): Fixed issue where selectedTabItem was not updated correctly during a porgrammatic transition. ## 2.12.3 * [issue-907](https://github.com/CosmicMind/Material/issues/907): Fixed Layout ordering issues. ## 2.12.2 * [issue-860](https://github.com/CosmicMind/Material/issues/860): Updated TabBar color states and added an independent line color state. ## 2.12.1 * [issue-911](https://github.com/CosmicMind/Material/issues/911): Added @objc to TabBar.tabItems for visibility in Obj-C. ## 2.12.0 * [issue-860](https://github.com/CosmicMind/Material/issues/860): Added `TabBar` color states. ## 2.11.4 * Added Cartfile for Carthage package manager, which includes the Motion dependency. ## 2.11.3 * Updated Motion submodule to use `https` over `git@`. ## 2.11.2 * Updated to [Motion 1.2.2](https://github.com/CosmicMind/Motion/releases/tag/1.2.2). ## 2.11.1 * Fixed duplicate `prepare` call in `TabsController`. ## 2.11.0 * Updated the installation guide for Material, [Material - It's time to download](https://www.cosmicmind.com/danieldahan/lesson/6). Material now uses [Motion](https://github.com/CosmicMind/Motion) as a submodule and CocoaPods dependancy. * [samples issue-70](https://github.com/CosmicMind/Samples/issues/70#issuecomment-335533243): Made an internal `_TabBarDelegate` to avoid needing to override the `TabBarDelegate` in `TabsController`. ## 2.10.4 * [issue-891](https://github.com/CosmicMind/Material/pull/891): Fixed conflict with addAttributes method in the NSMutableAttributedString extension. ## 2.10.3 * [issue-773](https://github.com/CosmicMind/Material/pull/773): Added `Swift 4` support. * [pr-873](https://github.com/CosmicMind/Material/pull/873): Fixes PlaceholderLabel position when right-aligned - iOS 11.0 * [issue-886](https://github.com/CosmicMind/Material/issues/886): Fixed a memory leak within Motion's references to previous `UINavigationControllerDelegate` and `UITabBarControllerDelegate`. * [issue-861](https://github.com/CosmicMind/Material/pull/861): Fixed `NavigationBar` being `nil` in some cases. * [issue-845](https://github.com/CosmicMind/Material/pull/845): Fixed ambiguity issues with all properties. ## 2.10.2 * [issue-849](https://github.com/CosmicMind/Material/issues/849): Fixed issue where `TextView.placeholderNormalColor` was not correctly displaying. Renamed `TextView.placeholderNormalColor` to `TextView.placeholderColor`. * [issue-856](https://github.com/CosmicMind/Material/issues/856): Fixed issue where `TextField.placeholderAnimation = .hidden` was not correctly being displayed when text was set to nil. * All default instances of `Color.grey.lighten3` have been switched to `Color.grey.lighten2`. ## 2.10.1 * [issue-833](https://github.com/CosmicMind/Material/issues/833): `TabsController` now be selected programmatically. * [issue-859](https://github.com/CosmicMind/Material/issues/859): `TabsController` now has a delegation protocol `TabsControllerDelegate`. * [issue-830](https://github.com/CosmicMind/Material/issues/830): Bug fix where `TabsController` did not animate to the correct tab when programmatically set. ## 2.10.0 * [issue-857](https://github.com/CosmicMind/Material/issues/857): Fixed an issue where setting the `statusBar` property for the `ToolbarController` was not updating the background color correctly. * [issue-858](https://github.com/CosmicMind/Material/issues/858): Fixed `Photos` sample project that was not updated to reflect the changes in the `TabBar`. * [pr-715](https://github.com/CosmicMind/Material/pull/715): Added `isPlaceholderUppercasedWhenEditing` property to `TextField`. * [pr-721](https://github.com/CosmicMind/Material/pull/721): Added `FABMenuItemTitleLabelPosition` which allows the `FABMenu` to place its `FABBMenuItems` to either the `left` or `right` position of the `FABButton`. * [pr-851](https://github.com/CosmicMind/Material/pull/851): Added `placeholderHorizontalOffset` property to `TextField`. * [pr-847](https://github.com/CosmicMind/Material/pull/847): Added `placeholderActiveScale` property to `TextField`. * [pr-848](https://github.com/CosmicMind/Material/pull/848): Natural motion transition added to `TabsController` when view controller `motionModalTransitionType` is set to `.auto`. * `Card` types default to a `depthPreset` of `.none`. * Added `shouldSelect` method to `TabBarDelegate`. * Updated `TabsController` to use `TabBarDelegate` rather than button handlers. * Updated `EdgeInsetsPreset` values to: ```swift .square1: EdgeInsets(top: 4, left: 4, bottom: 4, right: 4) .square2: EdgeInsets(top: 8, left: 8, bottom: 8, right: 8) .square3: EdgeInsets(top: 16, left: 16, bottom: 16, right: 16) .square4: EdgeInsets(top: 20, left: 20, bottom: 20, right: 20) .square5: EdgeInsets(top: 24, left: 24, bottom: 24, right: 24) .square6: EdgeInsets(top: 28, left: 28, bottom: 28, right: 28) .square7: EdgeInsets(top: 32, left: 32, bottom: 32, right: 32) .square8: EdgeInsets(top: 36, left: 36, bottom: 36, right: 36) .square9: EdgeInsets(top: 40, left: 40, bottom: 40, right: 40) .square10: EdgeInsets(top: 44, left: 44, bottom: 44, right: 44) .square11: EdgeInsets(top: 48, left: 48, bottom: 48, right: 48) .square12: EdgeInsets(top: 52, left: 52, bottom: 52, right: 52) .square13: EdgeInsets(top: 56, left: 56, bottom: 56, right: 56) .square14: EdgeInsets(top: 60, left: 60, bottom: 60, right: 60) .square15: EdgeInsets(top: 64, left: 64, bottom: 64, right: 64) ``` ## 2.9.4 * Added `ToolbarAlignment` to allow placement of the `Toolbar` at the top or bottom of the view controller. * Added `SearchBarAlignment` to allow placement of the `SearchBar` at the top or bottom of the view controller. ## 2.9.3 * Renamed `TabBarController` to `TabsController` to avoid name confusion with iOS `UITabBarController` helper properties. * Updated layout with `TabsController` to properly adjust during orientation changes. ## 2.9.2 * `TabBarController` now subclasses `TransitionController` to minimize code. * Fixed regression in `TabBarController` where line was incorrectly animation upon initial interaction. ## 2.9.1 * Renamed `TabsController` to `TabBarController`. * Renamed `ChipsController` to `ChipBarController`. * [issue-832](https://github.com/CosmicMind/Material/issues/832): Fixed issue where `TabBar` line was incorrectly laying out. ## 2.9.0 * Replaced `RootController` with `TransitionController`. * Updated CharacterAttribute API. * Added Chips - alpha phase. * [issue-824](https://github.com/CosmicMind/Material/issues/824): Fixed issue where `TabBar` line alignment was not correctly animating when initial engaged. * [issue-820](https://github.com/CosmicMind/Material/issues/820): Fixed retain cycle found in FABMenuController. ## 2.8.1 * [issue-815](https://github.com/CosmicMind/Material/issues/815): Fixed the TabBar line alignment issue when rotating the device orientation. ## 2.8.0 Removed PageTabBarController and added the TabsController. Now removing the following issues and feature requests: * [issue-642](https://github.com/CosmicMind/Material/issues/642) * [issue-742](https://github.com/CosmicMind/Material/issues/742) * [issue-768](https://github.com/CosmicMind/Material/issues/768) * [issue-605](https://github.com/CosmicMind/Material/issues/605) * [issue-743](https://github.com/CosmicMind/Material/issues/743) * [issue-619](https://github.com/CosmicMind/Material/issues/619) ## 2.7.1 * [issue-811](https://github.com/CosmicMind/Material/issues/811): Removed scrollable TabBar style, until feature is ready. ## 2.7.0 * Added [Motion](https://github.com/CosmicMind/Motion) framework to Material as new animation and transitions library. * Removed `Capture`. * Removed `PhotoLibrary`. * Removed `Reminders`. * [issue-809](https://github.com/CosmicMind/Material/issues/809): Fixed detailTextLabel visibility issue. * [issue-797](https://github.com/CosmicMind/Material/issues/797): Added `back button` hiding features to `NavigationController`. * [issue-552](https://github.com/CosmicMind/Material/issues/552): Fixed `@objc` declaration issues. * [pr-662](https://github.com/CosmicMind/Material/pull/662): Added the ability to set a text inset for a `TextField`. * [pr-707](https://github.com/CosmicMind/Material/pull/707): Moved the `statusBarStyle` extension to `NavigationController` from `UINavigationController`. * Updated `Display` to `DisplayStyle` and renamed corresponding properties, in `ToolbarController`, `StatusBarController`, `SearchBarController`, `CaptureController`, and `ImageCard`. ## 2.6.3 * Fixed an issue where using child view controllers would break `presenting` and `dismissing` transition animations. ## 2.6.2 * [issue-608](https://github.com/CosmicMind/Material/issues/608): Updated `PageTabBarController` to allow programmatic selection of the current index using the `selectedIndex` property. ## 2.6.1 * Updated for Xcode 8.3. ## 2.6.0 * [issue-704](https://github.com/CosmicMind/Material/issues/704): Fixed an issue where `TextView.clipsToBounds` was revealing the scrollable text. * [issue-663](https://github.com/CosmicMind/Material/issues/663): Added the ability to add insets for the `TextView`. * [issue-598](https://github.com/CosmicMind/Material/issues/598): Added a placeholder to the `TextView`. * Removed `Editor` and added all the necessary functionality in `TextView`. ## 2.5.2 * [issue-695](https://github.com/CosmicMind/Material/issues/695): Fixed issue with [pr-696](https://github.com/CosmicMind/Material/pull/696), where FABMenu was incorrectly being displayed used the SpringAnimation API. ## 2.5.1 * [issue-681](https://github.com/CosmicMind/Material/issues/681): Fixed regression where `NavigationBar` was not properly setting the background color. * Fixed issue where transitions using `UINavigationController` would sometimes flicker. ## 2.5.0 * Renamed `FabButton` to `FABButton`. * Moved `Menu` to `FABMenu`. * Moved `MenuController` to `FABMenuController`. * Added `FABMenuBacking` enum type to set a `blur` or `fade` for the opened `FABMenu` state using a `FABMenuController`. * Renamed `MenuItem` to `FABMenuItem`. * Added `SpringAnimation` animation API. * [issue-641](https://github.com/CosmicMind/Material/issues/641): Added a new `PulseAnimation.tap` type, which has an instant feedback response when tapping. * Updated `Toolbar.display` to `Toolbar.toolbarDisplay`. * Updated `SearchBar.display` to `SearchBar.searchBarDisplay`. * Added `StatusBar.statusBarDisplay`. * Added `MotionAnimation` enum type and helper methods to `CALayer` and `UIView` as extensions. * Added [Motion](https://github.com/CosmicMind/Motion) to Material. ## 2.4.19 * [issue-692](https://github.com/CosmicMind/Material/issues/692): Fixed issue where Carthage was failing to build the macOS target for Material. ## 2.4.18 * Removed macOS support. ## 2.4.17 * Updated Material.podspec to fix potential macOS issue. ## 2.4.16 * [issue-678](https://github.com/CosmicMind/Material/issues/678): Fixed issue where `Card` was incorrectly displaying its `contentView`. ## 2.4.15 * Fixed an issue where the `Card` was not displaying the `contentView` and `presenterView` correctly on initial load. ## 2.4.14 * Added missing `UIView` properties to `Material+UIView` extension that are used in the [Motion](https://github.com/CosmicMind/Motion) [PhotoCollection](https://github.com/CosmicMind/Samples/tree/master/Motion/PhotoCollection) sample. ## 2.4.13 * Added [Motion](https://github.com/CosmicMind/Motion) as a separate framework from [Material](https://github.com/CosmicMind/Material). ## 2.4.12 * [issue-676](https://github.com/CosmicMind/Material/issues/676): Fixed an issue where `Card` types were not adjusting size correctly when using `UILabels`. ## 2.4.11 * [issue-658](https://github.com/CosmicMind/Material/issues/658): Added `TextFieldPlaceholderAnimation` enum type to enable `TextField` to have different animations. ## 2.4.10 * Fixed an issue where `TabBar` was not correctly setting the `contentEdgeInsets*` and `interimSpace*`. ## 2.4.9 * [issue-655](https://github.com/CosmicMind/Material/issues/655): Updated date for release in README to 2017. ## 2.4.8 * [issue-653](https://github.com/CosmicMind/Material/issues/653): Fixed a `TextField` issue where the animation was not correctly responding to a programmatic text update. ## 2.4.7 * Updated README release dates. ## 2.4.6 * [issue-640](https://github.com/CosmicMind/Material/issues/640): Fixed an issue where `TextField` was not laying out correctly when setting the text programmatically. ## 2.4.5 * [issue-646](https://github.com/CosmicMind/Material/issues/646): Fixed an issue where calling `TextField.becomeFirstResponder` in `viewDidLoad` would cause a layout issue. ## 2.4.4 * [issue-644](https://github.com/CosmicMind/Material/issues/644): Fixed issue where `Switch.isOn` and `Switch.switchState` were not updating correctly. ## 2.4.3 * [issue-643](https://github.com/CosmicMind/Material/issues/643): Fixed an issue where `Layout` was not anchoring correctly for `View` subclasses. ## 2.4.2 * Fixed double `Menu.hitTest` call that was causing the delegation method to be fired more than once. * Updated `Toolbar.title`, `Toolbar.titleLabel`, `Toolbar.detail`, and `Toolbar.detailLabel` to be `@IBInspectable`. ## 2.4.1 * [issue-194](https://github.com/CosmicMind/Material/issues/194): Fixed an issue where hitTest was failing after translation animation. * [issue-624](https://github.com/CosmicMind/Material/issues/624): Updated `Switch` & `TabBar` control to only call the delegation methods when the control is updated through a user interaction. * Renamed `Switch.on` to `Switch.isOn`. * Removed `Switch.setOn` function. * [issue-630](https://github.com/CosmicMind/Material/issues/630): Added `Reminders` and `RemindersController`. * Added `isDividerHidden` for the `Divider UIView` extension. ## 2.4.0 * [issue-551](https://github.com/CosmicMind/Material/issues/551): Fixed issue where `TabBar` was not laying out buttons correctly when more than 6 were used. * [issue-618](https://github.com/CosmicMind/Material/issues/618): Updated `Grid` and `Layout` to solve a `Toolbar` layout challenge. * [issue-620](https://github.com/CosmicMind/Material/issues/620): Fixed issue where setting the `CAAnimation.delegate` was causing an animation issue. ## 2.3.22 * [issue-597](https://github.com/CosmicMind/Material/issues/597): Fixed an issue where `NavigationBar` was not adjusting to all sizes correctly when using modal presentation styles. * Fixed an issue when `cornerRadius` was not being calculated correctly when the `CALayer` was rotated. ## 2.3.21 * [issue-612](https://github.com/CosmicMind/Material/issues/612): Fixed issue where SnackbarController was not resizing correctly. * [issue-615](https://github.com/CosmicMind/Material/issues/615): Added `snackbarEdgeInsets` and `snackbarEdgeInsetsPreset` to position the `Snackbar` from the `SnackbarController's` edges. ## 2.3.20 * [issue-613](https://github.com/CosmicMind/Material/issues/613): Fixed an issue where the Grid system was not laying out in all cases that it should. * Removed `statusBarStyle` and `isStatusBarHidden` properties from `RootController` types, in favor of using the `Application` class. `StatusBarController` now provides `statusBarStyle` and `isStatusBarHidden` properties. ## 2.3.19 * [issue-553](https://github.com/CosmicMind/Material/issues/553): Fixed an issue where the `NavigationDrawerController.leftViewController` was sizing incorrectly. ## 2.3.18 * [issue-610](https://github.com/CosmicMind/Material/issues/610): Fixed issue where the `RootController.rootViewController` was not transitioning correctly when using certain UIViewController types. ## 2.3.17 * Added the ability to modify the `contentViewAlignment` of a `NavigationItem` dynamically. * Renamed the `ContentViewAlignment.any` value to `ContentViewAlignment.full`. ## 2.3.16 * Minor updates to `Card` types for code clarity. ## 2.3.15 * Fixed issue where `ImageCard` was not ordering the `UIImageView` behind the `Toolbar` correctly. ## 2.3.14 * Fixed an issue where iOS animations with `Motion` were not correctly writing their end value using `CAAnimationDelegate`. ## 2.3.13 * Fixed an issue where the `NavigationBar.backButton` was not placed at the most left position when present. ## 2.3.12 * Updates `Motion.translation*` to `Motion.translate*`. * [issue-595](https://github.com/CosmicMind/Material/issues/595): Fixed issue where CAAnimations for iOS 10 were not working correctly. * [issue-600](https://github.com/CosmicMind/Material/issues/600): Fixed issue where `Carthage` was not able to build due to failing to recognize the `NavigationDrawerController` gesture recognizer. ## 2.3.11 * [issue-600](https://github.com/CosmicMind/Material/issues/600): Fixed issue where `Carthage` was not able to build due to failing to recognize the `NavigationDrawerController` gesture recognizer. ## 2.3.10 * [issue-583](https://github.com/CosmicMind/Material/issues/583): Fixed issue where `TextField.detail` was not being displayed unless it was set upon preparation time. * [issue-594](https://github.com/CosmicMind/Material/issues/594): Added feature request to set the `TextField.leftView` coloring based on the `normal` or `active` states, using the `TextField.detail .leftViewNormalColor` and `TextField.detail .leftViewActiveColor` properties respectively. ## 2.3.9 * [issue-584](https://github.com/CosmicMind/Material/issues/584): Added enum types for `Device.model` value. * Divided `Device` into `Application`, `Device`, and `Screen`. This is for expansion of their APIs. * Fixed issue where `NavigationDrawer` and `RootController` types would conflict when showing the statusBar. ## 2.3.8 * [issue-588](https://github.com/CosmicMind/Material/issues/588): removed memory leaks that surround Grid. * `Grid` is now a struct from a class type. * `Divider` is now a struct from a class type. * `Pulse` is now a struct and views that have pulse now conform to the `Pulseable` protocol. ## 2.3.7 * [issue-592](https://github.com/CosmicMind/Material/issues/592): fixed `NavigationDrawerController` regression, where transitioning the `rootViewController` would go behind the `contentViewController`. ## 2.3.6 * Updated blur logic. ## 2.3.5 * [issue-591](https://github.com/CosmicMind/Material/issues/591): Fixed `blur` calculation complexity for `Carthage` compilation. ## 2.3.4 * [issue-588](https://github.com/CosmicMind/Material/issues/588): Fixed GridAxis memory leak issue. ## 2.3.3 * [issue-568](https://github.com/CosmicMind/Material/issues/568): fixed issue where `placeholderActiveColor` was not being set correctly when `TextField` was in an active state. * [issue-568](https://github.com/CosmicMind/Material/issues/568): fixed issue where setting `text` to `nil` broke the `TextField` layout. * [issue-581](https://github.com/CosmicMind/Material/issues/568): Added `UIImage.blur` extension. ## 2.3.2 * Fixed [issue-557](https://github.com/CosmicMind/Material/issues/577) where the transitioned view controller was overlapping the `RootController` type controls. ## 2.3.1 * Added `Capture` delegation methods that notify when `videoOrientation` changes. ## 2.3.0 * Merged in [PR-566](https://github.com/CosmicMind/Material/pull/566) to move `CAAnimation` String constants to enum types. * Renamed the `Animation` struct to `Motion`, in order to initiate the expansion of the Motion library within Material. * Fixed [issue-573](https://github.com/CosmicMind/Material/issues/573) where sample had incorrect spelling for `Material`. * Updated `Bar` type layout mathematics. * Updated `FabButton` default `backgroundColor` to `white`. * Updated `Capture` API with [sample project](https://github.com/CosmicMind/Samples/tree/master/Material/Programmatic/CaptureController). ## 2.2.5 * Merged in [PR-563](https://github.com/CosmicMind/Material/pull/563) for [issue-549](https://github.com/CosmicMind/Material/issues/549), where Privacy related features are now using CocoaPods subspecs. ## 2.2.4 * Fixed issue where `ImageCard` `top` and `bottom` `EdgeInsets` were not being applied correctly. ## 2.2.3 * Updated Card internals for better performance. * Added sample [CardTableView](https://github.com/CosmicMind/Samples/tree/master/Graph/CardTableView). ## 2.2.2 We moved all sample projects to a separate repo named [Samples](https://github.com/CosmicMind/Samples) to allow their development to be independent of the Material framework. There has been instances where we needed to update the versions of the framework to accommodate changes that only occurred in the sample projects. ## 2.2.1 * Fixed recursion issue with `Snackbar` and reloading. * issue-552: Removed extraneous @objc declarations. * Added `dividerContentEdgeInsets` and `dividerContentEdgeInsetsPreset` to Material+UIView extension. ## 2.2.0 * Updated default `pulseColor` to `Color.grey.base`. * Added `HeightPreset` type to dynamically set the height of the `CALayer` & `UIView` types. * `UIImage.tintWithColor(color: UIColor)` is now `UIImage.tint(with color: UIColor)` and always returns with `.alwaysOriginal` rendering mode. * `UIImage.imageWithColor(color: UIColor)` is now `UIImage.image(with color: UIColor)` and always returns with `.alwaysOriginal` rendering mode. * Merged in [PR 544](https://github.com/CosmicMind/Material/pull/544#pullrequestreview-3892111), which allows for the left view to be used in the `TextField`. * Updated the `TextField` example project to reflect [PR 544](https://github.com/CosmicMind/Material/pull/544#pullrequestreview-3892111). * Reworked `TextField`. * Added `contentEdgeInsets` to `Divider`. * Fixed issue where `pulse layer` was covering `Button` images when engaged. * For `Divider`, renamed `dividerHeight` to `dividerThickness` to accommodate alignment logic. * Added SearchBar delegation methods for when text changes and for when the text has been cleared. ## 2.1.2 * Updated default `pulseColor` to `Color.white`. * Updated [NavigationDrawerController Example Project](https://github.com/CosmicMind/Material/tree/master/Examples/Programmatic/NavigationDrawerController) to demonstrate how to transition the `rootViewController`, both with a `ToolbarController` and without, issue-546. ## 2.1.1 * Moved the Switch `trackLayer` property from a `CAShapeLayer` to a `UIView` that is now named `track`. * Fixed an issue where `Switch` in a `Bar` type or `NavigationBar` would behave incorrectly, issue-540. * Added a `ToolbarController` to the programmatic `NavigationDrawerController` example project. ## 2.1.0 * Added a new feature where `TextField`'s `placeholder` can be fixed at the top and not animated by setting the `isPlaceholderAnimated` property to `false` (issue-534). * Added a new feature where a `centerViews` property is added to `Bar` types, to automatically align views in the center outward positions. Updated sample `Card` projects and `Bar` projects to reflect this. * Added a new feature to `Grid`, where `columns` and `rows` are automatically set if their values have not been changed from the default `0` value. ## 2.0.0 * Renamed `MaterialColor` to `Color`. * Renamed `MaterialIcon` to `Icon`. * Renamed `MaterialSpacing` to `InterimSpacePreset`. * Renamed `MaterialButton` to `Button`. * Renamed `MaterialView` to `View`. * Renamed `MaterialPulseView` to `PulseView`. * Renamed `MaterialSwitch` to `Switch`. * Renamed `MaterialLayer` to `Layer`. * Renamed `MaterialFont` to `Font. * Renamed `MaterialEdgeInset` to `EdgeInsetsPreset`. * Renamed `MaterialDevice` to `Device`. * Renamed `MaterialDepth` to `Depth`. * Renamed `CaptureView` to `Capture`. * Renamed `CardView` to `Card`. * Renamed `ImageCardView` to `ImageCard`. * Renamed `MaterialBorder` to `BorderWidthPreset`. * Renamed `MaterialRadius` to `CornerRadiusPreset`. * Renamed `MaterialDataSourceItem` to `DataSourceItem`. * Renamed `MaterialTableViewCell` to `TableViewCell`. * Renamed `shadowPathAutoSizeEnabled` to `isShadowPathAutoSizing`. * Fixed issue where TextField placeholder was not respecting initial vertical offset (issue-469). * Added @objc to all enums to allow Obj-C to see the enum types and associated methods (issue-472). * Added `PageTabBarController`. * Added `JSON` to simplify working with JSON objects. ## 1.42.9 * Fixed issue where `textColor` was not being respected in Storyboards for `TextField` (issue-487). ## 1.42.8 * Fixed issue where initially setting the `TextField` did not correctly align the `placeholder` offset (issue-469). ## 1.42.7 * Fixed issue where `TextField` alignment was incorrect with RTL (issue-456). ## 1.42.6 * Fixed issue where `StatusBarController` was not calling its `super.prepareView` method. ## 1.42.5 * Fixed issue where NavigationBar was not aligning correctly with Storyboards. * Updated `CGRectZero` values to `CGRect.zero`. * Minor cleanups. ## 1.42.4 * Fixed issue where left and right view controllers are not enabled on `NavigationDrawerController` (issue-452). ## 1.42.3 * Fixed an issue where `MaterialSwitch` was exposing a memory leak with its `delegate` (issue-449). * Fixed a regression where FabButton was losing shape (issue-450). * Updated property `enableHideStatusbar` to `enableHideStatusBar` for `NavigationDrawerController`. * Updated `CGRectZero` to `CGRect.zero`. ## 1.42.2 * Fixed an issue where `Toolbar` and `NavigationBar` `title` alignment was off with a `detail` value of "" (issue-445). * Fixed an issue where `NavigationDrawerController` was crashing when transitioning between `rootViewController` (issue-444). ## 1.42.1 * Fixed issue with NavigationDrawerController rightView not aligning correctly when rotating device. ## 1.42.0 * Update `shadowPath` animation to happen when laying out subviews, rather than when laying out sublayers. * Renamed `MaterialLayout` to `Layout` for simplicity. * Added `Layout` extension to ease the usage of AutoLayout. * Added [Layout Documentation](http://www.cosmicmind.io/material/layout). * Updated the `MaterialLayout` project to `Layout`. * fixed width issue for all `ControlView` types when using dynamic `intrinsicContentSize` (issue-436). * Renamed `SideNavigationController` to `NavigationDrawerController`. * Removed `SideNavigationController` example project for both programmatic and storyboards. * Added `NavigationDrawerController` example project for both programmatic. * Added `StatusBarController` to manage a statusBarView. ## 1.41.8 * Fixed an issue where the `UINavigationItem.title` KVO would crash when releasing the instance. The fix completely removes KVO and utilizes Swift `nonobjc` tagging. ## 1.41.7 * Added `adjustOrientationForImage` to `CaptureSession` in order to fix image alignment issues. * Updated `CaptureView` sample project to reflect changes made in `ToolbarController`. * Updated `MaterialView` sample project to demonstrate aligning a `MaterialView` in the `center` of a view controller. * Fixed issue where UINavigationItem.title was not updating the titleLabel text. ## 1.41.6 * Fixed issue with ellipses in NavigationBar showing when panning back to the backItem (pr-409). ## 1.41.5 * Added delegation method `menuViewDidTapOutside` to `MenuView` to support closing the `Menu`, `MenuView`, or `MenuController` items when opened and clicking on any area of the view (issue-406). ## 1.41.4 * Removed `statusBarStyle` from `BarView` types. * Added `statusBarStyle` to BarController types. * Added `layoutInset` to `Grid` for an additional layer of flexibility. * Fixed an issue where BarControllers were not allowing `contentInset.top` to be used correctly. * Updated projects to reflect framework changes. ## 1.41.3 * Fixed issue where `MaterialSwitch` was referencing self and creating a bad access (issue-399). * Fixed issue where `TextField.secureTextEntry` would break the font being displayed (issue-400). * Moved `MenuViewController` to `MenuController`. * Updated `MenuController.itemViewSize` to `MenuController.itemSize`. * Updated `MenuController.baseViewSize` to `MenuController.baseSize`. * Updated all references to `unowned self` to `weak self`. * Added convenience properties `title` and `detail` to the `Toolbar`, which reload the view when changed. * Added convenience properties `title` and `detail` to UINavigationItem to easily handle text changes. * added to `TextField` the `placeholderVerticalOffset` and `detailVerticalOffset` to determine the alignment during animations and loading of the `placeholderLabel` and `detailLabel`. ## 1.41.2 * Fixed issue where Toolbar was not respective the frame size set (issue-382). * Fixed issue where Toolbar was not drawing the titleLabel and detailLabel text without left/right controls (issue-381). * `StatusBarView` is now `BarView`. * `StatusBarViewController` is now `BarViewController`. ## 1.41.1 * Fixed text alignment issue in NavigationBar and Toolbar. ## 1.41.0 * All references to `detailView` are now `contentView`. * Updated NavigationBar interface. * Reworked NavigationBar. * Reworked Toolbar measurements. * Reworked SearchBar measurements. ## 1.40.1 * Fixed issue where initializing a Toolbar in a method was causing an ambiguous initializer error (issue-363). * Added Boolean properties to SideNavigationController to enable and disable gestures (issue-365). ```swift sideNavigationController.enabled = true sideNavigationController.enabledLeftView = true sideNavigationController.enabledLeftTapGesture = true sideNavigationController.enabledLeftPanGesture = true sideNavigationController.enabledRightView = true sideNavigationController.enabledRightTapGesture = true sideNavigationController.enabledRightPanGesture = true ``` * Updated the SideNavigationController `leftThreshold` and `rightThreshold` to 64 as a default. * Updated MaterialIcon images to work better with CocoaPods (issue-362). ## 1.40.0 * Added Google visibility icon to MaterialIcon. * Added Google check icon to MaterialIcon. * Reworked TextField with [documentation](http://www.cosmicmind.io/material/textfield). * Added ErrorTextField. * Added visibility button and clear button auto enabling without conflicting with iOS clearButton for TextField. * Reworked pulse animations. * Added `PulseAnimation` enum type to select the type of pulse animation. * Added `IconButton` to simplify the usage of using icons and buttons. * Fixed issue where panning gestures were conflicting with the SideNavigationController rootViewController (1ssue-322, issue-320). ## 1.39.17 * Updated MaterialDepth to more accurately express Material Design's shadows (issue-323). * Fixed an issue where MaterialButtons could not update `textColor` (issue-333). ## 1.39.16 * Fixed issue where TextField `resignFirstResponder` was not hiding the `titleLabel` (issue-332). * TextField no longer needs to setup `detailLabel` property. * TextField `detailLabel` now supports @IBInspectable. ## 1.39.15 * Fixed issue where TextField doesn't hide the titleLabel when programmatically cleared (issue-330) (pr-331). ## 1.39.14 * Added UIImage extension `tintWithColor`, which allows an image to be tinted with a passed in color. * Added `pulseCenter` property, which forces the pulse animation to animate from the center of the view (pr-325). * Updated `prepareView` to be public, which allows for better subclassing and preparation of views (pr-329). * Fixed issue where TextField regressed when updating the `placeholder` value (issue-316). ## 1.39.13 * Fixed issue where TextField `placeholder` could be updated while a text value exists (issue-316). ## 1.39.12 * Updated Example/Programmatic/SideNavigationController project to demonstrate how to transition the rootViewController (issue-309). ## 1.39.11 * Added a link to download our sticker sheet. * Updated App project with correct naming in AppDelegate file. ## 1.39.10 * README Update. ## 1.39.9 * Added storyboard CardView example with two CardViews (issue-304). ## 1.39.8 * Fixed issue where TextField animation references `unowned self` and should be `weak self` (issue-301) (pr-302). * Added `lineLayerThickness` and `lineLayerActiveThickness` to TextField in order to adjust lineLayer during different states (issue-307). ## 1.39.7 * Fixed issue where TextField delegate method `textFieldShouldClear` was not being respected (issue-296). ## 1.39.6 * Updated TextField's default colors to the correct Material Design colors (pr-290). * Added UIImage blur effect (pr-291). * Added UIImage blur example project, FilterBlur. * Added `SideNavigationController.statusBarUpdateAnimation` property to set the animation type when hiding the statusBar. * Added `SideNavigationController.statusBarStyle` property to set the statusBar style. ## 1.39.5 * Added MaterialIcon example project. * Added additional Google and CosmicMind icons. * Added MaterialFontLoader to aid in loading packaged fonts with Material. * Updated App project to properly handle SideNavigationController ```openLeftView``` if used. ## 1.39.4 * Updated the TextField animations. ## 1.39.3 * Fixed bundle identifier issue with CocoaPods and MaterialIcon. ## 1.39.2 * Updated Material bundle identifier. ## 1.39.1 * Updated TextField to match Material Design input text spec. ## 1.39.0 * Added early release of TabBar. * Updated default `spacing` to `Spacing1` for Toolbar and SearchBar. * Updated default `contentInsetPreset` to `Square1` for Toolbar, SearchBar, and NavigationBar. * Updated ```MaterialGravityToString``` to ```MaterialGravityToValue```. * TextField's ```clearButton``` no longer needs to be setup. * TextField's ```titleLabel``` no longer needs to be setup (issue-241). * TextField and SearchBar - added ```clearButtonAutoHandleEnabled``` flag that when set to ```false```, the handler for clearing text is removed (issue-229). * TextField's ```titleLabelAnimationDistance``` is now 4 by default. * TextField's ```titleLabel``` now floats when focused (issue-203). * TextField's ```bottomBorderLayerDistance``` is now ```lineLayerDistance```. * TextField now supports the ```lineLayerActiveColor```, which is set when the TextField is focused. * TextField now supports the ```lineLayerDetailActiveColor```, which is set when the TextField detailLabel is displayed. * Fixed issue-203, where TextField's ```lineLayer``` property was not the proper thickness when active. * Fixed regression, where the TextField's ```bottomLayer``` color was not showing (issue-276). * Removed shape code that was not needed in TextField and TextView. * Updated example projects using SearchBar, and simplified the configuration for the SearchBar. * Added OSX build target. * Added MaterialColor to OSX. * Added Google icons as default for ```MaterialIcon``` and additional CosmicMind icons with ```cm``` namespace (issue-285). ## 1.38.5 * Fix for MaterialIcon, [issue-279](https://github.com/CosmicMind/Material/issues/279). * Fix for touch consumption, [issue-280](https://github.com/CosmicMind/Material/issues/280). ## 1.38.4 * Added custom icons to MaterialIcon. ## 1.38.3 * Updated App example project. * Added MaterialCollectionView example project. * Added NavigationController storyboard example project. * Default colors for MaterialSwitch moved from MaterialColor.lightBlue.* to MaterialColor.blue.*. * Updated MaterialColor with corrections to the colors from Material Design (issue-271). ## 1.38.2 * Switched BottomNavigationBar to BottomTabBar, as it is more appropriate in the context of iOS. BottomNavigationController will be comprised of a SnackBar (coming soon), and BottomTabBar. * Added fix for Interface Builder, where MaterialSwitch was causing crashes (issue-259). * Fixed issue where MaterialSwitch color was not propagating until engaged (issue-260). ## 1.38.1 * Added missing imports for UIKit in MaterialCollectionView classes. ## 1.38.0 * Added BottomNavigationBar. * Added default Fade animation to BottomNavigationBar. * Added programmatic BottomNavigationBar example project. * Added storyboard BottomNavigationBar example project. * Added BottomNavigationController. * Added programmatic BottomNavigationController example. * Removed c-style ```for``` loops. * Updated bundle detection for MaterialIcon. * References for `mainViewController` are now `rootViewController`. * References for `transitionFromMainViewController` are now `transitionFromRootViewController`. * The example App project demonstrates how to hide the `statusBar` without the `navigationBar` being animated. * Fixed warnings for Swift 3. * Added detection for latest iPad and iPhone. ## 1.37.3 * Fixed issue-244, where Test Coverage flag was causing an error. ## 1.37.2 * Updated the default width of the SideNavigationController to reflect the Material Design spec more accurately. For mobile, the default width is 280px, and for tablet, the default width is 320px. ## 1.37.1 * Removed SideNavigationControllerDelegate call from NavigationController class that was not used. ## 1.37.0 * Added `pulseFocus` Boolean flag that keeps the pulse displayed as the button is highlighted (issue-217). * Switched `pulseColorOpacity` to `pulseOpacity`. * Added @IBInspectable where appropriate (issue-151). * Added @IBDesignable where appropriate (issue-151). * Fixed an issue where the tests were being broken (issue-224). * `NavigationBarView` is now called `Toolbar`. * `SearchBarView` is now called `SearchBar`. * `SideNavigationViewController` is now `SideNavigationController`. * Added public `leftThreshold` and `rightThreshold` to `SideNavigationController` (issue-220). ## 1.36.0 * Added class NavigationBar and NavigationController. * Added contentsGravityPreset where appropriate, and now contentsGravity takes a String. * Fixed issue-213, `shadowPathAutoSizeEnabled` now defaults to true, from false. ## 1.35.3 * MaterialAnimation.rotate now accepts an `angle` parameter or a `rotation` parameter. * Fixed issue-194, where the MaterialAnimation.translation animations were not capturing the final state value. * Fixed issue-201, by guaranteeing that the minimal iOS version is 8. ## 1.35.2 * Updated storyboard NavigationBarView example project to correctly set the height of the NavigationBarView when rotating orientations on both iPad and iPhone. * Removed NavigationBarViewDelegate and SearchBarViewDelegate. ## 1.35.1 * Fixed issue-195, where MaterialSwitch was switching the switchState automatically through the highlighted property. * Fixed issue-196, where NavigationBarView was not sizing correctly on iPad. * Added MaterialDevice helper class that provides useful values for orientation and bounds size. ## 1.35.0 * Removed backDropLayer for SideNavigationViewController. Now, the mainViewController.view property is set to 50% alpha when opened and 100% alpha when closed. The color is adopted from the backgroundColor of the mainViewController.view property. * MenuViewController now animates its backdrop effect. ## 1.34.10 * Added `shadowPathAutoSizeEnabled` property that enables auto sizing for the shadowPath property. Defaults to false. ## 1.34.9 * Fixed shadowPath animation on rotation. * Proposed fixed for NavigationBarView geometry issue. * Updated CardView and ImageCardView to have a default rounded corner of .Radius1 as in the Material Design spec. * Updated the example App project. ## 1.34.8 * Updated example App project. ## 1.34.7 * Updated App project example, by showing how to switch NavigationBarViewController's mainViewController from a SideNavigationViewController. * Added a FeedViewController to the example App project that shows how to use CardViews in a MaterialCollectionView. * Fixed a performance issue with shadows and animations, issue-186. ## 1.34.6 * Fixed issue with MaterialSwitch, issue-181. * Fixed issue with NavigationBarView Geometry, issue-179. * Added MaterialCollectionView, MaterialCollectionViewLayout, MaterialCollectionViewCell. * Added enum MaterialSpacing type for spacing presets, spacingPreset. * Added TextField storyboard example. * Rework to better the performance of animations with depth. ## 1.34.5 * Fixed breaking examples. ## 1.34.4 * Added **NavigationBarViewControllerDelegate** that monitors the state of the `floatingViewController` property. * Added **TextField** `detailLabelAutoHideEnabled`property that flags the animation to hide the detailLabel automatically when the text value has changed. Default is true. ## 1.34.3 * Added headers to public build phase. ## 1.34.2 * License text update. ## 1.34.1 * CaptureView example has been updated to use the latest NavigationBarView API. * NavigationBarView default sizing: - Only titleLabel, font size is 20. - With titleLabel and detailLabel, font size is 17 for titleLabel, and 12 for detailLabel. * Example projects updated. ## 1.34.0 * Added App example project in Examples/Programmatic. * Added ControlView. * Added StatusBarView. * NavigationViewController is now NavigationBarViewController. * Added StatusBarViewController. * Added SearchBarViewController. * MaterialSwitch now supports setting the "on", "selected", "highlighted", and "switchState" properties to toggle the state of the control. * MaterialSwitch now supports setOn(on: Bool, animated: Bool) method to switch the state of the control. * MaterialSwitch now supports 'on', 'highlighted', 'selected', 'state', and 'switchState' public mutators. * MaterialSwitchDelegate updated 'materialSwitchStateChanged' delegation method to only pass a reference to the control, rather than control and state value. * Added MenuViewController. * SideNavigationBarViewController is now SideNavigationViewController. * UIViewController Optional property sideNavigationBarViewController is now sideNavigationViewController. * Added TextField placeholderTextColor property to set the placeholder text color. * TextField detailLabel property hides automatically when typing. * TextField now supports a custom clear UIButton by setting the clearButton property. ## 1.33.2 * Updated SerchBarView Example. * A rotation issue is fixed when in landscape mode and toggling the SideNavigationViewController, where the statusBar would * Added MaterialSwitch UIControl component with example projects in the Examples/Programmatic directory. * MaterialEdgeInsetPreset is now MaterialEdgeInset. * MaterialRadius preset values are now supported through the cornerRadiusPreset property. The cornerRadius property now supports CGFloat values directly. * Updated TableCardView example. * Added SearchBarView. * Updated NavigationBarView API. * Added NavigationViewController. * Updated TextField and TextView issue, where letters such as "y g p" would not display correctly. * MaterialButton has updated contentInsetPreset to contentEdgeInsetsPreset. * Added MaterialTableViewCell with pulse animation. * Updated SideNavigationViewController example project. ## 1.32.2 * Fixed an issue with MenuView, where outside views were not detected when touched. * Updated API to reference views. ## 1.32.1 * MenuView wraps a Menu with a MaterialPulseView to ease the use of laying out menus, as well as, provide a more robust approach to Menus. * Menus now hold an array of UIViews, allowing any UIView to be animated with Menu. * The borderWidth property for Material views no longer uses an enum MaterialBorder type. It now supports the CGFloat type. ## 1.32.0 * CardView and ImageCardView no longer support the detailLabel property. Now, a detailView property has been added to allow any UIView, including UILabel to be added in the detail area. ## 1.31.6 * Grid now supports management of rows and columns in a single mapping. * Updated Grid examples. * Visit the Examples directory to see example projects using Material. ## 1.31.5 * Updated README. ## 1.31.4 * Updated Grid Example. * Updated README. ## 1.31.3 * Updated Material usage description. ## 1.31.2 * Introducing Material Grid. A flexible grid system to handle complex layouts. * Default depth is now .Depth1. ## 1.31.1 * Added two more examples using Menu. ## 1.31.0 * Added a new component, Menu! * A Menu manages a group of UIButtons that may be animated open in the Up, Down, Left, and Right directions. The animations ## 1.30.2 * Fixed an issue where toggling the SideNavigationViewController enabled property caused the 'left' and 'right' view to stop ## 1.30.1 * Updated MaterialView example. ## 1.30.0 * Updated pulse animation to be a wave. * Added `pulse` method to programmatically trigger the pulse animation. * Updated examples to reflect pulse changes. * Updated README to reflect pulse changes. * Removed `pulseFill` and `spotlight` from pulse animatable views as they are no longer needed. ## 1.29.4 * Some minor updates to the internal references used for animations. * Removed Task example to its own repository. ## 1.29.3 ***This update is recommended.*** * Major update to SideNavigationViewController internals and animation. * Added an additional duration parameter to setLeftViewWidth with animation and setRightViewWidth with animation. ## 1.29.2 ***This is a recommended update.*** * Updated internal animations to handle UIView animations correctly. * Updated SideNavigationViewController to allow quick swipe to close. * Updated SideNavigationViewController to not block view touches when tap gesture is triggered. * Updated workspace reference to Material project. ## 1.29.1 * To remove deprecated warnings, and provide an overall better solution, UIImage now utilizes NSURLSession for asynchronous image loading. * The MaterialLayer example project demonstrates this feature update. ## 1.29.0 * SideNavigationViewController now supports the Right position for View Controllers. * SideNavigationViewController API updates should be reviewed from the Class file. ## 1.28.1 * Updated README. ## 1.28.0 * Updated framework name to Material to avoid conflicts with other frameworks. * Updated default TextField animation distance for titleLabel to 8. * Updated default TextView animation distance for titleLabel to 8. ## 1.27.14 * The SideNavigationViewController example project has been updated. * Updated README. ## 1.27.13 * An updated SideNavigationViewController example has been added to the Examples Programmatic directory. It includes a fresh new look and a reusable template for your applications. ## 1.27.12 * Updates to the TextView and TextField were made to follow Google's spacing guidelines. * An issue with setting the TextView or TextField's text property has been fixed, where the title label was not being * Additional updates were made to many of the example projects. ## 1.27.11 * Updated License Format. ## 1.27.10 * Updated License Format. ## 1.27.9 * Material is moving to a BSD license. ## 1.27.8 * Added @objc to TextView and TextViewDelegate to eliminate conflicts when using swift within an Objective-C project. Below is an example of a medium CardView using Grid. ## 1.27.7 * Fixed an issue where the optional TextView.text property was being accessed and throwing an error when used in combination with Objective-C. ## 1.27.6 * Updated TextView internals to avoid optimization build issue with Carthage. * Updated icon set in example projects. * Updated Resources folder with latest graphics. * Updated README. ## 1.27.4 * Removed the File Class. * Updated README to correct TextField example. Right out of the box to a fully customizable configuration, CardView always stands out. Take a look at a few examples in action. ### 1.27.2 * **titleLabelTextColor** is now **titleLabelColor**. * **titleLabelActiveTextColor** is now **titleLabelActiveColor**. * **detailLabelActiveTextColor** is now **detailLabelActiveColor**. * **titleLabelTextColor** is now **titleLabelColor**. * **titleLabelActiveTextColor** is now **titleLabelActiveColor**. ## 1.27.1 * Updated README to include Changelog link. ## 1.27.0 * Has been removed completely. * **MaterialEdgeInsets** is now **MaterialEdgeInsetPreset**. * **contentInsets** is now **contentInsetPreset**. * **contentInsetsRef** is now **contentInset**. * **dividerInsets** is now **dividerInsetPreset**. * **dividerInsetRef** is now **dividerInset**. * **contentInsets** is now **contentInsetPreset**. * **contentInsetsRef** is now **contentInset**. * **titleLabelInsets** is now **titleLabelInsetPreset**. * **titleLabelInsetsRef** is now **titleLabelInset**. * **detailLabelInsets** is now **detailLabelInsetPreset**. * **detailLabelInsetsRef** is now **detailLabelInset**. * **leftButtonsInsets** is now **leftButtonsInsetPreset**. * **leftButtonsInsetsRef** is now **leftButtonsInset**. * **rightButtonsInsets** is now **rightButtonsInsetPreset**. * **rightButtonsInsetsRef** is now **rightButtonsInset**. * **dividerInsets** is now **dividerInsetPreset**. * **dividerInsetRef** is now **dividerInset**. * **contentInsets** is now **contentInsetPreset**. * **contentInsetsRef** is now **contentInset**. * **titleLabelInsets** is now **titleLabelInsetPreset**. * **titleLabelInsetsRef** is now **titleLabelInset**. * **detailLabelInsets** is now **detailLabelInsetPreset**. * **detailLabelInsetsRef** is now **detailLabelInset**. * **leftButtonsInsets** is now **leftButtonsInsetPreset**. * **leftButtonsInsetsRef** is now **leftButtonsInset**. * **rightButtonsInsets** is now **rightButtonsInsetPreset**. * **rightButtonsInsetsRef** is now **rightButtonsInset**. * **shrink** is now **shrinkAnimation**. * **wrapped** is no longer an Optional. * **contentsScale** is no longer an Optional. * **shrink** is now **shrinkAnimation**. * **updatedPulseLayer** is now **updatePulseLayer**. * **contentInsets** is now **contentInsetPreset**. * **contentInsetsRef** is now **contentInset**. * **titleLabelInsets** is now **titleLabelInsetPreset**. * **titleLabelInsetsRef** is now **titleLabelInset**. * **detailLabelInsets** is now **detailLabelInsetPreset**. * **detailLabelInsetsRef** is now **detailLabelInset**. * **leftButtonsInsets** is now **leftButtonsInsetPreset**. * **leftButtonsInsetsRef** is now **leftButtonsInset**. * **rightButtonsInsets** is now **rightButtonsInsetPreset**. * **rightButtonsInsetsRef** is now **rightButtonsInset**. * When setting the **enabled** property, gestures are removed and added appropriately. * **titleLabelNormalColor** is now **titleLabelTextColor**. * **titleLabelHighlightedColor** is now **titleLabelActiveTextColor**.  ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at support@cosmicmind.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Guidelines This document contains information and guidelines about contributing to this project. Please read it before you start participating. **Topics** * [Pull Request Submissions](#pull-request-submissions) * [Asking Questions](#asking-questions) * [Reporting Security Issues](#reporting-security-issues) * [Reporting Issues](#reporting-other-issues) * [Developers Certificate of Origin](#developers-certificate-of-origin) * [Code of Conduct](#code-of-conduct) ## Pull Request Submissions. All pull requests should be made to the development branch. Details should be available describing your fix. Before submitting a pull request, please confirm that merge issues are resolved. ## Asking Questions We don't use GitHub as a support forum. For any usage questions that are not specific to the project itself, please ask on [Stack Overflow](http://stackoverflow.com/questions/tagged/cosmicmind) instead. By doing so, you'll be more likely to quickly solve your problem, and you'll allow anyone else with the same question to find the answer. This also allows maintainers to focus on improving the project for others. ## Reporting Security Issues CosmicMind takes security seriously. If you discover a security issue, please bring it to our attention right away! Please **DO NOT** file a public issue, instead send your report privately to . This will help ensure that any vulnerabilities that _are_ found can be [disclosed responsibly](http://en.wikipedia.org/wiki/Responsible_disclosure) to any affected parties. ## Reporting Other Issues A great way to contribute to the project is to send a detailed issue when you encounter an problem. We always appreciate a well-written, thorough bug report. Check that the project issues database doesn't already include that problem or suggestion before submitting an issue. If you find a match, add a quick "+1" or "I have this problem too." Doing this helps prioritize the most common problems and requests. When reporting issues, please include the following: * The version of Xcode you're using * The version of iOS you're targeting * The full output of any stack trace or compiler error * A code snippet that reproduces the described behavior, if applicable * Any other details that would be useful in understanding the problem This information will help us review and fix your issue faster. ## Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: - (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or - (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or - (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. - (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. --- *Some of the ideas and wording for the statements above were based on work by the [Alamofire](https://github.com/Alamofire/Alamofire/blob/master/CONTRIBUTING.md), [Docker](https://github.com/docker/docker/blob/master/CONTRIBUTING.md) and [Linux](http://elinux.org/Developer_Certificate_Of_Origin) communities. We commend them for their efforts to facilitate collaboration in their projects.* ================================================ FILE: Cartfile ================================================ github "CosmicMind/Motion" >= 1.2.2 ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) Copyright (C) 2019, CosmicMind, Inc. . All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Material.podspec ================================================ Pod::Spec.new do |s| s.name = 'Material' s.version = '3.1.8' s.swift_version = '5.0' s.license = 'BSD-3-Clause' s.summary = 'A UI/UX framework for creating beautiful applications.' s.homepage = 'http://cosmicmind.com' s.social_media_url = 'https://www.facebook.com/cosmicmindcom' s.authors = { 'CosmicMind, Inc.' => 'support@cosmicmind.com' } s.source = { :git => 'https://github.com/CosmicMind/Material.git', :tag => s.version } s.default_subspec = 'Core' s.platform = :ios, '8.0' s.subspec 'Core' do |s| s.ios.deployment_target = '8.0' s.ios.source_files = 'Sources/**/*.swift' s.requires_arc = true s.resource_bundles = { 'com.cosmicmind.material.icons' => ['Sources/**/*.xcassets'], 'com.cosmicmind.material.fonts' => ['Sources/**/*.ttf'] } s.dependency 'Motion', '~> 3.1.1' end end ================================================ FILE: Material.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 961154CC1F32A7B100A78D74 /* ChipBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961154CB1F32A7B100A78D74 /* ChipBar.swift */; }; 961527B91F3A509900E8B2AC /* ChipBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961527B81F3A509900E8B2AC /* ChipBarController.swift */; }; 9617B07D1DFCA8CF00410F8F /* Application.swift in Headers */ = {isa = PBXBuildFile; fileRef = 961E6BDE1DDA2A95004E6C93 /* Application.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9617B07E1DFCA8CF00410F8F /* Card.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB75D1CB40DC500C806FE /* Card.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9617B07F1DFCA8CF00410F8F /* ImageCard.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7621CB40DC500C806FE /* ImageCard.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9617B0801DFCA8CF00410F8F /* PresenterCard.swift in Headers */ = {isa = PBXBuildFile; fileRef = 9631A7C01D95E3AC00CFB109 /* PresenterCard.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9617B0861DFCA8CF00410F8F /* HeightPreset.swift in Headers */ = {isa = PBXBuildFile; fileRef = 9626CB9A1DAD3D1D003E2611 /* HeightPreset.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9617B08A1DFCA8CF00410F8F /* DisplayStyle.swift in Headers */ = {isa = PBXBuildFile; fileRef = 9626CA961DAB53A8003E2611 /* DisplayStyle.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9617B08B1DFCA8CF00410F8F /* Screen.swift in Headers */ = {isa = PBXBuildFile; fileRef = 961E6BE11DDA2AF3004E6C93 /* Screen.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9617B08C1DFCA8CF00410F8F /* SearchBar.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7951CB40DC500C806FE /* SearchBar.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9617B08D1DFCA8CF00410F8F /* SearchBarController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7961CB40DC500C806FE /* SearchBarController.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9617B08E1DFCA8CF00410F8F /* TabBar.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB79A1CB40DC500C806FE /* TabBar.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9617B08F1DFCA8CF00410F8F /* Material+NSMutableAttributedString.swift in Headers */ = {isa = PBXBuildFile; fileRef = 961276621DCD8B1800A7D920 /* Material+NSMutableAttributedString.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9617B0901DFCA8CF00410F8F /* Toolbar.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB79F1CB40DC500C806FE /* Toolbar.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9617B0911DFCA8CF00410F8F /* ToolbarController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7A01CB40DC500C806FE /* ToolbarController.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9618006D1F4D384200CD77A1 /* Material+UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9618006C1F4D384200CD77A1 /* Material+UIViewController.swift */; }; 9618006E1F4D38BC00CD77A1 /* ChipBar.swift in Headers */ = {isa = PBXBuildFile; fileRef = 961154CB1F32A7B100A78D74 /* ChipBar.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9618006F1F4D38BC00CD77A1 /* ChipBarController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 961527B81F3A509900E8B2AC /* ChipBarController.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 961800701F4D38BC00CD77A1 /* Material+UIViewController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 9618006C1F4D384200CD77A1 /* Material+UIViewController.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 961E6BDF1DDA2A95004E6C93 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961E6BDE1DDA2A95004E6C93 /* Application.swift */; }; 961E6BE21DDA2AF3004E6C93 /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961E6BE11DDA2AF3004E6C93 /* Screen.swift */; }; 96328B7A1E020A41009A4C90 /* CollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96328B791E020A41009A4C90 /* CollectionViewController.swift */; }; 96328B971E05C0BB009A4C90 /* TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96328B961E05C0BB009A4C90 /* TableView.swift */; }; 96328B991E05C0CE009A4C90 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96328B981E05C0CE009A4C90 /* TableViewController.swift */; }; 96328B9B1E05C24E009A4C90 /* CollectionViewController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96328B791E020A41009A4C90 /* CollectionViewController.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96328B9E1E05C24E009A4C90 /* TableView.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96328B961E05C0BB009A4C90 /* TableView.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96328B9F1E05C24E009A4C90 /* TableViewController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96328B981E05C0CE009A4C90 /* TableViewController.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96334EF61C8B84660083986B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 96334EF51C8B84660083986B /* Assets.xcassets */; }; 9656895F1F002F16001C656D /* CardCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9656895E1F002F16001C656D /* CardCollectionViewCell.swift */; }; 965689611F002F4C001C656D /* CardCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 965689601F002F4C001C656D /* CardCollectionViewController.swift */; }; 965E80CC1DD4C50600D61E4B /* Bar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7981CB40DC500C806FE /* Bar.swift */; }; 965E80CD1DD4C50600D61E4B /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7701CB40DC500C806FE /* Button.swift */; }; 965E80CE1DD4C50600D61E4B /* FABButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB75F1CB40DC500C806FE /* FABButton.swift */; }; 965E80CF1DD4C50600D61E4B /* FlatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7601CB40DC500C806FE /* FlatButton.swift */; }; 965E80D01DD4C50600D61E4B /* RaisedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7931CB40DC500C806FE /* RaisedButton.swift */; }; 965E80D11DD4C50600D61E4B /* IconButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9658F2161CD6FA4700B902C1 /* IconButton.swift */; }; 965E80D21DD4C50600D61E4B /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7761CB40DC500C806FE /* Color.swift */; }; 965E80D31DD4C50600D61E4B /* Device.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7791CB40DC500C806FE /* Device.swift */; }; 965E80D41DD4C50600D61E4B /* Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96230AB71D6A520C00AF47DC /* Divider.swift */; }; 965E80D51DD4C50600D61E4B /* Grid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7611CB40DC500C806FE /* Grid.swift */; }; 965E80D61DD4C50600D61E4B /* HeightPreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9626CB9A1DAD3D1D003E2611 /* HeightPreset.swift */; }; 965E80D71DD4C50600D61E4B /* Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB77D1CB40DC500C806FE /* Icon.swift */; }; 965E80D81DD4C50600D61E4B /* Layer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7801CB40DC500C806FE /* Layer.swift */; }; 965E80D91DD4C50600D61E4B /* Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7811CB40DC500C806FE /* Layout.swift */; }; 965E80DA1DD4C50600D61E4B /* Border.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB76F1CB40DC500C806FE /* Border.swift */; }; 965E80DB1DD4C50600D61E4B /* InterimSpace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7871CB40DC500C806FE /* InterimSpace.swift */; }; 965E80DC1DD4C50600D61E4B /* Depth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7781CB40DC500C806FE /* Depth.swift */; }; 965E80DD1DD4C50600D61E4B /* EdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB77A1CB40DC500C806FE /* EdgeInsets.swift */; }; 965E80DF1DD4C50600D61E4B /* CornerRadius.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7851CB40DC500C806FE /* CornerRadius.swift */; }; 965E80E01DD4C50600D61E4B /* Shape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7861CB40DC500C806FE /* Shape.swift */; }; 965E80E11DD4C50600D61E4B /* Offset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 968C99461D377849000074FF /* Offset.swift */; }; 965E80E21DD4C50600D61E4B /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB78C1CB40DC500C806FE /* View.swift */; }; 965E80E41DD4C53300D61E4B /* PulseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7841CB40DC500C806FE /* PulseView.swift */; }; 965E80E51DD4C53300D61E4B /* PulseAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7821CB40DC500C806FE /* PulseAnimation.swift */; }; 965E80E71DD4C55200D61E4B /* Material+UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E3C3931D397AE90086A024 /* Material+UIView.swift */; }; 965E80E81DD4C55200D61E4B /* Material+CALayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F1DC871D654FDF0025F925 /* Material+CALayer.swift */; }; 965E80E91DD4C55200D61E4B /* Material+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7641CB40DC500C806FE /* Material+String.swift */; }; 965E80EA1DD4C55200D61E4B /* Material+UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7651CB40DC500C806FE /* Material+UIFont.swift */; }; 965E80EB1DD4C55200D61E4B /* Material+UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB76C1CB40DC500C806FE /* Material+UIImage.swift */; }; 965E80EC1DD4C55200D61E4B /* Material+Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C1C8801D42C62800E6608F /* Material+Array.swift */; }; 965E80ED1DD4C55200D61E4B /* Material+UIWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962864591D53FE3E00690B69 /* Material+UIWindow.swift */; }; 965E80F71DD4D59500D61E4B /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB75D1CB40DC500C806FE /* Card.swift */; }; 965E80F81DD4D59500D61E4B /* ImageCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7621CB40DC500C806FE /* ImageCard.swift */; }; 965E80F91DD4D59500D61E4B /* PresenterCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9631A7C01D95E3AC00CFB109 /* PresenterCard.swift */; }; 965E80FB1DD4D59500D61E4B /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7951CB40DC500C806FE /* SearchBar.swift */; }; 965E80FC1DD4D59500D61E4B /* SearchBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7961CB40DC500C806FE /* SearchBarController.swift */; }; 965E80FD1DD4D59500D61E4B /* Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB79F1CB40DC500C806FE /* Toolbar.swift */; }; 965E80FE1DD4D59500D61E4B /* ToolbarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7A01CB40DC500C806FE /* ToolbarController.swift */; }; 965E80FF1DD4D5C800D61E4B /* BottomNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7581CB40DC500C806FE /* BottomNavigationController.swift */; }; 965E81031DD4D5C800D61E4B /* CollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7711CB40DC500C806FE /* CollectionView.swift */; }; 965E81041DD4D5C800D61E4B /* CollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7721CB40DC500C806FE /* CollectionViewCell.swift */; }; 965E81071DD4D5C800D61E4B /* CollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7751CB40DC500C806FE /* CollectionViewLayout.swift */; }; 965E81081DD4D5C800D61E4B /* CollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 966ECF291CF4C20100BB0BDF /* CollectionReusableView.swift */; }; 965E81091DD4D5C800D61E4B /* DataSourceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7771CB40DC500C806FE /* DataSourceItem.swift */; }; 965E810A1DD4D5C800D61E4B /* Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB77B1CB40DC500C806FE /* Font.swift */; }; 965E810B1DD4D5C800D61E4B /* RobotoFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7941CB40DC500C806FE /* RobotoFont.swift */; }; 965E810C1DD4D5C800D61E4B /* DynamicFontType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9628645E1D540AF300690B69 /* DynamicFontType.swift */; }; 965E81101DD4D5C800D61E4B /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7901CB40DC500C806FE /* NavigationBar.swift */; }; 965E81111DD4D5C800D61E4B /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7911CB40DC500C806FE /* NavigationController.swift */; }; 965E81121DD4D5C800D61E4B /* NavigationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7921CB40DC500C806FE /* NavigationItem.swift */; }; 965E81131DD4D5C800D61E4B /* NavigationDrawerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7971CB40DC500C806FE /* NavigationDrawerController.swift */; }; 965E81161DD4D5C800D61E4B /* DisplayStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9626CA961DAB53A8003E2611 /* DisplayStyle.swift */; }; 965E81171DD4D5C800D61E4B /* TransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7991CB40DC500C806FE /* TransitionController.swift */; }; 965E81181DD4D5C800D61E4B /* Snackbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 963FBEFC1D669510008F8512 /* Snackbar.swift */; }; 965E81191DD4D5C800D61E4B /* SnackbarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961EFC571D738FF600E84652 /* SnackbarController.swift */; }; 965E811A1DD4D5C800D61E4B /* StatusBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967A48181D0F425A00B8CEB7 /* StatusBarController.swift */; }; 965E811B1DD4D5C800D61E4B /* Switch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7881CB40DC500C806FE /* Switch.swift */; }; 965E811C1DD4D5C800D61E4B /* TabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB79A1CB40DC500C806FE /* TabBar.swift */; }; 965E811D1DD4D5C800D61E4B /* TableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB7891CB40DC500C806FE /* TableViewCell.swift */; }; 965E811E1DD4D5C800D61E4B /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB79C1CB40DC500C806FE /* TextField.swift */; }; 965E811F1DD4D5C800D61E4B /* ErrorTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961F18E71CD93E3E008927C5 /* ErrorTextField.swift */; }; 965E81211DD4D5C800D61E4B /* TextStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB79D1CB40DC500C806FE /* TextStorage.swift */; }; 965E81221DD4D5C800D61E4B /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BCB79E1CB40DC500C806FE /* TextView.swift */; }; 965E81261DD4D7C800D61E4B /* Material+NSMutableAttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 961276621DCD8B1800A7D920 /* Material+NSMutableAttributedString.swift */; }; 966C17731F0439F600D3E83C /* Material+MotionAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 966C17721F0439F600D3E83C /* Material+MotionAnimation.swift */; }; 9685D5AF1F0F04CB00AFEB79 /* CardCollectionViewCell.swift in Headers */ = {isa = PBXBuildFile; fileRef = 9656895E1F002F16001C656D /* CardCollectionViewCell.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9685D5B01F0F04CB00AFEB79 /* CardCollectionViewController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 965689601F002F4C001C656D /* CardCollectionViewController.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9685D5B11F0F04CB00AFEB79 /* Material+MotionAnimation.swift in Headers */ = {isa = PBXBuildFile; fileRef = 966C17721F0439F600D3E83C /* Material+MotionAnimation.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9697F7BF1D8F2572004741EC /* Divider.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96230AB71D6A520C00AF47DC /* Divider.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9697F7C01D8F2572004741EC /* Material+CALayer.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96F1DC871D654FDF0025F925 /* Material+CALayer.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9697F7C11D8F2572004741EC /* Material+Array.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96C1C8801D42C62800E6608F /* Material+Array.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9697F7C21D8F2572004741EC /* Material+UIWindow.swift in Headers */ = {isa = PBXBuildFile; fileRef = 962864591D53FE3E00690B69 /* Material+UIWindow.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9697F7C31D8F2572004741EC /* DynamicFontType.swift in Headers */ = {isa = PBXBuildFile; fileRef = 9628645E1D540AF300690B69 /* DynamicFontType.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9697F7CB1D8F2573004741EC /* Snackbar.swift in Headers */ = {isa = PBXBuildFile; fileRef = 963FBEFC1D669510008F8512 /* Snackbar.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9697F7CC1D8F2573004741EC /* SnackbarController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 961EFC571D738FF600E84652 /* SnackbarController.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96B8D22C20CF82D2008BD149 /* FABMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96A183641E0C6DD400083C30 /* FABMenuController.swift */; }; 96B8D22D20CF82D5008BD149 /* FABMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96A183621E0C6CE200083C30 /* FABMenu.swift */; }; 96BCB7F31CB40DE900C806FE /* Roboto-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 96BCB7EE1CB40DE900C806FE /* Roboto-Bold.ttf */; }; 96BCB7F51CB40DE900C806FE /* Roboto-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 96BCB7EF1CB40DE900C806FE /* Roboto-Light.ttf */; }; 96BCB7F71CB40DE900C806FE /* Roboto-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 96BCB7F01CB40DE900C806FE /* Roboto-Medium.ttf */; }; 96BCB7F91CB40DE900C806FE /* Roboto-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 96BCB7F11CB40DE900C806FE /* Roboto-Regular.ttf */; }; 96BCB7FB1CB40DE900C806FE /* Roboto-Thin.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 96BCB7F21CB40DE900C806FE /* Roboto-Thin.ttf */; }; 96BCB8141CB4115200C806FE /* PulseAnimation.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7821CB40DC500C806FE /* PulseAnimation.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8151CB4115200C806FE /* FABButton.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB75F1CB40DC500C806FE /* FABButton.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8161CB4115200C806FE /* FlatButton.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7601CB40DC500C806FE /* FlatButton.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8171CB4115200C806FE /* Button.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7701CB40DC500C806FE /* Button.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8181CB4115200C806FE /* RaisedButton.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7931CB40DC500C806FE /* RaisedButton.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB81E1CB4115200C806FE /* DataSourceItem.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7771CB40DC500C806FE /* DataSourceItem.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB81F1CB4115200C806FE /* CollectionView.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7711CB40DC500C806FE /* CollectionView.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8201CB4115200C806FE /* CollectionViewCell.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7721CB40DC500C806FE /* CollectionViewCell.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8231CB4115200C806FE /* CollectionViewLayout.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7751CB40DC500C806FE /* CollectionViewLayout.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8241CB4115200C806FE /* TableViewCell.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7891CB40DC500C806FE /* TableViewCell.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8251CB4115200C806FE /* Color.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7761CB40DC500C806FE /* Color.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8261CB4115200C806FE /* Device.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7791CB40DC500C806FE /* Device.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8281CB4115200C806FE /* Material+String.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7641CB40DC500C806FE /* Material+String.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8291CB4115200C806FE /* Material+UIFont.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7651CB40DC500C806FE /* Material+UIFont.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8301CB4115200C806FE /* Material+UIImage.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB76C1CB40DC500C806FE /* Material+UIImage.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8311CB4115200C806FE /* Font.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB77B1CB40DC500C806FE /* Font.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8321CB4115200C806FE /* RobotoFont.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7941CB40DC500C806FE /* RobotoFont.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8331CB4115200C806FE /* Icon.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB77D1CB40DC500C806FE /* Icon.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8341CB4115200C806FE /* Layer.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7801CB40DC500C806FE /* Layer.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8361CB4115200C806FE /* Grid.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7611CB40DC500C806FE /* Grid.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8371CB4115200C806FE /* Layout.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7811CB40DC500C806FE /* Layout.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB83B1CB4115200C806FE /* NavigationDrawerController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7971CB40DC500C806FE /* NavigationDrawerController.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB83C1CB4115200C806FE /* Bar.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7981CB40DC500C806FE /* Bar.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB83D1CB4115200C806FE /* TransitionController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7991CB40DC500C806FE /* TransitionController.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8441CB4115200C806FE /* BottomNavigationController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7581CB40DC500C806FE /* BottomNavigationController.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8461CB4115200C806FE /* NavigationBar.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7901CB40DC500C806FE /* NavigationBar.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8471CB4115200C806FE /* NavigationController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7911CB40DC500C806FE /* NavigationController.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8481CB4115200C806FE /* NavigationItem.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7921CB40DC500C806FE /* NavigationItem.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB84A1CB4115200C806FE /* TextField.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB79C1CB40DC500C806FE /* TextField.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB84B1CB4115200C806FE /* TextStorage.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB79D1CB40DC500C806FE /* TextStorage.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB84C1CB4115200C806FE /* TextView.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB79E1CB40DC500C806FE /* TextView.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB84D1CB4115200C806FE /* Border.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB76F1CB40DC500C806FE /* Border.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB84E1CB4115200C806FE /* InterimSpace.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7871CB40DC500C806FE /* InterimSpace.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB84F1CB4115200C806FE /* Depth.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7781CB40DC500C806FE /* Depth.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8501CB4115200C806FE /* EdgeInsets.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB77A1CB40DC500C806FE /* EdgeInsets.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8521CB4115200C806FE /* CornerRadius.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7851CB40DC500C806FE /* CornerRadius.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8531CB4115200C806FE /* Shape.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7861CB40DC500C806FE /* Shape.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8551CB4115200C806FE /* PulseView.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7841CB40DC500C806FE /* PulseView.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8561CB4115200C806FE /* Switch.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB7881CB40DC500C806FE /* Switch.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BCB8571CB4115200C806FE /* View.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96BCB78C1CB40DC500C806FE /* View.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96BFC1541E5E486F0075DE1F /* SpringAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 965532281E47E388005C2792 /* SpringAnimation.swift */; }; 96BFC16F1E63C10A0075DE1F /* SpringAnimation.swift in Headers */ = {isa = PBXBuildFile; fileRef = 965532281E47E388005C2792 /* SpringAnimation.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96D88C321C1328D800B91418 /* Material.h in Headers */ = {isa = PBXBuildFile; fileRef = 96D88C091C1328D800B91418 /* Material.h */; settings = {ATTRIBUTES = (Public, ); }; }; 96E09DC81F2287E50000B121 /* TabsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96E09DC71F2287E50000B121 /* TabsController.swift */; }; 96E3C3951D3A1CC20086A024 /* IconButton.swift in Headers */ = {isa = PBXBuildFile; fileRef = 9658F2161CD6FA4700B902C1 /* IconButton.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96E3C3961D3A1CC20086A024 /* CollectionReusableView.swift in Headers */ = {isa = PBXBuildFile; fileRef = 966ECF291CF4C20100BB0BDF /* CollectionReusableView.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96E3C3971D3A1CC20086A024 /* Material+UIView.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E3C3931D397AE90086A024 /* Material+UIView.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96E3C3991D3A1CC20086A024 /* StatusBarController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 967A48181D0F425A00B8CEB7 /* StatusBarController.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96E3C39A1D3A1CC20086A024 /* ErrorTextField.swift in Headers */ = {isa = PBXBuildFile; fileRef = 961F18E71CD93E3E008927C5 /* ErrorTextField.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96E3C39C1D3A1CC20086A024 /* Offset.swift in Headers */ = {isa = PBXBuildFile; fileRef = 968C99461D377849000074FF /* Offset.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 96F1A5531F24F17A001D8CAF /* TabsController.swift in Headers */ = {isa = PBXBuildFile; fileRef = 96E09DC71F2287E50000B121 /* TabsController.swift */; settings = {ATTRIBUTES = (Public, ); }; }; 9D00EBB4216675FB00DBCD69 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D00EBB3216675FB00DBCD69 /* Theme.swift */; }; 9D054A6520D175AC00D0528D /* Material+UIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D054A6320D175AC00D0528D /* Material+UIButton.swift */; }; 9D054A6620D175AC00D0528D /* Material+UILabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D054A6420D175AC00D0528D /* Material+UILabel.swift */; }; 9D39A81B20FE8ED100BA8FA1 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D39A81A20FE8ED100BA8FA1 /* ViewController.swift */; }; 9D494A38217F6B63003D66F1 /* LayoutAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D494A37217F6B63003D66F1 /* LayoutAttribute.swift */; }; 9D494A3A217F6B70003D66F1 /* LayoutAnchor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D494A39217F6B70003D66F1 /* LayoutAnchor.swift */; }; 9D494A3C217F6B7D003D66F1 /* LayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D494A3B217F6B7D003D66F1 /* LayoutConstraint.swift */; }; 9D9089B92118914500605DC9 /* Editor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9089B82118914500605DC9 /* Editor.swift */; }; 9DE25DE02170D7AF000C04DF /* Dialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE25DDF2170D7AF000C04DF /* Dialog.swift */; }; 9DE25DE22170D7C0000C04DF /* DialogController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE25DE12170D7C0000C04DF /* DialogController.swift */; }; 9DE25DE42170D7FF000C04DF /* DialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE25DE32170D7FF000C04DF /* DialogView.swift */; }; 9DE84D721FF0252600586C8B /* RadioButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE84D6F1FF0252500586C8B /* RadioButtonGroup.swift */; }; 9DE84D731FF0252600586C8B /* BaseButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE84D701FF0252500586C8B /* BaseButtonGroup.swift */; }; 9DE84D741FF0252600586C8B /* CheckButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DE84D711FF0252500586C8B /* CheckButtonGroup.swift */; }; 9DF352421FED20C900B2A11B /* BaseIconLayerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DF352411FED20C900B2A11B /* BaseIconLayerButton.swift */; }; 9DF352441FED20ED00B2A11B /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DF352431FED20ED00B2A11B /* RadioButton.swift */; }; 9DF352461FED210000B2A11B /* CheckButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DF352451FED210000B2A11B /* CheckButton.swift */; }; 9DF58CEE20C098C60098968D /* ErrorTextFieldValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DF58CED20C098C60098968D /* ErrorTextFieldValidator.swift */; }; 9DF74C4E20D15D84003C1D66 /* Material+UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DF74C4D20D15D84003C1D66 /* Material+UIColor.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 968BA8351F8D201B0091852E /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 968BA8311F8D201B0091852E /* Motion.xcodeproj */; proxyType = 2; remoteGlobalIDString = 96C98DD11E424AB000B22906; remoteInfo = "Motion iOS"; }; 968BA8371F8D20530091852E /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 968BA8311F8D201B0091852E /* Motion.xcodeproj */; proxyType = 1; remoteGlobalIDString = 96C98DD01E424AB000B22906; remoteInfo = "Motion iOS"; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 961154CB1F32A7B100A78D74 /* ChipBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChipBar.swift; sourceTree = ""; }; 961276621DCD8B1800A7D920 /* Material+NSMutableAttributedString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+NSMutableAttributedString.swift"; sourceTree = ""; }; 961527B81F3A509900E8B2AC /* ChipBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChipBarController.swift; sourceTree = ""; }; 9618006C1F4D384200CD77A1 /* Material+UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+UIViewController.swift"; sourceTree = ""; }; 961E6BDE1DDA2A95004E6C93 /* Application.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; 961E6BE11DDA2AF3004E6C93 /* Screen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = ""; }; 961EFC571D738FF600E84652 /* SnackbarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnackbarController.swift; sourceTree = ""; }; 961F18E71CD93E3E008927C5 /* ErrorTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorTextField.swift; sourceTree = ""; }; 96230AB71D6A520C00AF47DC /* Divider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Divider.swift; sourceTree = ""; }; 9626CA961DAB53A8003E2611 /* DisplayStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayStyle.swift; sourceTree = ""; }; 9626CB9A1DAD3D1D003E2611 /* HeightPreset.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeightPreset.swift; sourceTree = ""; }; 962864591D53FE3E00690B69 /* Material+UIWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+UIWindow.swift"; sourceTree = ""; }; 9628645E1D540AF300690B69 /* DynamicFontType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DynamicFontType.swift; sourceTree = ""; }; 9631A7C01D95E3AC00CFB109 /* PresenterCard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenterCard.swift; sourceTree = ""; }; 96328B791E020A41009A4C90 /* CollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewController.swift; sourceTree = ""; }; 96328B961E05C0BB009A4C90 /* TableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableView.swift; sourceTree = ""; }; 96328B981E05C0CE009A4C90 /* TableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = ""; }; 96334EF51C8B84660083986B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 963832361B88DFD80015F710 /* Material.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Material.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 963FBEFC1D669510008F8512 /* Snackbar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Snackbar.swift; sourceTree = ""; }; 965532281E47E388005C2792 /* SpringAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpringAnimation.swift; sourceTree = ""; }; 9656895E1F002F16001C656D /* CardCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardCollectionViewCell.swift; sourceTree = ""; }; 965689601F002F4C001C656D /* CardCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardCollectionViewController.swift; sourceTree = ""; }; 9658F2161CD6FA4700B902C1 /* IconButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IconButton.swift; sourceTree = ""; }; 966C17721F0439F600D3E83C /* Material+MotionAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+MotionAnimation.swift"; sourceTree = ""; }; 966ECF291CF4C20100BB0BDF /* CollectionReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionReusableView.swift; sourceTree = ""; }; 967A48181D0F425A00B8CEB7 /* StatusBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarController.swift; sourceTree = ""; }; 968BA8311F8D201B0091852E /* Motion.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = Motion.xcodeproj; sourceTree = ""; }; 968C99461D377849000074FF /* Offset.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Offset.swift; sourceTree = ""; }; 96A183621E0C6CE200083C30 /* FABMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FABMenu.swift; sourceTree = ""; }; 96A183641E0C6DD400083C30 /* FABMenuController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FABMenuController.swift; sourceTree = ""; }; 96BCB7581CB40DC500C806FE /* BottomNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BottomNavigationController.swift; sourceTree = ""; }; 96BCB75D1CB40DC500C806FE /* Card.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = ""; }; 96BCB75F1CB40DC500C806FE /* FABButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FABButton.swift; sourceTree = ""; }; 96BCB7601CB40DC500C806FE /* FlatButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlatButton.swift; sourceTree = ""; }; 96BCB7611CB40DC500C806FE /* Grid.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Grid.swift; sourceTree = ""; }; 96BCB7621CB40DC500C806FE /* ImageCard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCard.swift; sourceTree = ""; }; 96BCB7641CB40DC500C806FE /* Material+String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+String.swift"; sourceTree = ""; }; 96BCB7651CB40DC500C806FE /* Material+UIFont.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+UIFont.swift"; sourceTree = ""; }; 96BCB76C1CB40DC500C806FE /* Material+UIImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+UIImage.swift"; sourceTree = ""; }; 96BCB76F1CB40DC500C806FE /* Border.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Border.swift; sourceTree = ""; }; 96BCB7701CB40DC500C806FE /* Button.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; 96BCB7711CB40DC500C806FE /* CollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionView.swift; sourceTree = ""; }; 96BCB7721CB40DC500C806FE /* CollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewCell.swift; sourceTree = ""; }; 96BCB7751CB40DC500C806FE /* CollectionViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewLayout.swift; sourceTree = ""; }; 96BCB7761CB40DC500C806FE /* Color.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; }; 96BCB7771CB40DC500C806FE /* DataSourceItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataSourceItem.swift; sourceTree = ""; }; 96BCB7781CB40DC500C806FE /* Depth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Depth.swift; sourceTree = ""; }; 96BCB7791CB40DC500C806FE /* Device.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Device.swift; sourceTree = ""; }; 96BCB77A1CB40DC500C806FE /* EdgeInsets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EdgeInsets.swift; sourceTree = ""; }; 96BCB77B1CB40DC500C806FE /* Font.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Font.swift; sourceTree = ""; }; 96BCB77D1CB40DC500C806FE /* Icon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Icon.swift; sourceTree = ""; }; 96BCB7801CB40DC500C806FE /* Layer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Layer.swift; sourceTree = ""; }; 96BCB7811CB40DC500C806FE /* Layout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Layout.swift; sourceTree = ""; }; 96BCB7821CB40DC500C806FE /* PulseAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PulseAnimation.swift; sourceTree = ""; }; 96BCB7841CB40DC500C806FE /* PulseView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PulseView.swift; sourceTree = ""; }; 96BCB7851CB40DC500C806FE /* CornerRadius.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CornerRadius.swift; sourceTree = ""; }; 96BCB7861CB40DC500C806FE /* Shape.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shape.swift; sourceTree = ""; }; 96BCB7871CB40DC500C806FE /* InterimSpace.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InterimSpace.swift; sourceTree = ""; }; 96BCB7881CB40DC500C806FE /* Switch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Switch.swift; sourceTree = ""; }; 96BCB7891CB40DC500C806FE /* TableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewCell.swift; sourceTree = ""; }; 96BCB78C1CB40DC500C806FE /* View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; 96BCB7901CB40DC500C806FE /* NavigationBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationBar.swift; sourceTree = ""; }; 96BCB7911CB40DC500C806FE /* NavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = ""; }; 96BCB7921CB40DC500C806FE /* NavigationItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationItem.swift; sourceTree = ""; }; 96BCB7931CB40DC500C806FE /* RaisedButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RaisedButton.swift; sourceTree = ""; }; 96BCB7941CB40DC500C806FE /* RobotoFont.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RobotoFont.swift; sourceTree = ""; }; 96BCB7951CB40DC500C806FE /* SearchBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; 96BCB7961CB40DC500C806FE /* SearchBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchBarController.swift; sourceTree = ""; }; 96BCB7971CB40DC500C806FE /* NavigationDrawerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationDrawerController.swift; sourceTree = ""; }; 96BCB7981CB40DC500C806FE /* Bar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bar.swift; sourceTree = ""; }; 96BCB7991CB40DC500C806FE /* TransitionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransitionController.swift; sourceTree = ""; }; 96BCB79A1CB40DC500C806FE /* TabBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = TabBar.swift; sourceTree = ""; tabWidth = 2; }; 96BCB79C1CB40DC500C806FE /* TextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = ""; }; 96BCB79D1CB40DC500C806FE /* TextStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextStorage.swift; sourceTree = ""; }; 96BCB79E1CB40DC500C806FE /* TextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = ""; }; 96BCB79F1CB40DC500C806FE /* Toolbar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Toolbar.swift; sourceTree = ""; }; 96BCB7A01CB40DC500C806FE /* ToolbarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToolbarController.swift; sourceTree = ""; }; 96BCB7EE1CB40DE900C806FE /* Roboto-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Bold.ttf"; sourceTree = ""; }; 96BCB7EF1CB40DE900C806FE /* Roboto-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Light.ttf"; sourceTree = ""; }; 96BCB7F01CB40DE900C806FE /* Roboto-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Medium.ttf"; sourceTree = ""; }; 96BCB7F11CB40DE900C806FE /* Roboto-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Regular.ttf"; sourceTree = ""; }; 96BCB7F21CB40DE900C806FE /* Roboto-Thin.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Thin.ttf"; sourceTree = ""; }; 96C1C8801D42C62800E6608F /* Material+Array.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+Array.swift"; sourceTree = ""; }; 96D88BFC1C1328D800B91418 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 96D88BFD1C1328D800B91418 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 96D88C091C1328D800B91418 /* Material.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Material.h; sourceTree = ""; }; 96E09DC71F2287E50000B121 /* TabsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabsController.swift; sourceTree = ""; }; 96E3C3931D397AE90086A024 /* Material+UIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+UIView.swift"; sourceTree = ""; }; 96F1DC871D654FDF0025F925 /* Material+CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+CALayer.swift"; sourceTree = ""; }; 9D00EBB3216675FB00DBCD69 /* Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; 9D054A6320D175AC00D0528D /* Material+UIButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+UIButton.swift"; sourceTree = ""; }; 9D054A6420D175AC00D0528D /* Material+UILabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+UILabel.swift"; sourceTree = ""; }; 9D39A81A20FE8ED100BA8FA1 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 9D494A37217F6B63003D66F1 /* LayoutAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutAttribute.swift; sourceTree = ""; }; 9D494A39217F6B70003D66F1 /* LayoutAnchor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutAnchor.swift; sourceTree = ""; }; 9D494A3B217F6B7D003D66F1 /* LayoutConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutConstraint.swift; sourceTree = ""; }; 9D9089B82118914500605DC9 /* Editor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Editor.swift; sourceTree = ""; }; 9DE25DDF2170D7AF000C04DF /* Dialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dialog.swift; sourceTree = ""; }; 9DE25DE12170D7C0000C04DF /* DialogController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialogController.swift; sourceTree = ""; }; 9DE25DE32170D7FF000C04DF /* DialogView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DialogView.swift; sourceTree = ""; }; 9DE84D6F1FF0252500586C8B /* RadioButtonGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioButtonGroup.swift; sourceTree = ""; }; 9DE84D701FF0252500586C8B /* BaseButtonGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseButtonGroup.swift; sourceTree = ""; }; 9DE84D711FF0252500586C8B /* CheckButtonGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckButtonGroup.swift; sourceTree = ""; }; 9DF352411FED20C900B2A11B /* BaseIconLayerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseIconLayerButton.swift; sourceTree = ""; }; 9DF352431FED20ED00B2A11B /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; 9DF352451FED210000B2A11B /* CheckButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckButton.swift; sourceTree = ""; }; 9DF58CED20C098C60098968D /* ErrorTextFieldValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorTextFieldValidator.swift; sourceTree = ""; }; 9DF74C4D20D15D84003C1D66 /* Material+UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Material+UIColor.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXGroup section */ 9602F00C1DA1163000F3FB79 /* Grid */ = { isa = PBXGroup; children = ( 96BCB7611CB40DC500C806FE /* Grid.swift */, ); path = Grid; sourceTree = ""; }; 96090B031D9D709E00709CA6 /* Text */ = { isa = PBXGroup; children = ( 9D9089B82118914500605DC9 /* Editor.swift */, 961F18E71CD93E3E008927C5 /* ErrorTextField.swift */, 9DF58CED20C098C60098968D /* ErrorTextFieldValidator.swift */, 96BCB79C1CB40DC500C806FE /* TextField.swift */, 96BCB79D1CB40DC500C806FE /* TextStorage.swift */, 96BCB79E1CB40DC500C806FE /* TextView.swift */, ); path = Text; sourceTree = ""; }; 961154CA1F32999000A78D74 /* Chip */ = { isa = PBXGroup; children = ( 961154CB1F32A7B100A78D74 /* ChipBar.swift */, 961527B81F3A509900E8B2AC /* ChipBarController.swift */, ); path = Chip; sourceTree = ""; }; 961527531F393C0E00E8B2AC /* Motion */ = { isa = PBXGroup; children = ( 968BA8311F8D201B0091852E /* Motion.xcodeproj */, ); path = Motion; sourceTree = ""; }; 961E6BDD1DDA2A7E004E6C93 /* Application */ = { isa = PBXGroup; children = ( 961E6BDE1DDA2A95004E6C93 /* Application.swift */, ); path = Application; sourceTree = ""; }; 961E6BE01DDA2ADD004E6C93 /* Screen */ = { isa = PBXGroup; children = ( 961E6BE11DDA2AF3004E6C93 /* Screen.swift */, ); path = Screen; sourceTree = ""; }; 961E6BEF1DDA4B04004E6C93 /* NavigationDrawer */ = { isa = PBXGroup; children = ( 96BCB7971CB40DC500C806FE /* NavigationDrawerController.swift */, ); path = NavigationDrawer; sourceTree = ""; }; 96230AB61D6A51FD00AF47DC /* Divider */ = { isa = PBXGroup; children = ( 96230AB71D6A520C00AF47DC /* Divider.swift */, ); path = Divider; sourceTree = ""; }; 96264BE41D833C8400576F37 /* Bar */ = { isa = PBXGroup; children = ( 96BCB7981CB40DC500C806FE /* Bar.swift */, ); path = Bar; sourceTree = ""; }; 9626CA951DAB5370003E2611 /* Transition */ = { isa = PBXGroup; children = ( 9626CA961DAB53A8003E2611 /* DisplayStyle.swift */, 96BCB7991CB40DC500C806FE /* TransitionController.swift */, ); path = Transition; sourceTree = ""; }; 9626CBCC1DADA5F1003E2611 /* Height */ = { isa = PBXGroup; children = ( 9626CB9A1DAD3D1D003E2611 /* HeightPreset.swift */, ); path = Height; sourceTree = ""; }; 962DDD081D6FBBD0001C307C /* BottomTabBar */ = { isa = PBXGroup; children = ( 96BCB7581CB40DC500C806FE /* BottomNavigationController.swift */, ); path = BottomTabBar; sourceTree = ""; }; 9630ACB71F29A26B00B4217D /* Frameworks */ = { isa = PBXGroup; children = ( 961527531F393C0E00E8B2AC /* Motion */, ); path = Frameworks; sourceTree = ""; }; 96328B9A1E05C135009A4C90 /* Data */ = { isa = PBXGroup; children = ( 96BCB7771CB40DC500C806FE /* DataSourceItem.swift */, ); path = Data; sourceTree = ""; }; 9638322C1B88DFD80015F710 = { isa = PBXGroup; children = ( 96D88BF41C1328D800B91418 /* Sources */, 963832371B88DFD80015F710 /* Products */, ); indentWidth = 2; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; 963832371B88DFD80015F710 /* Products */ = { isa = PBXGroup; children = ( 963832361B88DFD80015F710 /* Material.framework */, ); name = Products; sourceTree = ""; }; 963FBEFB1D6694E8008F8512 /* Snackbar */ = { isa = PBXGroup; children = ( 963FBEFC1D669510008F8512 /* Snackbar.swift */, 961EFC571D738FF600E84652 /* SnackbarController.swift */, ); path = Snackbar; sourceTree = ""; }; 963FBF001D66964F008F8512 /* Toolbar */ = { isa = PBXGroup; children = ( 96BCB79F1CB40DC500C806FE /* Toolbar.swift */, 96BCB7A01CB40DC500C806FE /* ToolbarController.swift */, ); path = Toolbar; sourceTree = ""; }; 963FBF011D6696AB008F8512 /* Tab */ = { isa = PBXGroup; children = ( 96BCB79A1CB40DC500C806FE /* TabBar.swift */, 96E09DC71F2287E50000B121 /* TabsController.swift */, ); path = Tab; sourceTree = ""; }; 963FBF021D6696D0008F8512 /* FABMenu */ = { isa = PBXGroup; children = ( 96A183621E0C6CE200083C30 /* FABMenu.swift */, 96A183641E0C6DD400083C30 /* FABMenuController.swift */, ); path = FABMenu; sourceTree = ""; }; 963FBF031D6696EF008F8512 /* SearchBar */ = { isa = PBXGroup; children = ( 96BCB7951CB40DC500C806FE /* SearchBar.swift */, 96BCB7961CB40DC500C806FE /* SearchBarController.swift */, ); path = SearchBar; sourceTree = ""; }; 965689641F003476001C656D /* CardCollectionView */ = { isa = PBXGroup; children = ( 9656895E1F002F16001C656D /* CardCollectionViewCell.swift */, 965689601F002F4C001C656D /* CardCollectionViewController.swift */, ); path = CardCollectionView; sourceTree = ""; }; 966ECF2B1CF4C21B00BB0BDF /* Table */ = { isa = PBXGroup; children = ( 96328B961E05C0BB009A4C90 /* TableView.swift */, 96BCB7891CB40DC500C806FE /* TableViewCell.swift */, 96328B981E05C0CE009A4C90 /* TableViewController.swift */, ); path = Table; sourceTree = ""; }; 967A48171D0F424B00B8CEB7 /* StatusBar */ = { isa = PBXGroup; children = ( 967A48181D0F425A00B8CEB7 /* StatusBarController.swift */, ); path = StatusBar; sourceTree = ""; }; 968BA8321F8D201B0091852E /* Products */ = { isa = PBXGroup; children = ( 968BA8361F8D201B0091852E /* Motion.framework */, ); name = Products; sourceTree = ""; }; 968C99421D36EC9E000074FF /* Switch */ = { isa = PBXGroup; children = ( 96BCB7881CB40DC500C806FE /* Switch.swift */, ); path = Switch; sourceTree = ""; }; 96BCB7571CB40DC500C806FE /* iOS */ = { isa = PBXGroup; children = ( 96EF418E1E835E850012CA1C /* Animation */, 961E6BDD1DDA2A7E004E6C93 /* Application */, 96264BE41D833C8400576F37 /* Bar */, 962DDD081D6FBBD0001C307C /* BottomTabBar */, 96BCB8031CB40F4B00C806FE /* Button */, 9DE84D6E1FF0250E00586C8B /* ButtonGroup */, 96BCB8021CB40F3B00C806FE /* Card */, 961154CA1F32999000A78D74 /* Chip */, 96BCB8051CB40F9C00C806FE /* Collection */, 96BCB8001CB40F0300C806FE /* Color */, 96328B9A1E05C135009A4C90 /* Data */, 96BCB80B1CB410CC00C806FE /* Device */, 9DE25DDE2170D779000C04DF /* Dialogs */, 96230AB61D6A51FD00AF47DC /* Divider */, 96BCB80A1CB410A100C806FE /* Extension */, 963FBF021D6696D0008F8512 /* FABMenu */, 96BCB8071CB4101C00C806FE /* Font */, 9602F00C1DA1163000F3FB79 /* Grid */, 9626CBCC1DADA5F1003E2611 /* Height */, 96BCB8081CB4105E00C806FE /* Icon */, 96BCB80D1CB410FD00C806FE /* Layer */, 96BCB8041CB40F6C00C806FE /* Layout */, 96BCB8011CB40F1700C806FE /* Navigation */, 961E6BEF1DDA4B04004E6C93 /* NavigationDrawer */, 961E6BE01DDA2ADD004E6C93 /* Screen */, 963FBF031D6696EF008F8512 /* SearchBar */, 963FBEFB1D6694E8008F8512 /* Snackbar */, 967A48171D0F424B00B8CEB7 /* StatusBar */, 968C99421D36EC9E000074FF /* Switch */, 963FBF011D6696AB008F8512 /* Tab */, 966ECF2B1CF4C21B00BB0BDF /* Table */, 96090B031D9D709E00709CA6 /* Text */, 9D00EBB2216675A800DBCD69 /* Theme */, 963FBF001D66964F008F8512 /* Toolbar */, 9626CA951DAB5370003E2611 /* Transition */, 96BCB8061CB40FD000C806FE /* Type */, 96BCB80C1CB410DD00C806FE /* View */, ); path = iOS; sourceTree = ""; }; 96BCB7EC1CB40DE900C806FE /* Font */ = { isa = PBXGroup; children = ( 96BCB7ED1CB40DE900C806FE /* Roboto */, ); path = Font; sourceTree = ""; }; 96BCB7ED1CB40DE900C806FE /* Roboto */ = { isa = PBXGroup; children = ( 96BCB7EE1CB40DE900C806FE /* Roboto-Bold.ttf */, 96BCB7EF1CB40DE900C806FE /* Roboto-Light.ttf */, 96BCB7F01CB40DE900C806FE /* Roboto-Medium.ttf */, 96BCB7F11CB40DE900C806FE /* Roboto-Regular.ttf */, 96BCB7F21CB40DE900C806FE /* Roboto-Thin.ttf */, ); path = Roboto; sourceTree = ""; }; 96BCB8001CB40F0300C806FE /* Color */ = { isa = PBXGroup; children = ( 96BCB7761CB40DC500C806FE /* Color.swift */, ); path = Color; sourceTree = ""; }; 96BCB8011CB40F1700C806FE /* Navigation */ = { isa = PBXGroup; children = ( 96BCB7901CB40DC500C806FE /* NavigationBar.swift */, 96BCB7911CB40DC500C806FE /* NavigationController.swift */, 96BCB7921CB40DC500C806FE /* NavigationItem.swift */, ); path = Navigation; sourceTree = ""; }; 96BCB8021CB40F3B00C806FE /* Card */ = { isa = PBXGroup; children = ( 96BCB75D1CB40DC500C806FE /* Card.swift */, 96BCB7621CB40DC500C806FE /* ImageCard.swift */, 9631A7C01D95E3AC00CFB109 /* PresenterCard.swift */, ); path = Card; sourceTree = ""; }; 96BCB8031CB40F4B00C806FE /* Button */ = { isa = PBXGroup; children = ( 9DF352411FED20C900B2A11B /* BaseIconLayerButton.swift */, 96BCB7701CB40DC500C806FE /* Button.swift */, 9DF352451FED210000B2A11B /* CheckButton.swift */, 96BCB75F1CB40DC500C806FE /* FABButton.swift */, 96BCB7601CB40DC500C806FE /* FlatButton.swift */, 9658F2161CD6FA4700B902C1 /* IconButton.swift */, 9DF352431FED20ED00B2A11B /* RadioButton.swift */, 96BCB7931CB40DC500C806FE /* RaisedButton.swift */, ); path = Button; sourceTree = ""; }; 96BCB8041CB40F6C00C806FE /* Layout */ = { isa = PBXGroup; children = ( 96BCB7811CB40DC500C806FE /* Layout.swift */, 9D494A39217F6B70003D66F1 /* LayoutAnchor.swift */, 9D494A37217F6B63003D66F1 /* LayoutAttribute.swift */, 9D494A3B217F6B7D003D66F1 /* LayoutConstraint.swift */, ); path = Layout; sourceTree = ""; }; 96BCB8051CB40F9C00C806FE /* Collection */ = { isa = PBXGroup; children = ( 965689641F003476001C656D /* CardCollectionView */, 966ECF291CF4C20100BB0BDF /* CollectionReusableView.swift */, 96BCB7711CB40DC500C806FE /* CollectionView.swift */, 96BCB7721CB40DC500C806FE /* CollectionViewCell.swift */, 96328B791E020A41009A4C90 /* CollectionViewController.swift */, 96BCB7751CB40DC500C806FE /* CollectionViewLayout.swift */, ); path = Collection; sourceTree = ""; }; 96BCB8061CB40FD000C806FE /* Type */ = { isa = PBXGroup; children = ( 96BCB76F1CB40DC500C806FE /* Border.swift */, 96BCB7851CB40DC500C806FE /* CornerRadius.swift */, 96BCB7781CB40DC500C806FE /* Depth.swift */, 96BCB77A1CB40DC500C806FE /* EdgeInsets.swift */, 96BCB7871CB40DC500C806FE /* InterimSpace.swift */, 968C99461D377849000074FF /* Offset.swift */, 96BCB7861CB40DC500C806FE /* Shape.swift */, ); path = Type; sourceTree = ""; }; 96BCB8071CB4101C00C806FE /* Font */ = { isa = PBXGroup; children = ( 9628645E1D540AF300690B69 /* DynamicFontType.swift */, 96BCB77B1CB40DC500C806FE /* Font.swift */, 96BCB7941CB40DC500C806FE /* RobotoFont.swift */, ); path = Font; sourceTree = ""; }; 96BCB8081CB4105E00C806FE /* Icon */ = { isa = PBXGroup; children = ( 96BCB77D1CB40DC500C806FE /* Icon.swift */, ); path = Icon; sourceTree = ""; }; 96BCB80A1CB410A100C806FE /* Extension */ = { isa = PBXGroup; children = ( 96C1C8801D42C62800E6608F /* Material+Array.swift */, 96F1DC871D654FDF0025F925 /* Material+CALayer.swift */, 966C17721F0439F600D3E83C /* Material+MotionAnimation.swift */, 961276621DCD8B1800A7D920 /* Material+NSMutableAttributedString.swift */, 96BCB7641CB40DC500C806FE /* Material+String.swift */, 9D054A6320D175AC00D0528D /* Material+UIButton.swift */, 9DF74C4D20D15D84003C1D66 /* Material+UIColor.swift */, 96BCB7651CB40DC500C806FE /* Material+UIFont.swift */, 96BCB76C1CB40DC500C806FE /* Material+UIImage.swift */, 9D054A6420D175AC00D0528D /* Material+UILabel.swift */, 96E3C3931D397AE90086A024 /* Material+UIView.swift */, 9618006C1F4D384200CD77A1 /* Material+UIViewController.swift */, 962864591D53FE3E00690B69 /* Material+UIWindow.swift */, ); path = Extension; sourceTree = ""; }; 96BCB80B1CB410CC00C806FE /* Device */ = { isa = PBXGroup; children = ( 96BCB7791CB40DC500C806FE /* Device.swift */, ); path = Device; sourceTree = ""; }; 96BCB80C1CB410DD00C806FE /* View */ = { isa = PBXGroup; children = ( 96BCB7841CB40DC500C806FE /* PulseView.swift */, 96BCB78C1CB40DC500C806FE /* View.swift */, 9D39A81A20FE8ED100BA8FA1 /* ViewController.swift */, ); path = View; sourceTree = ""; }; 96BCB80D1CB410FD00C806FE /* Layer */ = { isa = PBXGroup; children = ( 96BCB7801CB40DC500C806FE /* Layer.swift */, ); path = Layer; sourceTree = ""; }; 96D88BF41C1328D800B91418 /* Sources */ = { isa = PBXGroup; children = ( 96BCB7EC1CB40DE900C806FE /* Font */, 9630ACB71F29A26B00B4217D /* Frameworks */, 96BCB7571CB40DC500C806FE /* iOS */, 96334EF51C8B84660083986B /* Assets.xcassets */, 96D88BFC1C1328D800B91418 /* Info.plist */, 96D88BFD1C1328D800B91418 /* LICENSE */, 96D88C091C1328D800B91418 /* Material.h */, ); path = Sources; sourceTree = ""; }; 96EF418E1E835E850012CA1C /* Animation */ = { isa = PBXGroup; children = ( 96BCB7821CB40DC500C806FE /* PulseAnimation.swift */, 965532281E47E388005C2792 /* SpringAnimation.swift */, ); path = Animation; sourceTree = ""; }; 9D00EBB2216675A800DBCD69 /* Theme */ = { isa = PBXGroup; children = ( 9D00EBB3216675FB00DBCD69 /* Theme.swift */, ); path = Theme; sourceTree = ""; }; 9DE25DDE2170D779000C04DF /* Dialogs */ = { isa = PBXGroup; children = ( 9DE25DDF2170D7AF000C04DF /* Dialog.swift */, 9DE25DE12170D7C0000C04DF /* DialogController.swift */, 9DE25DE32170D7FF000C04DF /* DialogView.swift */, ); path = Dialogs; sourceTree = ""; }; 9DE84D6E1FF0250E00586C8B /* ButtonGroup */ = { isa = PBXGroup; children = ( 9DE84D701FF0252500586C8B /* BaseButtonGroup.swift */, 9DE84D711FF0252500586C8B /* CheckButtonGroup.swift */, 9DE84D6F1FF0252500586C8B /* RadioButtonGroup.swift */, ); path = ButtonGroup; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ 963832331B88DFD80015F710 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 96D88C321C1328D800B91418 /* Material.h in Headers */, 96BCB8141CB4115200C806FE /* PulseAnimation.swift in Headers */, 96BCB8151CB4115200C806FE /* FABButton.swift in Headers */, 96BCB8161CB4115200C806FE /* FlatButton.swift in Headers */, 96BCB8171CB4115200C806FE /* Button.swift in Headers */, 96BCB8181CB4115200C806FE /* RaisedButton.swift in Headers */, 96BCB81E1CB4115200C806FE /* DataSourceItem.swift in Headers */, 96BCB81F1CB4115200C806FE /* CollectionView.swift in Headers */, 96BCB8201CB4115200C806FE /* CollectionViewCell.swift in Headers */, 96BCB8231CB4115200C806FE /* CollectionViewLayout.swift in Headers */, 96BCB8241CB4115200C806FE /* TableViewCell.swift in Headers */, 96BCB8251CB4115200C806FE /* Color.swift in Headers */, 96BCB8261CB4115200C806FE /* Device.swift in Headers */, 96BCB8281CB4115200C806FE /* Material+String.swift in Headers */, 96BCB8291CB4115200C806FE /* Material+UIFont.swift in Headers */, 96BCB8301CB4115200C806FE /* Material+UIImage.swift in Headers */, 96BCB8311CB4115200C806FE /* Font.swift in Headers */, 96BCB8321CB4115200C806FE /* RobotoFont.swift in Headers */, 96BCB8331CB4115200C806FE /* Icon.swift in Headers */, 96BCB8341CB4115200C806FE /* Layer.swift in Headers */, 96BCB8361CB4115200C806FE /* Grid.swift in Headers */, 96BCB8371CB4115200C806FE /* Layout.swift in Headers */, 96BCB83B1CB4115200C806FE /* NavigationDrawerController.swift in Headers */, 96BCB83C1CB4115200C806FE /* Bar.swift in Headers */, 96BCB83D1CB4115200C806FE /* TransitionController.swift in Headers */, 96BCB8441CB4115200C806FE /* BottomNavigationController.swift in Headers */, 96BCB8461CB4115200C806FE /* NavigationBar.swift in Headers */, 96BCB8471CB4115200C806FE /* NavigationController.swift in Headers */, 96BCB8481CB4115200C806FE /* NavigationItem.swift in Headers */, 96BCB84A1CB4115200C806FE /* TextField.swift in Headers */, 96BCB84B1CB4115200C806FE /* TextStorage.swift in Headers */, 96BCB84C1CB4115200C806FE /* TextView.swift in Headers */, 96BCB84D1CB4115200C806FE /* Border.swift in Headers */, 96BCB84E1CB4115200C806FE /* InterimSpace.swift in Headers */, 96BCB84F1CB4115200C806FE /* Depth.swift in Headers */, 96BCB8501CB4115200C806FE /* EdgeInsets.swift in Headers */, 96BCB8521CB4115200C806FE /* CornerRadius.swift in Headers */, 96BCB8531CB4115200C806FE /* Shape.swift in Headers */, 96BCB8551CB4115200C806FE /* PulseView.swift in Headers */, 96BCB8561CB4115200C806FE /* Switch.swift in Headers */, 96BCB8571CB4115200C806FE /* View.swift in Headers */, 96E3C3951D3A1CC20086A024 /* IconButton.swift in Headers */, 96E3C3961D3A1CC20086A024 /* CollectionReusableView.swift in Headers */, 96E3C3971D3A1CC20086A024 /* Material+UIView.swift in Headers */, 96E3C3991D3A1CC20086A024 /* StatusBarController.swift in Headers */, 96E3C39A1D3A1CC20086A024 /* ErrorTextField.swift in Headers */, 96E3C39C1D3A1CC20086A024 /* Offset.swift in Headers */, 9697F7BF1D8F2572004741EC /* Divider.swift in Headers */, 9697F7C01D8F2572004741EC /* Material+CALayer.swift in Headers */, 9697F7C11D8F2572004741EC /* Material+Array.swift in Headers */, 9697F7C21D8F2572004741EC /* Material+UIWindow.swift in Headers */, 9697F7C31D8F2572004741EC /* DynamicFontType.swift in Headers */, 9697F7CB1D8F2573004741EC /* Snackbar.swift in Headers */, 9697F7CC1D8F2573004741EC /* SnackbarController.swift in Headers */, 9617B07D1DFCA8CF00410F8F /* Application.swift in Headers */, 9617B07E1DFCA8CF00410F8F /* Card.swift in Headers */, 9617B07F1DFCA8CF00410F8F /* ImageCard.swift in Headers */, 9617B0801DFCA8CF00410F8F /* PresenterCard.swift in Headers */, 9617B0861DFCA8CF00410F8F /* HeightPreset.swift in Headers */, 9617B08A1DFCA8CF00410F8F /* DisplayStyle.swift in Headers */, 9617B08B1DFCA8CF00410F8F /* Screen.swift in Headers */, 9617B08C1DFCA8CF00410F8F /* SearchBar.swift in Headers */, 9617B08D1DFCA8CF00410F8F /* SearchBarController.swift in Headers */, 9617B08E1DFCA8CF00410F8F /* TabBar.swift in Headers */, 9617B08F1DFCA8CF00410F8F /* Material+NSMutableAttributedString.swift in Headers */, 9617B0901DFCA8CF00410F8F /* Toolbar.swift in Headers */, 9617B0911DFCA8CF00410F8F /* ToolbarController.swift in Headers */, 96328B9B1E05C24E009A4C90 /* CollectionViewController.swift in Headers */, 96328B9E1E05C24E009A4C90 /* TableView.swift in Headers */, 96328B9F1E05C24E009A4C90 /* TableViewController.swift in Headers */, 96BFC16F1E63C10A0075DE1F /* SpringAnimation.swift in Headers */, 9685D5AF1F0F04CB00AFEB79 /* CardCollectionViewCell.swift in Headers */, 9685D5B01F0F04CB00AFEB79 /* CardCollectionViewController.swift in Headers */, 9685D5B11F0F04CB00AFEB79 /* Material+MotionAnimation.swift in Headers */, 96F1A5531F24F17A001D8CAF /* TabsController.swift in Headers */, 9618006E1F4D38BC00CD77A1 /* ChipBar.swift in Headers */, 9618006F1F4D38BC00CD77A1 /* ChipBarController.swift in Headers */, 961800701F4D38BC00CD77A1 /* Material+UIViewController.swift in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ 963832351B88DFD80015F710 /* Material */ = { isa = PBXNativeTarget; buildConfigurationList = 9638324C1B88DFD80015F710 /* Build configuration list for PBXNativeTarget "Material" */; buildPhases = ( 963832311B88DFD80015F710 /* Sources */, 963832331B88DFD80015F710 /* Headers */, 963832341B88DFD80015F710 /* Resources */, ); buildRules = ( ); dependencies = ( 968BA8381F8D20530091852E /* PBXTargetDependency */, ); name = Material; productName = FocusKit; productReference = 963832361B88DFD80015F710 /* Material.framework */; productType = "com.apple.product-type.framework"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 9638322D1B88DFD80015F710 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftMigration = 0710; LastSwiftUpdateCheck = 0730; LastUpgradeCheck = 1100; ORGANIZATIONNAME = "CosmicMind, Inc."; TargetAttributes = { 963832351B88DFD80015F710 = { CreatedOnToolsVersion = 6.4; DevelopmentTeam = 9Z76XCNLGL; DevelopmentTeamName = "CosmicMind Inc."; LastSwiftMigration = 1020; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 963832301B88DFD80015F710 /* Build configuration list for PBXProject "Material" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 9638322C1B88DFD80015F710; productRefGroup = 963832371B88DFD80015F710 /* Products */; projectDirPath = ""; projectReferences = ( { ProductGroup = 968BA8321F8D201B0091852E /* Products */; ProjectRef = 968BA8311F8D201B0091852E /* Motion.xcodeproj */; }, ); projectRoot = ""; targets = ( 963832351B88DFD80015F710 /* Material */, ); }; /* End PBXProject section */ /* Begin PBXReferenceProxy section */ 968BA8361F8D201B0091852E /* Motion.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = Motion.framework; remoteRef = 968BA8351F8D201B0091852E /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ 963832341B88DFD80015F710 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 96334EF61C8B84660083986B /* Assets.xcassets in Resources */, 96BCB7F71CB40DE900C806FE /* Roboto-Medium.ttf in Resources */, 96BCB7F31CB40DE900C806FE /* Roboto-Bold.ttf in Resources */, 96BCB7FB1CB40DE900C806FE /* Roboto-Thin.ttf in Resources */, 96BCB7F91CB40DE900C806FE /* Roboto-Regular.ttf in Resources */, 96BCB7F51CB40DE900C806FE /* Roboto-Light.ttf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 963832311B88DFD80015F710 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 961E6BE21DDA2AF3004E6C93 /* Screen.swift in Sources */, 9DF352441FED20ED00B2A11B /* RadioButton.swift in Sources */, 965E81261DD4D7C800D61E4B /* Material+NSMutableAttributedString.swift in Sources */, 965E80FF1DD4D5C800D61E4B /* BottomNavigationController.swift in Sources */, 965E81031DD4D5C800D61E4B /* CollectionView.swift in Sources */, 965E81041DD4D5C800D61E4B /* CollectionViewCell.swift in Sources */, 965E81071DD4D5C800D61E4B /* CollectionViewLayout.swift in Sources */, 965E81081DD4D5C800D61E4B /* CollectionReusableView.swift in Sources */, 965E81091DD4D5C800D61E4B /* DataSourceItem.swift in Sources */, 9DF352461FED210000B2A11B /* CheckButton.swift in Sources */, 965E810A1DD4D5C800D61E4B /* Font.swift in Sources */, 965E810B1DD4D5C800D61E4B /* RobotoFont.swift in Sources */, 9D494A3C217F6B7D003D66F1 /* LayoutConstraint.swift in Sources */, 965E810C1DD4D5C800D61E4B /* DynamicFontType.swift in Sources */, 965E81101DD4D5C800D61E4B /* NavigationBar.swift in Sources */, 965E81111DD4D5C800D61E4B /* NavigationController.swift in Sources */, 965E81121DD4D5C800D61E4B /* NavigationItem.swift in Sources */, 965E81131DD4D5C800D61E4B /* NavigationDrawerController.swift in Sources */, 9656895F1F002F16001C656D /* CardCollectionViewCell.swift in Sources */, 965E81161DD4D5C800D61E4B /* DisplayStyle.swift in Sources */, 965E81171DD4D5C800D61E4B /* TransitionController.swift in Sources */, 965E81181DD4D5C800D61E4B /* Snackbar.swift in Sources */, 965E81191DD4D5C800D61E4B /* SnackbarController.swift in Sources */, 9618006D1F4D384200CD77A1 /* Material+UIViewController.swift in Sources */, 9DE84D741FF0252600586C8B /* CheckButtonGroup.swift in Sources */, 965E811A1DD4D5C800D61E4B /* StatusBarController.swift in Sources */, 965E811B1DD4D5C800D61E4B /* Switch.swift in Sources */, 965E811C1DD4D5C800D61E4B /* TabBar.swift in Sources */, 965E811D1DD4D5C800D61E4B /* TableViewCell.swift in Sources */, 9D9089B92118914500605DC9 /* Editor.swift in Sources */, 965E811E1DD4D5C800D61E4B /* TextField.swift in Sources */, 965E811F1DD4D5C800D61E4B /* ErrorTextField.swift in Sources */, 965E81211DD4D5C800D61E4B /* TextStorage.swift in Sources */, 965E81221DD4D5C800D61E4B /* TextView.swift in Sources */, 965E80E71DD4C55200D61E4B /* Material+UIView.swift in Sources */, 9D494A38217F6B63003D66F1 /* LayoutAttribute.swift in Sources */, 96B8D22D20CF82D5008BD149 /* FABMenu.swift in Sources */, 965E80E81DD4C55200D61E4B /* Material+CALayer.swift in Sources */, 965E80E91DD4C55200D61E4B /* Material+String.swift in Sources */, 9DE84D731FF0252600586C8B /* BaseButtonGroup.swift in Sources */, 965E80F71DD4D59500D61E4B /* Card.swift in Sources */, 965E80EA1DD4C55200D61E4B /* Material+UIFont.swift in Sources */, 965E80EB1DD4C55200D61E4B /* Material+UIImage.swift in Sources */, 965E80EC1DD4C55200D61E4B /* Material+Array.swift in Sources */, 965E80ED1DD4C55200D61E4B /* Material+UIWindow.swift in Sources */, 961527B91F3A509900E8B2AC /* ChipBarController.swift in Sources */, 965E80E41DD4C53300D61E4B /* PulseView.swift in Sources */, 9DF352421FED20C900B2A11B /* BaseIconLayerButton.swift in Sources */, 966C17731F0439F600D3E83C /* Material+MotionAnimation.swift in Sources */, 965E80E51DD4C53300D61E4B /* PulseAnimation.swift in Sources */, 9DE84D721FF0252600586C8B /* RadioButtonGroup.swift in Sources */, 9D00EBB4216675FB00DBCD69 /* Theme.swift in Sources */, 965E80FE1DD4D59500D61E4B /* ToolbarController.swift in Sources */, 9DE25DE22170D7C0000C04DF /* DialogController.swift in Sources */, 9DE25DE42170D7FF000C04DF /* DialogView.swift in Sources */, 96328B971E05C0BB009A4C90 /* TableView.swift in Sources */, 965E80F81DD4D59500D61E4B /* ImageCard.swift in Sources */, 96328B991E05C0CE009A4C90 /* TableViewController.swift in Sources */, 9DE25DE02170D7AF000C04DF /* Dialog.swift in Sources */, 965E80F91DD4D59500D61E4B /* PresenterCard.swift in Sources */, 96E09DC81F2287E50000B121 /* TabsController.swift in Sources */, 961154CC1F32A7B100A78D74 /* ChipBar.swift in Sources */, 965689611F002F4C001C656D /* CardCollectionViewController.swift in Sources */, 965E80CC1DD4C50600D61E4B /* Bar.swift in Sources */, 965E80CD1DD4C50600D61E4B /* Button.swift in Sources */, 965E80CE1DD4C50600D61E4B /* FABButton.swift in Sources */, 965E80CF1DD4C50600D61E4B /* FlatButton.swift in Sources */, 9D39A81B20FE8ED100BA8FA1 /* ViewController.swift in Sources */, 965E80D01DD4C50600D61E4B /* RaisedButton.swift in Sources */, 9DF58CEE20C098C60098968D /* ErrorTextFieldValidator.swift in Sources */, 9D054A6520D175AC00D0528D /* Material+UIButton.swift in Sources */, 965E80D11DD4C50600D61E4B /* IconButton.swift in Sources */, 965E80D21DD4C50600D61E4B /* Color.swift in Sources */, 96BFC1541E5E486F0075DE1F /* SpringAnimation.swift in Sources */, 9D054A6620D175AC00D0528D /* Material+UILabel.swift in Sources */, 9D494A3A217F6B70003D66F1 /* LayoutAnchor.swift in Sources */, 965E80D31DD4C50600D61E4B /* Device.swift in Sources */, 965E80FD1DD4D59500D61E4B /* Toolbar.swift in Sources */, 965E80D41DD4C50600D61E4B /* Divider.swift in Sources */, 965E80D51DD4C50600D61E4B /* Grid.swift in Sources */, 965E80D61DD4C50600D61E4B /* HeightPreset.swift in Sources */, 961E6BDF1DDA2A95004E6C93 /* Application.swift in Sources */, 965E80D71DD4C50600D61E4B /* Icon.swift in Sources */, 965E80FC1DD4D59500D61E4B /* SearchBarController.swift in Sources */, 965E80D81DD4C50600D61E4B /* Layer.swift in Sources */, 965E80D91DD4C50600D61E4B /* Layout.swift in Sources */, 965E80DA1DD4C50600D61E4B /* Border.swift in Sources */, 965E80DB1DD4C50600D61E4B /* InterimSpace.swift in Sources */, 965E80DC1DD4C50600D61E4B /* Depth.swift in Sources */, 965E80DD1DD4C50600D61E4B /* EdgeInsets.swift in Sources */, 9DF74C4E20D15D84003C1D66 /* Material+UIColor.swift in Sources */, 965E80DF1DD4C50600D61E4B /* CornerRadius.swift in Sources */, 965E80FB1DD4D59500D61E4B /* SearchBar.swift in Sources */, 965E80E01DD4C50600D61E4B /* Shape.swift in Sources */, 965E80E11DD4C50600D61E4B /* Offset.swift in Sources */, 965E80E21DD4C50600D61E4B /* View.swift in Sources */, 96B8D22C20CF82D2008BD149 /* FABMenuController.swift in Sources */, 96328B7A1E020A41009A4C90 /* CollectionViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 968BA8381F8D20530091852E /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = "Motion iOS"; targetProxy = 968BA8371F8D20530091852E /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ 9638324A1B88DFD80015F710 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MACOSX_DEPLOYMENT_TARGET = 10.9; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = Material; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; 9638324B1B88DFD80015F710 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.0; MACOSX_DEPLOYMENT_TARGET = 10.9; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_NAME = Material; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; 9638324D1B88DFD80015F710 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ANALYZER_OBJC_UNUSED_IVARS = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/**"; GCC_WARN_UNUSED_VALUE = YES; INFOPLIST_FILE = Sources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks @rpath/Frameworks"; MARKETING_VERSION = 3.1.8; PRODUCT_BUNDLE_IDENTIFIER = com.cosmicmind.Material; PRODUCT_NAME = Material; PROVISIONING_PROFILE = ""; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; 9638324E1B88DFD80015F710 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ANALYZER_OBJC_UNUSED_IVARS = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/**"; GCC_WARN_UNUSED_VALUE = YES; INFOPLIST_FILE = Sources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks @rpath/Frameworks"; MARKETING_VERSION = 3.1.8; PRODUCT_BUNDLE_IDENTIFIER = com.cosmicmind.Material; PRODUCT_NAME = Material; PROVISIONING_PROFILE = ""; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 963832301B88DFD80015F710 /* Build configuration list for PBXProject "Material" */ = { isa = XCConfigurationList; buildConfigurations = ( 9638324A1B88DFD80015F710 /* Debug */, 9638324B1B88DFD80015F710 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 9638324C1B88DFD80015F710 /* Build configuration list for PBXNativeTarget "Material" */ = { isa = XCConfigurationList; buildConfigurations = ( 9638324D1B88DFD80015F710 /* Debug */, 9638324E1B88DFD80015F710 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 9638322D1B88DFD80015F710 /* Project object */; } ================================================ FILE: Material.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Material.xcodeproj/project.xcworkspace/xcshareddata/Material.xcscmblueprint ================================================ { "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "9CF35BC641478FBD765F7442D4034ED362261E0A", "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { }, "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { "9CF35BC641478FBD765F7442D4034ED362261E0A" : 9223372036854775807, "0F3E254D46E5A5B90D1542854F510B7D145A9F31" : 9223372036854775807 }, "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "0F44C283-777E-4E3F-9E10-FC52C3C270CE", "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { "9CF35BC641478FBD765F7442D4034ED362261E0A" : "Material\/", "0F3E254D46E5A5B90D1542854F510B7D145A9F31" : "Material\/Sources\/Frameworks\/Motion\/" }, "DVTSourceControlWorkspaceBlueprintNameKey" : "Material", "DVTSourceControlWorkspaceBlueprintVersion" : 204, "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "Material.xcodeproj", "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ { "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/CosmicMind\/Motion.git", "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "0F3E254D46E5A5B90D1542854F510B7D145A9F31" }, { "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "https:\/\/github.com\/CosmicMind\/Material.git", "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "9CF35BC641478FBD765F7442D4034ED362261E0A" } ] } ================================================ FILE: Material.xcodeproj/xcshareddata/xcschemes/Material.xcscheme ================================================ ================================================ FILE: Package.swift ================================================ // swift-tools-version:4.2 import PackageDescription let package = Package( name: "Material", // platforms: [.iOS("8.0")], products: [ .library(name: "Material", targets: ["Material"]) ], dependencies: [ .package(url: "https://github.com/CosmicMind/Motion.git", .upToNextMajor(from: "3.1.0")), ], targets: [ .target( name: "Material", dependencies: ["Motion"], path: "Sources", exclude: ["Frameworks"] ) ] ) ================================================ FILE: README.md ================================================ ![Material](http://www.cosmicmind.com/material/github/material-logo.png) # Material Welcome to **Material,** a UI/UX framework for creating beautiful applications. Material's animation system has been completely reworked to take advantage of [Motion](https://github.com/CosmicMind/Motion), a library dedicated to animations and transitions. [![Carthage compatible](https://img.shields.io/badge/Carthage-Compatible-brightgreen.svg?style=flat)](https://github.com/Carthage/Carthage) [![Accio supported](https://img.shields.io/badge/Accio-supported-0A7CF5.svg?style=flat)](https://github.com/JamitLabs/Accio) [![Version](https://img.shields.io/cocoapods/v/Material.svg?style=flat)](http://cocoapods.org/pods/Material) [![License](https://img.shields.io/cocoapods/l/Material.svg?style=flat)](https://github.com/CosmicMind/Material/blob/master/LICENSE.md) ![Xcode 8.2+](https://img.shields.io/badge/Xcode-8.2%2B-blue.svg) ![iOS 8.0+](https://img.shields.io/badge/iOS-8.0%2B-blue.svg) ![Swift 4.0+](https://img.shields.io/badge/Swift-4.0%2B-orange.svg) [![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=9D6MURMLLUNQ2) ## Photos Sample Take a look at a sample [Photos](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/Photos) project to get started. ![Photos](http://www.cosmicmind.com/motion/projects/photos.gif) ## Sample Projects Take a look at [Sample Projects](https://github.com/CosmicMind/Samples) to get your projects started. ## Features - [x] Completely Customizable - [x] [Motion Animations & Transitions](https://github.com/CosmicMind/Motion) - [x] Layout Tools for AutoLayout & Grid Systems - [x] Color Library - [x] Cards - [x] FABMenu - [x] Icons - [x] TextField - [x] Snackbar - [x] Tabs - [x] Chips - [x] SearchBar - [x] NavigationController - [x] NavigationDrawer - [x] BottomNavigationBar - [x] [Sample Projects](https://github.com/CosmicMind/Samples) - [x] And More... ## Requirements * iOS 8.0+ * Xcode 8.0+ ## Communication - If you **need help**, use [Stack Overflow](http://stackoverflow.com/questions/tagged/cosmicmind). (Tag 'cosmicmind') - If you'd like to **ask a general question**, use [Stack Overflow](http://stackoverflow.com/questions/tagged/cosmicmind). - If you **found a bug**, _and can provide steps to reliably reproduce it_, open an issue. - If you **have a feature request**, open an issue. - If you **want to contribute**, submit a pull request. ## Installation > **Embedded frameworks require a minimum deployment target of iOS 8+.** > - [Download Material](https://github.com/CosmicMind/Material/archive/master.zip) ## CocoaPods [CocoaPods](http://cocoapods.org) is a dependency manager for Cocoa projects. You can install it with the following command: ```bash $ gem install cocoapods ``` To integrate Material's core features into your Xcode project using CocoaPods, specify it in your `Podfile`: ```ruby source 'https://github.com/CocoaPods/Specs.git' platform :ios, '8.0' use_frameworks! pod 'Material', '~> 3.1.0' ``` Then, run the following command: ```bash $ pod install ``` ## Carthage Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. You can install Carthage with Homebrew using the following command: ```bash $ brew update $ brew install carthage ``` To integrate Material into your Xcode project using Carthage, specify it in your Cartfile: ```bash github "CosmicMind/Material" ``` Run `carthage update` to build the framework and drag the built `Material.framework` into your Xcode project. ## Change Log Material is a growing project and will encounter changes throughout its development. It is recommended that the [Change Log](https://github.com/CosmicMind/Material/blob/master/CHANGELOG.md) be reviewed prior to updating versions. ## Icons Icons is a library of Google and CosmicMind icons that are available for use within your iOS applications. ![Icon](http://www.cosmicmind.com/gifs/marketplace/icons.png) [Learn More](https://github.com/CosmicMind/Material/blob/master/Sources/iOS/Icon/Icon.swift) ## Colors Try the Material Colors app to see the wonderful colors available in Material, or use the online version at [MaterialColor.com](http://materialcolor.com). ![MaterialColors](http://www.cosmicmind.com/gifs/shared/colors.gif) * [Material Colors iOS App](https://itunes.apple.com/app/x/id1111994400?mt=8) ## TextField A TextField is an excellent way to improve UX. It allows for a placeholder and additional hint details. ![TextField](http://www.cosmicmind.com/gifs/white/text-field.gif) * [TextField Sample](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/TextField) ## Button A button is used to trigger an action through a touch event. Material comes with a foundational button, and 4 specialized buttons that can be stylized in any way. ![Material Image](http://www.cosmicmind.com/material/white/button.gif) * [Button Sample](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/Button) ## Switch A switch is a control component that toggles between on and off states. ![Material Image](http://www.cosmicmind.com/material/white/switch.gif) * [Switch Sample](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/Switch) ## Card A Card is a flexible component that may be configured in any way you like. It has a Toolbar, Bar, and content area that may utilize any UIView type. ![Material Image](http://www.cosmicmind.com/gifs/white/card.gif) * [Card Sample](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/Card) ## ImageCard An ImageCard is an expansion of the base Card. The Toolbar overlays an image area that sits above the dynamic content area. ![Image Card Sample](http://www.cosmicmind.com/gifs/white/image-card.gif) * [ImageCard Sample](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/ImageCard) ## PresenterCard The PresenterCard is a completely new card style. It allows for a primary presentation area that may be any UIView type in addition to the content area, Toolbar, and Bar components. The options for this card are endless. ![Presenter Card Sample](http://www.cosmicmind.com/gifs/white/presenter-card.gif) * [PresenterCard Sample](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/PresenterCard) ## FABMenu A FABMenu manages a collection of views. A new MenuItem type has been added that manages a title and button to improve UX and visual beauty. ![Material Image](http://www.cosmicmind.com/material/white/menu-controller.gif) * [FABMenu Sample](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/FABMenuController) ## Toolbar Toolbars are super flexible and add excellent control to your navigation flow. They manage a set of left and right views with auto aligning title and detail labels. ![Material Image](http://www.cosmicmind.com/gifs/white/toolbar-controller.gif) * [Toolbar Sample](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/ToolbarController) ## SearchBar A SearchBar is a powerful navigation tool that allows for user's input with an instant visual response. A set of left and right views may be added to expand functionality. ![SearchBarController](http://www.cosmicmind.com/gifs/shared/search-bar-controller.gif) * [SearchBar Sample](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/Search) ## Tabs Tabs is a new component that links a customizable TabBar to a stack of view controllers making a powerful and visually pleasing component to have in any application. ![Tabs](http://www.cosmicmind.com/material/white/page-tab-bar-controller.gif) * [Tabs Sample](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/TabsController) ## NavigationController A NavigationController is a specialized view controller that manages a hierarchy of content efficiently, making it easier for users to move within an application. ![Material Image](http://www.cosmicmind.com/gifs/white/navigation-controller.gif) * [NavigationController Sample](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/NavigationController) ## NavigationDrawer A NavigationDrawer slides in from the left or right and contains the navigation destinations for your application. ![Material Image](http://www.cosmicmind.com/material/shared/navigation-drawer-controller.gif) * [NavigationDrawer Sample](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/NavigationDrawerController) ## Snackbar A Snackbar is a new component that is very simple in its behavior and very powerful in its message. It can be used application wide, or isolated to specific view controllers. ![Material Image](http://www.cosmicmind.com/material/white/snackbar-controller.gif) * [Snackbar Sample](https://github.com/CosmicMind/Samples/tree/master/Projects/Programmatic/SnackbarController) ## Sticker Sheet To help template your project, checkout Material Sticker Sheet. ![MaterialStickerSheet](http://www.cosmicmind.com/MK/material_iso_1.png) * [Material Sticker Sheet](http://www.materialup.com/posts/material-design-sticker-sheets) ## Much More... So much more inside. Enjoy! ## License The MIT License (MIT) Copyright (C) 2019, CosmicMind, Inc. . All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Sources/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_add_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_add_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_add_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_add_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_arrow_back_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_arrow_back_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_arrow_back_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_arrow_back_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_arrow_downward_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_arrow_downward_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_arrow_downward_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_arrow_downward_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_audio_library_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_audio_library_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_audio_library_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_audio_library_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_audio_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_audio_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_audio_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_audio_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_bell_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_bell_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_bell_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_bell_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_check_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_check_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_check_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_check_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_close_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_close_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_close_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_close_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_image_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_image_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_image_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_image_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_menu_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_menu_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_menu_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_menu_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_microphone_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_microphone_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_microphone_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_microphone_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_more_horiz_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_more_horiz_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_more_horiz_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_more_horiz_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_more_vert_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_more_vert_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_more_vert_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_more_vert_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_movie_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_movie_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_movie_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_movie_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_pause_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_pause_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_pause_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_pause_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_pen_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_pen_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_pen_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_pen_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_photo_camera_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_photo_camera_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_photo_camera_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_photo_camera_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_photo_library_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_photo_library_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_photo_library_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_photo_library_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_play_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_play_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_play_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_play_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_search_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_search_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_search_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_search_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_settings_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_settings_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_settings_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_settings_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_share_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_share_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_share_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_share_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_shuffle_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_shuffle_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_shuffle_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_shuffle_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_skip_backward_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_skip_backward_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_skip_backward_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_skip_backward_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_skip_forward_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_skip_forward_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_skip_forward_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_skip_forward_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_star_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_star_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_star_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_star_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_videocam_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_videocam_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_videocam_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_videocam_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_volume_high_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_volume_high_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_volume_high_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_volume_high_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_volume_medium_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_volume_medium_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_volume_medium_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_volume_medium_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/cm_volume_off_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "cm_volume_off_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "cm_volume_off_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "cm_volume_off_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_add_circle_outline_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_add_circle_outline_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_add_circle_outline_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_add_circle_outline_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_add_circle_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_add_circle_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_add_circle_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_add_circle_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_add_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_add_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_add_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_add_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_arrow_back_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_arrow_back_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_arrow_back_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_arrow_back_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_arrow_downward_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_arrow_downward_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_arrow_downward_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_arrow_downward_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_audiotrack_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_audiotrack_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_audiotrack_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_audiotrack_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_camera_front_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_camera_front_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_camera_front_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_camera_front_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_camera_rear_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_camera_rear_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_camera_rear_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_camera_rear_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_check_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_check_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_check_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_check_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_close_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_close_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_close_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_close_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_edit_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_edit_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_edit_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_edit_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_email_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_email_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_email_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_email_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_favorite_border_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_favorite_border_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_favorite_border_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_favorite_border_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_favorite_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_favorite_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_favorite_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_favorite_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_flash_auto_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_flash_auto_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_flash_auto_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_flash_auto_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_flash_off_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_flash_off_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_flash_off_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_flash_off_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_flash_on_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_flash_on_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_flash_on_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_flash_on_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_history_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_history_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_history_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_history_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_home_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_home_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_home_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_home_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_image_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_image_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_image_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_image_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_menu_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_menu_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_menu_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_menu_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_more_horiz_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_more_horiz_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_more_horiz_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_more_horiz_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_more_vert_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_more_vert_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_more_vert_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_more_vert_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_movie_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_movie_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_movie_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_movie_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_phone_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_phone_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_phone_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_phone_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_photo_camera_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_photo_camera_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_photo_camera_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_photo_camera_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_photo_library_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_photo_library_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_photo_library_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_photo_library_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_place_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_place_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_place_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_place_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_search_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_search_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_search_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_search_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_settings_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_settings_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_settings_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_settings_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_share_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_share_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_share_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_share_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_star_border_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_star_border_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_star_border_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_star_border_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_star_half_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_star_half_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_star_half_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_star_half_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_star_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_star_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_star_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_star_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_videocam_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_videocam_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_videocam_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_videocam_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_visibility_off_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_visibility_off_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_visibility_off_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_visibility_off_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_visibility_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_visibility_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_visibility_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_visibility_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Assets.xcassets/ic_work_white.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "ic_work_white.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "ic_work_white@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "ic_work_white@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Sources/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType FMWK CFBundleShortVersionString $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) LSApplicationCategoryType NSHumanReadableCopyright Copyright © 2017 CosmicMind, Inc. All rights reserved. NSPrincipalClass UIAppFonts Roboto-Thin Roboto-Light.ttf Roboto-Regular.ttf Roboto-Medium.ttf Roboto-Bold ================================================ FILE: Sources/LICENSE ================================================ The MIT License (MIT) Copyright (C) 2019, CosmicMind, Inc. . All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Sources/Material.h ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #import //! Project version number for Material. FOUNDATION_EXPORT double MaterialVersionNumber; //! Project version string for Material. FOUNDATION_EXPORT const unsigned char MaterialVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import ================================================ FILE: Sources/iOS/Animation/PulseAnimation.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion @objc(PulseAnimation) public enum PulseAnimation: Int { case none case center case centerWithBacking case centerRadialBeyondBounds case radialBeyondBounds case backing case point case pointWithBacking } public protocol Pulseable { /// A reference to the PulseAnimation. var pulseAnimation: PulseAnimation { get set } /// A UIColor. var pulseColor: UIColor { get set } /// The opcaity value for the pulse animation. var pulseOpacity: CGFloat { get set } } internal protocol PulseableLayer { /// A reference to the pulse layer. var pulseLayer: CALayer? { get } } public struct Pulse { /// A UIView that is Pulseable. fileprivate weak var pulseView: UIView? /// The layer the pulse layers are added to. internal weak var pulseLayer: CALayer? /// Pulse layers. fileprivate var layers = [CAShapeLayer]() /// A reference to the PulseAnimation. public var animation = PulseAnimation.pointWithBacking /// A UIColor. public var color = Color.grey.base /// The opcaity value for the pulse animation. public var opacity: CGFloat = 0.18 /** An initializer that takes a given view and pulse layer. - Parameter pulseView: An optional UIView. - Parameter pulseLayer: An optional CALayer. */ public init(pulseView: UIView?, pulseLayer: CALayer?) { self.pulseView = pulseView self.pulseLayer = pulseLayer } /** Triggers the expanding animation. - Parameter point: A point to pulse from. */ public mutating func expand(point: CGPoint) { guard let view = pulseView else { return } guard let layer = pulseLayer else { return } guard .none != animation else { return } let bLayer = CAShapeLayer() let pLayer = CAShapeLayer() bLayer.addSublayer(pLayer) layer.addSublayer(bLayer) bLayer.zPosition = 0 pLayer.zPosition = 0 layers.insert(bLayer, at: 0) layer.masksToBounds = !(.centerRadialBeyondBounds == animation || .radialBeyondBounds == animation) let w = view.bounds.width let h = view.bounds.height Motion.disable { [ n = .center == animation ? min(w, h) : max(w, h), bounds = layer.bounds, animation = animation, color = color, opacity = opacity ] in bLayer.frame = bounds pLayer.frame = CGRect(x: 0, y: 0, width: n, height: n) switch animation { case .center, .centerWithBacking, .centerRadialBeyondBounds: pLayer.position = CGPoint(x: w / 2, y: h / 2) default: pLayer.position = point } pLayer.cornerRadius = n / 2 pLayer.backgroundColor = color.withAlphaComponent(opacity).cgColor pLayer.transform = CATransform3DMakeAffineTransform(CGAffineTransform(scaleX: 0, y: 0)) } bLayer.setValue(false, forKey: "animated") let t: TimeInterval = .center == animation ? 0.16125 : 0.325 switch animation { case .centerWithBacking, .backing, .pointWithBacking: bLayer.animate(.background(color: color.withAlphaComponent(opacity / 2)), .duration(t)) default:break } switch animation { case .center, .centerWithBacking, .centerRadialBeyondBounds, .radialBeyondBounds, .point, .pointWithBacking: pLayer.animate(.scale(1), .duration(t)) default:break } Motion.delay(t) { bLayer.setValue(true, forKey: "animated") } } /// Triggers the contracting animation. public mutating func contract() { guard let bLayer = layers.popLast() else { return } guard let animated = bLayer.value(forKey: "animated") as? Bool else { return } Motion.delay(animated ? 0 : 0.15) { [animation = animation, color = color] in guard let pLayer = bLayer.sublayers?.first as? CAShapeLayer else { return } let t: TimeInterval = 0.325 switch animation { case .centerWithBacking, .backing, .pointWithBacking: bLayer.animate(.background(color: color.withAlphaComponent(0)), .duration(t)) default:break } switch animation { case .center, .centerWithBacking, .centerRadialBeyondBounds, .radialBeyondBounds, .point, .pointWithBacking: pLayer.animate(.background(color: color.withAlphaComponent(0))) default:break } Motion.delay(t) { pLayer.removeFromSuperlayer() bLayer.removeFromSuperlayer() } } } } ================================================ FILE: Sources/iOS/Animation/SpringAnimation.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit @objc(SpringDirection) public enum SpringDirection: Int { case up case down case left case right } open class SpringAnimation { /// A SpringDirection value. open var springDirection = SpringDirection.up /// A Boolean that indicates if the menu is open or not. open var isOpened = false /// Enables the animations for the Menu. open var isEnabled = true /// A preset wrapper around interimSpace. open var interimSpacePreset = InterimSpacePreset.none { didSet { interimSpace = InterimSpacePresetToValue(preset: interimSpacePreset) } } /// The space between views. open var interimSpace: InterimSpace = 0 { didSet { reload() } } /// An Array of UIViews. open var views = [UIView]() { didSet { reload() } } /// An Optional base view size. open var baseSize = CGSize(width: 48, height: 48) { didSet { reload() } } /// Size of views. open var itemSize = CGSize(width: 48, height: 48) { didSet { reload() } } /// Reload the view layout. open func reload() { isOpened = false for i in 0.. Void)? = nil, completion: ((UIView) -> Void)? = nil) { guard isEnabled else { return } disable() switch springDirection { case .up: expandUp(duration: duration, delay: delay, usingSpringWithDamping: usingSpringWithDamping, initialSpringVelocity: initialSpringVelocity, options: options, animations: animations, completion: completion) case .down: expandDown(duration: duration, delay: delay, usingSpringWithDamping: usingSpringWithDamping, initialSpringVelocity: initialSpringVelocity, options: options, animations: animations, completion: completion) case .left: expandLeft(duration: duration, delay: delay, usingSpringWithDamping: usingSpringWithDamping, initialSpringVelocity: initialSpringVelocity, options: options, animations: animations, completion: completion) case .right: expandRight(duration: duration, delay: delay, usingSpringWithDamping: usingSpringWithDamping, initialSpringVelocity: initialSpringVelocity, options: options, animations: animations, completion: completion) } } /** Contracts the Spring component with animation options. - Parameter duration: The time for each view's animation. - Parameter delay: A delay time for each view's animation. - Parameter usingSpringWithDamping: A damping ratio for the animation. - Parameter initialSpringVelocity: The initial velocity for the animation. - Parameter options: Options to pass to the animation. - Parameter animations: An animation block to execute on each view's animation. - Parameter completion: A completion block to execute on each view's animation. */ open func contract(duration: TimeInterval = 0.15, delay: TimeInterval = 0, usingSpringWithDamping: CGFloat = 0.5, initialSpringVelocity: CGFloat = 0, options: UIView.AnimationOptions = [], animations: ((UIView) -> Void)? = nil, completion: ((UIView) -> Void)? = nil) { guard isEnabled else { return } disable() switch springDirection { case .up: contractUp(duration: duration, delay: delay, usingSpringWithDamping: usingSpringWithDamping, initialSpringVelocity: initialSpringVelocity, options: options, animations: animations, completion: completion) case .down: contractDown(duration: duration, delay: delay, usingSpringWithDamping: usingSpringWithDamping, initialSpringVelocity: initialSpringVelocity, options: options, animations: animations, completion: completion) case .left: contractLeft(duration: duration, delay: delay, usingSpringWithDamping: usingSpringWithDamping, initialSpringVelocity: initialSpringVelocity, options: options, animations: animations, completion: completion) case .right: contractRight(duration: duration, delay: delay, usingSpringWithDamping: usingSpringWithDamping, initialSpringVelocity: initialSpringVelocity, options: options, animations: animations, completion: completion) } } } extension SpringAnimation { /** Handles the animation open completion. - Parameter view: A UIView. - Parameter completion: A completion handler. */ fileprivate func handleOpenCompletion(view: UIView, completion: ((UIView) -> Void)?) { enable(view: view) if view == views.last { isOpened = true } completion?(view) } /** Handles the animation contract completion. - Parameter view: A UIView. - Parameter completion: A completion handler. */ fileprivate func handleCloseCompletion(view: UIView, completion: ((UIView) -> Void)?) { view.isHidden = true enable(view: view) if view == views.last { isOpened = false } completion?(view) } } extension SpringAnimation { /** Open the Menu component with animation options in the Up direction. - Parameter duration: The time for each view's animation. - Parameter delay: A delay time for each view's animation. - Parameter usingSpringWithDamping: A damping ratio for the animation. - Parameter initialSpringVelocity: The initial velocity for the animation. - Parameter options: Options to pass to the animation. - Parameter animations: An animation block to execute on each view's animation. - Parameter completion: A completion block to execute on each view's animation. */ fileprivate func expandUp(duration: TimeInterval, delay: TimeInterval, usingSpringWithDamping: CGFloat, initialSpringVelocity: CGFloat, options: UIView.AnimationOptions, animations: ((UIView) -> Void)?, completion: ((UIView) -> Void)?) { for i in 0.. Void)?, completion: ((UIView) -> Void)?) { for i in 0.. Void)?, completion: ((UIView) -> Void)?) { for i in 0.. Void)?, completion: ((UIView) -> Void)?) { guard let first = views.first else { return } for i in 0.. Void)?, completion: ((UIView) -> Void)?) { for i in 0.. Void)?, completion: ((UIView) -> Void)?) { guard let first = views.first else { return } for i in 0.. Void)?, completion: ((UIView) -> Void)?) { for i in 0.. Void)?, completion: ((UIView) -> Void)?) { guard let first = views.first else { return } let w = baseSize.width for i in 0... * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit public struct Application { /// A reference to UIApplication.shared application which doesn't trigger linker errors when Material is included in an extension /// Note that this will crash if actually called from within an extension public static var shared: UIApplication { let sharedSelector = NSSelectorFromString("sharedApplication") guard UIApplication.responds(to: sharedSelector) else { fatalError("[Material: Extensions cannot access Application]") } let shared = UIApplication.perform(sharedSelector) return shared?.takeUnretainedValue() as! UIApplication } /// An optional reference to the main UIWindow. public static var keyWindow: UIWindow? { return Application.shared.keyWindow } /// An optional reference to the top most view controller. public static var rootViewController: UIViewController? { return keyWindow?.rootViewController } /// A boolean indicating if the device is in Landscape mode. public static var isLandscape: Bool { return Application.shared.statusBarOrientation.isLandscape } /// A boolean indicating if the device is in Portrait mode. public static var isPortrait: Bool { return !isLandscape } /// The current UIInterfaceOrientation value. public static var orientation: UIInterfaceOrientation { return Application.shared.statusBarOrientation } /// Retrieves the device status bar style. public static var statusBarStyle: UIStatusBarStyle { get { return Application.shared.statusBarStyle } set(value) { Application.shared.statusBarStyle = value } } /// Retrieves the device status bar hidden state. public static var isStatusBarHidden: Bool { get { return Application.shared.isStatusBarHidden } set(value) { Application.shared.isStatusBarHidden = value } } /** A boolean that indicates based on iPhone rules if the status bar should be shown. */ public static var shouldStatusBarBeHidden: Bool { return isLandscape && .phone == Device.userInterfaceIdiom } /// A reference to the user interface layout direction. public static var userInterfaceLayoutDirection: UIUserInterfaceLayoutDirection { return Application.shared.userInterfaceLayoutDirection } } ================================================ FILE: Sources/iOS/Bar/Bar.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit @objc(ContentViewAlignment) public enum ContentViewAlignment: Int { case full case center } open class Bar: View { /// Will layout the view. open var willLayout: Bool { return 0 < bounds.width && 0 < bounds.height && nil != superview && !grid.isDeferred } open override var intrinsicContentSize: CGSize { return bounds.size } /// Should center the contentView. open var contentViewAlignment = ContentViewAlignment.full { didSet { layoutSubviews() } } /// A preset wrapper around contentEdgeInsets. open var contentEdgeInsetsPreset: EdgeInsetsPreset { get { return grid.contentEdgeInsetsPreset } set(value) { grid.contentEdgeInsetsPreset = value } } /// A reference to EdgeInsets. @IBInspectable open var contentEdgeInsets: EdgeInsets { get { return grid.contentEdgeInsets } set(value) { grid.contentEdgeInsets = value } } /// A preset wrapper around interimSpace. open var interimSpacePreset: InterimSpacePreset { get { return grid.interimSpacePreset } set(value) { grid.interimSpacePreset = value } } /// A wrapper around grid.interimSpace. @IBInspectable open var interimSpace: InterimSpace { get { return grid.interimSpace } set(value) { grid.interimSpace = value } } /// Grid cell factor. @IBInspectable open var gridFactor: CGFloat = 12 { didSet { assert(0 < gridFactor, "[Material Error: gridFactor must be greater than 0.]") layoutSubviews() } } /// ContentView that holds the any desired subviews. public let contentView = UIView() /// Left side UIViews. open var leftViews = [UIView]() { didSet { oldValue.forEach { $0.removeFromSuperview() } layoutSubviews() } } /// Right side UIViews. open var rightViews = [UIView]() { didSet { oldValue.forEach { $0.removeFromSuperview() } layoutSubviews() } } /// Center UIViews. open var centerViews: [UIView] { get { return contentView.grid.views } set(value) { contentView.grid.views = value } } /** An initializer that initializes the object with a NSCoder object. - Parameter aDecoder: A NSCoder instance. */ public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } /** An initializer that initializes the object with a CGRect object. If AutoLayout is used, it is better to initilize the instance using the init() initializer. - Parameter frame: A CGRect instance. */ public override init(frame: CGRect) { super.init(frame: frame) } /** A convenience initializer with parameter settings. - Parameter leftViews: An Array of UIViews that go on the left side. - Parameter rightViews: An Array of UIViews that go on the right side. - Parameter centerViews: An Array of UIViews that go in the center. */ public convenience init(leftViews: [UIView]? = nil, rightViews: [UIView]? = nil, centerViews: [UIView]? = nil) { self.init() self.leftViews = leftViews ?? [] self.rightViews = rightViews ?? [] self.centerViews = centerViews ?? [] } open override func layoutSubviews() { super.layoutSubviews() guard willLayout else { return } var lc = 0 var rc = 0 grid.begin() grid.views.removeAll() for v in leftViews { if let b = v as? UIButton { b.contentEdgeInsets = .zero b.titleEdgeInsets = .zero } v.frame.size.width = v.intrinsicContentSize.width v.sizeToFit() v.grid.columns = Int(ceil(v.bounds.width / gridFactor)) + 2 lc += v.grid.columns grid.views.append(v) } grid.views.append(contentView) for v in rightViews { if let b = v as? UIButton { b.contentEdgeInsets = .zero b.titleEdgeInsets = .zero } v.frame.size.width = v.intrinsicContentSize.width v.sizeToFit() v.grid.columns = Int(ceil(v.bounds.width / gridFactor)) + 2 rc += v.grid.columns grid.views.append(v) } contentView.grid.begin() contentView.grid.offset.columns = 0 var l: CGFloat = 0 var r: CGFloat = 0 if .center == contentViewAlignment { if leftViews.count < rightViews.count { r = CGFloat(rightViews.count) * interimSpace l = r } else { l = CGFloat(leftViews.count) * interimSpace r = l } } let p = bounds.width - l - r - contentEdgeInsets.left - contentEdgeInsets.right let columns = Int(ceil(p / gridFactor)) if .center == contentViewAlignment { if lc < rc { contentView.grid.columns = columns - 2 * rc contentView.grid.offset.columns = rc - lc } else { contentView.grid.columns = columns - 2 * lc rightViews.first?.grid.offset.columns = lc - rc } } else { contentView.grid.columns = columns - lc - rc } grid.axis.columns = columns grid.commit() contentView.grid.commit() layoutDivider() } open override func prepare() { super.prepare() heightPreset = .normal autoresizingMask = .flexibleWidth interimSpacePreset = .interimSpace3 contentEdgeInsetsPreset = .square1 prepareContentView() } } extension Bar { /// Prepares the contentView. fileprivate func prepareContentView() { contentView.contentScaleFactor = Screen.scale } } ================================================ FILE: Sources/iOS/BottomTabBar/BottomNavigationController.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion extension UIViewController { /** A convenience property that provides access to the BottomNavigationController. This is the recommended method of accessing the BottomNavigationController through child UIViewControllers. */ public var bottomNavigationController: BottomNavigationController? { return traverseViewControllerHierarchyForClassType() } } private class MaterialTabBar: UITabBar { override func sizeThatFits(_ size: CGSize) -> CGSize { var v = super.sizeThatFits(size) let offset = v.height - HeightPreset.normal.rawValue v.height = heightPreset.rawValue + offset return v } } open class BottomNavigationController: UITabBarController, Themeable { /// A Boolean that controls if the swipe feature is enabled. open var isSwipeEnabled = true { didSet { guard isSwipeEnabled else { removeSwipeGesture() return } prepareSwipeGesture() } } /** A UIPanGestureRecognizer property internally used for the interactive swipe. */ public private(set) var interactiveSwipeGesture: UIPanGestureRecognizer? /** A private integer for storing index of current view controller during interactive transition. */ private var currentIndex = -1 /** An initializer that initializes the object with a NSCoder object. - Parameter aDecoder: A NSCoder instance. */ public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setTabBarClass() } /** An initializer that initializes the object with an Optional nib and bundle. - Parameter nibNameOrNil: An Optional String for the nib. - Parameter bundle: An Optional NSBundle where the nib is located. */ public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) setTabBarClass() } /// An initializer that accepts no parameters. public init() { super.init(nibName: nil, bundle: nil) setTabBarClass() } /** An initializer that initializes the object an Array of UIViewControllers. - Parameter viewControllers: An Array of UIViewControllers. */ public init(viewControllers: [UIViewController]) { super.init(nibName: nil, bundle: nil) setTabBarClass() self.viewControllers = viewControllers } open override func viewDidLoad() { super.viewDidLoad() prepare() } open override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() layoutSubviews() } /** To execute in the order of the layout chain, override this method. `layoutSubviews` should be called immediately, unless you have a certain need. */ open func layoutSubviews() { tabBar.layoutDivider() } /** Prepares the view instance when intialized. When subclassing, it is recommended to override the prepare method to initialize property values and other setup operations. The super.prepare method should always be called immediately when subclassing. */ open func prepare() { view.clipsToBounds = true view.backgroundColor = .white view.contentScaleFactor = Screen.scale prepareTabBar() isSwipeEnabled = true isMotionEnabled = true applyCurrentTheme() } /** Applies the given theme. - Parameter theme: A Theme. */ open func apply(theme: Theme) { tabBar.tintColor = theme.secondary tabBar.barTintColor = theme.background tabBar.dividerColor = theme.onSurface.withAlphaComponent(0.12) if #available(iOS 10.0, *) { tabBar.unselectedItemTintColor = theme.onSurface.withAlphaComponent(0.54) } } } private extension BottomNavigationController { /** A target method contolling interactive swipe transition based on gesture recognizer. - Parameter _ gesture: A UIPanGestureRecognizer. */ @objc func handleTransitionPan(_ gesture: UIPanGestureRecognizer) { guard selectedIndex != NSNotFound else { return } let translationX = gesture.translation(in: nil).x let velocityX = gesture.velocity(in: nil).x switch gesture.state { case .began, .changed: let isSlidingLeft = currentIndex == -1 ? velocityX < 0 : translationX < 0 if currentIndex == -1 { currentIndex = selectedIndex } let nextIndex = currentIndex + (isSlidingLeft ? 1 : -1) if selectedIndex != nextIndex { /// 5 point threshold guard abs(translationX) > 5 else { return } if currentIndex != selectedIndex { MotionTransition.shared.cancel(isAnimated: false) } guard canSelect(at: nextIndex) else { return } selectedIndex = nextIndex MotionTransition.shared.setCompletionCallbackForNextTransition { [weak self] isFinishing in guard let `self` = self, isFinishing else { return } self.delegate?.tabBarController?(self, didSelect: self.viewControllers![nextIndex]) } } else { let progress = abs(translationX / view.bounds.width) MotionTransition.shared.update(Double(progress)) } default: let progress = (translationX + velocityX) / view.bounds.width let isUserHandDirectionLeft = progress < 0 let isTargetHandDirectionLeft = selectedIndex > currentIndex if isUserHandDirectionLeft == isTargetHandDirectionLeft && abs(progress) > 0.5 { MotionTransition.shared.finish() } else { MotionTransition.shared.cancel() } currentIndex = -1 } } /// Prepares interactiveSwipeGesture. func prepareSwipeGesture() { guard nil == interactiveSwipeGesture else { return } interactiveSwipeGesture = UIPanGestureRecognizer(target: self, action: #selector(handleTransitionPan)) view.addGestureRecognizer(interactiveSwipeGesture!) } /// Removes interactiveSwipeGesture. func removeSwipeGesture() { guard let v = interactiveSwipeGesture else { return } view.removeGestureRecognizer(v) interactiveSwipeGesture = nil } } private extension BottomNavigationController { /// Sets tabBar class to MaterialTabBar. func setTabBarClass() { guard object_getClass(tabBar) === UITabBar.self else { return } object_setClass(tabBar, MaterialTabBar.self) } } private extension BottomNavigationController { /** Checks if the view controller at a given index can be selected. - Parameter at index: An Int. */ func canSelect(at index: Int) -> Bool { guard index != selectedIndex else { return false } let lastTabIndex = (tabBar.items?.count ?? 1) - 1 guard (0...lastTabIndex).contains(index) else { return false } guard !(index == lastTabIndex && tabBar.items?.last == moreNavigationController.tabBarItem) else { return false } let vc = viewControllers![index] guard delegate?.tabBarController?(self, shouldSelect: vc) != false else { return false } return true } } private extension BottomNavigationController { /// Prepares the tabBar. func prepareTabBar() { tabBar.isTranslucent = false tabBar.heightPreset = .normal tabBar.dividerColor = Color.grey.lighten2 tabBar.dividerAlignment = .top let image = UIImage() tabBar.shadowImage = image tabBar.backgroundImage = image tabBar.backgroundColor = .white } } ================================================ FILE: Sources/iOS/Button/BaseIconLayerButton.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion /// Implements common logic for CheckButton and RadioButton open class BaseIconLayerButton: Button { class var iconLayer: BaseIconLayer { fatalError("Has to be implemented by subclasses") } lazy var iconLayer: BaseIconLayer = type(of: self).iconLayer /// A Boolean value indicating whether the button is in the selected state /// /// Use `setSelected(_:, animated:)` if the state change needs to be animated open override var isSelected: Bool { didSet { iconLayer.setSelected(isSelected, animated: false) updatePulseColor() } } /// A Boolean value indicating whether the control is enabled. open override var isEnabled: Bool { didSet { iconLayer.isEnabled = isEnabled } } /// Sets the color of the icon to use for the specified state. /// /// - Parameters: /// - color: The color of the icon to use for the specified state. /// - state: The state that uses the specified color. Supports only (.normal, .selected, .disabled) open func setIconColor(_ color: UIColor, for state: UIControl.State) { switch state { case .normal: iconLayer.normalColor = color case .selected: iconLayer.selectedColor = color case .disabled: iconLayer.disabledColor = color default: fatalError("unsupported state") } } /// Returns the icon color used for a state. /// /// - Parameter state: The state that uses the icon color. Supports only (.normal, .selected, .disabled) /// - Returns: The color of the title for the specified state. open func iconColor(for state: UIControl.State) -> UIColor { switch state { case .normal: return iconLayer.normalColor case .selected: return iconLayer.selectedColor case .disabled: return iconLayer.disabledColor default: fatalError("unsupported state") } } /// A Boolean value indicating whether the button is being animated open var isAnimating: Bool { return iconLayer.isAnimating } /// Sets the `selected` state of the button, optionally animating the transition. /// /// - Parameters: /// - isSelected: A Boolean value indicating new `selected` state /// - animated: true if the state change should be animated, otherwise false. open func setSelected(_ isSelected: Bool, animated: Bool) { guard !isAnimating else { return } iconLayer.setSelected(isSelected, animated: animated) self.isSelected = isSelected } open override func prepare() { super.prepare() layer.addSublayer(iconLayer) iconLayer.prepare() contentHorizontalAlignment = .left // default was .center reloadImage() } open override func touchesBegan(_ touches: Set, with event: UIEvent?) { // pulse.animation set to .none so that when we call `super.touchesBegan` // pulse will not expand as there is a `guard` against .none case pulse.animation = .none super.touchesBegan(touches, with: event) pulse.animation = .point // expand pulse from the center of iconLayer/visualLayer (`point` is relative to self.view/self.layer) pulse.expand(point: iconLayer.frame.center) } open override func layoutSubviews() { super.layoutSubviews() // positioning iconLayer let insets = iconEdgeInsets iconLayer.frame.size = CGSize(width: iconSize, height: iconSize) iconLayer.frame.origin = CGPoint(x: imageView!.frame.minX + insets.left, y: imageView!.frame.minY + insets.top) // visualLayer is the layer where pulse layer is expanding. // So we position it at the center of iconLayer, and make it // small circle, so that the expansion of pulse layer is clipped off let w = iconSize + insets.left + insets.right let h = iconSize + insets.top + insets.bottom let pulseSize = min(w, h) visualLayer.bounds.size = CGSize(width: pulseSize, height: pulseSize) visualLayer.frame.center = iconLayer.frame.center visualLayer.cornerRadius = pulseSize / 2 } /// Size of the icon /// /// This property affects `intrinsicContentSize` and `sizeThatFits(_:)` /// Use `iconEdgeInsets` to set margins. open var iconSize: CGFloat = 18 { didSet { reloadImage() } } /// The *outset* margins for the rectangle around the button’s icon. /// /// You can specify a different value for each of the four margins (top, left, bottom, right) /// This property affects `intrinsicContentSize` and `sizeThatFits(_:)` and position of the icon /// within the rectangle. /// /// You can use `iconSize` and this property, or `titleEdgeInsets` and `contentEdgeInsets` to position /// the icon however you want. /// For negative values, behavior is undefined. Default is `8.0` for all four margins open var iconEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) { didSet { reloadImage() } } open override func apply(theme: Theme) { super.apply(theme: theme) setIconColor(theme.secondary, for: .selected) setIconColor(theme.onSurface.withAlphaComponent(0.38), for: .normal) titleColor = theme.onSurface.withAlphaComponent(0.60) selectedPulseColor = theme.secondary normalPulseColor = theme.onSurface updatePulseColor() } /// This might be considered as a hackish way, but it's just manipulation /// UIButton considers size of the `currentImage` to determine `intrinsicContentSize` /// and `sizeThatFits(_:)`, and to position `titleLabel`. /// So, we make use of this property (by setting transparent image) to make room for our icon /// without making much effort (like playing with `titleEdgeInsets` and `contentEdgeInsets`) /// Size of the image equals to `iconSize` plus corresponsing `iconEdgeInsets` values private func reloadImage() { let insets = iconEdgeInsets let w = iconSize + insets.left + insets.right let h = iconSize + insets.top + insets.bottom UIGraphicsBeginImageContext(CGSize(width: w, height: h)) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() self.image = image } /// Pulse color for selected state. open var selectedPulseColor = Color.white /// Pulse color for normal state. open var normalPulseColor = Color.white } private extension BaseIconLayerButton { func updatePulseColor() { pulseColor = isSelected ? selectedPulseColor : normalPulseColor } } // MARK: - BaseIconLayer internal class BaseIconLayer: CALayer { var selectedColor = Color.blue.base var normalColor = Color.lightGray var disabledColor = Color.gray func prepareForFirstAnimation() {} func firstAnimation() {} func prepareForSecondAnimation() {} func secondAnimation() {} private(set) var isAnimating = false private(set) var isSelected = false var isEnabled = true { didSet { selectedColor = { selectedColor }() normalColor = { normalColor }() disabledColor = { disabledColor }() } } func prepare() { normalColor = { normalColor }() // calling didSet selectedColor = { selectedColor }() // calling didSet } func setSelected(_ isSelected: Bool, animated: Bool) { guard self.isSelected != isSelected, !isAnimating else { return } self.isSelected = isSelected if animated { animate() } else { Motion.disable { prepareForFirstAnimation() firstAnimation() prepareForSecondAnimation() secondAnimation() } } } private func animate() { guard !isAnimating else { return } prepareForFirstAnimation() Motion.animate(duration: Constants.partialDuration, timingFunction: .easeInOut, animations: { self.isAnimating = true self.firstAnimation() }, completion: { Motion.disable { self.prepareForSecondAnimation() } Motion.delay(Constants.partialDuration * Constants.delayFactor) { Motion.animate(duration: Constants.partialDuration, timingFunction: .easeInOut, animations: { self.secondAnimation() }, completion: { self.isAnimating = false }) } }) } var sideLength: CGFloat { return frame.height } struct Constants { static let totalDuration = 0.5 static let delayFactor = 0.33 static let partialDuration = totalDuration / (1.0 + delayFactor + 1.0) } } // MARK: - Helper extension private extension CGRect { var center: CGPoint { get { return CGPoint(x: minX + width / 2 , y: minY + height / 2) } set { origin = CGPoint(x: newValue.x - width / 2, y: newValue.y - height / 2) } } } internal extension CALayer { /// Animates the propery of CALayer from current value to the specified value /// and does not reset to the initial value after the animation finishes /// /// - Parameters: /// - keyPath: Keypath to the animatable property of the layer /// - to: Final value of the property /// - dur: Duration of the animation in seconds. Defaults to 0, which results in taking the duration from enclosing CATransaction, or .25 seconds func animate(_ keyPath: String, to: CGFloat, dur: TimeInterval = 0) { let animation = CABasicAnimation(keyPath: keyPath) animation.timingFunction = .easeIn animation.fromValue = self.value(forKeyPath: keyPath) // from current value animation.duration = dur setValue(to, forKeyPath: keyPath) self.add(animation, forKey: nil) } } internal extension CATransform3D { static var identity: CATransform3D { return CATransform3DIdentity } } ================================================ FILE: Sources/iOS/Button/Button.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion open class Button: UIButton, Pulseable, PulseableLayer, Themeable { /** A CAShapeLayer used to manage elements that would be affected by the clipToBounds property of the backing layer. For example, this allows the dropshadow effect on the backing layer, while clipping the image to a desired shape within the visualLayer. */ public let visualLayer = CAShapeLayer() /// A Pulse reference. internal var pulse: Pulse! /// A reference to the pulse layer. internal var pulseLayer: CALayer? { return pulse.pulseLayer } /// PulseAnimation value. open var pulseAnimation: PulseAnimation { get { return pulse.animation } set(value) { pulse.animation = value } } /// PulseAnimation color. @IBInspectable open var pulseColor: UIColor { get { return pulse.color } set(value) { pulse.color = value } } /// Pulse opacity. @IBInspectable open var pulseOpacity: CGFloat { get { return pulse.opacity } set(value) { pulse.opacity = value } } /// A property that accesses the backing layer's background @IBInspectable open override var backgroundColor: UIColor? { didSet { layer.backgroundColor = backgroundColor?.cgColor } } /// A preset property for updated contentEdgeInsets. open var contentEdgeInsetsPreset = EdgeInsetsPreset.none { didSet { contentEdgeInsets = EdgeInsetsPresetToValue(preset: contentEdgeInsetsPreset) } } /// Sets the normal and highlighted image for the button. @IBInspectable open var image: UIImage? { didSet { setImage(image, for: .normal) setImage(image, for: .selected) setImage(image, for: .highlighted) setImage(image, for: .disabled) if #available(iOS 9, *) { setImage(image, for: .application) setImage(image, for: .focused) setImage(image, for: .reserved) } } } /// Sets the normal and highlighted title for the button. @IBInspectable open var title: String? { didSet { setTitle(title, for: .normal) setTitle(title, for: .selected) setTitle(title, for: .highlighted) setTitle(title, for: .disabled) if #available(iOS 9, *) { setTitle(title, for: .application) setTitle(title, for: .focused) setTitle(title, for: .reserved) } guard nil != title else { return } guard nil == titleColor else { return } titleColor = Color.blue.base } } /// Sets the normal and highlighted titleColor for the button. @IBInspectable open var titleColor: UIColor? { didSet { setTitleColor(titleColor, for: .normal) setTitleColor(titleColor, for: .highlighted) setTitleColor(titleColor, for: .disabled) if nil == selectedTitleColor { setTitleColor(titleColor, for: .selected) } if #available(iOS 9, *) { setTitleColor(titleColor, for: .application) setTitleColor(titleColor, for: .focused) setTitleColor(titleColor, for: .reserved) } } } /// Sets the selected titleColor for the button. @IBInspectable open var selectedTitleColor: UIColor? { didSet { setTitleColor(selectedTitleColor, for: .selected) } } /** An initializer that initializes the object with a NSCoder object. - Parameter aDecoder: A NSCoder instance. */ public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) prepare() } /** An initializer that initializes the object with a CGRect object. If AutoLayout is used, it is better to initilize the instance using the init() initializer. - Parameter frame: A CGRect instance. */ public override init(frame: CGRect) { super.init(frame: frame) /// Set these here to avoid overriding storyboard values tintColor = Color.blue.base titleLabel?.font = Theme.font.regular(with: fontSize) prepare() } /** A convenience initializer that acceps an image and tint - Parameter image: A UIImage. - Parameter tintColor: A UIColor. */ public init(image: UIImage?, tintColor: UIColor? = nil) { super.init(frame: .zero) prepare() prepare(with: image, tintColor: tintColor) } /** A convenience initializer that acceps a title and title - Parameter title: A String. - Parameter titleColor: A UIColor. */ public init(title: String?, titleColor: UIColor? = nil) { super.init(frame: .zero) prepare() prepare(with: title, titleColor: titleColor) } open override func layoutSubviews() { super.layoutSubviews() layoutShape() layoutVisualLayer() layoutShadowPath() } /** Triggers the pulse animation. - Parameter point: A Optional point to pulse from, otherwise pulses from the center. */ open func pulse(point: CGPoint? = nil) { pulse.expand(point: point ?? center) Motion.delay(0.35) { [weak self] in self?.pulse.contract() } } /** A delegation method that is executed when the view has began a touch event. - Parameter touches: A set of UITouch objects. - Parameter event: A UIEvent object. */ open override func touchesBegan(_ touches: Set, with event: UIEvent?) { super.touchesBegan(touches, with: event) pulse.expand(point: layer.convert(touches.first!.location(in: self), from: layer)) } /** A delegation method that is executed when the view touch event has ended. - Parameter touches: A set of UITouch objects. - Parameter event: A UIEvent object. */ open override func touchesEnded(_ touches: Set, with event: UIEvent?) { super.touchesEnded(touches, with: event) pulse.contract() } /** A delegation method that is executed when the view touch event has been cancelled. - Parameter touches: A set of UITouch objects. - Parameter event: A UIEvent object. */ open override func touchesCancelled(_ touches: Set, with event: UIEvent?) { super.touchesCancelled(touches, with: event) pulse.contract() } open func bringImageViewToFront() { guard let v = imageView else { return } bringSubviewToFront(v) } /** Prepares the view instance when intialized. When subclassing, it is recommended to override the prepare method to initialize property values and other setup operations. The super.prepare method should always be called immediately when subclassing. */ open func prepare() { contentScaleFactor = Screen.scale prepareVisualLayer() preparePulse() applyCurrentTheme() } /** Applies the given theme. - Parameter theme: A Theme. */ open func apply(theme: Theme) { } } extension Button { /// Prepares the visualLayer property. fileprivate func prepareVisualLayer() { visualLayer.zPosition = 0 visualLayer.masksToBounds = true layer.addSublayer(visualLayer) } /// Prepares the pulse motion. fileprivate func preparePulse() { pulse = Pulse(pulseView: self, pulseLayer: visualLayer) } /** Prepares the Button with an image and tint - Parameter image: A UIImage. - Parameter tintColor: A UI */ fileprivate func prepare(with image: UIImage?, tintColor: UIColor?) { self.image = image self.tintColor = tintColor ?? self.tintColor } /** Prepares the Button with a title and title - Parameter title: A String. - Parameter titleColor: A UI */ fileprivate func prepare(with title: String?, titleColor: UIColor?) { self.title = title self.titleColor = titleColor ?? self.titleColor } } extension Button { /// Manages the layout for the visualLayer property. fileprivate func layoutVisualLayer() { visualLayer.frame = bounds visualLayer.cornerRadius = layer.cornerRadius } } ================================================ FILE: Sources/iOS/Button/CheckButton.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit open class CheckButton: BaseIconLayerButton { class override var iconLayer: BaseIconLayer { return CheckBoxLayer() } /// Color of the checkmark (✓) open var checkmarkColor: UIColor { get { return (iconLayer as! CheckBoxLayer).checkmarkColor } set { (iconLayer as! CheckBoxLayer).checkmarkColor = newValue } } open override func prepare() { super.prepare() addTarget(self, action: #selector(didTap), for: .touchUpInside) } @objc private func didTap() { guard !isAnimating else { return } setSelected(!isSelected, animated: true) } open override func apply(theme: Theme) { super.apply(theme: theme) checkmarkColor = theme.onSecondary } } internal class CheckBoxLayer: BaseIconLayer { var checkmarkColor: UIColor = .white { didSet { checkMarkLeftLayer.strokeColor = checkmarkColor.cgColor checkMarkRightLayer.strokeColor = checkmarkColor.cgColor } } let borderLayer = CALayer() let checkMarkLeftLayer = CAShapeLayer() let checkMarkRightLayer = CAShapeLayer() let checkMarkLayer = CALayer() override var selectedColor: UIColor { didSet { guard isSelected, isEnabled else { return } borderLayer.borderColor = selectedColor.cgColor borderLayer.backgroundColor = selectedColor.cgColor } } override var normalColor: UIColor { didSet { guard !isSelected, isEnabled else { return } borderLayer.borderColor = normalColor.cgColor } } override var disabledColor: UIColor { didSet { guard !isEnabled else { return } borderLayer.borderColor = disabledColor.cgColor if isSelected { borderLayer.backgroundColor = disabledColor.cgColor } } } open override func prepare() { super.prepare() addSublayer(borderLayer) addSublayer(checkMarkLayer) checkMarkLayer.addSublayer(checkMarkLeftLayer) checkMarkLayer.addSublayer(checkMarkRightLayer) checkMarkLeftLayer.lineCap = CAShapeLayerLineCap.square checkMarkRightLayer.lineCap = CAShapeLayerLineCap.square checkMarkLeftLayer.strokeEnd = 0 checkMarkRightLayer.strokeEnd = 0 checkmarkColor = { checkmarkColor }() // calling didSet } override func prepareForFirstAnimation() { borderLayer.borderColor = (isEnabled ? (isSelected ? selectedColor : normalColor) : disabledColor).cgColor if isSelected { borderLayer.borderWidth = borderLayerNormalBorderWidth } else { borderLayer.borderWidth = 0 borderLayer.backgroundColor = (isEnabled ? normalColor : disabledColor).cgColor checkMarkLeftLayer.strokeEnd = 1 checkMarkRightLayer.strokeEnd = 1 } checkMarkLayer.transform = .identity } override func firstAnimation() { borderLayer.transform = borderLayerScaleToShrink checkMarkLayer.transform = borderLayerScaleToShrink if isSelected { borderLayer.animate(#keyPath(CALayer.borderWidth), to: borderLayerFullBorderWidth) } else { checkMarkLeftLayer.animate(#keyPath(CAShapeLayer.strokeEnd), to: 0) checkMarkRightLayer.animate(#keyPath(CAShapeLayer.strokeEnd), to: 0) checkMarkLayer.transform = CATransform3DMakeTranslation(sideLength / 2 - checkMarkStartPoint.x, -(checkMarkStartPoint.y - sideLength / 2), 0) } } override func prepareForSecondAnimation() { borderLayer.backgroundColor = (isSelected ? (isEnabled ? selectedColor : disabledColor) : .clear).cgColor if isSelected { borderLayer.borderWidth = borderLayerNormalBorderWidth checkMarkLeftLayer.strokeEnd = 0.0001 checkMarkRightLayer.strokeEnd = 0.0001 checkMarkLayer.opacity = 0 checkMarkLayer.animate(#keyPath(CALayer.opacity), to: 1, dur: Constants.totalDuration * 0.1) } else { borderLayer.borderWidth = borderLayerCenterDotBorderWidth } } override func secondAnimation() { borderLayer.transform = .identity checkMarkLayer.transform = .identity if isSelected { checkMarkLeftLayer.animate(#keyPath(CAShapeLayer.strokeEnd), to: 1) checkMarkRightLayer.animate(#keyPath(CAShapeLayer.strokeEnd), to: 1) } else { borderLayer.animate(#keyPath(CALayer.borderWidth), to: borderLayerNormalBorderWidth) } } override func layoutSublayers() { super.layoutSublayers() guard !isAnimating else { return } let s = CGSize(width: sideLength, height: sideLength) borderLayer.frame.size = s checkMarkLayer.frame.size = s checkMarkLeftLayer.frame.size = s checkMarkRightLayer.frame.size = s checkMarkLeftLayer.path = checkMarkPathLeft.cgPath checkMarkRightLayer.path = checkMarkPathRigth.cgPath checkMarkLeftLayer.lineWidth = lineWidth checkMarkRightLayer.lineWidth = lineWidth borderLayer.borderWidth = borderLayerNormalBorderWidth borderLayer.cornerRadius = borderLayerCornerRadius } } private extension CheckBoxLayer { var borderLayerFullBorderWidth: CGFloat { return sideLength / 2 * 1.1 } //without multipling 1.1 a weird plus sign (+) appears sometimes. var borderLayerCenterDotBorderWidth: CGFloat { return sideLength / 2 * 0.87 } var borderLayerNormalBorderWidth: CGFloat { return sideLength * 0.1 } var borderLayerCornerRadius: CGFloat { return sideLength * 0.1 } var borderLayerScalePercentageToShrink: CGFloat { return 0.9 } var borderLayerScaleToShrink: CATransform3D { return CATransform3DMakeScale(borderLayerScalePercentageToShrink, borderLayerScalePercentageToShrink, 1) } var checkMarkStartPoint: CGPoint { return CGPoint(x: sideLength * 14 / 36, y: sideLength * 25 / 36) } var checkMarkRightEndPoint: CGPoint { return CGPoint(x: sideLength - (sideLength * 6 / 36), y: sideLength * 9 / 36) } var checkMarkLeftEndPoint: CGPoint { return CGPoint(x: sideLength * 6 / 36, y: sideLength * 18 / 36) } var checkMarkPathRigth: UIBezierPath { let path = UIBezierPath() path.move(to: checkMarkStartPoint) path.addLine(to: checkMarkRightEndPoint) return path } var checkMarkPathLeft: UIBezierPath { let path = UIBezierPath() path.move(to: checkMarkStartPoint) path.addLine(to: checkMarkLeftEndPoint) return path } var lineWidth: CGFloat { return sideLength * 0.1 } } ================================================ FILE: Sources/iOS/Button/FABButton.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit open class FABButton: Button { open override func prepare() { super.prepare() depthPreset = .depth1 shapePreset = .circle pulseAnimation = .centerWithBacking } open override func apply(theme: Theme) { super.apply(theme: theme) backgroundColor = theme.secondary titleColor = theme.onSecondary tintColor = theme.onSecondary pulseColor = theme.onSecondary } } ================================================ FILE: Sources/iOS/Button/FlatButton.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit open class FlatButton: Button { open override func prepare() { super.prepare() cornerRadiusPreset = .cornerRadius1 } open override func apply(theme: Theme) { super.apply(theme: theme) backgroundColor = .clear titleColor = theme.secondary tintColor = theme.secondary pulseColor = theme.secondary } } ================================================ FILE: Sources/iOS/Button/IconButton.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit public enum IconButtonThemingStyle { /// Theming when background content is in background color. case onBackground /// Theming when background content is in primary color. case onPrimary } open class IconButton: Button { /// A reference to IconButtonThemingStyle. open var themingStyle = IconButtonThemingStyle.onBackground open override func prepare() { super.prepare() pulseAnimation = .center } open override func apply(theme: Theme) { super.apply(theme: theme) switch themingStyle { case .onBackground: tintColor = theme.secondary pulseColor = theme.secondary case .onPrimary: tintColor = theme.onPrimary pulseColor = theme.onPrimary } } } ================================================ FILE: Sources/iOS/Button/RadioButton.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit open class RadioButton: BaseIconLayerButton { class override var iconLayer: BaseIconLayer { return RadioBoxLayer() } open override func prepare() { super.prepare() addTarget(self, action: #selector(didTap), for: .touchUpInside) } @objc private func didTap() { setSelected(true, animated: true) } } internal class RadioBoxLayer: BaseIconLayer { private let centerDot = CALayer() private let outerCircle = CALayer() override var selectedColor: UIColor { didSet { guard isSelected, isEnabled else { return } outerCircle.borderColor = selectedColor.cgColor centerDot.backgroundColor = selectedColor.cgColor } } override var normalColor: UIColor { didSet { guard !isSelected, isEnabled else { return } outerCircle.borderColor = normalColor.cgColor } } override var disabledColor: UIColor { didSet { guard !isEnabled else { return } outerCircle.borderColor = disabledColor.cgColor if isSelected { centerDot.backgroundColor = disabledColor.cgColor } } } override func prepare() { super.prepare() addSublayer(centerDot) addSublayer(outerCircle) } override func prepareForFirstAnimation() { outerCircle.borderColor = (isEnabled ? (isSelected ? selectedColor : normalColor) : disabledColor).cgColor if !isSelected { centerDot.backgroundColor = (isEnabled ? normalColor : disabledColor).cgColor } outerCircle.borderWidth = outerCircleBorderWidth } override func firstAnimation() { outerCircle.transform = outerCircleScaleToShrink let to = isSelected ? sideLength / 2.0 : outerCircleBorderWidth * percentageOfOuterCircleWidthToStart outerCircle.animate(#keyPath(CALayer.borderWidth), to: to) if !isSelected { centerDot.transform = centerDotScaleForMeeting } } override func prepareForSecondAnimation() { centerDot.transform = isSelected ? centerDotScaleForMeeting : .identity centerDot.backgroundColor = (isSelected ? (isEnabled ? selectedColor : disabledColor) : .clear).cgColor outerCircle.borderWidth = isSelected ? outerCircleBorderWidth * percentageOfOuterCircleWidthToStart : outerCircleFullBorderWidth } override func secondAnimation() { outerCircle.transform = .identity outerCircle.animate(#keyPath(CALayer.borderWidth), to: outerCircleBorderWidth) if isSelected { centerDot.transform = .identity } } override func layoutSublayers() { super.layoutSublayers() guard !isAnimating else { return } centerDot.frame = CGRect(x: centerDotDiameter / 2.0, y: centerDotDiameter / 2.0, width: centerDotDiameter, height: centerDotDiameter) outerCircle.frame.size = CGSize(width: sideLength, height: sideLength) centerDot.cornerRadius = centerDot.bounds.width / 2 outerCircle.cornerRadius = sideLength / 2 outerCircle.borderWidth = outerCircleBorderWidth } } private extension RadioBoxLayer { var percentageOfOuterCircleSizeToShrinkTo: CGFloat { return 0.9 } var percentageOfOuterCircleWidthToStart: CGFloat { return 1 } var outerCircleScaleToShrink: CATransform3D { let s = percentageOfOuterCircleSizeToShrinkTo return CATransform3DMakeScale(s, s, 1) } var centerDotScaleForMeeting: CATransform3D { let s = ((sideLength - 2 * percentageOfOuterCircleWidthToStart * outerCircleBorderWidth) * percentageOfOuterCircleSizeToShrinkTo) / centerDotDiameter return CATransform3DMakeScale(s, s, 1) } var outerCircleFullBorderWidth: CGFloat { return (self.sideLength / 2.0) * 1.1 //without multipling 1.1 a weird plus sign (+) appears sometimes. } var centerDotDiameter: CGFloat { return sideLength / 2.0 } var outerCircleBorderWidth: CGFloat { return sideLength * 0.11 } } ================================================ FILE: Sources/iOS/Button/RaisedButton.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit open class RaisedButton: Button { open override func prepare() { super.prepare() depthPreset = .depth1 cornerRadiusPreset = .cornerRadius1 } open override func apply(theme: Theme) { super.apply(theme: theme) backgroundColor = theme.secondary titleColor = theme.onSecondary pulseColor = theme.onSecondary tintColor = theme.onSecondary } } ================================================ FILE: Sources/iOS/ButtonGroup/BaseButtonGroup.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit open class BaseButtonGroup: View { /// Holds reference to buttons within the group. open var buttons: [T] = [] { didSet { oldValue.forEach { $0.removeFromSuperview() $0.removeTarget(self, action: #selector(didTap(_:)), for: .touchUpInside) } prepareButtons() grid.views = buttons grid.axis.rows = buttons.count } } /// Initializes group with the provided buttons. /// /// - Parameter buttons: Array of buttons. public convenience init(buttons: [T]) { self.init(frame: .zero) defer { self.buttons = buttons } // defer allows didSet to be called } open override func prepare() { super.prepare() grid.axis.direction = .vertical grid.axis.columns = 1 } open override var intrinsicContentSize: CGSize { return sizeThatFits(bounds.size) } open override func sizeThatFits(_ size: CGSize) -> CGSize { let size = CGSize(width: size.width == 0 ? .greatestFiniteMagnitude : size.width, height: size.height == 0 ? .greatestFiniteMagnitude : size.height) let availableW = size.width - grid.contentEdgeInsets.left - grid.contentEdgeInsets.right - grid.layoutEdgeInsets.left - grid.layoutEdgeInsets.right let maxW = buttons.reduce(0) { max($0, $1.sizeThatFits(.init(width: availableW, height: .greatestFiniteMagnitude)).width) } let h = buttons.reduce(0) { $0 + $1.sizeThatFits(.init(width: maxW, height: .greatestFiniteMagnitude)).height } + grid.contentEdgeInsets.top + grid.contentEdgeInsets.bottom + grid.layoutEdgeInsets.top + grid.layoutEdgeInsets.bottom + CGFloat(buttons.count - 1) * grid.interimSpace return CGSize(width: maxW + grid.contentEdgeInsets.left + grid.contentEdgeInsets.right + grid.layoutEdgeInsets.left + grid.layoutEdgeInsets.right, height: min(h, size.height)) } open override func layoutSubviews() { super.layoutSubviews() grid.reload() } open func didTap(button: T, at index: Int) { } @objc private func didTap(_ sender: Button) { guard let sender = sender as? T, let index = buttons.firstIndex(of: sender) else { return } didTap(button: sender, at: index) } } private extension BaseButtonGroup { func prepareButtons() { buttons.forEach { addSubview($0) $0.removeTarget(nil, action: nil, for: .allEvents) $0.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside) } } } ================================================ FILE: Sources/iOS/ButtonGroup/CheckButtonGroup.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /// Lays out provided check buttons within itself. /// /// Unlike RadioButtonGroup, checking one check button that belongs to a check group *does not* unchecks any previously checked /// check button within the same group. Intially, all of the check buttons are unchecked. /// /// The buttons are laid out by `Grid` system, so that changing properites of grid instance /// (e.g interimSpace) are reflected. open class CheckButtonGroup: BaseButtonGroup { /// Initializes CheckButtonGroup with an array of check buttons each having /// title equal to corresponding string in the `titles` parameter. /// /// - Parameter titles: An array of title strings public convenience init(titles: [String]) { let buttons = titles.map { CheckButton(title: $0) } self.init(buttons: buttons) } /// Returns all selected check buttons within the group /// or empty array if none is seleceted. open var selecetedButtons: [CheckButton] { return buttons.filter { $0.isSelected } } /// Returns indexes of all selected check buttons within the group /// or empty array if none is seleceted. open var selectedIndices: [Int] { return selecetedButtons.map { buttons.firstIndex(of: $0)! } } open override func didTap(button: CheckButton, at index: Int) { button.setSelected(!button.isSelected, animated: true) } } ================================================ FILE: Sources/iOS/ButtonGroup/RadioButtonGroup.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /// Lays out provided radio buttons within itself. /// /// Checking one radio button that belongs to a radio group unchecks any previously checked /// radio button within the same group. Intially, all of the radio buttons are unchecked. /// /// The buttons are laid out by `Grid` system, so that changing properites of grid instance /// (e.g interimSpace) are reflected. open class RadioButtonGroup: BaseButtonGroup { /// Initializes RadioButtonGroup with an array of radio buttons each having /// title equal to corresponding string in the `titles` parameter. /// /// - Parameter titles: An array of title strings. public convenience init(titles: [String]) { let buttons = titles.map { RadioButton(title: $0) } self.init(buttons: buttons) } /// Returns selected radio button within the group. /// If none is selected (e.g in initial state), nil is returned. open var selectedButton: RadioButton? { return buttons.first { $0.isSelected } } /// Returns index of selected radio button within the group. /// If none is selected (e.g in initial state), -1 is returned. open var selectedIndex: Int { guard let b = selectedButton else { return -1 } return buttons.firstIndex(of: b)! } open override func didTap(button: RadioButton, at index: Int) { let isAnimating = buttons.reduce(false) { $0 || $1.isAnimating } guard !isAnimating else { return } buttons.forEach { $0.setSelected($0 == button, animated: true) } } } ================================================ FILE: Sources/iOS/Card/Card.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit open class Card: PulseView { /// A container view for subviews. public let container = UIView() @IBInspectable open override var cornerRadiusPreset: CornerRadiusPreset { didSet { container.cornerRadiusPreset = cornerRadiusPreset } } @IBInspectable open var cornerRadius: CGFloat { get { return container.layer.cornerRadius } set(value) { container.layer.cornerRadius = value } } open override var shapePreset: ShapePreset { didSet { container.shapePreset = shapePreset } } @IBInspectable open override var backgroundColor: UIColor? { didSet { container.backgroundColor = backgroundColor } } /// A reference to the toolbar. @IBInspectable open var toolbar: Toolbar? { didSet { oldValue?.removeFromSuperview() if let v = toolbar { container.addSubview(v) } layoutSubviews() } } /// A preset wrapper around toolbarEdgeInsets. open var toolbarEdgeInsetsPreset = EdgeInsetsPreset.none { didSet { toolbarEdgeInsets = EdgeInsetsPresetToValue(preset: toolbarEdgeInsetsPreset) } } /// A reference to toolbarEdgeInsets. @IBInspectable open var toolbarEdgeInsets = EdgeInsets.zero { didSet { layoutSubviews() } } /// A reference to the contentView. @IBInspectable open var contentView: UIView? { didSet { oldValue?.removeFromSuperview() if let v = contentView { v.clipsToBounds = true container.addSubview(v) } layoutSubviews() } } /// A preset wrapper around contentViewEdgeInsets. open var contentViewEdgeInsetsPreset = EdgeInsetsPreset.none { didSet { contentViewEdgeInsets = EdgeInsetsPresetToValue(preset: contentViewEdgeInsetsPreset) } } /// A reference to contentViewEdgeInsets. @IBInspectable open var contentViewEdgeInsets = EdgeInsets.zero { didSet { layoutSubviews() } } /// A reference to the bottomBar. @IBInspectable open var bottomBar: Bar? { didSet { oldValue?.removeFromSuperview() if let v = bottomBar { container.addSubview(v) } layoutSubviews() } } /// A preset wrapper around bottomBarEdgeInsets. open var bottomBarEdgeInsetsPreset = EdgeInsetsPreset.none { didSet { bottomBarEdgeInsets = EdgeInsetsPresetToValue(preset: bottomBarEdgeInsetsPreset) } } /// A reference to bottomBarEdgeInsets. @IBInspectable open var bottomBarEdgeInsets = EdgeInsets.zero { didSet { layoutSubviews() } } /** An initializer that accepts a NSCoder. - Parameter coder aDecoder: A NSCoder. */ public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } /** An initializer that accepts a CGRect. - Parameter frame: A CGRect. */ public override init(frame: CGRect) { super.init(frame: frame) } /** A convenience initiazlier. - Parameter toolbar: An optional Toolbar. - Parameter contentView: An optional UIView. - Parameter bottomBar: An optional Bar. */ public convenience init?(toolbar: Toolbar?, contentView: UIView?, bottomBar: Bar?) { self.init(frame: .zero) prepareProperties(toolbar: toolbar, contentView: contentView, bottomBar: bottomBar) } open override func layoutSubviews() { super.layoutSubviews() container.frame.size.width = bounds.width reload() } /// Reloads the layout. open func reload() { var h: CGFloat = 0 if let v = toolbar { h = prepare(view: v, with: toolbarEdgeInsets, from: h) } if let v = contentView { h = prepare(view: v, with: contentViewEdgeInsets, from: h) } if let v = bottomBar { h = prepare(view: v, with: bottomBarEdgeInsets, from: h) } container.frame.size.height = h bounds.size.height = h } open override func prepare() { super.prepare() pulseAnimation = .none cornerRadiusPreset = .cornerRadius1 prepareContainer() } /** Prepare the view size from a given top position. - Parameter view: A UIView. - Parameter edge insets: An EdgeInsets. - Parameter from top: A CGFloat. - Returns: A CGFloat. */ @discardableResult open func prepare(view: UIView, with insets: EdgeInsets, from top: CGFloat) -> CGFloat { let y = insets.top + top view.frame.origin.y = y view.frame.origin.x = insets.left let w = container.bounds.width - insets.left - insets.right var h = view.bounds.height if 0 == h || nil != view as? UILabel { (view as? UILabel)?.sizeToFit() h = view.sizeThatFits(CGSize(width: w, height: .greatestFiniteMagnitude)).height } view.frame.size.width = w view.frame.size.height = h return y + h + insets.bottom } /** A preparation method that sets the base UI elements. - Parameter toolbar: An optional Toolbar. - Parameter contentView: An optional UIView. - Parameter bottomBar: An optional Bar. */ internal func prepareProperties(toolbar: Toolbar?, contentView: UIView?, bottomBar: Bar?) { self.toolbar = toolbar self.contentView = contentView self.bottomBar = bottomBar } } extension Card { /// Prepares the container. fileprivate func prepareContainer() { container.clipsToBounds = true addSubview(container) } } ================================================ FILE: Sources/iOS/Card/ImageCard.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit open class ImageCard: Card { /** A Display value to indicate whether or not to display the imageView to the full view bounds. */ open var displayStyle = DisplayStyle.partial { didSet { layoutSubviews() } } /// A preset wrapper around imageViewEdgeInsets. open var imageViewEdgeInsetsPreset = EdgeInsetsPreset.none { didSet { imageViewEdgeInsets = EdgeInsetsPresetToValue(preset: imageViewEdgeInsetsPreset) } } /// A reference to imageViewEdgeInsets. @IBInspectable open var imageViewEdgeInsets = EdgeInsets.zero { didSet { layoutSubviews() } } /// A reference to the imageView. @IBInspectable open var imageView: UIImageView? { didSet { oldValue?.removeFromSuperview() if let v = imageView { container.addSubview(v) } layoutSubviews() } } /// An ImageCardToolbarAlignment value. open var toolbarAlignment = ToolbarAlignment.bottom { didSet { layoutSubviews() } } /// Reloads the view. open override func reload() { var h: CGFloat = 0 if let v = imageView { h = prepare(view: v, with: imageViewEdgeInsets, from: h) container.sendSubviewToBack(v) } if let v = toolbar { prepare(view: v, with: toolbarEdgeInsets, from: h) v.frame.origin.y = .top == toolbarAlignment ? toolbarEdgeInsets.top : h - v.bounds.height - toolbarEdgeInsets.bottom container.bringSubviewToFront(v) } if let v = contentView { h = prepare(view: v, with: contentViewEdgeInsets, from: h) } if let v = bottomBar { h = prepare(view: v, with: bottomBarEdgeInsets, from: h) } container.frame.size.height = h bounds.size.height = h } } ================================================ FILE: Sources/iOS/Card/PresenterCard.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit open class PresenterCard: Card { /// A preset wrapper around presenterViewEdgeInsets. open var presenterViewEdgeInsetsPreset = EdgeInsetsPreset.none { didSet { presenterViewEdgeInsets = EdgeInsetsPresetToValue(preset: presenterViewEdgeInsetsPreset) } } /// A reference to presenterViewEdgeInsets. @IBInspectable open var presenterViewEdgeInsets = EdgeInsets.zero { didSet { layoutSubviews() } } /// A reference to the presenterView. @IBInspectable open var presenterView: UIView? { didSet { oldValue?.removeFromSuperview() if let v = presenterView { v.clipsToBounds = true container.addSubview(v) } layoutSubviews() } } open override func reload() { var h: CGFloat = 0 if let v = toolbar { h = prepare(view: v, with: toolbarEdgeInsets, from: h) } if let v = presenterView { h = prepare(view: v, with: presenterViewEdgeInsets, from: h) } if let v = contentView { h = prepare(view: v, with: contentViewEdgeInsets, from: h) } if let v = bottomBar { h = prepare(view: v, with: bottomBarEdgeInsets, from: h) } container.frame.size.height = h bounds.size.height = h } } ================================================ FILE: Sources/iOS/Chip/ChipBar.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion @objc(ChipItemStyle) public enum ChipItemStyle: Int { case pill } open class ChipItem: FlatButton { /// Configures the visual display of the chip. var chipItemStyle: ChipItemStyle { get { return associatedInstance.chipItemStyle } set(value) { associatedInstance.chipItemStyle = value layoutSubviews() } } open override func layoutSubviews() { super.layoutSubviews() layoutChipItemStyle() } open override func prepare() { super.prepare() pulseAnimation = .none } } fileprivate extension ChipItem { /// Lays out the chipItem based on its style. func layoutChipItemStyle() { if .pill == chipItemStyle { layer.cornerRadius = bounds.height / 2 } } } fileprivate struct AssociatedInstance { /// A ChipItemStyle value. var chipItemStyle: ChipItemStyle } /// A memory reference to the ChipItemStyle instance for ChipItem extensions. fileprivate var ChipKey: UInt8 = 0 fileprivate extension ChipItem { /// AssociatedInstance reference. var associatedInstance: AssociatedInstance { get { return AssociatedObject.get(base: self, key: &ChipKey) { return AssociatedInstance(chipItemStyle: .pill) } } set(value) { AssociatedObject.set(base: self, key: &ChipKey, value: value) } } } @objc(ChipBarDelegate) public protocol ChipBarDelegate { /** A delegation method that is executed when the chipItem will trigger the animation to the next chip. - Parameter chipBar: A ChipBar. - Parameter chipItem: A ChipItem. */ @objc optional func chipBar(chipBar: ChipBar, willSelect chipItem: ChipItem) /** A delegation method that is executed when the chipItem did complete the animation to the next chip. - Parameter chipBar: A ChipBar. - Parameter chipItem: A ChipItem. */ @objc optional func chipBar(chipBar: ChipBar, didSelect chipItem: ChipItem) } @objc(ChipBarStyle) public enum ChipBarStyle: Int { case auto case nonScrollable case scrollable } open class ChipBar: Bar { /// The total width of the chipItems. fileprivate var chipItemsTotalWidth: CGFloat { var w: CGFloat = 0 let q = 2 * chipItemsInterimSpace let p = q + chipItemsInterimSpace for v in chipItems { let x = v.sizeThatFits(CGSize(width: .greatestFiniteMagnitude, height: scrollView.bounds.height)).width w += x w += p } w -= chipItemsInterimSpace return w } /// An enum that determines the chip bar style. open var chipBarStyle = ChipBarStyle.auto { didSet { layoutSubviews() } } /// A reference to the scroll view when the chip bar style is scrollable. public let scrollView = UIScrollView() /// Enables and disables bouncing when swiping. open var isScrollBounceEnabled: Bool { get { return scrollView.bounces } set(value) { scrollView.bounces = value } } /// A delegation reference. open weak var delegate: ChipBarDelegate? /// The currently selected chipItem. open fileprivate(set) var selectedChipItem: ChipItem? /// A preset wrapper around chipItems contentEdgeInsets. open var chipItemsContentEdgeInsetsPreset: EdgeInsetsPreset { get { return contentView.grid.contentEdgeInsetsPreset } set(value) { contentView.grid.contentEdgeInsetsPreset = value } } /// A reference to EdgeInsets. @IBInspectable open var chipItemsContentEdgeInsets: EdgeInsets { get { return contentView.grid.contentEdgeInsets } set(value) { contentView.grid.contentEdgeInsets = value } } /// A preset wrapper around chipItems interimSpace. open var chipItemsInterimSpacePreset: InterimSpacePreset { get { return contentView.grid.interimSpacePreset } set(value) { contentView.grid.interimSpacePreset = value } } /// A wrapper around chipItems interimSpace. @IBInspectable open var chipItemsInterimSpace: InterimSpace { get { return contentView.grid.interimSpace } set(value) { contentView.grid.interimSpace = value } } /// Buttons. open var chipItems = [ChipItem]() { didSet { oldValue.forEach { $0.removeFromSuperview() } prepareChipItems() layoutSubviews() } } open override func layoutSubviews() { super.layoutSubviews() guard willLayout else { return } layoutScrollView() updateScrollView() } open override func prepare() { super.prepare() interimSpacePreset = .interimSpace3 contentEdgeInsetsPreset = .square1 chipItemsInterimSpacePreset = .interimSpace4 chipItemsContentEdgeInsetsPreset = .square2 chipItemsContentEdgeInsets.left = 0 chipItemsContentEdgeInsets.right = 0 prepareContentView() prepareScrollView() prepareDivider() } } fileprivate extension ChipBar { /// Prepares the divider. func prepareDivider() { dividerColor = Color.grey.lighten2 } /// Prepares the chipItems. func prepareChipItems() { for v in chipItems { v.grid.columns = 0 v.layer.cornerRadius = 0 v.contentEdgeInsets = .zero v.removeTarget(self, action: #selector(handle(chipItem:)), for: .touchUpInside) v.addTarget(self, action: #selector(handle(chipItem:)), for: .touchUpInside) } } /// Prepares the contentView. func prepareContentView() { contentView.layer.zPosition = 6000 } /// Prepares the scroll view. func prepareScrollView() { scrollView.showsVerticalScrollIndicator = false scrollView.showsHorizontalScrollIndicator = false centerViews = [scrollView] } } fileprivate extension ChipBar { /// Layout the scrollView. func layoutScrollView() { contentView.grid.reload() if .scrollable == chipBarStyle || (.auto == chipBarStyle && chipItemsTotalWidth > scrollView.bounds.width) { var w: CGFloat = 0 let q = 2 * chipItemsInterimSpace let p = q + chipItemsInterimSpace for v in chipItems { let x = v.sizeThatFits(CGSize(width: .greatestFiniteMagnitude, height: scrollView.bounds.height)).width v.frame.size.height = scrollView.bounds.height v.frame.size.width = x + q v.frame.origin.x = w w += x w += p if scrollView != v.superview { scrollView.addSubview(v) } } w -= chipItemsInterimSpace scrollView.contentSize = CGSize(width: w, height: scrollView.bounds.height) } else { scrollView.grid.begin() scrollView.grid.views = chipItems scrollView.grid.axis.columns = chipItems.count scrollView.grid.contentEdgeInsets = chipItemsContentEdgeInsets scrollView.grid.interimSpace = chipItemsInterimSpace scrollView.grid.commit() scrollView.contentSize = scrollView.frame.size } } } fileprivate extension ChipBar { /// Handles the chipItem touch event. @objc func handle(chipItem: ChipItem) { animate(to: chipItem, isTriggeredByUserInteraction: true) } } extension ChipBar { /** Selects a given index from the chipItems array. - Parameter at index: An Int. - Paramater completion: An optional completion block. */ open func select(at index: Int, completion: ((ChipItem) -> Void)? = nil) { guard -1 < index, index < chipItems.count else { return } animate(to: chipItems[index], isTriggeredByUserInteraction: false, completion: completion) } /** Animates to a given chipItem. - Parameter to chipItem: A ChipItem. - Parameter completion: An optional completion block. */ open func animate(to chipItem: ChipItem, completion: ((ChipItem) -> Void)? = nil) { animate(to: chipItem, isTriggeredByUserInteraction: false, completion: completion) } } fileprivate extension ChipBar { /** Animates to a given chipItem. - Parameter to chipItem: A ChipItem. - Parameter isTriggeredByUserInteraction: A boolean indicating whether the state was changed by a user interaction, true if yes, false otherwise. - Parameter completion: An optional completion block. */ func animate(to chipItem: ChipItem, isTriggeredByUserInteraction: Bool, completion: ((ChipItem) -> Void)? = nil) { if isTriggeredByUserInteraction { delegate?.chipBar?(chipBar: self, willSelect: chipItem) } selectedChipItem = chipItem updateScrollView() } } fileprivate extension ChipBar { /// Updates the scrollView. func updateScrollView() { guard let v = selectedChipItem else { return } if !scrollView.bounds.contains(v.frame) { let contentOffsetX = (v.frame.origin.x < scrollView.bounds.minX) ? v.frame.origin.x : v.frame.maxX - scrollView.bounds.width let normalizedOffsetX = min(max(contentOffsetX, 0), scrollView.contentSize.width - scrollView.bounds.width) scrollView.setContentOffset(CGPoint(x: normalizedOffsetX, y: 0), animated: true) } } } ================================================ FILE: Sources/iOS/Chip/ChipBarController.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit fileprivate var ChipItemKey: UInt8 = 0 @objc(ChipBarAlignment) public enum ChipBarAlignment: Int { case top case bottom case hidden } extension UIViewController { /** A convenience property that provides access to the ChipBarController. This is the recommended method of accessing the ChipBarController through child UIViewControllers. */ public var chipBarController: ChipBarController? { return traverseViewControllerHierarchyForClassType() } } open class ChipBarController: TransitionController { /** A Display value to indicate whether or not to display the rootViewController to the full view bounds, or up to the toolbar height. */ open var displayStyle = DisplayStyle.partial { didSet { layoutSubviews() } } /// The ChipBar used to switch between view controllers. @IBInspectable public let chipBar = ChipBar() /// The chipBar alignment. open var chipBarAlignment = ChipBarAlignment.bottom { didSet { layoutSubviews() } } open override func layoutSubviews() { super.layoutSubviews() layoutChipBar() layoutContainer() layoutRootViewController() } open override func prepare() { super.prepare() prepareChipBar() } } fileprivate extension ChipBarController { /// Prepares the ChipBar. func prepareChipBar() { chipBar.depthPreset = .depth1 view.addSubview(chipBar) } } fileprivate extension ChipBarController { /// Layout the container. func layoutContainer() { chipBar.frame.size.width = view.bounds.width switch displayStyle { case .partial: let p = chipBar.bounds.height let y = view.bounds.height - p switch chipBarAlignment { case .top: container.frame.origin.y = p container.frame.size.height = y case .bottom: container.frame.origin.y = 0 container.frame.size.height = y case .hidden: container.frame.origin.y = 0 container.frame.size.height = view.bounds.height } container.frame.size.width = view.bounds.width case .full: container.frame = view.bounds } } /// Layout the chipBar. func layoutChipBar() { chipBar.frame.size.width = view.bounds.width switch chipBarAlignment { case .top: chipBar.isHidden = false chipBar.frame.origin.y = 0 case .bottom: chipBar.isHidden = false chipBar.frame.origin.y = view.bounds.height - chipBar.bounds.height case .hidden: chipBar.isHidden = true } } /// Layout the rootViewController. func layoutRootViewController() { rootViewController.view.frame = container.bounds } } ================================================ FILE: Sources/iOS/Collection/CardCollectionView/CardCollectionViewCell.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit open class CardCollectionViewCell: CollectionViewCell { /// An optional reference to the card being displayed in the cell. open var card: Card? { didSet { oldValue?.removeFromSuperview() guard let v = card else { return } contentView.addSubview(v) } } open override func prepare() { super.prepare() pulseAnimation = .none } } ================================================ FILE: Sources/iOS/Collection/CardCollectionView/CardCollectionViewController.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit extension UIViewController { /** A convenience property that provides access to the CardCollectionViewController. This is the recommended method of accessing the CardCollectionViewController through child UIViewControllers. */ public var cardCollectionViewController: CardCollectionViewController? { return traverseViewControllerHierarchyForClassType() } } open class CardCollectionViewController: ViewController { /// A reference to a Reminder. public let collectionView = CollectionView() open var dataSourceItems = [DataSourceItem]() /// An index of IndexPath to DataSourceItem. open var dataSourceItemsIndexPaths = [IndexPath: Any]() open override func prepare() { super.prepare() prepareCollectionView() } open override func layoutSubviews() { super.layoutSubviews() layoutCollectionView() } } extension CardCollectionViewController { /// Prepares the collectionView. fileprivate func prepareCollectionView() { collectionView.delegate = self collectionView.dataSource = self collectionView.register(CardCollectionViewCell.self, forCellWithReuseIdentifier: "CardCollectionViewCell") view.addSubview(collectionView) layoutCollectionView() } } extension CardCollectionViewController { /// Sets the frame for the collectionView. fileprivate func layoutCollectionView() { collectionView.frame = view.bounds } } extension CardCollectionViewController: CollectionViewDelegate {} extension CardCollectionViewController: CollectionViewDataSource { @objc open func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 } @objc open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return dataSourceItems.count } @objc open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CardCollectionViewCell", for: indexPath) as! CardCollectionViewCell guard let card = dataSourceItems[indexPath.item].data as? Card else { return cell } dataSourceItemsIndexPaths[indexPath] = card card.frame = cell.bounds cell.card = card return cell } } ================================================ FILE: Sources/iOS/Collection/CollectionReusableView.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion @objc(CollectionReusableView) open class CollectionReusableView: UICollectionReusableView, Pulseable, PulseableLayer { /** A CAShapeLayer used to manage elements that would be affected by the clipToBounds property of the backing layer. For example, this allows the dropshadow effect on the backing layer, while clipping the image to a desired shape within the visualLayer. */ public let visualLayer = CAShapeLayer() /// A Pulse reference. internal var pulse: Pulse! /// A reference to the pulse layer. internal var pulseLayer: CALayer? { return pulse.pulseLayer } /// PulseAnimation value. open var pulseAnimation: PulseAnimation { get { return pulse.animation } set(value) { pulse.animation = value } } /// PulseAnimation color. @IBInspectable open var pulseColor: UIColor { get { return pulse.color } set(value) { pulse.color = value } } /// Pulse opacity. @IBInspectable open var pulseOpacity: CGFloat { get { return pulse.opacity } set(value) { pulse.opacity = value } } /** A property that manages an image for the visualLayer's contents property. Images should not be set to the backing layer's contents property to avoid conflicts when using clipsToBounds. */ @IBInspectable open var image: UIImage? { didSet { visualLayer.contents = image?.cgImage } } /** Allows a relative subrectangle within the range of 0 to 1 to be specified for the visualLayer's contents property. This allows much greater flexibility than the contentsGravity property in terms of how the image is cropped and stretched. */ @IBInspectable open var contentsRect: CGRect { get { return visualLayer.contentsRect } set(value) { visualLayer.contentsRect = value } } /** A CGRect that defines a stretchable region inside the visualLayer with a fixed border around the edge. */ @IBInspectable open var contentsCenter: CGRect { get { return visualLayer.contentsCenter } set(value) { visualLayer.contentsCenter = value } } /** A floating point value that defines a ratio between the pixel dimensions of the visualLayer's contents property and the size of the view. By default, this value is set to the Screen.scale. */ @IBInspectable open var contentsScale: CGFloat { get { return visualLayer.contentsScale } set(value) { visualLayer.contentsScale = value } } /// Determines how content should be aligned within the visualLayer's bounds. @IBInspectable open var contentsGravity: CALayerContentsGravity { get { return visualLayer.contentsGravity } set(value) { visualLayer.contentsGravity = value } } /// A preset wrapper around contentEdgeInsets. open var contentEdgeInsetsPreset: EdgeInsetsPreset { get { return grid.contentEdgeInsetsPreset } set(value) { grid.contentEdgeInsetsPreset = value } } /// A reference to EdgeInsets. @IBInspectable open var contentEdgeInsets: UIEdgeInsets { get { return grid.contentEdgeInsets } set(value) { grid.contentEdgeInsets = value } } /// A preset wrapper around interimSpace. open var interimSpacePreset: InterimSpacePreset { get { return grid.interimSpacePreset } set(value) { grid.interimSpacePreset = value } } /// A wrapper around grid.interimSpace. @IBInspectable open var interimSpace: InterimSpace { get { return grid.interimSpace } set(value) { grid.interimSpace = value } } /// A property that accesses the backing layer's background @IBInspectable open override var backgroundColor: UIColor? { didSet { layer.backgroundColor = backgroundColor?.cgColor } } /** An initializer that initializes the object with a NSCoder object. - Parameter aDecoder: A NSCoder instance. */ public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) prepare() } /** An initializer that initializes the object with a CGRect object. If AutoLayout is used, it is better to initilize the instance using the init() initializer. - Parameter frame: A CGRect instance. */ public override init(frame: CGRect) { super.init(frame: frame) prepare() } open override func layoutSubviews() { super.layoutSubviews() layoutShape() layoutVisualLayer() layoutShadowPath() } /** Triggers the pulse animation. - Parameter point: A Optional point to pulse from, otherwise pulses from the center. */ open func pulse(point: CGPoint? = nil) { pulse.expand(point: point ?? center) Motion.delay(0.35) { [weak self] in self?.pulse.contract() } } /** A delegation method that is executed when the view has began a touch event. - Parameter touches: A set of UITouch objects. - Parameter event: A UIEvent object. */ open override func touchesBegan(_ touches: Set, with event: UIEvent?) { super.touchesBegan(touches, with: event) pulse.expand(point: layer.convert(touches.first!.location(in: self), from: layer)) } /** A delegation method that is executed when the view touch event has ended. - Parameter touches: A set of UITouch objects. - Parameter event: A UIEvent object. */ open override func touchesEnded(_ touches: Set, with event: UIEvent?) { super.touchesEnded(touches, with: event) pulse.contract() } /** A delegation method that is executed when the view touch event has been cancelled. - Parameter touches: A set of UITouch objects. - Parameter event: A UIEvent object. */ open override func touchesCancelled(_ touches: Set, with event: UIEvent?) { super.touchesCancelled(touches, with: event) pulse.contract() } /** Prepares the view instance when intialized. When subclassing, it is recommended to override the prepare method to initialize property values and other setup operations. The super.prepare method should always be called immediately when subclassing. */ open func prepare() { contentsGravity = .resizeAspectFill contentScaleFactor = Screen.scale prepareVisualLayer() preparePulse() } } extension CollectionReusableView { /// Prepares the pulse motion. fileprivate func preparePulse() { pulse = Pulse(pulseView: self, pulseLayer: visualLayer) pulseAnimation = .none } /// Prepares the visualLayer property. fileprivate func prepareVisualLayer() { visualLayer.zPosition = 0 visualLayer.masksToBounds = true layer.addSublayer(visualLayer) } } extension CollectionReusableView { /// Manages the layout for the visualLayer property. fileprivate func layoutVisualLayer() { visualLayer.frame = bounds visualLayer.cornerRadius = layer.cornerRadius } } ================================================ FILE: Sources/iOS/Collection/CollectionView.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit open class CollectionView: UICollectionView { /// A preset wrapper around contentEdgeInsets. open var contentEdgeInsetsPreset: EdgeInsetsPreset { get { return (collectionViewLayout as? CollectionViewLayout)!.contentEdgeInsetsPreset } set(value) { (collectionViewLayout as? CollectionViewLayout)!.contentEdgeInsetsPreset = value } } open var contentEdgeInsets: EdgeInsets { get { return (collectionViewLayout as? CollectionViewLayout)!.contentEdgeInsets } set(value) { (collectionViewLayout as? CollectionViewLayout)!.contentEdgeInsets = value } } /// Scroll direction. open var scrollDirection: UICollectionView.ScrollDirection { get { return (collectionViewLayout as? CollectionViewLayout)!.scrollDirection } set(value) { (collectionViewLayout as? CollectionViewLayout)!.scrollDirection = value } } /// A preset wrapper around interimSpace. open var interimSpacePreset: InterimSpacePreset { get { return (collectionViewLayout as? CollectionViewLayout)!.interimSpacePreset } set(value) { (collectionViewLayout as? CollectionViewLayout)!.interimSpacePreset = value } } /// Spacing between items. @IBInspectable open var interimSpace: InterimSpace { get { return (collectionViewLayout as? CollectionViewLayout)!.interimSpace } set(value) { (collectionViewLayout as? CollectionViewLayout)!.interimSpace = value } } /** An initializer that initializes the object with a NSCoder object. - Parameter aDecoder: A NSCoder instance. */ public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) prepare() } /** An initializer that initializes the object. - Parameter frame: A CGRect defining the view's frame. - Parameter collectionViewLayout: A UICollectionViewLayout reference. */ public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { super.init(frame: frame, collectionViewLayout: layout) prepare() } /** An initializer that initializes the object. - Parameter collectionViewLayout: A UICollectionViewLayout reference. */ public init(collectionViewLayout layout: UICollectionViewLayout) { super.init(frame: .zero, collectionViewLayout: layout) prepare() } /** An initializer that initializes the object. - Parameter frame: A CGRect defining the view's frame. */ public init(frame: CGRect) { let layout = CollectionViewLayout() super.init(frame: frame, collectionViewLayout: layout) prepare() } /// A convenience initializer that initializes the object. public init() { let layout = CollectionViewLayout() super.init(frame: .zero, collectionViewLayout: layout) prepare() } /** Prepares the view instance when intialized. When subclassing, it is recommended to override the prepare method to initialize property values and other setup operations. The super.prepare method should always be called immediately when subclassing. */ open func prepare() { backgroundColor = .white contentScaleFactor = Screen.scale register(CollectionViewCell.self, forCellWithReuseIdentifier: "CollectionViewCell") } } ================================================ FILE: Sources/iOS/Collection/CollectionViewCell.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion @objc(CollectionViewCell) open class CollectionViewCell: UICollectionViewCell, Pulseable, PulseableLayer { /** A CAShapeLayer used to manage elements that would be affected by the clipToBounds property of the backing layer. For example, this allows the dropshadow effect on the backing layer, while clipping the image to a desired shape within the visualLayer. */ public let visualLayer = CAShapeLayer() /// A Pulse reference. internal var pulse: Pulse! /// A reference to the pulse layer. internal var pulseLayer: CALayer? { return pulse.pulseLayer } /// PulseAnimation value. open var pulseAnimation: PulseAnimation { get { return pulse.animation } set(value) { pulse.animation = value } } /// PulseAnimation color. @IBInspectable open var pulseColor: UIColor { get { return pulse.color } set(value) { pulse.color = value } } /// Pulse opacity. @IBInspectable open var pulseOpacity: CGFloat { get { return pulse.opacity } set(value) { pulse.opacity = value } } /** A property that manages an image for the visualLayer's contents property. Images should not be set to the backing layer's contents property to avoid conflicts when using clipsToBounds. */ @IBInspectable open var image: UIImage? { didSet { visualLayer.contents = image?.cgImage } } /** Allows a relative subrectangle within the range of 0 to 1 to be specified for the visualLayer's contents property. This allows much greater flexibility than the contentsGravity property in terms of how the image is cropped and stretched. */ @IBInspectable open var contentsRect: CGRect { get { return visualLayer.contentsRect } set(value) { visualLayer.contentsRect = value } } /** A CGRect that defines a stretchable region inside the visualLayer with a fixed border around the edge. */ @IBInspectable open var contentsCenter: CGRect { get { return visualLayer.contentsCenter } set(value) { visualLayer.contentsCenter = value } } /** A floating point value that defines a ratio between the pixel dimensions of the visualLayer's contents property and the size of the view. By default, this value is set to the Screen.scale. */ @IBInspectable open var contentsScale: CGFloat { get { return visualLayer.contentsScale } set(value) { visualLayer.contentsScale = value } } /// Determines how content should be aligned within the visualLayer's bounds. @IBInspectable open var contentsGravity: CALayerContentsGravity { get { return visualLayer.contentsGravity } set(value) { visualLayer.contentsGravity = value } } /// A property that accesses the backing layer's background @IBInspectable open override var backgroundColor: UIColor? { didSet { layer.backgroundColor = backgroundColor?.cgColor } } /** An initializer that initializes the object with a NSCoder object. - Parameter aDecoder: A NSCoder instance. */ public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) prepare() } /** An initializer that initializes the object with a CGRect object. If AutoLayout is used, it is better to initilize the instance using the init() initializer. - Parameter frame: A CGRect instance. */ public override init(frame: CGRect) { super.init(frame: frame) prepare() } open override func layoutSubviews() { super.layoutSubviews() layoutShape() layoutVisualLayer() layoutShadowPath() } /** Triggers the pulse animation. - Parameter point: A Optional point to pulse from, otherwise pulses from the center. */ open func pulse(point: CGPoint? = nil) { pulse.expand(point: point ?? center) Motion.delay(0.35) { [weak self] in self?.pulse.contract() } } /** A delegation method that is executed when the view has began a touch event. - Parameter touches: A set of UITouch objects. - Parameter event: A UIEvent object. */ open override func touchesBegan(_ touches: Set, with event: UIEvent?) { super.touchesBegan(touches, with: event) pulse.expand(point: layer.convert(touches.first!.location(in: self), from: layer)) } /** A delegation method that is executed when the view touch event has ended. - Parameter touches: A set of UITouch objects. - Parameter event: A UIEvent object. */ open override func touchesEnded(_ touches: Set, with event: UIEvent?) { super.touchesEnded(touches, with: event) pulse.contract() } /** A delegation method that is executed when the view touch event has been cancelled. - Parameter touches: A set of UITouch objects. - Parameter event: A UIEvent object. */ open override func touchesCancelled(_ touches: Set, with event: UIEvent?) { super.touchesCancelled(touches, with: event) pulse.contract() } /** Prepares the view instance when intialized. When subclassing, it is recommended to override the prepare method to initialize property values and other setup operations. The super.prepare method should always be called immediately when subclassing. */ open func prepare() { contentsGravity = .resizeAspectFill contentScaleFactor = Screen.scale backgroundColor = .white prepareVisualLayer() preparePulse() } } fileprivate extension CollectionViewCell { /// Prepares the pulse motion. func preparePulse() { pulse = Pulse(pulseView: self, pulseLayer: visualLayer) } /// Prepares the visualLayer property. func prepareVisualLayer() { visualLayer.zPosition = 0 visualLayer.masksToBounds = true layer.addSublayer(visualLayer) } } fileprivate extension CollectionViewCell { /// Manages the layout for the visualLayer property. func layoutVisualLayer() { visualLayer.frame = bounds visualLayer.cornerRadius = layer.cornerRadius } } ================================================ FILE: Sources/iOS/Collection/CollectionViewController.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit public protocol CollectionViewDelegate: UICollectionViewDelegate {} public protocol CollectionViewDataSource: UICollectionViewDataSource { /** Retrieves the data source items for the collectionView. - Returns: An Array of DataSourceItem objects. */ var dataSourceItems: [DataSourceItem] { get } } extension UIViewController { /** A convenience property that provides access to the CollectionViewController. This is the recommended method of accessing the CollectionViewController through child UIViewControllers. */ public var collectionViewController: CollectionViewController? { return traverseViewControllerHierarchyForClassType() } } open class CollectionViewController: ViewController { /// A reference to a Reminder. public let collectionView = CollectionView() open var dataSourceItems = [DataSourceItem]() open override func prepare() { super.prepare() prepareCollectionView() } open override func layoutSubviews() { super.layoutSubviews() layoutCollectionView() } } extension CollectionViewController { /// Prepares the collectionView. fileprivate func prepareCollectionView() { collectionView.delegate = self collectionView.dataSource = self view.layout(collectionView).edges() } } extension CollectionViewController { /// Sets the frame for the collectionView. fileprivate func layoutCollectionView() { collectionView.frame = view.bounds } } extension CollectionViewController: CollectionViewDelegate {} extension CollectionViewController: CollectionViewDataSource { @objc open func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 } @objc open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return dataSourceItems.count } @objc open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { return collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) } } ================================================ FILE: Sources/iOS/Collection/CollectionViewLayout.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit open class CollectionViewLayout: UICollectionViewLayout { /// Used to calculate the dimensions of the cells. public var offset = CGPoint.zero /// The size of items. public var itemSize = CGSize.zero /// A preset wrapper around contentEdgeInsets. public var contentEdgeInsetsPreset = EdgeInsetsPreset.none { didSet { contentEdgeInsets = EdgeInsetsPresetToValue(preset: contentEdgeInsetsPreset) } } /// A wrapper around grid.contentEdgeInsets. public var contentEdgeInsets = EdgeInsets.zero /// Size of the content. public fileprivate(set) var contentSize = CGSize.zero /// Layout attribute items. public fileprivate(set) lazy var layoutItems = [(UICollectionViewLayoutAttributes, NSIndexPath)]() /// Cell data source items. public fileprivate(set) var dataSourceItems: [DataSourceItem]? /// Scroll direction. public var scrollDirection = UICollectionView.ScrollDirection.vertical /// A preset wrapper around interimSpace. public var interimSpacePreset = InterimSpacePreset.none { didSet { interimSpace = InterimSpacePresetToValue(preset: interimSpacePreset) } } /// Spacing between items. public var interimSpace: InterimSpace = 0 open override var collectionViewContentSize: CGSize { return contentSize } } extension CollectionViewLayout { /** Retrieves the index paths for the items within the passed in CGRect. - Parameter rect: A CGRect that acts as the bounds to find the items within. - Returns: An Array of NSIndexPath objects. */ public func indexPathsOfItems(in rect: CGRect) -> [NSIndexPath] { var paths = [NSIndexPath]() for (attribute, indexPath) in layoutItems { guard rect.intersects(attribute.frame) else { continue } paths.append(indexPath) } return paths } } extension CollectionViewLayout { /** Prepares the layout for the given data source items. - Parameter for dataSourceItems: An Array of DataSourceItems. */ fileprivate func prepareLayout(for dataSourceItems: [DataSourceItem]) { self.dataSourceItems = dataSourceItems layoutItems.removeAll() offset.x = contentEdgeInsets.left offset.y = contentEdgeInsets.top for i in 0.. UICollectionViewLayoutAttributes? { let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) let dataSourceItem = dataSourceItems![indexPath.item] if 0 < itemSize.width && 0 < itemSize.height { attributes.frame = CGRect(x: offset.x, y: offset.y, width: itemSize.width - contentEdgeInsets.left - contentEdgeInsets.right, height: itemSize.height - contentEdgeInsets.top - contentEdgeInsets.bottom) } else if .vertical == scrollDirection { if let h = dataSourceItem.height { attributes.frame = CGRect(x: contentEdgeInsets.left, y: offset.y, width: collectionView!.bounds.width - contentEdgeInsets.left - contentEdgeInsets.right, height: h) } else if let v = dataSourceItem.data as? UIView, 0 < v.bounds.height { v.updateConstraintsIfNeeded() v.updateConstraints() v.setNeedsLayout() v.layoutIfNeeded() attributes.frame = CGRect(x: contentEdgeInsets.left, y: offset.y, width: collectionView!.bounds.width - contentEdgeInsets.left - contentEdgeInsets.right, height: v.bounds.height) } else { attributes.frame = CGRect(x: contentEdgeInsets.left, y: offset.y, width: collectionView!.bounds.width - contentEdgeInsets.left - contentEdgeInsets.right, height: collectionView!.bounds.height) } } else { if let w = dataSourceItem.width { attributes.frame = CGRect(x: offset.x, y: contentEdgeInsets.top, width: w, height: collectionView!.bounds.height - contentEdgeInsets.top - contentEdgeInsets.bottom) } else if let v = dataSourceItem.data as? UIView, 0 < v.bounds.width { v.updateConstraintsIfNeeded() v.updateConstraints() v.setNeedsLayout() v.layoutIfNeeded() attributes.frame = CGRect(x: offset.x, y: contentEdgeInsets.top, width: v.bounds.width, height: collectionView!.bounds.height - contentEdgeInsets.top - contentEdgeInsets.bottom) } else { attributes.frame = CGRect(x: offset.x, y: contentEdgeInsets.top, width: collectionView!.bounds.width, height: collectionView!.bounds.height - contentEdgeInsets.top - contentEdgeInsets.bottom) } } return attributes } open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { var layoutAttributes = [UICollectionViewLayoutAttributes]() for (attribute, _) in layoutItems { if rect.intersects(attribute.frame) { layoutAttributes.append(attribute) } } return layoutAttributes } open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return .vertical == scrollDirection ? newBounds.width != collectionView!.bounds.width : newBounds.height != collectionView!.bounds.height } open override func prepare() { guard let dataSource = collectionView?.dataSource as? CollectionViewDataSource else { return } prepareLayout(for: dataSource.dataSourceItems) } open override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint { return proposedContentOffset } } ================================================ FILE: Sources/iOS/Color/Color.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit @objc(ColorPalette) public protocol ColorPalette { /// Material color code: 50 static var lighten5: UIColor { get } /// Material color code: 100 static var lighten4: UIColor { get } /// Material color code: 200 static var lighten3: UIColor { get } /// Material color code: 300 static var lighten2: UIColor { get } /// Material color code: 400 static var lighten1: UIColor { get } /// Material color code: 500 static var base: UIColor { get } /// Material color code: 600 static var darken1: UIColor { get } /// Material color code: 700 static var darken2: UIColor { get } /// Material color code: 800 static var darken3: UIColor { get } /// Material color code: 900 static var darken4: UIColor { get } /// Material color code: A100 @objc optional static var accent1: UIColor { get } /// Material color code: A200 @objc optional static var accent2: UIColor { get } /// Material color code: A400 @objc optional static var accent3: UIColor { get } /// Material color code: A700 @objc optional static var accent4: UIColor { get } } open class Color: UIColor { // dark text open class darkText { public static let primary = Color.black.withAlphaComponent(0.87) public static let secondary = Color.black.withAlphaComponent(0.54) public static let others = Color.black.withAlphaComponent(0.38) public static let dividers = Color.black.withAlphaComponent(0.12) } // light text open class lightText { public static let primary = Color.white public static let secondary = Color.white.withAlphaComponent(0.7) public static let others = Color.white.withAlphaComponent(0.5) public static let dividers = Color.white.withAlphaComponent(0.12) } // red open class red: ColorPalette { public static let lighten5 = UIColor(red: 255/255, green: 235/255, blue: 238/255, alpha: 1) public static let lighten4 = UIColor(red: 255/255, green: 205/255, blue: 210/255, alpha: 1) public static let lighten3 = UIColor(red: 239/255, green: 154/255, blue: 154/255, alpha: 1) public static let lighten2 = UIColor(red: 229/255, green: 115/255, blue: 115/255, alpha: 1) public static let lighten1 = UIColor(red: 229/255, green: 83/255, blue: 80/255, alpha: 1) public static let base = UIColor(red: 244/255, green: 67/255, blue: 54/255, alpha: 1) public static let darken1 = UIColor(red: 229/255, green: 57/255, blue: 53/255, alpha: 1) public static let darken2 = UIColor(red: 211/255, green: 47/255, blue: 47/255, alpha: 1) public static let darken3 = UIColor(red: 198/255, green: 40/255, blue: 40/255, alpha: 1) public static let darken4 = UIColor(red: 183/255, green: 28/255, blue: 28/255, alpha: 1) public static let accent1 = UIColor(red: 255/255, green: 138/255, blue: 128/255, alpha: 1) public static let accent2 = UIColor(red: 255/255, green: 82/255, blue: 82/255, alpha: 1) public static let accent3 = UIColor(red: 255/255, green: 23/255, blue: 68/255, alpha: 1) public static let accent4 = UIColor(red: 213/255, green: 0/255, blue: 0/255, alpha: 1) } // pink open class pink: ColorPalette { public static let lighten5 = UIColor(red: 252/255, green: 228/255, blue: 236/255, alpha: 1) public static let lighten4 = UIColor(red: 248/255, green: 187/255, blue: 208/255, alpha: 1) public static let lighten3 = UIColor(red: 244/255, green: 143/255, blue: 177/255, alpha: 1) public static let lighten2 = UIColor(red: 240/255, green: 98/255, blue: 146/255, alpha: 1) public static let lighten1 = UIColor(red: 236/255, green: 64/255, blue: 122/255, alpha: 1) public static let base = UIColor(red: 233/255, green: 30/255, blue: 99/255, alpha: 1) public static let darken1 = UIColor(red: 216/255, green: 27/255, blue: 96/255, alpha: 1) public static let darken2 = UIColor(red: 194/255, green: 24/255, blue: 91/255, alpha: 1) public static let darken3 = UIColor(red: 173/255, green: 20/255, blue: 87/255, alpha: 1) public static let darken4 = UIColor(red: 136/255, green: 14/255, blue: 79/255, alpha: 1) public static let accent1 = UIColor(red: 255/255, green: 128/255, blue: 171/255, alpha: 1) public static let accent2 = UIColor(red: 255/255, green: 64/255, blue: 129/255, alpha: 1) public static let accent3 = UIColor(red: 245/255, green: 0/255, blue: 87/255, alpha: 1) public static let accent4 = UIColor(red: 197/255, green: 17/255, blue: 98/255, alpha: 1) } // purple open class purple: ColorPalette { public static let lighten5 = UIColor(red: 243/255, green: 229/255, blue: 245/255, alpha: 1) public static let lighten4 = UIColor(red: 225/255, green: 190/255, blue: 231/255, alpha: 1) public static let lighten3 = UIColor(red: 206/255, green: 147/255, blue: 216/255, alpha: 1) public static let lighten2 = UIColor(red: 186/255, green: 104/255, blue: 200/255, alpha: 1) public static let lighten1 = UIColor(red: 171/255, green: 71/255, blue: 188/255, alpha: 1) public static let base = UIColor(red: 156/255, green: 39/255, blue: 176/255, alpha: 1) public static let darken1 = UIColor(red: 142/255, green: 36/255, blue: 170/255, alpha: 1) public static let darken2 = UIColor(red: 123/255, green: 31/255, blue: 162/255, alpha: 1) public static let darken3 = UIColor(red: 106/255, green: 27/255, blue: 154/255, alpha: 1) public static let darken4 = UIColor(red: 74/255, green: 20/255, blue: 140/255, alpha: 1) public static let accent1 = UIColor(red: 234/255, green: 128/255, blue: 252/255, alpha: 1) public static let accent2 = UIColor(red: 224/255, green: 64/255, blue: 251/255, alpha: 1) public static let accent3 = UIColor(red: 213/255, green: 0/255, blue: 249/255, alpha: 1) public static let accent4 = UIColor(red: 170/255, green: 0/255, blue: 255/255, alpha: 1) } // deepPurple open class deepPurple: ColorPalette { public static let lighten5 = UIColor(red: 237/255, green: 231/255, blue: 246/255, alpha: 1) public static let lighten4 = UIColor(red: 209/255, green: 196/255, blue: 233/255, alpha: 1) public static let lighten3 = UIColor(red: 179/255, green: 157/255, blue: 219/255, alpha: 1) public static let lighten2 = UIColor(red: 149/255, green: 117/255, blue: 205/255, alpha: 1) public static let lighten1 = UIColor(red: 126/255, green: 87/255, blue: 194/255, alpha: 1) public static let base = UIColor(red: 103/255, green: 58/255, blue: 183/255, alpha: 1) public static let darken1 = UIColor(red: 94/255, green: 53/255, blue: 177/255, alpha: 1) public static let darken2 = UIColor(red: 81/255, green: 45/255, blue: 168/255, alpha: 1) public static let darken3 = UIColor(red: 69/255, green: 39/255, blue: 160/255, alpha: 1) public static let darken4 = UIColor(red: 49/255, green: 27/255, blue: 146/255, alpha: 1) public static let accent1 = UIColor(red: 179/255, green: 136/255, blue: 255/255, alpha: 1) public static let accent2 = UIColor(red: 124/255, green: 77/255, blue: 255/255, alpha: 1) public static let accent3 = UIColor(red: 101/255, green: 31/255, blue: 255/255, alpha: 1) public static let accent4 = UIColor(red: 98/255, green: 0/255, blue: 234/255, alpha: 1) } // indigo open class indigo: ColorPalette { public static let lighten5 = UIColor(red: 232/255, green: 234/255, blue: 246/255, alpha: 1) public static let lighten4 = UIColor(red: 197/255, green: 202/255, blue: 233/255, alpha: 1) public static let lighten3 = UIColor(red: 159/255, green: 168/255, blue: 218/255, alpha: 1) public static let lighten2 = UIColor(red: 121/255, green: 134/255, blue: 203/255, alpha: 1) public static let lighten1 = UIColor(red: 92/255, green: 107/255, blue: 192/255, alpha: 1) public static let base = UIColor(red: 63/255, green: 81/255, blue: 181/255, alpha: 1) public static let darken1 = UIColor(red: 57/255, green: 73/255, blue: 171/255, alpha: 1) public static let darken2 = UIColor(red: 48/255, green: 63/255, blue: 159/255, alpha: 1) public static let darken3 = UIColor(red: 40/255, green: 53/255, blue: 147/255, alpha: 1) public static let darken4 = UIColor(red: 26/255, green: 35/255, blue: 126/255, alpha: 1) public static let accent1 = UIColor(red: 140/255, green: 158/255, blue: 255/255, alpha: 1) public static let accent2 = UIColor(red: 83/255, green: 109/255, blue: 254/255, alpha: 1) public static let accent3 = UIColor(red: 61/255, green: 90/255, blue: 254/255, alpha: 1) public static let accent4 = UIColor(red: 48/255, green: 79/255, blue: 254/255, alpha: 1) } // blue open class blue: ColorPalette { public static let lighten5 = UIColor(red: 227/255, green: 242/255, blue: 253/255, alpha: 1) public static let lighten4 = UIColor(red: 187/255, green: 222/255, blue: 251/255, alpha: 1) public static let lighten3 = UIColor(red: 144/255, green: 202/255, blue: 249/255, alpha: 1) public static let lighten2 = UIColor(red: 100/255, green: 181/255, blue: 246/255, alpha: 1) public static let lighten1 = UIColor(red: 66/255, green: 165/255, blue: 245/255, alpha: 1) public static let base = UIColor(red: 33/255, green: 150/255, blue: 243/255, alpha: 1) public static let darken1 = UIColor(red: 30/255, green: 136/255, blue: 229/255, alpha: 1) public static let darken2 = UIColor(red: 25/255, green: 118/255, blue: 210/255, alpha: 1) public static let darken3 = UIColor(red: 21/255, green: 101/255, blue: 192/255, alpha: 1) public static let darken4 = UIColor(red: 13/255, green: 71/255, blue: 161/255, alpha: 1) public static let accent1 = UIColor(red: 130/255, green: 177/255, blue: 255/255, alpha: 1) public static let accent2 = UIColor(red: 68/255, green: 138/255, blue: 255/255, alpha: 1) public static let accent3 = UIColor(red: 41/255, green: 121/255, blue: 255/255, alpha: 1) public static let accent4 = UIColor(red: 41/255, green: 98/255, blue: 255/255, alpha: 1) } // light blue open class lightBlue: ColorPalette { public static let lighten5 = UIColor(red: 225/255, green: 245/255, blue: 254/255, alpha: 1) public static let lighten4 = UIColor(red: 179/255, green: 229/255, blue: 252/255, alpha: 1) public static let lighten3 = UIColor(red: 129/255, green: 212/255, blue: 250/255, alpha: 1) public static let lighten2 = UIColor(red: 79/255, green: 195/255, blue: 247/255, alpha: 1) public static let lighten1 = UIColor(red: 41/255, green: 182/255, blue: 246/255, alpha: 1) public static let base = UIColor(red: 3/255, green: 169/255, blue: 244/255, alpha: 1) public static let darken1 = UIColor(red: 3/255, green: 155/255, blue: 229/255, alpha: 1) public static let darken2 = UIColor(red: 2/255, green: 136/255, blue: 209/255, alpha: 1) public static let darken3 = UIColor(red: 2/255, green: 119/255, blue: 189/255, alpha: 1) public static let darken4 = UIColor(red: 1/255, green: 87/255, blue: 155/255, alpha: 1) public static let accent1 = UIColor(red: 128/255, green: 216/255, blue: 255/255, alpha: 1) public static let accent2 = UIColor(red: 64/255, green: 196/255, blue: 255/255, alpha: 1) public static let accent3 = UIColor(red: 0/255, green: 176/255, blue: 255/255, alpha: 1) public static let accent4 = UIColor(red: 0/255, green: 145/255, blue: 234/255, alpha: 1) } // cyan open class cyan: ColorPalette { public static let lighten5 = UIColor(red: 224/255, green: 247/255, blue: 250/255, alpha: 1) public static let lighten4 = UIColor(red: 178/255, green: 235/255, blue: 242/255, alpha: 1) public static let lighten3 = UIColor(red: 128/255, green: 222/255, blue: 234/255, alpha: 1) public static let lighten2 = UIColor(red: 77/255, green: 208/255, blue: 225/255, alpha: 1) public static let lighten1 = UIColor(red: 38/255, green: 198/255, blue: 218/255, alpha: 1) public static let base = UIColor(red: 0/255, green: 188/255, blue: 212/255, alpha: 1) public static let darken1 = UIColor(red: 0/255, green: 172/255, blue: 193/255, alpha: 1) public static let darken2 = UIColor(red: 0/255, green: 151/255, blue: 167/255, alpha: 1) public static let darken3 = UIColor(red: 0/255, green: 131/255, blue: 143/255, alpha: 1) public static let darken4 = UIColor(red: 0/255, green: 96/255, blue: 100/255, alpha: 1) public static let accent1 = UIColor(red: 132/255, green: 255/255, blue: 255/255, alpha: 1) public static let accent2 = UIColor(red: 24/255, green: 255/255, blue: 255/255, alpha: 1) public static let accent3 = UIColor(red: 0/255, green: 229/255, blue: 255/255, alpha: 1) public static let accent4 = UIColor(red: 0/255, green: 184/255, blue: 212/255, alpha: 1) } // teal open class teal: ColorPalette { public static let lighten5 = UIColor(red: 224/255, green: 242/255, blue: 241/255, alpha: 1) public static let lighten4 = UIColor(red: 178/255, green: 223/255, blue: 219/255, alpha: 1) public static let lighten3 = UIColor(red: 128/255, green: 203/255, blue: 196/255, alpha: 1) public static let lighten2 = UIColor(red: 77/255, green: 182/255, blue: 172/255, alpha: 1) public static let lighten1 = UIColor(red: 38/255, green: 166/255, blue: 154/255, alpha: 1) public static let base = UIColor(red: 0/255, green: 150/255, blue: 136/255, alpha: 1) public static let darken1 = UIColor(red: 0/255, green: 137/255, blue: 123/255, alpha: 1) public static let darken2 = UIColor(red: 0/255, green: 121/255, blue: 107/255, alpha: 1) public static let darken3 = UIColor(red: 0/255, green: 105/255, blue: 92/255, alpha: 1) public static let darken4 = UIColor(red: 0/255, green: 77/255, blue: 64/255, alpha: 1) public static let accent1 = UIColor(red: 167/255, green: 255/255, blue: 235/255, alpha: 1) public static let accent2 = UIColor(red: 100/255, green: 255/255, blue: 218/255, alpha: 1) public static let accent3 = UIColor(red: 29/255, green: 233/255, blue: 182/255, alpha: 1) public static let accent4 = UIColor(red: 0/255, green: 191/255, blue: 165/255, alpha: 1) } // green open class green: ColorPalette { public static let lighten5 = UIColor(red: 232/255, green: 245/255, blue: 233/255, alpha: 1) public static let lighten4 = UIColor(red: 200/255, green: 230/255, blue: 201/255, alpha: 1) public static let lighten3 = UIColor(red: 165/255, green: 214/255, blue: 167/255, alpha: 1) public static let lighten2 = UIColor(red: 129/255, green: 199/255, blue: 132/255, alpha: 1) public static let lighten1 = UIColor(red: 102/255, green: 187/255, blue: 106/255, alpha: 1) public static let base = UIColor(red: 76/255, green: 175/255, blue: 80/255, alpha: 1) public static let darken1 = UIColor(red: 67/255, green: 160/255, blue: 71/255, alpha: 1) public static let darken2 = UIColor(red: 56/255, green: 142/255, blue: 60/255, alpha: 1) public static let darken3 = UIColor(red: 46/255, green: 125/255, blue: 50/255, alpha: 1) public static let darken4 = UIColor(red: 27/255, green: 94/255, blue: 32/255, alpha: 1) public static let accent1 = UIColor(red: 185/255, green: 246/255, blue: 202/255, alpha: 1) public static let accent2 = UIColor(red: 105/255, green: 240/255, blue: 174/255, alpha: 1) public static let accent3 = UIColor(red: 0/255, green: 230/255, blue: 118/255, alpha: 1) public static let accent4 = UIColor(red: 0/255, green: 200/255, blue: 83/255, alpha: 1) } // light green open class lightGreen: ColorPalette { public static let lighten5 = UIColor(red: 241/255, green: 248/255, blue: 233/255, alpha: 1) public static let lighten4 = UIColor(red: 220/255, green: 237/255, blue: 200/255, alpha: 1) public static let lighten3 = UIColor(red: 197/255, green: 225/255, blue: 165/255, alpha: 1) public static let lighten2 = UIColor(red: 174/255, green: 213/255, blue: 129/255, alpha: 1) public static let lighten1 = UIColor(red: 156/255, green: 204/255, blue: 101/255, alpha: 1) public static let base = UIColor(red: 139/255, green: 195/255, blue: 74/255, alpha: 1) public static let darken1 = UIColor(red: 124/255, green: 179/255, blue: 66/255, alpha: 1) public static let darken2 = UIColor(red: 104/255, green: 159/255, blue: 56/255, alpha: 1) public static let darken3 = UIColor(red: 85/255, green: 139/255, blue: 47/255, alpha: 1) public static let darken4 = UIColor(red: 51/255, green: 105/255, blue: 30/255, alpha: 1) public static let accent1 = UIColor(red: 204/255, green: 255/255, blue: 144/255, alpha: 1) public static let accent2 = UIColor(red: 178/255, green: 255/255, blue: 89/255, alpha: 1) public static let accent3 = UIColor(red: 118/255, green: 255/255, blue: 3/255, alpha: 1) public static let accent4 = UIColor(red: 100/255, green: 221/255, blue: 23/255, alpha: 1) } // lime open class lime: ColorPalette { public static let lighten5 = UIColor(red: 249/255, green: 251/255, blue: 231/255, alpha: 1) public static let lighten4 = UIColor(red: 240/255, green: 244/255, blue: 195/255, alpha: 1) public static let lighten3 = UIColor(red: 230/255, green: 238/255, blue: 156/255, alpha: 1) public static let lighten2 = UIColor(red: 220/255, green: 231/255, blue: 117/255, alpha: 1) public static let lighten1 = UIColor(red: 212/255, green: 225/255, blue: 87/255, alpha: 1) public static let base = UIColor(red: 205/255, green: 220/255, blue: 57/255, alpha: 1) public static let darken1 = UIColor(red: 192/255, green: 202/255, blue: 51/255, alpha: 1) public static let darken2 = UIColor(red: 175/255, green: 180/255, blue: 43/255, alpha: 1) public static let darken3 = UIColor(red: 158/255, green: 157/255, blue: 36/255, alpha: 1) public static let darken4 = UIColor(red: 130/255, green: 119/255, blue: 23/255, alpha: 1) public static let accent1 = UIColor(red: 244/255, green: 255/255, blue: 129/255, alpha: 1) public static let accent2 = UIColor(red: 238/255, green: 255/255, blue: 65/255, alpha: 1) public static let accent3 = UIColor(red: 198/255, green: 255/255, blue: 0/255, alpha: 1) public static let accent4 = UIColor(red: 174/255, green: 234/255, blue: 0/255, alpha: 1) } // yellow open class yellow: ColorPalette { public static let lighten5 = UIColor(red: 255/255, green: 253/255, blue: 231/255, alpha: 1) public static let lighten4 = UIColor(red: 255/255, green: 249/255, blue: 196/255, alpha: 1) public static let lighten3 = UIColor(red: 255/255, green: 245/255, blue: 157/255, alpha: 1) public static let lighten2 = UIColor(red: 255/255, green: 241/255, blue: 118/255, alpha: 1) public static let lighten1 = UIColor(red: 255/255, green: 238/255, blue: 88/255, alpha: 1) public static let base = UIColor(red: 255/255, green: 235/255, blue: 59/255, alpha: 1) public static let darken1 = UIColor(red: 253/255, green: 216/255, blue: 53/255, alpha: 1) public static let darken2 = UIColor(red: 251/255, green: 192/255, blue: 45/255, alpha: 1) public static let darken3 = UIColor(red: 249/255, green: 168/255, blue: 37/255, alpha: 1) public static let darken4 = UIColor(red: 245/255, green: 127/255, blue: 23/255, alpha: 1) public static let accent1 = UIColor(red: 255/255, green: 255/255, blue: 141/255, alpha: 1) public static let accent2 = UIColor(red: 255/255, green: 255/255, blue: 0/255, alpha: 1) public static let accent3 = UIColor(red: 255/255, green: 234/255, blue: 0/255, alpha: 1) public static let accent4 = UIColor(red: 255/255, green: 214/255, blue: 0/255, alpha: 1) } // amber open class amber: ColorPalette { public static let lighten5 = UIColor(red: 255/255, green: 248/255, blue: 225/255, alpha: 1) public static let lighten4 = UIColor(red: 255/255, green: 236/255, blue: 179/255, alpha: 1) public static let lighten3 = UIColor(red: 255/255, green: 224/255, blue: 130/255, alpha: 1) public static let lighten2 = UIColor(red: 255/255, green: 213/255, blue: 79/255, alpha: 1) public static let lighten1 = UIColor(red: 255/255, green: 202/255, blue: 40/255, alpha: 1) public static let base = UIColor(red: 255/255, green: 193/255, blue: 7/255, alpha: 1) public static let darken1 = UIColor(red: 255/255, green: 179/255, blue: 0/255, alpha: 1) public static let darken2 = UIColor(red: 255/255, green: 160/255, blue: 0/255, alpha: 1) public static let darken3 = UIColor(red: 255/255, green: 143/255, blue: 0/255, alpha: 1) public static let darken4 = UIColor(red: 255/255, green: 111/255, blue: 0/255, alpha: 1) public static let accent1 = UIColor(red: 255/255, green: 229/255, blue: 127/255, alpha: 1) public static let accent2 = UIColor(red: 255/255, green: 215/255, blue: 64/255, alpha: 1) public static let accent3 = UIColor(red: 255/255, green: 196/255, blue: 0/255, alpha: 1) public static let accent4 = UIColor(red: 255/255, green: 171/255, blue: 0/255, alpha: 1) } // orange open class orange: ColorPalette { public static let lighten5 = UIColor(red: 255/255, green: 243/255, blue: 224/255, alpha: 1) public static let lighten4 = UIColor(red: 255/255, green: 224/255, blue: 178/255, alpha: 1) public static let lighten3 = UIColor(red: 255/255, green: 204/255, blue: 128/255, alpha: 1) public static let lighten2 = UIColor(red: 255/255, green: 183/255, blue: 77/255, alpha: 1) public static let lighten1 = UIColor(red: 255/255, green: 167/255, blue: 38/255, alpha: 1) public static let base = UIColor(red: 255/255, green: 152/255, blue: 0/255, alpha: 1) public static let darken1 = UIColor(red: 251/255, green: 140/255, blue: 0/255, alpha: 1) public static let darken2 = UIColor(red: 245/255, green: 124/255, blue: 0/255, alpha: 1) public static let darken3 = UIColor(red: 239/255, green: 108/255, blue: 0/255, alpha: 1) public static let darken4 = UIColor(red: 230/255, green: 81/255, blue: 0/255, alpha: 1) public static let accent1 = UIColor(red: 255/255, green: 209/255, blue: 128/255, alpha: 1) public static let accent2 = UIColor(red: 255/255, green: 171/255, blue: 64/255, alpha: 1) public static let accent3 = UIColor(red: 255/255, green: 145/255, blue: 0/255, alpha: 1) public static let accent4 = UIColor(red: 255/255, green: 109/255, blue: 0/255, alpha: 1) } // deep orange open class deepOrange: ColorPalette { public static let lighten5 = UIColor(red: 251/255, green: 233/255, blue: 231/255, alpha: 1) public static let lighten4 = UIColor(red: 255/255, green: 204/255, blue: 188/255, alpha: 1) public static let lighten3 = UIColor(red: 255/255, green: 171/255, blue: 145/255, alpha: 1) public static let lighten2 = UIColor(red: 255/255, green: 138/255, blue: 101/255, alpha: 1) public static let lighten1 = UIColor(red: 255/255, green: 112/255, blue: 67/255, alpha: 1) public static let base = UIColor(red: 255/255, green: 87/255, blue: 34/255, alpha: 1) public static let darken1 = UIColor(red: 244/255, green: 81/255, blue: 30/255, alpha: 1) public static let darken2 = UIColor(red: 230/255, green: 74/255, blue: 25/255, alpha: 1) public static let darken3 = UIColor(red: 216/255, green: 67/255, blue: 21/255, alpha: 1) public static let darken4 = UIColor(red: 191/255, green: 54/255, blue: 12/255, alpha: 1) public static let accent1 = UIColor(red: 255/255, green: 158/255, blue: 128/255, alpha: 1) public static let accent2 = UIColor(red: 255/255, green: 110/255, blue: 64/255, alpha: 1) public static let accent3 = UIColor(red: 255/255, green: 61/255, blue: 0/255, alpha: 1) public static let accent4 = UIColor(red: 221/255, green: 44/255, blue: 0/255, alpha: 1) } // brown open class brown: ColorPalette { public static let lighten5 = UIColor(red: 239/255, green: 235/255, blue: 233/255, alpha: 1) public static let lighten4 = UIColor(red: 215/255, green: 204/255, blue: 200/255, alpha: 1) public static let lighten3 = UIColor(red: 188/255, green: 170/255, blue: 164/255, alpha: 1) public static let lighten2 = UIColor(red: 161/255, green: 136/255, blue: 127/255, alpha: 1) public static let lighten1 = UIColor(red: 141/255, green: 110/255, blue: 99/255, alpha: 1) public static let base = UIColor(red: 121/255, green: 85/255, blue: 72/255, alpha: 1) public static let darken1 = UIColor(red: 109/255, green: 76/255, blue: 65/255, alpha: 1) public static let darken2 = UIColor(red: 93/255, green: 64/255, blue: 55/255, alpha: 1) public static let darken3 = UIColor(red: 78/255, green: 52/255, blue: 46/255, alpha: 1) public static let darken4 = UIColor(red: 62/255, green: 39/255, blue: 35/255, alpha: 1) } // grey open class grey: ColorPalette { public static let lighten5 = UIColor(red: 250/255, green: 250/255, blue: 250/255, alpha: 1) public static let lighten4 = UIColor(red: 245/255, green: 245/255, blue: 245/255, alpha: 1) public static let lighten3 = UIColor(red: 238/255, green: 238/255, blue: 238/255, alpha: 1) public static let lighten2 = UIColor(red: 224/255, green: 224/255, blue: 224/255, alpha: 1) public static let lighten1 = UIColor(red: 189/255, green: 189/255, blue: 189/255, alpha: 1) public static let base = UIColor(red: 158/255, green: 158/255, blue: 158/255, alpha: 1) public static let darken1 = UIColor(red: 117/255, green: 117/255, blue: 117/255, alpha: 1) public static let darken2 = UIColor(red: 97/255, green: 97/255, blue: 97/255, alpha: 1) public static let darken3 = UIColor(red: 66/255, green: 66/255, blue: 66/255, alpha: 1) public static let darken4 = UIColor(red: 33/255, green: 33/255, blue: 33/255, alpha: 1) } // blue grey open class blueGrey: ColorPalette { public static let lighten5 = UIColor(red: 236/255, green: 239/255, blue: 241/255, alpha: 1) public static let lighten4 = UIColor(red: 207/255, green: 216/255, blue: 220/255, alpha: 1) public static let lighten3 = UIColor(red: 176/255, green: 190/255, blue: 197/255, alpha: 1) public static let lighten2 = UIColor(red: 144/255, green: 164/255, blue: 174/255, alpha: 1) public static let lighten1 = UIColor(red: 120/255, green: 144/255, blue: 156/255, alpha: 1) public static let base = UIColor(red: 96/255, green: 125/255, blue: 139/255, alpha: 1) public static let darken1 = UIColor(red: 84/255, green: 110/255, blue: 122/255, alpha: 1) public static let darken2 = UIColor(red: 69/255, green: 90/255, blue: 100/255, alpha: 1) public static let darken3 = UIColor(red: 55/255, green: 71/255, blue: 79/255, alpha: 1) public static let darken4 = UIColor(red: 38/255, green: 50/255, blue: 56/255, alpha: 1) } } ================================================ FILE: Sources/iOS/Data/DataSourceItem.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit public struct DataSourceItem { /// Stores an the data for the item. public var data: Any? /// Width for horizontal scroll direction. public var width: CGFloat? /// Height for vertical scroll direction. public var height: CGFloat? /** Initializer. - Parameter data: A reference to an Any that is associated with a width or height. - Parameter width: The width for the horizontal scroll direction. - Parameter height: The height for the vertical scroll direction. */ public init(data: Any? = nil, width: CGFloat? = nil, height: CGFloat? = nil) { self.data = data self.width = width self.height = height } } ================================================ FILE: Sources/iOS/Device/Device.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit @objc(DeviceModel) public enum DeviceModel: Int { case iPodTouch5 case iPodTouch6 case iPhone4 case iPhone4s case iPhone5 case iPhone5c case iPhone5s case iPhone6 case iPhone6Plus case iPhone6s case iPhone6sPlus case iPhone7 case iPhone7Plus case iPhone8 case iPhone8Plus case iPhoneX case iPhoneXS case iPhoneXSMax case iPhoneXR case iPhoneSE case iPad2 case iPad3 case iPad4 case iPadAir case iPadAir2 case iPadMini case iPadMini2 case iPadMini3 case iPadMini4 case iPadPro case iPadProLarge case iPad5 case iPadPro2 case iPadProLarge2 case iPad6 case iPadPro3 //iPad Pro (11-inch) case iPadProLarge3 //iPad Pro (12.9-inch) (3rd generation) case appleTv case appleTv4k case homePod case simulator case unknown } public struct Device { /// Gets the Device identifier String. public static var identifier: String { var systemInfo = utsname() uname(&systemInfo) let machineMirror = Mirror(reflecting: systemInfo.machine) let identifier = machineMirror.children.reduce("") { (identifier, element) in guard let value = element.value as? Int8, value != 0 else { return identifier } return identifier + String(UnicodeScalar(UInt8(value))) } return identifier } /// Gets the model name for the device. public static var model: DeviceModel { switch identifier { case "iPod5,1": return .iPodTouch5 case "iPod7,1": return .iPodTouch6 case "iPhone4,1": return .iPhone4s case "iPhone5,1", "iPhone5,2": return .iPhone5 case "iPhone5,3", "iPhone5,4": return .iPhone5c case "iPhone6,1", "iPhone6,2": return .iPhone5s case "iPhone7,2": return .iPhone6 case "iPhone7,1": return .iPhone6Plus case "iPhone8,1": return .iPhone6s case "iPhone8,2": return .iPhone6sPlus case "iPhone8,3", "iPhone8,4": return .iPhoneSE case "iPhone9,1", "iPhone9,3": return .iPhone7 case "iPhone9,2", "iPhone9,4": return .iPhone7Plus case "iPhone10,1", "iPhone10,4": return .iPhone8 case "iPhone10,2", "iPhone10,5": return .iPhone8Plus case "iPhone10,3","iPhone10,6": return .iPhoneX case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4": return .iPad2 case "iPad3,1", "iPad3,2", "iPad3,3": return .iPad3 case "iPad3,4", "iPad3,5", "iPad3,6": return .iPad4 case "iPad4,1", "iPad4,2", "iPad4,3": return .iPadAir case "iPad5,3", "iPad5,4": return .iPadAir2 case "iPad2,5", "iPad2,6", "iPad2,7": return .iPadMini case "iPad4,4", "iPad4,5", "iPad4,6": return .iPadMini2 case "iPad4,7", "iPad4,8", "iPad4,9": return .iPadMini3 case "iPad5,1", "iPad5,2": return .iPadMini4 case "iPad6,3", "iPad6,4": return .iPadPro case "iPad6,7", "iPad6,8": return .iPadProLarge case "iPad6,11", "iPad6,12": return .iPad5 case "iPad7,3", "iPad7,4": return .iPadPro2 case "iPad7,1", "iPad7,2": return .iPadProLarge2 case "iPad7,5", "iPad7,6": return .iPad6 case "i386", "x86_64": return .simulator case "iPhone3,1", "iPhone3,2", "iPhone3,3": return .iPhone4 case "iPhone11,2": return .iPhoneXS case "iPhone11,4", "iPhone11,6": return .iPhoneXSMax case "iPhone11,8": return .iPhoneXR case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4": return .iPadPro3 case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8": return .iPadProLarge3 case "AppleTV5,3": return .appleTv case "AppleTV6,2": return .appleTv4k case "AudioAccessory1,1": return .homePod default: return .unknown } } /// Retrieves the current device type. public static var userInterfaceIdiom: UIUserInterfaceIdiom { return UIDevice.current.userInterfaceIdiom } } public func ==(lhs: DeviceModel, rhs: DeviceModel) -> Bool { return lhs.rawValue == rhs.rawValue } public func !=(lhs: DeviceModel, rhs: DeviceModel) -> Bool { return lhs.rawValue != rhs.rawValue } ================================================ FILE: Sources/iOS/Dialogs/Dialog.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion @objc public protocol DialogDelegate { /** A delegation method that is executed when the Dialog is cancelled through tapping background. - Parameter _ dialog: A Dialog. */ @objc optional func dialogDidCancel(_ dialog: Dialog) /** A delegation method that is executed when the Dialog will appear. - Parameter _ dialog: A Dialog. */ @objc optional func dialogWillAppear(_ dialog: Dialog) /** A delegation method that is executed when the Dialog did disappear. - Parameter _ dialog: A Dialog. */ @objc optional func dialogDidDisappear(_ dialog: Dialog) /** A delegation method that is executed to determine if the Dialog should be dismissed. - Parameter _ dialog: A Dialog. - Parameter shouldDismiss button: The tapped button. nil if dialog is being cancelled through tapping background. - Returns: A Boolean. */ @objc optional func dialog(_ dialog: Dialog, shouldDismiss button: Button?) -> Bool /** A delegation method that is executed when the positive button of Dialog is tapped. - Parameter _ dialog: A Dialog. - Parameter didTapPositive button: A Button. */ @objc optional func dialog(_ dialog: Dialog, didTapPositive button: Button) /** A delegation method that is executed when the negative button of Dialog is tapped. - Parameter _ dialog: A Dialog. - Parameter didTapNegative button: A Button. */ @objc optional func dialog(_ dialog: Dialog, didTapNegative button: Button) /** A delegation method that is executed when the neutral button of Dialog is tapped. - Parameter _ dialog: A Dialog. - Parameter didTapNeutral button: A Button. */ @objc optional func dialog(_ dialog: Dialog, didTapNeutral button: Button) } /// A builder for DialogController. open class Dialog: NSObject { /// A reference to dialog controller. public let controller = DialogController() /// A weak reference to DialogDelegate. open weak var delegate: DialogDelegate? /// An empty initializer. public override init() { super.init() /// Set callbacks for delegate. shouldDismiss(handler: nil) .positive(nil, handler: nil) .negative(nil, handler: nil) .neutral(nil, handler: nil) .isCancelable(controller.isCancelable, handler: nil) .willAppear(handler: nil) .didDisappear(handler: nil) } /** Sets title of the dialog. - Parameter _ text: A string. - Returns: Dialog itself to allow chaining. */ @discardableResult open func title(_ text: String?) -> Dialog { dialogView.titleLabel.text = text return self } /** Sets details of the dialog. - Parameter _ text: A string. - Returns: Dialog itself to allow chaining. */ @discardableResult open func details(_ text: String?) -> Dialog { dialogView.detailsLabel.text = text return self } /** Sets title and handler for positive button of dialog. - Parameter _ title: A string. - Parameter handler: A closure handling tap. - Returns: Dialog itself to allow chaining. */ @discardableResult open func positive(_ title: String?, handler: (() -> Void)?) -> Dialog { dialogView.positiveButton.title = title controller.didTapPositiveButtonHandler = { [weak self] in guard let strongSelf = self else { return } strongSelf.delegate?.dialog?(strongSelf, didTapPositive: strongSelf.controller.dialogView.positiveButton) handler?() } return self } /** Sets title and handler for negative button of dialog. - Parameter _ title: A string. - Parameter handler: A closure handling tap. - Returns: Dialog itself to allow chaining. */ @discardableResult open func negative(_ title: String?, handler: (() -> Void)?) -> Dialog { dialogView.negativeButton.title = title controller.didTapNegativeButtonHandler = { [weak self] in guard let strongSelf = self else { return } strongSelf.delegate?.dialog?(strongSelf, didTapNegative: strongSelf.controller.dialogView.negativeButton) handler?() } return self } /** Sets title and handler for neutral button of dialog. - Parameter _ title: A string. - Parameter handler: A closure handling tap. - Returns: Dialog itself to allow chaining. */ @discardableResult open func neutral(_ title: String?, handler: (() -> Void)?) -> Dialog { dialogView.neutralButton.title = title controller.didTapNeutralButtonHandler = { [weak self] in guard let strongSelf = self else { return } strongSelf.delegate?.dialog?(strongSelf, didTapNeutral: strongSelf.controller.dialogView.neutralButton) handler?() } return self } /** Sets cancelability of dialog and handler for when it's cancelled. - Parameter _ value: A Bool. - Parameter handler: A closure handling cancellation. - Returns: Dialog itself to allow chaining. */ @discardableResult open func isCancelable(_ value: Bool, handler: (() -> Void)? = nil) -> Dialog { controller.isCancelable = value controller.didCancelHandler = { [weak self] in guard let strongSelf = self else { return } strongSelf.delegate?.dialogDidCancel?(strongSelf) handler?() } return self } /** Sets should-dismiss handler of dialog which takes dialogView and tapped button and returns a boolean indicating if dialog should be dismissed. - Parameter handler: A closure handling if dialog can be dismissed. - Returns: Dialog itself to allow chaining. */ @discardableResult open func shouldDismiss(handler: ((DialogView, Button?) -> Bool)?) -> Dialog { controller.shouldDismissHandler = { [weak self] dialogView, button in guard let strongSelf = self else { return true } let d = strongSelf.delegate?.dialog?(strongSelf, shouldDismiss: button) ?? true let h = handler?(dialogView, button) ?? true return d && h } return self } /** Sets handler for when view controller will appear. - Parameter handler: A closure handling the event. - Returns: Dialog itself to allow chaining. */ @discardableResult open func willAppear(handler: (() -> Void)?) -> Dialog { controller.willAppear = { [weak self] in guard let strongSelf = self else { return } strongSelf.delegate?.dialogWillAppear?(strongSelf) handler?() } return self } /** Sets handler for when view controller did disappear. - Parameter handler: A closure handling the event. - Returns: Dialog itself to allow chaining. */ @discardableResult open func didDisappear(handler: (() -> Void)?) -> Dialog { controller.didDisappear = { [weak self] in guard let strongSelf = self else { return } strongSelf.delegate?.dialogDidDisappear?(strongSelf) handler?() strongSelf.controller.dialog = nil } return self } /** Sets dialog delegate. - Parameter delegate: A DialogDelegate. - Returns: Dialog itself to allow chaining. */ @discardableResult open func delegate(_ delegate: DialogDelegate) -> Dialog { self.delegate = delegate return self } /** Presents dialog modally from given viewController. - Parameter _ viewController: A UIViewController. - Returns: Dialog itself to allow chaining. */ @discardableResult open func show(_ viewController: UIViewController) -> Dialog { controller.dialog = self viewController.present(controller, animated: true, completion: nil) return self } } private extension Dialog { /// Returns dialogView of controller. var dialogView: DialogView { return controller.dialogView } } /// A memory reference to companion Dialog instance. private var DialogKey: UInt8 = 0 private extension DialogController { /** A Dialog instance attached to the dialog controller. This is used to keep Dialog alive throughout the lifespan of the controller. */ var dialog: Dialog? { get { return AssociatedObject.get(base: self, key: &DialogKey) { return nil } } set(value) { AssociatedObject.set(base: self, key: &DialogKey, value: value) } } } ================================================ FILE: Sources/iOS/Dialogs/DialogController.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit /// A UIViewController managing DialogView. open class DialogController: UIViewController { /// A reference to dialogView. public let dialogView = T() /// A boolean indicating cancelability of dialog when user taps on background. open var isCancelable = false /// A reference to did-cancel handler. open var didCancelHandler: (() -> Void)? /** A reference to should-dismiss handler which takes dialogView and tapped button and returns Boolean indicating if dialog should be dismissed. */ open var shouldDismissHandler: ((T, Button?) -> Bool)? /// A reference to handler for when positiveButton is tapped. open var didTapPositiveButtonHandler: (() -> Void)? /// A reference to handler for when negativeButton is tapped. open var didTapNegativeButtonHandler: (() -> Void)? /// A reference to handler for when neutralButton is tapped. open var didTapNeutralButtonHandler: (() -> Void)? /// A reference to handler for when controller will appear. open var willAppear: (() -> Void)? /// A reference to handler for when controller did disappear. open var didDisappear: (() -> Void)? public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) prepare() } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) prepare() } /// Prepares controller for presentation. open func prepare() { isMotionEnabled = true motionTransitionType = .fade modalPresentationStyle = .overFullScreen } open override func viewDidLoad() { super.viewDidLoad() prepareView() prepareDialogView() } open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) willAppear?() } open override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) didDisappear?() } open override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() dialogView.maxSize = CGSize(width: Screen.width * 0.8, height: Screen.height * 0.9) } /** Dismisses dialog. - Parameter isAnimated: A boolean. */ open func dismiss(isAnimated: Bool = true) { dismiss(isTriggeredByUserInteraction: false, isAnimated: isAnimated) } /// Handler for when background scrim is tapped. @objc private func didTapBackgroundView() { guard isCancelable else { return } dismiss(isTriggeredByUserInteraction: true, isAnimated: true) } /// Handler for when one of 3 dialog buttons is tapped. @objc private func didTapButton(_ sender: Button) { switch sender { case dialogView.positiveButton: didTapPositiveButtonHandler?() case dialogView.negativeButton: didTapNegativeButtonHandler?() case dialogView.neutralButton: didTapNeutralButtonHandler?() default: break } dismiss(isTriggeredByUserInteraction: true, isAnimated: true, using: sender) } } private extension DialogController { /// Prepares view. func prepareView() { let v = UIControl() v.backgroundColor = Color.black.withAlphaComponent(0.33) v.addTarget(self, action: #selector(didTapBackgroundView), for: .touchUpInside) view = v } /// Prepares dialogView. func prepareDialogView() { view.layout(dialogView).center() dialogView.buttonArea.subviews.forEach { ($0 as? Button)?.addTarget(self, action: #selector(didTapButton), for: .touchUpInside) } } } private extension DialogController { /** Dismisses dialog. - Parameter isTriggeredByUserInteraction: A boolean indicating whether the action is triggered by a user interaction - Parameter isAnimated: A boolean indicating if the dismissal should be animated. - Parameter using button: A button triggering the dismissal. */ func dismiss(isTriggeredByUserInteraction: Bool, isAnimated: Bool, using button: Button? = nil) { if isTriggeredByUserInteraction { guard shouldDismissHandler?(dialogView, button) ?? true else { return } } presentingViewController?.dismiss(animated: isAnimated, completion: nil) guard isTriggeredByUserInteraction, nil == button else { return } didCancelHandler?() } } ================================================ FILE: Sources/iOS/Dialogs/DialogView.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit private struct Constants { struct titleArea { static let insets = UIEdgeInsets(top: 24, left: 24, bottom: 20, right: 24) } struct contentArea { static let insets = UIEdgeInsets(top: 0, left: 24, bottom: 24, right: 24) static let insetsNoTitle = UIEdgeInsets(top: 20, left: 24, bottom: 24, right: 24) } struct buttonArea { static let insets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) static let insetsStacked = UIEdgeInsets(top: 6, left: 8, bottom: 14, right: 8) } struct button { static let insets = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8) static let minWidth: CGFloat = 64 static let height: CGFloat = 36 static let interimStacked: CGFloat = 12 static let interim: CGFloat = 8 } } private class DialogScrollView: UIScrollView { /// A weak reference to DialogView. weak var dialogView: DialogView? override func layoutSubviews() { super.layoutSubviews() dialogView?.layoutDividers() } } open class DialogView: View, Themeable { /// A container view for title area. public let titleArea = UIView() /// A container view for button area. public let buttonArea = UIView() /// A container view for content area. public let contentArea = UIView() /// A scroll view containing contentArea. public let scrollView: UIScrollView = DialogScrollView() /// A UILabel. public let titleLabel = UILabel() /// A UILabel. public let detailsLabel = UILabel() /// A Button. public let neutralButton = FlatButton() /// A Button. public let positiveButton = FlatButton() /// A Button. public let negativeButton = FlatButton() /// Maximum size of the dialog. open var maxSize = CGSize(width: 200, height: 300) { didSet { invalidateIntrinsicContentSize() } } open override func prepare() { super.prepare() depthPreset = .depth5 cornerRadiusPreset = .cornerRadius2 prepareTitleArea() prepareTitleLabel() prepareScrollView() prepareContentArea() prepareDetailsLabel() prepareButtonArea() prepareButtons() applyCurrentTheme() } open override var intrinsicContentSize: CGSize { return sizeThatFits(maxSize) } open override func sizeThatFits(_ size: CGSize) -> CGSize { var w: CGFloat = 0 func setMaxWidth(_ width: CGFloat) { w = max(w, width) w = min(w, size.width) } setMaxWidth(titleAreaSizeThatFits(width: size.width).width) setMaxWidth(buttonAreaSizeThatFits(width: size.width).width) setMaxWidth(contentAreaSizeThatFits(width: size.width).width) var h: CGFloat = 0 h += titleAreaSizeThatFits(width: w).height h += buttonAreaSizeThatFits(width: w).height h += contentAreaSizeThatFits(width: w).height h = min(h, size.height) return CGSize(width: w, height: h) } open override func layoutSubviews() { super.layoutSubviews() layoutTitleArea() layoutButtonArea() layoutContentArea() layoutScrollView() layoutDividers() /// Position button area after having correct sizes. buttonArea.frame.origin.y = scrollView.frame.maxY } /** Calculates the size for title area that best fits the specified width. - Parameter width: A CGFloat. - Returns: Calculated CGSize. */ open func titleAreaSizeThatFits(width: CGFloat) -> CGSize { guard !titleLabel.isEmpty else { return .zero } let insets = Constants.titleArea.insets var size = titleLabel.sizeThatFits(CGSize(width: width - insets.left - insets.right, height: .greatestFiniteMagnitude)) size.width += insets.left + insets.right size.height += insets.top + insets.bottom return size } /** Calculates the size for button area that best fits the specified width. - Parameter width: A CGFloat. - Returns: Calculated CGSize. */ open func buttonAreaSizeThatFits(width: CGFloat) -> CGSize { guard !nonHiddenButtons.isEmpty else { return .zero } let isStacked = requiredButtonAreaWidth > width let buttonsHeight = Constants.button.height * CGFloat(isStacked ? nonHiddenButtons.count : 1) let buttonsInterim = isStacked ? CGFloat(nonHiddenButtons.count - 1) * Constants.button.interimStacked : 0 let insets = isStacked ? Constants.buttonArea.insetsStacked : Constants.buttonArea.insets let h = buttonsInterim + buttonsHeight + insets.bottom + insets.top let w = min(width, isStacked ? requiredButtonAreaWidthForStacked : requiredButtonAreaWidth) return CGSize(width: w, height: h) } /** Calculates the size for content area that best fits the specified width. - Parameter width: A CGFloat. - Returns: Calculated CGSize. */ open func contentAreaSizeThatFits(width: CGFloat) -> CGSize { guard !detailsLabel.isEmpty else { return .zero } let insets = titleLabel.isEmpty ? Constants.contentArea.insetsNoTitle : Constants.contentArea.insets var size = detailsLabel.sizeThatFits(CGSize(width: width - insets.left - insets.right, height: .greatestFiniteMagnitude)) size.width += insets.left + insets.right size.height += insets.top + insets.bottom return size } /** Applies the given theme. - Parameter theme: A Theme. */ open func apply(theme: Theme) { backgroundColor = theme.surface titleLabel.textColor = theme.onSurface.withAlphaComponent(0.87) detailsLabel.textColor = theme.onSurface.withAlphaComponent(0.60) titleArea.dividerColor = theme.onSurface.withAlphaComponent(0.12) buttonArea.dividerColor = theme.onSurface.withAlphaComponent(0.12) } } private extension DialogView { /// Prepares titleArea. func prepareTitleArea() { addSubview(titleArea) titleArea.dividerColor = Color.darkText.dividers titleArea.dividerThickness = 1 titleArea.dividerAlignment = .bottom } /// Prepares titleTitle. func prepareTitleLabel() { titleArea.addSubview(titleLabel) titleLabel.font = Theme.font.bold(with: 20) titleLabel.textColor = Color.darkText.primary titleLabel.numberOfLines = 0 } /// Prepares buttonArea. func prepareButtonArea() { addSubview(buttonArea) buttonArea.dividerColor = Color.darkText.dividers buttonArea.dividerThickness = 1 buttonArea.dividerAlignment = .top } /// Prepares buttons. func prepareButtons() { [positiveButton, negativeButton, neutralButton].forEach { buttonArea.addSubview($0) $0.titleLabel?.font = Theme.font.medium(with: 14) $0.contentEdgeInsets = Constants.button.insets $0.cornerRadiusPreset = .cornerRadius1 } } /// Prepares scrollView. func prepareScrollView() { (scrollView as! DialogScrollView).dialogView = self addSubview(scrollView) } /// Prepares contentArea. func prepareContentArea() { scrollView.addSubview(contentArea) } /// Prepares detailsLabel. func prepareDetailsLabel() { contentArea.addSubview(detailsLabel) detailsLabel.font = Theme.font.regular(with: detailsLabel.fontSize) detailsLabel.numberOfLines = 0 detailsLabel.textColor = Color.darkText.secondary } } private extension DialogView { /// Layout the titleArea. func layoutTitleArea() { let size = CGSize(width: frame.width, height: titleAreaSizeThatFits(width: frame.width).height) titleArea.frame.size = size guard !titleLabel.isEmpty else { return } let rect = CGRect(origin: .zero, size: size) titleLabel.frame = rect.inset(by: Constants.titleArea.insets) } /// Layout the buttonArea. func layoutButtonArea() { let width = frame.width buttonArea.frame.size.width = width buttonArea.frame.size.height = buttonAreaSizeThatFits(width: width).height let buttons = nonHiddenButtons guard !buttons.isEmpty else { return } let isStacked = requiredButtonAreaWidth > width if isStacked { let insets = Constants.buttonArea.insetsStacked buttons.forEach { let w = min($0.optimalWidth, width - insets.left - insets.right) $0.frame.size = CGSize(width: w, height: Constants.button.height) $0.frame.origin.x = width - insets.right - w } positiveButton.frame.origin.y = insets.top let belowPositive = positiveButton.isHidden ? insets.top : positiveButton.frame.maxY + Constants.button.interimStacked negativeButton.frame.origin.y = belowPositive neutralButton.frame.origin.y = negativeButton.isHidden ? belowPositive : negativeButton.frame.maxY + Constants.button.interimStacked } else { let insets = Constants.buttonArea.insets buttons.forEach { $0.frame.size = CGSize(width: $0.optimalWidth, height: Constants.button.height) $0.frame.origin.y = insets.top } neutralButton.frame.origin.x = insets.left positiveButton.frame.origin.x = width - insets.right - positiveButton.frame.width let maxX = positiveButton.isHidden ? width - insets.right : positiveButton.frame.minX - Constants.button.interim negativeButton.frame.origin.x = maxX - negativeButton.frame.width } } /// Layout the contentArea. func layoutContentArea() { let size = CGSize(width: frame.width, height: contentAreaSizeThatFits(width: frame.width).height) contentArea.frame.size = size guard !detailsLabel.isEmpty else { return } let rect = CGRect(origin: .zero, size: size) let insets = titleArea.frame.height == 0 ? Constants.contentArea.insetsNoTitle : Constants.contentArea.insets detailsLabel.frame = rect.inset(by: insets) } /// Layout the scrollView. func layoutScrollView() { let h = titleArea.frame.height + buttonArea.frame.height let allowed = min(frame.height - h, contentArea.frame.height) scrollView.frame.size = CGSize(width: frame.width, height: max(allowed, 0)) scrollView.frame.origin.y = titleArea.frame.maxY scrollView.contentSize = contentArea.frame.size } /** Layout the dividers. This method is also called (by scrollView) when scrolling happens */ func layoutDividers() { let isScrollable = contentArea.frame.height > scrollView.frame.height titleArea.isDividerHidden = titleArea.frame.height == 0 || !isScrollable || scrollView.isAtTop buttonArea.isDividerHidden = buttonArea.frame.height == 0 || !isScrollable || scrollView.isAtBottom titleArea.layoutDivider() buttonArea.layoutDivider() } } private extension DialogView { /// Required width to fit content of buttonArea. var requiredButtonAreaWidth: CGFloat { let buttons = nonHiddenButtons guard !buttons.isEmpty else { return 0 } let buttonsWidth: CGFloat = buttons.reduce(0) { $0 + $1.optimalWidth } let additional: CGFloat = neutralButton.isHidden ? 0 : 8 // additional spacing for neutral button let insets = Constants.buttonArea.insets return buttonsWidth + insets.left + insets.right + CGFloat((buttons.count - 1)) * Constants.button.interim + additional } /// Required width to fit statcked content of buttonArea. var requiredButtonAreaWidthForStacked: CGFloat { let insets = Constants.buttonArea.insetsStacked return insets.left + insets.right + nonHiddenButtons.reduce(0) { max($0, $1.optimalWidth) } } /// Non-hidden buttons within buttonArea. var nonHiddenButtons: [Button] { positiveButton.isHidden = positiveButton.title(for: .normal)?.isEmpty ?? true negativeButton.isHidden = negativeButton.title(for: .normal)?.isEmpty ?? true neutralButton.isHidden = neutralButton.title(for: .normal)?.isEmpty ?? true return [positiveButton, negativeButton, neutralButton].filter { !$0.isHidden } } } private extension UIScrollView { /// Checks if scroll view is at the top. var isAtTop: Bool { return contentOffset.y <= 0 } /// Checks if scroll view is at the bottom. var isAtBottom: Bool { /// -1 is used to get rid of precision errors /// make divider appear even when scroll is at the bottom. return contentOffset.y >= (contentSize.height - frame.height - 1) } } private extension Button { /// Optimal width for dialog button. var optimalWidth: CGFloat { let size = CGSize(width: .greatestFiniteMagnitude, height: Constants.button.height) return max(Constants.button.minWidth, sizeThatFits(size).width) } } private extension UILabel { /// Checks if label is empty. var isEmpty: Bool { let empty = text?.isEmpty ?? true isHidden = empty return empty } } ================================================ FILE: Sources/iOS/Divider/Divider.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion @objc(DividerAlignment) public enum DividerAlignment: Int { case top case left case bottom case right } public struct Divider { /// A reference to the UIView. internal weak var view: UIView? /// A reference to the divider UIView. internal var line: UIView? /// A reference to the height. public var thickness: CGFloat { didSet { reload() } } /// A preset wrapper around contentEdgeInsets. public var contentEdgeInsetsPreset = EdgeInsetsPreset.none { didSet { contentEdgeInsets = EdgeInsetsPresetToValue(preset: contentEdgeInsetsPreset) } } /// A reference to EdgeInsets. public var contentEdgeInsets = EdgeInsets.zero { didSet { reload() } } /// A UIColor. public var color: UIColor? { get { return line?.backgroundColor } set(value) { guard let v = value else { line?.removeFromSuperview() line = nil return } if nil == line { line = UIView() line?.layer.zPosition = 5000 view?.addSubview(line!) reload() } line?.backgroundColor = v } } /// A reference to the dividerAlignment. public var alignment = DividerAlignment.bottom { didSet { reload() } } /** Initializer that takes in a UIView. - Parameter view: A UIView reference. - Parameter thickness: A CGFloat value. */ internal init(view: UIView?, thickness: CGFloat = 1) { self.view = view self.thickness = thickness } /** Hides the divier line. */ internal var isHidden = false { didSet { line?.isHidden = isHidden } } /// Lays out the divider. public func reload() { guard let l = line, let v = view else { return } let c = contentEdgeInsets switch alignment { case .top: l.frame = CGRect(x: c.left, y: c.top, width: v.bounds.width - c.left - c.right, height: thickness) case .bottom: l.frame = CGRect(x: c.left, y: v.bounds.height - thickness - c.bottom, width: v.bounds.width - c.left - c.right, height: thickness) case .left: l.frame = CGRect(x: c.left, y: c.top, width: thickness, height: v.bounds.height - c.top - c.bottom) case .right: l.frame = CGRect(x: v.bounds.width - thickness - c.right, y: c.top, width: thickness, height: v.bounds.height - c.top - c.bottom) } } } /// A memory reference to the Divider instance. fileprivate var DividerKey: UInt8 = 0 extension UIView { /// TabBarItem reference. public private(set) var divider: Divider { get { return AssociatedObject.get(base: self, key: &DividerKey) { return Divider(view: self) } } set(value) { AssociatedObject.set(base: self, key: &DividerKey, value: value) } } /// A preset wrapper around divider.contentEdgeInsets. open var dividerContentEdgeInsetsPreset: EdgeInsetsPreset { get { return divider.contentEdgeInsetsPreset } set(value) { divider.contentEdgeInsetsPreset = value } } /// A reference to divider.contentEdgeInsets. open var dividerContentEdgeInsets: EdgeInsets { get { return divider.contentEdgeInsets } set(value) { divider.contentEdgeInsets = value } } /// Divider color. @IBInspectable open var dividerColor: UIColor? { get { return divider.color } set(value) { divider.color = value } } /// Divider visibility. @IBInspectable open var isDividerHidden: Bool { get { return divider.isHidden } set(value) { divider.isHidden = value } } /// Divider animation. open var dividerAlignment: DividerAlignment { get { return divider.alignment } set(value) { divider.alignment = value } } /// Divider thickness. @IBInspectable open var dividerThickness: CGFloat { get { return divider.thickness } set(value) { divider.thickness = value } } /// Sets the divider frame. open func layoutDivider() { divider.reload() } } ================================================ FILE: Sources/iOS/Extension/Material+Array.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ extension Array where Element: Equatable { /** Slices a out a segment of an array based on the start and end positions. - Parameter start: A start index. - Parameter end: An end index. - Returns: A segmented array based on the start and end indices. */ public func slice(start: Int, end: Int?) -> [Element] { var e = end ?? count - 1 if e >= count { e = count - 1 } guard -1 < start else { fatalError("Range out of bounds for \(start) - \(end ?? 0), should be 0 - \(count).") } var diff = abs(e - start) guard count > diff else { return self } var ret = [Element]() while -1 < diff { ret.insert(self[start + diff], at: 0) diff -= 1 } return ret } } ================================================ FILE: Sources/iOS/Extension/Material+CALayer.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion fileprivate class MaterialLayer { /// A reference to the CALayer. fileprivate weak var layer: CALayer? /// A property that sets the height of the layer's frame. fileprivate var heightPreset = HeightPreset.default { didSet { layer?.height = heightPreset.rawValue } } /// A property that sets the cornerRadius of the backing layer. fileprivate var cornerRadiusPreset = CornerRadiusPreset.none { didSet { layer?.cornerRadius = CornerRadiusPresetToValue(preset: cornerRadiusPreset) } } /// A preset property to set the borderWidth. fileprivate var borderWidthPreset = BorderWidthPreset.none { didSet { layer?.borderWidth = borderWidthPreset.cgFloatValue } } /// A preset property to set the shape. fileprivate var shapePreset = ShapePreset.none { didSet { layer?.layoutShape() } } /// A preset value for Depth. fileprivate var depthPreset: DepthPreset { get { return depth.preset } set(value) { depth.preset = value } } /// Grid reference. fileprivate var depth = Depth.zero { didSet { guard let v = layer else { return } v.shadowOffset = depth.offset.asSize v.shadowOpacity = depth.opacity v.shadowRadius = depth.radius v.layoutShadowPath() } } /// Enables automatic shadowPath sizing. fileprivate var isShadowPathAutoSizing = false /** Initializer that takes in a CALayer. - Parameter view: A CALayer reference. */ fileprivate init(layer: CALayer?) { self.layer = layer } } fileprivate var MaterialLayerKey: UInt8 = 0 extension CALayer { /// MaterialLayer Reference. fileprivate var materialLayer: MaterialLayer { get { return AssociatedObject.get(base: self, key: &MaterialLayerKey) { return MaterialLayer(layer: self) } } set(value) { AssociatedObject.set(base: self, key: &MaterialLayerKey, value: value) } } /// A property that accesses the frame.origin.x property. @IBInspectable open var x: CGFloat { get { return frame.origin.x } set(value) { frame.origin.x = value layoutShadowPath() } } /// A property that accesses the frame.origin.y property. @IBInspectable open var y: CGFloat { get { return frame.origin.y } set(value) { frame.origin.y = value layoutShadowPath() } } /// A property that accesses the frame.size.width property. @IBInspectable open var width: CGFloat { get { return frame.size.width } set(value) { frame.size.width = value if .none != shapePreset { frame.size.height = value layoutShape() } layoutShadowPath() } } /// A property that accesses the frame.size.height property. @IBInspectable open var height: CGFloat { get { return frame.size.height } set(value) { frame.size.height = value if .none != shapePreset { frame.size.width = value layoutShape() } layoutShadowPath() } } /// HeightPreset value. open var heightPreset: HeightPreset { get { return materialLayer.heightPreset } set(value) { materialLayer.heightPreset = value } } /** A property that manages the overall shape for the object. If either the width or height property is set, the other will be automatically adjusted to maintain the shape of the object. */ open var shapePreset: ShapePreset { get { return materialLayer.shapePreset } set(value) { materialLayer.shapePreset = value } } /// A preset value for Depth. open var depthPreset: DepthPreset { get { return depth.preset } set(value) { depth.preset = value } } /// Grid reference. open var depth: Depth { get { return materialLayer.depth } set(value) { materialLayer.depth = value } } /// Enables automatic shadowPath sizing. @IBInspectable open var isShadowPathAutoSizing: Bool { get { return materialLayer.isShadowPathAutoSizing } set(value) { materialLayer.isShadowPathAutoSizing = value } } /// A property that sets the cornerRadius of the backing layer. open var cornerRadiusPreset: CornerRadiusPreset { get { return materialLayer.cornerRadiusPreset } set(value) { materialLayer.cornerRadiusPreset = value } } /// A preset property to set the borderWidth. open var borderWidthPreset: BorderWidthPreset { get { return materialLayer.borderWidthPreset } set(value) { materialLayer.borderWidthPreset = value } } } extension CALayer { /// Manages the layout for the shape of the view instance. open func layoutShape() { guard .none != shapePreset else { return } if 0 == bounds.width { bounds.size.width = bounds.height } if 0 == bounds.height { bounds.size.height = bounds.width } guard .circle == shapePreset else { cornerRadius = 0 return } cornerRadius = bounds.width / 2 } /// Sets the shadow path. open func layoutShadowPath() { guard isShadowPathAutoSizing else { return } if case .none = depthPreset.rawValue { shadowPath = nil } else if nil == shadowPath { shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).cgPath } else { animate(.shadow(path: UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).cgPath)) } } } ================================================ FILE: Sources/iOS/Extension/Material+MotionAnimation.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion public extension MotionAnimation { /** Animates the view's current shadow offset to the given one. - Parameter offset: A CGSize. - Returns: A MotionAnimation. */ static func shadow(offset: Offset) -> MotionAnimation { return .shadow(offset: offset.asSize) } /** Animates the views shadow offset, opacity, and radius using a DepthPreset. - Parameter _ preset: A DepthPreset. */ static func depth(_ preset: DepthPreset) -> MotionAnimation { return .depth(DepthPresetToValue(preset: preset).rawValue) } /** Animates the views shadow offset, opacity, and radius using a given Depth. - Parameter _ preset: A Depth. */ static func depth(_ depth: Depth) -> MotionAnimation { return .depth(depth.rawValue) } } ================================================ FILE: Sources/iOS/Extension/Material+NSMutableAttributedString.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit extension NSMutableAttributedString { /** Updates a NSAttributedStringKey for a given range. - Parameter _ name: A NSAttributedStringKey. - Parameter value: Any type. - Parameter range: A NSRange. */ open func updateAttribute(_ name: NSAttributedString.Key, value: Any, range: NSRange) { removeAttribute(name, range: range) addAttribute(name, value: value, range: range) } /** Updates a Dictionary of NSAttributedStringKeys for a given range. - Parameter _ attrs: A Dictionary of NSAttributedStringKey type keys and Any type values. - Parameter range: A NSRange. */ open func updateAttributes(_ attrs: [NSAttributedString.Key: Any], range: NSRange) { for (k, v) in attrs { updateAttribute(k, value: v, range: range) } } /** Removes a Dictionary of NSAttributedStringKeys for a given range. - Parameter _ attrs: An Array of attributedStringKeys. - Parameter range: A NSRange. */ open func removeAttributes(_ attrs: [NSAttributedString.Key], range: NSRange) { for k in attrs { removeAttribute(k, range: range) } } } ================================================ FILE: Sources/iOS/Extension/Material+String.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit extension String { /** :name: trim */ public var trimmed: String { return trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) } /** :name: lines */ public var lines: [String] { return components(separatedBy: CharacterSet.newlines) } /** :name: firstLine */ public var firstLine: String? { return lines.first?.trimmed } /** :name: lastLine */ public var lastLine: String? { return lines.last?.trimmed } /** :name: replaceNewLineCharater */ public func replaceNewLineCharater(separator: String = " ") -> String { return components(separatedBy: CharacterSet.whitespaces).joined(separator: separator).trimmed } /** :name: replacePunctuationCharacters */ public func replacePunctuationCharacters(separator: String = "") -> String { return components(separatedBy: CharacterSet.punctuationCharacters).joined(separator: separator).trimmed } } ================================================ FILE: Sources/iOS/Extension/Material+UIButton.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit public extension UIButton { /// Convenience way to change titleLabel font size. var fontSize: CGFloat { get { return titleLabel?.font?.pointSize ?? UIFont.buttonFontSize } set(value) { titleLabel?.font = titleLabel?.font?.withSize(value) ?? UIFont.systemFont(ofSize: value) } } } ================================================ FILE: Sources/iOS/Extension/Material+UIColor.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit public extension UIColor { /** A convenience initializer that creates color from argb(alpha red green blue) hexadecimal representation. - Parameter argb: An unsigned 32 bit integer. E.g 0xFFAA44CC. */ convenience init(argb: UInt32) { let a = argb >> 24 let r = argb >> 16 let g = argb >> 8 let b = argb >> 0 func f(_ v: UInt32) -> CGFloat { return CGFloat(v & 0xff) / 255 } self.init(red: f(r), green: f(g), blue: f(b), alpha: f(a)) } /** A convenience initializer that creates color from rgb(red green blue) hexadecimal representation with alpha value 1. - Parameter rgb: An unsigned 32 bit integer. E.g 0xAA44CC. */ convenience init(rgb: UInt32) { self.init(argb: (0xff000000 as UInt32) | rgb) } } internal extension UIColor { /// A tuple of the rgba components. var components: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) { var r: CGFloat = 0 var g: CGFloat = 0 var b: CGFloat = 0 var a: CGFloat = 0 getRed(&r, green: &g, blue: &b, alpha: &a) return (r, g, b, a) } /** Blends given coverColor over this color. - Parameter with coverColor: A UIColor. - Returns: Resultant color of blending. */ func blend(with coverColor: UIColor) -> UIColor { /// Blends channels according to https://en.wikipedia.org/wiki/Alpha_compositing (see `over` operator). func blendChannel(value: CGFloat, bValue: CGFloat, alpha: CGFloat, bAlpha: CGFloat) -> CGFloat { return ((1 - alpha) * bValue * bAlpha + alpha * value) / (alpha + bAlpha * (1 - alpha)) } let (r, g, b, a) = coverColor.components let (bR, bG, bB, bA) = components let newR = blendChannel(value: r, bValue: bR, alpha: a, bAlpha: bA) let newG = blendChannel(value: g, bValue: bG, alpha: a, bAlpha: bA) let newB = blendChannel(value: b, bValue: bB, alpha: a, bAlpha: bA) let newA = a + bA * (1 - a) return UIColor(red: newR, green: newG, blue: newB, alpha: newA) } /** Adjusts brightness of the color by given value. - Parameter by value: A CGFloat value. - Returns: Adjusted color. */ func adjustingBrightness(by value: CGFloat) -> UIColor { var h: CGFloat = 0 var s: CGFloat = 0 var b: CGFloat = 0 var a: CGFloat = 0 getHue(&h, saturation: &s, brightness: &b, alpha: &a) return UIColor(hue: h, saturation: s, brightness: (b + value).clamp(0, 1), alpha: 1) } /// A lighter version of the color. var lighter: UIColor { return adjustingBrightness(by: 0.1) } /// A darker version of the color. var darker: UIColor { return adjustingBrightness(by: -0.1) } } ================================================ FILE: Sources/iOS/Extension/Material+UIFont.swift ================================================ /* * Copyright (C) 2015 - 2019 and CosmicMind, Inc. . * All rights reserved. * * Authors: * Daniel Dahan , * Orkhan Alikhanov * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * * Neither the name of CosmicMind nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import UIKit extension UIFont { /** Calculates a CGSize value based on a width and length of a string with a given UIFont. - Parameter string: A String. - Parameter constrainedTo width: A CGFloat. - Returns a CGSize. */ open func stringSize(string: String, constrainedTo width: CGFloat) -> CGSize { return string.boundingRect(with: CGSize(width: width, height: CGFloat(Double.greatestFiniteMagnitude)), options: .usesLineFragmentOrigin, attributes: [.font: self], context: nil).size } } ================================================ FILE: Sources/iOS/Extension/Material+UIImage.swift ================================================ /* * Copyright (C) 2015 - 2019 and CosmicMind, Inc. . * All rights reserved. * * Authors: * Daniel Dahan , * Orkhan Alikhanov * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * * Neither the name of CosmicMind nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import UIKit import Accelerate import Motion @objc(ImageFormat) public enum ImageFormat: Int { case png case jpeg } extension UIImage { /// Width of the UIImage. open var width: CGFloat { return size.width } /// Height of the UIImage. open var height: CGFloat { return size.height } } extension UIImage { /** Resizes an image based on a given width. - Parameter toWidth w: A width value. - Returns: An optional UIImage. */ open func resize(toWidth w: CGFloat) -> UIImage? { return internalResize(toWidth: w) } /** Resizes an image based on a given height. - Parameter toHeight h: A height value. - Returns: An optional UIImage. */ open func resize(toHeight h: CGFloat) -> UIImage? { return internalResize(toHeight: h) } /** Internally resizes the image. - Parameter toWidth tw: A width. - Parameter toHeight th: A height. - Returns: An optional UIImage. */ private func internalResize(toWidth tw: CGFloat = 0, toHeight th: CGFloat = 0) -> UIImage? { var w: CGFloat? var h: CGFloat? if 0 < tw { h = height * tw / width } else if 0 < th { w = width * th / height } let g: UIImage? let t: CGRect = CGRect(x: 0, y: 0, width: w ?? tw, height: h ?? th) UIGraphicsBeginImageContextWithOptions(t.size, false, Screen.scale) draw(in: t, blendMode: .normal, alpha: 1) g = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return g } } extension UIImage { /** Creates a new image with the passed in color. - Parameter color: The UIColor to create the image from. - Returns: A UIImage that is the color passed in. */ open func tint(with color: UIColor) -> UIImage? { UIGraphicsBeginImageContextWithOptions(size, false, Screen.scale) guard let context = UIGraphicsGetCurrentContext() else { return nil } context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: 0.0, y: -size.height) context.setBlendMode(.multiply) let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) context.clip(to: rect, mask: cgImage!) color.setFill() context.fill(rect) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image?.withRenderingMode(.alwaysOriginal) } } extension UIImage { /** Creates an Image that is a color. - Parameter color: The UIColor to create the image from. - Parameter size: The size of the image to create. - Returns: A UIImage that is the color passed in. */ open class func image(with color: UIColor, size: CGSize) -> UIImage? { UIGraphicsBeginImageContextWithOptions(size, false, Screen.scale) guard let context = UIGraphicsGetCurrentContext() else { return nil } context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: 0.0, y: -size.height) context.setBlendMode(.multiply) let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) color.setFill() context.fill(rect) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image?.withRenderingMode(.alwaysOriginal) } } extension UIImage { /** Crops an image to a specified width and height. - Parameter toWidth tw: A specified width. - Parameter toHeight th: A specified height. - Returns: An optional UIImage. */ open func crop(toWidth tw: CGFloat, toHeight th: CGFloat) -> UIImage? { let g: UIImage? let b = width > height let s: CGFloat = b ? th / height : tw / width let t: CGSize = CGSize(width: tw, height: th) let w = width * s let h = height * s UIGraphicsBeginImageContext(t) draw(in: b ? CGRect(x: -1 * (w - t.width) / 2, y: 0, width: w, height: h) : CGRect(x: 0, y: -1 * (h - t.height) / 2, width: w, height: h), blendMode: .normal, alpha: 1) g = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return g } } extension UIImage { /** Creates a clear image. - Returns: A UIImage that is clear. */ open class func clear(size: CGSize = CGSize(width: 16, height: 16)) -> UIImage? { UIGraphicsBeginImageContextWithOptions(size, false, 0) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image } } extension UIImage { /** Asynchronously load images with a completion block. - Parameter URL: A URL destination to fetch the image from. - Parameter completion: A completion block that is executed once the image has been retrieved. */ open class func contentsOfURL(url: URL, completion: @escaping ((UIImage?, Error?) -> Void)) { URLSession.shared.dataTask(with: URLRequest(url: url)) { [completion = completion] (data: Data?, response: URLResponse?, error: Error?) in Motion.async { if let v = error { completion(nil, v) } else if let v = data { completion(UIImage(data: v), nil) } } }.resume() } } extension UIImage { /** Adjusts the orientation of the image from the capture orientation. This is an issue when taking images, the capture orientation is not set correctly when using Portrait. - Returns: An optional UIImage if successful. */ open func adjustOrientation() -> UIImage? { guard .up != imageOrientation else { return self } var transform: CGAffineTransform = .identity // Rotate if Left, Right, or Down. switch imageOrientation { case .down, .downMirrored: transform = transform.translatedBy(x: size.width, y: size.height) transform = transform.rotated(by: CGFloat(Double.pi)) case .left, .leftMirrored: transform = transform.translatedBy(x: size.width, y: 0) transform = transform.rotated(by: CGFloat(Double.pi / 2)) case .right, .rightMirrored: transform = transform.translatedBy(x: 0, y: size.height) transform = transform.rotated(by: -CGFloat(Double.pi / 2)) default:break } // Flip if mirrored. switch imageOrientation { case .upMirrored, .downMirrored: transform = transform.translatedBy(x: size.width, y: 0) transform = transform.scaledBy(x: -1, y: 1) case .leftMirrored, .rightMirrored: transform = transform.translatedBy(x: size.height, y: 0) transform = transform.scaledBy(x: -1, y: 1) default:break } // Draw the underlying cgImage with the calculated transform. guard let context = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: cgImage!.bitsPerComponent, bytesPerRow: 0, space: cgImage!.colorSpace!, bitmapInfo: cgImage!.bitmapInfo.rawValue) else { return nil } context.concatenate(transform) switch imageOrientation { case .left, .leftMirrored, .right, .rightMirrored: context.draw(cgImage!, in: CGRect(x: 0, y: 0, width: size.height, height: size.width)) default: context.draw(cgImage!, in: CGRect(origin: .zero, size: size)) } guard let cgImage = context.makeImage() else { return nil } return UIImage(cgImage: cgImage) } } /** Creates an effect buffer for images that already have effects. - Parameter context: A CGContext. - Returns: vImage_Buffer. */ fileprivate func createEffectBuffer(context: CGContext) -> vImage_Buffer { let data = context.data let width = vImagePixelCount(context.width) let height = vImagePixelCount(context.height) let rowBytes = context.bytesPerRow return vImage_Buffer(data: data, height: height, width: width, rowBytes: rowBytes) } extension UIImage { /** Applies a blur effect to a UIImage. - Parameter radius: The radius of the blur effect. - Parameter tintColor: The color used for the blur effect (optional). - Parameter saturationDeltaFactor: The delta factor for the saturation of the blur effect. - Returns: a UIImage. */ open func blur(radius: CGFloat = 0, tintColor: UIColor? = nil, saturationDeltaFactor: CGFloat = 0) -> UIImage? { var effectImage = self let screenScale = Screen.scale let imageRect = CGRect(origin: .zero, size: size) let hasBlur = radius > CGFloat(Float.ulpOfOne) let hasSaturationChange = abs(saturationDeltaFactor - 1.0) > CGFloat(Float.ulpOfOne) if hasBlur || hasSaturationChange { UIGraphicsBeginImageContextWithOptions(size, false, screenScale) let inContext = UIGraphicsGetCurrentContext()! inContext.scaleBy(x: 1.0, y: -1.0) inContext.translateBy(x: 0, y: -size.height) inContext.draw(cgImage!, in: imageRect) var inBuffer = createEffectBuffer(context: inContext) UIGraphicsBeginImageContextWithOptions(size, false, screenScale) let outContext = UIGraphicsGetCurrentContext()! var outBuffer = createEffectBuffer(context: outContext) if hasBlur { let a = sqrt(2 * .pi) let b = CGFloat(a) / 4 let c = radius * screenScale let d = c * 3.0 * b var e = UInt32(floor(d + 0.5)) if 1 != e % 2 { e += 1 // force radius to be odd so that the three box-blur methodology works. } let imageEdgeExtendFlags = vImage_Flags(kvImageEdgeExtend) vImageBoxConvolve_ARGB8888(&inBuffer, &outBuffer, nil, 0, 0, e, e, nil, imageEdgeExtendFlags) vImageBoxConvolve_ARGB8888(&outBuffer, &inBuffer, nil, 0, 0, e, e, nil, imageEdgeExtendFlags) vImageBoxConvolve_ARGB8888(&inBuffer, &outBuffer, nil, 0, 0, e, e, nil, imageEdgeExtendFlags) } var effectImageBuffersAreSwapped = false if hasSaturationChange { let s = saturationDeltaFactor let floatingPointSaturationMatrix: [CGFloat] = [ 0.0722 + 0.9278 * s, 0.0722 - 0.0722 * s, 0.0722 - 0.0722 * s, 0, 0.7152 - 0.7152 * s, 0.7152 + 0.2848 * s, 0.7152 - 0.7152 * s, 0, 0.2126 - 0.2126 * s, 0.2126 - 0.2126 * s, 0.2126 + 0.7873 * s, 0, 0, 0, 0, 1 ] let divisor: CGFloat = 256 let matrixSize = floatingPointSaturationMatrix.count var saturationMatrix = [Int16](repeating: 0, count: matrixSize) for i in 0... * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit public extension UILabel { /// Convenience way to change font size. var fontSize: CGFloat { get { return font?.pointSize ?? UIFont.labelFontSize } set(value) { font = font?.withSize(value) ?? UIFont.systemFont(ofSize: value) } } } ================================================ FILE: Sources/iOS/Extension/Material+UIView.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit extension UIView { /// A property that accesses the backing layer's shadow @objc open var shadowColor: UIColor? { get { guard let v = layer.shadowColor else { return nil } return UIColor(cgColor: v) } set(value) { layer.shadowColor = value?.cgColor } } /// A property that accesses the layer.borderColor property. @objc open var borderColor: UIColor? { get { guard let v = layer.borderColor else { return nil } return UIColor(cgColor: v) } set(value) { layer.borderColor = value?.cgColor } } /// HeightPreset value. open var heightPreset: HeightPreset { get { return layer.heightPreset } set(value) { layer.heightPreset = value } } /** A property that manages the overall shape for the object. If either the width or height property is set, the other will be automatically adjusted to maintain the shape of the object. */ @objc open var shapePreset: ShapePreset { get { return layer.shapePreset } set(value) { layer.shapePreset = value } } /// A preset value for Depth. open var depthPreset: DepthPreset { get { return layer.depthPreset } set(value) { layer.depthPreset = value } } /// Depth reference. open var depth: Depth { get { return layer.depth } set(value) { layer.depth = value } } /// Enables automatic shadowPath sizing. @IBInspectable @objc open var isShadowPathAutoSizing: Bool { get { return layer.isShadowPathAutoSizing } set(value) { layer.isShadowPathAutoSizing = value } } /// A property that sets the cornerRadius of the backing layer. @objc open var cornerRadiusPreset: CornerRadiusPreset { get { return layer.cornerRadiusPreset } set(value) { layer.cornerRadiusPreset = value } } /// A preset property to set the borderWidth. @objc open var borderWidthPreset: BorderWidthPreset { get { return layer.borderWidthPreset } set(value) { layer.borderWidthPreset = value } } } internal extension UIView { /// Manages the layout for the shape of the view instance. func layoutShape() { layer.layoutShape() } /// Sets the shadow path. func layoutShadowPath() { layer.layoutShadowPath() } } ================================================ FILE: Sources/iOS/Extension/Material+UIViewController.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit internal extension UIViewController { /** Finds a view controller with a given type based on the view controller subclass. - Returns: An optional of type T. */ func traverseViewControllerHierarchyForClassType() -> T? { var v: UIViewController? = self while nil != v { if v is T { return v as? T } v = v?.parent } return Application.rootViewController?.traverseTransitionViewControllerHierarchyForClassType() } /** Traverses the child view controllers to find the correct view controller type T. - Returns: An optional of type T. */ func traverseTransitionViewControllerHierarchyForClassType() -> T? { if let v = self as? T { return v } else if let v = self as? TransitionController { return v.rootViewController.traverseTransitionViewControllerHierarchyForClassType() } return nil } } ================================================ FILE: Sources/iOS/Extension/Material+UIWindow.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit extension UIWindow { /** Captures a screenshot of the contents in the apps keyWindow. - Returns: An optional UIImage. */ open func capture() -> UIImage? { UIGraphicsBeginImageContextWithOptions(frame.size, isOpaque, Screen.scale) layer.render(in: UIGraphicsGetCurrentContext()!) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image } } ================================================ FILE: Sources/iOS/FABMenu/FABMenu.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit @objc(FABMenuItemTitleLabelPosition) public enum FABMenuItemTitleLabelPosition: Int { case left case right } @objc(FABMenuDirection) public enum FABMenuDirection: Int { case up case down case left case right } open class FABMenuItem: View { /// A reference to the titleLabel. public let titleLabel = UILabel() /// The titleLabel side. open var titleLabelPosition = FABMenuItemTitleLabelPosition.left /// A reference to the fabButton. public let fabButton = FABButton() open override func prepare() { super.prepare() backgroundColor = nil prepareFABButton() prepareTitleLabel() } /// A reference to the titleLabel text. open var title: String? { get { return titleLabel.text } set(value) { titleLabel.text = value layoutSubviews() } } open override func layoutSubviews() { super.layoutSubviews() guard let t = title, 0 < t.utf16.count else { titleLabel.removeFromSuperview() return } if nil == titleLabel.superview { addSubview(titleLabel) } } } extension FABMenuItem { /// Shows the titleLabel. open func showTitleLabel() { let interimSpace = InterimSpacePresetToValue(preset: .interimSpace6) titleLabel.sizeToFit() titleLabel.frame.size.width += 1.5 * interimSpace titleLabel.frame.size.height += interimSpace / 2 titleLabel.frame.origin.y = (bounds.height - titleLabel.bounds.height) / 2 switch titleLabelPosition { case .left: titleLabel.frame.origin.x = -titleLabel.bounds.width - interimSpace case .right: titleLabel.frame.origin.x = frame.bounds.width + interimSpace } titleLabel.alpha = 0 titleLabel.isHidden = false UIView.animate(withDuration: 0.25, animations: { [weak self] in guard let `self` = self else { return } `self`.titleLabel.alpha = 1 }) } /// Hides the titleLabel. open func hideTitleLabel() { titleLabel.isHidden = true } } extension FABMenuItem { /// Prepares the fabButton. fileprivate func prepareFABButton() { layout(fabButton).edges() } /// Prepares the titleLabel. fileprivate func prepareTitleLabel() { titleLabel.font = Theme.font.regular(with: 14) titleLabel.textAlignment = .center titleLabel.backgroundColor = .white titleLabel.depthPreset = fabButton.depthPreset titleLabel.cornerRadiusPreset = .cornerRadius1 } } @objc(FABMenuDelegate) public protocol FABMenuDelegate { /** A delegation method that is executed to determine whether fabMenu should open. - Parameter fabMenu: A FABMenu. */ @objc optional func fabMenuShouldOpen(fabMenu: FABMenu) -> Bool /** A delegation method that is execited when the fabMenu will open. - Parameter fabMenu: A FABMenu. */ @objc optional func fabMenuWillOpen(fabMenu: FABMenu) /** A delegation method that is execited when the fabMenu did open. - Parameter fabMenu: A FABMenu. */ @objc optional func fabMenuDidOpen(fabMenu: FABMenu) /** A delegation method that is executed to determine whether fabMenu should close. - Parameter fabMenu: A FABMenu. */ @objc optional func fabMenuShouldClose(fabMenu: FABMenu) -> Bool /** A delegation method that is execited when the fabMenu will close. - Parameter fabMenu: A FABMenu. */ @objc optional func fabMenuWillClose(fabMenu: FABMenu) /** A delegation method that is execited when the fabMenu did close. - Parameter fabMenu: A FABMenu. */ @objc optional func fabMenuDidClose(fabMenu: FABMenu) /** A delegation method that is executed when the user taps while the menu is opened. - Parameter fabMenu: A FABMenu. - Parameter tappedAt point: A CGPoint. - Parameter isOutside: A boolean indicating whether the tap was outside the menu button area. */ @objc optional func fabMenu(fabMenu: FABMenu, tappedAt point: CGPoint, isOutside: Bool) } @objc(FABMenu) open class FABMenu: View { /// A flag to avoid the double tap. fileprivate var shouldAvoidHitTest = false /// A reference to the SpringAnimation object. let spring = SpringAnimation() open var fabMenuDirection: FABMenuDirection { get { switch spring.springDirection { case .up: return .up case .down: return .down case .left: return .left case .right: return .right } } set(value) { switch value { case .up: spring.springDirection = .up case .down: spring.springDirection = .down case .left: spring.springDirection = .left case .right: spring.springDirection = .right } layoutSubviews() } } /// A reference to the base FABButton. open var fabButton: FABButton? { didSet { oldValue?.removeFromSuperview() guard let v = fabButton else { return } addSubview(v) v.addTarget(self, action: #selector(handleFABButton(button:)), for: .touchUpInside) } } /// An open handler for the FABButton. open var handleFABButtonCallback: ((UIButton) -> Void)? /// An internal handler for the open function. internal var handleOpenCallback: (() -> Void)? /// An internal handler for the close function. internal var handleCloseCallback: (() -> Void)? /// An internal handler for the completion function. internal var handleCompletionCallback: ((UIView) -> Void)? /// Size of FABMenuItems. open var fabMenuItemSize: CGSize { get { return spring.itemSize } set(value) { spring.itemSize = value } } /// A preset wrapper around interimSpace. open var interimSpacePreset: InterimSpacePreset { get { return spring.interimSpacePreset } set(value) { spring.interimSpacePreset = value } } /// The space between views. open var interimSpace: InterimSpace { get { return spring.interimSpace } set(value) { spring.interimSpace = value } } /// A boolean indicating if the menu is open or not. open var isOpened: Bool { get { return spring.isOpened } set(value) { spring.isOpened = value } } /// A boolean indicating if the menu is enabled. open var isEnabled: Bool { get { return spring.isEnabled } set(value) { spring.isEnabled = value } } /// An optional delegation handler. open weak var delegate: FABMenuDelegate? /// A reference to the FABMenuItems open var fabMenuItems: [FABMenuItem] { get { return spring.views as! [FABMenuItem] } set(value) { for v in spring.views { v.removeFromSuperview() } for v in value { addSubview(v) } spring.views = value } } open override func layoutSubviews() { super.layoutSubviews() fabButton?.frame = bounds fabButton?.setNeedsLayout() fabButton?.layoutIfNeeded() spring.baseSize = bounds.size } open override func prepare() { super.prepare() backgroundColor = nil interimSpacePreset = .interimSpace6 } } extension FABMenu { /** Open the Menu component with animation options. - Parameter duration: The time for each view's animation. - Parameter delay: A delay time for each view's animation. - Parameter usingSpringWithDamping: A damping ratio for the animation. - Parameter initialSpringVelocity: The initial velocity for the animation. - Parameter options: Options to pass to the animation. - Parameter animations: An animation block to execute on each view's animation. - Parameter completion: A completion block to execute on each view's animation. */ open func open(duration: TimeInterval = 0.15, delay: TimeInterval = 0, usingSpringWithDamping: CGFloat = 0.5, initialSpringVelocity: CGFloat = 0, options: UIView.AnimationOptions = [], animations: ((UIView) -> Void)? = nil, completion: ((UIView) -> Void)? = nil) { open(isTriggeredByUserInteraction: false, duration: duration, delay: delay, usingSpringWithDamping: usingSpringWithDamping, initialSpringVelocity: initialSpringVelocity, options: options, animations: animations, completion: completion) } /** Open the Menu component with animation options. - Parameter isTriggeredByUserInteraction: A boolean indicating whether the state was changed by a user interaction, true if yes, false otherwise. - Parameter duration: The time for each view's animation. - Parameter delay: A delay time for each view's animation. - Parameter usingSpringWithDamping: A damping ratio for the animation. - Parameter initialSpringVelocity: The initial velocity for the animation. - Parameter options: Options to pass to the animation. - Parameter animations: An animation block to execute on each view's animation. - Parameter completion: A completion block to execute on each view's animation. */ open func open(isTriggeredByUserInteraction: Bool, duration: TimeInterval = 0.15, delay: TimeInterval = 0, usingSpringWithDamping: CGFloat = 0.5, initialSpringVelocity: CGFloat = 0, options: UIView.AnimationOptions = [], animations: ((UIView) -> Void)? = nil, completion: ((UIView) -> Void)? = nil) { if isTriggeredByUserInteraction && false == delegate?.fabMenuShouldOpen?(fabMenu: self) { return } handleOpenCallback?() if isTriggeredByUserInteraction { delegate?.fabMenuWillOpen?(fabMenu: self) } spring.expand(duration: duration, delay: delay, usingSpringWithDamping: usingSpringWithDamping, initialSpringVelocity: initialSpringVelocity, options: options, animations: animations) { [weak self, isTriggeredByUserInteraction = isTriggeredByUserInteraction, completion = completion] (view) in guard let `self` = self else { return } (view as? FABMenuItem)?.showTitleLabel() if isTriggeredByUserInteraction && view == self.fabMenuItems.last { self.delegate?.fabMenuDidOpen?(fabMenu: self) } completion?(view) self.handleCompletionCallback?(view) } } /** Close the Menu component with animation options. - Parameter duration: The time for each view's animation. - Parameter delay: A delay time for each view's animation. - Parameter usingSpringWithDamping: A damping ratio for the animation. - Parameter initialSpringVelocity: The initial velocity for the animation. - Parameter options: Options to pass to the animation. - Parameter animations: An animation block to execute on each view's animation. - Parameter completion: A completion block to execute on each view's animation. */ open func close(duration: TimeInterval = 0.15, delay: TimeInterval = 0, usingSpringWithDamping: CGFloat = 0.5, initialSpringVelocity: CGFloat = 0, options: UIView.AnimationOptions = [], animations: ((UIView) -> Void)? = nil, completion: ((UIView) -> Void)? = nil) { close(isTriggeredByUserInteraction: false, duration: duration, delay: delay, usingSpringWithDamping: usingSpringWithDamping, initialSpringVelocity: initialSpringVelocity, options: options, animations: animations, completion: completion) } /** Close the Menu component with animation options. - Parameter isTriggeredByUserInteraction: A boolean indicating whether the state was changed by a user interaction, true if yes, false otherwise. - Parameter duration: The time for each view's animation. - Parameter delay: A delay time for each view's animation. - Parameter usingSpringWithDamping: A damping ratio for the animation. - Parameter initialSpringVelocity: The initial velocity for the animation. - Parameter options: Options to pass to the animation. - Parameter animations: An animation block to execute on each view's animation. - Parameter completion: A completion block to execute on each view's animation. */ open func close(isTriggeredByUserInteraction: Bool, duration: TimeInterval = 0.15, delay: TimeInterval = 0, usingSpringWithDamping: CGFloat = 0.5, initialSpringVelocity: CGFloat = 0, options: UIView.AnimationOptions = [], animations: ((UIView) -> Void)? = nil, completion: ((UIView) -> Void)? = nil) { if isTriggeredByUserInteraction && false == delegate?.fabMenuShouldClose?(fabMenu: self) { return } handleCloseCallback?() if isTriggeredByUserInteraction { delegate?.fabMenuWillClose?(fabMenu: self) } spring.contract(duration: duration, delay: delay, usingSpringWithDamping: usingSpringWithDamping, initialSpringVelocity: initialSpringVelocity, options: options, animations: animations) { [weak self, isTriggeredByUserInteraction = isTriggeredByUserInteraction, completion = completion] (view) in guard let `self` = self else { return } (view as? FABMenuItem)?.hideTitleLabel() if isTriggeredByUserInteraction && view == self.fabMenuItems.last { self.delegate?.fabMenuDidClose?(fabMenu: self) } completion?(view) self.handleCompletionCallback?(view) } } } extension FABMenu { /** Handles the hit test for the Menu and views outside of the Menu bounds. - Parameter _ point: A CGPoint. - Parameter with event: An optional UIEvent. - Returns: An optional UIView. */ open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { guard isOpened, isEnabled else { return super.hitTest(point, with: event) } for v in subviews { let p = v.convert(point, from: self) if v.bounds.contains(p) { if !shouldAvoidHitTest { delegate?.fabMenu?(fabMenu: self, tappedAt: point, isOutside: false) } shouldAvoidHitTest = !shouldAvoidHitTest return v.hitTest(p, with: event) } } delegate?.fabMenu?(fabMenu: self, tappedAt: point, isOutside: true) close(isTriggeredByUserInteraction: true) return super.hitTest(point, with: event) } } extension FABMenu { /** Handler to toggle the FABMenu opened or closed. - Parameter button: A UIButton. */ @objc fileprivate func handleFABButton(button: UIButton) { guard nil == handleFABButtonCallback else { handleFABButtonCallback?(button) return } guard isOpened else { open(isTriggeredByUserInteraction: true) return } close(isTriggeredByUserInteraction: true) } } ================================================ FILE: Sources/iOS/FABMenu/FABMenuController.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit public enum FABMenuBacking { case none case fade case blur } extension UIViewController { /** A convenience property that provides access to the FABMenuController. This is the recommended method of accessing the FABMenuController through child UIViewControllers. */ public var fabMenuController: FABMenuController? { return traverseViewControllerHierarchyForClassType() } } open class FABMenuController: TransitionController { /// Reference to the MenuView. @IBInspectable open var fabMenu = FABMenu() /// A FABMenuBacking value type. open var fabMenuBacking = FABMenuBacking.blur /// The fabMenuBacking UIBlurEffectStyle. open var fabMenuBackingBlurEffectStyle = UIBlurEffect.Style.light /// A reference to the blurView. open fileprivate(set) var blurView: UIView? open override func layoutSubviews() { super.layoutSubviews() rootViewController.view.frame = view.bounds } open override func prepare() { super.prepare() prepareFABMenu() } } extension FABMenuController: FABMenuDelegate {} fileprivate extension FABMenuController { /// Prepares the fabMenu. func prepareFABMenu() { fabMenu.delegate = self fabMenu.layer.zPosition = 1000 fabMenu.handleFABButtonCallback = { [weak self] in self?.handleFABButtonCallback(button: $0) } fabMenu.handleOpenCallback = { [weak self] in self?.handleOpenCallback() } fabMenu.handleCloseCallback = { [weak self] in self?.handleCloseCallback() } fabMenu.handleCompletionCallback = { [weak self] in self?.handleCompletionCallback(view: $0) } view.addSubview(fabMenu) } } fileprivate extension FABMenuController { /// Shows the fabMenuBacking. func showFabMenuBacking() { showFade() showBlurView() } /// Hides the fabMenuBacking. func hideFabMenuBacking() { hideFade() hideBlurView() } /// Shows the blurView. func showBlurView() { guard .blur == fabMenuBacking else { return } guard !fabMenu.isOpened, fabMenu.isEnabled else { return } guard nil == blurView else { return } let blur = UIVisualEffectView(effect: UIBlurEffect(style: fabMenuBackingBlurEffectStyle)) blurView = UIView() blurView?.layout(blur).edges() view.layout(blurView!).edges() view.bringSubviewToFront(fabMenu) } /// Hides the blurView. func hideBlurView() { guard .blur == fabMenuBacking else { return } guard fabMenu.isOpened, fabMenu.isEnabled else { return } blurView?.removeFromSuperview() blurView = nil } /// Shows the fade. func showFade() { guard .fade == fabMenuBacking else { return } guard !fabMenu.isOpened, fabMenu.isEnabled else { return } UIView.animate(withDuration: 0.15, animations: { [weak self] in self?.rootViewController.view.alpha = 0.15 }) } /// Hides the fade. func hideFade() { guard .fade == fabMenuBacking else { return } guard fabMenu.isOpened, fabMenu.isEnabled else { return } UIView.animate(withDuration: 0.15, animations: { [weak self] in self?.rootViewController.view.alpha = 1 }) } } fileprivate extension FABMenuController { /** Handler to toggle the FABMenu opened or closed. - Parameter button: A UIButton. */ func handleFABButtonCallback(button: UIButton) { guard fabMenu.isOpened else { fabMenu.open(isTriggeredByUserInteraction: true) return } fabMenu.close(isTriggeredByUserInteraction: true) } /// Handler for when the FABMenu.open function is called. func handleOpenCallback() { isUserInteractionEnabled = false showFabMenuBacking() } /// Handler for when the FABMenu.close function is called. func handleCloseCallback() { isUserInteractionEnabled = false hideFabMenuBacking() } /** Completion handler for FABMenu open and close calls. - Parameter view: A UIView. */ func handleCompletionCallback(view: UIView) { if view == fabMenu.fabMenuItems.last { isUserInteractionEnabled = true } } } ================================================ FILE: Sources/iOS/Font/DynamicFontType.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit @objc(DynamicFontTypeDelegate) public protocol DynamicFontTypeDelegate { /** A delegation method that is executed when the dynamic type is changed. - Parameter dynamicFontType: A DynamicFontType. */ func dynamicFontType(dynamicFontType: DynamicFontType) } @objc(DynamicFontType) open class DynamicFontType: NSObject { /// A weak reference to a DynamicFontTypeDelegate. open weak var delegate: DynamicFontTypeDelegate? /// Initializer. public override init() { super.init() prepare() } @objc internal func handleContentSizeChange() { delegate?.dynamicFontType(dynamicFontType: self) } /// Prepare the instance object. private func prepare() { prepareContentSizeObservation() } /// Prepares observation for content size changes. private func prepareContentSizeObservation() { NotificationCenter.default.addObserver(self, selector: #selector(handleContentSizeChange), name: UIContentSizeCategory.didChangeNotification, object: nil) } } ================================================ FILE: Sources/iOS/Font/Font.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit public protocol FontType { /** Regular with size font. - Parameter with size: A CGFLoat for the font size. - Returns: A UIFont. */ static func regular(with size: CGFloat) -> UIFont /** Medium with size font. - Parameter with size: A CGFLoat for the font size. - Returns: A UIFont. */ static func medium(with size: CGFloat) -> UIFont /** Bold with size font. - Parameter with size: A CGFLoat for the font size. - Returns: A UIFont. */ static func bold(with size: CGFloat) -> UIFont } public struct Font { /// Size of font. public static let pointSize: CGFloat = 16 /** Retrieves the system font with a specified size. - Parameter ofSize size: A CGFloat. */ public static func systemFont(ofSize size: CGFloat) -> UIFont { return UIFont.systemFont(ofSize: size) } /** Retrieves the bold system font with a specified size.. - Parameter ofSize size: A CGFloat. */ public static func boldSystemFont(ofSize size: CGFloat) -> UIFont { return UIFont.boldSystemFont(ofSize: size) } /** Retrieves the italic system font with a specified size. - Parameter ofSize size: A CGFloat. */ public static func italicSystemFont(ofSize size: CGFloat) -> UIFont { return UIFont.italicSystemFont(ofSize: size) } /** Loads a given font if needed. - Parameter name: A String font name. */ public static func loadFontIfNeeded(name: String) { FontLoader.loadFontIfNeeded(name: name) } } /// Loads fonts packaged with Material. private class FontLoader { /// A Dictionary of the fonts already loaded. static var loadedFonts: Dictionary = Dictionary() /** Loads a given font if needed. - Parameter fontName: A String font name. */ static func loadFontIfNeeded(name: String) { let loadedFont: String? = FontLoader.loadedFonts[name] if nil == loadedFont && nil == UIFont(name: name, size: 1) { FontLoader.loadedFonts[name] = name let bundle = Bundle(for: FontLoader.self) let identifier = bundle.bundleIdentifier let fontURL = true == identifier?.hasPrefix("org.cocoapods") ? bundle.url(forResource: name, withExtension: "ttf", subdirectory: "com.cosmicmind.material.fonts.bundle") : bundle.url(forResource: name, withExtension: "ttf") if let v = fontURL { let data = NSData(contentsOf: v as URL)! let provider = CGDataProvider(data: data)! let font = CGFont(provider) var error: Unmanaged? if !CTFontManagerRegisterGraphicsFont(font!, &error) { let errorDescription = CFErrorCopyDescription(error!.takeUnretainedValue()) let nsError = error!.takeUnretainedValue() as Any as! Error NSException(name: .internalInconsistencyException, reason: errorDescription as String?, userInfo: [NSUnderlyingErrorKey: nsError as Any]).raise() } } } } } ================================================ FILE: Sources/iOS/Font/RobotoFont.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit public struct RobotoFont: FontType { /// Size of font. public static var pointSize: CGFloat { return Font.pointSize } /// Thin font. public static var thin: UIFont { return thin(with: Font.pointSize) } /// Light font. public static var light: UIFont { return light(with: Font.pointSize) } /// Regular font. public static var regular: UIFont { return regular(with: Font.pointSize) } /// Medium font. public static var medium: UIFont { return medium(with: Font.pointSize) } /// Bold font. public static var bold: UIFont { return bold(with: Font.pointSize) } /** Thin with size font. - Parameter with size: A CGFLoat for the font size. - Returns: A UIFont. */ public static func thin(with size: CGFloat) -> UIFont { Font.loadFontIfNeeded(name: "Roboto-Thin") if let f = UIFont(name: "Roboto-Thin", size: size) { return f } return Font.systemFont(ofSize: size) } /** Light with size font. - Parameter with size: A CGFLoat for the font size. - Returns: A UIFont. */ public static func light(with size: CGFloat) -> UIFont { Font.loadFontIfNeeded(name: "Roboto-Light") if let f = UIFont(name: "Roboto-Light", size: size) { return f } return Font.systemFont(ofSize: size) } /** Regular with size font. - Parameter with size: A CGFLoat for the font size. - Returns: A UIFont. */ public static func regular(with size: CGFloat) -> UIFont { Font.loadFontIfNeeded(name: "Roboto-Regular") if let f = UIFont(name: "Roboto-Regular", size: size) { return f } return Font.systemFont(ofSize: size) } /** Medium with size font. - Parameter with size: A CGFLoat for the font size. - Returns: A UIFont. */ public static func medium(with size: CGFloat) -> UIFont { Font.loadFontIfNeeded(name: "Roboto-Medium") if let f = UIFont(name: "Roboto-Medium", size: size) { return f } return Font.boldSystemFont(ofSize: size) } /** Bold with size font. - Parameter with size: A CGFLoat for the font size. - Returns: A UIFont. */ public static func bold(with size: CGFloat) -> UIFont { Font.loadFontIfNeeded(name: "Roboto-Bold") if let f = UIFont(name: "Roboto-Bold", size: size) { return f } return Font.boldSystemFont(ofSize: size) } } ================================================ FILE: Sources/iOS/Grid/Grid.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion @objc(GridAxisDirection) public enum GridAxisDirection: Int { case any case horizontal case vertical } public struct GridAxis { /// The direction the grid lays its views out. public var direction = GridAxisDirection.horizontal /// The rows size. public var rows: Int /// The columns size. public var columns: Int /** Initializer. - Parameter rows: The number of rows, vertical axis the grid will use. - Parameter columns: The number of columns, horizontal axis the grid will use. */ internal init(rows: Int = 12, columns: Int = 12) { self.rows = rows self.columns = columns } } public struct GridOffset { /// The rows size. public var rows: Int /// The columns size. public var columns: Int /** Initializer. - Parameter rows: The number of rows, vertical axis the grid will use. - Parameter columns: The number of columns, horizontal axis the grid will use. */ internal init(rows: Int = 0, columns: Int = 0) { self.rows = rows self.columns = columns } } public struct Grid { /// Context view. internal weak var context: UIView? /// Defer the calculation. public var isDeferred = false /// Number of rows. public var rows: Int { didSet { reload() } } /// Number of columns. public var columns: Int { didSet { reload() } } /// Offsets for rows and columns. public var offset = GridOffset() { didSet { reload() } } /// The axis in which the Grid is laying out its views. public var axis = GridAxis() { didSet { reload() } } /// Preset inset value for grid. public var layoutEdgeInsetsPreset = EdgeInsetsPreset.none { didSet { layoutEdgeInsets = EdgeInsetsPresetToValue(preset: contentEdgeInsetsPreset) } } /// Insets value for grid. public var layoutEdgeInsets = EdgeInsets.zero { didSet { reload() } } /// Preset inset value for grid. public var contentEdgeInsetsPreset = EdgeInsetsPreset.none { didSet { contentEdgeInsets = EdgeInsetsPresetToValue(preset: contentEdgeInsetsPreset) } } /// Insets value for grid. public var contentEdgeInsets = EdgeInsets.zero { didSet { reload() } } /// A preset wrapper for interim space. public var interimSpacePreset = InterimSpacePreset.none { didSet { interimSpace = InterimSpacePresetToValue(preset: interimSpacePreset) } } /// The space between grid rows and columnss. public var interimSpace: InterimSpace { didSet { reload() } } /// An Array of UIButtons. public var views = [UIView]() { didSet { oldValue.forEach { $0.removeFromSuperview() } reload() } } /** Initializer. - Parameter rows: The number of rows, vertical axis the grid will use. - Parameter columns: The number of columns, horizontal axis the grid will use. - Parameter interimSpace: The interim space between rows or columns. */ internal init(context: UIView?, rows: Int = 0, columns: Int = 0, interimSpace: InterimSpace = 0) { self.context = context self.rows = rows self.columns = columns self.interimSpace = interimSpace } /// Begins a deferred block. public mutating func begin() { isDeferred = true } /// Completes a deferred block. public mutating func commit() { isDeferred = false reload() } /** Update grid in a deferred block. - Parameter _ block: An update code block. */ public mutating func update(_ block: (Grid) -> Void) { begin() block(self) commit() } /// Reload the button layout. public func reload() { guard !isDeferred else { return } guard let canvas = context else { return } for v in views { if canvas != v.superview { canvas.addSubview(v) } } let count = views.count guard 0 < count else { return } /// It is important to call `setNeedsLayout` and `layoutIfNeeded` /// in order to have the parent view's `frame` calculation be set /// for `Grid` to be able to then calculate the correct dimenions /// of its `child` views. canvas.setNeedsLayout() canvas.layoutIfNeeded() guard 0 < canvas.bounds.width && 0 < canvas.bounds.height else { return } var n = 0 var i = 0 for v in views { // Forces the view to adjust accordingly to size changes, ie: UILabel. (v as? UILabel)?.sizeToFit() switch axis.direction { case .horizontal: let c = 0 == v.grid.columns ? axis.columns / count : v.grid.columns let co = v.grid.offset.columns let w = (canvas.bounds.width - contentEdgeInsets.left - contentEdgeInsets.right - layoutEdgeInsets.left - layoutEdgeInsets.right + interimSpace) / CGFloat(axis.columns) v.frame.origin.x = CGFloat(i + n + co) * w + contentEdgeInsets.left + layoutEdgeInsets.left v.frame.origin.y = contentEdgeInsets.top + layoutEdgeInsets.top v.frame.size.width = w * CGFloat(c) - interimSpace v.frame.size.height = canvas.bounds.height - contentEdgeInsets.top - contentEdgeInsets.bottom - layoutEdgeInsets.top - layoutEdgeInsets.bottom n += c + co - 1 case .vertical: let r = 0 == v.grid.rows ? axis.rows / count : v.grid.rows let ro = v.grid.offset.rows let h = (canvas.bounds.height - contentEdgeInsets.top - contentEdgeInsets.bottom - layoutEdgeInsets.top - layoutEdgeInsets.bottom + interimSpace) / CGFloat(axis.rows) v.frame.origin.x = contentEdgeInsets.left + layoutEdgeInsets.left v.frame.origin.y = CGFloat(i + n + ro) * h + contentEdgeInsets.top + layoutEdgeInsets.top v.frame.size.width = canvas.bounds.width - contentEdgeInsets.left - contentEdgeInsets.right - layoutEdgeInsets.left - layoutEdgeInsets.right v.frame.size.height = h * CGFloat(r) - interimSpace n += r + ro - 1 case .any: let r = 0 == v.grid.rows ? axis.rows / count : v.grid.rows let ro = v.grid.offset.rows let c = 0 == v.grid.columns ? axis.columns / count : v.grid.columns let co = v.grid.offset.columns let w = (canvas.bounds.width - contentEdgeInsets.left - contentEdgeInsets.right - layoutEdgeInsets.left - layoutEdgeInsets.right + interimSpace) / CGFloat(axis.columns) let h = (canvas.bounds.height - contentEdgeInsets.top - contentEdgeInsets.bottom - layoutEdgeInsets.top - layoutEdgeInsets.bottom + interimSpace) / CGFloat(axis.rows) v.frame.origin.x = CGFloat(co) * w + contentEdgeInsets.left + layoutEdgeInsets.left v.frame.origin.y = CGFloat(ro) * h + contentEdgeInsets.top + layoutEdgeInsets.top v.frame.size.width = w * CGFloat(c) - interimSpace v.frame.size.height = h * CGFloat(r) - interimSpace } i += 1 /// reload the grid layout for each view on /// each iteration, in order to ensure that /// subviews are recalculated correctly. if (isDeferred) { continue } v.grid.reload() } } } fileprivate var AssociatedInstanceKey: UInt8 = 0 extension UIView { /// Grid reference. public var grid: Grid { get { return AssociatedObject.get(base: self, key: &AssociatedInstanceKey) { return Grid(context: self) } } set(value) { AssociatedObject.set(base: self, key: &AssociatedInstanceKey, value: value) } } /// A reference to grid's layoutEdgeInsetsPreset. open var layoutEdgeInsetsPreset: EdgeInsetsPreset { get { return grid.layoutEdgeInsetsPreset } set(value) { grid.layoutEdgeInsetsPreset = value } } /// A reference to grid's layoutEdgeInsets. @IBInspectable open var layoutEdgeInsets: EdgeInsets { get { return grid.layoutEdgeInsets } set(value) { grid.layoutEdgeInsets = value } } } ================================================ FILE: Sources/iOS/Height/HeightPreset.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit public enum HeightPreset { case none case tiny case xsmall case small case `default` case normal case medium case large case xlarge case xxlarge case custom(CGFloat) public var rawValue: CGFloat { switch self { case .none: return 0 case .tiny: return 20 case .xsmall: return 28 case .small: return 36 case .`default`: return 44 case .normal: return 49 case .medium: return 52 case .large: return 60 case .xlarge: return 68 case .xxlarge: return 104 case .custom(let v): return v } } } ================================================ FILE: Sources/iOS/Icon/Icon.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit public struct Icon { /// An internal reference to the icons bundle. private static var internalBundle: Bundle? /** A public reference to the icons bundle, that aims to detect the correct bundle to use. */ public static var bundle: Bundle { if nil == Icon.internalBundle { Icon.internalBundle = Bundle(for: View.self) let url = Icon.internalBundle!.resourceURL! let b = Bundle(url: url.appendingPathComponent("com.cosmicmind.material.icons.bundle")) if let v = b { Icon.internalBundle = v } } return Icon.internalBundle! } /// Get the icon by the file name. public static func icon(_ name: String) -> UIImage? { return UIImage(named: name, in: bundle, compatibleWith: nil)?.withRenderingMode(.alwaysTemplate) } /// Google icons. public static var add = Icon.icon("ic_add_white") public static var addCircle = Icon.icon("ic_add_circle_white") public static var addCircleOutline = Icon.icon("ic_add_circle_outline_white") public static var arrowBack = Icon.icon("ic_arrow_back_white") public static var arrowDownward = Icon.icon("ic_arrow_downward_white") public static var audio = Icon.icon("ic_audiotrack_white") public static var bell = Icon.icon("cm_bell_white") public static var cameraFront = Icon.icon("ic_camera_front_white") public static var cameraRear = Icon.icon("ic_camera_rear_white") public static var check = Icon.icon("ic_check_white") public static var clear = Icon.icon("ic_close_white") public static var close = Icon.icon("ic_close_white") public static var edit = Icon.icon("ic_edit_white") public static var email = Icon.icon("ic_email_white") public static var favorite = Icon.icon("ic_favorite_white") public static var favoriteBorder = Icon.icon("ic_favorite_border_white") public static var flashAuto = Icon.icon("ic_flash_auto_white") public static var flashOff = Icon.icon("ic_flash_off_white") public static var flashOn = Icon.icon("ic_flash_on_white") public static var history = Icon.icon("ic_history_white") public static var home = Icon.icon("ic_home_white") public static var image = Icon.icon("ic_image_white") public static var menu = Icon.icon("ic_menu_white") public static var moreHorizontal = Icon.icon("ic_more_horiz_white") public static var moreVertical = Icon.icon("ic_more_vert_white") public static var movie = Icon.icon("ic_movie_white") public static var pen = Icon.icon("ic_edit_white") public static var place = Icon.icon("ic_place_white") public static var phone = Icon.icon("ic_phone_white") public static var photoCamera = Icon.icon("ic_photo_camera_white") public static var photoLibrary = Icon.icon("ic_photo_library_white") public static var search = Icon.icon("ic_search_white") public static var settings = Icon.icon("ic_settings_white") public static var share = Icon.icon("ic_share_white") public static var star = Icon.icon("ic_star_white") public static var starBorder = Icon.icon("ic_star_border_white") public static var starHalf = Icon.icon("ic_star_half_white") public static var videocam = Icon.icon("ic_videocam_white") public static var visibility = Icon.icon("ic_visibility_white") public static var visibilityOff = Icon.icon("ic_visibility_off_white") public static var work = Icon.icon("ic_work_white") /// CosmicMind icons. public struct cm { public static var add = Icon.icon("cm_add_white") public static var arrowBack = Icon.icon("cm_arrow_back_white") public static var arrowDownward = Icon.icon("cm_arrow_downward_white") public static var audio = Icon.icon("cm_audio_white") public static var audioLibrary = Icon.icon("cm_audio_library_white") public static var bell = Icon.icon("cm_bell_white") public static var check = Icon.icon("cm_check_white") public static var clear = Icon.icon("cm_close_white") public static var close = Icon.icon("cm_close_white") public static var edit = Icon.icon("cm_pen_white") public static var image = Icon.icon("cm_image_white") public static var menu = Icon.icon("cm_menu_white") public static var microphone = Icon.icon("cm_microphone_white") public static var moreHorizontal = Icon.icon("cm_more_horiz_white") public static var moreVertical = Icon.icon("cm_more_vert_white") public static var movie = Icon.icon("cm_movie_white") public static var pause = Icon.icon("cm_pause_white") public static var pen = Icon.icon("cm_pen_white") public static var photoCamera = Icon.icon("cm_photo_camera_white") public static var photoLibrary = Icon.icon("cm_photo_library_white") public static var play = Icon.icon("cm_play_white") public static var search = Icon.icon("cm_search_white") public static var settings = Icon.icon("cm_settings_white") public static var share = Icon.icon("cm_share_white") public static var shuffle = Icon.icon("cm_shuffle_white") public static var skipBackward = Icon.icon("cm_skip_backward_white") public static var skipForward = Icon.icon("cm_skip_forward_white") public static var star = Icon.icon("cm_star_white") public static var videocam = Icon.icon("cm_videocam_white") public static var volumeHigh = Icon.icon("cm_volume_high_white") public static var volumeMedium = Icon.icon("cm_volume_medium_white") public static var volumeOff = Icon.icon("cm_volume_off_white") } } ================================================ FILE: Sources/iOS/Layer/Layer.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit @objc(Layer) open class Layer: CAShapeLayer { /** A CAShapeLayer used to manage elements that would be affected by the clipToBounds property of the backing layer. For example, this allows the dropshadow effect on the backing layer, while clipping the image to a desired shape within the visualLayer. */ public let visualLayer = CAShapeLayer() /** A property that manages an image for the visualLayer's contents property. Images should not be set to the backing layer's contents property to avoid conflicts when using clipsToBounds. */ @IBInspectable open var image: UIImage? { didSet { visualLayer.contents = image?.cgImage } } /** Allows a relative subrectangle within the range of 0 to 1 to be specified for the visualLayer's contents property. This allows much greater flexibility than the contentsGravity property in terms of how the image is cropped and stretched. */ open override var contentsRect: CGRect { didSet { visualLayer.contentsRect = contentsRect } } /** A CGRect that defines a stretchable region inside the visualLayer with a fixed border around the edge. */ open override var contentsCenter: CGRect { didSet { visualLayer.contentsCenter = contentsCenter } } /** A floating point value that defines a ratio between the pixel dimensions of the visualLayer's contents property and the size of the layer. By default, this value is set to the Screen.scale. */ @IBInspectable open override var contentsScale: CGFloat { didSet { visualLayer.contentsScale = contentsScale } } /// Determines how content should be aligned within the visualLayer's bounds. @IBInspectable open override var contentsGravity: CALayerContentsGravity { get { return visualLayer.contentsGravity } set(value) { visualLayer.contentsGravity = value } } /** A property that sets the cornerRadius of the backing layer. If the shape property has a value of .circle when the cornerRadius is set, it will become .none, as it no longer maintains its circle shape. */ @IBInspectable open override var cornerRadius: CGFloat { didSet { layoutShadowPath() shapePreset = .none } } /** An initializer that initializes the object with a NSCoder object. - Parameter aDecoder: A NSCoder instance. */ public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) prepareVisualLayer() } /** An initializer the same as init(). The layer parameter is ignored to avoid crashes on certain architectures. - Parameter layer: Any. */ public override init(layer: Any) { super.init(layer: layer) prepareVisualLayer() } /// A convenience initializer. public override init() { super.init() prepareVisualLayer() } /** An initializer that initializes the object with a CGRect object. - Parameter frame: A CGRect instance. */ public convenience init(frame: CGRect) { self.init() self.frame = frame } open override func layoutSublayers() { super.layoutSublayers() layoutShape() layoutVisualLayer() layoutShadowPath() } } fileprivate extension Layer { /// Prepares the visualLayer property. func prepareVisualLayer() { contentsGravity = .resizeAspectFill visualLayer.zPosition = 0 visualLayer.masksToBounds = true addSublayer(visualLayer) } /// Manages the layout for the visualLayer property. func layoutVisualLayer() { visualLayer.frame = bounds visualLayer.cornerRadius = cornerRadius } } ================================================ FILE: Sources/iOS/Layout/Layout.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion /// A protocol that's conformed by UIView and UILayoutGuide. public protocol Constraintable: class { } @available(iOS 9.0, *) extension UILayoutGuide: Constraintable { } extension UIView: Constraintable { } /// Layout extension for UIView. public extension UIView { /** Used to chain layout constraints on a child context. - Parameter child: A child UIView to layout. - Returns: A Layout instance. */ func layout(_ child: UIView) -> Layout { if self != child.superview { addSubview(child) } child.translatesAutoresizingMaskIntoConstraints = false return child.layout } /// Layout instance for the view. var layout: Layout { return Layout(constraintable: self) } /// Anchor instance for the view. var anchor: LayoutAnchor { return LayoutAnchor(constraintable: self) } /** Anchor instance for safeAreaLayoutGuide. Below iOS 11, it will be same as view.anchor. */ var safeAnchor: LayoutAnchor { if #available(iOS 11.0, *) { return LayoutAnchor(constraintable: safeAreaLayoutGuide) } else { return anchor } } } private extension NSLayoutConstraint { /** Checks if the constraint is equal to given constraint. - Parameter _ other: An NSLayoutConstraint. - Returns: A Bool indicating whether constraints are equal. */ func equalTo(_ other: NSLayoutConstraint) -> Bool { return firstItem === other.firstItem && secondItem === other.secondItem && firstAttribute == other.firstAttribute && secondAttribute == other.secondAttribute && relation == other.relation } } /// A memory reference to the lastConstraint of UIView. private var LastConstraintKey: UInt8 = 0 private extension UIView { /** The last consntraint that's created by Layout system. Used to set multiplier/priority on the last constraint. */ var lastConstraint: NSLayoutConstraint? { get { return AssociatedObject.get(base: self, key: &LastConstraintKey) { nil } } set(value) { AssociatedObject.set(base: self, key: &LastConstraintKey, value: value) } } } public struct Layout { /// A weak reference to the constraintable. public weak var constraintable: Constraintable? /// Parent view of the view. var parent: UIView? { return (constraintable as? UIView)?.superview } /// Returns the view that is being laied out. public var view: UIView? { var v = constraintable as? UIView if #available(iOS 9.0, *), v == nil { v = (constraintable as? UILayoutGuide)?.owningView } return v } /** An initializer taking Constraintable. - Parameter view: A Constraintable. */ init(constraintable: Constraintable) { self.constraintable = constraintable } } public extension Layout { /** Sets multiplier of the last created constraint. Not meant for updating the multiplier as it will re-create the constraint. - Parameter _ multiplier: A CGFloat multiplier. - Returns: A Layout instance to allow chaining. */ @discardableResult func multiply(_ multiplier: CGFloat) -> Layout { return resetLastConstraint(multiplier: multiplier) } /** Sets priority of the last created constraint. Not meant for updating the multiplier as it will re-create the constraint. - Parameter _ value: A Float priority. - Returns: A Layout instance to allow chaining. */ @discardableResult func priority(_ value: Float) -> Layout { return priority(.init(rawValue: value)) } /** Sets priority of the last created constraint. Not meant for updating the priority as it will re-create the constraint. - Parameter _ priority: A UILayoutPriority. - Returns: A Layout instance to allow chaining. */ @discardableResult func priority(_ priority: UILayoutPriority) -> Layout { return resetLastConstraint(priority: priority) } /** Removes the last created constraint and creates new one with the new multiplier and/or priority (if provided). - Parameter multiplier: An optional CGFloat. - Parameter priority: An optional UILayoutPriority. - Returns: A Layout instance to allow chaining. */ private func resetLastConstraint(multiplier: CGFloat? = nil, priority: UILayoutPriority? = nil) -> Layout { guard let v = view?.lastConstraint, v.isActive else { return self } v.isActive = false let newV = NSLayoutConstraint(item: v.firstItem as Any, attribute: v.firstAttribute, relatedBy: v.relation, toItem: v.secondItem, attribute: v.secondAttribute, multiplier: multiplier ?? v.multiplier, constant: v.constant) newV.priority = priority ?? v.priority newV.isActive = true view?.lastConstraint = newV return self } } public extension Layout { /** Constraints top of the view to its parent's. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func top(_ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.top, relationer: relationer, constant: offset) } /** Constraints left of the view to its parent's. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func left(_ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.left, relationer: relationer, constant: offset) } /** Constraints right of the view to its parent. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func right(_ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.right, relationer: relationer, constant: -offset) } /** Constraints leading of the view to its parent's. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func leading(_ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.leading, relationer: relationer, constant: offset) } /** Constraints trailing of the view to its parent. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func trailing(_ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.trailing, relationer: relationer, constant: -offset) } /** Constraints bottom of the view to its parent's. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func bottom(_ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.bottom, relationer: relationer, constant: -offset) } /** Constraints top-left of the view to its parent's. - Parameter top: A CGFloat offset for top. - Parameter left: A CGFloat offset for left. - Returns: A Layout instance to allow chaining. */ @discardableResult func topLeft(top: CGFloat = 0, left: CGFloat = 0) -> Layout { return constraint(.topLeft, constants: top, left) } /** Constraints top-right of the view to its parent's. - Parameter top: A CGFloat offset for top. - Parameter right: A CGFloat offset for right. - Returns: A Layout instance to allow chaining. */ @discardableResult func topRight(top: CGFloat = 0, right: CGFloat = 0) -> Layout { return constraint(.topRight, constants: top, -right) } /** Constraints bottom-left of the view to its parent's. - Parameter bottom: A CGFloat offset for bottom. - Parameter left: A CGFloat offset for left. - Returns: A Layout instance to allow chaining. */ @discardableResult func bottomLeft(bottom: CGFloat = 0, left: CGFloat = 0) -> Layout { return constraint(.bottomLeft, constants: -bottom, left) } /** Constraints bottom-right of the view to its parent's. - Parameter bottom: A CGFloat offset for bottom. - Parameter right: A CGFloat offset for right. - Returns: A Layout instance to allow chaining. */ @discardableResult func bottomRight(bottom: CGFloat = 0, right: CGFloat = 0) -> Layout { return constraint(.bottomRight, constants: -bottom, -right) } /** Constraints left and right of the view to its parent's. - Parameter left: A CGFloat offset for left. - Parameter right: A CGFloat offset for right. - Returns: A Layout instance to allow chaining. */ @discardableResult func leftRight(left: CGFloat = 0, right: CGFloat = 0) -> Layout { return constraint(.leftRight, constants: left, -right) } /** Constraints top-leading of the view to its parent's. - Parameter top: A CGFloat offset for top. - Parameter leading: A CGFloat offset for leading. - Returns: A Layout instance to allow chaining. */ @discardableResult func topLeading(top: CGFloat = 0, leading: CGFloat = 0) -> Layout { return constraint(.topLeading, constants: top, leading) } /** Constraints top-trailing of the view to its parent's. - Parameter top: A CGFloat offset for top. - Parameter trailing: A CGFloat offset for trailing. - Returns: A Layout instance to allow chaining. */ @discardableResult func topTrailing(top: CGFloat = 0, trailing: CGFloat = 0) -> Layout { return constraint(.topTrailing, constants: top, -trailing) } /** Constraints bottom-leading of the view to its parent's. - Parameter bottom: A CGFloat offset for bottom. - Parameter leading: A CGFloat offset for leading. - Returns: A Layout instance to allow chaining. */ @discardableResult func bottomLeading(bottom: CGFloat = 0, leading: CGFloat = 0) -> Layout { return constraint(.bottomLeading, constants: -bottom, leading) } /** Constraints bottom-trailing of the view to its parent's. - Parameter bottom: A CGFloat offset for bottom. - Parameter trailing: A CGFloat offset for trailing. - Returns: A Layout instance to allow chaining. */ @discardableResult func bottomTrailing(bottom: CGFloat = 0, trailing: CGFloat = 0) -> Layout { return constraint(.bottomTrailing, constants: -bottom, -trailing) } /** Constraints leading and trailing of the view to its parent's. - Parameter leading: A CGFloat offset for leading. - Parameter trailing: A CGFloat offset for trailing. - Returns: A Layout instance to allow chaining. */ @discardableResult func leadingTrailing(leading: CGFloat = 0, trailing: CGFloat = 0) -> Layout { return constraint(.leadingTrailing, constants: leading, -trailing) } /** Constraints top and bottom of the view to its parent's. - Parameter top: A CGFloat offset for top. - Parameter bottom: A CGFloat offset for bottom. - Returns: A Layout instance to allow chaining. */ @discardableResult func topBottom(top: CGFloat = 0, bottom: CGFloat = 0) -> Layout { return constraint(.topBottom, constants: top, -bottom) } /** Constraints center of the view to its parent's. - Parameter offsetX: A CGFloat offset for horizontal center. - Parameter offsetY: A CGFloat offset for vertical center. - Returns: A Layout instance to allow chaining. */ @discardableResult func center(offsetX: CGFloat = 0, offsetY: CGFloat = 0) -> Layout { return constraint(.center, constants: offsetX, offsetY) } /** Constraints horizontal center of the view to its parent's. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func centerX(_ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.centerX, relationer: relationer, constant: offset) } /** Constraints vertical center of the view to its parent's. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func centerY(_ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.centerY, relationer: relationer, constant: offset) } /** Constraints width of the view to its parent's. - Parameter offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func width(offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.width, relationer: relationer, constant: offset) } /** Constraints height of the view to its parent's. - Parameter offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func height(offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.height, relationer: relationer, constant: offset) } /** Constraints edges of the view to its parent's. - Parameter top: A CGFloat offset for top. - Parameter left: A CGFloat offset for left. - Parameter bottom: A CGFloat offset for bottom. - Parameter right: A CGFloat offset for right. - Returns: A Layout instance to allow chaining. */ @discardableResult func edges(top: CGFloat = 0, left: CGFloat = 0, bottom: CGFloat = 0, right: CGFloat = 0) -> Layout { return constraint(.edges, constants: top, left, -bottom, -right) } } public extension Layout { /** Constraints top of the view to its parent's safeArea. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func topSafe(_ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.top, relationer: relationer, constant: offset, useSafeArea: true) } /** Constraints left of the view to its parent's safeArea. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func leftSafe(_ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.left, relationer: relationer, constant: offset, useSafeArea: true) } /** Constraints right of the view to its parent. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func rightSafe(_ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.right, relationer: relationer, constant: -offset, useSafeArea: true) } /** Constraints leading of the view to its parent's safeArea. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func leadingSafe(_ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.leading, relationer: relationer, constant: offset, useSafeArea: true) } /** Constraints trailing of the view to its parent. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func trailingSafe(_ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.trailing, relationer: relationer, constant: -offset, useSafeArea: true) } /** Constraints bottom of the view to its parent's safeArea. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func bottomSafe(_ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.bottom, relationer: relationer, constant: -offset, useSafeArea: true) } /** Constraints top-left of the view to its parent's safeArea. - Parameter top: A CGFloat offset for top. - Parameter left: A CGFloat offset for left. - Returns: A Layout instance to allow chaining. */ @discardableResult func topLeftSafe(top: CGFloat = 0, left: CGFloat = 0) -> Layout { return constraint(.topLeft, constants: top, left, useSafeArea: true) } /** Constraints top-right of the view to its parent's safeArea. - Parameter top: A CGFloat offset for top. - Parameter right: A CGFloat offset for right. - Returns: A Layout instance to allow chaining. */ @discardableResult func topRightSafe(top: CGFloat = 0, right: CGFloat = 0) -> Layout { return constraint(.topRight, constants: top, -right, useSafeArea: true) } /** Constraints bottom-left of the view to its parent's safeArea. - Parameter bottom: A CGFloat offset for bottom. - Parameter left: A CGFloat offset for left. - Returns: A Layout instance to allow chaining. */ @discardableResult func bottomLeftSafe(bottom: CGFloat = 0, left: CGFloat = 0) -> Layout { return constraint(.bottomLeft, constants: -bottom, left, useSafeArea: true) } /** Constraints bottom-right of the view to its parent's safeArea. - Parameter bottom: A CGFloat offset for bottom. - Parameter right: A CGFloat offset for right. - Returns: A Layout instance to allow chaining. */ @discardableResult func bottomRightSafe(bottom: CGFloat = 0, right: CGFloat = 0) -> Layout { return constraint(.bottomRight, constants: -bottom, -right, useSafeArea: true) } /** Constraints left and right of the view to its parent's safeArea. - Parameter left: A CGFloat offset for left. - Parameter right: A CGFloat offset for right. - Returns: A Layout instance to allow chaining. */ @discardableResult func leftRightSafe(left: CGFloat = 0, right: CGFloat = 0) -> Layout { return constraint(.leftRight, constants: left, -right, useSafeArea: true) } /** Constraints top-leading of the view to its parent's safeArea. - Parameter top: A CGFloat offset for top. - Parameter leading: A CGFloat offset for leading. - Returns: A Layout instance to allow chaining. */ @discardableResult func topLeadingSafe(top: CGFloat = 0, leading: CGFloat = 0) -> Layout { return constraint(.topLeading, constants: top, leading, useSafeArea: true) } /** Constraints top-trailing of the view to its parent's safeArea. - Parameter top: A CGFloat offset for top. - Parameter trailing: A CGFloat offset for trailing. - Returns: A Layout instance to allow chaining. */ @discardableResult func topTrailingSafe(top: CGFloat = 0, trailing: CGFloat = 0) -> Layout { return constraint(.topTrailing, constants: top, -trailing, useSafeArea: true) } /** Constraints bottom-leading of the view to its parent's safeArea. - Parameter bottom: A CGFloat offset for bottom. - Parameter leading: A CGFloat offset for leading. - Returns: A Layout instance to allow chaining. */ @discardableResult func bottomLeadingSafe(bottom: CGFloat = 0, leading: CGFloat = 0) -> Layout { return constraint(.bottomLeading, constants: -bottom, leading, useSafeArea: true) } /** Constraints bottom-trailing of the view to its parent's safeArea. - Parameter bottom: A CGFloat offset for bottom. - Parameter trailing: A CGFloat offset for trailing. - Returns: A Layout instance to allow chaining. */ @discardableResult func bottomTrailingSafe(bottom: CGFloat = 0, trailing: CGFloat = 0) -> Layout { return constraint(.bottomTrailing, constants: -bottom, -trailing, useSafeArea: true) } /** Constraints leading and trailing of the view to its parent's safeArea. - Parameter leading: A CGFloat offset for leading. - Parameter trailing: A CGFloat offset for trailing. - Returns: A Layout instance to allow chaining. */ @discardableResult func leadingTrailingSafe(leading: CGFloat = 0, trailing: CGFloat = 0) -> Layout { return constraint(.leadingTrailing, constants: leading, -trailing, useSafeArea: true) } /** Constraints top and bottom of the view to its parent's safeArea. - Parameter top: A CGFloat offset for top. - Parameter bottom: A CGFloat offset for bottom. - Returns: A Layout instance to allow chaining. */ @discardableResult func topBottomSafe(top: CGFloat = 0, bottom: CGFloat = 0) -> Layout { return constraint(.topBottom, constants: top, -bottom, useSafeArea: true) } /** Constraints center of the view to its parent's safeArea. - Parameter offsetX: A CGFloat offset for horizontal center. - Parameter offsetY: A CGFloat offset for vertical center. - Returns: A Layout instance to allow chaining. */ @discardableResult func centerSafe(offsetX: CGFloat = 0, offsetY: CGFloat = 0) -> Layout { return constraint(.center, constants: offsetX, offsetY, useSafeArea: true) } /** Constraints horizontal center of the view to its parent's safeArea. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func centerXSafe(_ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.centerX, relationer: relationer, constant: offset, useSafeArea: true) } /** Constraints vertical center of the view to its parent's safeArea. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func centerYSafe(_ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.centerY, relationer: relationer, constant: offset, useSafeArea: true) } /** Constraints width of the view to its parent's safeArea. - Parameter offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func widthSafe(offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.width, relationer: relationer, constant: offset, useSafeArea: true) } /** Constraints height of the view to its parent's safeArea. - Parameter offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func heightSafe(offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.height, relationer: relationer, constant: offset, useSafeArea: true) } /** Constraints edges of the view to its parent's safeArea. - Parameter top: A CGFloat offset for top. - Parameter left: A CGFloat offset for left. - Parameter bottom: A CGFloat offset for bottom. - Parameter right: A CGFloat offset for right. - Returns: A Layout instance to allow chaining. */ @discardableResult func edgesSafe(top: CGFloat = 0, left: CGFloat = 0, bottom: CGFloat = 0, right: CGFloat = 0) -> Layout { return constraint(.edges, constants: top, left, -bottom, -right, useSafeArea: true) } } public extension Layout { /** Constraints width of the view to a constant value. - Parameter _ width: A CGFloat value. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func width(_ width: CGFloat, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.constantWidth, relationer: relationer, constants: width) } /** Constraints height of the view to a constant value. - Parameter _ height: A CGFloat value. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func height(_ height: CGFloat, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.constantHeight, relationer: relationer, constants: height) } /** The width and height of the view to its parent's. - Parameter _ size: A CGSize offset. - Returns: A Layout instance to allow chaining. */ @discardableResult func size(_ size: CGSize) -> Layout { return width(size.width).height(size.height) } } public extension Layout { /** Constraints top of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func top(_ anchor: LayoutAnchorable, _ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.top, to: anchor, relationer: relationer, constant: offset) } /** Constraints left of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func left(_ anchor: LayoutAnchorable, _ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.left, to: anchor, relationer: relationer, constant: offset) } /** Constraints right of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func right(_ anchor: LayoutAnchorable, _ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.right, to: anchor, relationer: relationer, constant: -offset) } /** Constraints leading of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func leading(_ anchor: LayoutAnchorable, _ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.leading, to: anchor, relationer: relationer, constant: offset) } /** Constraints trailing of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func trailing(_ anchor: LayoutAnchorable, _ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.trailing, to: anchor, relationer: relationer, constant: -offset) } /** Constraints bottom of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func bottom(_ anchor: LayoutAnchorable, _ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.bottom, to: anchor, relationer: relationer, constant: -offset) } /** Constraints top-leading of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter top: A CGFloat offset for top. - Parameter leading: A CGFloat offset for leading. - Returns: A Layout instance to allow chaining. */ @discardableResult func topLeading(_ anchor: LayoutAnchorable, top: CGFloat = 0, leading: CGFloat = 0) -> Layout { return constraint(.topLeading, to: anchor, constants: top, leading) } /** Constraints top-trailing of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter top: A CGFloat offset for top. - Parameter trailing: A CGFloat offset for trailing. - Returns: A Layout instance to allow chaining. */ @discardableResult func topTrailing(_ anchor: LayoutAnchorable, top: CGFloat = 0, trailing: CGFloat = 0) -> Layout { return constraint(.topTrailing, to: anchor, constants: top, -trailing) } /** Constraints bottom-leading of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter bottom: A CGFloat offset for bottom. - Parameter leading: A CGFloat offset for leading. - Returns: A Layout instance to allow chaining. */ @discardableResult func bottomLeading(_ anchor: LayoutAnchorable, bottom: CGFloat = 0, leading: CGFloat = 0) -> Layout { return constraint(.bottomLeading, to: anchor, constants: -bottom, leading) } /** Constraints bottom-trailing of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter bottom: A CGFloat offset for bottom. - Parameter trailing: A CGFloat offset for trailing. - Returns: A Layout instance to allow chaining. */ @discardableResult func bottomTrailing(_ anchor: LayoutAnchorable, bottom: CGFloat = 0, trailing: CGFloat = 0) -> Layout { return constraint(.bottomTrailing, to: anchor, constants: -bottom, -trailing) } /** Constraints leading and trailing of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter leading: A CGFloat offset for leading. - Parameter trailing: A CGFloat offset for trailing. - Returns: A Layout instance to allow chaining. */ @discardableResult func leadingTrailing(_ anchor: LayoutAnchorable, leading: CGFloat = 0, trailing: CGFloat = 0) -> Layout { return constraint(.leadingTrailing, to: anchor, constants: leading, -trailing) } /** Constraints top-left of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter top: A CGFloat offset for top. - Parameter left: A CGFloat offset for left. - Returns: A Layout instance to allow chaining. */ @discardableResult func topLeft(_ anchor: LayoutAnchorable, top: CGFloat = 0, left: CGFloat = 0) -> Layout { return constraint(.topLeft, to: anchor, constants: top, left) } /** Constraints top-right of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter top: A CGFloat offset for top. - Parameter right: A CGFloat offset for right. - Returns: A Layout instance to allow chaining. */ @discardableResult func topRight(_ anchor: LayoutAnchorable, top: CGFloat = 0, right: CGFloat = 0) -> Layout { return constraint(.topRight, to: anchor, constants: top, -right) } /** Constraints bottom-left of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter bottom: A CGFloat offset for bottom. - Parameter left: A CGFloat offset for left. - Returns: A Layout instance to allow chaining. */ @discardableResult func bottomLeft(_ anchor: LayoutAnchorable, bottom: CGFloat = 0, left: CGFloat = 0) -> Layout { return constraint(.bottomLeft, to: anchor, constants: -bottom, left) } /** Constraints bottom-right of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter bottom: A CGFloat offset for bottom. - Parameter right: A CGFloat offset for right. - Returns: A Layout instance to allow chaining. */ @discardableResult func bottomRight(_ anchor: LayoutAnchorable, bottom: CGFloat = 0, right: CGFloat = 0) -> Layout { return constraint(.bottomRight, to: anchor, constants: -bottom, -right) } /** Constraints left and right of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter left: A CGFloat offset for left. - Parameter right: A CGFloat offset for right. - Returns: A Layout instance to allow chaining. */ @discardableResult func leftRight(_ anchor: LayoutAnchorable, left: CGFloat = 0, right: CGFloat = 0) -> Layout { return constraint(.leftRight, to: anchor, constants: left, -right) } /** Constraints top and bottom of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter top: A CGFloat offset for top. - Parameter bottom: A CGFloat offset for bottom. - Returns: A Layout instance to allow chaining. */ @discardableResult func topBottom(_ anchor: LayoutAnchorable, top: CGFloat = 0, bottom: CGFloat = 0) -> Layout { return constraint(.topBottom, to: anchor, constants: top, -bottom) } /** Constraints center of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter offsetX: A CGFloat offset for horizontal center. - Parameter offsetY: A CGFloat offset for vertical center. - Returns: A Layout instance to allow chaining. */ @discardableResult func center(_ anchor: LayoutAnchorable, offsetX: CGFloat = 0, offsetY: CGFloat = 0) -> Layout { return constraint(.center, to: anchor, constants: offsetX, offsetY) } /** Constraints horizontal center of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func centerX(_ anchor: LayoutAnchorable, _ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.centerX, to: anchor, relationer: relationer, constant: offset) } /** Constraints vertical center of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func centerY(_ anchor: LayoutAnchorable, _ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.centerY, to: anchor, relationer: relationer, constant: offset) } /** Constraints width of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func width(_ anchor: LayoutAnchorable, _ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.width, to: anchor, relationer: relationer, constant: offset) } /** Constraints height of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter _ offset: A CGFloat offset. - Parameter _ relationer: A LayoutRelationer. - Returns: A Layout instance to allow chaining. */ @discardableResult func height(_ anchor: LayoutAnchorable, _ offset: CGFloat = 0, _ relationer: LayoutRelationer = LayoutRelationerStruct.equal) -> Layout { return constraint(.height, to: anchor, relationer: relationer, constant: offset) } /** Constraints edges of the view to the given anchor. - Parameter _ anchor: A LayoutAnchorable. - Parameter top: A CGFloat offset for top. - Parameter left: A CGFloat offset for left. - Parameter bottom: A CGFloat offset for bottom. - Parameter right: A CGFloat offset for right. - Returns: A Layout instance to allow chaining. */ @discardableResult func edges(_ anchor: LayoutAnchorable, top: CGFloat = 0, left: CGFloat = 0, bottom: CGFloat = 0, right: CGFloat = 0) -> Layout { return constraint(.edges, to: anchor, constants: top, left, -bottom, -right) } } public extension Layout { /** Constraints the object and sets it's anchor to `bottom`. - Parameter _ view: A UIView. - Parameter offset: A CGFloat offset for top. - Returns: A Layout instance to allow chaining. */ @discardableResult func below(_ view: UIView, _ offset: CGFloat = 0) -> Layout { return top(view.anchor.bottom, offset) } /** Constraints the object and sets it's anchor to `top`. - Parameter _ view: A UIView. - Parameter offset: A CGFloat offset for bottom. - Returns: A Layout instance to allow chaining. */ @discardableResult func above(_ view: UIView, _ offset: CGFloat = 0) -> Layout { return bottom(view.anchor.top, offset) } /** Constraints the object and sets it's anchor to `before/left`. - Parameter _ view: A UIView. - Parameter offset: A CGFloat offset for right. - Returns: A Layout instance to allow chaining. */ @discardableResult func before(_ view: UIView, _ offset: CGFloat = 0) -> Layout { return right(view.anchor.left, offset) } /** Constraints the object and sets it's anchor to `after/right`. - Parameter _ view: A UIView. - Parameter offset: A CGFloat offset for left. - Returns: A Layout instance to allow chaining. */ @discardableResult func after(_ view: UIView, _ offset: CGFloat = 0) -> Layout { return left(view.anchor.right, offset) } /** Constraints the object and sets it's aspect. - Parameter ratio: A CGFloat ratio multiplier. - Returns: A Layout instance to allow chaining. */ @discardableResult func aspect(_ ratio: CGFloat = 1) -> Layout { return height(view!.anchor.width).multiply(ratio) } } private extension Layout { /** Constraints the view to its parent according to the provided attribute. If the constraint already exists, will update its constant. - Parameter _ attribute: A LayoutAttribute. - Parameter _ relationer: A LayoutRelationer. - Parameter constant: A CGFloat. - Returns: A Layout instance to allow chaining. */ func constraint(_ attribute: LayoutAttribute, relationer: LayoutRelationer = LayoutRelationerStruct.equal, constant: CGFloat, useSafeArea: Bool = false) -> Layout { return constraint([attribute], relationer: relationer, constants: constant, useSafeArea: useSafeArea) } /** Constraints the view to its parent according to the provided attributes. If any of the constraints already exists, will update its constant. - Parameter _ attributes: An array of LayoutAttribute. - Parameter _ relationer: A LayoutRelationer. - Parameter constants: A list of CGFloat. - Returns: A Layout instance to allow chaining. */ func constraint(_ attributes: [LayoutAttribute], relationer: LayoutRelationer = LayoutRelationerStruct.equal, constants: CGFloat..., useSafeArea: Bool = false) -> Layout { var attributes = attributes var anchor: LayoutAnchor! if attributes == .constantHeight || attributes == .constantWidth { attributes.removeLast() anchor = LayoutAnchor(constraintable: nil, attributes: [.notAnAttribute]) } else { guard parent != nil else { fatalError("[Material Error: Constraint requires view to have parent.") } anchor = LayoutAnchor(constraintable: useSafeArea ? parent?.safeAnchor.constraintable : parent, attributes: attributes) } return constraint(attributes, to: anchor, relationer: relationer, constants: constants) } /** Constraints the view to the given anchor according to the provided attribute. If the constraint already exists, will update its constant. - Parameter _ attribute: A LayoutAttribute. - Parameter to anchor: A LayoutAnchorable. - Parameter relation: A LayoutRelation between anchors. - Parameter constant: A CGFloat. - Returns: A Layout instance to allow chaining. */ func constraint(_ attribute: LayoutAttribute, to anchor: LayoutAnchorable, relationer: LayoutRelationer = LayoutRelationerStruct.equal, constant: CGFloat) -> Layout { return constraint([attribute], to: anchor, relationer: relationer, constants: constant) } /** Constraints the view to the given anchor according to the provided attributes. If any of the constraints already exists, will update its constant. - Parameter _ attributes: An array of LayoutAttribute. - Parameter to anchor: A LayoutAnchorable. - Parameter relation: A LayoutRelation between anchors. - Parameter constants: A list of CGFloat. - Returns: A Layout instance to allow chaining. */ func constraint(_ attributes: [LayoutAttribute], to anchor: LayoutAnchorable, relationer: LayoutRelationer = LayoutRelationerStruct.equal, constants: CGFloat...) -> Layout { return constraint(attributes, to: anchor, relationer: relationer, constants: constants) } /** Constraints the view to the given anchor according to the provided attributes. If any of the constraints already exists, will update its constant. - Parameter _ attributes: An array of LayoutAttribute. - Parameter to anchor: A LayoutAnchorable. - Parameter relation: A LayoutRelation between anchors. - Parameter constants: An array of CGFloat. - Returns: A Layout instance to allow chaining. */ func constraint(_ attributes: [LayoutAttribute], to anchor: LayoutAnchorable, relationer: LayoutRelationer, constants: [CGFloat]) -> Layout { let from = LayoutAnchor(constraintable: constraintable, attributes: attributes) var to = anchor as? LayoutAnchor if to?.attributes.isEmpty ?? true { let v = (anchor as? UIView) ?? (anchor as? LayoutAnchor)?.constraintable to = LayoutAnchor(constraintable: v, attributes: attributes) } let constraint = LayoutConstraint(fromAnchor: from, toAnchor: to!, relation: relationer(.nil, .nil), constants: constants) let constraints = (view?.constraints ?? []) + (view?.superview?.constraints ?? []) let newConstraints = constraint.constraints for newConstraint in newConstraints { guard let activeConstraint = constraints.first(where: { $0.equalTo(newConstraint) }) else { newConstraint.isActive = true view?.lastConstraint = newConstraint continue } activeConstraint.constant = newConstraint.constant } return self } } /// A closure typealias for relation operators. public typealias LayoutRelationer = (LayoutRelationerStruct, LayoutRelationerStruct) -> LayoutRelation /// A dummy struct used in creating relation operators (==, >=, <=). public struct LayoutRelationerStruct { /// Passed as an unused argument to the LayoutRelationer closures. static let `nil` = LayoutRelationerStruct() /** A method used as a default parameter for LayoutRelationer closures. Swift does not allow using == operator directly, so we had to create this. */ public static func equal(left: LayoutRelationerStruct, right: LayoutRelationerStruct) -> LayoutRelation { return .equal } } /// A method returning `LayoutRelation.equal` public func ==(left: LayoutRelationerStruct, right: LayoutRelationerStruct) -> LayoutRelation { return .equal } /// A method returning `LayoutRelation.greaterThanOrEqual` public func >=(left: LayoutRelationerStruct, right: LayoutRelationerStruct) -> LayoutRelation { return .greaterThanOrEqual } /// A method returning `LayoutRelation.lessThanOrEqual` public func <=(left: LayoutRelationerStruct, right: LayoutRelationerStruct) -> LayoutRelation { return .lessThanOrEqual } ================================================ FILE: Sources/iOS/Layout/LayoutAnchor.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit /// A protocol that's conformed by UIView, LayoutAnchor, and Layout. public protocol LayoutAnchorable { } extension UIView: LayoutAnchorable { } extension Layout: LayoutAnchorable { } extension LayoutAnchor: LayoutAnchorable { } public struct LayoutAnchor { /// A weak reference to the constraintable. public weak var constraintable: Constraintable? /// An array of LayoutAttribute for the view. public let attributes: [LayoutAttribute] /** An initializer taking constraintable and anchor attributes. - Parameter view: A Constraintable. - Parameter attributes: An array of LayoutAtrribute. */ public init(constraintable: Constraintable?, attributes: [LayoutAttribute] = []) { self.constraintable = constraintable self.attributes = attributes } } public extension LayoutAnchor { /// A layout anchor representing top of the view. var top: LayoutAnchor { return anchor(.top) } /// A layout anchor representing bottom of the view. var bottom: LayoutAnchor { return anchor(.bottom) } /// A layout anchor representing left of the view. var left: LayoutAnchor { return anchor(.left) } /// A layout anchor representing right of the view. var right: LayoutAnchor { return anchor(.right) } /// A layout anchor representing leading of the view. var leading: LayoutAnchor { return anchor(.leading) } /// A layout anchor representing trailing of the view. var trailing: LayoutAnchor { return anchor(.trailing) } /// A layout anchor representing top-left of the view. var topLeft: LayoutAnchor { return acnhor(.topLeft) } /// A layout anchor representing top-right of the view. var topRight: LayoutAnchor { return acnhor(.topRight) } /// A layout anchor representing bottom-left of the view. var bottomLeft: LayoutAnchor { return acnhor(.bottomLeft) } /// A layout anchor representing bottom-right of the view. var bottomRight: LayoutAnchor { return acnhor(.bottomRight) } /// A layout anchor representing top-leading of the view. var topLeading: LayoutAnchor { return acnhor(.topLeading) } /// A layout anchor representing top-trailing of the view. var topTrailing: LayoutAnchor { return acnhor(.topTrailing) } /// A layout anchor representing bottom-leading of the view. var bottomLeading: LayoutAnchor { return acnhor(.bottomLeading) } /// A layout anchor representing bottom-trailing of the view. var bottomTrailing: LayoutAnchor { return acnhor(.bottomTrailing) } /// A layout anchor representing top and bottom of the view. var topBottom: LayoutAnchor { return acnhor(.topBottom) } /// A layout anchor representing left and right of the view. var leftRight: LayoutAnchor { return acnhor(.leftRight) } /// A layout anchor representing leading and trailing of the view. var leadingTrailing: LayoutAnchor { return acnhor(.leadingTrailing) } /// A layout anchor representing center of the view. var center: LayoutAnchor { return acnhor(.center) } /// A layout anchor representing horizontal center of the view. var centerX: LayoutAnchor { return anchor(.centerX) } /// A layout anchor representing vertical center of the view. var centerY: LayoutAnchor { return anchor(.centerY) } /// A layout anchor representing top, left, bottom and right of the view. var edges: LayoutAnchor { return acnhor(.edges) } /// A layout anchor representing width of the view. var width: LayoutAnchor { return anchor(.width) } /// A layout anchor representing height of the view. var height: LayoutAnchor { return anchor(.height) } } private extension LayoutAnchor { /** Creates LayoutAnchor with the given attribute. - Parameter attribute: A LayoutAttribute. - Returns: A LayoutAnchor. */ func anchor(_ attribute: LayoutAttribute) -> LayoutAnchor { return LayoutAnchor(constraintable: constraintable, attributes: [attribute]) } /** Creates LayoutAnchor with the given attributes. - Parameter attributes: An array of LayoutAttribute. - Returns: A LayoutAnchor. */ func acnhor(_ attributes: [LayoutAttribute]) -> LayoutAnchor { return LayoutAnchor(constraintable: constraintable, attributes: attributes) } } ================================================ FILE: Sources/iOS/Layout/LayoutAttribute.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit /// A typealias for NSLayoutConstraint.Attribute public typealias LayoutAttribute = NSLayoutConstraint.Attribute internal extension Array where Element == LayoutAttribute { /// A LayoutAttribute array containing top and left. static var topLeft: [LayoutAttribute] { return [.top, .left] } /// A LayoutAttribute array containing top and right. static var topRight: [LayoutAttribute] { return [.top, .right] } /// A LayoutAttribute array containing bottom and left. static var bottomLeft: [LayoutAttribute] { return [.bottom, .left] } /// A LayoutAttribute array containing bottom and right. static var bottomRight: [LayoutAttribute] { return [.bottom, .right] } /// A LayoutAttribute array containing left and right. static var leftRight: [LayoutAttribute] { return [.left, .right] } /// A LayoutAttribute array containing top and leading. static var topLeading: [LayoutAttribute] { return [.top, .leading] } /// A LayoutAttribute array containing top and trailing. static var topTrailing: [LayoutAttribute] { return [.top, .trailing] } /// A LayoutAttribute array containing bottom and leading. static var bottomLeading: [LayoutAttribute] { return [.bottom, .leading] } /// A LayoutAttribute array containing bottom and trailing. static var bottomTrailing: [LayoutAttribute] { return [.bottom, .trailing] } /// A LayoutAttribute array containing left and trailing. static var leadingTrailing: [LayoutAttribute] { return [.leading, .trailing] } /// A LayoutAttribute array containing top and bottom. static var topBottom: [LayoutAttribute] { return [.top, .bottom] } /// A LayoutAttribute array containing centerX and centerY. static var center: [LayoutAttribute] { return [.centerX, .centerY] } /// A LayoutAttribute array containing top, left, bottom and right. static var edges: [LayoutAttribute] { return [.top, .left, .bottom, .right] } /// A LayoutAttribute array for constant height. static var constantHeight: [LayoutAttribute] { return [.height, .notAnAttribute] } /// A LayoutAttribute array for constant width. static var constantWidth: [LayoutAttribute] { return [.width, .notAnAttribute] } } ================================================ FILE: Sources/iOS/Layout/LayoutConstraint.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit /// A typealias for NSLayoutConstraint.Relation public typealias LayoutRelation = NSLayoutConstraint.Relation internal struct LayoutConstraint { /// `From` anchor for the constraint. private let fromAnchor: LayoutAnchor /// `To` anchor for the constraint. private let toAnchor: LayoutAnchor /// An array of constants for the constraint. private let constants: [CGFloat] /// A LayoutRelation between anchors. private let relation: LayoutRelation /** An initializer taking `from` and `to` anchors, their `relation` and constants for the constraint. - Parameter fromAnchor: A LayoutAnchor. - Parameter toAnchor: A LayoutAnchor. - Parameter relation: A LayoutRelation between anchors. - Parameter constants: An array of CGFloat. */ init(fromAnchor: LayoutAnchor, toAnchor: LayoutAnchor, relation: LayoutRelation, constants: [CGFloat]) { self.fromAnchor = fromAnchor self.toAnchor = toAnchor self.relation = relation self.constants = constants } } internal extension LayoutConstraint { /// Creates an array of NSLayoutConstraint from a LayoutConstraint. var constraints: [NSLayoutConstraint] { guard fromAnchor.attributes.count == toAnchor.attributes.count else { fatalError("[Material Error: The number of attributes of anchors does not match.]") } guard fromAnchor.attributes.count == constants.count else { fatalError("[Material Error: The number of constants does not match the number of constraints.]") } var v: [NSLayoutConstraint] = [] zip(zip(fromAnchor.attributes, toAnchor.attributes), constants).forEach { v.append(NSLayoutConstraint(item: fromAnchor.constraintable as Any, attribute: $0.0, relatedBy: relation, toItem: toAnchor.constraintable, attribute: $0.1, multiplier: 1, constant: $1)) } return v } } ================================================ FILE: Sources/iOS/Navigation/NavigationBar.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit open class NavigationBar: UINavigationBar, Themeable { /// Will layout the view. open var willLayout: Bool { return 0 < bounds.width && 0 < bounds.height && nil != superview } /// Detail UILabel when in landscape for iOS 11. fileprivate var toolbarToText: [Toolbar: String?]? open override var intrinsicContentSize: CGSize { return CGSize(width: bounds.width, height: bounds.height) } /// A preset wrapper around contentEdgeInsets. open var contentEdgeInsetsPreset = EdgeInsetsPreset.square1 { didSet { contentEdgeInsets = EdgeInsetsPresetToValue(preset: contentEdgeInsetsPreset) } } /// A reference to EdgeInsets. @IBInspectable open var contentEdgeInsets = EdgeInsetsPresetToValue(preset: .square1) { didSet { layoutSubviews() } } /// A preset wrapper around interimSpace. open var interimSpacePreset = InterimSpacePreset.interimSpace3 { didSet { interimSpace = InterimSpacePresetToValue(preset: interimSpacePreset) } } /// A wrapper around grid.interimSpace. @IBInspectable open var interimSpace = InterimSpacePresetToValue(preset: .interimSpace3) { didSet { layoutSubviews() } } /** The back button image writes to the backIndicatorImage property and backIndicatorTransitionMaskImage property. */ @IBInspectable open var backButtonImage: UIImage? { get { return backIndicatorImage } set(value) { let image: UIImage? = value backIndicatorImage = image backIndicatorTransitionMaskImage = image } } /// A property that accesses the backing layer's background @IBInspectable open override var backgroundColor: UIColor? { get { return barTintColor } set(value) { barTintColor = value } } /** An initializer that initializes the object with a NSCoder object. - Parameter aDecoder: A NSCoder instance. */ public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) prepare() } /** An initializer that initializes the object with a CGRect object. If AutoLayout is used, it is better to initilize the instance using the init() initializer. - Parameter frame: A CGRect instance. */ public override init(frame: CGRect) { super.init(frame: frame) prepare() } open override func sizeThatFits(_ size: CGSize) -> CGSize { return intrinsicContentSize } open override func layoutSubviews() { super.layoutSubviews() layoutShape() layoutShadowPath() //iOS 11 added left/right layout margin in subviews of UINavigationBar //since we do not want to unsafely access private views directly, we //iterate through the subviews to set `layoutMargins` to zero for v in subviews { if #available(iOS 13.0, *) { let margins = v.layoutMargins v.frame.origin.x = -margins.left v.frame.size.width += margins.left + margins.right } else { v.layoutMargins = .zero } } if let v = topItem { layoutNavigationItem(item: v) } if let v = backItem { layoutNavigationItem(item: v) } layoutDivider() } /** Prepares the view instance when intialized. When subclassing, it is recommended to override the prepare method to initialize property values and other setup operations. The super.prepare method should always be called immediately when subclassing. */ open func prepare() { barStyle = .black isTranslucent = false depthPreset = .none contentScaleFactor = Screen.scale backButtonImage = Icon.cm.arrowBack if #available(iOS 11, *) { toolbarToText = [:] } let image = UIImage() shadowImage = image setBackgroundImage(image, for: .default) backgroundColor = .white applyCurrentTheme() } /** Applies the given theme. - Parameter theme: A Theme. */ open func apply(theme: Theme) { backgroundColor = theme.primary items?.forEach { apply(theme: theme, to: $0) } } /** Applies the given theme to the navigation item. - Parameter theme: A Theme. - Parameter to item: A UINavigationItem. */ private func apply(theme: Theme, to item: UINavigationItem) { Theme.apply(theme: theme, to: item.toolbar) } } internal extension NavigationBar { /** Lays out the UINavigationItem. - Parameter item: A UINavigationItem to layout. */ func layoutNavigationItem(item: UINavigationItem) { guard willLayout else { return } if isThemingEnabled { apply(theme: .current, to: item) } let toolbar = item.toolbar toolbar.backgroundColor = .clear toolbar.interimSpace = interimSpace toolbar.contentEdgeInsets = contentEdgeInsets if #available(iOS 11, *) { if Application.shouldStatusBarBeHidden { toolbar.contentEdgeInsetsPreset = .none if nil != toolbar.detailLabel.text { toolbarToText?[toolbar] = toolbar.detailLabel.text toolbar.detailLabel.text = nil } } else if nil != toolbarToText?[toolbar] { toolbar.detailLabel.text = toolbarToText?[toolbar] ?? nil toolbarToText?[toolbar] = nil } } item.titleView = toolbar item.titleView!.frame = bounds } } ================================================ FILE: Sources/iOS/Navigation/NavigationController.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion extension NavigationController { /// Device status bar style. open var statusBarStyle: UIStatusBarStyle { get { return Application.statusBarStyle } set(value) { Application.statusBarStyle = value } } } open class NavigationController: UINavigationController { /** An initializer that initializes the object with a NSCoder object. - Parameter aDecoder: A NSCoder instance. */ public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } /** An initializer that initializes the object with an Optional nib and bundle. - Parameter nibNameOrNil: An Optional String for the nib. - Parameter bundle: An Optional NSBundle where the nib is located. */ public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) } /** An initializer that initializes the object with a rootViewController. - Parameter rootViewController: A UIViewController for the rootViewController. */ public override init(rootViewController: UIViewController) { super.init(navigationBarClass: NavigationBar.self, toolbarClass: nil) setViewControllers([rootViewController], animated: false) } public init(rootViewController: UIViewController, navigationBarClass: Swift.AnyClass?) { super.init(navigationBarClass: navigationBarClass, toolbarClass: nil) setViewControllers([rootViewController], animated: false) } open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) guard let v = interactivePopGestureRecognizer else { return } guard let x = navigationDrawerController else { return } if let l = x.leftPanGesture { l.require(toFail: v) } if let r = x.rightPanGesture { r.require(toFail: v) } } open override func viewDidLoad() { super.viewDidLoad() prepare() } open override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) guard let v = navigationBar as? NavigationBar else { return } guard let item = v.topItem else { return } v.layoutNavigationItem(item: item) } open override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() layoutSubviews() } /** Prepares the view instance when intialized. When subclassing, it is recommended to override the prepare method to initialize property values and other setup operations. The super.prepare method should always be called immediately when subclassing. */ open func prepare() { navigationBar.frame.size.width = view.bounds.width navigationBar.heightPreset = .normal view.clipsToBounds = true view.backgroundColor = .white view.contentScaleFactor = Screen.scale // This ensures the panning gesture is available when going back between views. if let v = interactivePopGestureRecognizer { v.isEnabled = true v.delegate = self } } /// Calls the layout functions for the view heirarchy. open func layoutSubviews() { navigationBar.setNeedsUpdateConstraints() navigationBar.updateConstraintsIfNeeded() navigationBar.setNeedsLayout() navigationBar.layoutIfNeeded() } /** Sets whether the navigation bar is hidden. - Parameter _ hidden: Specify true to hide the navigation bar or false to show it. - Parameter animated: Specify true if you want to animate the change in visibility or false if you want the navigation bar to appear immediately. */ open override func setNavigationBarHidden(_ hidden: Bool, animated: Bool) { super.setNavigationBarHidden(hidden, animated: animated) guard let items = navigationBar.items, items.count > 1 else { return } items.forEach { prepareBackButton(for: $0, in: navigationBar) } } } extension NavigationController: UINavigationBarDelegate { /** Delegation method that is called when a new UINavigationItem is about to be pushed. This is used to prepare the transitions between UIViewControllers on the stack. - Parameter navigationBar: A UINavigationBar that is used in the NavigationController. - Parameter item: The UINavigationItem that will be pushed on the stack. - Returns: A Boolean value that indicates whether to push the item on to the stack or not. True is yes, false is no. */ public func navigationBar(_ navigationBar: UINavigationBar, shouldPush item: UINavigationItem) -> Bool { prepareBackButton(for: item, in: navigationBar) return true } public func navigationBar(_ navigationBar: UINavigationBar, didPop item: UINavigationItem) { removeBackButton(from: item) } } internal extension NavigationController { /// Handler for the backbutton. @objc func handle(backButton: UIButton) { popViewController(animated: true) } /** Prepares back button of the navigation item in navigation bar. - Parameter for item: A UINavigationItem. - Parameter in navigationBar: A UINavigationBar. */ func prepareBackButton(for item: UINavigationItem, in navigationBar: UINavigationBar) { guard let v = navigationBar as? NavigationBar else { return } if nil == item.backButton.image && nil == item.backButton.title { item.backButton.image = v.backButtonImage } if !item.backButton.isHidden && !item.leftViews.contains(item.backButton) { item.leftViews.insert(item.backButton, at: 0) } item.backButton.addTarget(self, action: #selector(handle(backButton:)), for: .touchUpInside) item.hidesBackButton = false item.setHidesBackButton(true, animated: false) v.layoutNavigationItem(item: item) } /** Removes back button of the navigation item. - Parameter from item: A UINavigationItem. */ func removeBackButton(from item: UINavigationItem) { if let index = item.leftViews.firstIndex(of: item.backButton) { item.leftViews.remove(at: index) } item.backButton.removeTarget(self, action: #selector(handle(backButton:)), for: .touchUpInside) } } extension NavigationController: UIGestureRecognizerDelegate { /** Detects the gesture recognizer being used. This is necessary when using NavigationDrawerController. It eliminates the conflict in panning. - Parameter gestureRecognizer: A UIGestureRecognizer to detect. - Parameter touch: The UITouch event. - Returns: A Boolean of whether to continue the gesture or not, true yes, false no. */ public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { return interactivePopGestureRecognizer == gestureRecognizer && nil != navigationBar.backItem } } ================================================ FILE: Sources/iOS/Navigation/NavigationItem.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion /// A memory reference to the NavigationItem instance. fileprivate var NavigationItemKey: UInt8 = 0 fileprivate var NavigationItemContext: UInt8 = 0 fileprivate class NavigationItem: NSObject { /// A reference to the toolbar. @objc let toolbar = Toolbar() /// Back Button. lazy var backButton = IconButton() /// An optional reference to the NavigationBar. var navigationBar: NavigationBar? { var v = toolbar.contentView.superview while nil != v { if let navigationBar = v as? NavigationBar { return navigationBar } v = v?.superview } return nil } } fileprivate extension UINavigationItem { /// NavigationItem reference. var navigationItem: NavigationItem { get { return AssociatedObject.get(base: self, key: &NavigationItemKey) { return NavigationItem() } } set(value) { AssociatedObject.set(base: self, key: &NavigationItemKey, value: value) } } } internal extension UINavigationItem { /// A reference to the NavigationItem Toolbar. var toolbar: Toolbar { return navigationItem.toolbar } } extension UINavigationItem { /// Should center the contentView. open var contentViewAlignment: ContentViewAlignment { get { return toolbar.contentViewAlignment } set(value) { toolbar.contentViewAlignment = value } } /// Content View. open var contentView: UIView { return toolbar.contentView } /// Back Button. open var backButton: IconButton { return navigationItem.backButton } /// Title Label. open var titleLabel: UILabel { return toolbar.titleLabel } /// Detail Label. open var detailLabel: UILabel { return toolbar.detailLabel } /// Left side UIViews. open var leftViews: [UIView] { get { return toolbar.leftViews } set(value) { toolbar.leftViews = value } } /// Right side UIViews. open var rightViews: [UIView] { get { return toolbar.rightViews } set(value) { toolbar.rightViews = value } } /// Center UIViews. open var centerViews: [UIView] { get { return toolbar.centerViews } set(value) { toolbar.centerViews = value } } } ================================================ FILE: Sources/iOS/NavigationDrawer/NavigationDrawerController.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion @objc(NavigationDrawerPosition) public enum NavigationDrawerPosition: Int { case left case right } extension UIViewController { /** A convenience property that provides access to the NavigationDrawerController. This is the recommended method of accessing the NavigationDrawerController through child UIViewControllers. */ public var navigationDrawerController: NavigationDrawerController? { return traverseViewControllerHierarchyForClassType() } } @objc(NavigationDrawerControllerDelegate) public protocol NavigationDrawerControllerDelegate { /** An optional delegation method that is fired before the NavigationDrawerController opens. - Parameter navigationDrawerController: A NavigationDrawerController. - Parameter position: The NavigationDrawerPosition. */ @objc optional func navigationDrawerController(navigationDrawerController: NavigationDrawerController, willOpen position: NavigationDrawerPosition) /** An optional delegation method that is fired after the NavigationDrawerController opened. - Parameter navigationDrawerController: A NavigationDrawerController. - Parameter position: The NavigationDrawerPosition. */ @objc optional func navigationDrawerController(navigationDrawerController: NavigationDrawerController, didOpen position: NavigationDrawerPosition) /** An optional delegation method that is fired before the NavigationDrawerController closes. - Parameter navigationDrawerController: A NavigationDrawerController. - Parameter position: The NavigationDrawerPosition. */ @objc optional func navigationDrawerController(navigationDrawerController: NavigationDrawerController, willClose position: NavigationDrawerPosition) /** An optional delegation method that is fired after the NavigationDrawerController closed. - Parameter navigationDrawerController: A NavigationDrawerController. - Parameter position: The NavigationDrawerPosition. */ @objc optional func navigationDrawerController(navigationDrawerController: NavigationDrawerController, didClose position: NavigationDrawerPosition) /** An optional delegation method that is fired when the NavigationDrawerController pan gesture begins. - Parameter navigationDrawerController: A NavigationDrawerController. - Parameter didBeginPanAt point: A CGPoint. - Parameter position: The NavigationDrawerPosition. */ @objc optional func navigationDrawerController(navigationDrawerController: NavigationDrawerController, didBeginPanAt point: CGPoint, position: NavigationDrawerPosition) /** An optional delegation method that is fired when the NavigationDrawerController pan gesture changes position. - Parameter navigationDrawerController: A NavigationDrawerController. - Parameter didChangePanAt point: A CGPoint. - Parameter position: The NavigationDrawerPosition. */ @objc optional func navigationDrawerController(navigationDrawerController: NavigationDrawerController, didChangePanAt point: CGPoint, position: NavigationDrawerPosition) /** An optional delegation method that is fired when the NavigationDrawerController pan gesture ends. - Parameter navigationDrawerController: A NavigationDrawerController. - Parameter didEndPanAt point: A CGPoint. - Parameter position: The NavigationDrawerPosition. */ @objc optional func navigationDrawerController(navigationDrawerController: NavigationDrawerController, didEndPanAt point: CGPoint, position: NavigationDrawerPosition) /** An optional delegation method that is fired when the NavigationDrawerController tap gesture executes. - Parameter navigationDrawerController: A NavigationDrawerController. - Parameter didTapAt point: A CGPoint. - Parameter position: The NavigationDrawerPosition. */ @objc optional func navigationDrawerController(navigationDrawerController: NavigationDrawerController, didTapAt point: CGPoint, position: NavigationDrawerPosition) /** An optional delegation method that is fired when the status bar is about to change display, isHidden or not. - Parameter navigationDrawerController: A NavigationDrawerController. - Parameter statusBar isHidden: A boolean. */ @objc optional func navigationDrawerController(navigationDrawerController: NavigationDrawerController, statusBar isHidden: Bool) } @objc(NavigationDrawerController) @objcMembers open class NavigationDrawerController: TransitionController { /// A boolean indicating if the panel is animating. fileprivate var isAnimating = false /** A CGFloat property that is used internally to track the original (x) position of the container view when panning. */ fileprivate var originalX: CGFloat = 0 /** A UIPanGestureRecognizer property internally used for the leftView pan gesture. */ internal fileprivate(set) var leftPanGesture: UIPanGestureRecognizer? /** A UITapGestureRecognizer property internally used for the leftView tap gesture. */ internal fileprivate(set) var leftTapGesture: UITapGestureRecognizer? /** A UIPanGestureRecognizer property internally used for the rightView pan gesture. */ internal fileprivate(set) var rightPanGesture: UIPanGestureRecognizer? /** A UITapGestureRecognizer property internally used for the rightView tap gesture. */ internal fileprivate(set) var rightTapGesture: UITapGestureRecognizer? /** A CGFloat property that accesses the leftView threshold of the NavigationDrawerController. When the panning gesture has ended, if the position is beyond the threshold, the leftView is opened, if it is below the threshold, the leftView is closed. */ @IBInspectable open var leftThreshold: CGFloat = 64 fileprivate var leftViewThreshold: CGFloat = 0 /** A CGFloat property that accesses the rightView threshold of the NavigationDrawerController. When the panning gesture has ended, if the position is beyond the threshold, the rightView is closed, if it is below the threshold, the rightView is opened. */ @IBInspectable open var rightThreshold: CGFloat = 64 fileprivate var rightViewThreshold: CGFloat = 0 /** A NavigationDrawerControllerDelegate property used to bind the delegation object. */ open weak var delegate: NavigationDrawerControllerDelegate? /** A CGFloat property that sets the animation duration of the leftView when closing and opening. Defaults to 0.25. */ @IBInspectable open var animationDuration: TimeInterval = 0.25 /** A Boolean property that enables and disables the leftView from opening and closing. Defaults to true. */ @IBInspectable open var isEnabled: Bool { get { return isLeftViewEnabled || isRightViewEnabled } set(value) { if nil != leftView { isLeftViewEnabled = value } if nil != rightView { isRightViewEnabled = value } } } /** A Boolean property that enables and disables the leftView from opening and closing. Defaults to true. */ @IBInspectable open var isLeftViewEnabled = false { didSet { isLeftPanGestureEnabled = isLeftViewEnabled isLeftTapGestureEnabled = isLeftViewEnabled } } /// Enables the left pan gesture. @IBInspectable open var isLeftPanGestureEnabled = false { didSet { if isLeftPanGestureEnabled { prepareLeftPanGesture() } else { removeLeftPanGesture() } } } /// Enables the left tap gesture. @IBInspectable open var isLeftTapGestureEnabled = false { didSet { if isLeftTapGestureEnabled { prepareLeftTapGesture() } else { removeLeftTapGesture() } } } /** A Boolean property that enables and disables the rightView from opening and closing. Defaults to true. */ @IBInspectable open var isRightViewEnabled = false { didSet { isRightPanGestureEnabled = isRightViewEnabled isRightTapGestureEnabled = isRightViewEnabled } } /// Enables the right pan gesture. @IBInspectable open var isRightPanGestureEnabled = false { didSet { if isRightPanGestureEnabled { prepareRightPanGesture() } else { removeRightPanGesture() } } } /// Enables the right tap gesture. @IBInspectable open var isRightTapGestureEnabled = false { didSet { if isRightTapGestureEnabled { prepareRightTapGesture() } else { removeRightTapGesture() } } } /** A Boolean property that triggers the status bar to be isHidden when the leftView is opened. Defaults to true. */ @IBInspectable open var isHiddenStatusBarEnabled = true /** A DepthPreset property that is used to set the depth of the leftView when opened. */ open var depthPreset = DepthPreset.depth1 /** A UIView property that is used to hide and reveal the leftViewController. It is very rare that this property will need to be accessed externally. */ open fileprivate(set) var leftView: UIView? /** A UIView property that is used to hide and reveal the rightViewController. It is very rare that this property will need to be accessed externally. */ open fileprivate(set) var rightView: UIView? /// Indicates whether the leftView or rightView is opened. open var isOpened: Bool { return isLeftViewOpened || isRightViewOpened } /// indicates if the leftView is opened. open var isLeftViewOpened: Bool { guard let v = leftView else { return false } return v.frame.origin.x != -leftViewWidth } /// Indicates if the rightView is opened. open var isRightViewOpened: Bool { guard let v = rightView else { return false } return v.frame.origin.x != Screen.width } /** Content view controller to encompase the entire component. This is primarily used when the StatusBar is being isHidden. The alpha value of the rootViewController decreases, and shows the StatusBar. To avoid this, and to add a isHidden transition viewController for complex situations, the contentViewController was added. */ open fileprivate(set) lazy var contentViewController = UIViewController() /** A UIViewController property that references the active left UIViewController. */ open fileprivate(set) var leftViewController: UIViewController? /** A UIViewController property that references the active right UIViewController. */ open fileprivate(set) var rightViewController: UIViewController? /** A CGFloat property to access the width that the leftView opens up to. */ @IBInspectable open fileprivate(set) var leftViewWidth: CGFloat = 0 /** A CGFloat property to access the width that the rightView opens up to. */ @IBInspectable open fileprivate(set) var rightViewWidth: CGFloat = 0 /** An initializer that initializes the object with a NSCoder object. - Parameter aDecoder: A NSCoder instance. */ public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } /** An initializer that initializes the object with an Optional nib and bundle. - Parameter nibNameOrNil: An Optional String for the nib. - Parameter bundle: An Optional NSBundle where the nib is located. */ public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) } /** An initializer for the NavigationDrawerController. - Parameter rootViewController: The main UIViewController. - Parameter leftViewController: An Optional left UIViewController. - Parameter rightViewController: An Optional right UIViewController. */ public init(rootViewController: UIViewController, leftViewController: UIViewController? = nil, rightViewController: UIViewController? = nil) { super.init(rootViewController: rootViewController) self.leftViewController = leftViewController self.rightViewController = rightViewController } open override func transition(to viewController: UIViewController, completion: ((Bool) -> Void)? = nil) { super.transition(to: viewController) { [weak self, completion = completion] (result) in guard let `self` = self else { return } self.view.sendSubviewToBack(self.contentViewController.view) completion?(result) } } /// Layout subviews. open override func layoutSubviews() { super.layoutSubviews() toggleStatusBar() if let v = leftView { v.frame.size.width = leftViewWidth v.frame.size.height = view.bounds.height leftViewThreshold = leftViewWidth / 2 if let vc = leftViewController { vc.view.frame = v.bounds vc.view.frame.size.width = leftViewWidth } } if let v = rightView { v.frame.size.width = rightViewWidth v.frame.size.height = view.bounds.height rightViewThreshold = view.bounds.width - rightViewWidth / 2 if let vc = rightViewController { vc.view.frame = v.bounds vc.view.frame.size.width = rightViewWidth } } rootViewController.view.frame = container.bounds } open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) leftViewController?.beginAppearanceTransition(true, animated: animated) rightViewController?.beginAppearanceTransition(true, animated: animated) } open override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) leftViewController?.endAppearanceTransition() rightViewController?.endAppearanceTransition() } open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) leftViewController?.beginAppearanceTransition(false, animated: animated) rightViewController?.beginAppearanceTransition(false, animated: animated) } open override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) leftViewController?.endAppearanceTransition() rightViewController?.endAppearanceTransition() } open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) // Ensures the view is isHidden. guard let v = rightView else { return } v.layer.position.x = size.width + (isRightViewOpened ? -v.bounds.width : v.bounds.width) / 2 } open override func prepare() { super.prepare() prepareContentViewController() prepareLeftView() prepareRightView() } /** A method that is used to set the width of the leftView when opened. This is the recommended method of setting the leftView width. - Parameter width: A CGFloat value to set as the new width. - Parameter isHidden: A Boolean value of whether the leftView should be isHidden after the width has been updated or not. - Parameter animated: A Boolean value that indicates to animate the leftView width change. */ open func setLeftViewWidth(width: CGFloat, isHidden: Bool, animated: Bool, duration: TimeInterval = 0.5) { guard let v = leftView else { return } leftViewWidth = width var hide = isHidden if isRightViewOpened { hide = true } if animated { v.isShadowPathAutoSizing = false if hide { UIView.animate(withDuration: duration, animations: { [weak self, v = v] in guard let `self` = self else { return } v.bounds.size.width = width v.layer.position.x = -width / 2 self.rootViewController.view.alpha = 1 }) { [weak self, v = v] _ in guard let `self` = self else { return } v.isShadowPathAutoSizing = true self.layoutSubviews() self.hideView(container: v) } } else { UIView.animate(withDuration: duration, animations: { [weak self, v = v] in guard let `self` = self else { return } v.bounds.size.width = width v.layer.position.x = width / 2 self.rootViewController.view.alpha = 0.5 }) { [weak self, v = v] _ in guard let `self` = self else { return } v.isShadowPathAutoSizing = true self.layoutSubviews() self.showView(container: v) } } } else { v.bounds.size.width = width if hide { hideView(container: v) v.layer.position.x = -v.bounds.width / 2 rootViewController.view.alpha = 1 } else { showView(container: v) v.layer.position.x = width / 2 rootViewController.view.alpha = 0.5 } layoutSubviews() } } /** A method that is used to set the width of the rightView when opened. This is the recommended method of setting the rightView width. - Parameter width: A CGFloat value to set as the new width. - Parameter isHidden: A Boolean value of whether the rightView should be isHidden after the width has been updated or not. - Parameter animated: A Boolean value that indicates to animate the rightView width change. */ open func setRightViewWidth(width: CGFloat, isHidden: Bool, animated: Bool, duration: TimeInterval = 0.5) { guard let v = rightView else { return } rightViewWidth = width var hide = isHidden if isLeftViewOpened { hide = true } if animated { v.isShadowPathAutoSizing = false if hide { UIView.animate(withDuration: duration, animations: { [weak self, v = v] in guard let `self` = self else { return } v.bounds.size.width = width v.layer.position.x = self.view.bounds.width + width / 2 self.rootViewController.view.alpha = 1 }) { [weak self, v = v] _ in guard let `self` = self else { return } v.isShadowPathAutoSizing = true self.layoutSubviews() self.hideView(container: v) } } else { UIView.animate(withDuration: duration, animations: { [weak self, v = v] in guard let `self` = self else { return } v.bounds.size.width = width v.layer.position.x = self.view.bounds.width - width / 2 self.rootViewController.view.alpha = 0.5 }) { [weak self, v = v] _ in guard let `self` = self else { return } v.isShadowPathAutoSizing = true self.layoutSubviews() self.showView(container: v) } } } else { v.bounds.size.width = width if hide { hideView(container: v) v.layer.position.x = view.bounds.width + v.bounds.width / 2 rootViewController.view.alpha = 1 } else { showView(container: v) v.layer.position.x = view.bounds.width - width / 2 rootViewController.view.alpha = 0.5 } layoutSubviews() } } /** A method that toggles the leftView opened if previously closed, or closed if previously opened. - Parameter velocity: A CGFloat value that sets the velocity of the user interaction when animating the leftView. Defaults to 0. */ open func toggleLeftView(velocity: CGFloat = 0) { isLeftViewOpened ? closeLeftView(velocity: velocity) : openLeftView(velocity: velocity) } /** A method that toggles the rightView opened if previously closed, or closed if previously opened. - Parameter velocity: A CGFloat value that sets the velocity of the user interaction when animating the leftView. Defaults to 0. */ open func toggleRightView(velocity: CGFloat = 0) { isRightViewOpened ? closeRightView(velocity: velocity) : openRightView(velocity: velocity) } /** A method that opens the leftView. - Parameter velocity: A CGFloat value that sets the velocity of the user interaction when animating the leftView. Defaults to 0. */ open func openLeftView(velocity: CGFloat = 0) { guard !isAnimating else { return } guard isLeftViewEnabled else { return } guard let v = leftView else { return } isAnimating = true hideStatusBar() showView(container: v) isUserInteractionEnabled = false delegate?.navigationDrawerController?(navigationDrawerController: self, willOpen: .left) UIView.animate(withDuration: TimeInterval(0 == velocity ? animationDuration : fmax(0.1, fmin(1, Double(v.frame.origin.x / velocity)))), animations: { [weak self, v = v] in guard let `self` = self else { return } v.layer.position.x = v.bounds.width / 2 self.rootViewController.view.alpha = 0.5 }) { [weak self] _ in guard let `self` = self else { return } self.isAnimating = false self.delegate?.navigationDrawerController?(navigationDrawerController: self, didOpen: .left) } } /** A method that opens the rightView. - Parameter velocity: A CGFloat value that sets the velocity of the user interaction when animating the leftView. Defaults to 0. */ open func openRightView(velocity: CGFloat = 0) { guard !isAnimating else { return } guard isRightViewEnabled else { return } guard let v = rightView else { return } isAnimating = true hideStatusBar() showView(container: v) isUserInteractionEnabled = false delegate?.navigationDrawerController?(navigationDrawerController: self, willOpen: .right) UIView.animate(withDuration: TimeInterval(0 == velocity ? animationDuration : fmax(0.1, fmin(1, Double(v.frame.origin.x / velocity)))), animations: { [weak self, v = v] in guard let `self` = self else { return } v.layer.position.x = self.view.bounds.width - v.bounds.width / 2 self.rootViewController.view.alpha = 0.5 }) { [weak self] _ in guard let `self` = self else { return } self.isAnimating = false self.delegate?.navigationDrawerController?(navigationDrawerController: self, didOpen: .right) } } /** A method that closes the leftView. - Parameter velocity: A CGFloat value that sets the velocity of the user interaction when animating the leftView. Defaults to 0. */ open func closeLeftView(velocity: CGFloat = 0) { guard !isAnimating else { return } guard isLeftViewEnabled else { return } guard let v = leftView else { return } isAnimating = true delegate?.navigationDrawerController?(navigationDrawerController: self, willClose: .left) UIView.animate(withDuration: TimeInterval(0 == velocity ? animationDuration : fmax(0.1, fmin(1, Double(v.frame.origin.x / velocity)))), animations: { [weak self, v = v] in guard let `self` = self else { return } v.layer.position.x = -v.bounds.width / 2 self.rootViewController.view.alpha = 1 }) { [weak self, v = v] _ in guard let `self` = self else { return } self.hideView(container: v) self.toggleStatusBar() self.isAnimating = false self.isUserInteractionEnabled = true self.delegate?.navigationDrawerController?(navigationDrawerController: self, didClose: .left) } } /** A method that closes the rightView. - Parameter velocity: A CGFloat value that sets the velocity of the user interaction when animating the leftView. Defaults to 0. */ open func closeRightView(velocity: CGFloat = 0) { guard !isAnimating else { return } guard isRightViewEnabled else { return } guard let v = rightView else { return } isAnimating = true delegate?.navigationDrawerController?(navigationDrawerController: self, willClose: .right) UIView.animate(withDuration: TimeInterval(0 == velocity ? animationDuration : fmax(0.1, fmin(1, Double(v.frame.origin.x / velocity)))), animations: { [weak self, v = v] in guard let `self` = self else { return } v.layer.position.x = self.view.bounds.width + v.bounds.width / 2 self.rootViewController.view.alpha = 1 }) { [weak self, v = v] _ in guard let `self` = self else { return } self.hideView(container: v) self.toggleStatusBar() self.isAnimating = false self.isUserInteractionEnabled = true self.delegate?.navigationDrawerController?(navigationDrawerController: self, didClose: .right) } } /// A method that removes the passed in pan and leftView tap gesture recognizers. fileprivate func removeLeftViewGestures() { removeLeftPanGesture() removeLeftTapGesture() } /// Removes the left pan gesture. fileprivate func removeLeftPanGesture() { guard let v = leftPanGesture else { return } view.removeGestureRecognizer(v) leftPanGesture = nil } /// Removes the left tap gesture. fileprivate func removeLeftTapGesture() { guard let v = leftTapGesture else { return } view.removeGestureRecognizer(v) leftTapGesture = nil } /// A method that removes the passed in pan and rightView tap gesture recognizers. fileprivate func removeRightViewGestures() { removeRightPanGesture() removeRightTapGesture() } /// Removes the right pan gesture. fileprivate func removeRightPanGesture() { guard let v = rightPanGesture else { return } view.removeGestureRecognizer(v) rightPanGesture = nil } /// Removes the right tap gesture. fileprivate func removeRightTapGesture() { guard let v = rightTapGesture else { return } view.removeGestureRecognizer(v) rightTapGesture = nil } /// Shows the statusBar. fileprivate func showStatusBar() { Motion.async { [weak self] in guard let v = Application.keyWindow else { return } v.windowLevel = UIWindow.Level.normal guard let `self` = self else { return } self.delegate?.navigationDrawerController?(navigationDrawerController: self, statusBar: false) } } /// Hides the statusBar. fileprivate func hideStatusBar() { guard isHiddenStatusBarEnabled else { return } Motion.async { [weak self] in guard let v = Application.keyWindow else { return } v.windowLevel = UIWindow.Level.statusBar + 1 guard let `self` = self else { return } self.delegate?.navigationDrawerController?(navigationDrawerController: self, statusBar: true) } } /// Toggles the statusBar fileprivate func toggleStatusBar() { if isOpened { hideStatusBar() } else { showStatusBar() } } /** A method that determines whether the passed point is contained within the bounds of the leftViewThreshold and height of the NavigationDrawerController view frame property. - Parameter point: A CGPoint to test against. - Returns: A Boolean of the result, true if yes, false otherwise. */ fileprivate func isPointContainedWithinLeftThreshold(point: CGPoint) -> Bool { return point.x <= leftThreshold } /** A method that determines whether the passed point is contained within the bounds of the rightViewThreshold and height of the NavigationDrawerController view frame property. - Parameter point: A CGPoint to test against. - Returns: A Boolean of the result, true if yes, false otherwise. */ fileprivate func isPointContainedWithinRighThreshold(point: CGPoint) -> Bool { return point.x >= view.bounds.width - rightThreshold } /** A method that determines whether the passed in point is contained within the bounds of the passed in container view. - Parameter container: A UIView that sets the bounds to test against. - Parameter point: A CGPoint to test whether or not it is within the bounds of the container parameter. - Returns: A Boolean of the result, true if yes, false otherwise. */ fileprivate func isPointContainedWithinView(container: UIView, point: CGPoint) -> Bool { return container.bounds.contains(point) } /** A method that shows a view. - Parameter container: A container view. */ fileprivate func showView(container: UIView) { container.depthPreset = depthPreset container.isHidden = false } /** A method that hides a view. - Parameter container: A container view. */ fileprivate func hideView(container: UIView) { container.depthPreset = .none container.isHidden = true } } extension NavigationDrawerController { /// Prepares the contentViewController. fileprivate func prepareContentViewController() { contentViewController.view.backgroundColor = .black prepare(viewController: contentViewController, in: view) view.sendSubviewToBack(contentViewController.view) } /// A method that prepares the leftView. fileprivate func prepareLeftView() { guard let v = leftViewController else { return } isLeftViewEnabled = true leftViewWidth = .phone == Device.userInterfaceIdiom ? 280 : 320 leftView = UIView() leftView!.frame = CGRect(x: 0, y: 0, width: leftViewWidth, height: view.bounds.height) leftView!.backgroundColor = .white view.addSubview(leftView!) leftView!.isHidden = true leftView!.layer.position.x = -leftViewWidth / 2 leftView!.layer.zPosition = 2000 prepare(viewController: v, in: leftView!) } /// A method that prepares the leftView. fileprivate func prepareRightView() { guard let v = rightViewController else { return } isRightViewEnabled = true rightViewWidth = .phone == Device.userInterfaceIdiom ? 280 : 320 rightView = UIView() rightView!.frame = CGRect(x: view.bounds.width, y: 0, width: rightViewWidth, height: view.bounds.height) rightView!.backgroundColor = .white view.addSubview(rightView!) rightView!.isHidden = true rightView!.layer.position.x = view.bounds.width + rightViewWidth / 2 rightView!.layer.zPosition = 2000 prepare(viewController: v, in: rightView!) } /// Prepare the left pan gesture. fileprivate func prepareLeftPanGesture() { guard nil == leftPanGesture else { return } leftPanGesture = UIPanGestureRecognizer(target: self, action: #selector(handleLeftViewPanGesture(recognizer:))) leftPanGesture!.delegate = self leftPanGesture!.cancelsTouchesInView = false view.addGestureRecognizer(leftPanGesture!) } /// Prepare the left tap gesture. fileprivate func prepareLeftTapGesture() { guard nil == leftTapGesture else { return } leftTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleLeftViewTapGesture(recognizer:))) leftTapGesture!.delegate = self leftTapGesture!.cancelsTouchesInView = false view.addGestureRecognizer(leftTapGesture!) } /// Prepares the right pan gesture. fileprivate func prepareRightPanGesture() { guard nil == rightPanGesture else { return } rightPanGesture = UIPanGestureRecognizer(target: self, action: #selector(handleRightViewPanGesture(recognizer:))) rightPanGesture!.delegate = self rightPanGesture!.cancelsTouchesInView = false view.addGestureRecognizer(rightPanGesture!) } /// Prepares the right tap gesture. fileprivate func prepareRightTapGesture() { guard nil == rightTapGesture else { return } rightTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleRightViewTapGesture(recognizer:))) rightTapGesture!.delegate = self rightTapGesture!.cancelsTouchesInView = false view.addGestureRecognizer(rightTapGesture!) } } extension NavigationDrawerController: UIGestureRecognizerDelegate { /** Detects the gesture recognizer being used. - Parameter gestureRecognizer: A UIGestureRecognizer to detect. - Parameter touch: The UITouch event. - Returns: A Boolean of whether to continue the gesture or not. */ @objc open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { if !isRightViewOpened && gestureRecognizer == leftPanGesture && (isLeftViewOpened || isPointContainedWithinLeftThreshold(point: touch.location(in: view))) { return true } if !isLeftViewOpened && gestureRecognizer == rightPanGesture && (isRightViewOpened || isPointContainedWithinRighThreshold(point: touch.location(in: view))) { return true } if isLeftViewOpened && gestureRecognizer == leftTapGesture { return true } if isRightViewOpened && gestureRecognizer == rightTapGesture { return true } return false } /** A method that is fired when the pan gesture is recognized for the leftView. - Parameter recognizer: A UIPanGestureRecognizer that is passed to the handler when recognized. */ @objc fileprivate func handleLeftViewPanGesture(recognizer: UIPanGestureRecognizer) { guard isLeftViewEnabled && (isLeftViewOpened || !isRightViewOpened && isPointContainedWithinLeftThreshold(point: recognizer.location(in: view))) else { return } guard let v = leftView else { return } let point = recognizer.location(in: view) // Animate the panel. switch recognizer.state { case .began: originalX = v.layer.position.x showView(container: v) delegate?.navigationDrawerController?(navigationDrawerController: self, didBeginPanAt: point, position: .left) case .changed: let w = v.bounds.width let translationX = recognizer.translation(in: v).x v.layer.position.x = originalX + translationX > (w / 2) ? (w / 2) : originalX + translationX let a = 1 - v.layer.position.x / v.bounds.width rootViewController.view.alpha = 0.5 < a && v.layer.position.x <= v.bounds.width / 2 ? a : 0.5 if translationX >= leftThreshold { hideStatusBar() } delegate?.navigationDrawerController?(navigationDrawerController: self, didChangePanAt: point, position: .left) case .ended, .cancelled, .failed: let p = recognizer.velocity(in: recognizer.view) let x = p.x >= 1000 || p.x <= -1000 ? p.x : 0 delegate?.navigationDrawerController?(navigationDrawerController: self, didEndPanAt: point, position: .left) if v.frame.origin.x <= -leftViewWidth + leftViewThreshold || x < -1000 { closeLeftView(velocity: x) } else { openLeftView(velocity: x) } case .possible:break default:break } } /** A method that is fired when the pan gesture is recognized for the rightView. - Parameter recognizer: A UIPanGestureRecognizer that is passed to the handler when recognized. */ @objc fileprivate func handleRightViewPanGesture(recognizer: UIPanGestureRecognizer) { guard isRightViewEnabled && (isRightViewOpened || !isLeftViewOpened && isPointContainedWithinRighThreshold(point: recognizer.location(in: view))) else { return } guard let v = rightView else { return } let point = recognizer.location(in: view) // Animate the panel. switch recognizer.state { case .began: originalX = v.layer.position.x showView(container: v) delegate?.navigationDrawerController?(navigationDrawerController: self, didBeginPanAt: point, position: .right) case .changed: let w = v.bounds.width let translationX = recognizer.translation(in: v).x v.layer.position.x = originalX + translationX < view.bounds.width - (w / 2) ? view.bounds.width - (w / 2) : originalX + translationX let a = 1 - (view.bounds.width - v.layer.position.x) / v.bounds.width rootViewController.view.alpha = 0.5 < a && v.layer.position.x >= v.bounds.width / 2 ? a : 0.5 if translationX <= -rightThreshold { hideStatusBar() } delegate?.navigationDrawerController?(navigationDrawerController: self, didChangePanAt: point, position: .right) case .ended, .cancelled, .failed: let p = recognizer.velocity(in: recognizer.view) let x = p.x >= 1000 || p.x <= -1000 ? p.x : 0 delegate?.navigationDrawerController?(navigationDrawerController: self, didEndPanAt: point, position: .right) if v.frame.origin.x >= rightViewThreshold || x > 1000 { closeRightView(velocity: x) } else { openRightView(velocity: x) } case .possible:break default:break } } /** A method that is fired when the tap gesture is recognized for the leftView. - Parameter recognizer: A UITapGestureRecognizer that is passed to the handler when recognized. */ @objc fileprivate func handleLeftViewTapGesture(recognizer: UITapGestureRecognizer) { guard isLeftViewOpened else { return } guard let v = leftView else { return } delegate?.navigationDrawerController?(navigationDrawerController: self, didTapAt: recognizer.location(in: view), position: .left) guard isLeftViewEnabled && isLeftViewOpened && !isPointContainedWithinView(container: v, point: recognizer.location(in: v)) else { return } closeLeftView() } /** A method that is fired when the tap gesture is recognized for the rightView. - Parameter recognizer: A UITapGestureRecognizer that is passed to the handler when recognized. */ @objc fileprivate func handleRightViewTapGesture(recognizer: UITapGestureRecognizer) { guard isRightViewOpened else { return } guard let v = rightView else { return } delegate?.navigationDrawerController?(navigationDrawerController: self, didTapAt: recognizer.location(in: view), position: .right) guard isRightViewEnabled && isRightViewOpened && !isPointContainedWithinView(container: v, point: recognizer.location(in: v)) else { return } closeRightView() } } ================================================ FILE: Sources/iOS/Screen/Screen.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit public struct Screen { /// Retrieves the device bounds. public static var bounds: CGRect { return UIScreen.main.bounds } /// Retrieves the device width. public static var width: CGFloat { return bounds.width } /// Retrieves the device height. public static var height: CGFloat { return bounds.height } /// Retrieves the device scale. public static var scale: CGFloat { return UIScreen.main.scale } } ================================================ FILE: Sources/iOS/SearchBar/SearchBar.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit @objc(SearchBarDelegate) public protocol SearchBarDelegate { /** A delegation method that is executed when the textField changed. - Parameter searchBar: A SearchBar. - Parameter didChange textField: A UITextField. - Parameter with text: An optional String. */ @objc optional func searchBar(searchBar: SearchBar, didChange textField: UITextField, with text: String?) /** A delegation method that is executed when the textField will clear. - Parameter searchBar: A SearchBar. - Parameter willClear textField: A UITextField. - Parameter with text: An optional String. */ @objc optional func searchBar(searchBar: SearchBar, willClear textField: UITextField, with text: String?) /** A delegation method that is executed when the textField is cleared. - Parameter searchBar: A SearchBar. - Parameter didClear textField: A UITextField. - Parameter with text: An optional String. */ @objc optional func searchBar(searchBar: SearchBar, didClear textField: UITextField, with text: String?) } open class SearchBar: Bar { /// The UITextField for the searchBar. @IBInspectable public let textField = UITextField() /// Reference to the clearButton. open fileprivate(set) var clearButton: IconButton! /// A reference to the delegate. open weak var delegate: SearchBarDelegate? /// Handle the clearButton manually. @IBInspectable open var isClearButtonAutoHandleEnabled = true { didSet { clearButton.removeTarget(self, action: #selector(handleClearButton), for: .touchUpInside) if isClearButtonAutoHandleEnabled { clearButton.addTarget(self, action: #selector(handleClearButton), for: .touchUpInside) } } } /// TintColor for searchBar. @IBInspectable open override var tintColor: UIColor? { get { return textField.tintColor } set(value) { textField.tintColor = value } } /// TextColor for searchBar. @IBInspectable open var textColor: UIColor? { get { return textField.textColor } set(value) { textField.textColor = value } } /// Sets the textField placeholder value. @IBInspectable open var placeholder: String? { didSet { if let v = placeholder { textField.attributedPlaceholder = NSAttributedString(string: v, attributes: [.foregroundColor: placeholderColor]) } } } /// Placeholder text @IBInspectable open var placeholderColor = Color.darkText.others { didSet { if let v = placeholder { textField.attributedPlaceholder = NSAttributedString(string: v, attributes: [.foregroundColor: placeholderColor]) } } } /** An initializer that initializes the object with a NSCoder object. - Parameter aDecoder: A NSCoder instance. */ public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } /** An initializer that initializes the object with a CGRect object. If AutoLayout is used, it is better to initilize the instance using the init() initializer. - Parameter frame: A CGRect instance. */ public override init(frame: CGRect) { super.init(frame: frame) } open override func layoutSubviews() { super.layoutSubviews() guard willLayout else { return } layoutTextField() layoutLeftView() layoutClearButton() } open override func prepare() { super.prepare() prepareTextField() prepareClearButton() } } extension SearchBar { /// Layout the textField. open func layoutTextField() { textField.frame = contentView.bounds } /// Layout the leftView. open func layoutLeftView() { guard let v = textField.leftView else { return } let h = textField.frame.height v.frame = CGRect(x: 4, y: 4, width: h, height: h - 8) (v as? UIImageView)?.contentMode = .scaleAspectFit } /// Layout the clearButton. open func layoutClearButton() { let h = textField.frame.height clearButton.frame = CGRect(x: textField.frame.width - h - 4, y: 4, width: h, height: h - 8) } } fileprivate extension SearchBar { /// Clears the textField text. @objc func handleClearButton() { guard nil == textField.delegate?.textFieldShouldClear || true == textField.delegate?.textFieldShouldClear?(textField) else { return } let t = textField.text delegate?.searchBar?(searchBar: self, willClear: textField, with: t) textField.text = nil delegate?.searchBar?(searchBar: self, didClear: textField, with: t) } // Live updates the search results. @objc func handleEditingChanged(textField: UITextField) { delegate?.searchBar?(searchBar: self, didChange: textField, with: textField.text) } } fileprivate extension SearchBar { /// Prepares the textField. func prepareTextField() { textField.contentScaleFactor = Screen.scale textField.font = Theme.font.regular(with: 17) textField.backgroundColor = Color.clear textField.clearButtonMode = .whileEditing textField.addTarget(self, action: #selector(handleEditingChanged(textField:)), for: .editingChanged) tintColor = placeholderColor textColor = Color.darkText.primary placeholder = "Search" contentView.addSubview(textField) } /// Prepares the clearButton. func prepareClearButton() { clearButton = IconButton(image: Icon.cm.close, tintColor: placeholderColor) clearButton.contentEdgeInsets = .zero isClearButtonAutoHandleEnabled = true textField.clearButtonMode = .never textField.rightViewMode = .whileEditing textField.rightView = clearButton } } ================================================ FILE: Sources/iOS/SearchBar/SearchBarController.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit @objc(SearchBarAlignment) public enum SearchBarAlignment: Int { case top case bottom } extension UIViewController { /** A convenience property that provides access to the SearchBarController. This is the recommended method of accessing the SearchBarController through child UIViewControllers. */ public var searchBarController: SearchBarController? { return traverseViewControllerHierarchyForClassType() } } open class SearchBarController: StatusBarController { /// Reference to the SearchBar. @IBInspectable public let searchBar = SearchBar() /// The searchBar alignment. open var searchBarAlignment = SearchBarAlignment.top { didSet { layoutSubviews() } } open override func layoutSubviews() { super.layoutSubviews() layoutSearchBar() layoutContainer() layoutRootViewController() } open override func prepare() { super.prepare() displayStyle = .partial prepareSearchBar() } } fileprivate extension SearchBarController { /// Prepares the searchBar. func prepareSearchBar() { searchBar.layer.zPosition = 1000 searchBar.depthPreset = .depth1 view.addSubview(searchBar) } } fileprivate extension SearchBarController { /// Layout the container. func layoutContainer() { switch displayStyle { case .partial: let p = searchBar.bounds.height let q = statusBarOffsetAdjustment let h = view.bounds.height - p - q switch searchBarAlignment { case .top: container.frame.origin.y = q + p container.frame.size.height = h case .bottom: container.frame.origin.y = q container.frame.size.height = h } container.frame.size.width = view.bounds.width case .full: container.frame = view.bounds } } /// Layout the searchBar. func layoutSearchBar() { searchBar.frame.origin.x = 0 searchBar.frame.origin.y = .top == searchBarAlignment ? statusBarOffsetAdjustment : view.bounds.height - searchBar.bounds.height searchBar.frame.size.width = view.bounds.width } /// Layout the rootViewController. func layoutRootViewController() { rootViewController.view.frame = container.bounds } } ================================================ FILE: Sources/iOS/Snackbar/Snackbar.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit @objc(SnackbarStatus) public enum SnackbarStatus: Int { case visible case hidden } open class Snackbar: Bar { /// A convenience property to set the titleLabel text. open var text: String? { get { return textLabel.text } set(value) { textLabel.text = value layoutSubviews() } } /// A convenience property to set the titleLabel attributedText. open var attributedText: NSAttributedString? { get { return textLabel.attributedText } set(value) { textLabel.attributedText = value layoutSubviews() } } /// Text label. @IBInspectable public let textLabel = UILabel() open override var intrinsicContentSize: CGSize { return CGSize(width: bounds.width, height: 49) } /// The status of the snackbar. open internal(set) var status = SnackbarStatus.hidden open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { for v in subviews { let p = v.convert(point, from: self) if v.bounds.contains(p) { return v.hitTest(p, with: event) } } return super.hitTest(point, with: event) } open override func layoutSubviews() { super.layoutSubviews() guard willLayout else { return } centerViews = [textLabel] } open override func prepare() { super.prepare() depthPreset = .none interimSpacePreset = .interimSpace8 contentEdgeInsets.left = interimSpace contentEdgeInsets.right = interimSpace backgroundColor = Color.grey.darken3 clipsToBounds = false prepareTextLabel() } /// Prepares the textLabel. private func prepareTextLabel() { textLabel.contentScaleFactor = Screen.scale textLabel.font = Theme.font.medium(with: 14) textLabel.textAlignment = .left textLabel.textColor = .white textLabel.numberOfLines = 0 } } ================================================ FILE: Sources/iOS/Snackbar/SnackbarController.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion @objc(SnackbarControllerDelegate) public protocol SnackbarControllerDelegate { /** A delegation method that is executed when a Snackbar will show. - Parameter snackbarController: A SnackbarController. - Parameter snackbar: A Snackbar. */ @objc optional func snackbarController(snackbarController: SnackbarController, willShow snackbar: Snackbar) /** A delegation method that is executed when a Snackbar did show. - Parameter snackbarController: A SnackbarController. - Parameter snackbar: A Snackbar. */ @objc optional func snackbarController(snackbarController: SnackbarController, didShow snackbar: Snackbar) /** A delegation method that is executed when a Snackbar will hide. - Parameter snackbarController: A SnackbarController. - Parameter snackbar: A Snackbar. */ @objc optional func snackbarController(snackbarController: SnackbarController, willHide snackbar: Snackbar) /** A delegation method that is executed when a Snackbar did hide. - Parameter snackbarController: A SnackbarController. - Parameter snackbar: A Snackbar. */ @objc optional func snackbarController(snackbarController: SnackbarController, didHide snackbar: Snackbar) } @objc(SnackbarAlignment) public enum SnackbarAlignment: Int { case top case bottom } extension UIViewController { /** A convenience property that provides access to the SnackbarController. This is the recommended method of accessing the SnackbarController through child UIViewControllers. */ public var snackbarController: SnackbarController? { return traverseViewControllerHierarchyForClassType() } } open class SnackbarController: TransitionController { /// Reference to the Snackbar. public let snackbar = Snackbar() /// A boolean indicating if the Snacbar is animating. open internal(set) var isAnimating = false /// Delegation handler. open weak var delegate: SnackbarControllerDelegate? /// Snackbar alignment setting. open var snackbarAlignment = SnackbarAlignment.bottom /// A preset wrapper around snackbarEdgeInsets. open var snackbarEdgeInsetsPreset = EdgeInsetsPreset.none { didSet { snackbarEdgeInsets = EdgeInsetsPresetToValue(preset: snackbarEdgeInsetsPreset) } } /// A reference to snackbarEdgeInsets. @IBInspectable open var snackbarEdgeInsets = EdgeInsets.zero { didSet { layoutSubviews() } } /** A boolean that controls if layoutEdgeInsets of snackbar is adjusted automatically. */ @IBInspectable open var automaticallyAdjustSnackbarLayoutEdgeInsets = true /** Animates to a SnackbarStatus. - Parameter status: A SnackbarStatus enum value. */ @discardableResult open func animate(snackbar status: SnackbarStatus, delay: TimeInterval = 0, animations: ((Snackbar) -> Void)? = nil, completion: ((Snackbar) -> Void)? = nil) -> MotionCancelBlock? { return Motion.delay(delay) { [weak self, status = status, animations = animations, completion = completion] in guard let s = self else { return } if .visible == status { s.delegate?.snackbarController?(snackbarController: s, willShow: s.snackbar) } else { s.delegate?.snackbarController?(snackbarController: s, willHide: s.snackbar) } s.isAnimating = true s.isUserInteractionEnabled = false UIView.animate(withDuration: 0.25, animations: { [weak self, status = status, animations = animations] in guard let s = self else { return } s.layoutSnackbar(status: status) animations?(s.snackbar) }) { [weak self, status = status, completion = completion] _ in guard let s = self else { return } s.isAnimating = false s.isUserInteractionEnabled = true s.snackbar.status = status s.layoutSubviews() if .visible == status { s.delegate?.snackbarController?(snackbarController: s, didShow: s.snackbar) } else { s.delegate?.snackbarController?(snackbarController: s, didHide: s.snackbar) } completion?(s.snackbar) } } } open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) reload() } open override func layoutSubviews() { super.layoutSubviews() guard !isAnimating else { return } reload() } /// Reloads the view. open func reload() { snackbar.frame.origin.x = snackbarEdgeInsets.left snackbar.frame.size.width = view.bounds.width - snackbarEdgeInsets.left - snackbarEdgeInsets.right snackbar.frame.size.height = snackbar.heightPreset.rawValue if automaticallyAdjustSnackbarLayoutEdgeInsets { snackbar.layoutEdgeInsets = .zero if .bottom == snackbarAlignment { snackbar.frame.size.height += bottomLayoutGuide.length snackbar.layoutEdgeInsets.bottom += bottomLayoutGuide.length } else { snackbar.frame.size.height += topLayoutGuide.length snackbar.layoutEdgeInsets.top += topLayoutGuide.length } rootViewController.view.frame = view.bounds layoutSnackbar(status: snackbar.status) } } open override func prepare() { super.prepare() prepareSnackbar() } /// Prepares the snackbar. private func prepareSnackbar() { snackbar.layer.zPosition = 10000 view.addSubview(snackbar) } /** Lays out the Snackbar. - Parameter status: A SnackbarStatus enum value. */ private func layoutSnackbar(status: SnackbarStatus) { if .bottom == snackbarAlignment { snackbar.frame.origin.y = .visible == status ? view.bounds.height - snackbar.bounds.height - snackbarEdgeInsets.bottom : view.bounds.height } else { snackbar.frame.origin.y = .visible == status ? snackbarEdgeInsets.top : -snackbar.bounds.height } } } ================================================ FILE: Sources/iOS/StatusBar/StatusBarController.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit extension UIViewController { /** A convenience property that provides access to the StatusBarController. This is the recommended method of accessing the StatusBarController through child UIViewControllers. */ public var statusBarController: StatusBarController? { return traverseViewControllerHierarchyForClassType() } } open class StatusBarController: TransitionController { /** A Display value to indicate whether or not to display the rootViewController to the full view bounds, or up to the toolbar height. */ open var displayStyle = DisplayStyle.full { didSet { layoutSubviews() } } /// Device status bar style. open var statusBarStyle: UIStatusBarStyle { get { return Application.statusBarStyle } set(value) { Application.statusBarStyle = value } } /// Device visibility state. open var isStatusBarHidden: Bool { get { return Application.isStatusBarHidden } set(value) { Application.isStatusBarHidden = value statusBar.isHidden = isStatusBarHidden } } /// An adjustment based on the rules for displaying the statusBar. open var statusBarOffsetAdjustment: CGFloat { return Application.shouldStatusBarBeHidden || statusBar.isHidden ? 0 : statusBar.bounds.height } /// A boolean that indicates to hide the statusBar on rotation. open var shouldHideStatusBarOnRotation = false /// A reference to the statusBar. public let statusBar = UIView() open override func layoutSubviews() { super.layoutSubviews() if shouldHideStatusBarOnRotation { statusBar.isHidden = Application.shouldStatusBarBeHidden } statusBar.frame.size.width = view.bounds.width if #available(iOS 11, *) { let v = topLayoutGuide.length statusBar.frame.size.height = 0 < v ? v : 20 } else { statusBar.frame.size.height = 20 } switch displayStyle { case .partial: let h = statusBar.bounds.height container.frame.origin.y = h container.frame.size.height = view.bounds.height - h case .full: container.frame = view.bounds } rootViewController.view.frame = container.bounds container.layer.zPosition = statusBar.layer.zPosition + (Application.shouldStatusBarBeHidden ? 1 : -1) } open override func prepare() { super.prepare() prepareStatusBar() } open override func apply(theme: Theme) { super.apply(theme: theme) statusBar.backgroundColor = theme.primary.darker } } fileprivate extension StatusBarController { /// Prepares the statusBar. func prepareStatusBar() { if nil == statusBar.backgroundColor { statusBar.backgroundColor = .white } view.addSubview(statusBar) } } ================================================ FILE: Sources/iOS/Switch/Switch.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit @objc(SwitchState) public enum SwitchState: Int { case on case off } public enum SwitchSize { case small case medium case large case custom(width: CGFloat, height: CGFloat) } @objc(SwitchDelegate) public protocol SwitchDelegate { /** A Switch delegate method for state changes. - Parameter control: Switch control. - Parameter state: SwitchState value. */ func switchDidChangeState(control: Switch, state: SwitchState) } open class Switch: UIControl, Themeable { /// Will layout the view. open var willLayout: Bool { return 0 < bounds.width && 0 < bounds.height && nil != superview } /// An internal reference to the switchState public property. fileprivate var internalSwitchState = SwitchState.off /// Track thickness. open var trackThickness: CGFloat = 0 { didSet { layoutSubviews() } } /// Button diameter. open var buttonDiameter: CGFloat = 0 { didSet { layoutSubviews() } } /// Position when in the .on state. fileprivate var onPosition: CGFloat = 0 /// Position when in the .off state. fileprivate var offPosition: CGFloat = 0 /// The bounce offset when animating. fileprivate var bounceOffset: CGFloat = 3 /// An Optional delegation method. open weak var delegate: SwitchDelegate? /// Indicates if the animation should bounce. @IBInspectable open var isBounceable = true { didSet { bounceOffset = isBounceable ? 3 : 0 } } /// Button on color. @IBInspectable open var buttonOnColor = Color.clear { didSet { styleForState(state: switchState) } } /// Button off color. @IBInspectable open var buttonOffColor = Color.clear { didSet { styleForState(state: switchState) } } /// Button on image. @IBInspectable open var buttonOnImage: UIImage? { didSet { styleForState(state: switchState) } } /// Button off image. @IBInspectable open var buttonOffImage: UIImage? { didSet { styleForState(state: switchState) } } /// Track on color. @IBInspectable open var trackOnColor = Color.clear { didSet { styleForState(state: switchState) } } /// Track off color. @IBInspectable open var trackOffColor = Color.clear { didSet { styleForState(state: switchState) } } /// Button on disabled color. @IBInspectable open var buttonOnDisabledColor = Color.clear { didSet { styleForState(state: switchState) } } /// Track on disabled color. @IBInspectable open var trackOnDisabledColor = Color.clear { didSet { styleForState(state: switchState) } } /// Button off disabled color. @IBInspectable open var buttonOffDisabledColor = Color.clear { didSet { styleForState(state: switchState) } } /// Track off disabled color. @IBInspectable open var trackOffDisabledColor = Color.clear { didSet { styleForState(state: switchState) } } /// Button on disabled image. @IBInspectable open var buttonOnDisabledImage: UIImage? { didSet { styleForState(state: switchState) } } /// Button off disabled image. @IBInspectable open var buttonOffDisabledImage: UIImage? { didSet { styleForState(state: switchState) } } /// Track view reference. open fileprivate(set) var track: UIView { didSet { prepareTrack() } } /// Button view reference. open fileprivate(set) var button: FABButton { didSet { prepareButton() } } @IBInspectable open override var isEnabled: Bool { didSet { styleForState(state: internalSwitchState) } } /// A boolean indicating if the switch is on or not. @IBInspectable public var isOn: Bool { get { return .on == internalSwitchState } set(value) { updateSwitchState(state: value ? .on : .off, animated: true, isTriggeredByUserInteraction: false) } } /// Switch state. open var switchState: SwitchState { get { return internalSwitchState } set(value) { updateSwitchState(state: value, animated: true, isTriggeredByUserInteraction: false) } } /// Switch size. open var switchSize = SwitchSize.medium { didSet { switch switchSize { case .small: trackThickness = 16 buttonDiameter = 20 case .medium: trackThickness = 20 buttonDiameter = 24 case .large: trackThickness = 24 buttonDiameter = 28 case .custom: break } frame.size = intrinsicContentSize } } open override var intrinsicContentSize: CGSize { switch switchSize { case .small: return CGSize(width: 34, height: 34) case .medium: return CGSize(width: 38, height: 38) case .large: return CGSize(width: 42, height: 42) case .custom(let width, let height): return CGSize(width: width, height: height) } } /** An initializer that initializes the object with a NSCoder object. - Parameter aDecoder: A NSCoder instance. */ public required init?(coder aDecoder: NSCoder) { track = UIView() button = FABButton() super.init(coder: aDecoder) prepare() } /** An initializer that initializes the object with a CGRect object. If AutoLayout is used, it is better to initilize the instance using the init(state:style:size:) initializer, or set the CGRect to CGRectNull. - Parameter frame: A CGRect instance. */ public override init(frame: CGRect) { track = UIView() button = FABButton() super.init(frame: frame) prepare() } /** An initializer that sets the state, style, and size of the Switch instance. - Parameter state: A SwitchState value. - Parameter style: A SwitchStyle value. - Parameter size: A SwitchSize value. */ public init(state: SwitchState = .off, size: SwitchSize = .medium) { track = UIView() button = FABButton() super.init(frame: .zero) prepare() prepareSwitchState(state: state) prepareSwitchSize(size: size) } open override func layoutSubviews() { super.layoutSubviews() guard willLayout else { return } reload() } /// Reloads the view. open func reload() { let w: CGFloat = intrinsicContentSize.width let px: CGFloat = (bounds.width - w) / 2 track.frame = CGRect(x: px, y: (bounds.height - trackThickness) / 2, width: w, height: trackThickness) track.layer.cornerRadius = min(w, trackThickness) / 2 button.frame = CGRect(x: px, y: (bounds.height - buttonDiameter) / 2, width: buttonDiameter, height: buttonDiameter) onPosition = bounds.width - px - buttonDiameter offPosition = px if .on == internalSwitchState { button.frame.origin.x = onPosition } } open override func willMove(toSuperview newSuperview: UIView?) { super.willMove(toSuperview: newSuperview) styleForState(state: internalSwitchState) } /** Toggle the Switch state, if On will be Off, and if Off will be On. - Parameter completion: An Optional completion block. */ open func toggle(completion: ((Switch) -> Void)? = nil) { updateSwitchState(state: .on == internalSwitchState ? .off : .on, animated: true, isTriggeredByUserInteraction: false, completion: completion) } open override func touchesBegan(_ touches: Set, with event: UIEvent?) { guard track.frame.contains(layer.convert(touches.first!.location(in: self), from: layer)) else { return } updateSwitchState(state: .on == internalSwitchState ? .on : .off, animated: true, isTriggeredByUserInteraction: true) } /** Prepares the view instance when intialized. When subclassing, it is recommended to override the prepare method to initialize property values and other setup operations. The super.prepare method should always be called immediately when subclassing. */ open func prepare() { contentScaleFactor = Screen.scale prepareTrack() prepareButton() prepareSwitchState() prepareSwitchSize() applyCurrentTheme() } /** Applies the given theme. - Parameter theme: A Theme. */ open func apply(theme: Theme) { buttonOnColor = theme.secondary trackOnColor = theme.secondary.withAlphaComponent(0.60) buttonOffColor = theme.surface.blend(with: theme.onSurface.withAlphaComponent(0.15).blend(with: theme.secondary.withAlphaComponent(0.06))) trackOffColor = theme.onSurface.withAlphaComponent(0.12) buttonOnDisabledColor = theme.surface.blend(with: theme.onSurface.withAlphaComponent(0.15)) trackOnDisabledColor = theme.onSurface.withAlphaComponent(0.15) buttonOffDisabledColor = buttonOnDisabledColor trackOffDisabledColor = trackOnDisabledColor } } extension Switch { /** Set the switchState property with an option to animate. - Parameter state: The SwitchState to set. - Parameter animated: A Boolean indicating to set the animation or not. - Parameter completion: An Optional completion block. */ open func setSwitchState(state: SwitchState, animated: Bool = true, completion: ((Switch) -> Void)? = nil) { updateSwitchState(state: state, animated: animated, isTriggeredByUserInteraction: false, completion: completion) } } fileprivate extension Switch { /** Set the switchState property with an option to animate. - Parameter state: The SwitchState to set. - Parameter animated: A Boolean indicating to set the animation or not. - Parameter isTriggeredByUserInteraction: A boolean indicating whether the state was changed by a user interaction, true if yes, false otherwise. - Parameter completion: An Optional completion block. */ func updateSwitchState(state: SwitchState, animated: Bool, isTriggeredByUserInteraction: Bool, completion: ((Switch) -> Void)? = nil) { guard isEnabled && internalSwitchState != state else { return } internalSwitchState = state if animated { animateToState(state: state) { [weak self, isTriggeredByUserInteraction = isTriggeredByUserInteraction] _ in guard let s = self else { return } guard isTriggeredByUserInteraction else { completion?(s) return } s.sendActions(for: .valueChanged) completion?(s) s.delegate?.switchDidChangeState(control: s, state: s.internalSwitchState) } } else { button.frame.origin.x = .on == state ? self.onPosition : self.offPosition styleForState(state: state) guard isTriggeredByUserInteraction else { completion?(self) return } sendActions(for: .valueChanged) completion?(self) delegate?.switchDidChangeState(control: self, state: internalSwitchState) } } /** Updates the coloring for the enabled state. - Parameter state: SwitchState. */ func updateColorForState(state: SwitchState) { if .on == state { button.backgroundColor = buttonOnColor track.backgroundColor = trackOnColor button.image = buttonOnImage } else { button.backgroundColor = buttonOffColor track.backgroundColor = trackOffColor button.image = buttonOffImage } } /** Updates the coloring for the disabled state. - Parameter state: SwitchState. */ func updateColorForDisabledState(state: SwitchState) { if .on == state { button.backgroundColor = buttonOnDisabledColor track.backgroundColor = trackOnDisabledColor button.image = buttonOnDisabledImage } else { button.backgroundColor = buttonOffDisabledColor track.backgroundColor = trackOffDisabledColor button.image = buttonOffDisabledImage } } /** Updates the style based on the state. - Parameter state: The SwitchState to set the style to. */ func styleForState(state: SwitchState) { if isEnabled { updateColorForState(state: state) } else { updateColorForDisabledState(state: state) } } } fileprivate extension Switch { /** Set the switchState property with an animate. - Parameter state: The SwitchState to set. - Parameter completion: An Optional completion block. */ func animateToState(state: SwitchState, completion: ((Switch) -> Void)? = nil) { isUserInteractionEnabled = false UIView.animate(withDuration: 0.15, delay: 0.05, options: [.curveEaseIn, .curveEaseOut], animations: { [weak self] in guard let s = self else { return } s.button.frame.origin.x = .on == state ? s.onPosition + s.bounceOffset : s.offPosition - s.bounceOffset s.styleForState(state: state) }) { [weak self] _ in UIView.animate(withDuration: 0.15, animations: { [weak self] in guard let s = self else { return } s.button.frame.origin.x = .on == state ? s.onPosition : s.offPosition }) { [weak self] _ in guard let s = self else { return } s.isUserInteractionEnabled = true completion?(s) } } } } fileprivate extension Switch { /** Handle the TouchUpOutside and TouchCancel moments. - Parameter sender: A UIButton. - Parameter event: A UIEvent. */ @objc func handleTouchUpOutsideOrCanceled(sender: FABButton, event: UIEvent) { guard let v = event.touches(for: sender)?.first else { return } let q: CGFloat = sender.frame.origin.x + v.location(in: sender).x - v.previousLocation(in: sender).x updateSwitchState(state: q > (bounds.width - button.bounds.width) / 2 ? .on : .off, animated: true, isTriggeredByUserInteraction: true) } /// Handles the TouchUpInside event. @objc func handleTouchUpInside() { updateSwitchState(state: isOn ? .off : .on, animated: true, isTriggeredByUserInteraction: true) } /** Handle the TouchDragInside event. - Parameter sender: A UIButton. - Parameter event: A UIEvent. */ @objc func handleTouchDragInside(sender: FABButton, event: UIEvent) { guard let v = event.touches(for: sender)?.first else { return } let q: CGFloat = max(min(sender.frame.origin.x + v.location(in: sender).x - v.previousLocation(in: sender).x, onPosition), offPosition) guard q != sender.frame.origin.x else { return } sender.frame.origin.x = q } } fileprivate extension Switch { /// Prepares the track. func prepareTrack() { addSubview(track) } /// Prepares the button. func prepareButton() { button.addTarget(self, action: #selector(handleTouchUpInside), for: .touchUpInside) button.addTarget(self, action: #selector(handleTouchDragInside), for: .touchDragInside) button.addTarget(self, action: #selector(handleTouchUpOutsideOrCanceled), for: .touchCancel) button.addTarget(self, action: #selector(handleTouchUpOutsideOrCanceled), for: .touchUpOutside) addSubview(button) } /** Prepares the switchState property. This is used mainly to allow init to set the state value and have an effect. - Parameter state: The SwitchState to set. */ func prepareSwitchState(state: SwitchState = .off) { updateSwitchState(state: state, animated: false, isTriggeredByUserInteraction: false) } /** Prepares the switchSize property. This is used mainly to allow init to set the size value and have an effect. - Parameter size: The SwitchSize to set. */ func prepareSwitchSize(size: SwitchSize = .medium) { switchSize = size } } ================================================ FILE: Sources/iOS/Tab/TabBar.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion open class TabItem: FlatButton { /// A dictionary of TabItemStates to UIColors for states. fileprivate var colorForState = [TabItemState: UIColor]() /// A dictionary of TabItemStates to UIImages for states. fileprivate var imageForState = [TabItemState: UIImage]() /// Sets the normal and highlighted image for the button. open override var image: UIImage? { didSet { setTabItemImage(image, for: .normal) setTabItemImage(image, for: .selected) setTabItemImage(image, for: .highlighted) super.image = image } } open override func prepare() { super.prepare() pulseAnimation = .none prepareImages() prepareColors() updateColors() } } fileprivate extension TabItem { /// Prepares the tabsItems images. func prepareImages() { imageForState[.normal] = image imageForState[.selected] = image imageForState[.highlighted] = image } /// Prepares the tabsItems colors. func prepareColors() { colorForState[.normal] = Color.grey.base colorForState[.selected] = Color.blue.base colorForState[.highlighted] = Color.blue.base } } fileprivate extension TabItem { /// Updates the tabItems colors. func updateColors() { let normalColor = colorForState[.normal]! let selectedColor = colorForState[.selected]! let highlightedColor = colorForState[.highlighted]! setTitleColor(normalColor, for: .normal) setImage(imageForState[.normal]?.tint(with: normalColor), for: .normal) setTitleColor(selectedColor, for: .selected) setImage(imageForState[.selected]?.tint(with: selectedColor), for: .selected) setTitleColor(highlightedColor, for: .highlighted) setImage(imageForState[.highlighted]?.tint(with: highlightedColor), for: .highlighted) } } extension TabItem { /** Retrieves the tabItem color for a given state. - Parameter for state: A TabItemState. - Returns: A UIColor. */ open func getTabItemColor(for state: TabItemState) -> UIColor { return colorForState[state]! } /** Sets the color for the tabItem given a TabItemState. - Parameter _ color: A UIColor. - Parameter for state: A TabItemState. */ open func setTabItemColor(_ color: UIColor, for state: TabItemState) { colorForState[state] = color updateColors() } /** Retrieves the tabItem image for a given state. - Parameter for state: A TabItemState. - Returns: An optional UIImage. */ open func getTabItemImage(for state: TabItemState) -> UIImage? { return imageForState[state] } /** Sets the image for the tabItem given a TabItemState. - Parameter _ image: An optional UIImage. - Parameter for state: A TabItemState. */ open func setTabItemImage(_ image: UIImage?, for state: TabItemState) { imageForState[state] = image updateColors() } } @objc(TabItemState) public enum TabItemState: Int { case normal case highlighted case selected } @objc(TabItemLineState) public enum TabItemLineState: Int { case selected } @objc(TabBarLineAlignment) public enum TabBarLineAlignment: Int { case top case bottom } @objc(TabBarDelegate) public protocol TabBarDelegate { /** A delegation method that is executed to determine if the TabBar should transition to the next tab. - Parameter tabBar: A TabBar. - Parameter tabItem: A TabItem. - Returns: A Boolean. */ @objc optional func tabBar(tabBar: TabBar, shouldSelect tabItem: TabItem) -> Bool /** A delegation method that is executed when the tabItem will trigger the animation to the next tab. - Parameter tabBar: A TabBar. - Parameter tabItem: A TabItem. */ @objc optional func tabBar(tabBar: TabBar, willSelect tabItem: TabItem) /** A delegation method that is executed when the tabItem did complete the animation to the next tab. - Parameter tabBar: A TabBar. - Parameter tabItem: A TabItem. */ @objc optional func tabBar(tabBar: TabBar, didSelect tabItem: TabItem) } @objc(_TabBarDelegate) internal protocol _TabBarDelegate { /** A delegation method that is executed to determine if the TabBar should transition to the next tab. - Parameter tabBar: A TabBar. - Parameter tabItem: A TabItem. - Returns: A Boolean. */ func _tabBar(tabBar: TabBar, shouldSelect tabItem: TabItem) -> Bool } @objc(TabBarStyle) public enum TabBarStyle: Int { case auto case nonScrollable case scrollable } public enum TabBarCenteringStyle { case never case auto case always } public enum TabBarLineStyle { case auto case fixed(CGFloat) case custom((TabItem) -> CGFloat) } open class TabBar: Bar { /// A dictionary of TabItemLineStates to UIColors for the line. fileprivate var lineColorForState = [TabItemLineState: UIColor]() /// Only for inital load to get the line animation correct. fileprivate var shouldNotAnimateLineView = false /// The total width of the tabItems. fileprivate var tabItemsTotalWidth: CGFloat { var w: CGFloat = 0 let q = 2 * tabItemsInterimSpace let p = q + tabItemsInterimSpace for v in tabItems { let x = v.sizeThatFits(CGSize(width: .greatestFiniteMagnitude, height: scrollView.bounds.height)).width w += x w += p } w -= tabItemsInterimSpace return w } /// An enum that determines the tab bar style. open var tabBarStyle = TabBarStyle.auto { didSet { layoutSubviews() } } /// An enum that determines the tab bar centering style. open var tabBarCenteringStyle = TabBarCenteringStyle.always { didSet { layoutSubviews() } } /// An enum that determines the tab bar items style. open var tabBarLineStyle = TabBarLineStyle.auto { didSet { layoutSubviews() } } /// A reference to the scroll view when the tab bar style is scrollable. public let scrollView = UIScrollView() /// Enables and disables bouncing when swiping. open var isScrollBounceEnabled: Bool { get { return scrollView.bounces } set(value) { scrollView.bounces = value } } /// A delegation reference. open weak var delegate: TabBarDelegate? internal weak var _delegate: _TabBarDelegate? /// The currently selected tabItem. open internal(set) var selectedTabItem: TabItem? { didSet { oldValue?.isSelected = false selectedTabItem?.isSelected = true updateScrollView() } } /// A preset wrapper around tabItems contentEdgeInsets. open var tabItemsContentEdgeInsetsPreset: EdgeInsetsPreset { get { return contentView.grid.contentEdgeInsetsPreset } set(value) { contentView.grid.contentEdgeInsetsPreset = value } } /// A reference to EdgeInsets. @IBInspectable open var tabItemsContentEdgeInsets: EdgeInsets { get { return contentView.grid.contentEdgeInsets } set(value) { contentView.grid.contentEdgeInsets = value } } /// A preset wrapper around tabItems interimSpace. open var tabItemsInterimSpacePreset: InterimSpacePreset { get { return contentView.grid.interimSpacePreset } set(value) { contentView.grid.interimSpacePreset = value } } /// A wrapper around tabItems interimSpace. @IBInspectable open var tabItemsInterimSpace: InterimSpace { get { return contentView.grid.interimSpace } set(value) { contentView.grid.interimSpace = value } } /// TabItems. @objc open var tabItems = [TabItem]() { didSet { oldValue.forEach { $0.removeFromSuperview() } prepareTabItems() layoutSubviews() } } /// A reference to the line UIView. public let line = UIView() /// A value for the line alignment. @objc open var lineAlignment = TabBarLineAlignment.bottom { didSet { layoutSubviews() } } /// The line height. @objc open var lineHeight: CGFloat { get { return line.bounds.height } set(value) { line.frame.size.height = value } } /// The line color. @objc open var lineColor: UIColor { get { return lineColorForState[.selected]! } set(value) { setLineColor(value, for: .selected) } } open override func layoutSubviews() { super.layoutSubviews() guard willLayout else { return } layoutScrollView() layoutLine() updateScrollView() } open override func prepare() { super.prepare() contentEdgeInsetsPreset = .none interimSpacePreset = .interimSpace6 tabItemsInterimSpacePreset = .interimSpace4 prepareContentView() prepareScrollView() prepareDivider() prepareLine() prepareLineColor() updateLineColors() } } fileprivate extension TabBar { // Prepares the line. func prepareLine() { line.layer.zPosition = 10000 lineHeight = 3 scrollView.addSubview(line) } /// Prepares the divider. func prepareDivider() { dividerColor = Color.grey.lighten2 dividerAlignment = .top } /// Prepares the tabItems. func prepareTabItems() { shouldNotAnimateLineView = true for v in tabItems { v.grid.columns = 0 v.contentEdgeInsets = .zero prepareTabItemHandler(tabItem: v) } selectedTabItem = tabItems.first } /// Prepares the line colors. func prepareLineColor() { lineColorForState[.selected] = Color.blue.base } /** Prepares the tabItem animation handler. - Parameter tabItem: A TabItem. */ func prepareTabItemHandler(tabItem: TabItem) { removeTabItemHandler(tabItem: tabItem) tabItem.addTarget(self, action: #selector(handleTabItemsChange(tabItem:)), for: .touchUpInside) } /// Prepares the contentView. func prepareContentView() { contentView.layer.zPosition = 6000 } /// Prepares the scroll view. func prepareScrollView() { scrollView.showsVerticalScrollIndicator = false scrollView.showsHorizontalScrollIndicator = false centerViews = [scrollView] } } fileprivate extension TabBar { /// Layout the scrollView. func layoutScrollView() { contentView.grid.reload() if .scrollable == tabBarStyle || (.auto == tabBarStyle && tabItemsTotalWidth > scrollView.bounds.width) { var w: CGFloat = 0 let q = 2 * tabItemsInterimSpace let p = q + tabItemsInterimSpace for v in tabItems { v.sizeToFit() let x = v.sizeThatFits(CGSize(width: .greatestFiniteMagnitude, height: scrollView.bounds.height)).width v.frame.size.height = scrollView.bounds.height v.frame.size.width = x + q v.frame.origin.x = w w += x w += p if scrollView != v.superview { scrollView.addSubview(v) } } w -= tabItemsInterimSpace scrollView.contentSize = CGSize(width: w, height: scrollView.bounds.height) } else { scrollView.grid.begin() scrollView.grid.views = tabItems scrollView.grid.axis.columns = tabItems.count scrollView.grid.contentEdgeInsets = tabItemsContentEdgeInsets scrollView.grid.interimSpace = tabItemsInterimSpace scrollView.grid.commit() scrollView.contentSize = scrollView.frame.size } } /// Layout the line view. func layoutLine() { guard let v = selectedTabItem else { return } guard shouldNotAnimateLineView else { let f = lineFrame(for: v, forMotion: true) line.animate(.duration(0), .size(f.size), .position(f.origin)) return } line.frame = lineFrame(for: v) shouldNotAnimateLineView = false } func lineFrame(for tabItem: TabItem, forMotion: Bool = false) -> CGRect { let y = .bottom == lineAlignment ? scrollView.bounds.height - (forMotion ? lineHeight / 2 : lineHeight) : (forMotion ? lineHeight / 2 : 0) let w: CGFloat = { switch tabBarLineStyle { case .auto: return tabItem.bounds.width case .fixed(let w): return w case .custom(let closure): return closure(tabItem) } }() let x = forMotion ? tabItem.center.x : (tabItem.frame.origin.x + (tabItem.bounds.width - w) / 2) return CGRect(x: x, y: y, width: w, height: lineHeight) } } extension TabBar { /** Retrieves the tabItem color for a given state. - Parameter for state: A TabItemState. - Returns: A optional UIColor. */ open func getTabItemColor(for state: TabItemState) -> UIColor? { return tabItems.first?.getTabItemColor(for: state) } /** Sets the color for the tabItem given a TabItemState. - Parameter _ color: A UIColor. - Parameter for state: A TabItemState. */ open func setTabItemsColor(_ color: UIColor, for state: TabItemState) { for v in tabItems { v.setTabItemColor(color, for: state) } } /** Retrieves the line color for a given state. - Parameter for state: A TabItemLineState. - Returns: A UIColor. */ open func getLineColor(for state: TabItemLineState) -> UIColor { return lineColorForState[state]! } /** Sets the color for the line given a TabItemLineState. - Parameter _ color: A UIColor. - Parameter for state: A TabItemLineState. */ open func setLineColor(_ color: UIColor, for state: TabItemLineState) { lineColorForState[state] = color updateLineColors() } } internal extension TabBar { /** Starts line transition for the index with the given duration. - Parameter for index: An Int. - Parameter duration: A TimeInterval. */ func startLineTransition(for index: Int, duration: TimeInterval = 0.35) { guard let s = selectedTabItem, let currentIndex = tabItems.firstIndex(of: s) else { return } guard currentIndex != index else { return } let targetFrame = lineFrame(for: tabItems[index], forMotion: true) line.transition(.size(targetFrame.size), .position(targetFrame.origin), .duration(duration)) line.motionViewTransition.start() } /** Updates line transition to the given progress value. - Parameter _ progress: A CGFloat. */ func updateLineTransition(_ progress: CGFloat) { line.motionViewTransition.update(progress) } /** Finishes line transition. - Parameter isAnimated: A Boolean indicating if the change should be animated. */ func finishLineTransition(isAnimated: Bool = true) { line.motionViewTransition.finish(isAnimated: isAnimated) line.transition([]) } /** Cancels line transition. - Parameter isAnimated: A Boolean indicating if the change should be animated. */ func cancelLineTransition(isAnimated: Bool = true) { line.motionViewTransition.cancel(isAnimated: isAnimated) line.transition([]) } } fileprivate extension TabBar { /** Removes the tabItem animation handler. - Parameter tabItem: A TabItem. */ func removeTabItemHandler(tabItem: TabItem) { tabItem.removeTarget(self, action: #selector(handleTabItemsChange(tabItem:)), for: .touchUpInside) } } fileprivate extension TabBar { /// Handles the tabItem touch event. @objc func handleTabItemsChange(tabItem: TabItem) { guard !(false == delegate?.tabBar?(tabBar: self, shouldSelect: tabItem)) else { return } guard !(false == _delegate?._tabBar(tabBar: self, shouldSelect: tabItem)) else { return } animate(to: tabItem, isTriggeredByUserInteraction: true) } } extension TabBar { /** Selects a given index from the tabItems array. - Parameter at index: An Int. - Paramater completion: An optional completion block. */ @objc open func select(at index: Int, completion: ((TabItem) -> Void)? = nil) { guard -1 < index, index < tabItems.count else { return } animate(to: tabItems[index], isTriggeredByUserInteraction: false, completion: completion) } /** Animates to a given tabItem. - Parameter to tabItem: A TabItem. - Parameter completion: An optional completion block. */ open func animate(to tabItem: TabItem, completion: ((TabItem) -> Void)? = nil) { animate(to: tabItem, isTriggeredByUserInteraction: false, completion: completion) } } fileprivate extension TabBar { /// Updates the line colors. func updateLineColors() { line.backgroundColor = lineColorForState[.selected] } } fileprivate extension TabBar { /** Animates to a given tabItem. - Parameter to tabItem: A TabItem. - Parameter isTriggeredByUserInteraction: A boolean indicating whether the state was changed by a user interaction, true if yes, false otherwise. - Parameter completion: An optional completion block. */ func animate(to tabItem: TabItem, isTriggeredByUserInteraction: Bool, completion: ((TabItem) -> Void)? = nil) { if isTriggeredByUserInteraction { delegate?.tabBar?(tabBar: self, willSelect: tabItem) } selectedTabItem = tabItem let f = lineFrame(for: tabItem, forMotion: true) line.animate(.duration(0.25), .size(f.size), .position(f.origin), .completion({ [weak self, isTriggeredByUserInteraction = isTriggeredByUserInteraction, tabItem = tabItem, completion = completion] in guard let `self` = self else { return } if isTriggeredByUserInteraction { self.delegate?.tabBar?(tabBar: self, didSelect: tabItem) } completion?(tabItem) })) updateScrollView() } } fileprivate extension TabBar { /// Updates the scrollView. func updateScrollView() { guard let v = selectedTabItem else { return } let contentOffsetX: CGFloat? = { let shouldScroll = !scrollView.bounds.contains(v.frame) switch tabBarCenteringStyle { case .auto: guard shouldScroll else { return nil } fallthrough case .always: return v.center.x - bounds.width / 2 case .never: guard shouldScroll else { return nil } return v.frame.origin.x < scrollView.bounds.minX ? v.frame.origin.x : v.frame.maxX - scrollView.bounds.width } }() if let x = contentOffsetX { let normalizedOffsetX = min(max(x, 0), scrollView.contentSize.width - scrollView.bounds.width) scrollView.setContentOffset(CGPoint(x: normalizedOffsetX, y: 0), animated: true) } } } ================================================ FILE: Sources/iOS/Tab/TabsController.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion fileprivate var TabItemKey: UInt8 = 0 @objc(TabBarAlignment) public enum TabBarAlignment: Int { case top case bottom } public enum TabBarThemingStyle { case auto case primary case secondary } extension UIViewController { /// TabItem reference. @objc open private(set) var tabItem: TabItem { get { return AssociatedObject.get(base: self, key: &TabItemKey) { return TabItem() } } set(value) { AssociatedObject.set(base: self, key: &TabItemKey, value: value) } } } extension UIViewController { /** A convenience property that provides access to the TabsController. This is the recommended method of accessing the TabsController through child UIViewControllers. */ public var tabsController: TabsController? { return traverseViewControllerHierarchyForClassType() } } @objc(TabsControllerDelegate) public protocol TabsControllerDelegate { /** A delegation method that is executed to determine if the TabsController should transition to the next view controller. - Parameter tabBar: A TabsController. - Parameter tabItem: A TabItem. - Returns: A Boolean. */ @objc optional func tabsController(tabsController: TabsController, shouldSelect viewController: UIViewController) -> Bool /** A delegation method that is executed when the view controller will transitioned to. - Parameter tabsController: A TabsController. - Parameter viewController: A UIViewController. */ @objc optional func tabsController(tabsController: TabsController, willSelect viewController: UIViewController) /** A delegation method that is executed when the view controller has been transitioned to. - Parameter tabsController: A TabsController. - Parameter viewController: A UIViewController. */ @objc optional func tabsController(tabsController: TabsController, didSelect viewController: UIViewController) /** A delegation method that is executed when the interactive transition to view controller will be cancelled. - Parameter tabsController: A TabsController. - Parameter viewController: A UIViewController. */ @objc optional func tabsController(tabsController: TabsController, willCancelSelecting viewController: UIViewController) /** A delegation method that is executed when the interactive transition to view controller has been cancelled. - Parameter tabsController: A TabsController. - Parameter viewController: A UIViewController. */ @objc optional func tabsController(tabsController: TabsController, didCancelSelecting viewController: UIViewController) } open class TabsController: TransitionController { /** A Display value to indicate whether or not to display the rootViewController to the full view bounds, or up to the toolbar height. */ open var displayStyle = DisplayStyle.partial { didSet { layoutSubviews() } } /// The TabBar used to switch between view controllers. @IBInspectable public let tabBar = TabBar() /// A Boolean that controls if the swipe feature is enabled. open var isSwipeEnabled = true { didSet { guard isSwipeEnabled else { removeSwipeGesture() return } prepareSwipeGesture() } } /// A delegation reference. open weak var delegate: TabsControllerDelegate? /// An Array of UIViewControllers. open var viewControllers: [UIViewController] { didSet { selectedIndex = 0 prepareSelectedIndexViewController() prepareTabBar() layoutSubviews() } } /// A reference to the currently selected view controller index value. @IBInspectable open fileprivate(set) var selectedIndex = 0 /// The tabBar alignment. open var tabBarAlignment = TabBarAlignment.bottom { didSet { updateTabBarAlignment() layoutSubviews() } } /// The tabBar theming style. open var tabBarThemingStyle = TabBarThemingStyle.auto /** A UIPanGestureRecognizer property internally used for the interactive swipe. */ public private(set) var interactiveSwipeGesture: UIPanGestureRecognizer? /** A private integer for storing index of target view controller during interactive transition. */ private var targetIndex = -1 /** An initializer that initializes the object with a NSCoder object. - Parameter aDecoder: A NSCoder instance. */ public required init?(coder aDecoder: NSCoder) { viewControllers = [] super.init(coder: aDecoder) } /** An initializer that accepts an Array of UIViewControllers. - Parameter viewControllers: An Array of UIViewControllers. */ public init(viewControllers: [UIViewController], selectedIndex: Int = 0) { self.viewControllers = viewControllers self.selectedIndex = selectedIndex super.init(nibName: nil, bundle: nil) } fileprivate override init(rootViewController: UIViewController) { self.viewControllers = [] super.init(rootViewController: rootViewController) } open override func layoutSubviews() { super.layoutSubviews() layoutTabBar() layoutContainer() layoutRootViewController() } open override func prepare() { super.prepare() view.backgroundColor = .white view.contentScaleFactor = Screen.scale isSwipeEnabled = true prepareTabBar() prepareTabItems() prepareSelectedIndexViewController() applyCurrentTheme() } open override func transition(to viewController: UIViewController, completion: ((Bool) -> Void)?) { transition(to: viewController, isTriggeredByUserInteraction: false, completion: completion) } open override func apply(theme: Theme) { super.apply(theme: theme) switch tabBarThemingStyle { case .auto where (parent is NavigationController || parent is ToolbarController) && tabBarAlignment == .top: fallthrough case .primary: applyPrimary(theme: theme) default: applySecondary(theme: theme) } } } private extension TabsController { /** Applies theming taking primary color as base. - Parameter theme: A Theme */ func applyPrimary(theme: Theme) { tabBar.lineColor = theme.onPrimary.withAlphaComponent(0.68) tabBar.backgroundColor = theme.primary tabBar.setTabItemsColor(theme.onPrimary, for: .normal) tabBar.setTabItemsColor(theme.onPrimary, for: .selected) tabBar.setTabItemsColor(theme.onPrimary, for: .highlighted) tabBar.isDividerHidden = true } /** Applies theming taking secondary color as base. - Parameter theme: A Theme */ func applySecondary(theme: Theme) { tabBar.lineColor = theme.secondary tabBar.backgroundColor = theme.background tabBar.setTabItemsColor(theme.onSurface.withAlphaComponent(0.60), for: .normal) tabBar.setTabItemsColor(theme.secondary, for: .selected) tabBar.setTabItemsColor(theme.secondary, for: .highlighted) tabBar.dividerColor = theme.onSurface.withAlphaComponent(0.12) } } fileprivate extension TabsController { /** Transitions to the given view controller. - Parameter to viewController: A UIViewController. - Parameter isTriggeredByUserInteraction: A Boolean. - Parameter completion: An optional completion block. */ func transition(to viewController: UIViewController, isTriggeredByUserInteraction: Bool, completion: ((Bool) -> Void)?) { guard let fvcIndex = viewControllers.firstIndex(of: rootViewController) else { return } guard let tvcIndex = viewControllers.firstIndex(of: viewController) else { return } if case .auto = motionTransitionType, case .auto = viewController.motionTransitionType { MotionTransition.shared.setAnimationForNextTransition(fvcIndex < tvcIndex ? .slide(direction: .left) : .slide(direction: .right)) } if isTriggeredByUserInteraction { delegate?.tabsController?(tabsController: self, willSelect: viewController) } super.transition(to: viewController) { [weak self] (isFinishing) in guard let `self` = self else { return } completion?(isFinishing) if isTriggeredByUserInteraction && isFinishing { self.delegate?.tabsController?(tabsController: self, didSelect: viewController) } else { self.delegate?.tabsController?(tabsController: self, didCancelSelecting: viewController) } } } } fileprivate extension TabsController { /// Prepares the view controller at the selectedIndex. func prepareSelectedIndexViewController() { rootViewController = viewControllers[selectedIndex] } /// Prepares the TabBar. func prepareTabBar() { tabBar._delegate = self view.addSubview(tabBar) updateTabBarAlignment() } func updateTabBarAlignment() { tabBar.lineAlignment = .bottom == tabBarAlignment ? .top : .bottom tabBar.dividerAlignment = .bottom == tabBarAlignment ? .top : .bottom } /// Prepares the `tabBar.tabItems`. func prepareTabItems() { var tabItems = [TabItem]() for v in viewControllers { // Expectation that viewDidLoad() triggers update of tabItem: if #available(iOS 9.0, *) { v.loadViewIfNeeded() } else { _ = v.view } tabItems.append(v.tabItem) } tabBar.tabItems = tabItems tabBar.selectedTabItem = tabItems[selectedIndex] } } private extension TabsController { /** A target method contolling interactive swipe transition based on gesture recognizer. - Parameter _ gesture: A UIPanGestureRecognizer. */ @objc func handleTransitionPan(_ gesture: UIPanGestureRecognizer) { let translationX = gesture.translation(in: nil).x let velocityX = gesture.velocity(in: nil).x switch gesture.state { case .began, .changed: let isSlidingLeft = targetIndex == -1 ? velocityX < 0 : translationX < 0 let nextIndex = selectedIndex + (isSlidingLeft ? 1 : -1) guard nextIndex >= 0, nextIndex < viewControllers.count else { return } if targetIndex != nextIndex { /// 5 point threshold guard abs(translationX) > 5 else { return } if targetIndex != -1 { delegate?.tabsController?(tabsController: self, willCancelSelecting: viewControllers[targetIndex]) tabBar.cancelLineTransition(isAnimated: false) MotionTransition.shared.cancel(isAnimated: false) } if internalSelect(at: nextIndex, isTriggeredByUserInteraction: true, selectTabItem: false) { tabBar.startLineTransition(for: nextIndex, duration: 0.35) targetIndex = nextIndex } } else { let progress = abs(translationX / view.bounds.width) tabBar.updateLineTransition(progress) MotionTransition.shared.update(Double(progress)) } default: guard targetIndex != -1 else { return } let progress = (translationX + velocityX) / view.bounds.width let isUserHandDirectionLeft = progress < 0 let isTargetHandDirectionLeft = targetIndex > selectedIndex if isUserHandDirectionLeft == isTargetHandDirectionLeft && abs(progress) > 0.5 { tabBar.finishLineTransition() MotionTransition.shared.finish() } else { tabBar.cancelLineTransition() MotionTransition.shared.cancel() delegate?.tabsController?(tabsController: self, willCancelSelecting: viewControllers[targetIndex]) } targetIndex = -1 } } /// Prepares interactiveSwipeGesture. func prepareSwipeGesture() { guard nil == interactiveSwipeGesture else { return } interactiveSwipeGesture = UIPanGestureRecognizer(target: self, action: #selector(handleTransitionPan)) container.addGestureRecognizer(interactiveSwipeGesture!) } /// Removes interactiveSwipeGesture. func removeSwipeGesture() { guard let v = interactiveSwipeGesture else { return } container.removeGestureRecognizer(v) interactiveSwipeGesture = nil } } fileprivate extension TabsController { /// Layout the container. func layoutContainer() { switch displayStyle { case .partial: let p = tabBar.bounds.height let y = view.bounds.height - p switch tabBarAlignment { case .top: container.frame.origin.y = p container.frame.size.height = y case .bottom: container.frame.origin.y = 0 container.frame.size.height = y } container.frame.size.width = view.bounds.width case .full: container.frame = view.bounds } } /// Layout the tabBar. func layoutTabBar() { if #available(iOS 11, *) { if .bottom == tabBarAlignment { let v = bottomLayoutGuide.length if 0 < v { tabBar.heightPreset = { tabBar.heightPreset }() tabBar.frame.size.height += v tabBar.grid.layoutEdgeInsets.bottom = v } } } tabBar.frame.origin.x = 0 tabBar.frame.origin.y = .top == tabBarAlignment ? 0 : view.bounds.height - tabBar.bounds.height tabBar.frame.size.width = view.bounds.width } /// Layout the rootViewController. func layoutRootViewController() { rootViewController.view.frame = container.bounds } } extension TabsController { /** Transitions to the view controller that is at the given index. - Parameter at index: An Int. */ open func select(at index: Int) { internalSelect(at: index, isTriggeredByUserInteraction: false, selectTabItem: true) } /** Transitions to the view controller that is at the given index. - Parameter at index: An Int. - Parameter isTriggeredByUserInteraction: A boolean indicating whether the state was changed by a user interaction, true if yes, false otherwise. - Returns: A boolean indicating whether the transition will take place. */ @discardableResult private func internalSelect(at index: Int, isTriggeredByUserInteraction: Bool, selectTabItem: Bool) -> Bool { guard index != selectedIndex else { return false } if isTriggeredByUserInteraction { guard !(false == delegate?.tabsController?(tabsController: self, shouldSelect: viewControllers[index])) else { return false } } if selectTabItem { tabBar.select(at: index) } transition(to: viewControllers[index], isTriggeredByUserInteraction: isTriggeredByUserInteraction) { [weak self] (isFinishing) in guard isFinishing else { return } self?.selectedIndex = index self?.tabBar.selectedTabItem = self?.tabBar.tabItems[index] } return true } } extension TabsController: _TabBarDelegate { @objc func _tabBar(tabBar: TabBar, shouldSelect tabItem: TabItem) -> Bool { guard let i = tabBar.tabItems.firstIndex(of: tabItem) else { return false } return internalSelect(at: i, isTriggeredByUserInteraction: true, selectTabItem: false) } } ================================================ FILE: Sources/iOS/Table/TableView.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit open class TableView: UITableView { /** An initializer that initializes the object with a NSCoder object. - Parameter aDecoder: A NSCoder instance. */ public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) prepare() } public override init(frame: CGRect, style: UITableView.Style) { super.init(frame: frame, style: style) prepare() } /** An initializer that initializes the object. - Parameter frame: A CGRect defining the view's frame. */ public convenience init(frame: CGRect) { self.init(frame: frame, style: .plain) } /** Prepares the view instance when intialized. When subclassing, it is recommended to override the prepare method to initialize property values and other setup operations. The super.prepare method should always be called immediately when subclassing. */ open func prepare() { backgroundColor = .white contentScaleFactor = Screen.scale separatorStyle = .none register(TableViewCell.self, forCellReuseIdentifier: "TableViewCell") } } ================================================ FILE: Sources/iOS/Table/TableViewCell.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion open class TableViewCell: UITableViewCell, Pulseable, PulseableLayer { /** A CAShapeLayer used to manage elements that would be affected by the clipToBounds property of the backing layer. For example, this allows the dropshadow effect on the backing layer, while clipping the image to a desired shape within the visualLayer. */ public let visualLayer = CAShapeLayer() /// A Pulse reference. internal var pulse: Pulse! /// A reference to the pulse layer. internal var pulseLayer: CALayer? { return pulse.pulseLayer } /// PulseAnimation value. open var pulseAnimation: PulseAnimation { get { return pulse.animation } set(value) { pulse.animation = value } } /// PulseAnimation color. @IBInspectable open var pulseColor: UIColor { get { return pulse.color } set(value) { pulse.color = value } } /// Pulse opacity. @IBInspectable open var pulseOpacity: CGFloat { get { return pulse.opacity } set(value) { pulse.opacity = value } } /// A property that accesses the backing layer's background @IBInspectable open override var backgroundColor: UIColor? { didSet { layer.backgroundColor = backgroundColor?.cgColor } } /** An initializer that initializes the object with a NSCoder object. - Parameter aDecoder: A NSCoder instance. */ public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) prepare() } /** An initializer that initializes the object. - Parameter style: A UITableViewCellStyle enum. - Parameter reuseIdentifier: A String identifier. */ public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String!) { super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) prepare() } open override func layoutSubviews() { super.layoutSubviews() layoutShape() layoutVisualLayer() layoutShadowPath() layoutDivider() } /** Triggers the pulse animation. - Parameter point: A Optional point to pulse from, otherwise pulses from the center. */ open func pulse(point: CGPoint? = nil) { pulse.expand(point: point ?? center) Motion.delay(0.35) { [weak self] in self?.pulse.contract() } } /** A delegation method that is executed when the view has began a touch event. - Parameter touches: A set of UITouch objects. - Parameter event: A UIEvent object. */ open override func touchesBegan(_ touches: Set, with event: UIEvent?) { super.touchesBegan(touches, with: event) pulse.expand(point: layer.convert(touches.first!.location(in: self), from: layer)) } /** A delegation method that is executed when the view touch event has ended. - Parameter touches: A set of UITouch objects. - Parameter event: A UIEvent object. */ open override func touchesEnded(_ touches: Set, with event: UIEvent?) { super.touchesEnded(touches, with: event) pulse.contract() } /** A delegation method that is executed when the view touch event has been cancelled. - Parameter touches: A set of UITouch objects. - Parameter event: A UIEvent object. */ open override func touchesCancelled(_ touches: Set, with event: UIEvent?) { super.touchesCancelled(touches, with: event) pulse.contract() } /** Prepares the view instance when intialized. When subclassing, it is recommended to override the prepare method to initialize property values and other setup operations. The super.prepare method should always be called immediately when subclassing. */ open func prepare() { selectionStyle = .none separatorInset = .zero contentScaleFactor = Screen.scale imageView?.isUserInteractionEnabled = false textLabel?.isUserInteractionEnabled = false detailTextLabel?.isUserInteractionEnabled = false prepareVisualLayer() preparePulse() } } extension TableViewCell { /// Prepares the pulse motion. fileprivate func preparePulse() { pulse = Pulse(pulseView: self, pulseLayer: visualLayer) } /// Prepares the visualLayer property. fileprivate func prepareVisualLayer() { visualLayer.zPosition = 0 visualLayer.masksToBounds = true contentView.layer.addSublayer(visualLayer) } } extension TableViewCell { /// Manages the layout for the visualLayer property. fileprivate func layoutVisualLayer() { visualLayer.frame = bounds visualLayer.cornerRadius = layer.cornerRadius } } ================================================ FILE: Sources/iOS/Table/TableViewController.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit public protocol TableViewDelegate: UITableViewDelegate {} public protocol TableViewDataSource: UITableViewDataSource { /** Retrieves the data source items for the tableView. - Returns: An Array of DataSourceItem objects. */ var dataSourceItems: [DataSourceItem] { get } } extension UIViewController { /** A convenience property that provides access to the TableViewController. This is the recommended method of accessing the TableViewController through child UIViewControllers. */ public var tableViewController: TableViewController? { return traverseViewControllerHierarchyForClassType() } } open class TableViewController: ViewController { /// A reference to a Reminder. public let tableView = TableView() /// An Array of DataSourceItems. open var dataSourceItems = [DataSourceItem]() open override func prepare() { super.prepare() prepareTableView() } } extension TableViewController { /// Prepares the tableView. fileprivate func prepareTableView() { tableView.delegate = self tableView.dataSource = self tableView.backgroundColor = .clear view.layout(tableView).edges() } } extension TableViewController: TableViewDelegate {} extension TableViewController: TableViewDataSource { @objc open func numberOfSections(in tableView: UITableView) -> Int { return 1 } @objc open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return dataSourceItems.count } @objc open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { return UITableViewCell() } } ================================================ FILE: Sources/iOS/Text/Editor.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit public enum EditorPlaceholderAnimation { case `default` case hidden } open class Editor: View, Themeable { /// Reference to textView. public let textView = TextView() /// A boolean indicating whether the textView is in edit mode. open var isEditing: Bool { return textView.isEditing } /// A boolean indicating whether the text is empty. open var isEmpty: Bool { return textView.isEmpty } /// The placeholder UILabel. @IBInspectable open var placeholderLabel: UILabel { return textView.placeholderLabel } /// A Boolean that indicates if the placeholder label is animated. @IBInspectable open var isPlaceholderAnimated = true /// Set the placeholder animation value. open var placeholderAnimation = EditorPlaceholderAnimation.default { didSet { updatePlaceholderVisibility() } } /// Placeholder normal text color. @IBInspectable open var placeholderNormalColor = Color.darkText.others { didSet { updatePlaceholderLabelColor() } } /// Placeholder active text color. @IBInspectable open var placeholderActiveColor = Color.blue.base { didSet { updatePlaceholderLabelColor() } } /// The scale of the active placeholder in relation to the inactive. @IBInspectable open var placeholderActiveScale: CGFloat = 0.75 { didSet { layoutPlaceholderLabel() } } /// This property adds a padding to placeholder y position animation @IBInspectable open var placeholderVerticalOffset: CGFloat = 0 /// This property adds a padding to placeholder x position animation @IBInspectable open var placeholderHorizontalOffset: CGFloat = 0 /// Divider normal height. @IBInspectable open var dividerNormalHeight: CGFloat = 1 { didSet { updateDividerHeight() } } /// Divider active height. @IBInspectable open var dividerActiveHeight: CGFloat = 2 { didSet { updateDividerHeight() } } /// Divider normal color. @IBInspectable open var dividerNormalColor = Color.grey.lighten2 { didSet { updateDividerColor() } } /// Divider active color. @IBInspectable open var dividerActiveColor = Color.blue.base { didSet { updateDividerColor() } } /// The detailLabel UILabel that is displayed. @IBInspectable public let detailLabel = UILabel() /// The detailLabel text value. @IBInspectable open var detail: String? { get { return detailLabel.text } set(value) { detailLabel.text = value layoutSubviews() } } /// The detailLabel text color. @IBInspectable open var detailColor = Color.darkText.others { didSet { updateDetailLabelColor() } } /// Vertical distance for the detailLabel from the divider. @IBInspectable open var detailVerticalOffset: CGFloat = 8 { didSet { layoutSubviews() } } /// A reference to titleLabel.textAlignment observation. private var placeholderLabelTextObserver: NSKeyValueObservation! /** A reference to textView.text observation. Only observes programmatic changes. */ private var textViewTextObserver: NSKeyValueObservation! deinit { placeholderLabelTextObserver.invalidate() placeholderLabelTextObserver = nil textViewTextObserver.invalidate() textViewTextObserver = nil } open override func prepare() { super.prepare() backgroundColor = nil prepareDivider() prepareTextView() preparePlaceholderLabel() prepareDetailLabel() prepareNotificationHandlers() applyCurrentTheme() } open override func layoutSubviews() { super.layoutSubviews() layoutPlaceholderLabel() layoutDivider() layoutBottomLabel(label: detailLabel, verticalOffset: detailVerticalOffset) } /** Applies the given theme. - Parameter theme: A Theme. */ open func apply(theme: Theme) { placeholderActiveColor = theme.secondary placeholderNormalColor = theme.onSurface.withAlphaComponent(0.38) dividerActiveColor = theme.secondary dividerNormalColor = theme.onSurface.withAlphaComponent(0.12) detailColor = theme.onSurface.withAlphaComponent(0.38) textView.tintColor = theme.secondary } @discardableResult open override func becomeFirstResponder() -> Bool { return textView.becomeFirstResponder() } @discardableResult open override func resignFirstResponder() -> Bool { return textView.resignFirstResponder() } } private extension Editor { /// Prepares the divider. func prepareDivider() { dividerColor = dividerNormalColor } /// Prepares the textView. func prepareTextView() { layout(textView).edges() textView.isPlaceholderLabelEnabled = false textViewTextObserver = textView.observe(\.text) { [weak self] _, _ in self?.updateEditorState() } } /// Prepares the placeholderLabel. func preparePlaceholderLabel() { addSubview(placeholderLabel) placeholderLabelTextObserver = placeholderLabel.observe(\.text) { [weak self] _, _ in self?.layoutPlaceholderLabel() } } /// Prepares the detailLabel. func prepareDetailLabel() { detailLabel.font = Theme.font.regular(with: 12) detailLabel.numberOfLines = 0 detailColor = Color.darkText.others addSubview(detailLabel) } /// Prepares the Notification handlers. func prepareNotificationHandlers() { let center = NotificationCenter.default center.addObserver(self, selector: #selector(handleTextViewTextDidBegin), name: UITextView.textDidBeginEditingNotification, object: textView) center.addObserver(self, selector: #selector(handleTextViewTextDidChange), name: UITextView.textDidChangeNotification, object: textView) center.addObserver(self, selector: #selector(handleTextViewTextDidEnd), name: UITextView.textDidEndEditingNotification, object: textView) } } private extension Editor { /// Updates the placeholderLabel text color. func updatePlaceholderLabelColor() { tintColor = placeholderActiveColor placeholderLabel.textColor = isEditing ? placeholderActiveColor : placeholderNormalColor } /// Updates the placeholder visibility. func updatePlaceholderVisibility() { guard isEditing else { placeholderLabel.isHidden = !isEmpty && .hidden == placeholderAnimation return } placeholderLabel.isHidden = .hidden == placeholderAnimation } /// Updates the dividerColor. func updateDividerColor() { dividerColor = isEditing ? dividerActiveColor : dividerNormalColor } /// Updates the dividerThickness. func updateDividerHeight() { dividerThickness = isEditing ? dividerActiveHeight : dividerNormalHeight } /// Updates the detailLabel text color. func updateDetailLabelColor() { detailLabel.textColor = detailColor } } private extension Editor { /// Layout the placeholderLabel. func layoutPlaceholderLabel() { let inset = textView.textContainerInsets let leftPadding = inset.left + textView.textContainer.lineFragmentPadding let rightPadding = inset.right + textView.textContainer.lineFragmentPadding let w = bounds.width - leftPadding - rightPadding var h = placeholderLabel.sizeThatFits(CGSize(width: w, height: .greatestFiniteMagnitude)).height h = max(h, textView.minimumTextHeight) h = min(h, bounds.height - inset.top - inset.bottom) placeholderLabel.bounds.size = CGSize(width: w, height: h) guard isEditing || !isEmpty || !isPlaceholderAnimated else { placeholderLabel.transform = CGAffineTransform.identity placeholderLabel.frame.origin = CGPoint(x: leftPadding, y: inset.top) return } placeholderLabel.transform = CGAffineTransform(scaleX: placeholderActiveScale, y: placeholderActiveScale) placeholderLabel.frame.origin.y = -placeholderLabel.frame.height + placeholderVerticalOffset switch placeholderLabel.textAlignment { case .left, .natural: placeholderLabel.frame.origin.x = leftPadding + placeholderHorizontalOffset case .right: let scaledWidth = w * placeholderActiveScale placeholderLabel.frame.origin.x = bounds.width - scaledWidth - rightPadding + placeholderHorizontalOffset default:break } } /// Layout given label at the bottom with the vertical offset provided. func layoutBottomLabel(label: UILabel, verticalOffset: CGFloat) { let c = dividerContentEdgeInsets label.frame.origin.x = c.left label.frame.origin.y = bounds.height + verticalOffset label.frame.size.width = bounds.width - c.left - c.right label.frame.size.height = label.sizeThatFits(CGSize(width: label.bounds.width, height: .greatestFiniteMagnitude)).height } } private extension Editor { /// Notification handler for when text editing began. @objc func handleTextViewTextDidBegin() { updateEditorState(isAnimated: true) } /// Notification handler for when text changed. @objc func handleTextViewTextDidChange() { updateEditorState() } /// Notification handler for when text editing ended. @objc func handleTextViewTextDidEnd() { updateEditorState(isAnimated: true) } /// Updates editor. func updateEditorState(isAnimated: Bool = false) { updatePlaceholderVisibility() updatePlaceholderLabelColor() updateDividerHeight() updateDividerColor() guard isAnimated && isPlaceholderAnimated else { layoutPlaceholderLabel() return } UIView.animate(withDuration: 0.15, animations: layoutPlaceholderLabel) } } public extension Editor { /// A reference to the textView text. var text: String! { get { return textView.text } set(value) { textView.text = value } } /// A reference to the textView font. var font: UIFont? { get { return textView.font } set(value) { textView.font = value } } /// A reference to the textView placeholder. var placeholder: String? { get { return textView.placeholder } set(value) { textView.placeholder = value } } /// A reference to the textView textAlignment. var textAlignment: NSTextAlignment { get { return textView.textAlignment } set(value) { textView.textAlignment = value detailLabel.textAlignment = value } } } ================================================ FILE: Sources/iOS/Text/ErrorTextField.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit open class ErrorTextField: TextField { /// The errorLabel UILabel that is displayed. @IBInspectable public let errorLabel = UILabel() /// The errorLabel text value. @IBInspectable open var error: String? { get { return errorLabel.text } set(value) { errorLabel.text = value layoutSubviews() } } /// Error text color @IBInspectable open var errorColor = Color.red.base { didSet { errorLabel.textColor = errorColor } } /// Vertical distance for the errorLabel from the divider. @IBInspectable open var errorVerticalOffset: CGFloat = 8 { didSet { layoutSubviews() } } /// Hide or show error text. open var isErrorRevealed: Bool { get { return !errorLabel.isHidden } set(value) { errorLabel.isHidden = !value detailLabel.isHidden = value layoutSubviews() } } open override func prepare() { super.prepare() isErrorRevealed = false prepareErrorLabel() } /// Prepares the errorLabel. func prepareErrorLabel() { errorLabel.font = Theme.font.regular(with: 12) errorLabel.numberOfLines = 0 errorColor = { errorColor }() // call didSet addSubview(errorLabel) } open override func layoutSubviews() { super.layoutSubviews() layoutBottomLabel(label: errorLabel, verticalOffset: errorVerticalOffset) } open override func apply(theme: Theme) { super.apply(theme: theme) errorColor = theme.error } } ================================================ FILE: Sources/iOS/Text/ErrorTextFieldValidator.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion /** Validator plugin for ErrorTextField and subclasses. Can be accessed via `textField.validator` ### Example ```swift field.validator .notEmpty(message: "Choose username") .min(length: 3, message: "Minimum 3 characters") .noWhitespaces(message: "Username cannot contain spaces") .username(message: "Unallowed characters in username") } ``` */ open class ErrorTextFieldValidator { /// A typealias for validation closure. public typealias ValidationClosure = (_ text: String) -> Bool /// Validation closures and their error messages. open var closures: [(code: ValidationClosure, message: String)] = [] /// A reference to the textField. open weak var textField: ErrorTextField? /// Behavior for auto-validation. open var autoValidationType: AutoValidationType = .default /** A flag indicating if error message is shown at least once. Used for `AutoValidationType.default`. */ open var isErrorShownOnce = false /** Initializes validator. - Parameter textField: An ErrorTextField to validate. */ public init(textField: ErrorTextField) { self.textField = textField prepare() } /** Prepares the validator instance when intialized. When subclassing, it is recommended to override the prepare method to initialize property values and other setup operations. The super.prepare method should always be called immediately when subclassing. */ open func prepare() { textField?.addTarget(self, action: #selector(autoValidate), for: .editingChanged) } /** Validates textField based on `autoValidationType`. This method is called when textField.text changes. */ @objc private func autoValidate() { guard let textField = textField else { return } switch autoValidationType { case .none: break case .custom(let closure): closure(textField) case .default: guard isErrorShownOnce else { return } textField.isValid() case .always: textField.isValid() } } /** Validates textField.text against criteria defined in `closures` and shows relevant error message on failure. - Parameter isDeferred: Defer showing error message. - Returns: Boolean indicating if validation passed. */ @discardableResult open func isValid(isDeferred: Bool) -> Bool { guard let textField = textField else { return false } for block in closures { if !block.code(textField.text ?? "") { if !isDeferred { textField.error = block.message textField.isErrorRevealed = true isErrorShownOnce = true } return false } } if !isDeferred { textField.isErrorRevealed = false } return true } /** Adds provided closure and its error message to the validation chain. - Parameter message: A message to be shown when validation fails. - Parameter code: Closure to run for validation. - Returns: Validator itself to allow chaining. */ @discardableResult open func validate(message: String, when code: @escaping ValidationClosure) -> Self { closures.append((code, message)) return self } /** Types for determining behaviour of auto-validation which is run when textField.text changes. */ public enum AutoValidationType { /// Turn off. case none /// Run validation only if error is shown once. case `default` /// Always run validation. case always /** Custom auto-validation logic passed as closure which accepts ErrorTextField. Closure is called when `textField.text` changes. */ case custom((ErrorTextField) -> Void) } } /// Memory key pointer for `validator`. private var AssociatedInstanceKey: UInt8 = 0 extension ErrorTextField { /// A reference to validator. open var validator: ErrorTextFieldValidator { get { return AssociatedObject.get(base: self, key: &AssociatedInstanceKey) { return ErrorTextFieldValidator(textField: self) } } set(value) { AssociatedObject.set(base: self, key: &AssociatedInstanceKey, value: value) } } /** Validates textField.text against criteria defined in `closures` and shows relevant error message on failure. - Parameter isDeferred: Defer showing error message. Default is false. - Returns: Boolean indicating if validation passed. */ @discardableResult open func isValid(isDeferred: Bool = false) -> Bool { return validator.isValid(isDeferred: isDeferred) } } public extension ErrorTextFieldValidator { /** Validate that field contains correct email address. - Parameter message: A message to show for incorrect emails. - Returns: Validator itself to allow chaining. */ @discardableResult func email(message: String) -> Self { return regex(message: message, pattern: "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}") } /** Validate that field contains allowed usernames characters. - Parameter message: A message to show for disallowed usernames. - Returns: Validator itself to allow chaining. */ @discardableResult func username(message: String) -> Self { return regex(message: message, pattern: "^[a-zA-Z0-9]+([_\\s\\-\\.\\']?[a-zA-Z0-9])*$") } /** Validate that field text matches provided regex pattern. - Parameter message: A message to show for unmatched texts. - Parameter pattern: A regex pattern to match. - Returns: Validator itself to allow chaining. */ @discardableResult func regex(message: String, pattern: String) -> Self { return validate(message: message) { let pred = NSPredicate(format: "SELF MATCHES %@", pattern) return pred.evaluate(with: $0) } } /** Validate that field text has minimum `length`. - Parameter length: Minimum allowed text length. - Parameter message: A message to show when requirement is not met. - Parameter trimmingSet: A trimming CharacterSet for trimming text before validation. - Returns: Validator itself to allow chaining. */ @discardableResult func min(length: Int, message: String, trimmingSet: CharacterSet? = .whitespacesAndNewlines) -> Self { let trimmingSet = trimmingSet ?? .init() return validate(message: message) { $0.trimmingCharacters(in: trimmingSet).count >= length } } /** Validate that field text has maximum `length`. - Parameter length: Minimum allowed text length. - Parameter message: A message to show when requirement is not met. - Parameter trimmingSet: A trimming CharacterSet for trimming text before validation. - Returns: Validator itself to allow chaining. */ @discardableResult func max(length: Int, message: String, trimmingSet: CharacterSet? = .whitespacesAndNewlines) -> Self { let trimmingSet = trimmingSet ?? .init() return validate(message: message) { $0.trimmingCharacters(in: trimmingSet).count <= length } } /** Validate that field text is not empty. - Parameter message: A message to show when requirement is not met. - Parameter trimmingSet: A trimming CharacterSet for trimming text before validation. - Returns: Validator itself to allow chaining. */ @discardableResult func notEmpty(message: String, trimmingSet: CharacterSet? = .whitespacesAndNewlines) -> Self { let trimmingSet = trimmingSet ?? .init() return validate(message: message) { $0.trimmingCharacters(in: trimmingSet).isEmpty == false } } /** Validate that field text contains no whitespaces. - Parameter message: A message to show when requirement is not met. - Parameter trimmingSet: A trimming CharacterSet for trimming text before validation. - Returns: Validator itself to allow chaining. */ @discardableResult func noWhitespaces(message: String) -> Self { return validate(message: message) { $0.rangeOfCharacter(from: .whitespaces) == nil } } } ================================================ FILE: Sources/iOS/Text/TextField.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit @objc(TextFieldPlaceholderAnimation) public enum TextFieldPlaceholderAnimation: Int { case `default` case hidden } @objc(TextFieldDelegate) public protocol TextFieldDelegate: UITextFieldDelegate { /** A delegation method that is executed when the textField changed. - Parameter textField: A TextField. - Parameter didChange text: An optional String. */ @objc optional func textField(textField: TextField, didChange text: String?) /** A delegation method that is executed when the textField will clear. - Parameter textField: A TextField. - Parameter willClear text: An optional String. */ @objc optional func textField(textField: TextField, willClear text: String?) /** A delegation method that is executed when the textField is cleared. - Parameter textField: A TextField. - Parameter didClear text: An optional String. */ @objc optional func textField(textField: TextField, didClear text: String?) } open class TextField: UITextField, Themeable { /// Minimum TextField text height. private let minimumTextHeight: CGFloat = 32 /// Default size when using AutoLayout. open override var intrinsicContentSize: CGSize { let h = textInsets.top + textInsets.bottom + minimumTextHeight return CGSize(width: bounds.width, height: max(h, super.intrinsicContentSize.height)) } /// A Boolean that indicates if the placeholder label is animated. @IBInspectable open var isPlaceholderAnimated = true /// Set the placeholder animation value. open var placeholderAnimation = TextFieldPlaceholderAnimation.default { didSet { updatePlaceholderVisibility() } } /// A boolean indicating whether the text is empty. open var isEmpty: Bool { return 0 == text?.utf16.count } open override var text: String? { didSet { updatePlaceholderVisibility() } } open override var leftView: UIView? { didSet { prepareLeftView() layoutSubviews() } } /// The leftView width value. open var leftViewWidth: CGFloat { guard nil != leftView else { return 0 } return leftViewOffset + bounds.height } /// The leftView offset value. open var leftViewOffset: CGFloat = 16 /// Placeholder normal text @IBInspectable open var leftViewNormalColor = Color.darkText.others { didSet { updateLeftViewColor() } } /// Placeholder active text @IBInspectable open var leftViewActiveColor = Color.blue.base { didSet { updateLeftViewColor() } } /// Divider normal height. @IBInspectable open var dividerNormalHeight: CGFloat = 1 { didSet { updateDividerHeight() } } /// Divider active height. @IBInspectable open var dividerActiveHeight: CGFloat = 2 { didSet { updateDividerHeight() } } /// Divider normal color. @IBInspectable open var dividerNormalColor = Color.grey.lighten2 { didSet { updateDividerColor() } } /// Divider active color. @IBInspectable open var dividerActiveColor = Color.blue.base { didSet { updateDividerColor() } } /// The placeholderLabel font value. @IBInspectable open override var font: UIFont? { didSet { placeholderLabel.font = font } } /// The placeholderLabel text value. @IBInspectable open override var placeholder: String? { get { return placeholderLabel.text } set(value) { if isEditing && isPlaceholderUppercasedWhenEditing { placeholderLabel.text = value?.uppercased() } else { placeholderLabel.text = value } layoutSubviews() } } open override var isSecureTextEntry: Bool { didSet { updateVisibilityIcon() fixCursorPosition() } } /// The placeholder UILabel. @IBInspectable public let placeholderLabel = UILabel() /// Placeholder normal text @IBInspectable open var placeholderNormalColor = Color.darkText.others { didSet { updatePlaceholderLabelColor() } } /// Placeholder active text @IBInspectable open var placeholderActiveColor = Color.blue.base { didSet { /// Keep tintColor update here. See #1229 tintColor = placeholderActiveColor updatePlaceholderLabelColor() } } /// This property adds a padding to placeholder y position animation @IBInspectable open var placeholderVerticalOffset: CGFloat = 0 /// This property adds a padding to placeholder y position animation @IBInspectable open var placeholderHorizontalOffset: CGFloat = 0 /// The scale of the active placeholder in relation to the inactive @IBInspectable open var placeholderActiveScale: CGFloat = 0.75 { didSet { layoutPlaceholderLabel() } } /// The detailLabel UILabel that is displayed. @IBInspectable public let detailLabel = UILabel() /// The detailLabel text value. @IBInspectable open var detail: String? { get { return detailLabel.text } set(value) { detailLabel.text = value layoutSubviews() } } /// Detail text @IBInspectable open var detailColor = Color.darkText.others { didSet { updateDetailLabelColor() } } /// Vertical distance for the detailLabel from the divider. @IBInspectable open var detailVerticalOffset: CGFloat = 8 { didSet { layoutSubviews() } } /// Handles the textAlignment of the placeholderLabel. open override var textAlignment: NSTextAlignment { didSet { placeholderLabel.textAlignment = textAlignment detailLabel.textAlignment = textAlignment } } /// A reference to the clearIconButton. open fileprivate(set) var clearIconButton: IconButton? /// Enables the clearIconButton. @IBInspectable open var isClearIconButtonEnabled: Bool { get { return nil != clearIconButton } set(value) { guard value else { clearIconButton?.removeTarget(self, action: #selector(handleClearIconButton), for: .touchUpInside) removeFromRightView(view: clearIconButton) clearIconButton = nil return } guard nil == clearIconButton else { return } clearIconButton = IconButton(image: Icon.cm.clear, tintColor: placeholderNormalColor) clearIconButton!.contentEdgeInsetsPreset = .none clearIconButton!.pulseAnimation = .none rightView?.grid.views.insert(clearIconButton!, at: 0) isClearIconButtonAutoHandled = { isClearIconButtonAutoHandled }() layoutSubviews() } } /// Enables the automatic handling of the clearIconButton. @IBInspectable open var isClearIconButtonAutoHandled = true { didSet { clearIconButton?.removeTarget(self, action: #selector(handleClearIconButton), for: .touchUpInside) guard isClearIconButtonAutoHandled else { return } clearIconButton?.addTarget(self, action: #selector(handleClearIconButton), for: .touchUpInside) } } /// A reference to the visibilityIconButton. open fileprivate(set) var visibilityIconButton: IconButton? /// Icon for visibilityIconButton when in the on state. open var visibilityIconOn = Icon.visibility { didSet { updateVisibilityIcon() } } /// Icon for visibilityIconButton when in the off state. open var visibilityIconOff = Icon.visibilityOff { didSet { updateVisibilityIcon() } } /// Enables the visibilityIconButton. @IBInspectable open var isVisibilityIconButtonEnabled: Bool { get { return nil != visibilityIconButton } set(value) { guard value else { visibilityIconButton?.removeTarget(self, action: #selector(handleVisibilityIconButton), for: .touchUpInside) removeFromRightView(view: visibilityIconButton) visibilityIconButton = nil return } guard nil == visibilityIconButton else { return } isSecureTextEntry = true visibilityIconButton = IconButton(image: nil, tintColor: placeholderNormalColor.withAlphaComponent(0.54)) updateVisibilityIcon() visibilityIconButton!.contentEdgeInsetsPreset = .none visibilityIconButton!.pulseAnimation = .centerRadialBeyondBounds rightView?.grid.views.append(visibilityIconButton!) isVisibilityIconButtonAutoHandled = { isVisibilityIconButtonAutoHandled }() layoutSubviews() } } /// Enables the automatic handling of the visibilityIconButton. @IBInspectable open var isVisibilityIconButtonAutoHandled = true { didSet { visibilityIconButton?.removeTarget(self, action: #selector(handleVisibilityIconButton), for: .touchUpInside) guard isVisibilityIconButtonAutoHandled else { return } visibilityIconButton?.addTarget(self, action: #selector(handleVisibilityIconButton), for: .touchUpInside) } } @IBInspectable open var isPlaceholderUppercasedWhenEditing = false { didSet { updatePlaceholderTextToActiveState() } } /** An initializer that initializes the object with a NSCoder object. - Parameter aDecoder: A NSCoder instance. */ public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) prepare() } /** An initializer that initializes the object with a CGRect object. If AutoLayout is used, it is better to initilize the instance using the init() initializer. - Parameter frame: A CGRect instance. */ public override init(frame: CGRect) { super.init(frame: frame) prepare() /// Fire didSet here to update tintColor placeholderActiveColor = { placeholderActiveColor }() } open override func layoutSubviews() { super.layoutSubviews() layoutShape() layoutPlaceholderLabel() layoutBottomLabel(label: detailLabel, verticalOffset: detailVerticalOffset) layoutDivider() layoutLeftView() layoutRightView() } open override func becomeFirstResponder() -> Bool { layoutSubviews() return super.becomeFirstResponder() } /// EdgeInsets for text. @objc open var textInsets: EdgeInsets = .zero /// EdgeInsets preset property for text. open var textInsetsPreset = EdgeInsetsPreset.none { didSet { textInsets = EdgeInsetsPresetToValue(preset: textInsetsPreset) } } open override func textRect(forBounds bounds: CGRect) -> CGRect { return super.textRect(forBounds: bounds).inset(by: textInsets) } open override func editingRect(forBounds bounds: CGRect) -> CGRect { return textRect(forBounds: bounds) } /** Prepares the view instance when intialized. When subclassing, it is recommended to override the prepare method to initialize property values and other setup operations. The super.prepare method should always be called immediately when subclassing. */ open func prepare() { clipsToBounds = false borderStyle = .none backgroundColor = nil contentScaleFactor = Screen.scale font = Theme.font.regular(with: 16) textColor = Color.darkText.primary prepareDivider() preparePlaceholderLabel() prepareDetailLabel() prepareTargetHandlers() prepareTextAlignment() prepareRightView() applyCurrentTheme() } /** Applies the given theme. - Parameter theme: A Theme. */ open func apply(theme: Theme) { placeholderActiveColor = theme.secondary placeholderNormalColor = theme.onSurface.withAlphaComponent(0.38) leftViewActiveColor = theme.secondary leftViewNormalColor = theme.onSurface.withAlphaComponent(0.38) dividerActiveColor = theme.secondary dividerNormalColor = theme.onSurface.withAlphaComponent(0.12) detailColor = theme.onSurface.withAlphaComponent(0.38) textColor = theme.onSurface.withAlphaComponent(0.87) } } fileprivate extension TextField { /// Prepares the divider. func prepareDivider() { dividerColor = dividerNormalColor } /// Prepares the placeholderLabel. func preparePlaceholderLabel() { placeholderNormalColor = Color.darkText.others placeholderLabel.backgroundColor = .clear addSubview(placeholderLabel) } /// Prepares the detailLabel. func prepareDetailLabel() { detailLabel.font = Theme.font.regular(with: 12) detailLabel.numberOfLines = 0 detailColor = Color.darkText.others addSubview(detailLabel) } /// Prepares the leftView. func prepareLeftView() { leftView?.contentMode = .left leftViewMode = .always updateLeftViewColor() } /// Prepares the target handlers. func prepareTargetHandlers() { addTarget(self, action: #selector(handleEditingDidBegin), for: .editingDidBegin) addTarget(self, action: #selector(handleEditingChanged), for: .editingChanged) addTarget(self, action: #selector(handleEditingDidEnd), for: .editingDidEnd) } /// Prepares the textAlignment. func prepareTextAlignment() { textAlignment = .rightToLeft == Application.userInterfaceLayoutDirection ? .right : .left } /// Prepares the rightView. func prepareRightView() { rightView = UIView() rightView?.grid.columns = 2 rightViewMode = .whileEditing clearButtonMode = .never } } fileprivate extension TextField { /// Updates the leftView tint color. func updateLeftViewColor() { leftView?.tintColor = isEditing ? leftViewActiveColor : leftViewNormalColor } /// Updates the placeholderLabel text color. func updatePlaceholderLabelColor() { placeholderLabel.textColor = isEditing ? placeholderActiveColor : placeholderNormalColor } /// Updates the placeholder visibility. func updatePlaceholderVisibility() { guard isEditing else { placeholderLabel.isHidden = !isEmpty && .hidden == placeholderAnimation return } placeholderLabel.isHidden = .hidden == placeholderAnimation } /// Updates the dividerColor. func updateDividerColor() { dividerColor = isEditing ? dividerActiveColor : dividerNormalColor } /// Updates the dividerThickness. func updateDividerHeight() { dividerThickness = isEditing ? dividerActiveHeight : dividerNormalHeight } /// Update the placeholder text to the active state. func updatePlaceholderTextToActiveState() { guard isPlaceholderUppercasedWhenEditing else { return } guard isEditing || !isEmpty else { return } placeholderLabel.text = placeholderLabel.text?.uppercased() } /// Update the placeholder text to the normal state. func updatePlaceholderTextToNormalState() { guard isPlaceholderUppercasedWhenEditing else { return } guard isEmpty else { return } placeholderLabel.text = placeholderLabel.text?.capitalized } /// Updates the detailLabel text color. func updateDetailLabelColor() { detailLabel.textColor = detailColor } } fileprivate extension TextField { /// Layout the placeholderLabel. func layoutPlaceholderLabel() { let leftPadding = leftViewWidth + textInsets.left let w = bounds.width - leftPadding - textInsets.right var h = placeholderLabel.sizeThatFits(CGSize(width: w, height: .greatestFiniteMagnitude)).height h = min(h, bounds.height - textInsets.top - textInsets.bottom) h = max(h, minimumTextHeight) placeholderLabel.bounds.size = CGSize(width: w, height: h) guard isEditing || !isEmpty || !isPlaceholderAnimated else { placeholderLabel.transform = CGAffineTransform.identity placeholderLabel.frame.origin = CGPoint(x: leftPadding, y: textInsets.top) return } placeholderLabel.transform = CGAffineTransform(scaleX: placeholderActiveScale, y: placeholderActiveScale) placeholderLabel.frame.origin.y = -placeholderLabel.frame.height + placeholderVerticalOffset switch placeholderLabel.textAlignment { case .left, .natural: placeholderLabel.frame.origin.x = leftPadding + placeholderHorizontalOffset case .right: let scaledWidth = w * placeholderActiveScale placeholderLabel.frame.origin.x = bounds.width - scaledWidth - textInsets.right + placeholderHorizontalOffset default:break } } /// Layout the leftView. func layoutLeftView() { guard let v = leftView else { return } let w = leftViewWidth v.frame = CGRect(x: 0, y: 0, width: w, height: bounds.height) dividerContentEdgeInsets.left = w } /// Layout the rightView. func layoutRightView() { guard let v = rightView else { return } let w = CGFloat(v.grid.views.count) * bounds.height v.frame = CGRect(x: bounds.width - w, y: 0, width: w, height: bounds.height) v.grid.reload() } } internal extension TextField { /// Layout given label at the bottom with the vertical offset provided. func layoutBottomLabel(label: UILabel, verticalOffset: CGFloat) { let c = dividerContentEdgeInsets label.frame.origin.x = c.left label.frame.origin.y = bounds.height + verticalOffset label.frame.size.width = bounds.width - c.left - c.right label.frame.size.height = label.sizeThatFits(CGSize(width: label.bounds.width, height: .greatestFiniteMagnitude)).height } } fileprivate extension TextField { /// Handles the text editing did begin state. @objc func handleEditingDidBegin() { leftViewEditingBeginAnimation() placeholderEditingDidBeginAnimation() dividerEditingDidBeginAnimation() } // Live updates the textField text. @objc func handleEditingChanged(textField: UITextField) { (delegate as? TextFieldDelegate)?.textField?(textField: self, didChange: textField.text) } /// Handles the text editing did end state. @objc func handleEditingDidEnd() { leftViewEditingEndAnimation() placeholderEditingDidEndAnimation() dividerEditingDidEndAnimation() } /// Handles the clearIconButton TouchUpInside event. @objc func handleClearIconButton() { guard nil == delegate?.textFieldShouldClear || true == delegate?.textFieldShouldClear?(self) else { return } let t = text (delegate as? TextFieldDelegate)?.textField?(textField: self, willClear: t) text = nil (delegate as? TextFieldDelegate)?.textField?(textField: self, didClear: t) } /// Handles the visibilityIconButton TouchUpInside event. @objc func handleVisibilityIconButton() { UIView.transition( with: (visibilityIconButton?.imageView)!, duration: 0.3, options: .transitionCrossDissolve, animations: { [weak self] in guard let `self` = self else { return } self.isSecureTextEntry = !self.isSecureTextEntry }) } } private extension TextField { /// The animation for leftView when editing begins. func leftViewEditingBeginAnimation() { updateLeftViewColor() } /// The animation for leftView when editing ends. func leftViewEditingEndAnimation() { updateLeftViewColor() } /// The animation for the divider when editing begins. func dividerEditingDidBeginAnimation() { updateDividerHeight() updateDividerColor() } /// The animation for the divider when editing ends. func dividerEditingDidEndAnimation() { updateDividerHeight() updateDividerColor() } /// The animation for the placeholder when editing begins. func placeholderEditingDidBeginAnimation() { updatePlaceholderVisibility() updatePlaceholderLabelColor() guard isPlaceholderAnimated else { return } updatePlaceholderTextToActiveState() UIView.animate(withDuration: 0.15, animations: layoutPlaceholderLabel) } /// The animation for the placeholder when editing ends. func placeholderEditingDidEndAnimation() { updatePlaceholderVisibility() updatePlaceholderLabelColor() guard isPlaceholderAnimated else { return } updatePlaceholderTextToNormalState() UIView.animate(withDuration: 0.15, animations: layoutPlaceholderLabel) } } private extension TextField { /// Updates visibilityIconButton image based on isSecureTextEntry value. func updateVisibilityIcon() { visibilityIconButton?.image = isSecureTextEntry ? visibilityIconOff : visibilityIconOn } /// Remove view from rightView. func removeFromRightView(view: UIView?) { guard let v = view, let i = rightView?.grid.views.firstIndex(of: v) else { return } rightView?.grid.views.remove(at: i) } /** Reassign text to reset cursor position. Fixes issue-1119. Previously issue-1030, and issue-1023. */ func fixCursorPosition() { let t = text text = nil text = t } } ================================================ FILE: Sources/iOS/Text/TextStorage.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit @objc(TextStorageDelegate) public protocol TextStorageDelegate: NSTextStorageDelegate { /** A delegation method that is executed when text will be processed during editing. - Parameter textStorage: A TextStorage. - Parameter willProcessEditing text: A String. - Parameter range: A NSRange. */ @objc optional func textStorage(textStorage: TextStorage, willProcessEditing text: String, range: NSRange) /** A delegation method that is executed when text has been processed after editing. - Parameter textStorage: A TextStorage. - Parameter didProcessEditing text: A String. - Parameter result: An optional NSTextCheckingResult. - Parameter flags: NSRegularExpression.MatchingFlags. - Parameter top: An UnsafeMutablePointer. */ @objc optional func textStorage(textStorage: TextStorage, didProcessEditing text: String, result: NSTextCheckingResult?, flags: NSRegularExpression.MatchingFlags, stop: UnsafeMutablePointer) } open class TextStorage: NSTextStorage { /// A storage facility for attributed text. public let storage = NSMutableAttributedString() /// The regular expression to match text fragments against. open var expression: NSRegularExpression? /// Initializer. public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } /// Initializer. public override init() { super.init() } } extension TextStorage { /// A String value of the attirbutedString property. open override var string: String { return storage.string } /// Processes the text when editing. open override func processEditing() { let range = (string as NSString).paragraphRange(for: editedRange) (delegate as? TextStorageDelegate)?.textStorage?(textStorage: self, willProcessEditing: string, range: range) expression?.enumerateMatches(in: string, options: [], range: range) { [unowned self] (result: NSTextCheckingResult?, flags: NSRegularExpression.MatchingFlags, stop: UnsafeMutablePointer) -> Void in (self.delegate as? TextStorageDelegate)?.textStorage?(textStorage: self, didProcessEditing: self.string, result: result, flags: flags, stop: stop) } storage.fixAttributes(in: range) super.processEditing() } /** Returns the attributes for the character at a given index. - Parameter location: An Int - Parameter effectiveRange range: Upon return, the range over which the attributes and values are the same as those at index. This range isn’t necessarily the maximum range covered, and its extent is implementation-dependent. If you need the maximum range, use attributesAtIndex:longestEffectiveRange:inRange:. If you don't need this value, pass NULL. - Returns: The attributes for the character at index. */ open override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key: Any] { return storage.attributes(at: location, effectiveRange: range) } /** Replaces a range of text with a string value. - Parameter range: The character range to replace. - Parameter str: The string value that the characters will be replaced with. */ open override func replaceCharacters(in range: NSRange, with str: String) { storage.replaceCharacters(in: range, with: str) edited(.editedCharacters, range: range, changeInLength: str.utf16.count - range.length) } /** Sets the attributedString attribute values. - Parameter attrs: The attributes to set. - Parameter range: A range of characters that will have their attributes updated. */ open override func setAttributes(_ attrs: [NSAttributedString.Key : Any]?, range: NSRange) { storage.setAttributes(attrs, range: range) edited(.editedAttributes, range: range, changeInLength: 0) } /** Adds an individual attribute. - Parameter _ name: Attribute name. - Parameter value: An Any type. - Parameter range: A range of characters that will have their attributes added. */ open override func addAttribute(_ name: NSAttributedString.Key, value: Any, range: NSRange) { storage.addAttribute(name, value: value, range: range) edited(.editedAttributes, range: range, changeInLength: 0) } /** Removes an individual attribute. - Parameter _ name: Attribute name. - Parameter range: A range of characters that will have their attributes removed. */ open override func removeAttribute(_ name: NSAttributedString.Key, range: NSRange) { storage.removeAttribute(name, range: range) edited(.editedAttributes, range: range, changeInLength: 0) } } ================================================ FILE: Sources/iOS/Text/TextView.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit @objc(TextViewDelegate) public protocol TextViewDelegate : UITextViewDelegate { /** A delegation method that is executed when the keyboard will open. - Parameter textView: A TextView. - Parameter willShowKeyboard value: A NSValue. */ @objc optional func textView(textView: TextView, willShowKeyboard value: NSValue) /** A delegation method that is executed when the keyboard will close. - Parameter textView: A TextView. - Parameter willHideKeyboard value: A NSValue. */ @objc optional func textView(textView: TextView, willHideKeyboard value: NSValue) /** A delegation method that is executed when the keyboard did open. - Parameter textView: A TextView. - Parameter didShowKeyboard value: A NSValue. */ @objc optional func textView(textView: TextView, didShowKeyboard value: NSValue) /** A delegation method that is executed when the keyboard did close. - Parameter textView: A TextView. - Parameter didHideKeyboard value: A NSValue. */ @objc optional func textView(textView: TextView, didHideKeyboard value: NSValue) /** A delegation method that is executed when text will be processed during editing. - Parameter textView: A TextView. - Parameter willProcessEditing textStorage: A TextStorage. - Parameter text: A String. - Parameter range: A NSRange. */ @objc optional func textView(textView: TextView, willProcessEditing textStorage: TextStorage, text: String, range: NSRange) /** A delegation method that is executed when text has been processed after editing. - Parameter textView: A TextView. - Parameter didProcessEditing textStorage: A TextStorage. - Parameter text: A String. - Parameter range: A NSRange. */ @objc optional func textView(textView: TextView, didProcessEditing textStorage: TextStorage, text: String, range: NSRange) } open class TextView: UITextView, Themeable { /// A boolean indicating whether the text is empty. open var isEmpty: Bool { return 0 == text?.utf16.count } /// A boolean indicating whether the text is in edit mode. open fileprivate(set) var isEditing = false /// Is the keyboard hidden. open fileprivate(set) var isKeyboardHidden = true /// A property that accesses the backing layer's background @IBInspectable open override var backgroundColor: UIColor? { didSet { layer.backgroundColor = backgroundColor?.cgColor } } /// Holds default font. private var _font: UIFont? /// The placeholderLabel font value. @IBInspectable open override var font: UIFont? { didSet { _font = font placeholderLabel.font = font } } /// The placeholderLabel text value. @IBInspectable open var placeholder: String? { get { return placeholderLabel.text } set(value) { placeholderLabel.text = value } } /// The placeholder UILabel. @IBInspectable public let placeholderLabel = UILabel() /// A property to enable/disable operations on the placeholderLabel internal var isPlaceholderLabelEnabled = true /// Placeholder normal text @IBInspectable open var placeholderColor = Color.darkText.others { didSet { updatePlaceholderLabelColor() } } /// NSTextContainer EdgeInsets preset property. open var textContainerInsetsPreset = EdgeInsetsPreset.none { didSet { textContainerInsets = EdgeInsetsPresetToValue(preset: textContainerInsetsPreset) } } /// NSTextContainer EdgeInsets property. open var textContainerInsets: EdgeInsets { get { return textContainerInset } set(value) { textContainerInset = value } } /// Handles the textAlignment of the placeholderLabel and textView itself. open override var textAlignment: NSTextAlignment { didSet { placeholderLabel.textAlignment = textAlignment } } /** An initializer that initializes the object with a NSCoder object. - Parameter aDecoder: A NSCoder instance. */ public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) prepare() } /// The string pattern to match within the textStorage. open var pattern = "(^|\\s)#[\\d\\w_\u{203C}\u{2049}\u{20E3}\u{2122}\u{2139}\u{2194}-\u{2199}\u{21A9}-\u{21AA}\u{231A}-\u{231B}\u{23E9}-\u{23EC}\u{23F0}\u{23F3}\u{24C2}\u{25AA}-\u{25AB}\u{25B6}\u{25C0}\u{25FB}-\u{25FE}\u{2600}-\u{2601}\u{260E}\u{2611}\u{2614}-\u{2615}\u{261D}\u{263A}\u{2648}-\u{2653}\u{2660}\u{2663}\u{2665}-\u{2666}\u{2668}\u{267B}\u{267F}\u{2693}\u{26A0}-\u{26A1}\u{26AA}-\u{26AB}\u{26BD}-\u{26BE}\u{26C4}-\u{26C5}\u{26CE}\u{26D4}\u{26EA}\u{26F2}-\u{26F3}\u{26F5}\u{26FA}\u{26FD}\u{2702}\u{2705}\u{2708}-\u{270C}\u{270F}\u{2712}\u{2714}\u{2716}\u{2728}\u{2733}-\u{2734}\u{2744}\u{2747}\u{274C}\u{274E}\u{2753}-\u{2755}\u{2757}\u{2764}\u{2795}-\u{2797}\u{27A1}\u{27B0}\u{2934}-\u{2935}\u{2B05}-\u{2B07}\u{2B1B}-\u{2B1C}\u{2B50}\u{2B55}\u{3030}\u{303D}\u{3297}\u{3299}\u{1F004}\u{1F0CF}\u{1F170}-\u{1F171}\u{1F17E}-\u{1F17F}\u{1F18E}\u{1F191}-\u{1F19A}\u{1F1E7}-\u{1F1EC}\u{1F1EE}-\u{1F1F0}\u{1F1F3}\u{1F1F5}\u{1F1F7}-\u{1F1FA}\u{1F201}-\u{1F202}\u{1F21A}\u{1F22F}\u{1F232}-\u{1F23A}\u{1F250}-\u{1F251}\u{1F300}-\u{1F320}\u{1F330}-\u{1F335}\u{1F337}-\u{1F37C}\u{1F380}-\u{1F393}\u{1F3A0}-\u{1F3C4}\u{1F3C6}-\u{1F3CA}\u{1F3E0}-\u{1F3F0}\u{1F400}-\u{1F43E}\u{1F440}\u{1F442}-\u{1F4F7}\u{1F4F9}-\u{1F4FC}\u{1F500}-\u{1F507}\u{1F509}-\u{1F53D}\u{1F550}-\u{1F567}\u{1F5FB}-\u{1F640}\u{1F645}-\u{1F64F}\u{1F680}-\u{1F68A}]+" { didSet { prepareRegularExpression() } } /// A reference to the textView text. open override var text: String! { didSet { setContentOffset(.zero, animated: true) updatePlaceholderVisibility() } } /** A convenience property that accesses the textStorage string. */ open var string: String { return textStorage.string } /// An Array of matches that match the pattern expression. open var matches: [String] { guard let v = (textStorage as? TextStorage)?.expression else { return [] } return v.matches(in: string, options: [], range: NSMakeRange(0, string.utf16.count)).map { [unowned self] in (self.string as NSString).substring(with: $0.range).trimmed } } /** An Array of unique matches that match the pattern expression. */ open var uniqueMatches: [String] { var set = Set() for x in matches { set.insert(x) } return Array(set) } /** An initializer that initializes the object with a CGRect object. If AutoLayout is used, it is better to initilize the instance using the init() initializer. - Parameter frame: A CGRect instance. - Parameter textContainer: A NSTextContainer instance. */ public override init(frame: CGRect, textContainer: NSTextContainer?) { super.init(frame: frame, textContainer: textContainer) prepare() } /** A convenience initializer that is mostly used with AutoLayout. - Parameter textContainer: A NSTextContainer instance. */ public convenience init(textContainer: NSTextContainer?) { self.init(frame: .zero, textContainer: textContainer) } /// A convenience initializer that constructs all aspects of the textView. public convenience init() { let textContainer = NSTextContainer(size: .zero) let layoutManager = NSLayoutManager() layoutManager.addTextContainer(textContainer) let textStorage = TextStorage() textStorage.addLayoutManager(layoutManager) self.init(textContainer: textContainer) textContainer.size = bounds.size textStorage.delegate = self } /// Denitializer. deinit { NotificationCenter.default.removeObserver(self) } open override func layoutSubviews() { super.layoutSubviews() layoutShape() layoutShadowPath() layoutPlaceholderLabel() } /** Prepares the view instance when intialized. When subclassing, it is recommended to override the prepare method to initialize property values and other setup operations. The super.prepare method should always be called immediately when subclassing. */ open func prepare() { contentScaleFactor = Screen.scale textContainerInset = .zero backgroundColor = nil font = Theme.font.regular(with: 16) textColor = Color.darkText.primary prepareNotificationHandlers() prepareRegularExpression() preparePlaceholderLabel() applyCurrentTheme() } open override var contentSize: CGSize { didSet { guard isGrowEnabled else { return } invalidateIntrinsicContentSize() guard isEditing && isHeightChangeAnimated else { superview?.layoutIfNeeded() return } UIView.animate(withDuration: 0.15) { let v = self.superview as? Editor ?? self v.superview?.layoutIfNeeded() } } } /// A Boolean that indicates if the height change during growing is animated. open var isHeightChangeAnimated = true /// Maximum preffered layout height before scrolling. open var preferredMaxLayoutHeight: CGFloat = 0 { didSet { invalidateIntrinsicContentSize() superview?.layoutIfNeeded() } } /// A property indicating if textView allowed to grow. private var isGrowEnabled: Bool { return preferredMaxLayoutHeight > 0 && isScrollEnabled } /// Minimum TextView text height. internal let minimumTextHeight: CGFloat = 32 open override var intrinsicContentSize: CGSize { guard isGrowEnabled else { return super.intrinsicContentSize } let insets = textContainerInsets let w = bounds.width - insets.left - insets.right - 2 * textContainer.lineFragmentPadding let placeholderH = placeholderLabel.sizeThatFits(CGSize(width: w, height: .greatestFiniteMagnitude)).height var h = max(minimumTextHeight, placeholderH) + insets.top + insets.bottom h = max(h, contentSize.height) return CGSize(width: UIView.noIntrinsicMetric, height: min(h, preferredMaxLayoutHeight)) } open override func insertText(_ text: String) { fixTypingFont() super.insertText(text) fixTypingFont() } open override func paste(_ sender: Any?) { fixTypingFont() super.paste(sender) fixTypingFont() } /** Applies the given theme. - Parameter theme: A Theme. */ open func apply(theme: Theme) { textColor = theme.onSurface.withAlphaComponent(0.87) placeholderColor = theme.onSurface.withAlphaComponent(0.38) } } fileprivate extension TextView { /// Prepares the Notification handlers. func prepareNotificationHandlers() { let defaultCenter = NotificationCenter.default defaultCenter.addObserver(self, selector: #selector(handleKeyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil) defaultCenter.addObserver(self, selector: #selector(handleKeyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil) defaultCenter.addObserver(self, selector: #selector(handleKeyboardDidShow(notification:)), name: UIResponder.keyboardDidShowNotification, object: nil) defaultCenter.addObserver(self, selector: #selector(handleKeyboardDidHide(notification:)), name: UIResponder.keyboardDidHideNotification, object: nil) defaultCenter.addObserver(self, selector: #selector(handleTextViewTextDidBegin), name: UITextView.textDidBeginEditingNotification, object: self) defaultCenter.addObserver(self, selector: #selector(handleTextViewTextDidChange), name: UITextView.textDidChangeNotification, object: self) defaultCenter.addObserver(self, selector: #selector(handleTextViewTextDidEnd), name: UITextView.textDidEndEditingNotification, object: self) } /// Prepares the regular expression for matching. func prepareRegularExpression() { (textStorage as? TextStorage)?.expression = try? NSRegularExpression(pattern: pattern, options: []) } /// prepares the placeholderLabel property. func preparePlaceholderLabel() { placeholderLabel.textColor = Color.darkText.others placeholderLabel.textAlignment = textAlignment placeholderLabel.numberOfLines = 0 placeholderLabel.backgroundColor = .clear addSubview(placeholderLabel) } } fileprivate extension TextView { /// Updates the placeholderLabel text color. func updatePlaceholderLabelColor() { guard isPlaceholderLabelEnabled else { return } tintColor = placeholderColor placeholderLabel.textColor = placeholderColor } /// Updates the placeholderLabel visibility. func updatePlaceholderVisibility() { guard isPlaceholderLabelEnabled else { return } placeholderLabel.isHidden = !isEmpty } } fileprivate extension TextView { /// Laysout the placeholder UILabel. func layoutPlaceholderLabel() { guard isPlaceholderLabelEnabled else { return } let insets = textContainerInsets let leftPadding = insets.left + textContainer.lineFragmentPadding let rightPadding = insets.right + textContainer.lineFragmentPadding let w = bounds.width - leftPadding - rightPadding var h = placeholderLabel.sizeThatFits(CGSize(width: w, height: .greatestFiniteMagnitude)).height h = max(h, minimumTextHeight) h = min(h, bounds.height - insets.top - insets.bottom) placeholderLabel.frame = CGRect(x: leftPadding, y: insets.top, width: w, height: h) } } fileprivate extension TextView { /** Handler for when the keyboard will open. - Parameter notification: A Notification. */ @objc func handleKeyboardWillShow(notification: Notification) { guard isKeyboardHidden else { return } guard let v = notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue else { return } (delegate as? TextViewDelegate)?.textView?(textView: self, willShowKeyboard: v) } /** Handler for when the keyboard did open. - Parameter notification: A Notification. */ @objc func handleKeyboardDidShow(notification: Notification) { guard isKeyboardHidden else { return } isKeyboardHidden = false guard let v = notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue else { return } (delegate as? TextViewDelegate)?.textView?(textView: self, didShowKeyboard: v) } /** Handler for when the keyboard will close. - Parameter notification: A Notification. */ @objc func handleKeyboardWillHide(notification: Notification) { guard !isKeyboardHidden else { return } guard let v = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return } (delegate as? TextViewDelegate)?.textView?(textView: self, willHideKeyboard: v) } /** Handler for when the keyboard did close. - Parameter notification: A Notification. */ @objc func handleKeyboardDidHide(notification: Notification) { guard !isKeyboardHidden else { return } isKeyboardHidden = true guard let v = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return } (delegate as? TextViewDelegate)?.textView?(textView: self, didHideKeyboard: v) } /// Notification handler for when text editing began. @objc func handleTextViewTextDidBegin() { isEditing = true } /// Notification handler for when text changed. @objc func handleTextViewTextDidChange() { updatePlaceholderVisibility() } /// Notification handler for when text editing ended. @objc func handleTextViewTextDidEnd() { isEditing = false updatePlaceholderVisibility() } } extension TextView: TextStorageDelegate { @objc open func textStorage(textStorage: TextStorage, willProcessEditing text: String, range: NSRange) { (delegate as? TextViewDelegate)?.textView?(textView: self, willProcessEditing: textStorage, text: string, range: range) } @objc open func textStorage(textStorage: TextStorage, didProcessEditing text: String, result: NSTextCheckingResult?, flags: NSRegularExpression.MatchingFlags, stop: UnsafeMutablePointer) { guard let range = result?.range else { return } (delegate as? TextViewDelegate)?.textView?(textView: self, didProcessEditing: textStorage, text: string, range: range) } } private extension TextView { /// issue-838 and pr-1117 /// /// Inserting (typing or pasting) an emoji character or placing cursor after some /// emoji characters (e.g ☺️) was causing typing font to change to "AppleColorEmoji" /// and which was eventually falling back to "Courier New" when a non-emoji /// character is inserted. This only happens only if `NSTextStorage` subclass is /// used, otherwise typing font never changed to "AppleColorEmoji". So, we fix it /// by resetting typing font from AppleColorEmoji to the default font set by the /// developer. The fix is applied before and after inserting text. The former fixes /// typing font change due to cursor placed after an emoji character, and the /// latter fixes the typing font change due to the insertion of an emoji character /// (typing font changes somehow are reflected in `UITextView.font` parameter). func fixTypingFont() { let fontAttribute = NSAttributedString.Key.font guard (typingAttributes[fontAttribute] as? UIFont)?.fontName == "AppleColorEmoji" else { return } typingAttributes[fontAttribute] = _font } } ================================================ FILE: Sources/iOS/Theme/Theme.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion public protocol Themeable: class { /** Applies given theme. - Parameter theme: A Theme. */ func apply(theme: Theme) /// A boolean indicating if theming is enabled. var isThemingEnabled: Bool { get set } } public struct Theme: Hashable { /// The color displayed most frequently across the app. public var primary = Color.blue.darken2 /// Accent color for some components such as FABMenu. public var secondary = Color.blue.base /// Background color for view controllers and some components. public var background = Color.white /// Background color for components such as cards, and dialogs. public var surface = Color.white /// Error color for components such as ErrorTextField. public var error = Color.red.base /// Text and iconography color to be used on primary color. public var onPrimary = Color.white /// Text and iconography color to be used on secondary color. public var onSecondary = Color.white /// Text and iconography color to be used on background color. public var onBackground = Color.black /// Text and iconography color to be used on surface color. public var onSurface = Color.black /// Text and iconography color to be used on error color. public var onError = Color.white /// A boolean indicating if theming is enabled globally. public static var isEnabled = false /// Global font for app. public static var font: FontType.Type = RobotoFont.self /// An initializer. public init() { } } public extension Theme { /// Current theme for Material. static private(set) var current = Theme.light /// A light theme. static var light = Theme() /// A dark theme. static var dark: Theme = { var t = Theme() t.primary = UIColor(rgb: 0x202020) t.secondary = Color.teal.base t.background = UIColor(rgb: 0x303030) t.surface = t.background t.onBackground = .white t.onSurface = .white return t }() } public extension Theme { /** Applies theme to the entire app. - Parameter theme: A Theme. */ static func apply(theme: Theme) { current = theme guard let v = Application.rootViewController else { return } apply(theme: theme, to: v) } /** Applies theme to the hierarchy of given view. - Parameter theme: A Theme. - Parameter to view: A UIView. */ static func apply(theme: Theme, to view: UIView) { guard !((view as? Themeable)?.isThemingEnabled == false), !view.isProcessed else { return } view.subviews.forEach { apply(theme: theme, to: $0) } (view as? Themeable)?.apply(theme: theme) } /** Applies theme to the hierarchy of given view controller. - Parameter theme: A Theme. - Parameter to viewController: A UIViewController. */ static func apply(theme: Theme, to viewController: UIViewController) { guard !((viewController as? Themeable)?.isThemingEnabled == false) else { return } viewController.allChildren.forEach { apply(theme: theme, to: $0) $0.view.isProcessed = true } apply(theme: theme, to: viewController.view) viewController.allChildren.forEach { $0.view.isProcessed = false } (viewController as? Themeable)?.apply(theme: theme) } /** Applies provided theme for the components created within the given block without chaging app's theme. - Parameter theme: A Theme. - Parameter for block: A code block. */ static func applying(theme: Theme, for execute: () -> Void) { let v = current current = theme execute() current = v } } /// A memory reference to the isThemingEnabled for Themeable NSObject subclasses. private var IsThemingEnabledKey: UInt8 = 0 public extension Themeable where Self: NSObject { /// A class-wide boolean indicating if theming is enabled. static var isThemingEnabled: Bool { get { return Theme.isEnabled && AssociatedObject.get(base: self, key: &IsThemingEnabledKey) { true } } set(value) { AssociatedObject.set(base: self, key: &IsThemingEnabledKey, value: value) } } /// A boolean indicating if theming is enabled. var isThemingEnabled: Bool { get { return type(of: self).isThemingEnabled && AssociatedObject.get(base: self, key: &IsThemingEnabledKey) { true } } set(value) { AssociatedObject.set(base: self, key: &IsThemingEnabledKey, value: value) } } /// Applies current theme to itself if theming is enabled. internal func applyCurrentTheme() { guard isThemingEnabled else { return } apply(theme: .current) } } private extension UIViewController { /// Returns all possible child view controllers. var allChildren: [UIViewController] { var all = children if let v = self as? TabsController { all += v.viewControllers } if let v = presentedViewController, v.presentingViewController === self { all.append(v) } return all } } /// A memory reference to the isProcessed for UIView. private var IsProcessedKey: UInt8 = 0 private extension UIView { /// A boolean indicating if view is already themed. var isProcessed: Bool { get { return AssociatedObject.get(base: self, key: &IsProcessedKey) { false } } set(value) { AssociatedObject.set(base: self, key: &IsProcessedKey, value: value) } } } ================================================ FILE: Sources/iOS/Toolbar/Toolbar.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit open class Toolbar: Bar, Themeable { /// A convenience property to set the titleLabel.text. @IBInspectable open var title: String? { get { return titleLabel.text } set(value) { titleLabel.text = value layoutSubviews() } } /// Title label. @IBInspectable public let titleLabel = UILabel() /// A convenience property to set the detailLabel.text. @IBInspectable open var detail: String? { get { return detailLabel.text } set(value) { detailLabel.text = value layoutSubviews() } } /// Detail label. @IBInspectable public let detailLabel = UILabel() open override var leftViews: [UIView] { didSet { prepareIconButtons(leftViews) } } open override var centerViews: [UIView] { didSet { prepareIconButtons(centerViews) } } open override var rightViews: [UIView] { didSet { prepareIconButtons(rightViews) } } deinit { titleLabelTextAlignmentObserver.invalidate() titleLabelTextAlignmentObserver = nil } /** An initializer that initializes the object with a NSCoder object. - Parameter aDecoder: A NSCoder instance. */ public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } /** An initializer that initializes the object with a CGRect object. If AutoLayout is used, it is better to initilize the instance using the init() initializer. - Parameter frame: A CGRect instance. */ public override init(frame: CGRect) { super.init(frame: frame) } open override func layoutSubviews() { super.layoutSubviews() guard willLayout else { return } if 0 < titleLabel.text?.utf16.count ?? 0 { if nil == titleLabel.superview { contentView.addSubview(titleLabel) } titleLabel.frame = contentView.bounds } else { titleLabel.removeFromSuperview() } if 0 < detailLabel.text?.utf16.count ?? 0 { if nil == detailLabel.superview { contentView.addSubview(detailLabel) } if nil == titleLabel.superview { detailLabel.frame = contentView.bounds } else { titleLabel.sizeToFit() detailLabel.sizeToFit() let diff: CGFloat = (contentView.bounds.height - titleLabel.bounds.height - detailLabel.bounds.height) / 2 titleLabel.frame.size.height += diff titleLabel.frame.size.width = contentView.bounds.width detailLabel.frame.size.height += diff detailLabel.frame.size.width = contentView.bounds.width detailLabel.frame.origin.y = titleLabel.bounds.height } } else { detailLabel.removeFromSuperview() } } open override func prepare() { super.prepare() contentViewAlignment = .center prepareTitleLabel() prepareDetailLabel() } /** Applies the given theme. - Parameter theme: A Theme. */ open func apply(theme: Theme) { backgroundColor = theme.primary (leftViews + rightViews + centerViews).forEach { guard let v = $0 as? IconButton, v.isThemingEnabled else { return } v.apply(theme: theme) } if !((titleLabel as? Themeable)?.isThemingEnabled == false) { titleLabel.textColor = theme.onPrimary } if !((detailLabel as? Themeable)?.isThemingEnabled == false) { detailLabel.textColor = theme.onPrimary } } /// A reference to titleLabel.textAlignment observation. private var titleLabelTextAlignmentObserver: NSKeyValueObservation! } private extension Toolbar { /// Prepares the titleLabel. func prepareTitleLabel() { titleLabel.textAlignment = .center titleLabel.contentScaleFactor = Screen.scale titleLabel.font = Theme.font.medium(with: 17) titleLabel.textColor = Color.darkText.primary titleLabelTextAlignmentObserver = titleLabel.observe(\.textAlignment) { [weak self] titleLabel, _ in self?.contentViewAlignment = .center == titleLabel.textAlignment ? .center : .full } } /// Prepares the detailLabel. func prepareDetailLabel() { detailLabel.textAlignment = .center detailLabel.contentScaleFactor = Screen.scale detailLabel.font = Theme.font.regular(with: 12) detailLabel.textColor = Color.darkText.secondary } func prepareIconButtons(_ views: [UIView]) { views.forEach { ($0 as? IconButton)?.themingStyle = .onPrimary } applyCurrentTheme() } } ================================================ FILE: Sources/iOS/Toolbar/ToolbarController.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit @objc(ToolbarAlignment) public enum ToolbarAlignment: Int { case top case bottom } extension UIViewController { /** A convenience property that provides access to the ToolbarController. This is the recommended method of accessing the ToolbarController through child UIViewControllers. */ public var toolbarController: ToolbarController? { return traverseViewControllerHierarchyForClassType() } } @objc(ToolbarController) open class ToolbarController: StatusBarController { /// Reference to the Toolbar. @IBInspectable public let toolbar = Toolbar() /// The toolbar alignment. open var toolbarAlignment = ToolbarAlignment.top { didSet { layoutSubviews() } } open override func layoutSubviews() { super.layoutSubviews() layoutToolbar() layoutContainer() layoutRootViewController() } open override func prepare() { super.prepare() displayStyle = .partial prepareToolbar() } } fileprivate extension ToolbarController { /// Prepares the toolbar. func prepareToolbar() { toolbar.layer.zPosition = 1000 toolbar.depthPreset = .depth1 view.addSubview(toolbar) } } fileprivate extension ToolbarController { /// Layout the container. func layoutContainer() { switch displayStyle { case .partial: let p = toolbar.bounds.height let q = statusBarOffsetAdjustment let h = view.bounds.height - p - q switch toolbarAlignment { case .top: container.frame.origin.y = q + p container.frame.size.height = h case .bottom: container.frame.origin.y = q container.frame.size.height = h } container.frame.size.width = view.bounds.width case .full: container.frame = view.bounds } } /// Layout the toolbar. func layoutToolbar() { toolbar.frame.origin.x = 0 toolbar.frame.origin.y = .top == toolbarAlignment ? statusBarOffsetAdjustment : view.bounds.height - toolbar.bounds.height toolbar.frame.size.width = view.bounds.width } /// Layout the rootViewController. func layoutRootViewController() { rootViewController.view.frame = container.bounds } } ================================================ FILE: Sources/iOS/Transition/DisplayStyle.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit @objc(DisplayStyle) public enum DisplayStyle: Int { case partial case full } ================================================ FILE: Sources/iOS/Transition/TransitionController.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion open class TransitionController: ViewController { /** A Boolean property used to enable and disable interactivity with the rootViewController. */ @IBInspectable open var isUserInteractionEnabled: Bool { get { return rootViewController.view.isUserInteractionEnabled } set(value) { rootViewController.view.isUserInteractionEnabled = value } } /// A Boolean indicating whether the controller is in transitioning state. open var isTransitioning: Bool { return MotionTransition.shared.isTransitioning && MotionTransition.shared.fromViewController == rootViewController } open override var childForStatusBarStyle: UIViewController? { return isTransitioning ? MotionTransition.shared.toViewController ?? rootViewController : rootViewController } open override var childForStatusBarHidden: UIViewController? { return childForStatusBarStyle } open override var childForHomeIndicatorAutoHidden: UIViewController? { return childForStatusBarStyle } open override var childForScreenEdgesDeferringSystemGestures: UIViewController? { return childForStatusBarStyle } /// A reference to the container view. @IBInspectable public let container = UIView() /** A UIViewController property that references the active main UIViewController. To swap the rootViewController, it is recommended to use the transitionFromRootViewController helper method. */ open internal(set) var rootViewController: UIViewController! { didSet { guard oldValue != rootViewController else { return } if let v = oldValue { removeViewController(viewController: v) } prepare(viewController: rootViewController, in: container) } } /** An initializer that initializes the object with a NSCoder object. - Parameter aDecoder: A NSCoder instance. */ public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } /** An initializer that initializes the object with an Optional nib and bundle. - Parameter nibNameOrNil: An Optional String for the nib. - Parameter bundle: An Optional NSBundle where the nib is located. */ public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) } /** An initializer for the BarController. - Parameter rootViewController: The main UIViewController. */ public init(rootViewController: UIViewController) { super.init(nibName: nil, bundle: nil) self.rootViewController = rootViewController } open override var shouldAutomaticallyForwardAppearanceMethods: Bool { return false } open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) rootViewController.beginAppearanceTransition(true, animated: animated) } open override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) rootViewController.endAppearanceTransition() } open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) rootViewController.beginAppearanceTransition(false, animated: animated) } open override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) rootViewController.endAppearanceTransition() } /** A method to swap rootViewController objects. - Parameter toViewController: The UIViewController to swap with the active rootViewController. - Parameter completion: A completion block that is execited after the transition animation from the active rootViewController to the toViewController has completed. */ open func transition(to viewController: UIViewController, completion: ((Bool) -> Void)? = nil) { prepare(viewController: viewController, in: container) if case .auto = viewController.motionTransitionType { viewController.motionTransitionType = motionTransitionType } view.isUserInteractionEnabled = false MotionTransition.shared.transition(from: rootViewController, to: viewController, in: container) { [weak self] isFinishing in guard let s = self else { return } defer { s.view.isUserInteractionEnabled = true completion?(isFinishing) } guard isFinishing else { s.removeViewController(viewController: viewController) s.removeViewController(viewController: s.rootViewController) s.prepare(viewController: s.rootViewController, in: s.container) return } s.rootViewController = viewController } } open override func prepare() { super.prepare() prepareContainer() guard let v = rootViewController else { return } prepare(viewController: v, in: container) } } internal extension TransitionController { /// Prepares the container view. func prepareContainer() { container.frame = view.bounds container.clipsToBounds = true container.contentScaleFactor = Screen.scale container.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.addSubview(container) } /** A method that adds the passed in controller as a child of the BarController within the passed in container view. - Parameter viewController: A UIViewController to add as a child. - Parameter in container: A UIView that is the parent of the passed in controller view within the view hierarchy. */ func prepare(viewController: UIViewController, in container: UIView) { addChild(viewController) container.addSubview(viewController.view) viewController.didMove(toParent: self) viewController.view.frame = container.bounds viewController.view.clipsToBounds = true viewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] viewController.view.contentScaleFactor = Screen.scale } } internal extension TransitionController { /** Removes a given view controller from the childViewControllers array. - Parameter at index: An Int for the view controller position. */ func removeViewController(viewController: UIViewController) { viewController.willMove(toParent: nil) viewController.view.removeFromSuperview() viewController.removeFromParent() } } ================================================ FILE: Sources/iOS/Type/Border.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit @objc(BorderWidthPreset) public enum BorderWidthPreset: Int { case none case border1 case border2 case border3 case border4 case border5 case border6 case border7 case border8 case border9 /// A CGFloat representation of the border width preset. public var cgFloatValue: CGFloat { switch self { case .none: return 0 case .border1: return 0.5 case .border2: return 1 case .border3: return 2 case .border4: return 3 case .border5: return 4 case .border6: return 5 case .border7: return 6 case .border8: return 7 case .border9: return 8 } } } ================================================ FILE: Sources/iOS/Type/CornerRadius.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit @objc(CornerRadiusPreset) public enum CornerRadiusPreset: Int { case none case cornerRadius1 case cornerRadius2 case cornerRadius3 case cornerRadius4 case cornerRadius5 case cornerRadius6 case cornerRadius7 case cornerRadius8 case cornerRadius9 } /// Converts the CornerRadiusPreset enum to a CGFloat value. public func CornerRadiusPresetToValue(preset: CornerRadiusPreset) -> CGFloat { switch preset { case .none: return 0 case .cornerRadius1: return 2 case .cornerRadius2: return 4 case .cornerRadius3: return 8 case .cornerRadius4: return 12 case .cornerRadius5: return 16 case .cornerRadius6: return 20 case .cornerRadius7: return 24 case .cornerRadius8: return 28 case .cornerRadius9: return 32 } } ================================================ FILE: Sources/iOS/Type/Depth.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit public enum DepthPreset { case none case depth1 case depth2 case depth3 case depth4 case depth5 indirect case above(DepthPreset) indirect case below(DepthPreset) indirect case left(DepthPreset) indirect case right(DepthPreset) /// Returns raw depth value without considering direction. public var rawValue: DepthPreset { switch self { case .above(let v): return v.rawValue case .below(let v): return v.rawValue case .left(let v): return v.rawValue case .right(let v): return v.rawValue default: return self } } } public struct Depth { /// Offset. public var offset: Offset /// Opacity. public var opacity: Float /// Radius. public var radius: CGFloat /// A tuple of raw values. public var rawValue: (CGSize, Float, CGFloat) { return (offset.asSize, opacity, radius) } /// Preset. public var preset = DepthPreset.none { didSet { let depth = DepthPresetToValue(preset: preset) offset = depth.offset opacity = depth.opacity radius = depth.radius } } /** Initializer that takes in an offset, opacity, and radius. - Parameter offset: A UIOffset. - Parameter opacity: A Float. - Parameter radius: A CGFloat. */ public init(offset: Offset = .zero, opacity: Float = 0, radius: CGFloat = 0) { self.offset = offset self.opacity = opacity self.radius = radius } /** Initializer that takes in a DepthPreset. - Parameter preset: DepthPreset. */ public init(preset: DepthPreset) { self.init() self.preset = preset } /** Static constructor for Depth with values of 0. - Returns: A Depth struct with values of 0. */ static var zero: Depth { return Depth() } } /// Converts the DepthPreset enum to a Depth value. public func DepthPresetToValue(preset: DepthPreset) -> Depth { switch preset { case .none: return .zero case .depth1: return Depth(offset: Offset(horizontal: 0, vertical: 0.5), opacity: 0.3, radius: 0.5) case .depth2: return Depth(offset: Offset(horizontal: 0, vertical: 1), opacity: 0.3, radius: 1) case .depth3: return Depth(offset: Offset(horizontal: 0, vertical: 2), opacity: 0.3, radius: 2) case .depth4: return Depth(offset: Offset(horizontal: 0, vertical: 4), opacity: 0.3, radius: 4) case .depth5: return Depth(offset: Offset(horizontal: 0, vertical: 8), opacity: 0.3, radius: 8) case .above(let preset): var v = DepthPresetToValue(preset: preset) if preset.isRoot { v.offset.vertical *= -1 } else { let value = DepthPresetToValue(preset: preset.rawValue) v.offset.vertical -= value.offset.vertical } return v case .below(let preset): var v = DepthPresetToValue(preset: preset) if preset.isRoot { return v } else { let value = DepthPresetToValue(preset: preset.rawValue) v.offset.vertical += value.offset.vertical } return v case .left(let preset): var v = DepthPresetToValue(preset: preset) if preset.isRoot { v.offset.horizontal = -v.offset.vertical v.offset.vertical = 0 } else { let value = DepthPresetToValue(preset: preset.rawValue) v.offset.horizontal -= value.offset.vertical } return v case .right(let preset): var v = DepthPresetToValue(preset: preset) if preset.isRoot { v.offset.horizontal = v.offset.vertical v.offset.vertical = 0 } else { let value = DepthPresetToValue(preset: preset.rawValue) v.offset.horizontal += value.offset.vertical } return v } } fileprivate extension DepthPreset { /// Checks if the preset is the root value (has no direction). var isRoot: Bool { switch self { case .above(_): return false case .below(_): return false case .left(_): return false case .right(_): return false default: return true } } } ================================================ FILE: Sources/iOS/Type/EdgeInsets.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit @objc(EdgeInsetsPreset) public enum EdgeInsetsPreset: Int { case none // square case square1 case square2 case square3 case square4 case square5 case square6 case square7 case square8 case square9 case square10 case square11 case square12 case square13 case square14 case square15 // rectangle case wideRectangle1 case wideRectangle2 case wideRectangle3 case wideRectangle4 case wideRectangle5 case wideRectangle6 case wideRectangle7 case wideRectangle8 case wideRectangle9 // flipped rectangle case tallRectangle1 case tallRectangle2 case tallRectangle3 case tallRectangle4 case tallRectangle5 case tallRectangle6 case tallRectangle7 case tallRectangle8 case tallRectangle9 /// horizontally case horizontally1 case horizontally2 case horizontally3 case horizontally4 case horizontally5 /// vertically case vertically1 case vertically2 case vertically3 case vertically4 case vertically5 } public typealias EdgeInsets = UIEdgeInsets /// Converts the EdgeInsetsPreset to a Inset value. public func EdgeInsetsPresetToValue(preset: EdgeInsetsPreset) -> EdgeInsets { switch preset { case .none: return .zero // square case .square1: return EdgeInsets(top: 4, left: 4, bottom: 4, right: 4) case .square2: return EdgeInsets(top: 8, left: 8, bottom: 8, right: 8) case .square3: return EdgeInsets(top: 16, left: 16, bottom: 16, right: 16) case .square4: return EdgeInsets(top: 20, left: 20, bottom: 20, right: 20) case .square5: return EdgeInsets(top: 24, left: 24, bottom: 24, right: 24) case .square6: return EdgeInsets(top: 28, left: 28, bottom: 28, right: 28) case .square7: return EdgeInsets(top: 32, left: 32, bottom: 32, right: 32) case .square8: return EdgeInsets(top: 36, left: 36, bottom: 36, right: 36) case .square9: return EdgeInsets(top: 40, left: 40, bottom: 40, right: 40) case .square10: return EdgeInsets(top: 44, left: 44, bottom: 44, right: 44) case .square11: return EdgeInsets(top: 48, left: 48, bottom: 48, right: 48) case .square12: return EdgeInsets(top: 52, left: 52, bottom: 52, right: 52) case .square13: return EdgeInsets(top: 56, left: 56, bottom: 56, right: 56) case .square14: return EdgeInsets(top: 60, left: 60, bottom: 60, right: 60) case .square15: return EdgeInsets(top: 64, left: 64, bottom: 64, right: 64) // rectangle case .wideRectangle1: return EdgeInsets(top: 2, left: 4, bottom: 2, right: 4) case .wideRectangle2: return EdgeInsets(top: 4, left: 8, bottom: 4, right: 8) case .wideRectangle3: return EdgeInsets(top: 8, left: 16, bottom: 8, right: 16) case .wideRectangle4: return EdgeInsets(top: 12, left: 24, bottom: 12, right: 24) case .wideRectangle5: return EdgeInsets(top: 16, left: 32, bottom: 16, right: 32) case .wideRectangle6: return EdgeInsets(top: 20, left: 40, bottom: 20, right: 40) case .wideRectangle7: return EdgeInsets(top: 24, left: 48, bottom: 24, right: 48) case .wideRectangle8: return EdgeInsets(top: 28, left: 56, bottom: 28, right: 56) case .wideRectangle9: return EdgeInsets(top: 32, left: 64, bottom: 32, right: 64) // flipped rectangle case .tallRectangle1: return EdgeInsets(top: 4, left: 2, bottom: 4, right: 2) case .tallRectangle2: return EdgeInsets(top: 8, left: 4, bottom: 8, right: 4) case .tallRectangle3: return EdgeInsets(top: 16, left: 8, bottom: 16, right: 8) case .tallRectangle4: return EdgeInsets(top: 24, left: 12, bottom: 24, right: 12) case .tallRectangle5: return EdgeInsets(top: 32, left: 16, bottom: 32, right: 16) case .tallRectangle6: return EdgeInsets(top: 40, left: 20, bottom: 40, right: 20) case .tallRectangle7: return EdgeInsets(top: 48, left: 24, bottom: 48, right: 24) case .tallRectangle8: return EdgeInsets(top: 56, left: 28, bottom: 56, right: 28) case .tallRectangle9: return EdgeInsets(top: 64, left: 32, bottom: 64, right: 32) /// horizontally case .horizontally1: return EdgeInsets(top: 0, left: 2, bottom: 0, right: 2) case .horizontally2: return EdgeInsets(top: 0, left: 4, bottom: 0, right: 4) case .horizontally3: return EdgeInsets(top: 0, left: 8, bottom: 0, right: 8) case .horizontally4: return EdgeInsets(top: 0, left: 16, bottom: 0, right: 16) case .horizontally5: return EdgeInsets(top: 0, left: 24, bottom: 0, right: 24) /// vertically case .vertically1: return EdgeInsets(top: 2, left: 0, bottom: 2, right: 0) case .vertically2: return EdgeInsets(top: 4, left: 0, bottom: 4, right: 0) case .vertically3: return EdgeInsets(top: 8, left: 0, bottom: 8, right: 0) case .vertically4: return EdgeInsets(top: 16, left: 0, bottom: 16, right: 0) case .vertically5: return EdgeInsets(top: 24, left: 0, bottom: 24, right: 0) } } ================================================ FILE: Sources/iOS/Type/InterimSpace.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit @objc(InterimSpacePreset) public enum InterimSpacePreset: Int { case none case interimSpace1 case interimSpace2 case interimSpace3 case interimSpace4 case interimSpace5 case interimSpace6 case interimSpace7 case interimSpace8 case interimSpace9 case interimSpace10 case interimSpace11 case interimSpace12 case interimSpace13 case interimSpace14 case interimSpace15 case interimSpace16 case interimSpace17 case interimSpace18 } public typealias InterimSpace = CGFloat /// Converts the InterimSpacePreset enum to an InterimSpace value. public func InterimSpacePresetToValue(preset: InterimSpacePreset) -> InterimSpace { switch preset { case .none: return 0 case .interimSpace1: return 1 case .interimSpace2: return 2 case .interimSpace3: return 4 case .interimSpace4: return 8 case .interimSpace5: return 12 case .interimSpace6: return 16 case .interimSpace7: return 20 case .interimSpace8: return 24 case .interimSpace9: return 28 case .interimSpace10: return 32 case .interimSpace11: return 36 case .interimSpace12: return 40 case .interimSpace13: return 44 case .interimSpace14: return 48 case .interimSpace15: return 52 case .interimSpace16: return 56 case .interimSpace17: return 60 case .interimSpace18: return 64 } } ================================================ FILE: Sources/iOS/Type/Offset.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit public typealias Offset = UIOffset extension CGSize { /// Returns an Offset version of the CGSize. public var asOffset: Offset { return Offset(size: self) } } extension Offset { /** Initializer that accepts a CGSize value. - Parameter size: A CGSize value. */ public init(size: CGSize) { self.init(horizontal: size.width, vertical: size.height) } } extension Offset { /// Returns a CGSize version of the Offset. public var asSize: CGSize { return CGSize(width: horizontal, height: vertical) } } ================================================ FILE: Sources/iOS/Type/Shape.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import Foundation @objc(ShapePreset) public enum ShapePreset: Int { case none case square case circle } ================================================ FILE: Sources/iOS/View/PulseView.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit import Motion open class PulseView: View, Pulseable, PulseableLayer { /// A Pulse reference. internal var pulse: Pulse! /// A reference to the pulse layer. internal var pulseLayer: CALayer? { return pulse.pulseLayer } /// PulseAnimation value. open var pulseAnimation: PulseAnimation { get { return pulse.animation } set(value) { pulse.animation = value } } /// PulseAnimation color. @IBInspectable open var pulseColor: UIColor { get { return pulse.color } set(value) { pulse.color = value } } /// Pulse opacity. @IBInspectable open var pulseOpacity: CGFloat { get { return pulse.opacity } set(value) { pulse.opacity = value } } /** Triggers the pulse animation. - Parameter point: A Optional point to pulse from, otherwise pulses from the center. */ open func pulse(point: CGPoint? = nil) { pulse.expand(point: point ?? center) Motion.delay(0.35) { [weak self] in self?.pulse.contract() } } /** A delegation method that is executed when the view has began a touch event. - Parameter touches: A set of UITouch objects. - Parameter event: A UIEvent object. */ open override func touchesBegan(_ touches: Set, with event: UIEvent?) { super.touchesBegan(touches, with: event) pulse.expand(point: layer.convert(touches.first!.location(in: self), from: layer)) } /** A delegation method that is executed when the view touch event has ended. - Parameter touches: A set of UITouch objects. - Parameter event: A UIEvent object. */ open override func touchesEnded(_ touches: Set, with event: UIEvent?) { super.touchesEnded(touches, with: event) pulse.contract() } /** A delegation method that is executed when the view touch event has been cancelled. - Parameter touches: A set of UITouch objects. - Parameter event: A UIEvent object. */ open override func touchesCancelled(_ touches: Set, with event: UIEvent?) { super.touchesCancelled(touches, with: event) pulse.contract() } open override func prepare() { super.prepare() preparePulse() } } extension PulseView { /// Prepares the pulse motion. fileprivate func preparePulse() { pulse = Pulse(pulseView: self, pulseLayer: visualLayer) } } ================================================ FILE: Sources/iOS/View/View.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit open class View: UIView { open override var intrinsicContentSize: CGSize { return bounds.size } /** A CAShapeLayer used to manage elements that would be affected by the clipToBounds property of the backing layer. For example, this allows the dropshadow effect on the backing layer, while clipping the image to a desired shape within the visualLayer. */ public let visualLayer = CAShapeLayer() /** A property that manages an image for the visualLayer's contents property. Images should not be set to the backing layer's contents property to avoid conflicts when using clipsToBounds. */ @IBInspectable open var image: UIImage? { get { guard let v = visualLayer.contents else { return nil } return UIImage(cgImage: v as! CGImage) } set(value) { visualLayer.contents = value?.cgImage } } /** Allows a relative subrectangle within the range of 0 to 1 to be specified for the visualLayer's contents property. This allows much greater flexibility than the contentsGravity property in terms of how the image is cropped and stretched. */ @IBInspectable open var contentsRect: CGRect { get { return visualLayer.contentsRect } set(value) { visualLayer.contentsRect = value } } /** A CGRect that defines a stretchable region inside the visualLayer with a fixed border around the edge. */ @IBInspectable open var contentsCenter: CGRect { get { return visualLayer.contentsCenter } set(value) { visualLayer.contentsCenter = value } } /** A floating point value that defines a ratio between the pixel dimensions of the visualLayer's contents property and the size of the view. By default, this value is set to the Screen.scale. */ @IBInspectable open var contentsScale: CGFloat { get { return visualLayer.contentsScale } set(value) { visualLayer.contentsScale = value } } /// Determines how content should be aligned within the visualLayer's bounds. @IBInspectable open var contentsGravity: CALayerContentsGravity { get { return visualLayer.contentsGravity } set(value) { visualLayer.contentsGravity = value } } /// A property that accesses the backing layer's background @IBInspectable open override var backgroundColor: UIColor? { didSet { layer.backgroundColor = backgroundColor?.cgColor } } /** An initializer that initializes the object with a NSCoder object. - Parameter aDecoder: A NSCoder instance. */ public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) prepare() } /** An initializer that initializes the object with a CGRect object. If AutoLayout is used, it is better to initilize the instance using the init() initializer. - Parameter frame: A CGRect instance. */ public override init(frame: CGRect) { super.init(frame: frame) prepare() } open override func layoutSubviews() { super.layoutSubviews() layoutShape() layoutVisualLayer() layoutShadowPath() } /** Prepares the view instance when intialized. When subclassing, it is recommended to override the prepare method to initialize property values and other setup operations. The super.prepare method should always be called immediately when subclassing. */ open func prepare() { contentsGravity = .resizeAspectFill contentScaleFactor = Screen.scale backgroundColor = .white prepareVisualLayer() } } extension View { /// Prepares the visualLayer property. fileprivate func prepareVisualLayer() { visualLayer.zPosition = 0 visualLayer.masksToBounds = true layer.addSublayer(visualLayer) } } extension View { /// Manages the layout for the visualLayer property. fileprivate func layoutVisualLayer() { visualLayer.frame = bounds visualLayer.cornerRadius = layer.cornerRadius } } ================================================ FILE: Sources/iOS/View/ViewController.swift ================================================ /* * The MIT License (MIT) * * Copyright (C) 2019, CosmicMind, Inc. . * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import UIKit open class ViewController: UIViewController, Themeable { open override func viewDidLoad() { super.viewDidLoad() prepare() } /** Prepares the view instance when intialized. When subclassing, it is recommended to override the prepareView method to initialize property values and other setup operations. The super.prepareView method should always be called immediately when subclassing. */ open func prepare() { view.clipsToBounds = true view.backgroundColor = .white view.contentScaleFactor = Screen.scale applyCurrentTheme() } open override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() layoutSubviews() } /** Calls the layout functions for the view heirarchy. To execute in the order of the layout chain, override this method. `layoutSubviews` should be called immediately, unless you have a certain need. */ open func layoutSubviews() { } /** Applies given theme to the view controller. - Parameter theme: A Theme. */ open func apply(theme: Theme) { view.backgroundColor = theme.background } }