├── Tests ├── Utils │ ├── .gitkeep │ └── Array+UtilsTest.swift ├── StaticMap │ └── .gitkeep ├── TableView │ └── .gitkeep ├── Validation │ └── .gitkeep ├── ActivityIndicator │ └── .gitkeep ├── CollectionView │ └── .gitkeep ├── Core │ ├── Component │ │ ├── Note.swift │ │ ├── ComponentTestParts.swift │ │ └── ComponentBaseTest.swift │ └── Styling │ │ ├── UILabel+InitTest.swift │ │ ├── UIFont+InitTest.swift │ │ ├── UITableView+InitTest.swift │ │ ├── CGPoint+InitTest.swift │ │ ├── AttributeTest.swift │ │ ├── UICollectionView+InitTest.swift │ │ ├── UIOffset+InitTest.swift │ │ ├── CGSize+InitTest.swift │ │ ├── PercentUtilsTest.swift │ │ ├── UIColor+InitTest.swift │ │ ├── NSAttributedString+AttributeTest.swift │ │ ├── CGAffineTransform+ShortcutTest.swift │ │ ├── UIButton+UtilsTest.swift │ │ └── StyleableTest.swift ├── TestUtils │ ├── ComponentBase+spy.swift │ └── Rx+recording.swift ├── Info.plist └── Configuration │ ├── PropertyTest.swift │ ├── ConfigurableTest.swift │ └── ConfigurationTest.swift ├── CNAME ├── _config.yml ├── docs ├── _config.yml ├── parts │ ├── controller.md │ ├── collectionview.md │ ├── staticmap.md │ ├── validation.md │ ├── rootview.md │ ├── wireframe.md │ └── tableview.md ├── img │ ├── ReactantTutorial.png │ ├── ReactantLiveUIDebug.png │ ├── ReactantLiveUIError.png │ ├── Tutorials │ │ ├── RunButton.png │ │ ├── Notes │ │ │ ├── Simulator1.png │ │ │ ├── Simulator2.png │ │ │ └── Reactant-init-console.png │ │ └── GitExplorer │ │ │ ├── Users.png │ │ │ ├── Repositories.png │ │ │ └── UserHeader.png │ └── ReactantLiveUIPreview.png ├── scripts │ └── zopim.js ├── docpress.json ├── README.md └── getting-started │ └── troubleshooting.md ├── TVPrototyping ├── Assets.xcassets │ ├── Contents.json │ ├── App Icon & Top Shelf Image.brandassets │ │ ├── App Icon.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Middle.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── App Icon - App Store.imagestack │ │ │ ├── Back.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Front.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ ├── Middle.imagestacklayer │ │ │ │ ├── Contents.json │ │ │ │ └── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Top Shelf Image.imageset │ │ │ └── Contents.json │ │ ├── Top Shelf Image Wide.imageset │ │ │ └── Contents.json │ │ └── Contents.json │ └── LaunchImage.launchimage │ │ └── Contents.json ├── Components │ ├── TabController │ │ └── TabController.swift │ ├── StaticMap │ │ └── MapController.swift │ ├── TableView │ │ └── TableViewController.swift │ └── ButtonTest │ │ └── ButtonController.swift ├── AppDelegate.swift └── Info.plist ├── ReactantPrototyping ├── Assets.xcassets │ ├── Contents.json │ ├── falcon.imageset │ │ ├── falcon.jpg │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── AppDelegate.swift ├── Info.plist └── Base.lproj │ └── LaunchScreen.storyboard ├── Source ├── Validation │ ├── Rules │ │ ├── Rules.swift │ │ ├── Error │ │ │ └── String+EmailValidationError.swift │ │ └── Rules+String.swift │ ├── ValidationError.swift │ └── Rule.swift ├── TableView │ ├── TableViewState.swift │ ├── Properties+TableView.swift │ ├── ReactantTableView.swift │ ├── Identifier │ │ ├── TableViewHeaderFooterIdentifier.swift │ │ ├── AnyTableViewHeaderFooterIdentifier.swift │ │ ├── AnyTableViewCellIdentifier.swift │ │ └── TableViewCellIdentifier.swift │ ├── Configuration+TableView.swift │ ├── TableViewCell.swift │ ├── Internal │ │ └── TableViewHeaderFooterWrapper.swift │ └── Implementation │ │ └── PlainTableView.swift ├── Configuration │ ├── SubConfiguration.swift │ ├── Configuration+Style.swift │ ├── BaseSubConfiguration.swift │ ├── OptionalType.swift │ ├── Properties.swift │ ├── Configurable.swift │ ├── Property.swift │ └── Configuration.swift ├── Utils │ ├── Recycler │ │ ├── UIView+Reusable.swift │ │ ├── Reusable.swift │ │ └── SynchronizedRecycler.swift │ ├── UIImage+Utils.swift │ ├── Scrollable │ │ ├── Scrollable.swift │ │ └── UIScrollView+Scrollable.swift │ ├── Swift+Compatibility.swift │ ├── Dictionary+Utils.swift │ ├── Array+Utils.swift │ ├── Visibility │ │ ├── Visibility.swift │ │ ├── ConstraintAction.swift │ │ └── CollapseAxis.swift │ ├── RxUtils │ │ ├── ObserverType+Utils.swift │ │ └── UIBarButtonItem+ClosureAction.swift │ ├── UINavigationController+DialogDismissalListener.swift │ ├── AssociatedObject.swift │ ├── Hash.swift │ ├── UIStackView+ArrangedChildren.swift │ ├── Collection+Utils.swift │ ├── RangeReplaceableCollection+Utils.swift │ ├── Internal │ │ └── InternalUtils.swift │ └── UIView+Utils.swift ├── Core │ ├── Styling │ │ ├── PercentUtils.swift │ │ ├── Extensions │ │ │ ├── UILabel+Init.swift │ │ │ ├── UITableView+Init.swift │ │ │ ├── UIFont+Init.swift │ │ │ ├── UICollectionView+Init.swift │ │ │ ├── CGPoint+Init.swift │ │ │ ├── CGSize+Init.swift │ │ │ ├── UIOffset+Init.swift │ │ │ ├── CGAffineTransform+Shortcut.swift │ │ │ ├── UIButton+Utils.swift │ │ │ ├── UITabBarController+Styles.swift │ │ │ ├── UINavigationController+Styles.swift │ │ │ ├── UIEdgeInsets+Init.swift │ │ │ ├── CGRect+Init.swift │ │ │ └── UIColor+Init.swift │ │ └── AttributedString │ │ │ └── NSAttributedString+Attribute.swift │ ├── Properties+Style.swift │ ├── Controller │ │ ├── DialogDismissalListener.swift │ │ ├── DialogControllerBase.swift │ │ └── ScrollControllerBase.swift │ ├── View │ │ ├── RootView.swift │ │ ├── Internal │ │ │ ├── DialogView.swift │ │ │ └── ControllerRootViewContainer.swift │ │ ├── ContainerView.swift │ │ └── Impl │ │ │ └── PickerView.swift │ ├── Component │ │ ├── ReactantUI.swift │ │ └── ComponentBase.swift │ ├── Wireframe │ │ ├── Internal │ │ │ └── FutureControllerProvider.swift │ │ └── UIViewController+Navigation.swift │ └── Properties+Core.swift ├── CollectionView │ ├── CollectionViewCell.swift │ ├── FlowCollectionViewBase.swift │ ├── Properties+CollectionView.swift │ ├── ReactantCollectionView.swift │ ├── Identifier │ │ ├── AnyCollectionViewCellIdentifier.swift │ │ ├── AnyCollectionSupplementaryViewIdentifier.swift │ │ ├── CollectionSupplementaryViewIdentifier.swift │ │ └── CollectionViewCellIdentifier.swift │ ├── ReactantCollectionView+Delegates.swift │ ├── Configuration+CollectionView.swift │ ├── Internal │ │ ├── CollectionReusableViewWrapper.swift │ │ └── CollectionViewCellWrapper.swift │ ├── Implementation │ │ ├── SimpleCollectionView.swift │ │ └── PagingCollectionView.swift │ └── CollectionViewState.swift ├── StaticMap │ ├── MKCoordinateRegion+utils.swift │ ├── MKCoordinateSpan+inset.swift │ └── CLLocationCoordinate2D+utils.swift └── Info.plist ├── Example ├── Reactant.xcodeproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── Reactant.xcworkspace │ └── contents.xcworkspacedata ├── Application │ ├── Sources │ │ ├── DependencyModule.swift │ │ ├── AppModule.swift │ │ ├── Service │ │ │ └── NameService.swift │ │ ├── Components │ │ │ ├── Table │ │ │ │ ├── TableRootView.swift │ │ │ │ └── TableViewController.swift │ │ │ └── Main │ │ │ │ ├── LabelView.swift │ │ │ │ ├── MainViewController.swift │ │ │ │ └── MainRootView.swift │ │ ├── AppDelegate.swift │ │ └── Wireframe │ │ │ └── MainWireframe.swift │ └── Resources │ │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Info.plist ├── Podfile ├── Tests │ ├── Supporting Files │ │ └── Info.plist │ └── Tests.swift └── Podfile.lock ├── Reactant.xcodeproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── Reactant.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── .gitignore ├── Podfile ├── LICENSE ├── .travis.yml ├── CHANGELOG.md └── README.md /Tests/Utils/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | docs.reactant.tech 2 | -------------------------------------------------------------------------------- /Tests/StaticMap/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Tests/TableView/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Tests/Validation/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Tests/ActivityIndicator/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Tests/CollectionView/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/parts/controller.md: -------------------------------------------------------------------------------- 1 | # Controllers 2 | 3 | **Documentation will be available shortly.** 4 | -------------------------------------------------------------------------------- /docs/parts/collectionview.md: -------------------------------------------------------------------------------- 1 | # CollectionViews 2 | 3 | **Documentation will be available shortly.** 4 | -------------------------------------------------------------------------------- /docs/img/ReactantTutorial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brightify/Reactant/HEAD/docs/img/ReactantTutorial.png -------------------------------------------------------------------------------- /docs/img/ReactantLiveUIDebug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brightify/Reactant/HEAD/docs/img/ReactantLiveUIDebug.png -------------------------------------------------------------------------------- /docs/img/ReactantLiveUIError.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brightify/Reactant/HEAD/docs/img/ReactantLiveUIError.png -------------------------------------------------------------------------------- /docs/img/Tutorials/RunButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brightify/Reactant/HEAD/docs/img/Tutorials/RunButton.png -------------------------------------------------------------------------------- /docs/img/ReactantLiveUIPreview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brightify/Reactant/HEAD/docs/img/ReactantLiveUIPreview.png -------------------------------------------------------------------------------- /TVPrototyping/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ReactantPrototyping/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /docs/img/Tutorials/Notes/Simulator1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brightify/Reactant/HEAD/docs/img/Tutorials/Notes/Simulator1.png -------------------------------------------------------------------------------- /docs/img/Tutorials/Notes/Simulator2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brightify/Reactant/HEAD/docs/img/Tutorials/Notes/Simulator2.png -------------------------------------------------------------------------------- /docs/img/Tutorials/GitExplorer/Users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brightify/Reactant/HEAD/docs/img/Tutorials/GitExplorer/Users.png -------------------------------------------------------------------------------- /docs/img/Tutorials/GitExplorer/Repositories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brightify/Reactant/HEAD/docs/img/Tutorials/GitExplorer/Repositories.png -------------------------------------------------------------------------------- /docs/img/Tutorials/GitExplorer/UserHeader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brightify/Reactant/HEAD/docs/img/Tutorials/GitExplorer/UserHeader.png -------------------------------------------------------------------------------- /docs/img/Tutorials/Notes/Reactant-init-console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brightify/Reactant/HEAD/docs/img/Tutorials/Notes/Reactant-init-console.png -------------------------------------------------------------------------------- /Tests/Core/Component/Note.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Note.swift 3 | // Reactant-Notes 4 | // 5 | // Created by Matyáš Kříž on 27/10/2017. 6 | // 7 | 8 | import Foundation 9 | -------------------------------------------------------------------------------- /ReactantPrototyping/Assets.xcassets/falcon.imageset/falcon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brightify/Reactant/HEAD/ReactantPrototyping/Assets.xcassets/falcon.imageset/falcon.jpg -------------------------------------------------------------------------------- /TVPrototyping/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /TVPrototyping/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /TVPrototyping/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /TVPrototyping/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /TVPrototyping/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /TVPrototyping/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Source/Validation/Rules/Rules.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Rules.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 10.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | public struct Rules { 10 | } 11 | -------------------------------------------------------------------------------- /Example/Reactant.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Reactant.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Source/TableView/TableViewState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewState.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 12.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | public typealias TableViewState = CollectionViewState 10 | -------------------------------------------------------------------------------- /Source/Validation/ValidationError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValidationError.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 10.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | public enum ValidationError: Error { 10 | case invalid 11 | } 12 | -------------------------------------------------------------------------------- /TVPrototyping/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /TVPrototyping/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Reactant.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Reactant.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Reactant.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /TVPrototyping/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /TVPrototyping/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /TVPrototyping/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /TVPrototyping/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /TVPrototyping/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /TVPrototyping/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /Example/Application/Sources/DependencyModule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DependencyModule.swift 3 | // Reactant 4 | // 5 | // Created by Matous Hybl on 3/24/17. 6 | // Copyright © 2017 Brightify s.r.o. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol DependencyModule { 12 | var nameService: NameService { get } 13 | } 14 | -------------------------------------------------------------------------------- /Source/Configuration/SubConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SubConfiguration.swift 3 | // Reactant 4 | // 5 | // Created by Robin Krenecky on 24/04/2018. 6 | // Copyright © 2018 Brightify. All rights reserved. 7 | // 8 | 9 | public protocol SubConfiguration { 10 | var configuration: Configuration { get } 11 | 12 | init(configuration: Configuration) 13 | } 14 | -------------------------------------------------------------------------------- /TVPrototyping/Assets.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "landscape", 5 | "idiom" : "tv", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "9.0", 8 | "scale" : "1x" 9 | } 10 | ], 11 | "info" : { 12 | "version" : 1, 13 | "author" : "xcode" 14 | } 15 | } -------------------------------------------------------------------------------- /Source/Utils/Recycler/UIView+Reusable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Reusable.swift 3 | // Reactant 4 | // 5 | // Created by Tadeáš Kříž on 1/24/17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView: Reusable { 12 | 13 | public func prepareForReuse() { 14 | removeFromSuperview() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Source/Utils/UIImage+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Utils.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 21.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIImage { 12 | 13 | public var aspectRatio: CGFloat { 14 | return size.width / size.height 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /Example/Application/Sources/AppModule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppModule.swift 3 | // Reactant 4 | // 5 | // Created by Matous Hybl on 3/24/17. 6 | // Copyright © 2017 Brightify s.r.o. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class AppModule: DependencyModule { 12 | 13 | var nameService: NameService { 14 | return NameService() 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Example/Application/Sources/Service/NameService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NameService.swift 3 | // Reactant 4 | // 5 | // Created by Matous Hybl on 3/24/17. 6 | // Copyright © 2017 Brightify s.r.o. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct NameService { 12 | 13 | func names() -> [String] { 14 | return ["Tadeas", "Filip", "Matous"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Source/Core/Styling/PercentUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PercentUtils.swift 3 | // Reactant 4 | // 5 | // Created by Tadeas Kriz on 24/01/16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | postfix operator % 12 | 13 | /// Returns input / 100. 14 | public postfix func %(input: CGFloat) -> CGFloat { 15 | return input / 100 16 | } 17 | 18 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '9.0' 2 | use_frameworks! 3 | 4 | target 'Reactant_Example' do 5 | pod 'Reactant', :path => '../' 6 | pod 'Reactant/TableView', :path => '../' 7 | pod 'Reactant/CollectionView', :path => '../' 8 | pod 'Reactant/Validation', :path => '../' 9 | 10 | target 'Reactant_Tests' do 11 | inherit! :search_paths 12 | 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /Source/Validation/Rules/Error/String+EmailValidationError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+EmailValidationError.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 10.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | extension Rules.String { 10 | 11 | public enum EmailValidationError: Error { 12 | case empty 13 | case invalid 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docs/scripts/zopim.js: -------------------------------------------------------------------------------- 1 | window.$zopim||(function(d,s){var z=$zopim=function(c){z._.push(c)},$=z.s= 2 | d.createElement(s),e=d.getElementsByTagName(s)[0];z.set=function(o){z.set. 3 | _.push(o)};z._=[];z.set._=[];$.async=!0;$.setAttribute("charset","utf-8"); 4 | $.src="https://v2.zopim.com/?4o5d8TVw1Gt95sL69RIn0iWT2V4NSeye";z.t=+new Date;$. 5 | type="text/javascript";e.parentNode.insertBefore($,e)})(document,"script"); 6 | -------------------------------------------------------------------------------- /Source/Core/Styling/Extensions/UILabel+Init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UILabel+Init.swift 3 | // Reactant 4 | // 5 | // Created by Tadeas Kriz on 31/03/16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UILabel { 12 | 13 | public convenience init(text: String) { 14 | self.init() 15 | 16 | self.text = text 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /TVPrototyping/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "layers" : [ 3 | { 4 | "filename" : "Front.imagestacklayer" 5 | }, 6 | { 7 | "filename" : "Middle.imagestacklayer" 8 | }, 9 | { 10 | "filename" : "Back.imagestacklayer" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | } 17 | } -------------------------------------------------------------------------------- /Source/Core/Styling/Extensions/UITableView+Init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableView+Init.swift 3 | // Reactant 4 | // 5 | // Created by Tadeas Kriz on 31/03/16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UITableView { 12 | 13 | public convenience init(style: UITableView.Style) { 14 | self.init(frame: CGRect.zero, style: style) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Source/Utils/Recycler/Reusable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Reusable.swift 3 | // Reactant 4 | // 5 | // Created by Tadeáš Kříž on 1/24/17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | public protocol Reusable: class { 10 | /** 11 | * Called during recyclation. Usually resets the Component's state. 12 | * NOTE: For more info see `Recycler`. 13 | */ 14 | func prepareForReuse() 15 | } 16 | -------------------------------------------------------------------------------- /TVPrototyping/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "layers" : [ 3 | { 4 | "filename" : "Front.imagestacklayer" 5 | }, 6 | { 7 | "filename" : "Middle.imagestacklayer" 8 | }, 9 | { 10 | "filename" : "Back.imagestacklayer" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | } 17 | } -------------------------------------------------------------------------------- /Source/Utils/Scrollable/Scrollable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Scrollable.swift 3 | // Reactant 4 | // 5 | // Created by Matouš Hýbl on 7/13/16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | public protocol Scrollable { 10 | 11 | func scrollToTop(animated: Bool) 12 | } 13 | 14 | public extension Scrollable { 15 | 16 | func scrollToTop() { 17 | scrollToTop(animated: true) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/docpress.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": "Brightify/Reactant", 3 | "markdown": { 4 | "typographer": true, 5 | "plugins": { 6 | "decorate": {} 7 | } 8 | }, 9 | "plugins": { 10 | "docpress-core": {}, 11 | "docpress-base": {} 12 | }, 13 | "scripts": [ 14 | "scripts/zopim.js" 15 | ], 16 | "googleAnalytics": { 17 | "id": "UA-48374666-17", 18 | "domain": "docs.reactant.tech" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Source/Core/Styling/Extensions/UIFont+Init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIFont+Init.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 16.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIFont { 12 | 13 | public convenience init(_ name: String, _ size: CGFloat) { 14 | self.init(descriptor: UIFontDescriptor(name: name, size: size), size: 0) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ReactantPrototyping/Assets.xcassets/falcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "filename" : "falcon.jpg", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Source/Configuration/Configuration+Style.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Configuration+Style.swift 3 | // Reactant 4 | // 5 | // Created by Robin Krenecky on 24/04/2018. 6 | // Copyright © 2018 Brightify. All rights reserved. 7 | // 8 | 9 | public final class StyleConfiguration: BaseSubConfiguration { 10 | } 11 | 12 | extension Configuration { 13 | public var style: StyleConfiguration { 14 | return StyleConfiguration(configuration: self) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Source/Configuration/BaseSubConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseSubConfiguration.swift 3 | // Reactant 4 | // 5 | // Created by Robin Krenecky on 24/04/2018. 6 | // Copyright © 2018 Brightify. All rights reserved. 7 | // 8 | 9 | open class BaseSubConfiguration: SubConfiguration { 10 | public let configuration: Configuration 11 | 12 | public required init(configuration: Configuration) { 13 | self.configuration = configuration 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Source/Core/Properties+Style.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Properties+Style.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 15.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | extension Properties.Style { 10 | 11 | public static func style(for type: T.Type, defaultValue: @escaping (T) -> Void = { _ in }) -> Property<(T) -> Void> { 12 | return Property<(T) -> Void>(defaultValue: defaultValue) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Source/Core/Styling/Extensions/UICollectionView+Init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView+Init.swift 3 | // Reactant 4 | // 5 | // Created by Tadeas Kriz on 31/03/16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UICollectionView { 12 | 13 | public convenience init(collectionViewLayout layout: UICollectionViewLayout) { 14 | self.init(frame: CGRect.zero, collectionViewLayout: layout) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Source/Utils/Scrollable/UIScrollView+Scrollable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+Scrollable.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 09.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIScrollView: Scrollable { 12 | 13 | public func scrollToTop(animated: Bool) { 14 | let inset = contentInset 15 | setContentOffset(CGPoint(x: -inset.left, y: -inset.top), animated: animated) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Source/Utils/Swift+Compatibility.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Swift+Compatibility.swift 3 | // Reactant 4 | // 5 | // Created by Matouš Hýbl on 06/04/2018. 6 | // Copyright © 2018 Brightify. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | #if !swift(>=4.1) 12 | public extension Sequence { 13 | func compactMap(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] { 14 | return try self.flatMap(transform) 15 | } 16 | } 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /Source/Utils/Dictionary+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dictionary+Utils.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 21.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Dictionary { 12 | 13 | public init(keyValueTuples: [(Key, Value)]) { 14 | var result: [Key: Value] = [:] 15 | for item in keyValueTuples { 16 | result[item.0] = item.1 17 | } 18 | self = result 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /Source/Core/Controller/DialogDismissalListener.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DialogDismissalListener.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 09.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | public protocol DialogDismissalListener { 10 | 11 | func dialogWillDismiss() 12 | 13 | func dialogDidDismiss() 14 | } 15 | 16 | extension DialogDismissalListener { 17 | 18 | public func dialogWillDismiss() { } 19 | 20 | public func dialogDidDismiss() { } 21 | } 22 | -------------------------------------------------------------------------------- /Source/Configuration/OptionalType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionalType.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 14.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | /// A helper protocol to allow Property's default value be nil automatically, if its type is optional. 10 | public protocol OptionalType { 11 | static var null: Self { get } 12 | } 13 | 14 | extension Optional: OptionalType { 15 | public static var null: Optional { 16 | return nil 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Source/Core/Styling/Extensions/CGPoint+Init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGPoint+Init.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 16.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension CGPoint { 12 | 13 | public init(_ both: CGFloat) { 14 | self.init(x: both, y: both) 15 | } 16 | 17 | public init(x: CGFloat) { 18 | self.init(x: x, y: 0) 19 | } 20 | 21 | public init(y: CGFloat) { 22 | self.init(x: 0, y: y) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/Core/Styling/UILabel+InitTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UILabel+InitTest.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 18.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import Reactant 12 | 13 | class UILabelInitTest: QuickSpec { 14 | 15 | override func spec() { 16 | describe("UILabel init") { 17 | it("creates UILabel with text") { 18 | expect(UILabel(text: "text").text) == "text" 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Source/Core/Styling/Extensions/CGSize+Init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGSize+Init.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 16.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension CGSize { 12 | 13 | public init(_ both: CGFloat) { 14 | self.init(width: both, height: both) 15 | } 16 | 17 | public init(width: CGFloat) { 18 | self.init(width: width, height: 0) 19 | } 20 | 21 | public init(height: CGFloat) { 22 | self.init(width: 0, height: height) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Source/Utils/Array+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Utils.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 20.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | extension Array { 10 | 11 | public func arrayByAppending(_ elements: Element...) -> Array { 12 | return arrayByAppending(elements) 13 | } 14 | 15 | public func arrayByAppending(_ elements: [Element]) -> Array { 16 | var mutableCopy = self 17 | mutableCopy.append(contentsOf: elements) 18 | return mutableCopy 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Source/Utils/Visibility/Visibility.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Visibility.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 21.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | import Foundation 9 | 10 | /** 11 | * ## Visible 12 | * View is fully visible with no modifications. 13 | * ## Hidden 14 | * View is not visible, but its dimensions stay the same. 15 | * ## Collapsed 16 | * View is not visible and is squashed on either X or Y axis. 17 | */ 18 | @objc 19 | public enum Visibility: Int { 20 | case visible 21 | case hidden 22 | case collapsed 23 | } 24 | -------------------------------------------------------------------------------- /Example/Application/Sources/Components/Table/TableRootView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableRootView.swift 3 | // Reactant 4 | // 5 | // Created by Matous Hybl on 3/24/17. 6 | // Copyright © 2017 Brightify s.r.o. All rights reserved. 7 | // 8 | 9 | import Reactant 10 | 11 | class TableViewRootView: PlainTableView, RootView { 12 | 13 | override var edgesForExtendedLayout: UIRectEdge { 14 | return .all 15 | } 16 | 17 | init() { 18 | super.init(cellFactory: LabelView.init, 19 | style: .plain, 20 | reloadable: false) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Tests/TestUtils/ComponentBase+spy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComponentBase+spy.swift 3 | // ReactantTests 4 | // 5 | // Created by Tadeas Kriz on 07/11/2019. 6 | // Copyright © 2019 Brightify. All rights reserved. 7 | // 8 | 9 | import Reactant 10 | import Cuckoo 11 | 12 | extension ComponentBase { 13 | static func spy(canUpdate: Bool = true) -> MockComponentBase { 14 | return createMock(MockComponentBase.self) { builder, stub in 15 | builder.enableSuperclassSpy() 16 | return MockComponentBase(canUpdate: canUpdate) 17 | } 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Source/Core/Styling/Extensions/UIOffset+Init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIOffset+Init.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 16.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIOffset { 12 | 13 | public init(_ all: CGFloat) { 14 | self.init(horizontal: all, vertical: all) 15 | } 16 | 17 | public init(horizontal: CGFloat) { 18 | self.init(horizontal: horizontal, vertical: 0) 19 | } 20 | 21 | public init(vertical: CGFloat) { 22 | self.init(horizontal: 0, vertical: vertical) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Source/TableView/Properties+TableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Properties+TableView.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 15.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension Properties.Style { 12 | 13 | public struct TableView { 14 | 15 | public static let tableView = Properties.Style.style(for: ReactantTableView.self) 16 | public static let headerFooterWrapper = Properties.Style.style(for: UITableViewHeaderFooterView.self) 17 | public static let cellWrapper = Properties.Style.style(for: UITableViewCell.self) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/Core/Styling/UIFont+InitTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIFont+InitTest.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 18.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import Reactant 12 | 13 | class UIFontInitTest: QuickSpec { 14 | 15 | override func spec() { 16 | describe("UIFont init") { 17 | it("creates UIFont") { 18 | let font = UIFont("HelveticaNeue", 12) 19 | 20 | expect(font.fontName) == "HelveticaNeue" 21 | expect(font.pointSize) == 12 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Source/Utils/Visibility/ConstraintAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConstraintAction.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 21.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /** 12 | * ## Set Constant 13 | * Sets constants to `Constraint` in its visible and collapsed form. 14 | * ## Install 15 | * Activates the constraint. 16 | * ## Uninstall 17 | * Deactivates the constraint. 18 | * - NOTE: Deactivated constraint is not taken into account when AutoLayouting. 19 | */ 20 | public enum ConstraintAction { 21 | case setConstant(visible: CGFloat, collapsed: CGFloat) 22 | case install 23 | case uninstall 24 | } 25 | -------------------------------------------------------------------------------- /Source/CollectionView/CollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewCell.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 14.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol CollectionViewCell { 12 | 13 | func setSelected(_ selected: Bool) 14 | 15 | func setHighlighted(_ highlighted: Bool) 16 | } 17 | 18 | extension CollectionViewCell { 19 | /// Called after the user lifts the finger after tapping the cell. 20 | public func setSelected(_ selected: Bool) { 21 | } 22 | 23 | /// Called when user taps the cell. 24 | public func setHighlighted(_ highlighted: Bool) { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Source/TableView/ReactantTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReactantTableView.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 16.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol ReactantTableView: class, Scrollable { 12 | 13 | var tableView: UITableView { get } 14 | #if os(iOS) 15 | var refreshControl: UIRefreshControl? { get } 16 | #endif 17 | var emptyLabel: UILabel { get } 18 | var loadingIndicator: UIActivityIndicatorView { get } 19 | } 20 | 21 | extension ReactantTableView { 22 | 23 | public func scrollToTop(animated: Bool) { 24 | tableView.scrollToTop(animated: animated) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Source/Utils/RxUtils/ObserverType+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObserverType+Utils.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 20.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | 11 | public extension ObserverType { 12 | 13 | /** 14 | * Convenience method equivalent to `on(.Next(element: E))` and `on(.Completed)` 15 | * - parameter element: Next element to send to observer(s) 16 | */ 17 | func onLast(_ element: Element) { 18 | on(.next(element)) 19 | on(.completed) 20 | } 21 | } 22 | 23 | public extension ObserverType where Element == Void { 24 | func onLast() { 25 | onLast(()) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Source/Utils/Visibility/CollapseAxis.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollapseAxis.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 21.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | import Foundation 9 | 10 | /** 11 | * Axis on which a **UIView** should collapse. 12 | * ## Horizontal 13 | * **UIView** collapses on horizontal axis (i.e. its width is squashed). 14 | * ## Vertical 15 | * **UIView** collapses on vertical axis (i.e. its height is squashed). 16 | * ## Both 17 | * **UIView** collapses on both axes (i.e. both its weight and height are squashed). 18 | */ 19 | @objc 20 | public enum CollapseAxis: Int { 21 | case horizontal 22 | case vertical 23 | case both 24 | } 25 | -------------------------------------------------------------------------------- /Tests/Core/Styling/UITableView+InitTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableView+InitTest.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 18.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import Reactant 12 | 13 | class UITableViewInitTest: QuickSpec { 14 | 15 | override func spec() { 16 | describe("UITableView init") { 17 | it("creates UITableView with zero CGRect") { 18 | let view = UITableView(style: .plain) 19 | 20 | expect(view.frame) == CGRect.zero 21 | expect(view.style) == UITableView.Style.plain 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /TVPrototyping/Components/TabController/TabController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabController.swift 3 | // TVPrototyping 4 | // 5 | // Created by Matous Hybl on 03/11/2017. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import Reactant 10 | 11 | class TabController: UITabBarController { 12 | 13 | private let buttonController = ButtonController() 14 | private let tableController = TableViewController() 15 | private let collectionController = CollectionViewController() 16 | private let mapController = MapController() 17 | 18 | override func viewDidLoad() { 19 | setViewControllers([buttonController, tableController, collectionController, mapController], animated: false) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/Core/Styling/CGPoint+InitTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGPoint+InitTest.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 18.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import Reactant 12 | 13 | class CGPointInitTest: QuickSpec { 14 | 15 | override func spec() { 16 | describe("CGPoint init") { 17 | it("creates CGPoint") { 18 | expect(CGPoint(x: 0, y: 0)) == CGPoint() 19 | expect(CGPoint(x: 1, y: 1)) == CGPoint(1) 20 | expect(CGPoint(x: 1, y: 0)) == CGPoint(x: 1) 21 | expect(CGPoint(x: 0, y: 1)) == CGPoint(y: 1) 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Source/Utils/UINavigationController+DialogDismissalListener.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UINavigationController+DialogDismissListener.swift 3 | // Reactant 4 | // 5 | // Created by Matouš Hýbl on 29/05/2017. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UINavigationController: DialogDismissalListener { 12 | 13 | public func dialogWillDismiss() { 14 | if let listener = topViewController as? DialogDismissalListener { 15 | listener.dialogWillDismiss() 16 | } 17 | } 18 | 19 | public func dialogDidDismiss() { 20 | if let listener = topViewController as? DialogDismissalListener { 21 | listener.dialogDidDismiss() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Source/Configuration/Properties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Properties.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 14.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | /// A structure containing all available properties. These are added through extensions. 10 | public struct Properties { 11 | 12 | // Has to be declared here because of https://bugs.swift.org/browse/SR-631 . (Move to Properties+Style when resolved.) 13 | /** 14 | * A structure for style properties. The main difference is that style properties are clusures with one parameter 15 | * which is the styled view. 16 | */ 17 | public struct Style { 18 | private init() { } 19 | } 20 | 21 | private init() { } 22 | } 23 | -------------------------------------------------------------------------------- /Source/StaticMap/MKCoordinateRegion+utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MKCoordinateRegion+utils.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 26.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import MapKit 10 | 11 | extension MKCoordinateRegion { 12 | 13 | public func inset(percent: Double) -> MKCoordinateRegion { 14 | return inset(horizontalPercent: percent, verticalPercent: percent) 15 | } 16 | 17 | public func inset(horizontalPercent horizontal: Double, verticalPercent vertical: Double) -> MKCoordinateRegion { 18 | return MKCoordinateRegion( 19 | center: center, 20 | span: span.inset(horizontalPercent: horizontal, verticalPercent: vertical)) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Source/CollectionView/FlowCollectionViewBase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlowCollectionViewBase.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 13.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class FlowCollectionViewBase: CollectionViewBase { 12 | 13 | public let collectionViewLayout = UICollectionViewFlowLayout() 14 | 15 | /// - parameter reloadable: determines whether the **FlowCollectionViewBase** should be reloadable by pulling 16 | public init(reloadable: Bool = true, automaticallyDeselect: Bool = true) { 17 | super.init(layout: collectionViewLayout, reloadable: reloadable, automaticallyDeselect: automaticallyDeselect) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Source/CollectionView/Properties+CollectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Properties+CollectionView.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 15.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension Properties.Style { 12 | 13 | public struct CollectionView { 14 | 15 | public static let collectionView = Properties.Style.style(for: ReactantCollectionView.self) 16 | public static let reusableViewWrapper = Properties.Style.style(for: UICollectionReusableView.self) 17 | public static let cellWrapper = Properties.Style.style(for: UICollectionViewCell.self) 18 | public static let pageControl = Properties.Style.style(for: UIPageControl.self) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Source/StaticMap/MKCoordinateSpan+inset.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MKCoordinateSpan+inset.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 26.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import MapKit 10 | 11 | extension MKCoordinateSpan { 12 | 13 | public func inset(percent: Double) -> MKCoordinateSpan { 14 | return inset(horizontalPercent: percent, verticalPercent: percent) 15 | } 16 | 17 | public func inset(horizontalPercent horizontal: Double, verticalPercent vertical: Double) -> MKCoordinateSpan { 18 | return MKCoordinateSpan( 19 | latitudeDelta: latitudeDelta * (1 + vertical), 20 | longitudeDelta: longitudeDelta * (1 + horizontal) 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /TVPrototyping/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // TVPrototyping 4 | // 5 | // Created by Matouš Hýbl on 02/11/2017. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Reactant 11 | import SnapKit 12 | 13 | @UIApplicationMain 14 | class AppDelegate: UIResponder, UIApplicationDelegate { 15 | 16 | var window: UIWindow? 17 | 18 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool { 19 | let window = UIWindow() 20 | self.window = window 21 | window.backgroundColor = .white 22 | window.rootViewController = TabController() 23 | window.makeKeyAndVisible() 24 | return true 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | 25 | Carthage 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 29 | # 30 | # Note: if you ignore the Pods directory, make sure to uncomment 31 | # `pod install` in .travis.yml 32 | # 33 | Pods/ 34 | _docpress 35 | .idea 36 | *.xcworkspace 37 | -------------------------------------------------------------------------------- /Example/Application/Sources/Components/Main/LabelView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LabelView.swift 3 | // Reactant 4 | // 5 | // Created by Matous Hybl on 3/17/17. 6 | // Copyright © 2017 Brightify s.r.o. All rights reserved. 7 | // 8 | 9 | import Reactant 10 | 11 | final class LabelView: ViewBase { 12 | 13 | private let label = UILabel() 14 | 15 | override func update() { 16 | label.text = componentState 17 | } 18 | 19 | override func loadView() { 20 | children( 21 | label 22 | ) 23 | 24 | label.textColor = .black 25 | label.textAlignment = .center 26 | } 27 | 28 | override func setupConstraints() { 29 | label.snp.makeConstraints { make in 30 | make.edges.equalToSuperview() 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/Core/Styling/AttributeTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttributeTest.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 18.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import Reactant 12 | 13 | class AttributeTest: QuickSpec { 14 | 15 | override func spec() { 16 | describe("toDictionary") { 17 | it("creates dictionary from array of attributes") { 18 | let attributes = [Attribute.baselineOffset(1), Attribute.expansion(2)].toDictionary() 19 | 20 | expect(attributes[NSAttributedString.Key.baselineOffset] as? Float) == 1 21 | expect(attributes[NSAttributedString.Key.expansion] as? Float) == 2 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ReactantPrototyping/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ReactantPrototyping 4 | // 5 | // Created by Filip Dolnik on 16.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 17 | let window = UIWindow() 18 | self.window = window 19 | window.backgroundColor = .white 20 | window.rootViewController = UINavigationController(rootViewController: ViewController()) 21 | window.makeKeyAndVisible() 22 | return true 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /Tests/Core/Styling/UICollectionView+InitTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView+InitTest.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 18.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import Reactant 12 | 13 | class UICollectionViewInitTest: QuickSpec { 14 | 15 | override func spec() { 16 | describe("UICollectionView init") { 17 | it("creates UICollectionView with zero CGRect") { 18 | let layout = UICollectionViewFlowLayout() 19 | let view = UICollectionView(collectionViewLayout: layout) 20 | 21 | expect(view.frame) == CGRect.zero 22 | expect(view.collectionViewLayout) == layout 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/Core/Styling/UIOffset+InitTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIOffset+InitTest.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 18.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import Reactant 12 | 13 | class UIOffsetInitTest: QuickSpec { 14 | 15 | override func spec() { 16 | describe("UIOffset init") { 17 | it("creates UIOffset") { 18 | expect(UIOffset(horizontal: 0, vertical: 0)) == UIOffset() 19 | expect(UIOffset(horizontal: 1, vertical: 1)) == UIOffset(1) 20 | expect(UIOffset(horizontal: 1, vertical: 0)) == UIOffset(horizontal: 1) 21 | expect(UIOffset(horizontal: 0, vertical: 1)) == UIOffset(vertical: 1) 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/Core/Styling/CGSize+InitTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGSize+InitTest.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 18.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import Reactant 12 | 13 | class CGSizeInitTest: QuickSpec { 14 | 15 | override func spec() { 16 | describe("CGSize init") { 17 | it("creates CGSize") { 18 | expect(CGSize(width: 0 as CGFloat, height: 0 as CGFloat)) == CGSize() 19 | expect(CGSize(width: 0, height: 0)) == CGSize() 20 | expect(CGSize(width: 1, height: 1)) == CGSize(1) 21 | expect(CGSize(width: 1, height: 0)) == CGSize(width: 1) 22 | expect(CGSize(width: 0, height: 1)) == CGSize(height: 1) 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Source/Core/View/RootView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootView.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 09.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol RootView { 12 | 13 | var edgesForExtendedLayout: UIRectEdge { get } 14 | 15 | func viewWillAppear() 16 | 17 | func viewDidAppear() 18 | 19 | func viewWillDisappear() 20 | 21 | func viewDidDisappear() 22 | } 23 | 24 | extension RootView { 25 | 26 | public var edgesForExtendedLayout: UIRectEdge { 27 | return [] 28 | } 29 | 30 | public func viewWillAppear() { 31 | } 32 | 33 | public func viewDidAppear() { 34 | } 35 | 36 | public func viewWillDisappear() { 37 | } 38 | 39 | public func viewDidDisappear() { 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Example/Tests/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Source/Utils/Recycler/SynchronizedRecycler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SynchronizedRecycler.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 21.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class SynchronizedRecycler: Recycler { 12 | 13 | private let syncQueue = DispatchQueue(label: "SynchronizedRecycler_syncQueue") 14 | 15 | public override func obtain() -> T { 16 | return syncQueue.sync { 17 | return super.obtain() 18 | } 19 | } 20 | 21 | public override func recycle(_ instance: T) { 22 | syncQueue.sync { 23 | super.recycle(instance) 24 | } 25 | } 26 | 27 | public override func recycleAll() { 28 | syncQueue.sync { 29 | super.recycleAll() 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /TVPrototyping/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "size" : "1280x768", 5 | "idiom" : "tv", 6 | "filename" : "App Icon - App Store.imagestack", 7 | "role" : "primary-app-icon" 8 | }, 9 | { 10 | "size" : "400x240", 11 | "idiom" : "tv", 12 | "filename" : "App Icon.imagestack", 13 | "role" : "primary-app-icon" 14 | }, 15 | { 16 | "size" : "2320x720", 17 | "idiom" : "tv", 18 | "filename" : "Top Shelf Image Wide.imageset", 19 | "role" : "top-shelf-image-wide" 20 | }, 21 | { 22 | "size" : "1920x720", 23 | "idiom" : "tv", 24 | "filename" : "Top Shelf Image.imageset", 25 | "role" : "top-shelf-image" 26 | } 27 | ], 28 | "info" : { 29 | "version" : 1, 30 | "author" : "xcode" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/Core/Styling/PercentUtilsTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PercentUtilsTest.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 18.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import Reactant 12 | 13 | class PercentUtilsTest: QuickSpec { 14 | 15 | override func spec() { 16 | describe("%") { 17 | it("returns percents") { 18 | expect(35%).to(beCloseTo(0.35, within: 0.35.ulp)) 19 | } 20 | it("handles edge cases") { 21 | expect(0%).to(beCloseTo(0.0, within: 0.0.ulp)) 22 | expect(100%).to(beCloseTo(1.0, within: Double.ulpOfOne)) 23 | expect(-20%).to(beCloseTo(-0.2, within: 0.2.ulp)) 24 | expect(1058%).to(beCloseTo(10.58, within: 10.58.ulp)) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Example/Tests/Tests.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import XCTest 3 | import Reactant 4 | 5 | class Tests: XCTestCase { 6 | 7 | override func setUp() { 8 | super.setUp() 9 | // Put setup code here. This method is called before the invocation of each test method in the class. 10 | } 11 | 12 | override func tearDown() { 13 | // Put teardown code here. This method is called after the invocation of each test method in the class. 14 | super.tearDown() 15 | } 16 | 17 | func testExample() { 18 | // This is an example of a functional test case. 19 | XCTAssert(true, "Pass") 20 | } 21 | 22 | func testPerformanceExample() { 23 | // This is an example of a performance test case. 24 | self.measure() { 25 | // Put the code you want to measure the time of here. 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Source/CollectionView/ReactantCollectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReactantCollectionView.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 16.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol ReactantCollectionView: class, Scrollable { 12 | 13 | var collectionView: UICollectionView { get } 14 | #if os(iOS) 15 | var refreshControl: UIRefreshControl? { get } 16 | #endif 17 | var emptyLabel: UILabel { get } 18 | var loadingIndicator: UIActivityIndicatorView { get } 19 | } 20 | 21 | extension ReactantCollectionView { 22 | 23 | /** 24 | * Scroll the Reactant Collection View to top. 25 | * - parameter animated: determines whether the scroll to top is animated 26 | */ 27 | public func scrollToTop(animated: Bool) { 28 | collectionView.scrollToTop(animated: animated) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Source/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.6 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Source/Core/Component/ReactantUI.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | public protocol ReactantUI: class { 4 | var __rui: ReactantUIContainer { get } 5 | } 6 | 7 | public protocol ReactantUIContainer: class { 8 | var xmlPath: String { get } 9 | 10 | var typeName: String { get } 11 | 12 | func setupReactantUI() 13 | 14 | func updateReactantUI() 15 | 16 | static func destroyReactantUI(target: UIView) 17 | } 18 | 19 | internal extension UIView { 20 | func tryUpdateReactantUI() { 21 | guard self is ReactantUI else { 22 | return 23 | } 24 | 25 | updateReactantUIRecursive() 26 | } 27 | 28 | private func updateReactantUIRecursive() { 29 | if let reactantUi = self as? ReactantUI { 30 | reactantUi.__rui.updateReactantUI() 31 | } 32 | 33 | for child in subviews { 34 | child.updateReactantUIRecursive() 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Source/Core/Styling/Extensions/CGAffineTransform+Shortcut.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGAffineTransform+Shortcut.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 16.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public func + (lhs: CGAffineTransform, rhs: CGAffineTransform) -> CGAffineTransform { 12 | return lhs.concatenating(rhs) 13 | } 14 | 15 | public func rotate(degrees: CGFloat) -> CGAffineTransform { 16 | return rotate(degrees / 180 * .pi) 17 | } 18 | 19 | public func rotate(_ radians: CGFloat = 0) -> CGAffineTransform { 20 | return CGAffineTransform(rotationAngle: radians) 21 | } 22 | 23 | public func translate(x: CGFloat = 0, y: CGFloat = 0) -> CGAffineTransform { 24 | return CGAffineTransform(translationX: x, y: y) 25 | } 26 | 27 | public func scale(x: CGFloat = 1, y: CGFloat = 1) -> CGAffineTransform { 28 | return CGAffineTransform(scaleX: x, y: y) 29 | } 30 | -------------------------------------------------------------------------------- /TVPrototyping/Components/StaticMap/MapController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapController.swift 3 | // TVPrototyping 4 | // 5 | // Created by Matous Hybl on 03/11/2017. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import Reactant 10 | import MapKit 11 | 12 | final class MapController: ControllerBase { 13 | 14 | override func afterInit() { 15 | tabBarItem = UITabBarItem(title: "StaticMap", image: nil, tag: 0) 16 | } 17 | } 18 | 19 | final class MapRootView: ViewBase, RootView { 20 | 21 | private let map = StaticMap() 22 | 23 | override func update() { 24 | map.componentState = MKCoordinateRegionMakeWithDistance(CLLocationCoordinate2DMake(40.0, 14.0), 10000, 10000) 25 | } 26 | 27 | override func loadView() { 28 | children(map) 29 | 30 | map.snp.makeConstraints { make in 31 | make.edges.equalToSuperview() 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Source/Core/Wireframe/Internal/FutureControllerProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FutureControllerProvider.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 09.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public final class FutureControllerProvider { 12 | 13 | /** 14 | * The get-only controller that provider is working with. 15 | * 16 | * Thanks to generics you can access all the methods you would expect from any subclass of **UIViewController**. 17 | */ 18 | public internal(set) weak var controller: T? 19 | 20 | /** 21 | * Controller's navigation controller. Useful for not having to pass UINavigationController around. 22 | * 23 | * - NOTE: See also **UINavigationController+Navigation.swift** for useful methods. 24 | */ 25 | public var navigation: UINavigationController? { 26 | return controller?.navigationController 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | * [Reactant](../README.md) 4 | 5 | * Getting started 6 | * [Quick-start guide](getting-started/quickstart.md) 7 | * [Architecture](getting-started/architecture.md) 8 | * [Troubleshooting](getting-started/troubleshooting.md) 9 | 10 | * Tutorials 11 | * [Simple note taking app](tutorials/notes.md) 12 | * [Random GitHub users explorer](tutorials/explorer.md) 13 | 14 | * Reactant UI 15 | * [Introduction](reactant-ui/introduction.md) 16 | * [Live Reload](reactant-ui/live-reload.md) 17 | 18 | * Parts 19 | * [Configuration](parts/configuration.md) 20 | * [Controller](parts/controller.md) 21 | * [RootView](parts/rootview.md) 22 | * [Wireframe](parts/wireframe.md) 23 | * [Styling](parts/styling.md) 24 | * [TableView](parts/tableview.md) 25 | * [CollectionView](parts/collectionview.md) 26 | * [Validation](parts/validation.md) 27 | * [ActivityIndicator](parts/activityindicator.md) 28 | * [StaticMap](parts/staticmap.md) 29 | -------------------------------------------------------------------------------- /TVPrototyping/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIRequiredDeviceCapabilities 24 | 25 | arm64 26 | 27 | UIUserInterfaceStyle 28 | Automatic 29 | 30 | 31 | -------------------------------------------------------------------------------- /Example/Application/Resources/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | } 43 | ], 44 | "info" : { 45 | "version" : 1, 46 | "author" : "xcode" 47 | } 48 | } -------------------------------------------------------------------------------- /Example/Application/Sources/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Reactant 4 | // 5 | // Created by matoushybl on 03/16/2017. 6 | // Copyright (c) 2017 matoushybl. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Reactant 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | window = UIWindow(frame: UIScreen.main.bounds) 19 | window?.backgroundColor = .white 20 | window?.makeKeyAndVisible() 21 | 22 | Configuration.global.set(Properties.Style.controllerRoot) { 23 | $0.backgroundColor = .white 24 | } 25 | 26 | let module = AppModule() 27 | 28 | let wireframe = MainWireframe(dependencyModule: module) 29 | 30 | window?.rootViewController = wireframe.entrypoint() 31 | return true 32 | } 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /Tests/Core/Component/ComponentTestParts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComponentTestParts.swift 3 | // ReactantTests 4 | // 5 | // Created by Matyáš Kříž on 16/11/2017. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | enum ComponentTestAction { 10 | case none 11 | case one 12 | case two 13 | case three 14 | } 15 | 16 | struct ComponentTestState { 17 | let primitive: Int 18 | var tuple: (String, Int) 19 | var classy: ComponentTestClass? 20 | 21 | func something() { 22 | return 23 | } 24 | } 25 | 26 | final class ComponentTestClass { 27 | let primitive: Int 28 | var tuple: (String, Int) 29 | var structy: ComponentTestState 30 | 31 | init(primitive: Int = 12, structy: Bool = false) { 32 | self.primitive = primitive 33 | tuple = ("tup", 10) 34 | self.structy = ComponentTestState(primitive: primitive, tuple: tuple, classy: structy ? ComponentTestClass(primitive: primitive) : nil) 35 | } 36 | 37 | func something() { 38 | return 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source 'https://cdn.cocoapods.org/' 2 | use_frameworks! 3 | inhibit_all_warnings! 4 | 5 | def shared 6 | pod 'RxSwift', '~> 5.0' 7 | pod 'RxCocoa', '~> 5.0' 8 | pod 'RxDataSources', '~> 4.0' 9 | pod 'RxOptional', '~> 4.0' 10 | pod 'SnapKit', '~> 5.0' 11 | pod 'Kingfisher', '~> 5.0' 12 | end 13 | 14 | target 'Reactant' do 15 | platform :ios, '10.0' 16 | 17 | shared 18 | end 19 | 20 | target 'ReactantTests' do 21 | platform :ios, '10.0' 22 | 23 | shared 24 | 25 | pod 'Quick', '~> 2.0' 26 | pod 'Nimble', '~> 7.0' 27 | pod 'Cuckoo', :git => 'https://github.com/Brightify/Cuckoo.git', :branch => 'master' 28 | pod 'RxNimble' 29 | pod 'RxTest' 30 | end 31 | 32 | target 'ReactantPrototyping' do 33 | platform :ios, '10.0' 34 | 35 | shared 36 | 37 | pod 'Reactant', :subspecs => ['All-iOS'], :path => './' 38 | end 39 | 40 | target 'TVPrototyping' do 41 | platform :tvos, '10.0' 42 | shared 43 | 44 | pod 'Reactant', :subspecs => ['All-tvOS', 'FallbackSafeAreaInsets'], :path => './' 45 | end 46 | -------------------------------------------------------------------------------- /Source/Core/Styling/Extensions/UIButton+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIButton+Utils.swift 3 | // Reactant 4 | // 5 | // Created by Tadeas Kriz on 31/03/16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIButton { 12 | 13 | public convenience init(title: String) { 14 | self.init() 15 | 16 | setTitle(title, for: UIControl.State()) 17 | } 18 | } 19 | 20 | extension UIButton { 21 | 22 | @objc(setBackgroundColor:forState:) 23 | public func setBackgroundColor(_ color: UIColor, for state: UIControl.State) { 24 | let rectangle = CGRect(size: CGSize(1)); 25 | UIGraphicsBeginImageContext(rectangle.size); 26 | 27 | let context = UIGraphicsGetCurrentContext(); 28 | context?.setFillColor(color.cgColor); 29 | context?.fill(rectangle); 30 | 31 | let image = UIGraphicsGetImageFromCurrentImageContext(); 32 | UIGraphicsEndImageContext(); 33 | 34 | setBackgroundImage(image!, for: state) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Source/CollectionView/Identifier/AnyCollectionViewCellIdentifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyCollectionViewCellIdentifier.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 15.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public struct AnyCollectionViewCellIdentifier { 12 | 13 | internal let name: String 14 | internal let type: UICollectionViewCell.Type 15 | } 16 | 17 | extension CollectionViewCellIdentifier { 18 | 19 | public func typeErased() -> AnyCollectionViewCellIdentifier { 20 | return AnyCollectionViewCellIdentifier(name: name, type: CollectionViewCellWrapper.self) 21 | } 22 | } 23 | 24 | extension UICollectionView { 25 | 26 | public func register(identifier: AnyCollectionViewCellIdentifier) { 27 | register(identifier.type, forCellWithReuseIdentifier: identifier.name) 28 | } 29 | 30 | public func unregister(identifier: AnyCollectionViewCellIdentifier) { 31 | register(nil as AnyClass?, forCellWithReuseIdentifier: identifier.name) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/Core/Styling/UIColor+InitTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+InitTest.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 18.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import Reactant 12 | 13 | class UIColorInitTest: QuickSpec { 14 | 15 | override func spec() { 16 | describe("UIColor init") { 17 | it("accept rgb hex as String") { 18 | expect(UIColor(hex: "#FFFFFF")) == UIColor(red: 1, green: 1, blue: 1, alpha: 1) 19 | } 20 | it("accept rgba hex as String") { 21 | expect(UIColor(hex: "#00FFFF00")) == UIColor(red: 0, green: 1, blue: 1, alpha: 0) 22 | } 23 | it("accept rgb hex as Int") { 24 | expect(UIColor(rgb: 0xFFFFFF)) == UIColor(red: 1, green: 1, blue: 1, alpha: 1) 25 | } 26 | it("accept rgba hex as Int") { 27 | expect(UIColor(rgba: 0x00FFFF00)) == UIColor(red: 0, green: 1, blue: 1, alpha: 0) 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/parts/staticmap.md: -------------------------------------------------------------------------------- 1 | # StaticMap 2 | 3 | StaticMap is one of Reactant's prebuilt components. To integrate it into your project, you need to add `StaticMap` subspec to your `Podfile`. To do so, open your `Podfile` in a text editor and add the following line to your application's target: 4 | 5 | ```ruby 6 | pod 'Reactant/StaticMap' 7 | ``` 8 | 9 | `StaticMap` is meant to be used wherever you want to display a map without letting the user control it. Were you to use the `MKMapView`, you would get noticeable lags when pushing a controller as `MKMapView` renders on main thread. `StaticMap` renders in background and presents a loaded image. 10 | 11 | To render the map, we recommend using `MKMapSnapshotter`, and to cache the map we recommend a library called `Kingfisher`. 12 | 13 | `StaticMap` has a `componentState` of type `MKCoordinateRegion`. This region specifies the visible part of the map. It also has an `action` of type `StaticMapAction`. This action is just an enum, having a single case `selected`. Whenever user taps on the map, `StaticMap` sends this `.selected` action to be handled. 14 | -------------------------------------------------------------------------------- /Source/Configuration/Configurable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Configurable.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 14.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | public protocol Configurable: class { 10 | /// See `Configuration` for more info. 11 | var configuration: Configuration { get set } 12 | } 13 | 14 | extension Configurable { 15 | 16 | /** 17 | * Reloads object's configuration. Essentially just calls `didSet` on its configuration. 18 | */ 19 | public func reloadConfiguration() { 20 | let temp = configuration 21 | configuration = temp 22 | } 23 | 24 | /** 25 | * Applies passed `Configuration` to this object. 26 | * This function is destructive, but also returns itself to allow chaining. 27 | * - parameter configuration: new configuration to set to the object 28 | * - returns: self with new configuration set 29 | */ 30 | public func with(configuration: Configuration) -> Self { 31 | self.configuration = configuration 32 | return self 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/parts/validation.md: -------------------------------------------------------------------------------- 1 | # Validation 2 | 3 | Validation classes can be used for an easy validation of user input such as emails or passwords. 4 | 5 | A good example of creating Rules for validation is in `Rules.String` class. Let's have a look at the `minLength` Rule. 6 | 7 | ```swift 8 | public static func minLength(_ length: Int) -> Rule { 9 | return Rule { value in 10 | guard let value = value, value.count >= length else { 11 | return .invalid 12 | } 13 | return nil 14 | } 15 | } 16 | ``` 17 | 18 | If the condition for valid string is not true, then this code returns `ValidationError.invalid`, `nil` otherwise. 19 | 20 | Usage of this rule can look like this, when we use it as a part of `Observable` stream. 21 | 22 | ```swift 23 | Observable.from(["this", "is", "a", "message"]) 24 | .map { Rules.String.minLength(4).run($0) } 25 | .filterError() 26 | .subscribe(onNext: { print($0 ?? "") }) 27 | .disposed(by: stateDisposeBag) 28 | ``` 29 | 30 | This code prints only `this` `message` - the strings that are valid. 31 | -------------------------------------------------------------------------------- /Source/Configuration/Property.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Property.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 14.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | * A definition of a configurable property. It defines its type and a default value. 13 | */ 14 | public struct Property { 15 | 16 | public let id: Int 17 | public let defaultValue: T 18 | 19 | public init(defaultValue: T) { 20 | id = PropertyIdProvider.nextId() 21 | self.defaultValue = defaultValue 22 | } 23 | } 24 | 25 | extension Property where T: OptionalType { 26 | public init() { 27 | self.init(defaultValue: T.null) 28 | } 29 | } 30 | 31 | private struct PropertyIdProvider { 32 | 33 | private static var lastUsedId = -1 34 | 35 | private static let syncQueue = DispatchQueue(label: "PropertyIdProvider_syncQueue") 36 | 37 | fileprivate static func nextId() -> Int { 38 | return syncQueue.sync { 39 | lastUsedId += 1 40 | return lastUsedId 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Source/Core/Styling/Extensions/UITabBarController+Styles.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITabBarController+Styles.swift 3 | // Reactant 4 | // 5 | // Created by Matouš Hýbl on 23/02/2018. 6 | // Copyright © 2018 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UITabBarController: Styleable { } 12 | 13 | extension UITabBarController { 14 | 15 | public func apply(style: Style) { 16 | style(tabBar) 17 | } 18 | 19 | public func apply(styles: Style...) { 20 | styles.forEach(apply(style:)) 21 | } 22 | 23 | public func apply(styles: [Style]) { 24 | styles.forEach(apply(style:)) 25 | } 26 | 27 | public func styled(using styles: Style...) -> Self { 28 | styles.forEach(apply(style:)) 29 | return self 30 | } 31 | 32 | public func styled(using styles: [Style]) -> Self { 33 | apply(styles: styles) 34 | return self 35 | } 36 | 37 | public func with(_ style: Style) -> Self { 38 | apply(style: style) 39 | return self 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Brightify.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Source/Utils/AssociatedObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssociatedObject.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 21.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public func associatedObject(_ base: Any, key: UnsafePointer, defaultValue: @autoclosure () -> T) -> T { 12 | if let associated = objc_getAssociatedObject(base, key) as? T { 13 | return associated 14 | } else { 15 | let value = defaultValue() 16 | associateObject(base, key: key, value: value) 17 | return value 18 | } 19 | } 20 | 21 | public func associatedObject(_ base: Any, key: UnsafePointer, initializer: () -> T) -> T { 22 | if let associated = objc_getAssociatedObject(base, key) as? T { 23 | return associated 24 | } else { 25 | let defaultValue = initializer() 26 | associateObject(base, key: key, value: defaultValue) 27 | return defaultValue 28 | } 29 | } 30 | 31 | public func associateObject(_ base: Any, key: UnsafePointer, value: T) { 32 | objc_setAssociatedObject(base, key, value, .OBJC_ASSOCIATION_RETAIN) 33 | } 34 | -------------------------------------------------------------------------------- /Example/Application/Sources/Components/Table/TableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewController.swift 3 | // Reactant 4 | // 5 | // Created by Matous Hybl on 3/24/17. 6 | // Copyright © 2017 Brightify s.r.o. All rights reserved. 7 | // 8 | 9 | import Reactant 10 | 11 | class TableViewController: ControllerBase { 12 | 13 | struct Dependencies { 14 | let nameService: NameService 15 | } 16 | 17 | struct Reactions { 18 | let displayName: (String) -> Void 19 | } 20 | 21 | private let dependencies: Dependencies 22 | private let reactions: Reactions 23 | 24 | init(dependencies: Dependencies, reactions: Reactions) { 25 | self.dependencies = dependencies 26 | self.reactions = reactions 27 | super.init() 28 | } 29 | 30 | override func update() { 31 | rootView.componentState = .items(dependencies.nameService.names()) 32 | } 33 | 34 | override func act(on action: TableViewRootView.ActionType) { 35 | switch action { 36 | case .selected(let name): 37 | reactions.displayName(name) 38 | default: 39 | break 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Source/Core/Styling/Extensions/UINavigationController+Styles.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UINavigationController+Styles.swift 3 | // Reactant 4 | // 5 | // Created by Matouš Hýbl on 23/02/2018. 6 | // Copyright © 2018 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UINavigationController: Styleable { } 12 | 13 | extension UINavigationController { 14 | 15 | public func apply(style: Style) { 16 | style(navigationBar) 17 | } 18 | 19 | public func apply(styles: Style...) { 20 | styles.forEach(apply(style:)) 21 | } 22 | 23 | public func apply(styles: [Style]) { 24 | styles.forEach(apply(style:)) 25 | } 26 | 27 | public func styled(using styles: Style...) -> Self { 28 | styles.forEach(apply(style:)) 29 | return self 30 | } 31 | 32 | public func styled(using styles: [Style]) -> Self { 33 | apply(styles: styles) 34 | return self 35 | } 36 | 37 | public func with(_ style: Style) -> Self { 38 | apply(style: style) 39 | return self 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/Configuration/PropertyTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PropertyTest.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 26.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import Reactant 12 | 13 | class PropertyTest: QuickSpec { 14 | 15 | override func spec() { 16 | describe("Property") { 17 | describe("init") { 18 | it("assigns unique id") { 19 | let property1 = Property(defaultValue: 0) 20 | let property2 = Property(defaultValue: 0) 21 | 22 | expect(property1.id) != property2.id 23 | } 24 | it("sets defaultValue") { 25 | let property = Property(defaultValue: 1) 26 | 27 | expect(property.defaultValue) == 1 28 | } 29 | it("sets defaultValue to nil for Optional types") { 30 | let property = Property() 31 | 32 | expect(property.defaultValue).to(beNil()) 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Source/Utils/Hash.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Hash.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 10.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | private let djbHashInitialValue = 5381 12 | 13 | private func computeDjbHash(accumulator: Int, hashValue: Int) -> Int { 14 | return (accumulator << 5) &+ accumulator &+ hashValue 15 | } 16 | 17 | extension Sequence where Iterator.Element == Int { 18 | 19 | public func djbHash() -> Int { 20 | return reduce(djbHashInitialValue, computeDjbHash) 21 | } 22 | } 23 | 24 | extension Sequence where Iterator.Element == Int? { 25 | 26 | public func djbHash() -> Int { 27 | return reduce(djbHashInitialValue) { 28 | if let hashValue = $1 { 29 | return computeDjbHash(accumulator: $0, hashValue: hashValue) 30 | } else { 31 | return $0 32 | } 33 | } 34 | } 35 | } 36 | 37 | extension Sequence where Iterator.Element: Hashable { 38 | 39 | public func djbHash() -> Int { 40 | return reduce(djbHashInitialValue) { 41 | computeDjbHash(accumulator: $0, hashValue: $1.hashValue) 42 | } 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /Source/Utils/UIStackView+ArrangedChildren.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIStackView+ArrangedChildren.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 20.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @available(iOS 9.0, *) 12 | extension UIStackView { 13 | 14 | /** 15 | * Adds arranged subviews to the stack view that this method is called upon. 16 | * Convenience method working the same as `addArrangedSubview(_:)` but letting you pass multiple UIViews at once. 17 | * - parameter children: `UIView`s to be added as arranged subviews 18 | */ 19 | @discardableResult 20 | public func arrangedChildren(_ children: UIView...) -> UIStackView { 21 | return arrangedChildren(children) 22 | } 23 | 24 | /** 25 | * Adds arranged subviews to the stack view that this method is called upon. 26 | * Convenience method working the same as `addArrangedSubview(_:)` but letting you pass an array of UIViews. 27 | * - parameter children: `UIView`s to be added as arranged subviews 28 | */ 29 | @discardableResult 30 | public func arrangedChildren(_ children: [UIView]) -> UIStackView { 31 | children.forEach(addArrangedSubview) 32 | return self 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Example/Application/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Reactant 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Source/Utils/Collection+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Collection+Utils.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 21.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Collection { 12 | 13 | public func groupBy(_ extractKey: (Iterator.Element) -> KEY) -> [(KEY, [Iterator.Element])] { 14 | return groupBy { Optional(extractKey($0)) } 15 | } 16 | 17 | public func groupBy(_ extractKey: (Iterator.Element) -> KEY?) -> [(KEY, [Iterator.Element])] { 18 | var grouped: [(KEY, [Iterator.Element])] = [] 19 | var t: [String] = [] 20 | func add(_ item: Iterator.Element, forKey key: KEY) { 21 | if let index = grouped.firstIndex(where: { $0.0 == key }) { 22 | let value = grouped[index] 23 | grouped[index] = (key, value.1.arrayByAppending(item)) 24 | } else { 25 | grouped.append((key, [item])) 26 | } 27 | } 28 | 29 | for item in self { 30 | guard let key = extractKey(item) else { 31 | continue 32 | } 33 | add(item, forKey: key) 34 | } 35 | return grouped 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /TVPrototyping/Components/TableView/TableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableController.swift 3 | // TVPrototyping 4 | // 5 | // Created by Matous Hybl on 03/11/2017. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import Reactant 10 | 11 | final class TableViewController: ControllerBase { 12 | 13 | override func afterInit() { 14 | rootView.componentState = .items(["Cell 1", "Cell 2", "Cell 3", "Cell 4"]) 15 | 16 | tabBarItem = UITabBarItem(title: "TableView", image: nil, tag: 0) 17 | } 18 | 19 | } 20 | 21 | final class TableViewRootView: PlainTableView { 22 | 23 | @objc 24 | init() { 25 | super.init(cellFactory: TestCell.init, style: .plain, reloadable: false) 26 | 27 | tableView.rowHeight = UITableViewAutomaticDimension 28 | tableView.estimatedRowHeight = 150 29 | } 30 | } 31 | 32 | final class TestCell: ViewBase { 33 | private let label = UILabel() 34 | 35 | override func update() { 36 | label.text = componentState 37 | } 38 | 39 | override func loadView() { 40 | children(label) 41 | } 42 | 43 | override func setupConstraints() { 44 | label.snp.makeConstraints { make in 45 | make.edges.equalToSuperview().inset(50) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Source/TableView/Identifier/TableViewHeaderFooterIdentifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewHeaderFooterIdentifier.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 15.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public struct TableViewHeaderFooterIdentifier { 12 | 13 | internal let name: String 14 | 15 | public init(name: String = NSStringFromClass(T.self)) { 16 | self.name = name 17 | } 18 | } 19 | 20 | extension UITableView { 21 | 22 | public func register(identifier: TableViewHeaderFooterIdentifier) { 23 | register(TableViewHeaderFooterWrapper.self, forHeaderFooterViewReuseIdentifier: identifier.name) 24 | } 25 | 26 | public func unregister(identifier: TableViewHeaderFooterIdentifier) { 27 | register(nil as AnyClass?, forHeaderFooterViewReuseIdentifier: identifier.name) 28 | } 29 | } 30 | 31 | extension UITableView { 32 | 33 | public func dequeue(identifier: TableViewHeaderFooterIdentifier) -> TableViewHeaderFooterWrapper { 34 | guard let view = dequeueReusableHeaderFooterView(withIdentifier: identifier.name) as? TableViewHeaderFooterWrapper else { 35 | fatalError("\(identifier) is not registered.") 36 | } 37 | return view 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Source/CollectionView/Identifier/AnyCollectionSupplementaryViewIdentifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyCollectionSupplementaryViewIdentifier.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 15.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public struct AnyCollectionSupplementaryViewIdentifier { 12 | 13 | internal typealias IdentifiedType = UICollectionReusableView 14 | 15 | internal var name: String 16 | internal var kind: String 17 | internal var type: UICollectionReusableView.Type 18 | } 19 | 20 | extension CollectionSupplementaryViewIdentifier { 21 | 22 | public func typeErased() -> AnyCollectionSupplementaryViewIdentifier { 23 | return AnyCollectionSupplementaryViewIdentifier(name: name, kind: kind, type: CollectionReusableViewWrapper.self) 24 | } 25 | } 26 | 27 | extension UICollectionView { 28 | 29 | public func register(identifier: AnyCollectionSupplementaryViewIdentifier) { 30 | register(identifier.type, forSupplementaryViewOfKind: identifier.kind, withReuseIdentifier: identifier.name) 31 | } 32 | 33 | public func unregister(identifier: AnyCollectionSupplementaryViewIdentifier) { 34 | register(nil as AnyClass?, forSupplementaryViewOfKind: identifier.kind, withReuseIdentifier: identifier.name) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * http://www.objc.io/issue-6/travis-ci.html 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | osx_image: xcode10.2 6 | language: objective-c 7 | # cache: cocoapods 8 | # podfile: Example/Podfile 9 | # before_install: 10 | # - gem install cocoapods # Since Travis is not always on latest version 11 | # - pod install --project-directory=Example 12 | before_install: 13 | - nvm install 4 14 | # - gem install cocoapods -v '1.2.1' 15 | - pod repo update --silent 16 | 17 | script: 18 | - (xcodebuild -workspace Reactant.xcworkspace -scheme Reactant -sdk iphonesimulator build-for-testing | egrep -A 3 "(error|warning|note):\ "; exit ${PIPESTATUS[0]}) 19 | - xctool -workspace Reactant.xcworkspace -scheme Reactant -sdk iphonesimulator run-tests 20 | # - travis_wait pod lib lint --allow-warnings 21 | - npm install docpress && ./node_modules/.bin/docpress build 22 | - cp CNAME ./_docpress/CNAME 23 | 24 | after_success: 25 | - if [ "$TRAVIS_BRANCH" = "master" -a "$TRAVIS_PULL_REQUEST" = "false" ]; then npm install git-update-ghpages && ./node_modules/.bin/git-update-ghpages -e; fi 26 | 27 | env: 28 | global: 29 | - GIT_NAME: Travis CI 30 | - GIT_EMAIL: info@brightify.org 31 | - GITHUB_REPO: Brightify/Reactant 32 | - GIT_SOURCE: _docpress 33 | 34 | notifications: 35 | slack: brightify:00rIGJIfWqG5RyWCVoRNEgxt 36 | -------------------------------------------------------------------------------- /Source/StaticMap/CLLocationCoordinate2D+utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLLocationCoordinate2D+utils.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 26.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import MapKit 10 | 11 | extension CLLocationCoordinate2D { 12 | 13 | public static func startAndEnd(region: MKCoordinateRegion) -> (start: CLLocationCoordinate2D, end: CLLocationCoordinate2D) { 14 | let center = region.center 15 | let centerLat = center.latitude 16 | let centerLon = center.longitude 17 | let span = region.span 18 | let latDelta = span.latitudeDelta 19 | let lonDelta = span.longitudeDelta 20 | let start = CLLocationCoordinate2D(latitude: centerLat - (latDelta / 2), longitude: centerLon - (lonDelta / 2)) 21 | let end = CLLocationCoordinate2D(latitude: centerLat + (latDelta / 2), longitude: centerLon + (lonDelta / 2)) 22 | 23 | return (start, end) 24 | } 25 | } 26 | 27 | extension CLLocationCoordinate2D: Hashable { 28 | public func hash(into hasher: inout Hasher) { 29 | hasher.combine(latitude) 30 | hasher.combine(longitude) 31 | } 32 | } 33 | 34 | public func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { 35 | return lhs.latitude.equal(to: rhs.latitude) && lhs.longitude.equal(to: rhs.longitude) 36 | } 37 | -------------------------------------------------------------------------------- /Example/Application/Sources/Components/Main/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Reactant 4 | // 5 | // Created by matoushybl on 03/16/2017. 6 | // Copyright (c) 2017 Brightify s.r.o. All rights reserved. 7 | // 8 | 9 | import Reactant 10 | import RxSwift 11 | 12 | final class MainViewController: ControllerBase { 13 | 14 | struct Reactions { 15 | let openTable: () -> Void 16 | } 17 | 18 | private let reactions: Reactions 19 | 20 | init(reactions: Reactions) { 21 | self.reactions = reactions 22 | // this needs to be called like this, because of a bug in Swift 23 | super.init(title: "Main") 24 | 25 | // set inital state of RootView 26 | rootView.componentState = Date() 27 | } 28 | 29 | override func update() { 30 | // do nothing since this controller has void state 31 | } 32 | 33 | // Act according to action from RootView 34 | override func act(on action: MainRootView.ActionType) { 35 | switch action { 36 | case .updateLabel: 37 | // when this action is sent from RootView, set new state to RootView's componentState 38 | rootView.componentState = Date() 39 | break 40 | // just a dummy action 41 | case .openTable: 42 | reactions.openTable() 43 | break 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Source/Core/Properties+Core.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Properties+Core.swift 3 | // Reactant 4 | // 5 | // Created by Tadeáš Kříž on 12/06/16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension Properties { 12 | 13 | public static let layoutMargins = Property(defaultValue: .zero) 14 | public static let closeButtonTitle = Property(defaultValue: "Close") 15 | public static let defaultBackButton = Property() 16 | } 17 | 18 | extension Properties.Style { 19 | 20 | public static let controllerRoot = style(for: ControllerRootViewContainer.self) 21 | 22 | /// NOTE: Applied after `controllerRoot` style 23 | public static let dialogControllerRoot = style(for: ControllerRootViewContainer.self) { root in 24 | root.backgroundColor = UIColor.black.fadedOut(by: 20%) 25 | } 26 | public static let dialog = style(for: UIView.self) 27 | public static let dialogContentContainer = style(for: UIView.self) 28 | public static let scroll = style(for: UIScrollView.self) 29 | public static let button = style(for: UIButton.self) 30 | public static let control = style(for: UIControl.self) 31 | public static let container = style(for: ContainerView.self) 32 | public static let view = style(for: UIView.self) 33 | public static let textField = style(for: TextField.self) 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Source/Utils/RangeReplaceableCollection+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RangeReplaceableCollection+Utils.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 21.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension RangeReplaceableCollection { 12 | 13 | // TODO: Make sure no one is using it and remove from Reactant ASAP 14 | /** 15 | * - parameter until: actually a `while` in disguise, pass a closure that needs to be true for as long as many you want elements 16 | * - WARNING: Although the name implies that this function takes elements *until* the condition is `true`, 17 | * it's quite the opposite. Tread carefully when using this function as it does the same job as `prefix(while:)` 18 | * along with `removeFirst(n:)` with the difference that `prefix(while:)` needs to be converted to array like so: 19 | * ``` 20 | * let newArray = Array(prefix(while: { /* condition */ })) 21 | * oldArray.removeFirst(newArray.count) 22 | * ``` 23 | */ 24 | @available(*, deprecated, message: "This method will be removed in Reactant 2.0 as it doesn't provide any value to Reactant Architecture itself.") 25 | public mutating func remove(until: (Iterator.Element) -> Bool) -> [Iterator.Element] { 26 | let result = Array(prefix(while: until)) 27 | removeFirst(result.count) 28 | return result 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Source/TableView/Identifier/AnyTableViewHeaderFooterIdentifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyTableViewHeaderFooterIdentifier.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 15.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public struct AnyTableViewHeaderFooterIdentifier { 12 | 13 | internal let name: String 14 | internal let type: UITableViewHeaderFooterView.Type 15 | } 16 | 17 | extension TableViewHeaderFooterIdentifier { 18 | 19 | public func typeErased() -> AnyTableViewHeaderFooterIdentifier { 20 | return AnyTableViewHeaderFooterIdentifier(name: name, type: TableViewHeaderFooterWrapper.self) 21 | } 22 | } 23 | 24 | extension UITableView { 25 | 26 | public func register(identifier: AnyTableViewHeaderFooterIdentifier) { 27 | register(identifier.type, forHeaderFooterViewReuseIdentifier: identifier.name) 28 | } 29 | 30 | public func unregister(identifier: AnyTableViewHeaderFooterIdentifier) { 31 | register(nil as AnyClass?, forHeaderFooterViewReuseIdentifier: identifier.name) 32 | } 33 | 34 | public func dequeue(identifier: AnyTableViewHeaderFooterIdentifier) -> UITableViewHeaderFooterView { 35 | guard let view = dequeueReusableHeaderFooterView(withIdentifier: identifier.name) else { 36 | fatalError("\(identifier) is not registered.") 37 | } 38 | return view 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Source/Validation/Rules/Rules+String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Rules+String.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 20.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Rules { 12 | 13 | public struct String { 14 | 15 | private static let emailPredicate = NSPredicate(format: "SELF MATCHES %@", "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}") 16 | 17 | public static let notEmpty = Rule { value in 18 | guard let value = value, value.isEmpty == false else { 19 | return .invalid 20 | } 21 | 22 | return nil 23 | } 24 | 25 | public static func minLength(_ length: Int) -> Rule { 26 | return Rule { value in 27 | guard let value = value, value.count >= length else { 28 | return .invalid 29 | } 30 | return nil 31 | } 32 | } 33 | 34 | public static let email = Rule { value in 35 | guard Rules.String.notEmpty.test(value) else { 36 | return .empty 37 | } 38 | 39 | guard emailPredicate.evaluate(with: value) else { return .invalid } 40 | 41 | return nil 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Source/TableView/Configuration+TableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Configuration+TableView.swift 3 | // Reactant 4 | // 5 | // Created by Robin Krenecky on 24/04/2018. 6 | // Copyright © 2018 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public final class TableViewConfiguration: BaseSubConfiguration { 12 | public var tableView: (ReactantTableView) -> Void { 13 | get { 14 | return configuration.get(valueFor: Properties.Style.TableView.tableView) 15 | } 16 | set { 17 | configuration.set(Properties.Style.TableView.tableView, to: newValue) 18 | } 19 | } 20 | 21 | public var headerFooterWrapper: (UITableViewHeaderFooterView) -> Void { 22 | get { 23 | return configuration.get(valueFor: Properties.Style.TableView.headerFooterWrapper) 24 | } 25 | set { 26 | configuration.set(Properties.Style.TableView.headerFooterWrapper, to: newValue) 27 | } 28 | } 29 | 30 | public var cellWrapper: (UITableViewCell) -> Void { 31 | get { 32 | return configuration.get(valueFor: Properties.Style.TableView.cellWrapper) 33 | } 34 | set { 35 | configuration.set(Properties.Style.TableView.cellWrapper, to: newValue) 36 | } 37 | } 38 | } 39 | 40 | public extension Configuration.Style { 41 | var tableView: TableViewConfiguration { 42 | return TableViewConfiguration(configuration: configuration) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Source/TableView/TableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewCell.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 13.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol TableViewCell { 12 | 13 | /** 14 | * The style of selected cells. 15 | * Use `UITableViewCellSelectionStyle` constants to set the value of the `selectionStyle` property. 16 | */ 17 | var selectionStyle: UITableViewCell.SelectionStyle { get } 18 | 19 | /** 20 | * The style of focused cells. 21 | * Use `UITableViewCellFocusStyle` constants to set the value of the `focusStyle` property. 22 | */ 23 | @available(iOS 9.0, *) 24 | var focusStyle: UITableViewCell.FocusStyle { get } 25 | 26 | /// Called after the user lifts the finger after tapping the cell. 27 | func setSelected(_ selected: Bool, animated: Bool) 28 | 29 | /// Called when user taps the cell. 30 | func setHighlighted(_ highlighted: Bool, animated: Bool) 31 | } 32 | 33 | extension TableViewCell { 34 | 35 | public var selectionStyle: UITableViewCell.SelectionStyle { 36 | return .default 37 | } 38 | 39 | @available(iOS 9.0, *) 40 | public var focusStyle: UITableViewCell.FocusStyle { 41 | return .default 42 | } 43 | 44 | public func setSelected(_ selected: Bool, animated: Bool) { 45 | } 46 | 47 | public func setHighlighted(_ highlighted: Bool, animated: Bool) { 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/getting-started/troubleshooting.md: -------------------------------------------------------------------------------- 1 | 2 | [cocoapods]: http://cocoapods.org 3 | 4 | # Troubleshooting Tips 5 | As we all know, not always everything goes according to plan in IT. That's why we created this section with solutions to problems you are most likely to encounter. 6 | 7 | **NOTE**: Always clean the build after trying any of the provided solutions. Some things may depend on it. The shortcut in Xcode is `Cmd+Shift+K`. 8 | 9 | ## General 10 | - try `pod repo update` to update your Cocoapods repository 11 | - try `pod install` to update the Pods and reopen the `.xcworkspace` (this is important otherwise the changes might not take effect). 12 | 13 | ## Reactant 14 | 15 | ## ReactantUI 16 | **I get a compiler error saying that generated things do not exist.** 17 | 18 | Make sure that in `Build Phases`->`Compile Sources` the `GeneratedUI.swift` file is the topmost one. 19 | 20 | --- 21 | 22 | **I get an error ".../Application/Sources/Generated/GeneratedUI.swift no such file or directory".** 23 | 24 | You need to create the `Generated/` folder yourself. If you're in the root folder of your project, you can use this command: `mkdir ./Application/Generated/`. 25 | 26 | --- 27 | 28 | ## ReactantCLI 29 | **I created a new project with `reactant init` command and it doesn't run.** 30 | 31 | It's possible that something went wrong during the project creation. Make sure you have [Cocoapods][cocoapods] installed and afterwards try to call `pod repo update` and afterwards `pod install`. If it still doesn't work, consider creating a new project. 32 | -------------------------------------------------------------------------------- /Source/Core/Component/ComponentBase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComponentBase.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 08.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | 11 | open class ComponentBase: ComponentWithDelegate { 12 | 13 | public typealias StateType = STATE 14 | public typealias ActionType = ACTION 15 | 16 | public let lifetimeDisposeBag = DisposeBag() 17 | 18 | public let componentDelegate = ComponentDelegate>() 19 | 20 | open var action: Observable { 21 | return componentDelegate.action 22 | } 23 | 24 | /** 25 | * Collection of Component's `Observable`s which are merged into `Component.action`. 26 | * - Note: When listening to Component's actions, using `action` is preferred to `actions`. 27 | */ 28 | open var actions: [Observable] { 29 | return [] 30 | } 31 | 32 | open func needsUpdate() -> Bool { 33 | return true 34 | } 35 | 36 | public init(canUpdate: Bool = true) { 37 | componentDelegate.ownerComponent = self 38 | 39 | resetActions() 40 | 41 | afterInit() 42 | 43 | componentDelegate.canUpdate = canUpdate 44 | } 45 | 46 | open func afterInit() { 47 | } 48 | 49 | open func update() { 50 | } 51 | 52 | public func observeState(_ when: ObservableStateEvent) -> Observable { 53 | return componentDelegate.observeState(when) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ReactantPrototyping/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Source/Core/Styling/Extensions/UIEdgeInsets+Init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIEdgeInsets+Init.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 16.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIEdgeInsets { 12 | 13 | public init(left: CGFloat = 0, bottom: CGFloat = 0, right: CGFloat = 0) { 14 | self.init(top: 0, left: left, bottom: bottom, right: right) 15 | } 16 | 17 | public init(top: CGFloat, bottom: CGFloat = 0, right: CGFloat = 0) { 18 | self.init(top: top, left: 0, bottom: bottom, right: right) 19 | } 20 | 21 | public init(top: CGFloat, left: CGFloat, right: CGFloat = 0) { 22 | self.init(top: top, left: left, bottom: 0, right: right) 23 | } 24 | 25 | public init(top: CGFloat, left: CGFloat, bottom: CGFloat) { 26 | self.init(top: top, left: left, bottom: bottom, right: 0) 27 | } 28 | 29 | public init(_ all: CGFloat) { 30 | self.init(horizontal: all, vertical: all) 31 | } 32 | 33 | public init(horizontal: CGFloat, vertical: CGFloat) { 34 | self.init(top: vertical, left: horizontal, bottom: vertical, right: horizontal) 35 | } 36 | 37 | public init(horizontal: CGFloat, top: CGFloat = 0, bottom: CGFloat = 0) { 38 | self.init(top: top, left: horizontal, bottom: bottom, right: horizontal) 39 | } 40 | 41 | public init(vertical: CGFloat, left: CGFloat = 0, right: CGFloat = 0) { 42 | self.init(top: vertical, left: left, bottom: vertical, right: right) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/Core/Styling/NSAttributedString+AttributeTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSAttributedString+AttributeTest.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 18.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import Reactant 12 | 13 | class NSAttributedStringAttributeTest: QuickSpec { 14 | 15 | override func spec() { 16 | describe("AttributedString + AttributedString") { 17 | it("sums") { 18 | expect(NSAttributedString(string: "A") + NSAttributedString(string: "B")) == NSAttributedString(string: "AB") 19 | } 20 | } 21 | describe("String + AttributedString") { 22 | it("sums") { 23 | expect("A" + NSAttributedString(string: "B")) == NSAttributedString(string: "AB") 24 | } 25 | } 26 | describe("AttributedString + String") { 27 | it("sums") { 28 | expect(NSAttributedString(string: "A") + "B") == NSAttributedString(string: "AB") 29 | } 30 | } 31 | describe("attributed") { 32 | it("creates AttributedString") { 33 | let attributes = [Attribute.baselineOffset(1), Attribute.expansion(1)] 34 | let attributedString = NSAttributedString(string: "A", attributes: attributes.toDictionary()) 35 | 36 | expect("A".attributed(attributes)) == attributedString 37 | expect("A".attributed(Attribute.baselineOffset(1), Attribute.expansion(1))) == attributedString 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /TVPrototyping/Components/ButtonTest/ButtonController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ButtonController.swift 3 | // TVPrototyping 4 | // 5 | // Created by Matous Hybl on 03/11/2017. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import Reactant 10 | 11 | class ButtonController: ControllerBase { 12 | 13 | override func afterInit() { 14 | tabBarItem = UITabBarItem(title: "Button", image: nil, tag: 0) 15 | } 16 | } 17 | 18 | final class ButtonRootView: ViewBase, RootView { 19 | 20 | private let button = UIButton(title: "Button") 21 | 22 | override func loadView() { 23 | children(button) 24 | 25 | button.setTitleColor(.black, for: .normal) 26 | button.setBackgroundColor(.blue, for: .normal) 27 | button.setBackgroundColor(.red, for: .focused) 28 | } 29 | 30 | override func setupConstraints() { 31 | button.snp.makeConstraints { make in 32 | make.center.equalToSuperview() 33 | make.size.equalTo(CGSize(width: 300, height: 100)) 34 | } 35 | } 36 | 37 | override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { 38 | coordinator.addCoordinatedAnimations({ 39 | UIView.animate(withDuration: 0.3, animations: { 40 | if self.button.isFocused { 41 | self.button.transform = CGAffineTransform(scaleX: 1.2, y: 1.2) 42 | } else { 43 | self.button.transform = .identity 44 | } 45 | }) 46 | 47 | }, completion: nil) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Source/Core/View/Internal/DialogView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DialogView.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 09.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public final class DialogView: ViewBase { 12 | 13 | private let contentContainer = ContainerView() 14 | private let content: UIView 15 | 16 | public override var configuration: Configuration { 17 | didSet { 18 | contentContainer.configuration = configuration 19 | configuration.get(valueFor: Properties.Style.dialogContentContainer)(contentContainer) 20 | configuration.get(valueFor: Properties.Style.dialog)(self) 21 | } 22 | } 23 | 24 | public init(content: UIView) { 25 | self.content = content 26 | 27 | super.init() 28 | } 29 | 30 | override public func loadView() { 31 | children( 32 | contentContainer.children( 33 | content 34 | ) 35 | ) 36 | } 37 | 38 | override public func setupConstraints() { 39 | contentContainer.snp.makeConstraints { make in 40 | make.leading.greaterThanOrEqualTo(snp.leadingMargin) 41 | make.top.greaterThanOrEqualTo(snp.topMargin) 42 | make.trailing.greaterThanOrEqualTo(snp.trailingMargin) 43 | make.bottom.lessThanOrEqualTo(snp.bottomMargin) 44 | make.center.equalTo(self) 45 | } 46 | 47 | content.snp.makeConstraints { make in 48 | make.edges.equalTo(contentContainer) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Source/CollectionView/ReactantCollectionView+Delegates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReactantCollectionView+Delegates.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 13.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension ReactantCollectionView { 12 | 13 | #if os(iOS) 14 | /** 15 | * The tint color for the refresh control. 16 | * The default value of this property is nil. 17 | */ 18 | public var refreshControlTintColor: UIColor? { 19 | get { 20 | return refreshControl?.tintColor 21 | } 22 | set { 23 | refreshControl?.tintColor = newValue 24 | } 25 | } 26 | #endif 27 | 28 | /** 29 | * The basic appearance of the activity indicator. 30 | * See **UIActivityIndicatorViewStyle** for the available styles. The default value is white. 31 | */ 32 | public var activityIndicatorStyle: UIActivityIndicatorView.Style { 33 | get { 34 | return loadingIndicator.style 35 | } 36 | set { 37 | loadingIndicator.style = newValue 38 | } 39 | } 40 | 41 | /** 42 | * The distance that the content view is inset from the enclosing scroll view. 43 | * Use this property to add to the scrolling area around the content. The unit of size is points. The default value is `UIEdgeInsetsZero`. 44 | */ 45 | public var contentInset: UIEdgeInsets { 46 | get { 47 | return collectionView.contentInset 48 | } 49 | set { 50 | collectionView.contentInset = newValue 51 | } 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /Source/Core/Styling/Extensions/CGRect+Init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGRect+Init.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 16.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension CGRect { 12 | 13 | public init(x: CGFloat, width: CGFloat = 0, height: CGFloat = 0) { 14 | self.init(x: x, y: 0, width: width, height: height) 15 | } 16 | 17 | public init(y: CGFloat, width: CGFloat = 0, height: CGFloat = 0) { 18 | self.init(x: 0, y: y, width: width, height: height) 19 | } 20 | 21 | public init(x: CGFloat, y: CGFloat) { 22 | self.init(x: x, y: y, width: 0, height: 0) 23 | } 24 | 25 | public init(x: CGFloat, y: CGFloat, width: CGFloat) { 26 | self.init(x: x, y: y, width: width, height: 0) 27 | } 28 | 29 | public init(x: CGFloat, y: CGFloat, height: CGFloat) { 30 | self.init(x: x, y: y, width: 0, height: height) 31 | } 32 | 33 | public init(width: CGFloat) { 34 | self.init(x: 0, y: 0, width: width, height: 0) 35 | } 36 | 37 | public init(height: CGFloat) { 38 | self.init(x: 0, y: 0, width: 0, height: height) 39 | } 40 | 41 | public init(width: CGFloat, height: CGFloat) { 42 | self.init(x: 0, y: 0, width: width, height: height) 43 | } 44 | 45 | public init(x: CGFloat = 0, y: CGFloat = 0, size: CGSize) { 46 | self.init(origin: CGPoint(x: x, y: y), size: size) 47 | } 48 | 49 | public init(origin: CGPoint, width: CGFloat = 0, height: CGFloat = 0) { 50 | self.init(origin: origin, size: CGSize(width: width, height: height)) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Source/Utils/RxUtils/UIBarButtonItem+ClosureAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIBarButtonItem+ClosureAction.swift 3 | // Reactant 4 | // 5 | // Created by Tadeas Kriz on 06/10/2016. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | import RxCocoa 12 | 13 | /// Extension of UIBarButtonItem, that adds option to use closure instead of target and selector 14 | extension UIBarButtonItem { 15 | 16 | public convenience init(image: UIImage?, style: UIBarButtonItem.Style, action: (() -> Void)? = nil) { 17 | self.init(image: image, style: style, target: nil, action: nil) 18 | 19 | register(action: action) 20 | } 21 | 22 | public convenience init(image: UIImage?, landscapeImagePhone: UIImage?, style: UIBarButtonItem.Style, 23 | action: (() -> Void)? = nil) { 24 | self.init(image: image, landscapeImagePhone: landscapeImagePhone, style: style, target: nil, action: nil) 25 | 26 | register(action: action) 27 | } 28 | 29 | public convenience init(title: String?, style: UIBarButtonItem.Style, action: (() -> Void)? = nil) { 30 | self.init(title: title, style: style, target: nil, action: nil) 31 | 32 | register(action: action) 33 | } 34 | 35 | public convenience init(barButtonSystemItem systemItem: UIBarButtonItem.SystemItem, action: (() -> Void)? = nil) { 36 | self.init(barButtonSystemItem: systemItem, target: nil, action: nil) 37 | 38 | register(action: action) 39 | } 40 | 41 | private func register(action: (() -> Void)?) { 42 | if let action = action { 43 | _ = rx.tap.takeUntil(rx.deallocating).subscribe(onNext: action) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/Core/Styling/CGAffineTransform+ShortcutTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGAffineTransform+ShortcutTest.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 18.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import Reactant 12 | 13 | class CGAffineTransformShortcutTest: QuickSpec { 14 | 15 | override func spec() { 16 | describe("rotate") { 17 | it("creates CGAffineTransform") { 18 | expect(rotate()) == CGAffineTransform(rotationAngle: 0) 19 | expect(rotate(10)) == CGAffineTransform(rotationAngle: 10) 20 | } 21 | } 22 | describe("translate") { 23 | it("creates CGAffineTransform") { 24 | expect(translate()) == CGAffineTransform(translationX: 0, y: 0) 25 | expect(translate(x: 1)) == CGAffineTransform(translationX: 1, y: 0) 26 | expect(translate(y: 1)) == CGAffineTransform(translationX: 0, y: 1) 27 | expect(translate(x: 1, y: 1)) == CGAffineTransform(translationX: 1, y: 1) 28 | } 29 | } 30 | describe("scale") { 31 | it("creates CGAffineTransform") { 32 | expect(scale()) == CGAffineTransform(scaleX: 1, y: 1) 33 | expect(scale(x: 2)) == CGAffineTransform(scaleX: 2, y: 1) 34 | expect(scale(y: 2)) == CGAffineTransform(scaleX: 1, y: 2) 35 | expect(scale(x: 2, y: 2)) == CGAffineTransform(scaleX: 2, y: 2) 36 | } 37 | } 38 | describe("+") { 39 | it("sums vectors") { 40 | expect(translate(x: 5) + translate(y: 3)) == translate(x: 5, y: 3) 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Source/TableView/Identifier/AnyTableViewCellIdentifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyTableViewCellIdentifier.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 15.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public struct AnyTableViewCellIdentifier { 12 | 13 | internal let name: String 14 | internal let type: UITableViewCell.Type 15 | } 16 | 17 | extension TableViewCellIdentifier { 18 | 19 | public func typeErased() -> AnyTableViewCellIdentifier { 20 | return AnyTableViewCellIdentifier(name: name, type: TableViewCellWrapper.self) 21 | } 22 | } 23 | 24 | extension UITableView { 25 | 26 | public func register(identifier: AnyTableViewCellIdentifier) { 27 | register(identifier.type, forCellReuseIdentifier: identifier.name) 28 | } 29 | 30 | public func unregister(identifier: AnyTableViewCellIdentifier) { 31 | register(nil as AnyClass?, forCellReuseIdentifier: identifier.name) 32 | } 33 | 34 | public func dequeue(identifier: AnyTableViewCellIdentifier) -> UITableViewCell { 35 | guard let cell = dequeueReusableCell(withIdentifier: identifier.name) else { 36 | fatalError("\(identifier) is not registered.") 37 | } 38 | return cell 39 | } 40 | 41 | public func dequeue(identifier: AnyTableViewCellIdentifier, for indexPath: IndexPath) -> UITableViewCell { 42 | return dequeueReusableCell(withIdentifier: identifier.name, for: indexPath) 43 | } 44 | 45 | public func dequeue(identifier: AnyTableViewCellIdentifier, forRow row: Int, inSection section: Int = 0) -> UITableViewCell { 46 | return dequeue(identifier: identifier, for: IndexPath(row: row, section: section)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/Core/Styling/UIButton+UtilsTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIButton+UtilsTest.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 18.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import Reactant 12 | 13 | class UIButtonUtilsTest: QuickSpec { 14 | 15 | override func spec() { 16 | describe("UIButton") { 17 | describe("init") { 18 | it("creates UIButton with title") { 19 | expect(UIButton(title: "title").title(for: UIControl.State())) == "title" 20 | } 21 | } 22 | describe("setBackgroundColor") { 23 | it("sets background color") { 24 | let button = UIButton() 25 | let controlState = UIControl.State.highlighted 26 | 27 | button.setBackgroundColor(UIColor.green, for: controlState) 28 | 29 | let image = button.backgroundImage(for: controlState) 30 | if let pixelData = image?.cgImage?.dataProvider?.data, let data = CFDataGetBytePtr(pixelData) { 31 | let r = CGFloat(data[0]) / 255 32 | let g = CGFloat(data[1]) / 255 33 | let b = CGFloat(data[2]) / 255 34 | let a = CGFloat(data[3]) / 255 35 | 36 | expect(UIColor(red: r, green: g, blue: b, alpha: a)) == UIColor.green 37 | expect(image?.size) == CGSize(1) 38 | } else { 39 | fail("Cannot find color of background image.") 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Source/Core/Controller/DialogControllerBase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DialogControllerBase.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 08.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class DialogControllerBase: ControllerBase where ROOT: Component { 12 | 13 | private let rootViewContainer = ControllerRootViewContainer() 14 | public var dialogView: DialogView 15 | 16 | open override var configuration: Configuration { 17 | didSet { 18 | dialogView.configuration = configuration 19 | configuration.get(valueFor: Properties.Style.dialogControllerRoot)(rootViewContainer) 20 | } 21 | } 22 | 23 | public override init(title: String = "", root: ROOT = ROOT()) { 24 | dialogView = DialogView(content: root) 25 | 26 | super.init(title: title, root: root) 27 | 28 | modalTransitionStyle = .crossDissolve 29 | modalPresentationStyle = .overCurrentContext 30 | } 31 | 32 | open override func loadView() { 33 | view = rootViewContainer 34 | 35 | view.addSubview(dialogView) 36 | } 37 | 38 | open override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { 39 | let dismissalListener = presentingViewController as? DialogDismissalListener 40 | dismissalListener?.dialogWillDismiss() 41 | super.dismiss(animated: flag) { 42 | dismissalListener?.dialogDidDismiss() 43 | completion?() 44 | } 45 | } 46 | 47 | open override func updateRootViewConstraints() { 48 | dialogView.snp.updateConstraints { make in 49 | make.edges.equalTo(view) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Source/Core/View/ContainerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContainerView.swift 3 | // Reactant 4 | // 5 | // Created by Tadeáš Kříž on 05/04/16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class ContainerView: UIView, Configurable { 12 | 13 | open var configuration: Configuration = .global { 14 | didSet { 15 | layoutMargins = configuration.get(valueFor: Properties.layoutMargins) 16 | configuration.get(valueFor: Properties.Style.container)(self) 17 | } 18 | } 19 | 20 | open override class var requiresConstraintBasedLayout: Bool { 21 | return true 22 | } 23 | 24 | #if ENABLE_SAFEAREAINSETS_FALLBACK 25 | open override var frame: CGRect { 26 | didSet { 27 | fallback_computeSafeAreaInsets() 28 | } 29 | } 30 | 31 | open override var bounds: CGRect { 32 | didSet { 33 | fallback_computeSafeAreaInsets() 34 | } 35 | } 36 | 37 | open override var center: CGPoint { 38 | didSet { 39 | fallback_computeSafeAreaInsets() 40 | } 41 | } 42 | #endif 43 | 44 | public required init?(coder aDecoder: NSCoder) { 45 | super.init(coder: aDecoder) 46 | 47 | reloadConfiguration() 48 | } 49 | 50 | public override init(frame: CGRect) { 51 | super.init(frame: frame) 52 | 53 | reloadConfiguration() 54 | } 55 | 56 | public convenience init() { 57 | self.init(frame: CGRect.zero) 58 | 59 | reloadConfiguration() 60 | } 61 | 62 | open override func addSubview(_ view: UIView) { 63 | view.translatesAutoresizingMaskIntoConstraints = false 64 | 65 | super.addSubview(view) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Source/Core/Controller/ScrollControllerBase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollControllerBase.swift 3 | // Reactant 4 | // 5 | // Created by Tadeáš Kříž on 09/09/16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | open class ScrollControllerBase: ControllerBase where ROOT: Component { 12 | 13 | public let scrollView = UIScrollView() 14 | 15 | open override var configuration: Configuration { 16 | didSet { 17 | configuration.get(valueFor: Properties.Style.scroll)(scrollView) 18 | } 19 | } 20 | 21 | open override func loadView() { 22 | view = ControllerRootViewContainer().with(configuration: configuration) 23 | 24 | view.children( 25 | scrollView.children( 26 | rootView 27 | ) 28 | ) 29 | } 30 | 31 | public override init(title: String = "", root: ROOT = ROOT()) { 32 | super.init(title: title, root: root) 33 | } 34 | 35 | open override func updateRootViewConstraints() { 36 | scrollView.snp.updateConstraints { make in 37 | make.edges.equalTo(view) 38 | } 39 | 40 | rootView.snp.updateConstraints { make in 41 | make.leading.equalTo(view) 42 | make.top.equalTo(scrollView) 43 | make.trailing.equalTo(view) 44 | make.bottom.equalTo(scrollView) 45 | } 46 | } 47 | 48 | open override func viewDidLayoutSubviews() { 49 | scrollView.contentSize = rootView.bounds.size 50 | 51 | super.viewDidLayoutSubviews() 52 | } 53 | } 54 | 55 | extension ScrollControllerBase: Scrollable { 56 | 57 | public func scrollToTop(animated: Bool) { 58 | scrollView.scrollToTop(animated: animated) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/TestUtils/Rx+recording.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Rx+recording.swift 3 | // ReactantTests 4 | // 5 | // Created by Matyáš Kříž on 17/11/2017. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | import RxTest 11 | import Reactant 12 | 13 | private final class RecordingObserver 14 | : ObserverType { 15 | typealias Element = ElementType 16 | 17 | /// Recorded events. 18 | fileprivate(set) var events = [Event]() 19 | 20 | init() { } 21 | 22 | /// Notify observer about sequence event. 23 | /// 24 | /// - parameter event: Event that occurred. 25 | public func on(_ event: Event) { 26 | events.append(event) 27 | } 28 | } 29 | 30 | struct Recording { 31 | let events: [Event] 32 | let elements: [Element] 33 | let didComplete: Bool 34 | let didError: Bool 35 | let error: Swift.Error? 36 | 37 | init(events: [Event]) { 38 | self.events = events 39 | elements = events.compactMap { $0.element } 40 | 41 | if case .completed? = events.last { 42 | didComplete = true 43 | } else { 44 | didComplete = false 45 | } 46 | 47 | if case .error(let error)? = events.last { 48 | self.error = error 49 | } else { 50 | self.error = nil 51 | } 52 | didError = error != nil 53 | } 54 | } 55 | 56 | func recording(of recordedObservable: Observable, do work: () throws -> Void) rethrows -> Recording { 57 | let recordingObserver = RecordingObserver() 58 | let disposable = recordedObservable.subscribe(recordingObserver) 59 | defer { disposable.dispose() } 60 | try work() 61 | return Recording(events: recordingObserver.events) 62 | } 63 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Reactant (1.0.0): 3 | - Reactant/Core (= 1.0.0) 4 | - Reactant/Result (= 1.0.0) 5 | - Reactant/CollectionView (1.0.0): 6 | - Reactant/Core 7 | - RxCocoa (~> 3.0) 8 | - RxDataSources (~> 1.0) 9 | - Reactant/Configuration (1.0.0): 10 | - RxSwift (~> 3.0) 11 | - SnapKit (~> 3.0) 12 | - Reactant/Core (1.0.0): 13 | - Reactant/Configuration 14 | - RxCocoa (~> 3.0) 15 | - RxOptional (~> 3.0) 16 | - RxSwift (~> 3.0) 17 | - SnapKit (~> 3.0) 18 | - Reactant/Result (1.0.0): 19 | - Result (~> 3.0) 20 | - RxOptional (~> 3.0) 21 | - RxSwift (~> 3.0) 22 | - Reactant/TableView (1.0.0): 23 | - Reactant/Core 24 | - RxCocoa (~> 3.0) 25 | - RxDataSources (~> 1.0) 26 | - Reactant/Validation (1.0.0): 27 | - Result (~> 3.0) 28 | - Result (3.0.0) 29 | - RxCocoa (3.0.1): 30 | - RxSwift (~> 3.0) 31 | - RxDataSources (1.0.3): 32 | - RxCocoa (~> 3.0) 33 | - RxSwift (~> 3.0) 34 | - RxOptional (3.1.3): 35 | - RxCocoa 36 | - RxSwift 37 | - RxSwift (3.0.1) 38 | - SnapKit (3.2.0) 39 | 40 | DEPENDENCIES: 41 | - Reactant (from `../`) 42 | - Reactant/CollectionView (from `../`) 43 | - Reactant/TableView (from `../`) 44 | - Reactant/Validation (from `../`) 45 | 46 | EXTERNAL SOURCES: 47 | Reactant: 48 | :path: "../" 49 | 50 | SPEC CHECKSUMS: 51 | Reactant: 4062d70c2ad431246e45e4cde2d3ac265bb7252f 52 | Result: 1b3e431f37cbcd3ad89c6aa9ab0ae55515fae3b6 53 | RxCocoa: 15a52fc590dcc700cb4a690a633b5c5184ce3a78 54 | RxDataSources: a021a0e944ba5f7991259829d973afdbfa719c0b 55 | RxOptional: b97b59183af80d1e729ac9e51d09a10be668d6a8 56 | RxSwift: af5680055c4ad04480189c52d28385b1029493a6 57 | SnapKit: 1ca44df72cfa543218d177cb8aab029d10d86ea7 58 | 59 | PODFILE CHECKSUM: 8b14c208eae1cb2624c4f57db51a67aca61a132f 60 | 61 | COCOAPODS: 1.2.1 62 | -------------------------------------------------------------------------------- /ReactantPrototyping/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Example/Application/Sources/Wireframe/MainWireframe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainWireframe.swift 3 | // Reactant 4 | // 5 | // Created by Matous Hybl on 3/24/17. 6 | // Copyright © 2017 Brightify s.r.o. All rights reserved. 7 | // 8 | 9 | import Reactant 10 | 11 | class MainWireframe: Wireframe { 12 | 13 | private let dependencyModule: DependencyModule 14 | 15 | init(dependencyModule: DependencyModule) { 16 | self.dependencyModule = dependencyModule 17 | } 18 | 19 | func entrypoint() -> UIViewController { 20 | return UINavigationController(rootViewController: mainController()) 21 | } 22 | 23 | private func mainController() -> UIViewController { 24 | return create { provider in 25 | let reactions = MainViewController.Reactions(openTable: { 26 | provider.navigation?.push(controller: self.tableViewController()) 27 | }) 28 | 29 | return MainViewController(reactions: reactions) 30 | } 31 | } 32 | 33 | private func tableViewController() -> UIViewController { 34 | return create { provider in 35 | let dependencies = TableViewController.Dependencies(nameService: dependencyModule.nameService) 36 | let reactions = TableViewController.Reactions(displayName: { name in 37 | provider.navigation?.present(controller: self.nameAlertController(name: name)) 38 | }) 39 | return TableViewController(dependencies: dependencies, reactions: reactions) 40 | } 41 | } 42 | 43 | private func nameAlertController(name: String) -> UIViewController { 44 | let controller = UIAlertController(title: "This is a name", message: name, preferredStyle: .actionSheet) 45 | controller.addAction(UIAlertAction(title: "Close", style: .cancel, handler: { _ in controller.dismiss() })) 46 | return controller 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Source/CollectionView/Identifier/CollectionSupplementaryViewIdentifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionSupplementaryViewIdentifier.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 15.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public struct CollectionSupplementaryViewIdentifier { 12 | 13 | internal let name: String 14 | internal let kind: String 15 | 16 | public init(name: String = NSStringFromClass(T.self), kind: String) { 17 | self.name = name 18 | self.kind = kind 19 | } 20 | } 21 | 22 | extension UICollectionView { 23 | 24 | public func register(identifier: CollectionSupplementaryViewIdentifier) { 25 | register(CollectionReusableViewWrapper.self, forSupplementaryViewOfKind: identifier.kind, withReuseIdentifier: identifier.name) 26 | } 27 | 28 | public func unregister(identifier: CollectionSupplementaryViewIdentifier) { 29 | register(nil as AnyClass?, forSupplementaryViewOfKind: identifier.kind, withReuseIdentifier: identifier.name) 30 | } 31 | } 32 | 33 | extension UICollectionView { 34 | 35 | public func dequeue(identifier: CollectionSupplementaryViewIdentifier, for indexPath: IndexPath) -> CollectionReusableViewWrapper { 36 | guard let view = dequeueReusableSupplementaryView(ofKind: identifier.kind, withReuseIdentifier: identifier.name, for: indexPath) as? CollectionReusableViewWrapper else { 37 | fatalError("\(identifier) is not registered.") 38 | } 39 | return view 40 | } 41 | 42 | public func dequeue(identifier: CollectionSupplementaryViewIdentifier, forRow row: Int, inSection section: Int = 0) -> CollectionReusableViewWrapper { 43 | return dequeue(identifier: identifier, for: IndexPath(row: row, section: section)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Source/Utils/Internal/InternalUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InternalUtils.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 26.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | internal func degreesToRadians(_ value: Double) -> Double { 12 | return value * Double.pi / 180.0 13 | } 14 | 15 | internal func radiansToDegrees(_ value: Double) -> Double { 16 | return value * 180.0 / Double.pi 17 | } 18 | 19 | extension Double { 20 | 21 | internal func equal(to value: Double, precision: Double = Double.ulpOfOne) -> Bool { 22 | return abs(self - value) <= precision 23 | } 24 | } 25 | 26 | #if DEBUG 27 | func fatalError(_ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) -> Never { 28 | #if _runtime(_ObjC) 29 | NSException(name: .internalInconsistencyException, reason: message(), userInfo: nil).raise() 30 | #endif 31 | 32 | Swift.fatalError(message(), file: file, line: line) 33 | } 34 | 35 | func preconditionFailure(_ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) -> Never { 36 | #if _runtime(_ObjC) 37 | NSException(name: .internalInconsistencyException, reason: message(), userInfo: nil).raise() 38 | #endif 39 | 40 | Swift.preconditionFailure(message(), file: file, line: line) 41 | } 42 | 43 | func precondition(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) { 44 | guard !condition() else { return } 45 | #if _runtime(_ObjC) 46 | NSException(name: .internalInconsistencyException, reason: message(), userInfo: nil).raise() 47 | #endif 48 | 49 | Swift.preconditionFailure(message(), file: file, line: line) 50 | } 51 | #endif 52 | -------------------------------------------------------------------------------- /Source/CollectionView/Configuration+CollectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Configuration+CollectionView.swift 3 | // Reactant 4 | // 5 | // Created by Robin Krenecky on 24/04/2018. 6 | // Copyright © 2018 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public final class CollectionViewConfiguration: BaseSubConfiguration { 12 | public var collectionView: (ReactantCollectionView) -> Void { 13 | get { 14 | return configuration.get(valueFor: Properties.Style.CollectionView.collectionView) 15 | } 16 | set { 17 | configuration.set(Properties.Style.CollectionView.collectionView, to: newValue) 18 | } 19 | } 20 | 21 | public var reusableViewWrapper: (UICollectionReusableView) -> Void { 22 | get { 23 | return configuration.get(valueFor: Properties.Style.CollectionView.reusableViewWrapper) 24 | } 25 | set { 26 | configuration.set(Properties.Style.CollectionView.reusableViewWrapper, to: newValue) 27 | } 28 | } 29 | 30 | public var cellWrapper: (UICollectionViewCell) -> Void { 31 | get { 32 | return configuration.get(valueFor: Properties.Style.CollectionView.cellWrapper) 33 | } 34 | set { 35 | configuration.set(Properties.Style.CollectionView.cellWrapper, to: newValue) 36 | } 37 | } 38 | 39 | public var pageControl: (UIPageControl) -> Void { 40 | get { 41 | return configuration.get(valueFor: Properties.Style.CollectionView.pageControl) 42 | } 43 | set { 44 | configuration.set(Properties.Style.CollectionView.pageControl, to: newValue) 45 | } 46 | } 47 | } 48 | 49 | public extension Configuration.Style { 50 | var collectionView: CollectionViewConfiguration { 51 | return CollectionViewConfiguration(configuration: configuration) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/Utils/Array+UtilsTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+UtilsTest.swift 3 | // ReactantTests 4 | // 5 | // Created by Matyáš Kříž on 17/11/2017. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import Reactant 12 | 13 | class ArrayExtensionsTest: QuickSpec { 14 | override func spec() { 15 | describe("Array extension methods") { 16 | let emptyArray = [] as [String] 17 | let nonEmptyArray = [1, 2, 3, 4] 18 | describe("arrayByAppending using empty array returns original array") { 19 | expect(emptyArray.arrayByAppending()).to(equal(emptyArray)) 20 | expect(emptyArray.arrayByAppending([])).to(equal(emptyArray)) 21 | expect(nonEmptyArray.arrayByAppending()).to(equal(nonEmptyArray)) 22 | expect(nonEmptyArray.arrayByAppending([])).to(equal(nonEmptyArray)) 23 | } 24 | describe("arrayByAppending can append one element") { 25 | expect(emptyArray.arrayByAppending("testko")).to(equal(["testko"])) 26 | expect(emptyArray.arrayByAppending(["testko"])).to(equal(["testko"])) 27 | expect(nonEmptyArray.arrayByAppending(5)).to(equal([1, 2, 3, 4, 5])) 28 | expect(nonEmptyArray.arrayByAppending([5])).to(equal([1, 2, 3, 4, 5])) 29 | } 30 | describe("arrayByAppending can append arbitrary number of elements") { 31 | expect(emptyArray.arrayByAppending("testko", "otestovane")).to(equal(["testko", "otestovane"])) 32 | expect(emptyArray.arrayByAppending(["testko", "otestovane"])).to(equal(["testko", "otestovane"])) 33 | expect(nonEmptyArray.arrayByAppending(5, 6, 7)).to(equal([1, 2, 3, 4, 5, 6, 7])) 34 | expect(nonEmptyArray.arrayByAppending([5, 6, 7])).to(equal([1, 2, 3, 4, 5, 6, 7])) 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/parts/rootview.md: -------------------------------------------------------------------------------- 1 | # RootView 2 | 3 | As forementioned in the [Reactant architecture guide](../getting-started/architecture.md), `RootView` is a protocol and a marker for views that should be the root of a screen (filling the whole screen). Apart from being a marker, it's also used for communication with its parent controller. 4 | 5 | ## edgesForExtendedLayout 6 | 7 | This property is very similar to `UIViewController#edgesForExtendedLayout`. It specifies whether the view should be positioned behind translucent bars like the `UINavigationBar` and `UITabBar`. If you set `.all` (or `.top` / `.bottom`), the parent controller will make sure the view is under those bars. This is often used for root views that contain scroll views or tables. 8 | 9 | The default value is `[]` (or none), meaning that the root view will not be extended under those bars and thus no part of the view will be obstructed. 10 | 11 | ## viewWillAppear() 12 | Called by the parent Controller when the screen will appear. 13 | 14 | For more information take a look at [`UIViewController#viewWillAppear`](https://developer.apple.com/reference/uikit/uiviewcontroller/1621510-viewwillappear) 15 | 16 | ## viewDidAppear() 17 | Called by the parent Controller when the screen did appear. 18 | 19 | For more information take a look at [`UIViewController#viewDidAppear`](https://developer.apple.com/reference/uikit/uiviewcontroller/1621423-viewdidappear) 20 | 21 | ## viewWillDisappear() 22 | Called by the parent Controller when the screen will disappear. 23 | 24 | For more information take a look at [`UIViewController#viewWillDisappear`](https://developer.apple.com/reference/uikit/uiviewcontroller/1621485-viewwilldisappear) 25 | 26 | ## viewDidDisappear() 27 | Called by the parent Controller when the screen did disappear. 28 | 29 | For more information take a look at [`UIViewController#viewDidDisappear`](https://developer.apple.com/reference/uikit/uiviewcontroller/1621477-viewdiddisappear) 30 | -------------------------------------------------------------------------------- /Source/CollectionView/Internal/CollectionReusableViewWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionReusableViewWrapper.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 14.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | 12 | public final class CollectionReusableViewWrapper: UICollectionReusableView, Configurable { 13 | 14 | public var configurationChangeTime: clock_t = 0 15 | 16 | private var wrappedView: VIEW? 17 | 18 | public var configuration: Configuration = .global { 19 | didSet { 20 | (wrappedView as? Configurable)?.configuration = configuration 21 | configuration.get(valueFor: Properties.Style.CollectionView.reusableViewWrapper)(self) 22 | } 23 | } 24 | 25 | public var configureDisposeBag = DisposeBag() 26 | 27 | public override class var requiresConstraintBasedLayout: Bool { 28 | return true 29 | } 30 | 31 | public override func updateConstraints() { 32 | super.updateConstraints() 33 | 34 | wrappedView?.snp.updateConstraints { make in 35 | make.edges.equalTo(self) 36 | } 37 | } 38 | 39 | public func cachedViewOrCreated(factory: () -> VIEW) -> VIEW { 40 | if let wrappedView = wrappedView { 41 | return wrappedView 42 | } else { 43 | let wrappedView = factory() 44 | (wrappedView as? Configurable)?.configuration = configuration 45 | self.wrappedView = wrappedView 46 | children(wrappedView) 47 | setNeedsUpdateConstraints() 48 | return wrappedView 49 | } 50 | } 51 | 52 | public func deleteCachedView() -> VIEW? { 53 | let wrappedView = self.wrappedView 54 | wrappedView?.removeFromSuperview() 55 | self.wrappedView = nil 56 | return wrappedView 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Source/TableView/Internal/TableViewHeaderFooterWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewHeaderFooterWrapper.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 13.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | 12 | public final class TableViewHeaderFooterWrapper: UITableViewHeaderFooterView, Configurable { 13 | 14 | public var configurationChangeTime: clock_t = 0 15 | 16 | private var wrappedView: VIEW? 17 | 18 | public var configuration: Configuration = .global { 19 | didSet { 20 | (wrappedView as? Configurable)?.configuration = configuration 21 | configuration.get(valueFor: Properties.Style.TableView.headerFooterWrapper)(self) 22 | } 23 | } 24 | 25 | public var configureDisposeBag = DisposeBag() 26 | 27 | public override class var requiresConstraintBasedLayout: Bool { 28 | return true 29 | } 30 | 31 | public override func updateConstraints() { 32 | super.updateConstraints() 33 | 34 | wrappedView?.snp.updateConstraints { make in 35 | make.edges.equalTo(contentView) 36 | } 37 | } 38 | 39 | public func cachedViewOrCreated(factory: () -> VIEW) -> VIEW { 40 | if let wrappedView = wrappedView { 41 | return wrappedView 42 | } else { 43 | let wrappedView = factory() 44 | (wrappedView as? Configurable)?.configuration = configuration 45 | self.wrappedView = wrappedView 46 | contentView.children(wrappedView) 47 | setNeedsUpdateConstraints() 48 | return wrappedView 49 | } 50 | } 51 | 52 | public func deleteCachedView() -> VIEW? { 53 | let wrappedView = self.wrappedView 54 | wrappedView?.removeFromSuperview() 55 | self.wrappedView = nil 56 | return wrappedView 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Source/Core/Styling/AttributedString/NSAttributedString+Attribute.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSAttributedString+Attribute.swift 3 | // Reactant 4 | // 5 | // Created by Tadeas Kriz on 5/2/17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public func + (lhs: NSAttributedString, rhs: NSAttributedString) -> NSAttributedString { 12 | let mutableString = NSMutableAttributedString(attributedString: lhs) 13 | mutableString.append(rhs) 14 | return mutableString 15 | } 16 | 17 | public func + (lhs: String, rhs: NSAttributedString) -> NSAttributedString { 18 | return lhs.attributed() + rhs 19 | } 20 | 21 | public func + (lhs: NSAttributedString, rhs: String) -> NSAttributedString { 22 | return lhs + rhs.attributed() 23 | } 24 | 25 | extension String { 26 | 27 | /** 28 | * Allows you to easily create an `NSAttributedString` out of regular `String` 29 | * For available attributes see `Attribute`. 30 | * parameter attributes: passed attributes with which NSAttributedString is created 31 | * ## Example 32 | * ``` 33 | * let attributedString = "Beautiful String".attributed(.kern(1.2), .strokeWidth(1), .strokeColor(.red)) 34 | * ``` 35 | */ 36 | public func attributed(_ attributes: [Attribute]) -> NSAttributedString { 37 | return NSAttributedString(string: self, attributes: attributes.toDictionary()) 38 | } 39 | 40 | /** 41 | * Allows you to easily create an `NSAttributedString` out of regular `String` 42 | * For available attributes see `Attribute`. 43 | * parameter attributes: passed attributes with which NSAttributedString is created 44 | * ## Example 45 | * ``` 46 | * let attributedString = "Beautiful String".attributed(.kern(1.2), .strokeWidth(1), .strokeColor(.red)) 47 | * ``` 48 | */ 49 | public func attributed(_ attributes: Attribute...) -> NSAttributedString { 50 | return attributed(attributes) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Source/CollectionView/Identifier/CollectionViewCellIdentifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewCellIdentifier.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 15.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | 11 | public struct CollectionViewCellIdentifier { 12 | 13 | internal let name: String 14 | 15 | public init(name: String = NSStringFromClass(T.self)) { 16 | self.name = name 17 | } 18 | } 19 | 20 | extension UICollectionView { 21 | 22 | public func register(identifier: CollectionViewCellIdentifier) { 23 | register(CollectionViewCellWrapper.self, forCellWithReuseIdentifier: identifier.name) 24 | } 25 | 26 | public func unregister(identifier: CollectionViewCellIdentifier) { 27 | register(nil as AnyClass?, forCellWithReuseIdentifier: identifier.name) 28 | } 29 | } 30 | 31 | extension UICollectionView { 32 | 33 | public func dequeue(identifier: CollectionViewCellIdentifier, for indexPath: IndexPath) -> CollectionViewCellWrapper { 34 | guard let cell = dequeueReusableCell(withReuseIdentifier: identifier.name, for: indexPath) as? CollectionViewCellWrapper else { 35 | fatalError("\(identifier) is not registered.") 36 | } 37 | return cell 38 | } 39 | 40 | public func dequeue(identifier: CollectionViewCellIdentifier, forRow row: Int, inSection section: Int = 0) -> CollectionViewCellWrapper { 41 | return dequeue(identifier: identifier, for: IndexPath(row: row, section: section)) 42 | } 43 | 44 | public func items(with identifier: CollectionViewCellIdentifier) -> 45 | (_ source: O) -> (_ configureCell: @escaping (Int, S.Iterator.Element, CollectionViewCellWrapper) -> Void) -> Disposable where O.Element == S { 46 | return rx.items(cellIdentifier: identifier.name) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/parts/wireframe.md: -------------------------------------------------------------------------------- 1 | # Wireframe 2 | 3 | In Reactant, Wireframe is meant to handle transitions between controllers, so that no controller knows about any other controllers. To learn more, head over to our [Reactant Architecture Guide](../getting-started/architecture.md). 4 | 5 | To make this a little easier we include a protocol `Wireframe` with two helper methods: 6 | 7 | ## `create(factory:)` 8 | 9 | Use create to get access to enclosing `UINavigationController` and the created controller in reaction closures you provide the controller. This lets you break the dependency cycle. 10 | 11 | The `create` method has following signature: 12 | 13 | ```swift 14 | public func create(factory: (FutureControllerProvider) -> T) -> T 15 | ``` 16 | 17 | And you would use it like so: 18 | 19 | ```swift 20 | class MainWireframe: Wireframe { 21 | func controller1() -> Controller1 { 22 | return create { provider in 23 | let reactions = Controller1.Reactions( 24 | openController2: { 25 | provider.navigation?.push(self.controller2()) 26 | } 27 | ) 28 | return Controller1(reactions: reactions) 29 | } 30 | } 31 | 32 | func controller2() -> Controller2 { 33 | return create { _ in 34 | return Controller1() 35 | } 36 | } 37 | } 38 | ``` 39 | 40 | 41 | ## `branchNavigation(controller:)` 42 | 43 | Navigation branching is especially useful when you need to present a controller modally and want to display a navigation bar (with the possibility to dismiss the controller). When that happens, you can use the `branchNavigation` to wrap your controller inside `UINavigationController`. It will also set `leftBarButtonItem` for you that will dismiss the modal controller. 44 | 45 | ```swift 46 | public func branchNavigation(controller: UIViewController, closeButtonTitle: String?) -> UINavigationController 47 | 48 | public func branchNavigation(controller: ControllerBase) -> UINavigationController 49 | ``` 50 | -------------------------------------------------------------------------------- /Tests/Configuration/ConfigurableTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurableTest.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 26.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import Reactant 12 | 13 | private typealias Configuration = Reactant.Configuration 14 | 15 | class ConfigurableTest: QuickSpec { 16 | 17 | override func spec() { 18 | describe("Configurable") { 19 | let configuration = Configuration() 20 | 21 | describe("reloadConfiguration") { 22 | it("sets configuration with the same value") { 23 | let configurable = ConfigurableStub(configuration: configuration) 24 | var called = false 25 | configurable.didSetCallback = { 26 | expect(configurable.configuration) === configuration 27 | called = true 28 | } 29 | 30 | configurable.reloadConfiguration() 31 | 32 | expect(called).to(beTrue()) 33 | } 34 | } 35 | describe("with") { 36 | it("sets configuration") { 37 | let differentConfiguration = Configuration() 38 | let configurable = ConfigurableStub(configuration: configuration) 39 | 40 | expect(configurable.with(configuration: differentConfiguration).configuration) === differentConfiguration 41 | } 42 | } 43 | } 44 | } 45 | 46 | private class ConfigurableStub: Configurable { 47 | 48 | var configuration: Configuration { 49 | didSet { 50 | didSetCallback() 51 | } 52 | } 53 | 54 | var didSetCallback: () -> Void = {} 55 | 56 | init(configuration: Configuration) { 57 | self.configuration = configuration 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /docs/parts/tableview.md: -------------------------------------------------------------------------------- 1 | # TableView 2 | 3 | In Reactant, using table views is simple. There are TableView classes prepared for drop-in use, suiting most of the use cases. The TableViews are Components so they have their `componentState` and actions depending on the type of TableView. 4 | 5 | Every TableView's component State is a `TableViewState` which is an enum containing these cases: 6 | ```swift 7 | public enum TableViewState { 8 | case items([MODEL]) 9 | case empty(message: String) 10 | case loading 11 | } 12 | ``` 13 | 14 | In `init` of every TableView, you can configure if the TableView is reloadable and other properties depending on TableView type. 15 | 16 | #### SimpleTableView 17 | `SimpleTableView` is the most generic TableView of them all. Its purpose is to display table with footers and headers. It has three generic parameters: `HEADER` - View component used as headers, `CELL` View component used as cells, `FOOTER` view component used as footers. `MODEL` parameter in this case is a `SectionModel` which binds together component State of section header, array of component States of cells and component State of section footer. 18 | 19 | #### PlainTableView 20 | `PlainTableView` is used for displaying plain table view consisting of cells only. `MODEL` parameter of this TableView's `TableViewState` is component State of the cell. 21 | 22 | Example of using this type of TableView directly as RootView is shown here. 23 | ```swift 24 | class TableViewRootView: PlainTableView, RootView { 25 | 26 | override var edgesForExtendedLayout: UIRectEdge { 27 | return .all 28 | } 29 | 30 | init() { 31 | super.init(cellFactory: LabelView.init, 32 | style: .plain, 33 | reloadable: false) 34 | } 35 | } 36 | ``` 37 | 38 | #### HeaderTableView and FooterTableView 39 | These two TableViews show sections with headers only or footers only. 40 | 41 | #### SimulatedSeparatorTableView 42 | This TableView is used for displaying TableView with separators of bigger height than default. 43 | -------------------------------------------------------------------------------- /ReactantPrototyping/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Source/Core/View/Impl/PickerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PickerView.swift 3 | // Reactant 4 | // 5 | // Created by Matouš Hýbl on 02/04/2018. 6 | // Copyright © 2018 Brightify. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | 11 | #if os(iOS) 12 | public class PickerView: ViewBase, UIPickerViewDataSource, UIPickerViewDelegate { 13 | private let pickerView = UIPickerView() 14 | 15 | public let items: [MODEL] 16 | public let titleSelection: (MODEL) -> String 17 | 18 | public init(items: [MODEL], titleSelection: @escaping (MODEL) -> String) { 19 | self.items = items 20 | self.titleSelection = titleSelection 21 | super.init() 22 | } 23 | 24 | public override func update() { 25 | let title = titleSelection(componentState) 26 | guard let index = items.firstIndex(where: { titleSelection($0) == title }) else { return } 27 | pickerView.selectRow(index, inComponent: 0, animated: true) 28 | } 29 | 30 | public override func loadView() { 31 | children( 32 | pickerView 33 | ) 34 | 35 | pickerView.dataSource = self 36 | pickerView.delegate = self 37 | } 38 | 39 | public override func setupConstraints() { 40 | pickerView.snp.makeConstraints { make in 41 | make.edges.equalToSuperview() 42 | } 43 | } 44 | 45 | public func numberOfComponents(in pickerView: UIPickerView) -> Int { 46 | return 1 47 | } 48 | 49 | public func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { 50 | return items.count 51 | } 52 | 53 | public func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { 54 | let model = items[row] 55 | 56 | return titleSelection(model) 57 | } 58 | 59 | public func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { 60 | let model = items[row] 61 | 62 | perform(action: model) 63 | } 64 | } 65 | #endif 66 | -------------------------------------------------------------------------------- /Source/Core/View/Internal/ControllerRootViewContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ControllerRootViewContainer.swift 3 | // Reactant 4 | // 5 | // Created by Tadeas Kriz on 08/01/16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public final class ControllerRootViewContainer: UIView, Configurable { 12 | 13 | public let wrappedView: UIView? 14 | 15 | public var configuration: Configuration = .global { 16 | didSet { 17 | configuration.get(valueFor: Properties.Style.controllerRoot)(self) 18 | } 19 | } 20 | 21 | public override var frame: CGRect { 22 | didSet { 23 | #if ENABLE_SAFEAREAINSETS_FALLBACK 24 | fallback_computeSafeAreaInsets() 25 | #endif 26 | wrappedView?.frame = bounds 27 | } 28 | } 29 | 30 | #if ENABLE_SAFEAREAINSETS_FALLBACK 31 | public override var bounds: CGRect { 32 | didSet { 33 | fallback_computeSafeAreaInsets() 34 | } 35 | } 36 | 37 | public override var center: CGPoint { 38 | didSet { 39 | fallback_computeSafeAreaInsets() 40 | } 41 | } 42 | #endif 43 | 44 | public required init?(coder aDecoder: NSCoder) { 45 | wrappedView = nil 46 | 47 | super.init(coder: aDecoder) 48 | 49 | loadView() 50 | reloadConfiguration() 51 | } 52 | 53 | public override init(frame: CGRect = .zero) { 54 | wrappedView = nil 55 | 56 | super.init(frame: frame) 57 | 58 | loadView() 59 | reloadConfiguration() 60 | } 61 | 62 | public init(wrap: UIView) { 63 | wrappedView = wrap 64 | 65 | super.init(frame: CGRect.zero) 66 | 67 | addSubview(wrap) 68 | reloadConfiguration() 69 | } 70 | 71 | private func loadView() { 72 | autoresizingMask = [.flexibleWidth, .flexibleHeight] 73 | if frame == CGRect.zero { 74 | frame = window?.bounds ?? UIScreen.main.bounds 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/Core/Component/ComponentBaseTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComponentBase.swift 3 | // Reactant 4 | // 5 | // Created by Matyáš Kříž on 16/11/2017. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import Reactant 12 | import RxSwift 13 | import Cuckoo 14 | 15 | class ComponentBaseTest: QuickSpec { 16 | override func spec() { 17 | describe("ComponentBase") { 18 | it("constructs successfully") { 19 | _ = ComponentBase(canUpdate: false) 20 | _ = ComponentBase(canUpdate: true) 21 | _ = ComponentBase<[String], String>(canUpdate: true) 22 | _ = ComponentBase(canUpdate: false) 23 | _ = ComponentBase<[Int], [Int]>(canUpdate: false) 24 | } 25 | it("calls update on init only once when componentState is Void") { 26 | let componentWithUpdate = ComponentBase.spy(canUpdate: true) 27 | let componentNoUpdate = ComponentBase.spy(canUpdate: false) 28 | 29 | verify(componentWithUpdate).update() 30 | verify(componentNoUpdate, never()).update() 31 | } 32 | it("does not call update on init when componentState is not Void") { 33 | let componentWithUpdate = ComponentBase.spy(canUpdate: true) 34 | let componentNoUpdate = ComponentBase.spy(canUpdate: false) 35 | 36 | verify(componentWithUpdate, never()).update() 37 | verify(componentNoUpdate, never()).update() 38 | } 39 | it("calls afterInit on init only once") { 40 | let componentWithUpdate = ComponentBase.spy(canUpdate: true) 41 | let componentNoUpdate = ComponentBase.spy(canUpdate: false) 42 | 43 | verify(componentWithUpdate).afterInit() 44 | verify(componentNoUpdate).afterInit() 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Source/TableView/Identifier/TableViewCellIdentifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TableViewCellIdentifier.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 15.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | 11 | public struct TableViewCellIdentifier { 12 | 13 | internal let name: String 14 | 15 | public init(name: String = NSStringFromClass(T.self)) { 16 | self.name = name 17 | } 18 | } 19 | 20 | public extension UITableView { 21 | 22 | func register(identifier: TableViewCellIdentifier) { 23 | register(TableViewCellWrapper.self, forCellReuseIdentifier: identifier.name) 24 | } 25 | 26 | func unregister(identifier: TableViewCellIdentifier) { 27 | register(nil as AnyClass?, forCellReuseIdentifier: identifier.name) 28 | } 29 | } 30 | 31 | public extension UITableView { 32 | 33 | func items(with identifier: TableViewCellIdentifier) -> 34 | (_ source: O) -> (_ configureCell: @escaping (Int, S.Iterator.Element, TableViewCellWrapper) -> Void) -> Disposable where O.Element == S { 35 | return rx.items(cellIdentifier: identifier.name) 36 | } 37 | 38 | func dequeue(identifier: TableViewCellIdentifier) -> TableViewCellWrapper { 39 | guard let cell = dequeueReusableCell(withIdentifier: identifier.name) as? TableViewCellWrapper else { 40 | fatalError("\(identifier) is not registered.") 41 | } 42 | return cell 43 | } 44 | 45 | func dequeue(identifier: TableViewCellIdentifier, for indexPath: IndexPath) -> TableViewCellWrapper { 46 | guard let cell = dequeueReusableCell(withIdentifier: identifier.name, for: indexPath) as? TableViewCellWrapper else { 47 | fatalError("\(identifier) is not registered.") 48 | } 49 | return cell 50 | } 51 | 52 | func dequeue(identifier: TableViewCellIdentifier, forRow row: Int, inSection section: Int = 0) -> TableViewCellWrapper { 53 | return dequeue(identifier: identifier, for: IndexPath(row: row, section: section)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Source/Utils/UIView+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Utils.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 20.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | 13 | /** 14 | * Adds subviews to the view that this method is called upon. 15 | * Convenience method working the same as `addSubview(_:)` but letting you pass multiple UIViews at once. 16 | * - parameter children: `UIView`s to be added as subviews 17 | * ## Example 18 | * ``` 19 | * override func loadView() { 20 | * children( 21 | * titleLabel, 22 | * loginContainer.children( 23 | * loginTextField, 24 | * passwordTextField, 25 | * ), 26 | * passwordRecoveryButton 27 | * ) 28 | * } 29 | * ``` 30 | */ 31 | @discardableResult 32 | public func children(_ children: UIView...) -> UIView { 33 | return self.children(children) 34 | } 35 | 36 | /** 37 | * Adds subviews to the view that this method is called upon. 38 | * Convenience method working the same as `addSubview(_:)` but letting you pass an array of UIViews. 39 | * - parameter children: `UIView`s to be added as subviews 40 | * ## Example 41 | * ``` 42 | * override func loadView() { 43 | * children( 44 | * titleLabel, 45 | * loginContainer.children( 46 | * loginTextField, 47 | * passwordTextField, 48 | * ), 49 | * passwordRecoveryButton 50 | * ) 51 | * } 52 | * ``` 53 | */ 54 | @discardableResult 55 | public func children(_ children: [UIView]) -> UIView { 56 | children.forEach(addSubview) 57 | return self 58 | } 59 | 60 | /** 61 | * Variable pointing to view's superview if there is one or self if there isn't. 62 | * Usually used in Controller when passing `Component.componentState` to the corresponding RootView. 63 | */ 64 | public var rootView: UIView { 65 | if let superview = superview { 66 | return superview.rootView 67 | } else { 68 | return self 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Source/CollectionView/Implementation/SimpleCollectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleCollectionView.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 12.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | 11 | public enum SimpleCollectionViewAction { 12 | case selected(CELL.StateType) 13 | case cellAction(CELL.StateType, CELL.ActionType) 14 | case refresh 15 | } 16 | 17 | open class SimpleCollectionView: FlowCollectionViewBase> where CELL: Component { 18 | 19 | public typealias MODEL = CELL.StateType 20 | 21 | private let cellIdentifier = CollectionViewCellIdentifier() 22 | 23 | open override var actions: [Observable>] { 24 | #if os(iOS) 25 | return [ 26 | collectionView.rx.modelSelected(MODEL.self).map(SimpleCollectionViewAction.selected), 27 | refreshControl?.rx.controlEvent(.valueChanged).rewrite(with: SimpleCollectionViewAction.refresh) 28 | ].compactMap { $0 } 29 | #else 30 | return [ 31 | collectionView.rx.modelSelected(MODEL.self).map(SimpleCollectionViewAction.selected) 32 | ] 33 | #endif 34 | } 35 | 36 | private let cellFactory: () -> CELL 37 | 38 | public init(cellFactory: @escaping () -> CELL = CELL.init, 39 | reloadable: Bool = true, 40 | automaticallyDeselect: Bool = true) { 41 | self.cellFactory = cellFactory 42 | 43 | super.init(reloadable: reloadable, automaticallyDeselect: automaticallyDeselect) 44 | } 45 | 46 | open override func loadView() { 47 | super.loadView() 48 | 49 | collectionView.register(identifier: cellIdentifier) 50 | } 51 | 52 | open override func bind(items: Observable<[MODEL]>) { 53 | items 54 | .bind(to: collectionView.items(with: cellIdentifier)) { [unowned self] row, model, cell in 55 | self.configure(cell: cell, factory: self.cellFactory, model: model, mapAction: { SimpleCollectionViewAction.cellAction(model, $0) }) 56 | } 57 | .disposed(by: lifetimeDisposeBag) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Source/Validation/Rule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Rule.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 20.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | /** 10 | * Structure used for setting up a validation rule for values of generic type `T`. 11 | * ## Example 12 | * ``` 13 | * typealias Human = (name: String, friendly: Bool) 14 | * 15 | * let people: [Human] = [("Adam", true), ("Eva", false), ("Agnes", true), ("Rob", false)] 16 | * 17 | * let friendRule = Rule(validate: { human in 18 | * if human.friendly { 19 | * return nil // no error here, we want to be friends 20 | * } else { 21 | * return FriendError.notFriendly 22 | * } 23 | * }) 24 | * 25 | * let friends = people.filter { potentialFriend in 26 | * friendRule.test(potentialFriend) 27 | * } 28 | * - NOTE: There is a ready-made `ValidationError` with `case invalid` ready for you to use if you don't feel like creating your own `Error` enum. 29 | * ``` 30 | */ 31 | public struct Rule { 32 | 33 | /// Closure used for validating generic value `T`. 34 | public let validate: (T) -> E? 35 | 36 | /** 37 | * Main initializer for `Rule`. 38 | * - parameter validate: closure that is later used in `test(_:)` and `run(_:)` to validate generic value `T` 39 | */ 40 | public init(validate: @escaping (T) -> E?) { 41 | self.validate = validate 42 | } 43 | 44 | /** 45 | * Method testing the passed value with the validation closure. 46 | * - parameter value: value to be tested 47 | * - returns: `Bool`, `true` if validation was successful and `false` otherwise 48 | */ 49 | public func test(_ value: T) -> Bool { 50 | return validate(value) == nil 51 | } 52 | 53 | /** 54 | * Method running the passed value through the validation closure. 55 | * - parameter value: value to be tested, must be of generic type `T` 56 | * - returns: `Result`, `.success` with the passed value if validation was successful and `.failure` with the error otherwise 57 | */ 58 | public func run(_ value: T) -> Result { 59 | if let error = validate(value) { 60 | return .failure(error) 61 | } else { 62 | return .success(value) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Tests/Configuration/ConfigurationTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigurationTest.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 26.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import Reactant 12 | 13 | private typealias Configuration = Reactant.Configuration 14 | 15 | class ConfigurationTest: QuickSpec { 16 | 17 | override func spec() { 18 | describe("Configuration") { 19 | describe("init") { 20 | it("copies configurations preserving last value") { 21 | let configuration1 = Configuration() 22 | configuration1.set(Properties.int, to: 1) 23 | let configuration2 = Configuration() 24 | configuration2.set(Properties.int, to: 2) 25 | 26 | let configuration = Configuration(copy: configuration1, configuration2) 27 | 28 | expect(configuration.get(valueFor: Properties.int)) == 2 29 | expect(configuration.get(valueFor: Properties.string)) == "" 30 | } 31 | } 32 | describe("get/set") { 33 | it("gets and sets value for property") { 34 | let configuration = Configuration() 35 | 36 | configuration.set(Properties.int, to: 1) 37 | configuration.set(Properties.string, to: "A") 38 | 39 | expect(configuration.get(valueFor: Properties.int)) == 1 40 | expect(configuration.get(valueFor: Properties.string)) == "A" 41 | } 42 | } 43 | describe("get") { 44 | it("returns Property.defaultValue if not set") { 45 | let configuration = Configuration() 46 | 47 | expect(configuration.get(valueFor: Properties.int)) == 0 48 | expect(configuration.get(valueFor: Properties.string)) == "" 49 | } 50 | } 51 | } 52 | } 53 | 54 | func apiTest() { 55 | _ = Configuration.global 56 | } 57 | 58 | private struct Properties { 59 | 60 | static let int = Property(defaultValue: 0) 61 | static let string = Property(defaultValue: "") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Source/Core/Wireframe/UIViewController+Navigation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Navigation.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 09.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | 11 | extension UIViewController { 12 | 13 | /** 14 | * Presents a view controller and returns `Observable` that indicates when the view has been successfully presented. 15 | * - parameter controller: generic controller to present 16 | * - parameter animated: determines whether the view controller presentation should be animated, default is `true` 17 | */ 18 | @discardableResult 19 | public func present(controller: C, animated: Bool = true) -> Observable { 20 | let replay = ReplaySubject.create(bufferSize: 1) 21 | present(controller, animated: animated, completion: { replay.onLast() }) 22 | return replay.rewrite(with: controller) 23 | } 24 | 25 | /** 26 | * Dismisses topmost view controller and returns `Observable` that indicates when the view has been dismissed. 27 | * - parameter animated: determines whether the view controller dismissal should be animated, default is `true` 28 | */ 29 | @discardableResult 30 | public func dismiss(animated: Bool = true) -> Observable { 31 | let replay = ReplaySubject.create(bufferSize: 1) 32 | dismiss(animated: animated, completion: { replay.onLast() }) 33 | return replay 34 | } 35 | 36 | @discardableResult 37 | public func present(controller: Observable, animated: Bool = true) -> Observable { 38 | let replay = ReplaySubject.create(bufferSize: 1) 39 | _ = controller 40 | .takeUntil(rx.deallocated) 41 | .flatMapLatest { [weak self] controller in 42 | self?.present(controller: controller).rewrite(with: controller) ?? .empty() 43 | } 44 | .subscribe( 45 | onNext: { [weak self] controller in 46 | guard self != nil else { 47 | replay.onCompleted() 48 | return 49 | } 50 | replay.onNext(controller) 51 | }, 52 | onDisposed: { 53 | replay.onCompleted() 54 | }) 55 | return replay 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.3.1 4 | 5 | * Make sure to call `__rui.updateReactantUI` after `__rui.setupReactantUI` on iOS 13+, tvOS 13+ and macOS 10.15+. 6 | 7 | ## 1.3.0 8 | * Support for Swift 5.0 9 | * Upgraded to RxSwift 5.0 10 | * Min system version increased to: 11 | * iOS 10.0 12 | * tvOS 10.0 13 | 14 | 15 | ## 1.2.0 16 | * Add takeUntil to Wireframe 17 | * Add option to disable automatic cell deselection 18 | * Add ActivityIndicator to subspec groups 19 | * Add reference for create() and rewrite() 20 | * Deprecate ButtonBase with ControlBase 21 | * Add system font support 22 | * Update project for Swift 4.1 23 | * Add generic PickerView 24 | * Add method for stacking UIViews inside UIView 25 | * Fix `rotate` in CGAffineTransformation extensions 26 | * Add `dequeueAndConfigure(identifier:indexPath:factory:model:mapAction:)` to `CollectionView` 27 | * Add better initial configuration of `TableView` and `CollectionView` 28 | * Add styling methods for `UINavigationController` and `UITabBarController` 29 | * Remove RxSwift and SnapKit dependencies from `Configuration` subspec 30 | * Add Observable navigation 31 | 32 | ## 1.1.0 33 | * Add FallbackSafeAreaInsets subspec with a basic implementation of a `safeAreaInsets` and `safeAreaLayoutGuide` fallback for iOS 10. 34 | * Add support for tvOS 35 | * Add reference guide 36 | * Add more docs 37 | * Add tutorials 38 | * Add more tests 39 | 40 | ## 1.0.6 41 | * Fix TableViewBase and CollectionViewBase memory leak. 42 | 43 | ## 1.0.5 44 | * Add create to Wireframe with controller result helper 45 | * Add default implementation to DialogDismissalController 46 | * Add option to have present dialog with result in UINavigationController 47 | * Add styling for DialogControllerBase's `view` 48 | * Possibly breaking: changed `bind(items: [MODEL])` to `bind(items: Observable<[MODEL]>)` in both `TableViewBase` and `CollectionViewBase`. This change was made because RxSwift changed the internals of delegates and dataSources and each `update` caused the TableView/CollectionView to scroll to the beginning. 49 | 50 | ## 1.0.4 51 | * Improved documentation 52 | 53 | ## 1.0.3 54 | * Fixed `TextField` where placeholder didn't have correct position 55 | 56 | ## 1.0.2 57 | * Added `setBackgroundColor:forState` objc alias for ReactantUI 58 | 59 | ## 1.0.1 60 | * Fixed Reactant Example project 61 | 62 | ## 0.6.0 63 | 64 | * Complete API redesign 65 | * Added tests 66 | * Added documentation 67 | * Added changelog 68 | -------------------------------------------------------------------------------- /Source/Configuration/Configuration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Configuration.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 14.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | /** 10 | * A container for properties. This class is intended for global setup of Reactant internals. At the same time it can be 11 | * instantiated for multiple configurations (for example each part of application can have different background color). 12 | * 13 | * Reactant comes with prebuilt properties which you can find in extensions to the Properties struct. It's recommended 14 | * to add any new property into the Properties struct via an extension to keep properties easily discoverable via auto-complete. 15 | */ 16 | public final class Configuration { 17 | // XXX: This code is here due to bug in Swift/Xcode where types inside extensions depend on compilation order 18 | public typealias Style = StyleConfiguration 19 | 20 | /// Global configuration instance. It's used as a default configuration in every component throughout Reactant. 21 | public static var global = Configuration() 22 | 23 | private var data: [Int: Any] = [:] 24 | 25 | /** 26 | * Initializes configuration as a combination of provided configurations. 27 | * If no configuration is provided, an empty Configuration is created. 28 | * - parameter copy: variadic parameter, passing in multiple `Configuration`s merges them into one 29 | */ 30 | public init(copy: Configuration...) { 31 | self.data = copy.reduce([:]) { acc, value in 32 | var dictionary = acc 33 | value.data.forEach { dictionary[$0] = $1 } 34 | return dictionary 35 | } 36 | } 37 | 38 | /** 39 | * Standard setter for a value for passed property. 40 | * - parameter property: property to which you want to set the `Configuration` 41 | * - parameter value: value to set for specified property 42 | */ 43 | public func set(_ property: Property, to value: T) { 44 | data[property.id] = value 45 | } 46 | 47 | /** 48 | * Standard getter for configuration properties. 49 | * - parameter property: property from which you want to get the `Configuration` 50 | * - returns: value for specified property or a property's default value if it hasn't been set in this `Configuration` 51 | */ 52 | public func get(valueFor property: Property) -> T { 53 | return (data[property.id] as? T) ?? property.defaultValue 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Source/CollectionView/Implementation/PagingCollectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PagingCollectionView.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 16.02.17. 6 | // Copyright © 2017 Brightify. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | 11 | open class PagingCollectionView: SimpleCollectionView where CELL: Component { 12 | 13 | open override var configuration: Configuration { 14 | didSet { 15 | configuration.get(valueFor: Properties.Style.CollectionView.pageControl)(pageControl) 16 | } 17 | } 18 | 19 | open var showPageControl: Bool { 20 | get { 21 | return pageControl.visibility == .visible 22 | } 23 | set { 24 | pageControl.visibility = newValue ? .visible : .hidden 25 | } 26 | } 27 | 28 | public let pageControl = UIPageControl() 29 | 30 | public override init(cellFactory: @escaping () -> CELL = CELL.init, reloadable: Bool = true, automaticallyDeselect: Bool = true) { 31 | super.init(cellFactory: cellFactory, reloadable: reloadable, automaticallyDeselect: automaticallyDeselect) 32 | } 33 | 34 | open override func loadView() { 35 | super.loadView() 36 | 37 | children( 38 | pageControl 39 | ) 40 | 41 | #if os(iOS) 42 | collectionView.isPagingEnabled = true 43 | #endif 44 | collectionView.showsHorizontalScrollIndicator = false 45 | collectionViewLayout.scrollDirection = .horizontal 46 | collectionViewLayout.minimumLineSpacing = 0 47 | } 48 | 49 | open override func setupConstraints() { 50 | super.setupConstraints() 51 | 52 | pageControl.snp.makeConstraints { make in 53 | make.width.equalTo(self) 54 | make.bottom.equalTo(8) 55 | } 56 | } 57 | 58 | open override func bind(items: Observable<[CELL.StateType]>) { 59 | super.bind(items: items) 60 | 61 | items.subscribe(onNext: { [pageControl] items in 62 | pageControl.numberOfPages = items.count 63 | }).disposed(by: lifetimeDisposeBag) 64 | } 65 | 66 | open override func layoutSubviews() { 67 | super.layoutSubviews() 68 | 69 | itemSize = collectionView.bounds.size 70 | } 71 | 72 | open func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 73 | pageControl.currentPage = Int(collectionView.contentOffset.x / itemSize.width) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Source/CollectionView/Internal/CollectionViewCellWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewCellWrapper.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 14.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import RxSwift 11 | 12 | public final class CollectionViewCellWrapper: UICollectionViewCell, Configurable { 13 | 14 | public var configurationChangeTime: clock_t = 0 15 | 16 | private var cell: CELL? 17 | 18 | public var configuration: Configuration = .global { 19 | didSet { 20 | (cell as? Configurable)?.configuration = configuration 21 | configuration.get(valueFor: Properties.Style.CollectionView.cellWrapper)(self) 22 | } 23 | } 24 | 25 | public override class var requiresConstraintBasedLayout: Bool { 26 | return true 27 | } 28 | 29 | public override var preferredFocusedView: UIView? { 30 | return cell 31 | } 32 | 33 | @available(iOS 9.0, tvOS 9.0, *) 34 | public override var preferredFocusEnvironments: [UIFocusEnvironment] { 35 | return cell.map { [$0] } ?? [] 36 | } 37 | 38 | private var collectionViewCell: CollectionViewCell? { 39 | return cell as? CollectionViewCell 40 | } 41 | 42 | public override var isSelected: Bool { 43 | didSet { 44 | collectionViewCell?.setSelected(isSelected) 45 | } 46 | } 47 | 48 | public var configureDisposeBag = DisposeBag() 49 | 50 | public override var isHighlighted: Bool { 51 | didSet { 52 | collectionViewCell?.setHighlighted(isHighlighted) 53 | } 54 | } 55 | 56 | public override func updateConstraints() { 57 | super.updateConstraints() 58 | 59 | cell?.snp.updateConstraints { make in 60 | make.edges.equalTo(contentView) 61 | } 62 | } 63 | 64 | 65 | public func cachedCellOrCreated(factory: () -> CELL) -> CELL { 66 | if let cell = cell { 67 | return cell 68 | } else { 69 | let cell = factory() 70 | (cell as? Configurable)?.configuration = configuration 71 | self.cell = cell 72 | contentView.children(cell) 73 | setNeedsUpdateConstraints() 74 | return cell 75 | } 76 | } 77 | 78 | public func deleteCachedCell() -> CELL? { 79 | let cell = self.cell 80 | cell?.removeFromSuperview() 81 | self.cell = nil 82 | return cell 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Source/TableView/Implementation/PlainTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlainTableView.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 16.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | 11 | public enum PlainTableViewAction { 12 | case selected(CELL.StateType) 13 | case rowAction(CELL.StateType, CELL.ActionType) 14 | case refresh 15 | } 16 | 17 | open class PlainTableView: TableViewBase> where CELL: Component { 18 | 19 | public typealias MODEL = CELL.StateType 20 | 21 | private let cellIdentifier = TableViewCellIdentifier() 22 | 23 | open override var actions: [Observable>] { 24 | #if os(iOS) 25 | return [ 26 | tableView.rx.modelSelected(MODEL.self).map(PlainTableViewAction.selected), 27 | refreshControl?.rx.controlEvent(.valueChanged).rewrite(with: PlainTableViewAction.refresh) 28 | ].compactMap { $0 } 29 | #else 30 | return [ 31 | tableView.rx.modelSelected(MODEL.self).map(PlainTableViewAction.selected) 32 | ] 33 | #endif 34 | } 35 | 36 | private let cellFactory: () -> CELL 37 | 38 | public init( 39 | cellFactory: @escaping () -> CELL = CELL.init, 40 | style: UITableView.Style = .plain, 41 | options: TableViewOptions = []) 42 | { 43 | self.cellFactory = cellFactory 44 | 45 | super.init(style: style, options: options) 46 | } 47 | 48 | @available(*, deprecated, message: "This init will be removed in Reactant 2.0") 49 | public init( 50 | cellFactory: @escaping () -> CELL = CELL.init, 51 | style: UITableView.Style = .plain, 52 | reloadable: Bool = true, 53 | automaticallyDeselect: Bool = true) 54 | { 55 | self.cellFactory = cellFactory 56 | 57 | super.init(style: style, reloadable: reloadable, automaticallyDeselect: automaticallyDeselect) 58 | } 59 | 60 | open override func loadView() { 61 | super.loadView() 62 | 63 | tableView.register(identifier: cellIdentifier) 64 | } 65 | 66 | open override func bind(items: Observable<[CELL.StateType]>) { 67 | items 68 | .bind(to: tableView.items(with: cellIdentifier)) { [unowned self] _, model, cell in 69 | self.configure(cell: cell, factory: self.cellFactory, model: model, mapAction: { PlainTableViewAction.rowAction(model, $0) }) 70 | } 71 | .disposed(by: lifetimeDisposeBag) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Source/Core/Styling/Extensions/UIColor+Init.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Init.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 16.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | 13 | /** 14 | * Parses passed hex string and returns corresponding UIColor. 15 | * - parameter hex: string consisting of hex color, including hash symbol - accepted formats: "#RRGGBB" and "#RRGGBBAA" 16 | * - WARNING: This is not a failable **init** (i.e. it doesn't return nil if it fails to parse the passed string, instead it crashes the app) and therefore is NOT recommended for parsing colors received from back-end. 17 | */ 18 | public convenience init(hex: String) { 19 | let hexNumber = String(hex.dropFirst()) 20 | let length = hexNumber.count 21 | guard length == 6 || length == 8 else { 22 | preconditionFailure("Hex string \(hex) has to be in format #RRGGBB or #RRGGBBAA !") 23 | } 24 | 25 | if let hexValue = UInt(hexNumber, radix: 16) { 26 | if length == 6 { 27 | self.init(rgb: hexValue) 28 | } else { 29 | self.init(rgba: hexValue) 30 | } 31 | } else { 32 | preconditionFailure("Hex string \(hex) could not be parsed!") 33 | } 34 | } 35 | 36 | /** 37 | * Parses passed (Red, Green, Blue) UInt and returns corresponding UIColor, alpha is 1. 38 | * - parameter rgba: UInt of a color - (e.g. 0x33F100) 39 | * - WARNING: Passing in negative number or number higher than 0xFFFFFF is undefined behavior. 40 | */ 41 | public convenience init(rgb: UInt) { 42 | if rgb > 0xFFFFFF { 43 | print("WARNING: RGB color is greater than the value of white (0xFFFFFF) which is probably developer error.") 44 | } 45 | self.init(rgba: (rgb << 8) + 0xFF) 46 | } 47 | 48 | /** 49 | * Parses passed (Red, Green, Blue, Alpha) UInt and returns corresponding UIColor. 50 | * - parameter rgba: UInt of a color - (e.g. 0x33F100EE) 51 | * - WARNING: Passing in negative number or number higher than 0xFFFFFFFF is undefined behavior. 52 | */ 53 | public convenience init(rgba: UInt) { 54 | let red = CGFloat((rgba & 0xFF000000) >> 24) / 255.0 55 | let green = CGFloat((rgba & 0xFF0000) >> 16) / 255.0 56 | let blue = CGFloat((rgba & 0xFF00) >> 8) / 255.0 57 | let alpha = CGFloat(rgba & 0xFF) / 255.0 58 | 59 | self.init(red: red, green: green, blue: blue, alpha: alpha) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Source/CollectionView/CollectionViewState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionViewState.swift 3 | // Reactant 4 | // 5 | // Created by Filip Dolnik on 15.11.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | /** 10 | * Enum suited for usage with table view as a `Component.componentState`. 11 | * 12 | * `case items([MODEL])` is used for passing the `MODEL` to the table view for setting up the cells 13 | * 14 | * `case empty(message: String)` is used for showing no cells (or dividers) 15 | * and instead putting the `message` value in the center of the screen 16 | * 17 | * `case loading` shows no cells (or dividers) and instead puts an animated loading indicator to the top of the table view 18 | * 19 | * ## Example 20 | * Example's `componentState` is `[Friend]` and its `RootView` is a table view (`PlainTableView` for example). 21 | * ``` 22 | * override func afterInit() { 23 | * rootView.componentState = .loading 24 | * } 25 | * 26 | * override func update() { 27 | * if componentState.isEmpty { 28 | * rootView.componentState = .empty("You don't have any friends.") 29 | * } else { 30 | * rootView.componentState = .items(componentState) 31 | * } 32 | * } 33 | * ``` 34 | * - NOTE: This enum also contains a method for convenient mapping of `.items`' innards to a different type. 35 | * This state is `Equatable` as long as `.items`' `MODEL` is `Equatable`. 36 | */ 37 | public enum CollectionViewState { 38 | 39 | case items([MODEL]) 40 | case empty(message: String) 41 | case loading 42 | 43 | /** 44 | * Used to transform items from MODEL to generic T of the same enum `CollectionViewState` using provided closure. 45 | * 46 | * Doesn't affect `case empty(message: String)` or `case loading` in any way. 47 | */ 48 | public func mapItems(transform: (MODEL) -> T) -> CollectionViewState { 49 | switch self { 50 | case .items(let items): 51 | return .items(items.map(transform)) 52 | case .empty(let message): 53 | return .empty(message: message) 54 | case .loading: 55 | return .loading 56 | } 57 | } 58 | } 59 | 60 | public func == (lhs: CollectionViewState, rhs: CollectionViewState) -> Bool { 61 | switch (lhs, rhs) { 62 | case (.items(let lhsItems), .items(let rhsItems)): 63 | return lhsItems == rhsItems 64 | case (.empty(let lhsMessage), .empty(let rhsMessage)): 65 | return lhsMessage == rhsMessage 66 | case (.loading, .loading): 67 | return true 68 | default: 69 | return false 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reactant 2 | 3 | Reactant is a foundation for rapid and safe iOS development. It allows you to cut down your development costs by improving reusability, testability and safety of your code, especially your UI. Check our our [quick-start guide][quick-start] to learn more. 4 | 5 | [![CI Status](https://img.shields.io/travis/Brightify/Reactant.svg?style=flat)](https://travis-ci.org/Brightify/Reactant) 6 | [![Version](https://img.shields.io/cocoapods/v/Reactant.svg?style=flat)][reactant-cocoapods] 7 | [![License](https://img.shields.io/cocoapods/l/Reactant.svg?style=flat)][reactant-cocoapods] 8 | [![Platform](https://img.shields.io/cocoapods/p/Reactant.svg?style=flat)][reactant-cocoapods] 9 | [![Apps](https://img.shields.io/cocoapods/at/Reactant.svg?style=flat)][reactant-cocoapods] 10 | [![Slack Status](https://swiftkit.brightify.org/badge.svg)][slack] 11 | 12 | ## Requirements 13 | 14 | * iOS 9.0+ 15 | * Xcode 8.0+ 16 | * Swift 3.0+ 17 | 18 | **NOTE**: We have experimental support for *macOS* platform in `macOS` branch, but it's incomplete and definitely not recommended for production. Our plan is to add full `macOS` support in near future. 19 | 20 | ## Communication 21 | Feel free to reach us on Slack! [https://swiftkit.brightify.org/][slack] 22 | 23 | ## Get Started 24 | Head to our [quick-start guide][quick-start] to learn how Reactant works and what it can do to decrease your development costs! 25 | 26 | ## Versioning 27 | This library uses semantic versioning. We won't introduce any breaking changes without releasing a new major version. Our main goal is to keep the API as stable as possible. We build our applications on top of Reactant as well and we absolutely hate any breaking changes. 28 | 29 | ## Authors 30 | * Tadeas Kriz, [tadeas@brightify.org](mailto:tadeas@brightify.org) 31 | * Matous Hybl, [matous@brightify.org](mailto:matous@brightify.org) 32 | * Filip Dolník, [filip@brightify.org](mailto:filip@brightify.org) 33 | 34 | ## Used libraries 35 | 36 | ### Runtime 37 | 38 | * [Result](https://github.com/antitypical/Result) 39 | * [SnapKit](https://github.com/SnapKit/SnapKit) 40 | * [RxSwift](https://github.com/ReactiveX/RxSwift) 41 | * [RxCocoa](https://github.com/ReactiveX/RxSwift) 42 | * [RxOptional](https://github.com/RxSwiftCommunity/RxOptional) 43 | * [RxDataSources](https://github.com/RxSwiftCommunity/RxDataSources) 44 | 45 | ### Tests 46 | 47 | * [Quick](https://github.com/Quick/Quick) 48 | * [Nimble](https://github.com/Quick/Nimble) 49 | 50 | [quick-start]: https://docs.reactant.tech/getting-started/quickstart.html 51 | [reactant-cocoapods]: https://cocoapods.org/pods/Reactant 52 | [slack]: https://swiftkit.brightify.org/ 53 | -------------------------------------------------------------------------------- /Tests/Core/Styling/StyleableTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StyleableTest.swift 3 | // ReactantTests 4 | // 5 | // Created by Filip Dolnik on 18.10.16. 6 | // Copyright © 2016 Brightify. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import Reactant 12 | 13 | class StyleableTest: QuickSpec { 14 | 15 | override func spec() { 16 | var view: UIView! 17 | beforeEach { 18 | view = UIView() 19 | } 20 | describe("applyStyle") { 21 | it("applies style") { 22 | view.apply(style: Styles.background) 23 | view.apply(style: Styles.tint) 24 | 25 | self.assert(view: view) 26 | } 27 | } 28 | describe("applyStyles") { 29 | it("applies styles") { 30 | view.apply(styles: [Styles.background, Styles.tint]) 31 | 32 | self.assert(view: view) 33 | } 34 | it("applies styles with vararg") { 35 | view.apply(styles: Styles.background, Styles.tint) 36 | 37 | self.assert(view: view) 38 | } 39 | } 40 | describe("styled") { 41 | it("applies styles") { 42 | let styledView = view.styled(using: [Styles.background, Styles.tint]) 43 | 44 | self.assert(view: styledView) 45 | } 46 | it("applies styles with vararg") { 47 | let styledView = view.styled(using: Styles.background, Styles.tint) 48 | 49 | self.assert(view: styledView) 50 | } 51 | } 52 | describe("with") { 53 | it("applies style") { 54 | let styledView = view.with { 55 | $0.backgroundColor = UIColor.blue 56 | $0.tintColor = UIColor.black 57 | } 58 | 59 | self.assert(view: styledView) 60 | } 61 | } 62 | } 63 | 64 | private func assert(view: UIView, file: StaticString = #file, line: UInt = #line) { 65 | XCTAssertEqual(UIColor.blue, view.backgroundColor, file: file, line: line) 66 | XCTAssertEqual(UIColor.black, view.tintColor, file: file, line: line) 67 | } 68 | } 69 | 70 | extension StyleableTest { 71 | 72 | fileprivate struct Styles { 73 | 74 | static func background(_ view: UIView) { 75 | view.backgroundColor = UIColor.blue 76 | } 77 | 78 | static func tint(_ view: UIView) { 79 | view.tintColor = UIColor.black 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Example/Application/Sources/Components/Main/MainRootView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainRootView.swift 3 | // Reactant 4 | // 5 | // Created by Matouš Hýbl on 3/16/17. 6 | // Copyright © 2017 Brightify s.r.o. All rights reserved. 7 | // 8 | 9 | import Reactant 10 | import RxSwift 11 | 12 | enum MainAction { 13 | case updateLabel 14 | case openTable 15 | } 16 | 17 | final class MainRootView: ViewBase, RootView { 18 | 19 | // this property gather's producers of all actions that should be propagated to parent component 20 | override var actions: [Observable] { 21 | return [ 22 | updateButton.rx.tap.rewrite(with: .updateLabel), 23 | tableButton.rx.tap.rewrite(with: .openTable) 24 | ] 25 | } 26 | 27 | private let label = LabelView() 28 | private let updateButton = UIButton() 29 | private let tableButton = UIButton() 30 | 31 | private let dateFormatter = DateFormatter() 32 | 33 | override init() { 34 | super.init() 35 | 36 | dateFormatter.timeStyle = .long 37 | } 38 | 39 | override func update() { 40 | label.componentState = dateFormatter.string(from: componentState) 41 | } 42 | 43 | override func loadView() { 44 | children( 45 | label, 46 | updateButton, 47 | tableButton 48 | ) 49 | 50 | updateButton.setTitleColor(.white, for: .normal) 51 | updateButton.setTitle("Update", for: .normal) 52 | updateButton.backgroundColor = .blue 53 | updateButton.contentEdgeInsets = UIEdgeInsetsMake(8, 8, 8, 8) 54 | 55 | tableButton.setTitleColor(.white, for: .normal) 56 | tableButton.setTitle("Open table", for: .normal) 57 | tableButton.backgroundColor = .green 58 | tableButton.contentEdgeInsets = UIEdgeInsetsMake(8, 8, 8, 8) 59 | } 60 | 61 | override func setupConstraints() { 62 | label.snp.makeConstraints { make in 63 | make.center.equalToSuperview() 64 | } 65 | 66 | updateButton.snp.makeConstraints { make in 67 | make.centerX.equalToSuperview() 68 | make.leading.greaterThanOrEqualToSuperview().offset(20) 69 | make.trailing.lessThanOrEqualToSuperview().inset(20) 70 | 71 | make.top.equalTo(label.snp.bottom).offset(20) 72 | } 73 | 74 | tableButton.snp.makeConstraints { make in 75 | make.centerX.equalToSuperview() 76 | make.leading.greaterThanOrEqualToSuperview().offset(20) 77 | make.trailing.lessThanOrEqualToSuperview().inset(20) 78 | 79 | make.top.equalTo(updateButton.snp.bottom).offset(20) 80 | } 81 | } 82 | } 83 | --------------------------------------------------------------------------------