diff --git a/1.png b/1.png new file mode 100644 index 0000000..180f48b Binary files /dev/null and b/1.png differ diff --git a/2.png b/2.png new file mode 100644 index 0000000..217a8bb Binary files /dev/null and b/2.png differ diff --git a/3.png b/3.png new file mode 100644 index 0000000..7496179 Binary files /dev/null and b/3.png differ diff --git a/4.png b/4.png new file mode 100644 index 0000000..16640da Binary files /dev/null and b/4.png differ diff --git a/README.md b/README.md index 42bbeea..86a3518 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ This repository hosts each sample app in separate directory. | Example | Description | | ------------- | ------------- | | [tmdb-mvvm-rxswift-pure](tmdb-mvvm-rxswift-pure) | Uses [RxSwift](https://github.com/ReactiveX/RxSwift) and observables as binding mechanism between `ViewController` and `ViewModel`. Also, it uses simple navigator pattern for transitions between screens. (README in progress) | +| [tmdb-rx-driver](tmdb-rx-driver) | Uses [RxSwift](https://github.com/ReactiveX/RxSwift) and observables as binding mechanism between `ViewController` and `Driver`. Also, it uses binding techniques to separate logic. [Wiki.](https://github.com/dmsl1805/Cookbook/wiki) | ### Single screen app examples diff --git a/tmdb-rx-driver/1.png b/tmdb-rx-driver/1.png new file mode 100644 index 0000000..180f48b Binary files /dev/null and b/tmdb-rx-driver/1.png differ diff --git a/tmdb-rx-driver/2.png b/tmdb-rx-driver/2.png new file mode 100644 index 0000000..217a8bb Binary files /dev/null and b/tmdb-rx-driver/2.png differ diff --git a/tmdb-rx-driver/3.png b/tmdb-rx-driver/3.png new file mode 100644 index 0000000..7496179 Binary files /dev/null and b/tmdb-rx-driver/3.png differ diff --git a/tmdb-rx-driver/4.png b/tmdb-rx-driver/4.png new file mode 100644 index 0000000..16640da Binary files /dev/null and b/tmdb-rx-driver/4.png differ diff --git a/tmdb-rx-driver/Podfile b/tmdb-rx-driver/Podfile new file mode 100644 index 0000000..8594351 --- /dev/null +++ b/tmdb-rx-driver/Podfile @@ -0,0 +1,16 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +target 'tmdb-rx-driver' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + pod 'RxSwift', '~> 5' + pod 'RxCocoa', '~> 5' + pod 'RxSwiftExt', '~> 5' + pod 'RxViewController' + pod 'RxDataSources' + pod 'R.swift' + pod 'Nuke', '~> 7.0' + +end diff --git a/tmdb-rx-driver/Podfile.lock b/tmdb-rx-driver/Podfile.lock new file mode 100644 index 0000000..6fa7f2f --- /dev/null +++ b/tmdb-rx-driver/Podfile.lock @@ -0,0 +1,65 @@ +PODS: + - Differentiator (4.0.1) + - Nuke (7.6.3) + - R.swift (5.2.2): + - R.swift.Library (~> 5.2.0) + - R.swift.Library (5.2.0) + - RxCocoa (5.1.1): + - RxRelay (~> 5) + - RxSwift (~> 5) + - RxDataSources (4.0.1): + - Differentiator (~> 4.0) + - RxCocoa (~> 5.0) + - RxSwift (~> 5.0) + - RxRelay (5.1.1): + - RxSwift (~> 5) + - RxSwift (5.1.1) + - RxSwiftExt (5.2.0): + - RxSwiftExt/Core (= 5.2.0) + - RxSwiftExt/RxCocoa (= 5.2.0) + - RxSwiftExt/Core (5.2.0): + - RxSwift (~> 5.0) + - RxSwiftExt/RxCocoa (5.2.0): + - RxCocoa (~> 5.0) + - RxSwiftExt/Core + - RxViewController (1.0.0): + - RxCocoa (~> 5.0) + - RxSwift (~> 5.0) + +DEPENDENCIES: + - Nuke (~> 7.0) + - R.swift + - RxCocoa (~> 5) + - RxDataSources + - RxSwift (~> 5) + - RxSwiftExt (~> 5) + - RxViewController + +SPEC REPOS: + trunk: + - Differentiator + - Nuke + - R.swift + - R.swift.Library + - RxCocoa + - RxDataSources + - RxRelay + - RxSwift + - RxSwiftExt + - RxViewController + +SPEC CHECKSUMS: + Differentiator: 886080237d9f87f322641dedbc5be257061b0602 + Nuke: 44130e95e09463f8773ae4b96b90de1eba6b3350 + R.swift: 7c52cdc57a66840ffe6cbd8a823d732059d42a32 + R.swift.Library: 5ba4f1631300caf9a4d890186930da85d540769d + RxCocoa: 32065309a38d29b5b0db858819b5bf9ef038b601 + RxDataSources: efee07fa4de48477eca0a4611e6d11e2da9c1114 + RxRelay: d77f7d771495f43c556cbc43eebd1bb54d01e8e9 + RxSwift: 81470a2074fa8780320ea5fe4102807cb7118178 + RxSwiftExt: 4ca80336f43c28f11a2825cdd2fc61dd6c044697 + RxViewController: 7330a46e5c31cd680db169da4c9fc8676e975a81 + +PODFILE CHECKSUM: 1b323ff7b941979a9274ad85169b0f5f4130f733 + +COCOAPODS: 1.9.3 diff --git a/tmdb-rx-driver/README.md b/tmdb-rx-driver/README.md new file mode 100644 index 0000000..5112eff --- /dev/null +++ b/tmdb-rx-driver/README.md @@ -0,0 +1,25 @@ +# tmdb-mvvm-rxswift-driver +🔒 ** If you want to login, use username `iostest` and password `test`.** + + +This example uses RxSwift Drivers as binding mechanism between `Driver` and `ViewController`. +### [Read Wiki](https://github.com/dmsl1805/Cookbook/wiki) + +| ![](1.png) | ![](2.png) | +| --- | --- | +| ![](3.png) | ![](4.png) | + +## Installation +Clone the repository: + +`git clone git@github.com:dmsl1805/ios-architecture.git` + +branch `dsml` + +Navigate to `tmdb-rx-driver` directory: + +`cd tmdb-rx-driver` + +Install dependencies: + + `pod install` diff --git a/tmdb-rx-driver/tmdb-rx-driver.xcodeproj/project.pbxproj b/tmdb-rx-driver/tmdb-rx-driver.xcodeproj/project.pbxproj new file mode 100644 index 0000000..0613c39 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver.xcodeproj/project.pbxproj @@ -0,0 +1,829 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXBuildFile section */ + 1A0DE661251C818B00BF3D31 /* LoginActionBinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0DE660251C818B00BF3D31 /* LoginActionBinder.swift */; }; + 1A0DE663251C81A300BF3D31 /* LoginStateBinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0DE662251C81A300BF3D31 /* LoginStateBinder.swift */; }; + 1A1DA8B72529C0E2008A08BE /* DiscoverViewControllerBinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A1DA8B62529C0E2008A08BE /* DiscoverViewControllerBinder.swift */; }; + 1A8CB9B42519DC6800F25598 /* MovieDetailStateBinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A8CB9B32519DC6800F25598 /* MovieDetailStateBinder.swift */; }; + 1A8CB9B62519DC7600F25598 /* MovieDetailActionBinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A8CB9B52519DC7600F25598 /* MovieDetailActionBinder.swift */; }; + 1AA01CB42520C8A7000C2B5B /* CarouselViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AA01CB32520C8A7000C2B5B /* CarouselViewModel.swift */; }; + 1AE0178025175EF50019F8FA /* Movie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE0177C25175EF50019F8FA /* Movie.swift */; }; + 1AE0178125175EF50019F8FA /* Genre.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE0177D25175EF50019F8FA /* Genre.swift */; }; + 1AE0178225175EF50019F8FA /* Person.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE0177E25175EF50019F8FA /* Person.swift */; }; + 1AE0178325175EF50019F8FA /* Show.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE0177F25175EF50019F8FA /* Show.swift */; }; + 1AE0178825175F280019F8FA /* TMDBApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE0178525175F280019F8FA /* TMDBApi.swift */; }; + 1AE0178925175F280019F8FA /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE0178625175F280019F8FA /* HTTPClient.swift */; }; + 1AE0178A25175F280019F8FA /* TMDBApiResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE0178725175F280019F8FA /* TMDBApiResponses.swift */; }; + 1AE0179025175F490019F8FA /* UIStoryboard+ViewControllers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE0178C25175F490019F8FA /* UIStoryboard+ViewControllers.swift */; }; + 1AE0179125175F490019F8FA /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE0178D25175F490019F8FA /* ActivityIndicator.swift */; }; + 1AE0179225175F490019F8FA /* ViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE0178E25175F490019F8FA /* ViewModelType.swift */; }; + 1AE017972517607F0019F8FA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1AE017952517607F0019F8FA /* Main.storyboard */; }; + 1AE017BA251760900019F8FA /* DiscoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE0179B251760900019F8FA /* DiscoverViewController.swift */; }; + 1AE017BB251760900019F8FA /* DiscoverViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE0179C251760900019F8FA /* DiscoverViewModel.swift */; }; + 1AE017BC251760900019F8FA /* CarouselSectionCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1AE0179E251760900019F8FA /* CarouselSectionCell.xib */; }; + 1AE017BD251760900019F8FA /* DiscoverMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE0179F251760900019F8FA /* DiscoverMainView.swift */; }; + 1AE017BE251760900019F8FA /* MovieCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE017A0251760900019F8FA /* MovieCell.swift */; }; + 1AE017BF251760900019F8FA /* MovieCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1AE017A1251760900019F8FA /* MovieCell.xib */; }; + 1AE017C0251760900019F8FA /* DiscoverMainView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1AE017A2251760900019F8FA /* DiscoverMainView.xib */; }; + 1AE017C1251760900019F8FA /* CarouselSectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE017A3251760900019F8FA /* CarouselSectionCell.swift */; }; + 1AE017C2251760900019F8FA /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE017A5251760900019F8FA /* SearchViewController.swift */; }; + 1AE017C3251760900019F8FA /* SearchDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE017A6251760900019F8FA /* SearchDriver.swift */; }; + 1AE017C4251760900019F8FA /* SearchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE017A8251760900019F8FA /* SearchCell.swift */; }; + 1AE017C5251760900019F8FA /* SearchCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1AE017A9251760900019F8FA /* SearchCell.xib */; }; + 1AE017C7251760900019F8FA /* MovieDetailDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE017AC251760900019F8FA /* MovieDetailDriver.swift */; }; + 1AE017C9251760900019F8FA /* MovieDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE017AE251760900019F8FA /* MovieDetailViewController.swift */; }; + 1AE017CA251760900019F8FA /* GradientImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE017B0251760900019F8FA /* GradientImageView.swift */; }; + 1AE017CB251760900019F8FA /* MovieDetailHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1AE017B1251760900019F8FA /* MovieDetailHeaderView.xib */; }; + 1AE017CC251760900019F8FA /* MovieDetailTipsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1AE017B2251760900019F8FA /* MovieDetailTipsView.xib */; }; + 1AE017CD251760900019F8FA /* MovieDetailTipsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE017B3251760900019F8FA /* MovieDetailTipsView.swift */; }; + 1AE017CE251760900019F8FA /* MovieDetailHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE017B4251760900019F8FA /* MovieDetailHeaderView.swift */; }; + 1AE017CF251760900019F8FA /* LoginDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE017B6251760900019F8FA /* LoginDriver.swift */; }; + 1AE017D1251760900019F8FA /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE017B8251760900019F8FA /* LoginViewController.swift */; }; + 1AE017D3251761180019F8FA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1AE017D2251761180019F8FA /* Assets.xcassets */; }; + 1AE017D52517612C0019F8FA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1AE017D42517612C0019F8FA /* LaunchScreen.storyboard */; }; + 1AE017D9251761610019F8FA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE017D7251761610019F8FA /* AppDelegate.swift */; }; + 1AE017DA251761610019F8FA /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE017D8251761610019F8FA /* App.swift */; }; + 1AE10937251765B6001E5B01 /* SearchResultItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE10936251765B6001E5B01 /* SearchResultItemViewModel.swift */; }; + 1AE109432517671A001E5B01 /* Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE109392517671A001E5B01 /* Next.swift */; }; + 1AE109442517671A001E5B01 /* Publicsher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE1093A2517671A001E5B01 /* Publicsher.swift */; }; + 1AE109452517671A001E5B01 /* Callable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE1093B2517671A001E5B01 /* Callable.swift */; }; + 1AE109462517671A001E5B01 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE1093C2517671A001E5B01 /* Util.swift */; }; + 1AE109472517671A001E5B01 /* Driver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE1093D2517671A001E5B01 /* Driver.swift */; }; + 1AE109482517671A001E5B01 /* Latest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE1093E2517671A001E5B01 /* Latest.swift */; }; + 1AE109492517671A001E5B01 /* NSObject+Disposable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE1093F2517671A001E5B01 /* NSObject+Disposable.swift */; }; + 1AE1094A2517671A001E5B01 /* Loading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE109402517671A001E5B01 /* Loading.swift */; }; + 1AE1094B2517671A001E5B01 /* Bool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE109412517671A001E5B01 /* Bool.swift */; }; + 1AE1094C2517671A001E5B01 /* Just.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE109422517671A001E5B01 /* Just.swift */; }; + 1AE1095225176720001E5B01 /* Unown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE1094E25176720001E5B01 /* Unown.swift */; }; + 1AE1095325176720001E5B01 /* Closure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE1094F25176720001E5B01 /* Closure.swift */; }; + 1AE1095425176720001E5B01 /* RawClosure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE1095025176720001E5B01 /* RawClosure.swift */; }; + 1AE1095525176720001E5B01 /* Set.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE1095125176720001E5B01 /* Set.swift */; }; + 1AE1095B25176731001E5B01 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE1095725176731001E5B01 /* Logging.swift */; }; + 1AE1095C25176731001E5B01 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE1095825176731001E5B01 /* Log.swift */; }; + 1AE1095D25176731001E5B01 /* Params.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE1095925176731001E5B01 /* Params.swift */; }; + 1AE1095E25176731001E5B01 /* Level.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE1095A25176731001E5B01 /* Level.swift */; }; + 1AE1096125176854001E5B01 /* DefaultInitializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE1096025176854001E5B01 /* DefaultInitializable.swift */; }; + 1AE1096725177FA4001E5B01 /* ViewControllerBinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE1096425177FA3001E5B01 /* ViewControllerBinder.swift */; }; + 1AE1096825177FA4001E5B01 /* NavigationBinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE1096525177FA3001E5B01 /* NavigationBinder.swift */; }; + 1AE1096C2517800D001E5B01 /* Transitioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE1096B2517800D001E5B01 /* Transitioning.swift */; }; + 1AE1096E2517809F001E5B01 /* DisposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE1096D2517809F001E5B01 /* DisposeViewController.swift */; }; + 1AE109A52517A3AC001E5B01 /* R.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE109A42517A3AC001E5B01 /* R.generated.swift */; }; + 1AE109A72517A461001E5B01 /* SearchStateBinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE109A62517A461001E5B01 /* SearchStateBinder.swift */; }; + 1AE109A92517C026001E5B01 /* SearchActionBinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE109A82517C026001E5B01 /* SearchActionBinder.swift */; }; + CE8D6EC627327C1DFC949704 /* Pods_tmdb_rx_driver.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 973976C40C7DC73287C8EF44 /* Pods_tmdb_rx_driver.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 1A0DE660251C818B00BF3D31 /* LoginActionBinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginActionBinder.swift; sourceTree = ""; }; + 1A0DE662251C81A300BF3D31 /* LoginStateBinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginStateBinder.swift; sourceTree = ""; }; + 1A1DA8B62529C0E2008A08BE /* DiscoverViewControllerBinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoverViewControllerBinder.swift; sourceTree = ""; }; + 1A8CB9B32519DC6800F25598 /* MovieDetailStateBinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailStateBinder.swift; sourceTree = ""; }; + 1A8CB9B52519DC7600F25598 /* MovieDetailActionBinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailActionBinder.swift; sourceTree = ""; }; + 1AA01CB32520C8A7000C2B5B /* CarouselViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselViewModel.swift; sourceTree = ""; }; + 1AE0176425175DD40019F8FA /* tmdb-rx-driver.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "tmdb-rx-driver.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 1AE0177525175DD50019F8FA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1AE0177C25175EF50019F8FA /* Movie.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Movie.swift; sourceTree = ""; }; + 1AE0177D25175EF50019F8FA /* Genre.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Genre.swift; sourceTree = ""; }; + 1AE0177E25175EF50019F8FA /* Person.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Person.swift; sourceTree = ""; }; + 1AE0177F25175EF50019F8FA /* Show.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Show.swift; sourceTree = ""; }; + 1AE0178525175F280019F8FA /* TMDBApi.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TMDBApi.swift; sourceTree = ""; }; + 1AE0178625175F280019F8FA /* HTTPClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; + 1AE0178725175F280019F8FA /* TMDBApiResponses.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TMDBApiResponses.swift; sourceTree = ""; }; + 1AE0178C25175F490019F8FA /* UIStoryboard+ViewControllers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIStoryboard+ViewControllers.swift"; sourceTree = ""; }; + 1AE0178D25175F490019F8FA /* ActivityIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; + 1AE0178E25175F490019F8FA /* ViewModelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewModelType.swift; sourceTree = ""; }; + 1AE017962517607F0019F8FA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 1AE0179B251760900019F8FA /* DiscoverViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoverViewController.swift; sourceTree = ""; }; + 1AE0179C251760900019F8FA /* DiscoverViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoverViewModel.swift; sourceTree = ""; }; + 1AE0179E251760900019F8FA /* CarouselSectionCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CarouselSectionCell.xib; sourceTree = ""; }; + 1AE0179F251760900019F8FA /* DiscoverMainView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoverMainView.swift; sourceTree = ""; }; + 1AE017A0251760900019F8FA /* MovieCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieCell.swift; sourceTree = ""; }; + 1AE017A1251760900019F8FA /* MovieCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MovieCell.xib; sourceTree = ""; }; + 1AE017A2251760900019F8FA /* DiscoverMainView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = DiscoverMainView.xib; sourceTree = ""; }; + 1AE017A3251760900019F8FA /* CarouselSectionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarouselSectionCell.swift; sourceTree = ""; }; + 1AE017A5251760900019F8FA /* SearchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; + 1AE017A6251760900019F8FA /* SearchDriver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchDriver.swift; sourceTree = ""; }; + 1AE017A8251760900019F8FA /* SearchCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchCell.swift; sourceTree = ""; }; + 1AE017A9251760900019F8FA /* SearchCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SearchCell.xib; sourceTree = ""; }; + 1AE017AC251760900019F8FA /* MovieDetailDriver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieDetailDriver.swift; sourceTree = ""; }; + 1AE017AE251760900019F8FA /* MovieDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieDetailViewController.swift; sourceTree = ""; }; + 1AE017B0251760900019F8FA /* GradientImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GradientImageView.swift; sourceTree = ""; }; + 1AE017B1251760900019F8FA /* MovieDetailHeaderView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MovieDetailHeaderView.xib; sourceTree = ""; }; + 1AE017B2251760900019F8FA /* MovieDetailTipsView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MovieDetailTipsView.xib; sourceTree = ""; }; + 1AE017B3251760900019F8FA /* MovieDetailTipsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieDetailTipsView.swift; sourceTree = ""; }; + 1AE017B4251760900019F8FA /* MovieDetailHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieDetailHeaderView.swift; sourceTree = ""; }; + 1AE017B6251760900019F8FA /* LoginDriver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginDriver.swift; sourceTree = ""; }; + 1AE017B8251760900019F8FA /* LoginViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; + 1AE017D2251761180019F8FA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 1AE017D42517612C0019F8FA /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + 1AE017D7251761610019F8FA /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 1AE017D8251761610019F8FA /* App.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; + 1AE10936251765B6001E5B01 /* SearchResultItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItemViewModel.swift; sourceTree = ""; }; + 1AE109392517671A001E5B01 /* Next.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Next.swift; sourceTree = ""; }; + 1AE1093A2517671A001E5B01 /* Publicsher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Publicsher.swift; sourceTree = ""; }; + 1AE1093B2517671A001E5B01 /* Callable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Callable.swift; sourceTree = ""; }; + 1AE1093C2517671A001E5B01 /* Util.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Util.swift; sourceTree = ""; }; + 1AE1093D2517671A001E5B01 /* Driver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Driver.swift; sourceTree = ""; }; + 1AE1093E2517671A001E5B01 /* Latest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Latest.swift; sourceTree = ""; }; + 1AE1093F2517671A001E5B01 /* NSObject+Disposable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSObject+Disposable.swift"; sourceTree = ""; }; + 1AE109402517671A001E5B01 /* Loading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Loading.swift; sourceTree = ""; }; + 1AE109412517671A001E5B01 /* Bool.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bool.swift; sourceTree = ""; }; + 1AE109422517671A001E5B01 /* Just.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Just.swift; sourceTree = ""; }; + 1AE1094E25176720001E5B01 /* Unown.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Unown.swift; sourceTree = ""; }; + 1AE1094F25176720001E5B01 /* Closure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Closure.swift; sourceTree = ""; }; + 1AE1095025176720001E5B01 /* RawClosure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RawClosure.swift; sourceTree = ""; }; + 1AE1095125176720001E5B01 /* Set.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Set.swift; sourceTree = ""; }; + 1AE1095725176731001E5B01 /* Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; + 1AE1095825176731001E5B01 /* Log.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; + 1AE1095925176731001E5B01 /* Params.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Params.swift; sourceTree = ""; }; + 1AE1095A25176731001E5B01 /* Level.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Level.swift; sourceTree = ""; }; + 1AE1096025176854001E5B01 /* DefaultInitializable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultInitializable.swift; sourceTree = ""; }; + 1AE1096425177FA3001E5B01 /* ViewControllerBinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewControllerBinder.swift; sourceTree = ""; }; + 1AE1096525177FA3001E5B01 /* NavigationBinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationBinder.swift; sourceTree = ""; }; + 1AE1096B2517800D001E5B01 /* Transitioning.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Transitioning.swift; sourceTree = ""; }; + 1AE1096D2517809F001E5B01 /* DisposeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisposeViewController.swift; sourceTree = ""; }; + 1AE109A42517A3AC001E5B01 /* R.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = R.generated.swift; sourceTree = ""; }; + 1AE109A62517A461001E5B01 /* SearchStateBinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchStateBinder.swift; sourceTree = ""; }; + 1AE109A82517C026001E5B01 /* SearchActionBinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchActionBinder.swift; sourceTree = ""; }; + 41160404C5F5FF90CD712B13 /* Pods-tmdb-rx-driver.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-tmdb-rx-driver.release.xcconfig"; path = "Target Support Files/Pods-tmdb-rx-driver/Pods-tmdb-rx-driver.release.xcconfig"; sourceTree = ""; }; + 973976C40C7DC73287C8EF44 /* Pods_tmdb_rx_driver.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_tmdb_rx_driver.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AF7C2B4A13E312E1FD20CF55 /* Pods-tmdb-rx-driver.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-tmdb-rx-driver.debug.xcconfig"; path = "Target Support Files/Pods-tmdb-rx-driver/Pods-tmdb-rx-driver.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 1AE0176125175DD40019F8FA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CE8D6EC627327C1DFC949704 /* Pods_tmdb_rx_driver.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1AE0175B25175DD40019F8FA = { + isa = PBXGroup; + children = ( + 1AE0176625175DD40019F8FA /* tmdb-rx-driver */, + 1AE0176525175DD40019F8FA /* Products */, + B13475FE0AB43ADE61034608 /* Pods */, + E22C1B6881174EBB0CB30744 /* Frameworks */, + ); + sourceTree = ""; + }; + 1AE0176525175DD40019F8FA /* Products */ = { + isa = PBXGroup; + children = ( + 1AE0176425175DD40019F8FA /* tmdb-rx-driver.app */, + ); + name = Products; + sourceTree = ""; + }; + 1AE0176625175DD40019F8FA /* tmdb-rx-driver */ = { + isa = PBXGroup; + children = ( + 1AE017D6251761610019F8FA /* Application */, + 1AE01798251760900019F8FA /* Scenes */, + 1AE017942517607F0019F8FA /* Storyboard */, + 1AE0178425175F280019F8FA /* Networking */, + 1AE0177B25175EF50019F8FA /* Models */, + 1AE109A32517A3AC001E5B01 /* Resource */, + 1AE0178B25175F490019F8FA /* Utils */, + 1AE017D2251761180019F8FA /* Assets.xcassets */, + 1AE0177525175DD50019F8FA /* Info.plist */, + ); + path = "tmdb-rx-driver"; + sourceTree = ""; + }; + 1AE0177B25175EF50019F8FA /* Models */ = { + isa = PBXGroup; + children = ( + 1AE0177C25175EF50019F8FA /* Movie.swift */, + 1AE0177D25175EF50019F8FA /* Genre.swift */, + 1AE0177E25175EF50019F8FA /* Person.swift */, + 1AE0177F25175EF50019F8FA /* Show.swift */, + ); + path = Models; + sourceTree = ""; + }; + 1AE0178425175F280019F8FA /* Networking */ = { + isa = PBXGroup; + children = ( + 1AE0178525175F280019F8FA /* TMDBApi.swift */, + 1AE0178625175F280019F8FA /* HTTPClient.swift */, + 1AE0178725175F280019F8FA /* TMDBApiResponses.swift */, + ); + path = Networking; + sourceTree = ""; + }; + 1AE0178B25175F490019F8FA /* Utils */ = { + isa = PBXGroup; + children = ( + 1AE1096225177FA3001E5B01 /* Binder */, + 1AE1095F25176854001E5B01 /* DefaultInitializable */, + 1AE1095625176731001E5B01 /* Log */, + 1AE1094D25176720001E5B01 /* Closure */, + 1AE109382517671A001E5B01 /* RxUtils */, + 1AE0178C25175F490019F8FA /* UIStoryboard+ViewControllers.swift */, + 1AE0178D25175F490019F8FA /* ActivityIndicator.swift */, + 1AE0178E25175F490019F8FA /* ViewModelType.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 1AE017942517607F0019F8FA /* Storyboard */ = { + isa = PBXGroup; + children = ( + 1AE017952517607F0019F8FA /* Main.storyboard */, + ); + path = Storyboard; + sourceTree = ""; + }; + 1AE01798251760900019F8FA /* Scenes */ = { + isa = PBXGroup; + children = ( + 1AE01799251760900019F8FA /* Discover */, + 1AE017A4251760900019F8FA /* Search */, + 1AE017AB251760900019F8FA /* MovieDetail */, + 1AE017B5251760900019F8FA /* Login */, + ); + path = Scenes; + sourceTree = ""; + }; + 1AE01799251760900019F8FA /* Discover */ = { + isa = PBXGroup; + children = ( + 1AE0179B251760900019F8FA /* DiscoverViewController.swift */, + 1A1DA8B62529C0E2008A08BE /* DiscoverViewControllerBinder.swift */, + 1AE0179C251760900019F8FA /* DiscoverViewModel.swift */, + 1AA01CB32520C8A7000C2B5B /* CarouselViewModel.swift */, + 1AE0179D251760900019F8FA /* Views */, + ); + path = Discover; + sourceTree = ""; + }; + 1AE0179D251760900019F8FA /* Views */ = { + isa = PBXGroup; + children = ( + 1AE0179E251760900019F8FA /* CarouselSectionCell.xib */, + 1AE0179F251760900019F8FA /* DiscoverMainView.swift */, + 1AE017A0251760900019F8FA /* MovieCell.swift */, + 1AE017A1251760900019F8FA /* MovieCell.xib */, + 1AE017A2251760900019F8FA /* DiscoverMainView.xib */, + 1AE017A3251760900019F8FA /* CarouselSectionCell.swift */, + ); + path = Views; + sourceTree = ""; + }; + 1AE017A4251760900019F8FA /* Search */ = { + isa = PBXGroup; + children = ( + 1AE017A5251760900019F8FA /* SearchViewController.swift */, + 1AE109A62517A461001E5B01 /* SearchStateBinder.swift */, + 1AE109A82517C026001E5B01 /* SearchActionBinder.swift */, + 1AE017A6251760900019F8FA /* SearchDriver.swift */, + 1AE10936251765B6001E5B01 /* SearchResultItemViewModel.swift */, + 1AE017A7251760900019F8FA /* Cell */, + ); + path = Search; + sourceTree = ""; + }; + 1AE017A7251760900019F8FA /* Cell */ = { + isa = PBXGroup; + children = ( + 1AE017A8251760900019F8FA /* SearchCell.swift */, + 1AE017A9251760900019F8FA /* SearchCell.xib */, + ); + path = Cell; + sourceTree = ""; + }; + 1AE017AB251760900019F8FA /* MovieDetail */ = { + isa = PBXGroup; + children = ( + 1AE017AE251760900019F8FA /* MovieDetailViewController.swift */, + 1A8CB9B32519DC6800F25598 /* MovieDetailStateBinder.swift */, + 1A8CB9B52519DC7600F25598 /* MovieDetailActionBinder.swift */, + 1AE017AC251760900019F8FA /* MovieDetailDriver.swift */, + 1AE017AF251760900019F8FA /* Views */, + ); + path = MovieDetail; + sourceTree = ""; + }; + 1AE017AF251760900019F8FA /* Views */ = { + isa = PBXGroup; + children = ( + 1AE017B0251760900019F8FA /* GradientImageView.swift */, + 1AE017B1251760900019F8FA /* MovieDetailHeaderView.xib */, + 1AE017B2251760900019F8FA /* MovieDetailTipsView.xib */, + 1AE017B3251760900019F8FA /* MovieDetailTipsView.swift */, + 1AE017B4251760900019F8FA /* MovieDetailHeaderView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 1AE017B5251760900019F8FA /* Login */ = { + isa = PBXGroup; + children = ( + 1AE017B8251760900019F8FA /* LoginViewController.swift */, + 1A0DE662251C81A300BF3D31 /* LoginStateBinder.swift */, + 1A0DE660251C818B00BF3D31 /* LoginActionBinder.swift */, + 1AE017B6251760900019F8FA /* LoginDriver.swift */, + ); + path = Login; + sourceTree = ""; + }; + 1AE017D6251761610019F8FA /* Application */ = { + isa = PBXGroup; + children = ( + 1AE017D42517612C0019F8FA /* LaunchScreen.storyboard */, + 1AE017D7251761610019F8FA /* AppDelegate.swift */, + 1AE017D8251761610019F8FA /* App.swift */, + ); + path = Application; + sourceTree = ""; + }; + 1AE109382517671A001E5B01 /* RxUtils */ = { + isa = PBXGroup; + children = ( + 1AE109392517671A001E5B01 /* Next.swift */, + 1AE1093A2517671A001E5B01 /* Publicsher.swift */, + 1AE1093B2517671A001E5B01 /* Callable.swift */, + 1AE1093C2517671A001E5B01 /* Util.swift */, + 1AE1093D2517671A001E5B01 /* Driver.swift */, + 1AE1093E2517671A001E5B01 /* Latest.swift */, + 1AE1093F2517671A001E5B01 /* NSObject+Disposable.swift */, + 1AE109402517671A001E5B01 /* Loading.swift */, + 1AE109412517671A001E5B01 /* Bool.swift */, + 1AE109422517671A001E5B01 /* Just.swift */, + ); + path = RxUtils; + sourceTree = ""; + }; + 1AE1094D25176720001E5B01 /* Closure */ = { + isa = PBXGroup; + children = ( + 1AE1094E25176720001E5B01 /* Unown.swift */, + 1AE1094F25176720001E5B01 /* Closure.swift */, + 1AE1095025176720001E5B01 /* RawClosure.swift */, + 1AE1095125176720001E5B01 /* Set.swift */, + ); + path = Closure; + sourceTree = ""; + }; + 1AE1095625176731001E5B01 /* Log */ = { + isa = PBXGroup; + children = ( + 1AE1095725176731001E5B01 /* Logging.swift */, + 1AE1095825176731001E5B01 /* Log.swift */, + 1AE1095925176731001E5B01 /* Params.swift */, + 1AE1095A25176731001E5B01 /* Level.swift */, + ); + path = Log; + sourceTree = ""; + }; + 1AE1095F25176854001E5B01 /* DefaultInitializable */ = { + isa = PBXGroup; + children = ( + 1AE1096025176854001E5B01 /* DefaultInitializable.swift */, + ); + path = DefaultInitializable; + sourceTree = ""; + }; + 1AE1096225177FA3001E5B01 /* Binder */ = { + isa = PBXGroup; + children = ( + 1AE1096D2517809F001E5B01 /* DisposeViewController.swift */, + 1AE1096B2517800D001E5B01 /* Transitioning.swift */, + 1AE1096425177FA3001E5B01 /* ViewControllerBinder.swift */, + 1AE1096525177FA3001E5B01 /* NavigationBinder.swift */, + ); + path = Binder; + sourceTree = ""; + }; + 1AE109A32517A3AC001E5B01 /* Resource */ = { + isa = PBXGroup; + children = ( + 1AE109A42517A3AC001E5B01 /* R.generated.swift */, + ); + name = Resource; + path = "tmdb-rx-driver/Resource"; + sourceTree = SOURCE_ROOT; + }; + B13475FE0AB43ADE61034608 /* Pods */ = { + isa = PBXGroup; + children = ( + AF7C2B4A13E312E1FD20CF55 /* Pods-tmdb-rx-driver.debug.xcconfig */, + 41160404C5F5FF90CD712B13 /* Pods-tmdb-rx-driver.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + E22C1B6881174EBB0CB30744 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 973976C40C7DC73287C8EF44 /* Pods_tmdb_rx_driver.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 1AE0176325175DD40019F8FA /* tmdb-rx-driver */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1AE0177825175DD50019F8FA /* Build configuration list for PBXNativeTarget "tmdb-rx-driver" */; + buildPhases = ( + EDC835FD4A08A05D3C174CC7 /* [CP] Check Pods Manifest.lock */, + 1AE1096F2517A274001E5B01 /* ShellScript */, + 1AE0176025175DD40019F8FA /* Sources */, + 1AE0176125175DD40019F8FA /* Frameworks */, + 1AE0176225175DD40019F8FA /* Resources */, + 340925E38404B49368B8B0B7 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "tmdb-rx-driver"; + productName = "tmdb-rx-driver"; + productReference = 1AE0176425175DD40019F8FA /* tmdb-rx-driver.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 1AE0175C25175DD40019F8FA /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1170; + LastUpgradeCheck = 1170; + TargetAttributes = { + 1AE0176325175DD40019F8FA = { + CreatedOnToolsVersion = 11.7; + }; + }; + }; + buildConfigurationList = 1AE0175F25175DD40019F8FA /* Build configuration list for PBXProject "tmdb-rx-driver" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 1AE0175B25175DD40019F8FA; + productRefGroup = 1AE0176525175DD40019F8FA /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 1AE0176325175DD40019F8FA /* tmdb-rx-driver */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 1AE0176225175DD40019F8FA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1AE017D52517612C0019F8FA /* LaunchScreen.storyboard in Resources */, + 1AE017C0251760900019F8FA /* DiscoverMainView.xib in Resources */, + 1AE017CB251760900019F8FA /* MovieDetailHeaderView.xib in Resources */, + 1AE017CC251760900019F8FA /* MovieDetailTipsView.xib in Resources */, + 1AE017972517607F0019F8FA /* Main.storyboard in Resources */, + 1AE017C5251760900019F8FA /* SearchCell.xib in Resources */, + 1AE017BC251760900019F8FA /* CarouselSectionCell.xib in Resources */, + 1AE017D3251761180019F8FA /* Assets.xcassets in Resources */, + 1AE017BF251760900019F8FA /* MovieCell.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 1AE1096F2517A274001E5B01 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$TEMP_DIR/rswift-lastrun", + ); + outputFileListPaths = ( + ); + outputPaths = ( + "${SRCROOT}/${TARGET_NAME}/Resource/R.generated.swift", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "$PODS_ROOT/R.swift/rswift generate ${SRCROOT}/${TARGET_NAME}/Resource/R.generated.swift\n"; + }; + 340925E38404B49368B8B0B7 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-tmdb-rx-driver/Pods-tmdb-rx-driver-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-tmdb-rx-driver/Pods-tmdb-rx-driver-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-tmdb-rx-driver/Pods-tmdb-rx-driver-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + EDC835FD4A08A05D3C174CC7 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-tmdb-rx-driver-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 1AE0176025175DD40019F8FA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1AE1095E25176731001E5B01 /* Level.swift in Sources */, + 1AE017C3251760900019F8FA /* SearchDriver.swift in Sources */, + 1AE1095525176720001E5B01 /* Set.swift in Sources */, + 1AE0178225175EF50019F8FA /* Person.swift in Sources */, + 1AE1095D25176731001E5B01 /* Params.swift in Sources */, + 1AE0178825175F280019F8FA /* TMDBApi.swift in Sources */, + 1AE0179025175F490019F8FA /* UIStoryboard+ViewControllers.swift in Sources */, + 1AE0179125175F490019F8FA /* ActivityIndicator.swift in Sources */, + 1AE017BA251760900019F8FA /* DiscoverViewController.swift in Sources */, + 1AE1094A2517671A001E5B01 /* Loading.swift in Sources */, + 1AE109472517671A001E5B01 /* Driver.swift in Sources */, + 1AE017D9251761610019F8FA /* AppDelegate.swift in Sources */, + 1AE0178025175EF50019F8FA /* Movie.swift in Sources */, + 1AE109A72517A461001E5B01 /* SearchStateBinder.swift in Sources */, + 1AE017CD251760900019F8FA /* MovieDetailTipsView.swift in Sources */, + 1AE1096725177FA4001E5B01 /* ViewControllerBinder.swift in Sources */, + 1AE1095225176720001E5B01 /* Unown.swift in Sources */, + 1AE017BD251760900019F8FA /* DiscoverMainView.swift in Sources */, + 1AE10937251765B6001E5B01 /* SearchResultItemViewModel.swift in Sources */, + 1AE1096E2517809F001E5B01 /* DisposeViewController.swift in Sources */, + 1AE017C1251760900019F8FA /* CarouselSectionCell.swift in Sources */, + 1AE109432517671A001E5B01 /* Next.swift in Sources */, + 1A8CB9B62519DC7600F25598 /* MovieDetailActionBinder.swift in Sources */, + 1AE1095425176720001E5B01 /* RawClosure.swift in Sources */, + 1AE0178925175F280019F8FA /* HTTPClient.swift in Sources */, + 1AE1094C2517671A001E5B01 /* Just.swift in Sources */, + 1AE1096C2517800D001E5B01 /* Transitioning.swift in Sources */, + 1AA01CB42520C8A7000C2B5B /* CarouselViewModel.swift in Sources */, + 1AE0178A25175F280019F8FA /* TMDBApiResponses.swift in Sources */, + 1AE109462517671A001E5B01 /* Util.swift in Sources */, + 1AE0178325175EF50019F8FA /* Show.swift in Sources */, + 1AE017CF251760900019F8FA /* LoginDriver.swift in Sources */, + 1AE109492517671A001E5B01 /* NSObject+Disposable.swift in Sources */, + 1AE017C2251760900019F8FA /* SearchViewController.swift in Sources */, + 1AE1095C25176731001E5B01 /* Log.swift in Sources */, + 1A8CB9B42519DC6800F25598 /* MovieDetailStateBinder.swift in Sources */, + 1AE109482517671A001E5B01 /* Latest.swift in Sources */, + 1AE1095B25176731001E5B01 /* Logging.swift in Sources */, + 1AE1096125176854001E5B01 /* DefaultInitializable.swift in Sources */, + 1AE017D1251760900019F8FA /* LoginViewController.swift in Sources */, + 1AE017CA251760900019F8FA /* GradientImageView.swift in Sources */, + 1AE109A92517C026001E5B01 /* SearchActionBinder.swift in Sources */, + 1AE017BE251760900019F8FA /* MovieCell.swift in Sources */, + 1AE1095325176720001E5B01 /* Closure.swift in Sources */, + 1AE017DA251761610019F8FA /* App.swift in Sources */, + 1AE109A52517A3AC001E5B01 /* R.generated.swift in Sources */, + 1AE0178125175EF50019F8FA /* Genre.swift in Sources */, + 1AE017CE251760900019F8FA /* MovieDetailHeaderView.swift in Sources */, + 1AE109452517671A001E5B01 /* Callable.swift in Sources */, + 1AE017C7251760900019F8FA /* MovieDetailDriver.swift in Sources */, + 1A0DE663251C81A300BF3D31 /* LoginStateBinder.swift in Sources */, + 1AE0179225175F490019F8FA /* ViewModelType.swift in Sources */, + 1A1DA8B72529C0E2008A08BE /* DiscoverViewControllerBinder.swift in Sources */, + 1AE017BB251760900019F8FA /* DiscoverViewModel.swift in Sources */, + 1AE017C9251760900019F8FA /* MovieDetailViewController.swift in Sources */, + 1AE1096825177FA4001E5B01 /* NavigationBinder.swift in Sources */, + 1AE017C4251760900019F8FA /* SearchCell.swift in Sources */, + 1AE109442517671A001E5B01 /* Publicsher.swift in Sources */, + 1A0DE661251C818B00BF3D31 /* LoginActionBinder.swift in Sources */, + 1AE1094B2517671A001E5B01 /* Bool.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 1AE017952517607F0019F8FA /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 1AE017962517607F0019F8FA /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 1AE0177625175DD50019F8FA /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.7; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 1AE0177725175DD50019F8FA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.7; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 1AE0177925175DD50019F8FA /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AF7C2B4A13E312E1FD20CF55 /* Pods-tmdb-rx-driver.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = "tmdb-rx-driver/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.tmdb-rx-driver.tmdb-rx-driver"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 1AE0177A25175DD50019F8FA /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 41160404C5F5FF90CD712B13 /* Pods-tmdb-rx-driver.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = "tmdb-rx-driver/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.tmdb-rx-driver.tmdb-rx-driver"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1AE0175F25175DD40019F8FA /* Build configuration list for PBXProject "tmdb-rx-driver" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1AE0177625175DD50019F8FA /* Debug */, + 1AE0177725175DD50019F8FA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1AE0177825175DD50019F8FA /* Build configuration list for PBXNativeTarget "tmdb-rx-driver" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1AE0177925175DD50019F8FA /* Debug */, + 1AE0177A25175DD50019F8FA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 1AE0175C25175DD40019F8FA /* Project object */; +} diff --git a/tmdb-rx-driver/tmdb-rx-driver.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/tmdb-rx-driver/tmdb-rx-driver.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..b6afe7c --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/tmdb-rx-driver/tmdb-rx-driver.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/tmdb-rx-driver/tmdb-rx-driver.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/tmdb-rx-driver/tmdb-rx-driver.xcodeproj/xcshareddata/xcschemes/tmdb-rx-driver.xcscheme b/tmdb-rx-driver/tmdb-rx-driver.xcodeproj/xcshareddata/xcschemes/tmdb-rx-driver.xcscheme new file mode 100644 index 0000000..3141c14 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver.xcodeproj/xcshareddata/xcschemes/tmdb-rx-driver.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tmdb-rx-driver/tmdb-rx-driver.xcworkspace/contents.xcworkspacedata b/tmdb-rx-driver/tmdb-rx-driver.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..c170bec --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/tmdb-rx-driver/tmdb-rx-driver.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/tmdb-rx-driver/tmdb-rx-driver.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/tmdb-rx-driver/tmdb-rx-driver/Application/App.swift b/tmdb-rx-driver/tmdb-rx-driver/Application/App.swift new file mode 100644 index 0000000..14aa113 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Application/App.swift @@ -0,0 +1,42 @@ +// +// App.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 30/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import UIKit + +final class App { + static let shared = App() + + func startInterface(in window: UIWindow) { + let discoverViewController = DiscoverViewController.Factory.default + let discoverNavigationController = UINavigationController(rootViewController: discoverViewController) + + let searchNavigationController = UINavigationController(rootViewController: SearchViewController.Factory.default) + + let tabBarController = UITabBarController() + tabBarController.tabBar.barTintColor = UIColor(red: 18/255, green: 18/255, blue: 18/255, alpha: 1.0) + tabBarController.tabBar.tintColor = .white + + discoverNavigationController.tabBarItem = UITabBarItem(title: "Discover", image: nil, selectedImage: nil) + + searchNavigationController.tabBarItem = UITabBarItem(title: "Search", image: nil, selectedImage: nil) + + tabBarController.viewControllers = [ + discoverNavigationController, + searchNavigationController + ] + + let loginNavigationController = UINavigationController(rootViewController: LoginViewController.Factory.default) + + window.rootViewController = tabBarController + window.makeKeyAndVisible() + + // Not the nicest solution, if someone has any idea how to manage login/main screens, please let me know! + tabBarController.present(loginNavigationController, animated: true, completion: nil) + + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Application/AppDelegate.swift b/tmdb-rx-driver/tmdb-rx-driver/Application/AppDelegate.swift new file mode 100644 index 0000000..72f3846 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Application/AppDelegate.swift @@ -0,0 +1,25 @@ +// +// AppDelegate.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 27/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + window = UIWindow(frame: UIScreen.main.bounds) + + if let window = window { + App.shared.startInterface(in: window) + } + + return true + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Application/LaunchScreen.storyboard b/tmdb-rx-driver/tmdb-rx-driver/Application/LaunchScreen.storyboard new file mode 100644 index 0000000..bfa3612 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Application/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/AppIcon.appiconset/Contents.json b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d8db8d6 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/Contents.json b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/aquaman.imageset/Contents.json b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/aquaman.imageset/Contents.json new file mode 100644 index 0000000..223d5ac --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/aquaman.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "aquaman.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/aquaman.imageset/aquaman.jpg b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/aquaman.imageset/aquaman.jpg new file mode 100644 index 0000000..12651dd Binary files /dev/null and b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/aquaman.imageset/aquaman.jpg differ diff --git a/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/back_arrow.imageset/Contents.json b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/back_arrow.imageset/Contents.json new file mode 100644 index 0000000..510eff1 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/back_arrow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "icons8-left-100 (1).png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/back_arrow.imageset/icons8-left-100 (1).png b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/back_arrow.imageset/icons8-left-100 (1).png new file mode 100644 index 0000000..c19689b Binary files /dev/null and b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/back_arrow.imageset/icons8-left-100 (1).png differ diff --git a/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/sample.imageset/Contents.json b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/sample.imageset/Contents.json new file mode 100644 index 0000000..ecf5bce --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/sample.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "sample.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/sample.imageset/sample.jpg b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/sample.imageset/sample.jpg new file mode 100644 index 0000000..78e39e9 Binary files /dev/null and b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/sample.imageset/sample.jpg differ diff --git a/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/search_icon.imageset/Contents.json b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/search_icon.imageset/Contents.json new file mode 100644 index 0000000..9d1ff52 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/search_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "icons8-search-100.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/search_icon.imageset/icons8-search-100.png b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/search_icon.imageset/icons8-search-100.png new file mode 100644 index 0000000..d4f1f20 Binary files /dev/null and b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/search_icon.imageset/icons8-search-100.png differ diff --git a/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/star_icon.imageset/Contents.json b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/star_icon.imageset/Contents.json new file mode 100644 index 0000000..fefb68a --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/star_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "icons8-star-filled-96.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/star_icon.imageset/icons8-star-filled-96.png b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/star_icon.imageset/icons8-star-filled-96.png new file mode 100644 index 0000000..42c4379 Binary files /dev/null and b/tmdb-rx-driver/tmdb-rx-driver/Assets.xcassets/star_icon.imageset/icons8-star-filled-96.png differ diff --git a/tmdb-rx-driver/tmdb-rx-driver/Info.plist b/tmdb-rx-driver/tmdb-rx-driver/Info.plist new file mode 100644 index 0000000..25eeb5c --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Info.plist @@ -0,0 +1,50 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/tmdb-rx-driver/tmdb-rx-driver/Models/Genre.swift b/tmdb-rx-driver/tmdb-rx-driver/Models/Genre.swift new file mode 100644 index 0000000..65ec7d8 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Models/Genre.swift @@ -0,0 +1,14 @@ +// +// Genre.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 30/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import Foundation + +struct Genre: Decodable { + let id: Int + let name: String +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Models/Movie.swift b/tmdb-rx-driver/tmdb-rx-driver/Models/Movie.swift new file mode 100644 index 0000000..76f5e0a --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Models/Movie.swift @@ -0,0 +1,35 @@ +// +// Movie.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 30/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import Foundation + +struct Movie: Decodable { + let id: Int + let title: String + let overview: String + let genres: [Genre]? + let posterUrl: String? + let releaseDate: String + let runtime: Int? + let voteAverage: Double? + let voteCount: Int? + let status: String? + + enum CodingKeys: String, CodingKey { + case id + case title + case overview + case genres + case posterUrl = "poster_path" + case releaseDate = "release_date" + case runtime + case voteAverage = "vote_average" + case voteCount = "vote_count" + case status + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Models/Person.swift b/tmdb-rx-driver/tmdb-rx-driver/Models/Person.swift new file mode 100644 index 0000000..14d4971 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Models/Person.swift @@ -0,0 +1,46 @@ +// +// Person.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 30/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import Foundation + +struct KnownFor: Decodable { + let title: String? +} + +struct Person: Decodable { + let id: Int + let name: String + let profileUrl: String? + let knownForTitles: [String]? + + enum CodingKeys: String, CodingKey { + case id, name, profileUrl = "profile_path", knownFor = "known_for" + } + + enum KnownForKeys: String, CodingKey { + case knownForTitle = "title" + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(Int.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + profileUrl = try? container.decode(String.self, forKey: .profileUrl) + var known = try container.nestedUnkeyedContainer(forKey: .knownFor) + var titles: [String]? = [] + while !known.isAtEnd { + if let knownForDecodable = try? known.decode(KnownFor.self), + let title = knownForDecodable.title { + titles?.append(title) + } else { + titles = nil + } + } + knownForTitles = titles + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Models/Show.swift b/tmdb-rx-driver/tmdb-rx-driver/Models/Show.swift new file mode 100644 index 0000000..97d517c --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Models/Show.swift @@ -0,0 +1,20 @@ +// +// Show.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 30/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import Foundation + +struct Show: Decodable { + let id: Int + let name: String + let posterUrl: String + let releaseDate: String + + enum CodingKeys: String, CodingKey { + case id, name, posterUrl = "poster_path", releaseDate = "first_air_date" + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Networking/HTTPClient.swift b/tmdb-rx-driver/tmdb-rx-driver/Networking/HTTPClient.swift new file mode 100644 index 0000000..4120057 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Networking/HTTPClient.swift @@ -0,0 +1,38 @@ +// +// HTTPClient.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 30/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import RxSwift +import RxCocoa + +protocol HTTPClientProvider { + func get(url: String) -> Observable + func post(url: String, params: [String: Any]) -> Observable +} + +final class HTTPClient: HTTPClientProvider { + func get(url: String) -> Observable { + guard let url = URL(string: url) else { return Observable.empty() } + let request = URLRequest(url: url) + return URLSession.shared.rx.data(request: request) + .map { Optional.init($0) } + .catchErrorJustReturn(nil) + } + + func post(url: String, params: [String: Any]) -> Observable { + guard let url = URL(string: url) else { return Observable.empty() } + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + let jsonData = try? JSONSerialization.data(withJSONObject: params, options: .prettyPrinted) + request.httpBody = jsonData + return URLSession.shared.rx.data(request: request) + .map { Optional.init($0) } + .catchErrorJustReturn(nil) + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Networking/TMDBApi.swift b/tmdb-rx-driver/tmdb-rx-driver/Networking/TMDBApi.swift new file mode 100644 index 0000000..c250553 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Networking/TMDBApi.swift @@ -0,0 +1,145 @@ +// +// TMDBApi.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 27/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import RxSwift +import RxCocoa + +protocol TMDBApiMoviesProvider { + func fetchPopularMovies() -> Observable<[Movie]?> + func fetchMovieDetails(forMovieId id: Int) -> Observable + func searchMovies(forQuery query: String) -> Observable<[Movie]?> +} + +protocol TMDBApiPeopleProvider { + func fetchPopularPeople() -> Observable<[Person]?> + func searchPeople(forQuery query: String) -> Observable<[Person]?> +} + +protocol TMDBApiShowsProvider { + func fetchPopularShows() -> Observable<[Show]?> +} + +protocol TMDBApiAuthProvider { + func login(withUsername username: String, password: String) -> Observable +} + +protocol TMDBApiProvider: TMDBApiMoviesProvider, TMDBApiPeopleProvider, TMDBApiShowsProvider, TMDBApiAuthProvider { } + +final class TMDBApi: TMDBApiProvider { + private struct Constants { + static let apiKey = "3f093e78fd47d26523d784196a33f00a" + } + + private let httpClient: HTTPClientProvider + + init(httpClient: HTTPClientProvider = HTTPClient()) { + self.httpClient = httpClient + } + + func fetchPopularMovies() -> Observable<[Movie]?> { + return httpClient.get(url: "https://api.themoviedb.org/3/discover/movie?api_key=\(Constants.apiKey)&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false") + .map { data -> [Movie]? in + guard let data = data, + let response = try? JSONDecoder().decode(MoviesResponse.self, from: data) else { + return nil + } + return response.results + } + } + + func fetchPopularPeople() -> Observable<[Person]?> { + return httpClient.get(url: "https://api.themoviedb.org/3/person/popular?api_key=\(Constants.apiKey)&language=en-US&page=1") + .map { data -> [Person]? in + guard let data = data, + let response = try? JSONDecoder().decode(PeopleResponse.self, from: data) else { + return nil + } + return response.results + } + } + + func fetchPopularShows() -> Observable<[Show]?> { + return httpClient.get(url: "https://api.themoviedb.org/3/discover/tv?api_key=\(Constants.apiKey)&language=en-US&sort_by=popularity.desc&page=1&include_null_first_air_dates=false") + .map { data -> [Show]? in + guard let data = data, + let response = try? JSONDecoder().decode(ShowsResponse.self, from: data) else { + return nil + } + return response.results + } + } + + func fetchMovieDetails(forMovieId id: Int) -> Observable { + return httpClient.get(url: "https://api.themoviedb.org/3/movie/\(id)?api_key=\(Constants.apiKey)&language=en-US)") + .map { data -> Movie? in + guard let data = data, + let response = try? JSONDecoder().decode(Movie.self, from: data) else { + return nil + } + return response + } + } + + func searchMovies(forQuery query: String) -> Observable<[Movie]?> { + return httpClient.get(url: "https://api.themoviedb.org/3/search/movie?api_key=\(Constants.apiKey)&language=en-US&query=\(query)&page=1&include_adult=false") + .map { data -> [Movie]? in + guard let data = data, + let response = try? JSONDecoder().decode(MoviesResponse.self, from: data) else { + return nil + } + + return response.results + } + } + + func searchPeople(forQuery query: String) -> Observable<[Person]?> { + return httpClient.get(url: "https://api.themoviedb.org/3/search/person?api_key=\(Constants.apiKey)&language=en-US&query=\(query)&page=1&include_adult=false") + .map { data -> [Person]? in + guard let data = data, + let response = try? JSONDecoder().decode(PeopleResponse.self, from: data) else { + return nil + } + return response.results + } + } + + func login(withUsername username: String, password: String) -> Observable { + return fetchAuthToken() + .flatMap { [weak self] (token: String?) -> Observable in + guard let strongSelf = self, + let token = token else { return Observable.just(nil) } + return strongSelf.httpClient.post(url: "https://api.themoviedb.org/3/authentication/token/validate_with_login?api_key=\(Constants.apiKey)", + params: ["username": username, "password": password, "request_token": token]) + } + .map { (data: Data?) -> Bool in + guard let data = data, + let response = try? JSONDecoder().decode(LoginResponse.self, from: data) else { + return false + } + return response.success + } + } + + + private func fetchAuthToken() -> Observable { + return httpClient.get(url: "https://api.themoviedb.org/3/authentication/token/new?api_key=\(Constants.apiKey)") + .map { data -> String? in + guard let data = data, + let response = try? JSONDecoder().decode(AuthTokenResponse.self, from: data) else { + return nil + } + return response.requestToken + } + } +} + +extension TMDBApi: StaticFactory { + enum Factory { + static let `default`: TMDBApiProvider = TMDBApi() + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Networking/TMDBApiResponses.swift b/tmdb-rx-driver/tmdb-rx-driver/Networking/TMDBApiResponses.swift new file mode 100644 index 0000000..9e357d3 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Networking/TMDBApiResponses.swift @@ -0,0 +1,33 @@ +// +// TMDBApiResponses.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 30/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import Foundation + +struct PeopleResponse: Decodable { + let results: [Person] +} + +struct ShowsResponse: Decodable { + let results: [Show] +} + +struct MoviesResponse: Decodable { + let results: [Movie] +} + +struct LoginResponse: Decodable { + let success: Bool +} + +struct AuthTokenResponse: Decodable { + let requestToken: String + + enum CodingKeys: String, CodingKey { + case requestToken = "request_token" + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Resource/R.generated.swift b/tmdb-rx-driver/tmdb-rx-driver/Resource/R.generated.swift new file mode 100644 index 0000000..1addcf3 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Resource/R.generated.swift @@ -0,0 +1,466 @@ +// +// This is a generated file, do not edit! +// Generated by R.swift, see https://github.com/mac-cain13/R.swift +// + +import Foundation +import Rswift +import UIKit + +/// This `R` struct is generated and contains references to static resources. +struct R: Rswift.Validatable { + fileprivate static let applicationLocale = hostingBundle.preferredLocalizations.first.flatMap { Locale(identifier: $0) } ?? Locale.current + fileprivate static let hostingBundle = Bundle(for: R.Class.self) + + /// Find first language and bundle for which the table exists + fileprivate static func localeBundle(tableName: String, preferredLanguages: [String]) -> (Foundation.Locale, Foundation.Bundle)? { + // Filter preferredLanguages to localizations, use first locale + var languages = preferredLanguages + .map { Locale(identifier: $0) } + .prefix(1) + .flatMap { locale -> [String] in + if hostingBundle.localizations.contains(locale.identifier) { + if let language = locale.languageCode, hostingBundle.localizations.contains(language) { + return [locale.identifier, language] + } else { + return [locale.identifier] + } + } else if let language = locale.languageCode, hostingBundle.localizations.contains(language) { + return [language] + } else { + return [] + } + } + + // If there's no languages, use development language as backstop + if languages.isEmpty { + if let developmentLocalization = hostingBundle.developmentLocalization { + languages = [developmentLocalization] + } + } else { + // Insert Base as second item (between locale identifier and languageCode) + languages.insert("Base", at: 1) + + // Add development language as backstop + if let developmentLocalization = hostingBundle.developmentLocalization { + languages.append(developmentLocalization) + } + } + + // Find first language for which table exists + // Note: key might not exist in chosen language (in that case, key will be shown) + for language in languages { + if let lproj = hostingBundle.url(forResource: language, withExtension: "lproj"), + let lbundle = Bundle(url: lproj) + { + let strings = lbundle.url(forResource: tableName, withExtension: "strings") + let stringsdict = lbundle.url(forResource: tableName, withExtension: "stringsdict") + + if strings != nil || stringsdict != nil { + return (Locale(identifier: language), lbundle) + } + } + } + + // If table is available in main bundle, don't look for localized resources + let strings = hostingBundle.url(forResource: tableName, withExtension: "strings", subdirectory: nil, localization: nil) + let stringsdict = hostingBundle.url(forResource: tableName, withExtension: "stringsdict", subdirectory: nil, localization: nil) + + if strings != nil || stringsdict != nil { + return (applicationLocale, hostingBundle) + } + + // If table is not found for requested languages, key will be shown + return nil + } + + /// Load string from Info.plist file + fileprivate static func infoPlistString(path: [String], key: String) -> String? { + var dict = hostingBundle.infoDictionary + for step in path { + guard let obj = dict?[step] as? [String: Any] else { return nil } + dict = obj + } + return dict?[key] as? String + } + + static func validate() throws { + try intern.validate() + } + + #if os(iOS) || os(tvOS) + /// This `R.storyboard` struct is generated, and contains static references to 2 storyboards. + struct storyboard { + /// Storyboard `LaunchScreen`. + static let launchScreen = _R.storyboard.launchScreen() + /// Storyboard `Main`. + static let main = _R.storyboard.main() + + #if os(iOS) || os(tvOS) + /// `UIStoryboard(name: "LaunchScreen", bundle: ...)` + static func launchScreen(_: Void = ()) -> UIKit.UIStoryboard { + return UIKit.UIStoryboard(resource: R.storyboard.launchScreen) + } + #endif + + #if os(iOS) || os(tvOS) + /// `UIStoryboard(name: "Main", bundle: ...)` + static func main(_: Void = ()) -> UIKit.UIStoryboard { + return UIKit.UIStoryboard(resource: R.storyboard.main) + } + #endif + + fileprivate init() {} + } + #endif + + /// This `R.image` struct is generated, and contains static references to 5 images. + struct image { + /// Image `aquaman`. + static let aquaman = Rswift.ImageResource(bundle: R.hostingBundle, name: "aquaman") + /// Image `back_arrow`. + static let back_arrow = Rswift.ImageResource(bundle: R.hostingBundle, name: "back_arrow") + /// Image `sample`. + static let sample = Rswift.ImageResource(bundle: R.hostingBundle, name: "sample") + /// Image `search_icon`. + static let search_icon = Rswift.ImageResource(bundle: R.hostingBundle, name: "search_icon") + /// Image `star_icon`. + static let star_icon = Rswift.ImageResource(bundle: R.hostingBundle, name: "star_icon") + + #if os(iOS) || os(tvOS) + /// `UIImage(named: "aquaman", bundle: ..., traitCollection: ...)` + static func aquaman(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? { + return UIKit.UIImage(resource: R.image.aquaman, compatibleWith: traitCollection) + } + #endif + + #if os(iOS) || os(tvOS) + /// `UIImage(named: "back_arrow", bundle: ..., traitCollection: ...)` + static func back_arrow(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? { + return UIKit.UIImage(resource: R.image.back_arrow, compatibleWith: traitCollection) + } + #endif + + #if os(iOS) || os(tvOS) + /// `UIImage(named: "sample", bundle: ..., traitCollection: ...)` + static func sample(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? { + return UIKit.UIImage(resource: R.image.sample, compatibleWith: traitCollection) + } + #endif + + #if os(iOS) || os(tvOS) + /// `UIImage(named: "search_icon", bundle: ..., traitCollection: ...)` + static func search_icon(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? { + return UIKit.UIImage(resource: R.image.search_icon, compatibleWith: traitCollection) + } + #endif + + #if os(iOS) || os(tvOS) + /// `UIImage(named: "star_icon", bundle: ..., traitCollection: ...)` + static func star_icon(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? { + return UIKit.UIImage(resource: R.image.star_icon, compatibleWith: traitCollection) + } + #endif + + fileprivate init() {} + } + + /// This `R.nib` struct is generated, and contains static references to 6 nibs. + struct nib { + /// Nib `CarouselSectionCell`. + static let carouselSectionCell = _R.nib._CarouselSectionCell() + /// Nib `DiscoverMainView`. + static let discoverMainView = _R.nib._DiscoverMainView() + /// Nib `MovieCell`. + static let movieCell = _R.nib._MovieCell() + /// Nib `MovieDetailHeaderView`. + static let movieDetailHeaderView = _R.nib._MovieDetailHeaderView() + /// Nib `MovieDetailTipsView`. + static let movieDetailTipsView = _R.nib._MovieDetailTipsView() + /// Nib `SearchCell`. + static let searchCell = _R.nib._SearchCell() + + #if os(iOS) || os(tvOS) + /// `UINib(name: "CarouselSectionCell", in: bundle)` + @available(*, deprecated, message: "Use UINib(resource: R.nib.carouselSectionCell) instead") + static func carouselSectionCell(_: Void = ()) -> UIKit.UINib { + return UIKit.UINib(resource: R.nib.carouselSectionCell) + } + #endif + + #if os(iOS) || os(tvOS) + /// `UINib(name: "DiscoverMainView", in: bundle)` + @available(*, deprecated, message: "Use UINib(resource: R.nib.discoverMainView) instead") + static func discoverMainView(_: Void = ()) -> UIKit.UINib { + return UIKit.UINib(resource: R.nib.discoverMainView) + } + #endif + + #if os(iOS) || os(tvOS) + /// `UINib(name: "MovieCell", in: bundle)` + @available(*, deprecated, message: "Use UINib(resource: R.nib.movieCell) instead") + static func movieCell(_: Void = ()) -> UIKit.UINib { + return UIKit.UINib(resource: R.nib.movieCell) + } + #endif + + #if os(iOS) || os(tvOS) + /// `UINib(name: "MovieDetailHeaderView", in: bundle)` + @available(*, deprecated, message: "Use UINib(resource: R.nib.movieDetailHeaderView) instead") + static func movieDetailHeaderView(_: Void = ()) -> UIKit.UINib { + return UIKit.UINib(resource: R.nib.movieDetailHeaderView) + } + #endif + + #if os(iOS) || os(tvOS) + /// `UINib(name: "MovieDetailTipsView", in: bundle)` + @available(*, deprecated, message: "Use UINib(resource: R.nib.movieDetailTipsView) instead") + static func movieDetailTipsView(_: Void = ()) -> UIKit.UINib { + return UIKit.UINib(resource: R.nib.movieDetailTipsView) + } + #endif + + #if os(iOS) || os(tvOS) + /// `UINib(name: "SearchCell", in: bundle)` + @available(*, deprecated, message: "Use UINib(resource: R.nib.searchCell) instead") + static func searchCell(_: Void = ()) -> UIKit.UINib { + return UIKit.UINib(resource: R.nib.searchCell) + } + #endif + + static func carouselSectionCell(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> CarouselSectionCell? { + return R.nib.carouselSectionCell.instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? CarouselSectionCell + } + + static func discoverMainView(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> UIKit.UIView? { + return R.nib.discoverMainView.instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? UIKit.UIView + } + + static func movieCell(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> MovieCell? { + return R.nib.movieCell.instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? MovieCell + } + + static func movieDetailHeaderView(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> UIKit.UIView? { + return R.nib.movieDetailHeaderView.instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? UIKit.UIView + } + + static func movieDetailTipsView(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> UIKit.UIView? { + return R.nib.movieDetailTipsView.instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? UIKit.UIView + } + + static func searchCell(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> SearchCell? { + return R.nib.searchCell.instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? SearchCell + } + + fileprivate init() {} + } + + /// This `R.reuseIdentifier` struct is generated, and contains static references to 3 reuse identifiers. + struct reuseIdentifier { + /// Reuse identifier `CarouselSectionCell`. + static let carouselSectionCell: Rswift.ReuseIdentifier = Rswift.ReuseIdentifier(identifier: "CarouselSectionCell") + /// Reuse identifier `MovieCell`. + static let movieCell: Rswift.ReuseIdentifier = Rswift.ReuseIdentifier(identifier: "MovieCell") + /// Reuse identifier `SearchCell`. + static let searchCell: Rswift.ReuseIdentifier = Rswift.ReuseIdentifier(identifier: "SearchCell") + + fileprivate init() {} + } + + fileprivate struct intern: Rswift.Validatable { + fileprivate static func validate() throws { + try _R.validate() + } + + fileprivate init() {} + } + + fileprivate class Class {} + + fileprivate init() {} +} + +struct _R: Rswift.Validatable { + static func validate() throws { + #if os(iOS) || os(tvOS) + try nib.validate() + #endif + #if os(iOS) || os(tvOS) + try storyboard.validate() + #endif + } + + #if os(iOS) || os(tvOS) + struct nib: Rswift.Validatable { + static func validate() throws { + try _MovieCell.validate() + try _MovieDetailHeaderView.validate() + } + + struct _CarouselSectionCell: Rswift.NibResourceType, Rswift.ReuseIdentifierType { + typealias ReusableType = CarouselSectionCell + + let bundle = R.hostingBundle + let identifier = "CarouselSectionCell" + let name = "CarouselSectionCell" + + func firstView(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> CarouselSectionCell? { + return instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? CarouselSectionCell + } + + fileprivate init() {} + } + + struct _DiscoverMainView: Rswift.NibResourceType { + let bundle = R.hostingBundle + let name = "DiscoverMainView" + + func firstView(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> UIKit.UIView? { + return instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? UIKit.UIView + } + + fileprivate init() {} + } + + struct _MovieCell: Rswift.NibResourceType, Rswift.ReuseIdentifierType, Rswift.Validatable { + typealias ReusableType = MovieCell + + let bundle = R.hostingBundle + let identifier = "MovieCell" + let name = "MovieCell" + + func firstView(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> MovieCell? { + return instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? MovieCell + } + + static func validate() throws { + if UIKit.UIImage(named: "sample", in: R.hostingBundle, compatibleWith: nil) == nil { throw Rswift.ValidationError(description: "[R.swift] Image named 'sample' is used in nib 'MovieCell', but couldn't be loaded.") } + if #available(iOS 11.0, tvOS 11.0, *) { + } + } + + fileprivate init() {} + } + + struct _MovieDetailHeaderView: Rswift.NibResourceType, Rswift.Validatable { + let bundle = R.hostingBundle + let name = "MovieDetailHeaderView" + + func firstView(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> UIKit.UIView? { + return instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? UIKit.UIView + } + + static func validate() throws { + if UIKit.UIImage(named: "star_icon", in: R.hostingBundle, compatibleWith: nil) == nil { throw Rswift.ValidationError(description: "[R.swift] Image named 'star_icon' is used in nib 'MovieDetailHeaderView', but couldn't be loaded.") } + if #available(iOS 11.0, tvOS 11.0, *) { + } + } + + fileprivate init() {} + } + + struct _MovieDetailTipsView: Rswift.NibResourceType { + let bundle = R.hostingBundle + let name = "MovieDetailTipsView" + + func firstView(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> UIKit.UIView? { + return instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? UIKit.UIView + } + + fileprivate init() {} + } + + struct _SearchCell: Rswift.NibResourceType, Rswift.ReuseIdentifierType { + typealias ReusableType = SearchCell + + let bundle = R.hostingBundle + let identifier = "SearchCell" + let name = "SearchCell" + + func firstView(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> SearchCell? { + return instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? SearchCell + } + + fileprivate init() {} + } + + fileprivate init() {} + } + #endif + + #if os(iOS) || os(tvOS) + struct storyboard: Rswift.Validatable { + static func validate() throws { + #if os(iOS) || os(tvOS) + try launchScreen.validate() + #endif + #if os(iOS) || os(tvOS) + try main.validate() + #endif + } + + #if os(iOS) || os(tvOS) + struct launchScreen: Rswift.StoryboardResourceWithInitialControllerType, Rswift.Validatable { + typealias InitialController = UIKit.UIViewController + + let bundle = R.hostingBundle + let name = "LaunchScreen" + + static func validate() throws { + if #available(iOS 11.0, tvOS 11.0, *) { + } + } + + fileprivate init() {} + } + #endif + + #if os(iOS) || os(tvOS) + struct main: Rswift.StoryboardResourceWithInitialControllerType, Rswift.Validatable { + typealias InitialController = LoginViewController + + let bundle = R.hostingBundle + let discoverViewController = StoryboardViewControllerResource(identifier: "DiscoverViewController") + let loginViewController = StoryboardViewControllerResource(identifier: "LoginViewController") + let movieDetailViewController = StoryboardViewControllerResource(identifier: "MovieDetailViewController") + let name = "Main" + let searchViewController = StoryboardViewControllerResource(identifier: "SearchViewController") + + func discoverViewController(_: Void = ()) -> DiscoverViewController? { + return UIKit.UIStoryboard(resource: self).instantiateViewController(withResource: discoverViewController) + } + + func loginViewController(_: Void = ()) -> LoginViewController? { + return UIKit.UIStoryboard(resource: self).instantiateViewController(withResource: loginViewController) + } + + func movieDetailViewController(_: Void = ()) -> MovieDetailViewController? { + return UIKit.UIStoryboard(resource: self).instantiateViewController(withResource: movieDetailViewController) + } + + func searchViewController(_: Void = ()) -> SearchViewController? { + return UIKit.UIStoryboard(resource: self).instantiateViewController(withResource: searchViewController) + } + + static func validate() throws { + if UIKit.UIImage(named: "aquaman", in: R.hostingBundle, compatibleWith: nil) == nil { throw Rswift.ValidationError(description: "[R.swift] Image named 'aquaman' is used in storyboard 'Main', but couldn't be loaded.") } + if UIKit.UIImage(named: "back_arrow", in: R.hostingBundle, compatibleWith: nil) == nil { throw Rswift.ValidationError(description: "[R.swift] Image named 'back_arrow' is used in storyboard 'Main', but couldn't be loaded.") } + if UIKit.UIImage(named: "search_icon", in: R.hostingBundle, compatibleWith: nil) == nil { throw Rswift.ValidationError(description: "[R.swift] Image named 'search_icon' is used in storyboard 'Main', but couldn't be loaded.") } + if #available(iOS 11.0, tvOS 11.0, *) { + } + if _R.storyboard.main().discoverViewController() == nil { throw Rswift.ValidationError(description:"[R.swift] ViewController with identifier 'discoverViewController' could not be loaded from storyboard 'Main' as 'DiscoverViewController'.") } + if _R.storyboard.main().loginViewController() == nil { throw Rswift.ValidationError(description:"[R.swift] ViewController with identifier 'loginViewController' could not be loaded from storyboard 'Main' as 'LoginViewController'.") } + if _R.storyboard.main().movieDetailViewController() == nil { throw Rswift.ValidationError(description:"[R.swift] ViewController with identifier 'movieDetailViewController' could not be loaded from storyboard 'Main' as 'MovieDetailViewController'.") } + if _R.storyboard.main().searchViewController() == nil { throw Rswift.ValidationError(description:"[R.swift] ViewController with identifier 'searchViewController' could not be loaded from storyboard 'Main' as 'SearchViewController'.") } + } + + fileprivate init() {} + } + #endif + + fileprivate init() {} + } + #endif + + fileprivate init() {} +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/CarouselViewModel.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/CarouselViewModel.swift new file mode 100644 index 0000000..c85fefb --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/CarouselViewModel.swift @@ -0,0 +1,74 @@ +// +// CarouselViewModel.swift +// tmdb-rx-driver +// +// Created by Dmytro Shulzhenko on 27.09.2020. +// + +import Foundation + +struct CarouselViewModel { + enum DataType { + case movie + case person + case show + } + + let type: DataType + let title: String + let subtitle: String + let items: [CarouselItemViewModel] +} + +extension CarouselViewModel { + init?(movies: [Movie]?) { + guard let movies = movies else { return nil } + self.type = .movie + self.title = "Popular movies" + self.subtitle = "Most popular in the world" + self.items = movies.map { CarouselItemViewModel(movie: $0) } + } + + init?(people: [Person]?) { + guard let people = people else { return nil } + self.type = .person + self.title = "Trending people" + self.subtitle = "Find out which celebrities are trending today" + self.items = people.map { CarouselItemViewModel(person: $0) } + + } + + init?(shows: [Show]?) { + guard let shows = shows else { return nil } + self.type = .show + self.title = "TV shows" + self.subtitle = "Latest updates on popular TV shows" + self.items = shows.map { CarouselItemViewModel(show: $0) } + } +} + +struct CarouselItemViewModel { + let title: String + let subtitle: String + let imageUrl: String? +} + +extension CarouselItemViewModel { + init(movie: Movie) { + self.title = movie.title + self.subtitle = movie.releaseDate + self.imageUrl = movie.posterUrl.flatMap { "http://image.tmdb.org/t/p/w185/" + $0 } + } + + init(person: Person) { + self.title = person.name + self.subtitle = person.knownForTitles?.first ?? " " + self.imageUrl = person.profileUrl.flatMap { "http://image.tmdb.org/t/p/w185/" + $0 } + } + + init(show: Show) { + self.title = show.name + self.subtitle = show.releaseDate + self.imageUrl = "http://image.tmdb.org/t/p/w185/" + show.posterUrl + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/DiscoverViewController.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/DiscoverViewController.swift new file mode 100644 index 0000000..9a73328 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/DiscoverViewController.swift @@ -0,0 +1,44 @@ +// +// DiscoverViewController.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 27/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import UIKit +import RxSwift +import RxCocoa + +final class DiscoverViewController: DisposeViewController { + @IBOutlet private (set) var carouselsView: DiscoverMainView! +} + +extension DiscoverViewController: StaticFactory { + enum Factory { + static var `default`: DiscoverViewController { + let vc = UIStoryboard.main.discoverViewController + let driver = DiscoverDriver(api: TMDBApi.Factory.default) + let binder = DiscoverViewControllerBinder(viewController: vc, driver: driver) + let navigationBinder = NavigationPushBinder.Factory + .push(viewController: vc, + driver: driver.didSelect, + factory: viewControllerFactory) + + vc.bag.insert( + binder, + navigationBinder + ) + return vc + } + + private static func viewControllerFactory(selection: DiscoverSelection) -> UIViewController? { + switch selection.item { + case .movie: + return MovieDetailViewController.Factory.default(id: selection.index) + case .person, .show: + return nil + } + } + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/DiscoverViewControllerBinder.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/DiscoverViewControllerBinder.swift new file mode 100644 index 0000000..50884b5 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/DiscoverViewControllerBinder.swift @@ -0,0 +1,57 @@ +// +// DiscoverViewControllerBinder.swift +// tmdb-rx-driver +// +// Created by Dmytro Shulzhenko on 04.10.2020. +// + +import UIKit + +final class DiscoverViewControllerBinder: ViewControllerBinder { + unowned let viewController: DiscoverViewController + private let driver: DiscoverDriving + + init(viewController: DiscoverViewController, + driver: DiscoverDriving) { + self.viewController = viewController + self.driver = driver + + bind() + } + + func dispose() { } + + func bindLoaded() { + viewController.statusBarStyle = .lightContent + + bag.insert( + viewController.rx.viewWillAppear + .bind(onNext: unowned(self, in: DiscoverViewControllerBinder.viewWillAppear)), + driver.state + .drive(onNext: unowned(self, in: DiscoverViewControllerBinder.apply)) + ) + + let select = viewController.carouselsView + .selectedIndex + .asDriver(onErrorJustReturn: (0, 0)) + + bag.insert( + select.drive(onNext: driver.select) + ) + } + + private func viewWillAppear(_ animated: Bool) { + viewController.navigationController?.setNavigationBarHidden(true, animated: animated) + } + + private func apply(state: DiscoverState) { + switch state { + case .loading, .none: + UIApplication.shared.isNetworkActivityIndicatorVisible = true + case let .results(viewModel): + UIApplication.shared.isNetworkActivityIndicatorVisible = false + viewController.carouselsView.setDataSource(viewModel) + viewController.carouselsView.reloadData() + } + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/DiscoverViewModel.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/DiscoverViewModel.swift new file mode 100644 index 0000000..a1ad672 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/DiscoverViewModel.swift @@ -0,0 +1,70 @@ +// +// DiscoverViewModel.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 27/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import RxSwift +import RxCocoa + +enum DiscoverState { + case none + case loading + case results([CarouselViewModel]) +} + +typealias DiscoverSelection = (item: CarouselViewModel.DataType, index: Int) + +protocol DiscoverDriving { + var didSelect: Driver { get } + var state: Driver { get } + + func select(item: Int, index: Int) +} + +final class DiscoverDriver: DiscoverDriving { + private let bag = DisposeBag() + private let didSelectRelay = PublishRelay() + private let stateRelay = BehaviorRelay(value: .none) + private var results: (movie: [Movie]?, person: [Person]?, show: [Show]?)? { + didSet { + let values = [CarouselViewModel(movies: results?.movie), + CarouselViewModel(people: results?.person), + CarouselViewModel(shows: results?.show)] + .compactMap { $0 } + stateRelay.accept(.results(values)) + } + } + private let api: TMDBApiProvider + + var didSelect: Driver { didSelectRelay.asDriver() } + var state: Driver { stateRelay.asDriver() } + + init(api: TMDBApiProvider) { + self.api = api + bind() + } + + func select(item: Int, index: Int) { + switch item { + case 0: + guard let id = results?.movie?[index].id else { return } + didSelectRelay.accept((.movie, id)) + case 1: + didSelectRelay.accept((.person, 0)) + case 2: + didSelectRelay.accept((.show, 0)) + default: + break + } + } + + private func bind() { + Observable + .zip(api.fetchPopularMovies(), api.fetchPopularPeople(), api.fetchPopularShows()) + .bind(onNext: set(unowned: self, to: \.results)) + .disposed(by: bag) + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/Views/CarouselSectionCell.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/Views/CarouselSectionCell.swift new file mode 100644 index 0000000..69a148c --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/Views/CarouselSectionCell.swift @@ -0,0 +1,48 @@ +// +// CarouselSection.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 28/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import UIKit + +final class CarouselSectionCell: UITableViewCell { + @IBOutlet private (set) var collectionViewHeightConstraint: NSLayoutConstraint! + @IBOutlet private (set) var titleLabel: UILabel! + @IBOutlet private (set) var subtitleLabel: UILabel! + @IBOutlet private (set) var collectionView: UICollectionView! + + override func awakeFromNib() { + let layout: UICollectionViewFlowLayout = { + let layout = UICollectionViewFlowLayout() + layout.estimatedItemSize = CGSize(width: 140, height: 235) + layout.scrollDirection = .horizontal + layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize + return layout + }() + + collectionView.register(UINib(nibName: String(describing: MovieCell.self), bundle: nil), + forCellWithReuseIdentifier: String(describing: MovieCell.self)) + collectionView.collectionViewLayout = layout + collectionViewHeightConstraint.constant = MovieCell.height(forWidth: 140) + collectionViewHeightConstraint.isActive = true + collectionView.contentInset = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20) + } +} + +extension CarouselSectionCell { + var collectionViewOffset: CGFloat { + get { return collectionView.contentOffset.x } + set { collectionView.contentOffset.x = newValue } + } + + func setCollectionViewDataSourceDelegate(_ dataSourceDelegate: D, forRow row: Int) { + collectionView.delegate = dataSourceDelegate + collectionView.dataSource = dataSourceDelegate + collectionView.tag = row + collectionView.setContentOffset(collectionView.contentOffset, animated:false) // Stops collection view if it was scrolling. + collectionView.reloadData() + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/Views/CarouselSectionCell.xib b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/Views/CarouselSectionCell.xib new file mode 100644 index 0000000..14793ba --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/Views/CarouselSectionCell.xib @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/Views/DiscoverMainView.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/Views/DiscoverMainView.swift new file mode 100644 index 0000000..e5cf9b9 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/Views/DiscoverMainView.swift @@ -0,0 +1,121 @@ + +// +// CarouselVIew.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 28/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import UIKit +import Nuke +import RxSwift +import RxCocoa + +final class DiscoverMainView: UIView { + private var dataSource: [CarouselViewModel]? + private var storedOffsets = [Int: CGFloat]() + @IBOutlet private var contentView: UIView! + @IBOutlet private weak var tableView: UITableView! + private let selectedIndexSubject = PublishSubject<(Int, Int)>() + var selectedIndex: Observable<(Int, Int)> { + return selectedIndexSubject.asObservable() + } + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + override func awakeFromNib() { + tableView.register(UINib(nibName: String(describing: CarouselSectionCell.self), bundle: nil), + forCellReuseIdentifier: String(describing: CarouselSectionCell.self)) + tableView.delegate = self + tableView.dataSource = self + } + + func reloadData() { + tableView.reloadData() + } + + func setDataSource(_ dataSource: [CarouselViewModel]) { + self.dataSource = dataSource + } + + private func setup() { + Bundle.main.loadNibNamed(String(describing: DiscoverMainView.self), owner: self, options: nil) + addSubview(contentView) + + contentView.frame = self.bounds + contentView.autoresizingMask = [.flexibleHeight, .flexibleWidth] + } +} + +extension DiscoverMainView: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return dataSource?.count ?? 0 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let data = dataSource else { return UITableViewCell() } // No-op + + let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: CarouselSectionCell.self), + for: indexPath) as! CarouselSectionCell + let carouselData = data[indexPath.row] + cell.titleLabel.text = carouselData.title + cell.subtitleLabel.text = carouselData.subtitle + return cell + } +} + +extension DiscoverMainView: UITableViewDelegate { + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + guard let tableViewCell = cell as? CarouselSectionCell else { return } + + tableViewCell.setCollectionViewDataSourceDelegate(self, forRow: indexPath.row) + tableViewCell.collectionViewOffset = storedOffsets[indexPath.row] ?? 0 + } + + func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) { + guard let tableViewCell = cell as? CarouselSectionCell else { return } + storedOffsets[indexPath.row] = tableViewCell.collectionViewOffset + } +} + +extension DiscoverMainView :UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + selectedIndexSubject.onNext((collectionView.tag, indexPath.item)) + } +} + +extension DiscoverMainView: UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return dataSource?[collectionView.tag].items.count ?? 0 + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let data = dataSource else { return UICollectionViewCell() } // No-op + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: MovieCell.self), for: indexPath) as! MovieCell + let item = data[collectionView.tag].items[indexPath.item] + cell.titleLabel.text = item.title + cell.subtitleLabel.text = item.subtitle + if let url = item.imageUrl { + Nuke.loadImage(with: URL(string: url)!, into: cell.imageView) + } + return cell + } +} + +extension DiscoverMainView: UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let itemsPerRow: CGFloat = 4 + let width = collectionView.bounds.width / itemsPerRow + let height = collectionView.bounds.height + return CGSize(width: width, height: height) + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/Views/DiscoverMainView.xib b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/Views/DiscoverMainView.xib new file mode 100644 index 0000000..2a13052 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/Views/DiscoverMainView.xib @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/Views/MovieCell.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/Views/MovieCell.swift new file mode 100644 index 0000000..836e834 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/Views/MovieCell.swift @@ -0,0 +1,39 @@ +// +// MovieCell.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 28/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import UIKit + +class MovieCell: UICollectionViewCell { + @IBOutlet private (set) var imageView: UIImageView! + @IBOutlet private (set) var titleLabel: UILabel! + @IBOutlet private (set) var subtitleLabel: UILabel! + + private struct Constants { + static let maxHeight: CGFloat = 400 + } + + private static let sizingCell = UINib(nibName: String(describing: MovieCell.self), bundle: nil) + .instantiate(withOwner: nil, options: nil).first! as! MovieCell + + static func height(forWidth width: CGFloat) -> CGFloat { + sizingCell.prepareForReuse() + sizingCell.layoutIfNeeded() + + var fittingSize = UIView.layoutFittingCompressedSize + fittingSize.width = width + let size = sizingCell.contentView.systemLayoutSizeFitting(fittingSize, + withHorizontalFittingPriority: .required, + verticalFittingPriority: .defaultLow) + + guard size.height < Constants.maxHeight else { + return Constants.maxHeight + } + + return size.height + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/Views/MovieCell.xib b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/Views/MovieCell.xib new file mode 100644 index 0000000..31440bc --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Discover/Views/MovieCell.xib @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Login/LoginActionBinder.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Login/LoginActionBinder.swift new file mode 100644 index 0000000..a08512e --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Login/LoginActionBinder.swift @@ -0,0 +1,32 @@ +// +// LoginActionBinder.swift +// tmdb-rx-driver +// +// Created by Dmytro Shulzhenko on 24.09.2020. +// + +import Foundation + +final class LoginActionBinder: ViewControllerBinder { + private let driver: LoginDriving + unowned let viewController: LoginViewController + + init(viewController: LoginViewController, driver: LoginDriving) { + self.driver = driver + self.viewController = viewController + bind() + } + + func dispose() { } + + func bindLoaded() { + let userName = viewController.usernameTextField.rx.text.map({ $0 ?? "" }) + let password = viewController.passwordTextField.rx.text.map({ $0 ?? "" }) + + viewController.bag.insert( + userName.bind(onNext: set(unowned: self, to: \.driver.userName)), + password.bind(onNext: set(unowned: self, to: \.driver.password)), + viewController.loginButton.rx.tap.bind(onNext: driver.login) + ) + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Login/LoginDriver.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Login/LoginDriver.swift new file mode 100644 index 0000000..7f194c0 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Login/LoginDriver.swift @@ -0,0 +1,75 @@ +// +// LoginViewModel.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 27/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import RxSwift +import RxCocoa + +enum LoginViewState { + case success + case failure + case loading + case disabled + case enabled +} + +protocol LoginDriving: class { + var state: Driver { get } + var userName: String { get set } + var password: String { get set } + func login() +} + +final class LoginDriver: LoginDriving { + private let activityIndicator = ActivityIndicator() + private let stateRelay = PublishRelay() + + private let api: TMDBApiProvider + + let bag = DisposeBag() + + var state: Driver { stateRelay.asDriver() } + var userName: String = "" { didSet { validateCredentials() } } + var password: String = "" { didSet { validateCredentials() } } + + private var areCredentialsValid: Bool { + userName.count > 0 && password.count >= 4 + } + + init(api: TMDBApiProvider) { + self.api = api + bind() + } + + func login() { + api.login(withUsername: userName, password: password) + .trackActivity(activityIndicator) + .map({ $0 ? .success : .failure }) + .bind(onNext: stateRelay.accept) + .disposed(by: bag) + } + + private func bind() { + activityIndicator + .filter({ $0 }) + .map({ _ in LoginViewState.loading }) + .drive(onNext: stateRelay.accept) + .disposed(by: bag) + } + + private func validateCredentials() { + stateRelay.accept(areCredentialsValid ? .enabled : .disabled) + } +} + +extension LoginDriver: StaticFactory { + enum Factory { + static var `default`: LoginDriving { + LoginDriver(api: TMDBApi.Factory.default) + } + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Login/LoginStateBinder.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Login/LoginStateBinder.swift new file mode 100644 index 0000000..fb321bd --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Login/LoginStateBinder.swift @@ -0,0 +1,48 @@ +// +// LoginStateBinder.swift +// tmdb-rx-driver +// +// Created by Dmytro Shulzhenko on 24.09.2020. +// + +import UIKit + +final class LoginStateBinder: ViewControllerBinder { + private let driver: LoginDriving + unowned let viewController: LoginViewController + + init(viewController: LoginViewController, driver: LoginDriving) { + self.driver = driver + self.viewController = viewController + bind() + } + + func dispose() { } + + func bindLoaded() { + viewController.statusBarStyle = .lightContent + + viewController.bag.insert( + viewController.rx.viewWillAppear + .bind(onNext: unowned(self, in: LoginStateBinder.viewWillAppear)), + driver.state + .drive(onNext: unowned(self, in: LoginStateBinder.applyState)) + ) + } + + private func viewWillAppear(_ animated: Bool) { + viewController.navigationController?.setNavigationBarHidden(true, animated: animated) + } + + private func applyState(_ state: LoginViewState) { + let isLoading = state == .loading + UIApplication.shared.isNetworkActivityIndicatorVisible = isLoading + + let isEnabled = state != .disabled + + viewController.loginButton.isEnabled = isEnabled + viewController.loginButton.backgroundColor = isEnabled ? + UIColor(red: 255/255, green: 185/255, blue: 45/255, alpha: 1.0) : + UIColor.lightGray + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Login/LoginViewController.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Login/LoginViewController.swift new file mode 100644 index 0000000..e1c2bad --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Login/LoginViewController.swift @@ -0,0 +1,35 @@ +// +// LoginViewController.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 27/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import UIKit +import RxSwift +import RxCocoa + +final class LoginViewController: DisposeViewController { + @IBOutlet private (set) var usernameTextField: UITextField! + @IBOutlet private (set) var passwordTextField: UITextField! + @IBOutlet private (set) var loginButton: UIButton! +} + +extension LoginViewController: StaticFactory { + enum Factory { + static var `default`: LoginViewController { + let vc = R.storyboard.main.loginViewController()! + let driver = LoginDriver.Factory.default + let stateBinder = LoginStateBinder(viewController: vc, driver: driver) + let actionBinder = LoginActionBinder(viewController: vc, driver: driver) + + let toMainDriver = driver.state.compactMap({ $0 == .success ? () : nil }) + let navigationBinder = DismissBinder.Factory + .dismiss(viewController: vc, driver: toMainDriver) + + vc.bag.insert(stateBinder, actionBinder, navigationBinder) + return vc + } + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/MovieDetailActionBinder.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/MovieDetailActionBinder.swift new file mode 100644 index 0000000..29a5315 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/MovieDetailActionBinder.swift @@ -0,0 +1,29 @@ +// +// MovieDetailActionBinder.swift +// tmdb-rx-driver +// +// Created by Dmytro Shulzhenko on 22.09.2020. +// + +import Foundation + +final class MovieDetailActionBinder: ViewControllerBinder { + unowned let viewController: MovieDetailViewController + private let driver: MovieDetailDriving + + init(viewController: MovieDetailViewController, + driver: MovieDetailDriving) { + self.viewController = viewController + self.driver = driver + bind() + } + + func dispose() { } + + func bindLoaded() { + viewController.bag.insert( + viewController.backButton.rx.tap + .bind(onNext: driver.close) + ) + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/MovieDetailDriver.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/MovieDetailDriver.swift new file mode 100644 index 0000000..de198a9 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/MovieDetailDriver.swift @@ -0,0 +1,84 @@ +// +// MovieDetailDriver.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 28/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import RxSwift +import RxCocoa + +struct MovieDetailData { + let title: String + let releaseDate: String + let overview: String + let genres: String + let runtime: String + let voteAverage: String + let posterUrl: String? + let voteCount: String + let status: String +} + +extension MovieDetailData { + init(movie: Movie) { + self.title = movie.title + self.releaseDate = movie.releaseDate + self.overview = movie.overview + self.genres = movie.genres + .flatMap { + $0.map { $0.name } + .prefix(2) + .joined(separator: ", ") + } ?? "" + self.runtime = movie.runtime + .flatMap { "\($0 / 60)hr \($0 % 60)min" } ?? "" + self.voteAverage = movie.voteAverage + .flatMap { String($0) } ?? "" + self.posterUrl = movie.posterUrl.flatMap { "http://image.tmdb.org/t/p/w780/" + $0 } + self.voteCount = movie.voteCount + .flatMap { String($0) } ?? "" + self.status = movie.status ?? "" + } +} + +protocol MovieDetailDriving { + var data: Driver { get } + var didClose: Driver { get } + + func close() +} + +final class MovieDetailDriver: MovieDetailDriving { + private let closeRelay = PublishRelay() + + private let id: Int + private let api: TMDBApiProvider + + var data: Driver { + api.fetchMovieDetails(forMovieId: id) + .unwrap() + .compactMap(MovieDetailData.init) + .asDriver() + } + + var didClose: Driver { closeRelay.asDriver() } + + init(id: Int, api: TMDBApiProvider) { + self.id = id + self.api = api + } + + func close() { + closeRelay.accept(()) + } +} + +extension MovieDetailDriver: StaticFactory { + enum Factory { + static func `default`(id: Int) -> MovieDetailDriving { + MovieDetailDriver(id: id, api: TMDBApi.Factory.default) + } + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/MovieDetailStateBinder.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/MovieDetailStateBinder.swift new file mode 100644 index 0000000..a5ba4b3 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/MovieDetailStateBinder.swift @@ -0,0 +1,46 @@ +// +// MovieDetailStateBinder.swift +// tmdb-rx-driver +// +// Created by Dmytro Shulzhenko on 22.09.2020. +// + +import Foundation +import Nuke + +final class MovieDetailStateBinder: ViewControllerBinder { + unowned let viewController: MovieDetailViewController + private let driver: MovieDetailDriving + + init(viewController: MovieDetailViewController, + driver: MovieDetailDriving) { + self.viewController = viewController + self.driver = driver + bind() + } + + func dispose() { } + + func bindLoaded() { + viewController.statusBarStyle = .lightContent + + viewController.bag.insert( + viewController.rx.viewWillAppear + .bind(onNext: unowned(self, in: MovieDetailStateBinder.viewWillAppear)), + driver.data + .drive(onNext: unowned(self, in: MovieDetailStateBinder.configure)) + ) + } + + private func configure(_ data: MovieDetailData) { + viewController.headerView.configure(with: data) + viewController.tipsView.configure(with: data) + if let url = data.posterUrl { + Nuke.loadImage(with: URL(string: url)!, into: viewController.posterImageView) + } + } + + private func viewWillAppear(_ animated: Bool) { + viewController.navigationController?.setNavigationBarHidden(true, animated: animated) + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/MovieDetailViewController.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/MovieDetailViewController.swift new file mode 100644 index 0000000..59c8916 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/MovieDetailViewController.swift @@ -0,0 +1,36 @@ +// +// MovieDetailViewController.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 28/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import UIKit +import RxSwift + +final class MovieDetailViewController: DisposeViewController { + @IBOutlet private(set) var headerView: MovieDetailHeaderView! + @IBOutlet private(set) var tipsView: MovieDetailTipsView! + @IBOutlet private(set) var posterImageView: GradientImageView! + @IBOutlet private(set) var backButton: UIButton! +} + +extension MovieDetailViewController: StaticFactory { + enum Factory { + static func`default`(id: Int) -> MovieDetailViewController { + let vc = R.storyboard.main.movieDetailViewController()! + let driver = MovieDetailDriver.Factory.default(id: id) + let stateBinder = MovieDetailStateBinder(viewController: vc, driver: driver) + let actionBinder = MovieDetailActionBinder(viewController: vc, driver: driver) + let navigationBinder = NavigationPopBinder.Factory + .pop(viewController: vc, driver: driver.didClose) + vc.bag.insert( + stateBinder, + actionBinder, + navigationBinder + ) + return vc + } + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/Views/GradientImageView.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/Views/GradientImageView.swift new file mode 100644 index 0000000..e4db376 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/Views/GradientImageView.swift @@ -0,0 +1,32 @@ +// +// GradientImageView.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 30/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import UIKit + +final class GradientImageView: UIImageView { + override init(image: UIImage?) { + super.init(image: image) + setup() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + private func setup() { + layer.backgroundColor = UIColor.black.cgColor + layer.opacity = 0.1 + + let gradient = CAGradientLayer() + gradient.frame = bounds + gradient.colors = [UIColor.clear.cgColor, UIColor(red: 18/255, green: 18/255, blue: 18/255, alpha: 1.0).cgColor, UIColor(red: 18/255, green: 18/255, blue: 18/255, alpha: 1.0).cgColor, UIColor.clear.cgColor] + gradient.locations = [0, 0.1, 0.9, 1] + layer.mask = gradient + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/Views/MovieDetailHeaderView.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/Views/MovieDetailHeaderView.swift new file mode 100644 index 0000000..5c7e078 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/Views/MovieDetailHeaderView.swift @@ -0,0 +1,63 @@ +// +// MovieDetailHeaderView.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 28/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import UIKit + +final class MovieDetailHeaderView: UIView { + @IBOutlet private(set) var titleLabel: UILabel! + @IBOutlet private(set) var releaseDateLabel: UILabel! + @IBOutlet private(set) var genresLabel: UILabel! + @IBOutlet private(set) var runtimeLabel: UILabel! + @IBOutlet private(set) var voteAverageLabel: UILabel! + @IBOutlet private(set) var overviewLabel: UILabel! + @IBOutlet private(set) var contentView: UIView! + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + func configure(withTitle title: String, + releaseDate: String, + genres: String, + runtime: String, + voteAverage: String, + overview: String) { + titleLabel.text = title + releaseDateLabel.text = releaseDate + genresLabel.text = genres + runtimeLabel.text = runtime + voteAverageLabel.text = voteAverage + overviewLabel.text = overview + } + + private func setup() { + Bundle.main.loadNibNamed("MovieDetailHeaderView", owner: self, options: nil) + addSubview(contentView) + + contentView.frame = self.bounds + contentView.autoresizingMask = [.flexibleHeight, .flexibleWidth] + + } +} + +extension MovieDetailHeaderView { + func configure(with data: MovieDetailData) { + configure(withTitle: data.title, + releaseDate: data.releaseDate, + genres: data.genres, + runtime: data.runtime, + voteAverage: data.voteAverage, + overview: data.overview) + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/Views/MovieDetailHeaderView.xib b/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/Views/MovieDetailHeaderView.xib new file mode 100644 index 0000000..c6bd4d7 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/Views/MovieDetailHeaderView.xib @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/Views/MovieDetailTipsView.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/Views/MovieDetailTipsView.swift new file mode 100644 index 0000000..85023b4 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/Views/MovieDetailTipsView.swift @@ -0,0 +1,46 @@ +// +// MovieDetailTipsView.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 29/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import UIKit + +final class MovieDetailTipsView: UIView { + @IBOutlet private(set) var voteCountLabel: UILabel! + @IBOutlet private(set) var statusLabel: UILabel! + @IBOutlet private(set) var contentView: UIView! + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + setup() + } + + func configure(withVoteCount voteCount: String, + status: String) { + self.voteCountLabel.text = voteCount + self.statusLabel.text = status + } + + private func setup() { + Bundle.main.loadNibNamed("MovieDetailTipsView", owner: self, options: nil) + addSubview(contentView) + + contentView.frame = self.bounds + contentView.autoresizingMask = [.flexibleHeight, .flexibleWidth] + + } +} + +extension MovieDetailTipsView { + func configure(with data: MovieDetailData) { + configure(withVoteCount: data.voteCount, + status: data.status) + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/Views/MovieDetailTipsView.xib b/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/Views/MovieDetailTipsView.xib new file mode 100644 index 0000000..8341a47 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/MovieDetail/Views/MovieDetailTipsView.xib @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/Cell/SearchCell.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/Cell/SearchCell.swift new file mode 100644 index 0000000..1013379 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/Cell/SearchCell.swift @@ -0,0 +1,38 @@ +// +// SearchCell.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 30/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import UIKit +import Nuke + +class SearchCell: UITableViewCell { + @IBOutlet private(set) var titleImageView: UIImageView! + @IBOutlet private(set) var titleLabel: UILabel! + @IBOutlet private(set) var subtitleLabel: UILabel! + + override func awakeFromNib() { + selectionStyle = .none + } + + func configure(withImageUrl url: String?, + title: String, + subtitle: String) { + if let url = url { + Nuke.loadImage(with: URL(string: url)!, into: titleImageView) + } + titleLabel.text = title + subtitleLabel.text = subtitle + } +} + +extension SearchCell { + func configure(withSearchResultItem item: SearchResultItem) { + configure(withImageUrl: item.imageUrl, + title: item.title, + subtitle: item.subtitle) + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/Cell/SearchCell.xib b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/Cell/SearchCell.xib new file mode 100644 index 0000000..7255d44 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/Cell/SearchCell.xib @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/SearchActionBinder.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/SearchActionBinder.swift new file mode 100644 index 0000000..bfe2fd1 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/SearchActionBinder.swift @@ -0,0 +1,39 @@ +// +// SearchViewControllerActionBinder.swift +// tmdb-rx-driver +// +// Created by Dmytro Shulzhenko on 20.09.2020. +// + +import Foundation + +final class SearchActionBinder: ViewControllerBinder { + unowned let viewController: SearchViewController + private let driver: SearchDriving + + init(viewController: SearchViewController, + driver: SearchDriving) { + self.viewController = viewController + self.driver = driver + + bind() + } + + func dispose() { } + + func bindLoaded() { + let query = viewController.searchTextField.rx.text.orEmpty + let didSelectedCategory = viewController.segmentedControl.rx.value + .compactMap(SearchResultItemType.init) + let didSelectItem = viewController.tableView.rx.modelSelected(SearchResultItem.self) + + viewController.bag.insert( + query + .bind(onNext: driver.search), + didSelectedCategory + .bind(onNext: driver.selectCategory), + didSelectItem + .bind(onNext: driver.select) + ) + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/SearchDriver.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/SearchDriver.swift new file mode 100644 index 0000000..5c7082d --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/SearchDriver.swift @@ -0,0 +1,93 @@ +// +// SearchViewModel.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 29/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import RxSwift +import RxCocoa +import RxSwiftExt + +protocol SearchDriving { + var isSwitchHidden: Driver { get } + var isLoading: Driver { get } + var results: Driver<[SearchResultItem]> { get } + var didSelect: Driver { get } + + func search(_ query: String) + func selectCategory(_ category: SearchResultItemType) + func select(_ model: SearchResultItem) +} + +final class SearchDriver: SearchDriving { + private let activityIndicator = ActivityIndicator() + + private let isSwitchHiddenRelay = BehaviorRelay(value: nil) + private let resultsRelay = BehaviorRelay<[SearchResultItem]?>(value: nil) + private let didSelectRelay = BehaviorRelay(value: nil) + + private var searchBag = DisposeBag() + private var selectedCategory: SearchResultItemType = .movies + + private let api: TMDBApiProvider + + var isSwitchHidden: Driver { isSwitchHiddenRelay.unwrap().asDriver() } + var isLoading: Driver { activityIndicator.asDriver() } + var results: Driver<[SearchResultItem]> { resultsRelay.unwrap().asDriver() } + var didSelect: Driver { didSelectRelay.unwrap().asDriver() } + + init(api: TMDBApiProvider) { + self.api = api + } + + func search(_ query: String) { + searchBag = DisposeBag() + + let isValid = query.count >= 3 + + isSwitchHiddenRelay.accept(isValid) + + guard isValid else { + resultsRelay.accept([]) + return + } + + let searchResult: Observable<[SearchResultItem]> + + switch selectedCategory { + case .movies: + searchResult = api.searchMovies(forQuery: query) + .map({ $0 ?? [] }) + .mapMany(SearchResultItem.init) + + case .people: + searchResult = api.searchPeople(forQuery: query) + .map({ $0 ?? [] }) + .mapMany(SearchResultItem.init) + } + + searchResult + .trackActivity(activityIndicator) + .throttle(.milliseconds(500), scheduler: MainScheduler.instance) + .bind(onNext: resultsRelay.accept) + .disposed(by: searchBag) + } + + func selectCategory(_ category: SearchResultItemType) { + selectedCategory = category + } + + func select(_ model: SearchResultItem) { + didSelectRelay.accept(model) + } +} + +extension SearchDriver: StaticFactory { + enum Factory { + static var `default`: SearchDriving { + SearchDriver(api: TMDBApi.Factory.default) + } + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/SearchResultItemViewModel.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/SearchResultItemViewModel.swift new file mode 100644 index 0000000..9c32fd0 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/SearchResultItemViewModel.swift @@ -0,0 +1,38 @@ +// +// SearchResultItemViewModel.swift +// tmdb-rx-driver +// +// Created by Dmytro Shulzhenko on 20.09.2020. +// + +import Foundation + +enum SearchResultItemType: Int { + case movies, people +} + +struct SearchResultItem { + let id: Int + let title: String + let subtitle: String + let imageUrl: String? + let type: SearchResultItemType +} + +extension SearchResultItem { + init(movie: Movie) { + self.id = movie.id + self.title = movie.title + self.subtitle = movie.overview + self.imageUrl = movie.posterUrl.flatMap { "http://image.tmdb.org/t/p/w185/" + $0 } + self.type = .movies + } + + init(person: Person) { + self.id = person.id + self.title = person.name + self.subtitle = person.knownForTitles?.first ?? " " + self.imageUrl = person.profileUrl.flatMap { "http://image.tmdb.org/t/p/w185/" + $0 } + self.type = .people + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/SearchStateBinder.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/SearchStateBinder.swift new file mode 100644 index 0000000..217460e --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/SearchStateBinder.swift @@ -0,0 +1,87 @@ +// +// SearchViewControllerBinder.swift +// tmdb-rx-driver +// +// Created by Dmytro Shulzhenko on 20.09.2020. +// + +import Foundation +import RxSwift +import RxCocoa +import RxDataSources + +final class SearchStateBinder: ViewControllerBinder { + typealias Item = SearchResultItem + + struct Section: SectionModelType { + let items: [Item] + + init(items: [Item]) { + self.items = items + } + + init(original: Self, items: [Item]) { + self.items = items + } + } + + unowned let viewController: SearchViewController + private let driver: SearchDriving + private let dataSource: RxTableViewSectionedReloadDataSource
+ private let cell = R.nib.searchCell + + init(viewController: SearchViewController, + driver: SearchDriving, + dataSource: RxTableViewSectionedReloadDataSource
) { + self.viewController = viewController + self.driver = driver + self.dataSource = dataSource + + bind() + } + + func dispose() { } + + func bindLoaded() { + viewController.statusBarStyle = .lightContent + viewController.tableView.register(cell) + + let section = driver.results.map(Section.init).map({ [$0] }) + + viewController.bag.insert( + viewController.rx.viewWillAppear + .bind(onNext: unowned(self, in: SearchStateBinder.viewWillAppear)), + driver.isSwitchHidden + .drive(viewController.segmentedControl.rx.isHidden), + driver.isLoading + .drive(UIApplication.shared.rx.isNetworkActivityIndicatorVisible), + section.drive(viewController.tableView.rx.items(dataSource: dataSource)) + ) + } + + private func viewWillAppear(_ animated: Bool) { + viewController.navigationController?.setNavigationBarHidden(true, animated: animated) + } +} + +extension SearchStateBinder: StaticFactory { + enum Factory { + static func `default`(_ viewController: SearchViewController, + driver: SearchDriving) -> SearchStateBinder { + let dataSource = RxTableViewSectionedReloadDataSource(configureCell: cellFactory) + return SearchStateBinder(viewController: viewController, + driver: driver, + dataSource: dataSource) + } + } + + private static func cellFactory(_: TableViewSectionedDataSource
, + tableView: UITableView, + indexPath: IndexPath, + item: Section.Item) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: R.reuseIdentifier.searchCell, + for: indexPath)! + cell.configure(withSearchResultItem: item) + return cell + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/SearchViewController.swift b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/SearchViewController.swift new file mode 100644 index 0000000..2705f41 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Scenes/Search/SearchViewController.swift @@ -0,0 +1,43 @@ +// +// SearchViewController.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 29/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import UIKit +import RxSwift + +final class SearchViewController: DisposeViewController { + @IBOutlet private(set) var searchTextField: UITextField! + @IBOutlet private(set) var segmentedControl: UISegmentedControl! + @IBOutlet private(set) var tableView: UITableView! +} + +extension SearchViewController: StaticFactory { + enum Factory { + static var `default`: SearchViewController { + let vc = R.storyboard.main.searchViewController()! + let driver = SearchDriver.Factory.default + let stateBinder = SearchStateBinder.Factory + .default(vc, driver: driver) + let actionBinder = SearchActionBinder(viewController: vc, + driver: driver) + let navigationBinder = NavigationPushBinder.Factory + .push(viewController: vc, + driver: driver.didSelect, + factory: detailViewControllerFactory) + vc.bag.insert( + stateBinder, + actionBinder, + navigationBinder + ) + return vc + } + + private static func detailViewControllerFactory(_ item: SearchResultItem) -> UIViewController { + MovieDetailViewController.Factory.default(id: item.id) + } + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Storyboard/Base.lproj/Main.storyboard b/tmdb-rx-driver/tmdb-rx-driver/Storyboard/Base.lproj/Main.storyboard new file mode 100644 index 0000000..38a20c7 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Storyboard/Base.lproj/Main.storyboard @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Welcome +Back. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/ActivityIndicator.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/ActivityIndicator.swift new file mode 100644 index 0000000..e9638bd --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/ActivityIndicator.swift @@ -0,0 +1,76 @@ +// +// ActivityIndicator.swift +// RxExample +// +// Created by Krunoslav Zaher on 10/18/15. +// Copyright © 2015 Krunoslav Zaher. All rights reserved. +// +import RxSwift +import RxCocoa + +private struct ActivityToken : ObservableConvertibleType, Disposable { + private let _source: Observable + private let _dispose: Cancelable + + init(source: Observable, disposeAction: @escaping () -> ()) { + _source = source + _dispose = Disposables.create(with: disposeAction) + } + + func dispose() { + _dispose.dispose() + } + + func asObservable() -> Observable { + return _source + } +} + +/** + Enables monitoring of sequence computation. + If there is at least one sequence computation in progress, `true` will be sent. + When all activities complete `false` will be sent. + */ +public class ActivityIndicator : SharedSequenceConvertibleType { + public typealias Element = Bool + public typealias SharingStrategy = DriverSharingStrategy + + private let _lock = NSRecursiveLock() + private let _relay = BehaviorRelay(value: 0) + private let _loading: SharedSequence + + public init() { + _loading = _relay.asDriver() + .map { $0 > 0 } + .distinctUntilChanged() + } + + fileprivate func trackActivityOfObservable(_ source: O) -> Observable { + return Observable.using({ () -> ActivityToken in + self.increment() + return ActivityToken(source: source.asObservable(), disposeAction: self.decrement) + }) { $0.asObservable() } + } + + private func increment() { + _lock.lock() + _relay.accept(_relay.value + 1) + _lock.unlock() + } + + private func decrement() { + _lock.lock() + _relay.accept(_relay.value - 1) + _lock.unlock() + } + + public func asSharedSequence() -> SharedSequence { + _loading + } +} + +extension ObservableConvertibleType { + public func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable { + activityIndicator.trackActivityOfObservable(self) + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/Binder/DisposeViewController.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/Binder/DisposeViewController.swift new file mode 100644 index 0000000..de78c19 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/Binder/DisposeViewController.swift @@ -0,0 +1,56 @@ +// +// DisposeViewController.swift +// Headway +// +// Created by Eugene Morozov on 11/5/19. +// Copyright © 2019 Headway. All rights reserved. +// + +import UIKit +import RxSwift + +protocol DisposeContainer { + var bag: DisposeBag { get } +} + +class DisposeViewController: UIViewController, DisposeContainer { + let bag = DisposeBag() + + static var defaultStatusBarStyle: UIStatusBarStyle { + if #available(iOS 13.0, *) { + return .darkContent + } else { + return .default + } + } + + var statusBarStyle = defaultStatusBarStyle { + didSet { + setNeedsStatusBarAppearanceUpdate() + } + } + var statusBarUpdateAnimation: UIStatusBarAnimation = .none { + didSet { + setNeedsStatusBarAppearanceUpdate() + } + } + var isStatusBarHidden: Bool = false { + didSet { + setNeedsStatusBarAppearanceUpdate() + } + } + + // MARK: - Override + + override var prefersStatusBarHidden: Bool { + isStatusBarHidden + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + statusBarStyle + } + + override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { + statusBarUpdateAnimation + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/Binder/NavigationBinder.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/Binder/NavigationBinder.swift new file mode 100644 index 0000000..a9267bb --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/Binder/NavigationBinder.swift @@ -0,0 +1,129 @@ +// +// NavigationBinder.swift +// Headway +// +// Created by Dmytro Shulzhenko on 18.06.2020. +// Copyright © 2020 Headway. All rights reserved. +// + +import UIKit +import RxSwift +import RxCocoa + +final class NavigationBinder: ViewControllerBinder + where Transition: Transitioning, + Transition.Prop == Prop, + ViewController: UIViewController, +ViewController: DisposeContainer { + + unowned let viewController: ViewController + private let transition: Transition + private let driver: Driver + + init(viewController: ViewController, + transition: Transition, + driver: Driver) { + self.viewController = viewController + self.transition = transition + self.driver = driver + + bind() + } + + func dispose() { } + + func bindLoaded() { + driver + .drive(onNext: transition.perform) + .disposed(by: viewController.bag) + } +} + +typealias NavigationPushBinder = NavigationBinder, ViewController> where ViewController: UIViewController, ViewController: DisposeContainer + +typealias NavigationPopBinder = NavigationBinder where ViewController: UIViewController, ViewController: DisposeContainer + +typealias DismissBinder = NavigationBinder where ViewController: UIViewController, ViewController: DisposeContainer + +extension NavigationBinder: StaticFactory { + typealias ViewControllerFactory = (Prop) -> UIViewController? + + enum Factory { + static func present(viewController: ViewController, + driver: Driver, + animated: Bool = true, + sourceViewFactory: (() -> UIView)? = nil, + factory: @escaping ViewControllerFactory) -> NavigationBinder, ViewController> { + let transition = PresentTransition(isAnimated: animated, + viewController: viewController, + sourceViewFactory: sourceViewFactory, + presentedFactory: factory) + return NavigationBinder, ViewController>(viewController: viewController, + transition: transition, + driver: driver) + } + + static func push(viewController: ViewController, + driver: Driver, + animated: Bool = true, + factory: @escaping ViewControllerFactory) -> NavigationPushBinder { + let transition = NavigationPushTransition(isAnimated: animated, + viewController: viewController, + presentedFactory: factory) + return NavigationPushBinder(viewController: viewController, + transition: transition, + driver: driver) + } + + static func dismiss(viewController: ViewController, + driver: Driver, + animated: Bool = true) -> DismissBinder { + let transition = DismissTransition(isAnimated: animated, viewController: viewController) + return DismissBinder(viewController: viewController, + transition: transition, + driver: driver) + } + + static func pop(viewController: ViewController, + driver: Driver, + animated: Bool = true) -> NavigationPopBinder { + let transition = NavigationPopTransition(isAnimated: animated, viewController: viewController) + return NavigationPopBinder(viewController: viewController, + transition: transition, + driver: driver) + } +// +// static func bindPush(on container: UINavigationController & DisposeContainer, +// driver: Driver, +// animated: Bool = true, +// factory: @escaping ViewControllerFactory) { +// let transition = NavigationPushTransition( +// isAnimated: animated, +// navigationController: container, +// presentedFactory: factory +// ) +// bind(driver: driver, transition: transition, bag: container.bag) +// container.bag.insert(transition) +// } +// +// static func bindDismiss(on container: UIViewController & DisposeContainer, +// driver: Driver, +// animated: Bool = true) { +// let transition = DismissTransition(isAnimated: animated, +// viewController: container) +// bind(driver: driver, transition: transition, bag: container.bag) +// container.bag.insert(transition) +// } +// +// static func bindPop(on container: UINavigationController & DisposeContainer, +// driver: Driver, +// animated: Bool = true) { +// let transition = NavigationPopTransition( +// isAnimated: animated, +// navigationController: container +// ) +// bind(driver: driver, transition: transition, bag: container.bag) +// container.bag.insert(transition) +// } + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/Binder/Transitioning.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/Binder/Transitioning.swift new file mode 100644 index 0000000..5be548d --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/Binder/Transitioning.swift @@ -0,0 +1,94 @@ +// +// Transitioning.swift +// Headway +// +// Created by Dmytro Shulzhenko on 18.06.2020. +// Copyright © 2020 Headway. All rights reserved. +// + +import UIKit +import RxSwift + +protocol Transitioning { + associatedtype Prop + func perform(prop: Prop) +} + +final class PresentTransition: Disposable, Transitioning { + private let isAnimated: Bool + private unowned let viewController: UIViewController + private let sourceViewFactory: (() -> UIView)? + private let presentedFactory: (Prop) -> UIViewController? + + init(isAnimated: Bool, + viewController: UIViewController, + sourceViewFactory: (() -> UIView)?, + presentedFactory: @escaping (Prop) -> UIViewController?) { + self.isAnimated = isAnimated + self.viewController = viewController + self.sourceViewFactory = sourceViewFactory + self.presentedFactory = presentedFactory + } + + func dispose() { } + + func perform(prop: Prop) { + guard let presented = presentedFactory(prop) else { return } + presented.popoverPresentationController?.sourceView = sourceViewFactory?() + viewController.present(presented, animated: isAnimated) + } +} + +final class DismissTransition: Disposable, Transitioning { + private let isAnimated: Bool + private unowned let viewController: UIViewController + + init(isAnimated: Bool, viewController: UIViewController) { + self.isAnimated = isAnimated + self.viewController = viewController + } + + func dispose() { } + + func perform(prop: Void) { + viewController.dismiss(animated: isAnimated) + } +} + +final class NavigationPushTransition: Disposable, Transitioning { + private let isAnimated: Bool + private unowned let viewController: UIViewController + private let presentedFactory: (Prop) -> UIViewController? + + init(isAnimated: Bool, + viewController: UIViewController, + presentedFactory: @escaping (Prop) -> UIViewController?) { + self.isAnimated = isAnimated + self.viewController = viewController + self.presentedFactory = presentedFactory + } + + func dispose() { } + + func perform(prop: Prop) { + guard let presented = presentedFactory(prop) else { return } + viewController.navigationController? + .pushViewController(presented, animated: isAnimated) + } +} + +final class NavigationPopTransition: Disposable, Transitioning { + private let isAnimated: Bool + private unowned let viewController: UIViewController + + init(isAnimated: Bool, viewController: UIViewController) { + self.isAnimated = isAnimated + self.viewController = viewController + } + + func dispose() { } + + func perform(prop: Void) { + viewController.navigationController?.popViewController(animated: true) + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/Binder/ViewControllerBinder.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/Binder/ViewControllerBinder.swift new file mode 100644 index 0000000..a11b599 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/Binder/ViewControllerBinder.swift @@ -0,0 +1,46 @@ +// +// ViewBinder.swift +// Headway +// +// Created by Dmytro Shulzhenko on 5/31/19. +// Copyright © 2019 Dmitriy. All rights reserved. +// + +import Foundation +import RxSwift +import RxViewController + +protocol Binding { + func bind() +} + +// MARK: - ViewControllerBinder + +// swiftlint:disable view_binding_before_did_load + +protocol ViewControllerBinder: Disposable { + associatedtype DisposeViewControllerContainer: UIViewController, DisposeContainer + + var viewController: DisposeViewControllerContainer { get } + + func bindLoaded() +} + +extension ViewControllerBinder { + var bag: DisposeBag { + viewController.bag + } +} + +extension ViewControllerBinder where Self: AnyObject { + func bind() { + viewController.rx.viewDidLoad + .subscribe(onNext: unowned(self, in: Self.bindLoaded)) + .disposed(by: viewController.bag) + } + + var binded: Self { + bind() + return self + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/Closure/Closure.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/Closure/Closure.swift new file mode 100644 index 0000000..01e8179 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/Closure/Closure.swift @@ -0,0 +1,35 @@ +// +// Closure.swift +// Headway +// +// Created by Dmytro Shulzhenko on 3/6/19. +// Copyright © 2019 Dmitriy. All rights reserved. +// + +import Foundation + +struct Closure { + let closure: (Inputs) -> Outputs + + init(_ closure: @escaping (Inputs) -> Outputs) { + self.closure = closure + } +} + +extension Closure { + func execute(_ inputs: Inputs) -> Outputs { + return closure(inputs) + } +} + +extension Closure { + func map(_ transform: @escaping (T) -> Inputs) -> Closure { + return Closure { self.execute(transform($0)) } + } +} + +extension Closure { + func filter(_ predicate: @escaping (Inputs) -> Bool) -> Closure { + return Closure { predicate($0) ? self.execute($0) : nil } + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/Closure/RawClosure.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/Closure/RawClosure.swift new file mode 100644 index 0000000..df18a8a --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/Closure/RawClosure.swift @@ -0,0 +1,30 @@ +// +// RawClosure.swift +// Headway +// +// Created by Dmytro Shulzhenko on 3/21/19. +// Copyright © 2019 Dmitriy. All rights reserved. +// + +import Foundation + +func + (lhs: @escaping (T) -> Void, rhs: @escaping (T) -> Void) -> (T) -> Void { + return { prop in + lhs(prop) + rhs(prop) + } +} + +func + (lhs: @escaping () -> Void, rhs: @escaping () -> Void) -> () -> Void { + return { + lhs() + rhs() + } +} + +func + (lhs: @escaping (A, B) -> Void, rhs: @escaping () -> Void) -> (A, B) -> Void { + return { + lhs($0, $1) + rhs() + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/Closure/Set.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/Closure/Set.swift new file mode 100644 index 0000000..4b58fd8 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/Closure/Set.swift @@ -0,0 +1,141 @@ +// +// Set.swift +// Headway +// +// Created by Dmytro Shulzhenko on 3/6/19. +// Copyright © 2019 Dmitriy. All rights reserved. +// + +import Foundation + +// MARK: Set Closure inputs to Keypath + +func set(weak element: Element, + to path: ReferenceWritableKeyPath) + -> Closure { + return Closure { [weak element] in element?[keyPath: path] = $0 } +} + +func set(unowned element: Element, + to path: ReferenceWritableKeyPath) + -> Closure { + return Closure { [unowned element] in element[keyPath: path] = $0 } +} + +func set(unowned element: Element, + to path: ReferenceWritableKeyPath) + -> (Inputs) -> Void { + return { [unowned element] in element[keyPath: path] = $0 } +} + +func set(capture element: Element, + to path: ReferenceWritableKeyPath) + -> Closure { + return Closure { element[keyPath: path] = $0 } +} + +// MARK: Optional value in setter + +func set(weak element: Element, + to path: ReferenceWritableKeyPath) + -> Closure { + return Closure { [weak element] in element?[keyPath: path] = $0 } +} + +func set(unowned element: Element, + to path: ReferenceWritableKeyPath) + -> Closure { + return Closure { [unowned element] in element[keyPath: path] = $0 } +} + +func set(unowned element: Element, + to path: ReferenceWritableKeyPath) + -> (Inputs) -> Void { + return { [unowned element] in element[keyPath: path] = $0 } +} + +func set(capture element: Element, + to path: ReferenceWritableKeyPath) + -> Closure { + return Closure { element[keyPath: path] = $0 } +} + +// MARK: Optional element + +func set(weak element: Element?, + to path: ReferenceWritableKeyPath) + -> Closure { + return Closure { [weak element] in element?[keyPath: path] = $0 } +} + +func set(unowned element: Element?, + to path: ReferenceWritableKeyPath) + -> Closure { + guard let element = element else { return Closure { _ in } } + return Closure { [unowned element] in element[keyPath: path] = $0 } +} + +func set(capture element: Element?, + to path: ReferenceWritableKeyPath) + -> Closure { + guard let element = element else { return Closure { _ in } } + return Closure { element[keyPath: path] = $0 } +} + +// MARK: Optional element and value + +func set(weak element: Element?, + to path: ReferenceWritableKeyPath) + -> Closure { + return Closure { [weak element] in element?[keyPath: path] = $0 } +} + +func set(unowned element: Element?, + to path: ReferenceWritableKeyPath) + -> Closure { + guard let element = element else { return Closure { _ in } } + return Closure { [unowned element] in element[keyPath: path] = $0 } +} + +func set(capture element: Element?, + to path: ReferenceWritableKeyPath) + -> Closure { + guard let element = element else { return Closure { _ in } } + return Closure { element[keyPath: path] = $0 } +} + +// MARK: Set on queue + +func setOnMain + (unowned element: Element, to path: ReferenceWritableKeyPath) + -> Closure { + return setOn(queue: .main, + check: !Thread.isMainThread, + unowned: element, + to: path) +} + +func setOn(queue: DispatchQueue, + check: @autoclosure @escaping () -> Bool = true, + unowned element: Element, + to path: ReferenceWritableKeyPath) + -> Closure { + return Closure { [unowned element] value in + if check() { + queue.async { + element[keyPath: path] = value + } + } else { + element[keyPath: path] = value + } + } +} + +// + +func get( + unowned element: Element, + from path: KeyPath +) -> () -> Outputs { + return { [unowned element] in element[keyPath: path] } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/Closure/Unown.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/Closure/Unown.swift new file mode 100644 index 0000000..853c28a --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/Closure/Unown.swift @@ -0,0 +1,273 @@ +// http://sveinhal.github.io/2016/03/16/retain-cycles-function-references/ + +//swiftlint:disable identifier_name +import Foundation + +func unowned(_ type: Type, + in block: @escaping (Type) -> R) -> () -> R { + return { [unowned type] in block(type) } +} + +func unowned(_ type: Type, + in block: @escaping (Type) throws -> R) -> () throws -> R { + return { [unowned type] in try block(type) } +} + +/// + +func unowned(_ type: Type, + in block: @escaping (Type) -> (() -> Void), + file: StaticString = #file, + line: Int = #line) + -> () -> Void { + return { [weak type] in + guard let strong = type else { + fatalError("\(file), \(line), \(String.init(describing: Type.self))") + } + let instanceFunction = block(strong) + return instanceFunction() + } +} + +func unowned(_ type: Type, + in block: @escaping (Type) -> ((Arg) -> ReturnType)) + -> (Arg) -> ReturnType { + return { [weak type] arg in + guard let type = type else { + fatalError("\(String(describing: Type.self))") + } + let instanceFunction = block(type) + return instanceFunction(arg) + } +} + +func unowned(_ type: Type, + in block: @escaping (Type) -> ((Arg1, Arg2) -> ReturnType)) + -> (Arg1, Arg2) -> ReturnType { + return { [unowned type] arg1, arg2 in + let instanceFunction = block(type) + return instanceFunction(arg1, arg2) + } +} + +func unowned(_ type: Type, + in block: @escaping (Type) -> ((Arg1, Arg2, Arg3) -> ReturnType)) + -> (Arg1, Arg2, Arg3) -> ReturnType { + return { [unowned type] arg1, arg2, arg3 in + let instanceFunction = block(type) + return instanceFunction(arg1, arg2, arg3) + } +} + +func unowned(_ type: Type, + in block: @escaping (Type) -> ((Arg1, Arg2, Arg3) throws -> ReturnType)) + -> (Arg1, Arg2, Arg3) throws -> ReturnType { + return { [unowned type] arg1, arg2, arg3 in + let instanceFunction = block(type) + return try instanceFunction(arg1, arg2, arg3) + } +} + +func unowned(_ type: Type, + in block: @escaping (Type) -> ((Arg1, Arg2, Arg3, Arg4) -> ReturnType)) + -> (Arg1, Arg2, Arg3, Arg4) -> ReturnType { + return { [unowned type] arg1, arg2, arg3, arg4 in + let instanceFunction = block(type) + return instanceFunction(arg1, arg2, arg3, arg4) + } +} + +func unowned(_ type: Type, + in block: @escaping (Type) -> ((Arg1, Arg2, Arg3, Arg4, Arg5) -> ReturnType)) + -> (Arg1, Arg2, Arg3, Arg4, Arg5) -> ReturnType { + return { [unowned type] arg1, arg2, arg3, arg4, arg5 in + let instanceFunction = block(type) + return instanceFunction(arg1, arg2, arg3, arg4, arg5) + } +} + +func unowned(_ type: Type, + in block: @escaping (Type) -> ((A, B, C, D, E, F) -> ReturnType)) + -> (A, B, C, D, E, F) -> ReturnType { + return { [unowned type] a, b, c, d, e, f in + let instanceFunction = block(type) + return instanceFunction(a, b, c, d, e, f) + } +} + +// MARK: Weak + +// Void as return + +// Type + +func weak(_ type: Type?, + in block: @escaping (Type) -> Void) -> () -> Void { + return { + guard let strong = type else { + return + } + block(strong) + } +} + +func weak(_ type: Type?, + in block: @escaping (Type, Arg) -> Void) -> (Arg) -> Void { + return { arg in + guard let strong = type else { + return + } + block(strong, arg) + } +} + +func weak(_ type: Type?, + in block: @escaping (Type, Arg1, Arg2) -> Void) -> (Arg1, Arg2) -> Void { + return { arg1, arg2 in + guard let strong = type else { + return + } + block(strong, arg1, arg2) + } +} + +func weak(_ type: Type?, + in block: @escaping (Type, Arg1, Arg2, Arg3) -> Void) -> (Arg1, Arg2, Arg3) -> Void { + return { arg1, arg2, arg3 in + guard let strong = type else { + return + } + block(strong, arg1, arg2, arg3) + } +} + +func weak(_ type: Type?, + in block: @escaping (Type, Arg1, Arg2, Arg3, Arg4) -> Void) -> (Arg1, Arg2, Arg3, Arg4) -> Void { + return { arg1, arg2, arg3, arg4 in + guard let strong = type else { + return + } + block(strong, arg1, arg2, arg3, arg4) + } +} + +// Method + +func weak(_ type: Type?, + in block: @escaping (Type) -> (() -> Void)) + -> () -> Void { + return { [weak type] in + guard let type = type else { + log(.fatal("\(String(describing: Type.self))")) + return + } + let instanceFunction = block(type) + return instanceFunction() + } +} + +func weak(_ type: Type?, + in block: @escaping (Type) -> ((Arg) -> Void)) + -> (Arg) -> Void { + return { [weak type] arg in + guard let type = type else { + log(.fatal("\(String(describing: Type.self))")) + return + } + let instanceFunction = block(type) + return instanceFunction(arg) + } +} + +func weak(_ type: Type?, + in block: @escaping (Type) -> ((Arg1, Arg2) -> Void)) + -> (Arg1, Arg2) -> Void { + return { [weak type] arg1, arg2 in + guard let type = type else { + log(.fatal("\(String(describing: Type.self))")) + return + } + let instanceFunction = block(type) + return instanceFunction(arg1, arg2) + } +} + +func weak(_ type: Type?, + in block: @escaping (Type) -> ((Arg1, Arg2, Arg3) -> Void)) + -> (Arg1, Arg2, Arg3) -> Void { + return { [weak type] arg1, arg2, arg3 in + guard let type = type else { + log(.fatal("\(String(describing: Type.self))")) + return + } + let instanceFunction = block(type) + return instanceFunction(arg1, arg2, arg3) + } +} + +// + +func weak(_ type: Type?, + in block: @escaping (Type) -> ((Arg) -> ReturnType?)) + -> (Arg) -> ReturnType? { + return { [weak type] arg in + guard let type = type else { + log(.fatal("\(String(describing: Type.self))")) + return nil + } + let instanceFunction = block(type) + return instanceFunction(arg) + } +} + +func weak(_ type: Type?, + in block: @escaping (Type) -> ((Arg1, Arg2) -> ReturnType?)) + -> (Arg1, Arg2) -> ReturnType? { + return { [weak type] arg1, arg2 in + guard let type = type else { + log(.fatal("\(String(describing: Type.self))")) + return nil + } + let instanceFunction = block(type) + return instanceFunction(arg1, arg2) + } +} + +func weak(_ type: Type?, + in block: @escaping (Type) -> ((Arg1, Arg2, Arg3) -> ReturnType?)) + -> (Arg1, Arg2, Arg3) -> ReturnType? { + return { [weak type] arg1, arg2, arg3 in + guard let type = type else { + log(.fatal("\(String(describing: Type.self))")) + return nil + } + let instanceFunction = block(type) + return instanceFunction(arg1, arg2, arg3) + } +} + +func weak(_ type: Type?, + in block: @escaping (Type) -> ((Arg1, Arg2, Arg3, Arg4) -> ReturnType?)) + -> (Arg1, Arg2, Arg3, Arg4) -> ReturnType? { + return { [weak type] arg1, arg2, arg3, arg4 in + guard let type = type else { + log(.fatal("\(String(describing: Type.self))")) + return nil + } + let instanceFunction = block(type) + return instanceFunction(arg1, arg2, arg3, arg4) + } +} + +func weak(_ type: Type?, + in block: @escaping (Type) -> ((Arg1, Arg2, Arg3, Arg4, Arg5) -> ReturnType?)) + -> (Arg1, Arg2, Arg3, Arg4, Arg5) -> ReturnType? { + return { [weak type] arg1, arg2, arg3, arg4, arg5 in + guard let type = type else { + log(.fatal("\(String(describing: Type.self))")) + return nil + } + let instanceFunction = block(type) + return instanceFunction(arg1, arg2, arg3, arg4, arg5) + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/DefaultInitializable/DefaultInitializable.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/DefaultInitializable/DefaultInitializable.swift new file mode 100644 index 0000000..95b32a2 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/DefaultInitializable/DefaultInitializable.swift @@ -0,0 +1,27 @@ +// +// DefaultInitializable.swift +// Headway +// +// Created by Dmitriy on 2/19/19. +// Copyright © 2019 Dmitriy. All rights reserved. +// + +import Foundation + +protocol DefaultInitializable { + associatedtype ReturnType + static var `default`: ReturnType { get } +} + +protocol Mockable { + associatedtype MockReturnType + static var mock: MockReturnType { get } +} + +protocol StaticFactory { + associatedtype Factory +} + +extension StaticFactory { + static var factory: Factory.Type { return Factory.self } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/Log/Level.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/Log/Level.swift new file mode 100644 index 0000000..2de4917 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/Log/Level.swift @@ -0,0 +1,25 @@ +// +// Level.swift +// Log +// +// Created by Dmytro Shulzhenko on 4/25/19. +// Copyright © 2019 Dmytro Shulzhenko. All rights reserved. +// + +import Foundation + +struct Level: Equatable { + let description: String + + init(description: String) { + self.description = description + } +} + +extension Level { + static var debug: Level { return Level(description: "♡ DEBUG:") } + static var info: Level { return Level(description: "💚 INFO:") } + static var warning: Level { return Level(description: "💛 WARNING:") } + static var error: Level { return Level(description: "💔 ERROR:") } + static var fatal: Level { return Level(description: "‼️ FATAL ‼️:") } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/Log/Log.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/Log/Log.swift new file mode 100644 index 0000000..ea4bc96 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/Log/Log.swift @@ -0,0 +1,11 @@ +// +// Log.swift +// Headway +// +// Created by Dmitriy on 2/19/19. +// Copyright © 2019 Dmitriy. All rights reserved. +// + +import Foundation + +var log = Logging.console.log diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/Log/Logging.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/Log/Logging.swift new file mode 100644 index 0000000..e467e64 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/Log/Logging.swift @@ -0,0 +1,45 @@ +// +// Logging.swift +// Log +// +// Created by Dmytro Shulzhenko on 4/25/19. +// Copyright © 2019 Dmytro Shulzhenko. All rights reserved. +// + +import Foundation + +struct Logging { + let log: (Params) -> Void +} + +extension Logging { + static let formatFilename: (StaticString) -> String = { + String($0).components(separatedBy: "/").last ?? "" + } + + static var console: Logging { + return Logging { params in + #if DEBUG + print( + Date(), + params.level.description, + formatFilename(params.filename), + params.function, + params.line, + "\(params.message())" + ) + if params.level == Level.fatal { + assertionFailure("\(params.filename) \(params.function) \(params.line) \(params.message())") + } + #endif + } + } +} + +extension String { + init(_ staticString: StaticString) { + self = staticString.withUTF8Buffer { + String(decoding: $0, as: UTF8.self) + } + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/Log/Params.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/Log/Params.swift new file mode 100644 index 0000000..d026e12 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/Log/Params.swift @@ -0,0 +1,86 @@ +// +// Params.swift +// Log +// +// Created by Dmytro Shulzhenko on 4/25/19. +// Copyright © 2019 Dmytro Shulzhenko. All rights reserved. +// + +import Foundation + +struct Params { + let level: Level + let filename: StaticString + let function: StaticString + let line: UInt + let message: () -> Any + + init(level: Level, + filename: StaticString, + function: StaticString, + line: UInt, + message: @escaping () -> Any) { + self.level = level + self.filename = filename + self.function = function + self.line = line + self.message = message + } +} + +extension Params { + static func debug(_ message: @escaping @autoclosure () -> Any = "", + _ file: StaticString = #file, + _ function: StaticString = #function, + _ line: UInt = #line) -> Params { + return Params(level: .debug, + filename: file, + function: function, + line: line, + message: message) + } + + static func info(_ message: @escaping @autoclosure () -> Any = "", + _ file: StaticString = #file, + _ function: StaticString = #function, + _ line: UInt = #line) -> Params { + return Params(level: .info, + filename: file, + function: function, + line: line, + message: message) + } + + static func warning(_ message: @escaping @autoclosure () -> Any = "", + _ file: StaticString = #file, + _ function: StaticString = #function, + _ line: UInt = #line) -> Params { + return Params(level: .warning, + filename: file, + function: function, + line: line, + message: message) + } + + static func error(_ message: @escaping @autoclosure () -> Any = "", + _ file: StaticString = #file, + _ function: StaticString = #function, + _ line: UInt = #line) -> Params { + return Params(level: .error, + filename: file, + function: function, + line: line, + message: message) + } + + static func fatal(_ message: @escaping @autoclosure () -> Any = "", + _ file: StaticString = #file, + _ function: StaticString = #function, + _ line: UInt = #line) -> Params { + return Params(level: .fatal, + filename: file, + function: function, + line: line, + message: message) + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Bool.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Bool.swift new file mode 100644 index 0000000..f817063 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Bool.swift @@ -0,0 +1,31 @@ +// +// Bool.swift +// RxUtils +// +// Created by Dmytro Shulzhenko on 4/29/19. +// Copyright © 2019 Dmytro Shulzhenko. All rights reserved. +// + +import Foundation +import RxSwift +import RxCocoa + +extension Observable where Element == Bool { + func whenTrue() -> Observable { + return filter { $0 } + } + + func whenFalse() -> Observable { + return filter { !$0 } + } +} + +extension SharedSequence where Element == Bool { + func whenTrue() -> SharedSequence { + return filter { $0 } + } + + func whenFalse() -> SharedSequence { + return filter { !$0 } + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Callable.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Callable.swift new file mode 100644 index 0000000..c60bd68 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Callable.swift @@ -0,0 +1,23 @@ +// +// Calable.swift +// RxUtils +// +// Created by Dmytro Shulzhenko on 4/29/19. +// Copyright © 2019 Dmytro Shulzhenko. All rights reserved. +// + +import RxSwift + +extension Completable { + static func fromCallable(_ execute: @escaping (@escaping (Error?) -> Void) -> Void) -> Completable { + return Completable + .create { observer in + execute { error in + error.flatMap { observer(.error($0)) } + observer(.completed) + } + + return Disposables.create() + } + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Driver.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Driver.swift new file mode 100644 index 0000000..0ff9bd1 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Driver.swift @@ -0,0 +1,38 @@ +// +// Driver.swift +// RxUtils +// +// Created by Dmytro Shulzhenko on 4/29/19. +// Copyright © 2019 Dmytro Shulzhenko. All rights reserved. +// + +import Foundation +import RxSwift +import RxCocoa + +extension ObservableConvertibleType { + func asDriver(_ file: StaticString = #file, + _ function: StaticString = #function, + _ line: UInt = #line, + onError: ((Error) -> Void)? = nil) -> Driver { + asObservable() + .do(onError: { error in + onError?(error) + log(.fatal(error, file, function, line)) + }) + .asDriver(onErrorRecover: { _ in .never() }) + } + + func asDriver(onError `default`: @escaping @autoclosure () -> Element) -> Driver { + return Observable.create { observer in + self.asObservable() + .subscribe(onNext: observer.onNext, + onError: { error in + log(.error(error)) + observer.onNext(`default`()) + }, + onCompleted: observer.onCompleted) + } + .asDriver(onErrorRecover: { _ in .never() }) + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Just.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Just.swift new file mode 100644 index 0000000..fad7943 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Just.swift @@ -0,0 +1,47 @@ +// +// Just.swift +// RxUtils +// +// Created by Dmytro Shulzhenko on 4/29/19. +// Copyright © 2019 Dmytro Shulzhenko. All rights reserved. +// + +import Foundation +import RxSwift +import RxCocoa + +extension Completable { + static func just(block: @escaping () throws -> Void) -> Completable { + return Completable.create { observer in + do { + try block() + observer(.completed) + } catch let error { + observer(.error(error)) + } + + return Disposables.create() + } + } +} + +extension Single { + static func just(block: @escaping () throws -> Element) -> Single { + return Single.create { observer in + do { + let element = try block() + observer(.success(element)) + } catch let error { + observer(.error(error)) + } + + return Disposables.create() + } + } +} + +extension PrimitiveSequenceType { + static var completed: Completable { + return Completable.just { } + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Latest.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Latest.swift new file mode 100644 index 0000000..84e14a9 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Latest.swift @@ -0,0 +1,18 @@ +// +// Latest.swift +// RxUtils +// +// Created by Dmytro Shulzhenko on 4/29/19. +// Copyright © 2019 Dmytro Shulzhenko. All rights reserved. +// + +import Foundation +import RxSwift + +extension ObservableType { + var latestIfReplayed: Element? { + var element: Element? + subscribe(onNext: { element = $0 }).dispose() + return element + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Loading.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Loading.swift new file mode 100644 index 0000000..650ee77 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Loading.swift @@ -0,0 +1,27 @@ +// +// Loading.swift +// RxUtils +// +// Created by Dmytro Shulzhenko on 4/29/19. +// Copyright © 2019 Dmytro Shulzhenko. All rights reserved. +// + +import Foundation +import RxSwift + +extension Observable { + func processLoading(with observer: AnyObserver) -> Observable { + return `do`(onNext: { _ in observer.onNext(false) }, + onError: { _ in observer.onNext(false) }, + onCompleted: { observer.onNext(false) }, + onSubscribe: { observer.onNext(true) }) + } +} + +extension PrimitiveSequence where Trait == CompletableTrait, Element == Swift.Never { + func processLoading(with observer: AnyObserver) -> Completable { + return `do`(onError: { _ in observer.onNext(false) }, + onCompleted: { observer.onNext(false) }, + onSubscribe: { observer.onNext(true) }) + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/NSObject+Disposable.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/NSObject+Disposable.swift new file mode 100644 index 0000000..5fa6993 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/NSObject+Disposable.swift @@ -0,0 +1,13 @@ +// +// NSObject+Disposable.swift +// Headway +// +// Created by Dmytro Shulzhenko on 26.11.2019. +// Copyright © 2019 Headway. All rights reserved. +// + +import RxSwift + +@objc extension NSObject: Disposable { + public func dispose() { } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Next.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Next.swift new file mode 100644 index 0000000..8a2fc93 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Next.swift @@ -0,0 +1,16 @@ +// +// Next.swift +// RxUtils +// +// Created by Dmytro Shulzhenko on 4/29/19. +// Copyright © 2019 Dmytro Shulzhenko. All rights reserved. +// + +import Foundation +import RxSwift + +extension ObserverType where Element == Void { + func onNext() { + onNext(()) + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Publicsher.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Publicsher.swift new file mode 100644 index 0000000..d3787a0 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Publicsher.swift @@ -0,0 +1,74 @@ +// +// Publicsher.swift +// RxUtils +// +// Created by Dmytro Shulzhenko on 4/29/19. +// Copyright © 2019 Dmytro Shulzhenko. All rights reserved. +// + +import RxSwift + +// MARK: Publisher. +// Like publish subject that accepts only next events. + +struct Publisher { + private let subject = PublishSubject() + + init() { } +} + +extension Publisher: ObserverType { + func on(_ event: RxSwift.Event) { + guard case .next = event else { return } + subject.on(event) + } +} + +extension Publisher: ObservableType { + func subscribe(_ observer: Observer) -> Disposable + where Observer: ObserverType, Element == Observer.Element { + return subject.subscribe(observer) + } +} + +// MARK: SharedPublisher. +// Like behavior subject, but replays only when first element was fired, also accepts only next events. + +final class SharedPublisher { + private let lock = NSRecursiveLock() + private let subject = PublishSubject() + private(set) var latestEvent: RxSwift.Event? + + init() { } +} + +extension SharedPublisher { + convenience init(initial: Element) { + self.init() + on(.next(initial)) + } +} + +extension SharedPublisher: ObserverType { + func on(_ event: RxSwift.Event) { + lock.lock() + defer { lock.unlock() } + switch event { + case .completed, .next: + latestEvent = event + subject.on(event) + case let .error(error): + log(.fatal(error)) + } + } +} + +extension SharedPublisher: ObservableType { + func subscribe(_ observer: Observer) -> Disposable + where Observer: ObserverType, SharedPublisher.Element == Observer.Element { + if let replayed = latestEvent { + observer.on(replayed) + } + return subject.subscribe(observer) + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Util.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Util.swift new file mode 100644 index 0000000..0deb2d9 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/RxUtils/Util.swift @@ -0,0 +1,357 @@ +// +// Util.swift +// RxUtils +// +// Created by Dmytro Shulzhenko on 4/29/19. +// Copyright © 2019 Dmytro Shulzhenko. All rights reserved. +// + +import Foundation +import RxSwift +import RxCocoa + +extension Observable { + func toVoid() -> Observable { + return map { _ in } + } +} + +extension PrimitiveSequence where Trait == CompletableTrait, Element == Swift.Never { + func onErrorJustComplete() -> Completable { + return Completable.create { complete in + return self.subscribe( + onCompleted: { complete(.completed) }, + onError: { _ in complete(.completed) } + ) + } + } + + func passErrorsAssert(_ file: StaticString = #file, + _ function: StaticString = #function, + _ line: UInt = #line, + onError: @escaping (Error) -> Void) -> Completable { + return Completable.create { complete in + return self.subscribe( + onCompleted: { complete(.completed) }, + onError: { error in + onError(error) + log(.fatal(error, file, function, line)) + } + ) + } + } + + func passErrors() -> Completable { + return Completable.create { complete in + return self.subscribe( + onCompleted: { complete(.completed) }, + onError: { error in + log(.error(error)) + } + ) + } + } + + static func amb(_ sources: Completable...) -> Completable { + return Completable.create { complete in + Disposables.create( + sources.map { source in + source.subscribe( + onCompleted: { complete(.completed) }, + onError: { complete(.error($0)) } + ) + } + ) + } + } +} + +extension ObservableType { + func passErrors() -> Observable { + return Observable.create { observer in + self + .subscribe( + onNext: observer.onNext, + onError: { error in + log(.error(error)) + }, + onCompleted: observer.onCompleted + ) + } + } +} + +extension PrimitiveSequence { + func onError(switchTo other: PrimitiveSequence) -> PrimitiveSequence { + return catchError { _ in other } + } + + static func | (lhs: PrimitiveSequence, + rhs: PrimitiveSequence) -> PrimitiveSequence { + return lhs.catchError { _ in rhs } + } +} + +extension Observable { + func onError(switchTo other: Observable) -> Observable { + return catchError { _ in other } + } + + static func | (lhs: Observable, rhs: Observable) -> Observable { + return lhs.onError(switchTo: rhs) + } +} +/// + +// MARK: Observer +// Like Any Observer but accepts only next events. + +struct Observer { + private let observer: AnyObserver + + init(_ observer: AnyObserver) { + self.observer = observer + } + + func next(_ element: Element) { + observer.onNext(element) + } + + func complete() { + observer.onCompleted() + } +} + +extension Observer where Element == Void { + func next() { + next(()) + } +} + +extension ObservableType { + func bind(to observer: Observer) -> Disposable { + return subscribe(onNext: observer.next, + onError: { log(.fatal($0)) }) + } + + func bind(next observer: Observer, + error: @escaping (Error) -> Void = { log(.fatal($0)) }) -> Disposable { + return subscribe(onNext: observer.next, + onError: error) + } +} + +extension Observable { + func `do`(onNil action: @escaping () -> Void) -> Observable where Element == T? { + return `do`(onNext: { element in + guard element == nil else { + return + } + action() + }) + } +} + +extension ObservableType { + func withLatestFromBuffer + (_ second: Source, + resultSelector: @escaping (Element, Source.Element) -> ResultType) + -> Observable { + return Observable.create { observer in + var buffer: [Element] = [] + var latest: Source.Element? + let lock = NSRecursiveLock() + + let secondDisposable = second.asObservable().subscribe { event in + lock.lock(); defer { lock.unlock() } + guard case let .next(element) = event else { return } + latest = element + guard !buffer.isEmpty else { return } + buffer.map { resultSelector($0, element) }.forEach(observer.onNext) + buffer = [] + } + let disposable = self.subscribe { event in + lock.lock(); defer { lock.unlock() } + switch event { + case .next(let element): + buffer.append(element) + guard let latest = latest else { return } + buffer.map { resultSelector($0, latest) }.forEach(observer.onNext) + buffer = [] + case .completed: + observer.onCompleted() + case .error(let error): + observer.onError(error) + } + } + return Disposables.create([disposable, secondDisposable]) + } + } +} + +extension Disposables { + static func create(_ disposable: Disposable) -> Cancelable { + return create([disposable]) + } +} + +extension Single where Trait == SingleTrait { + static func createThrows(subscribe: @escaping (@escaping SingleObserver) throws -> Disposable) -> Single { + return .create { complete in + do { + return Disposables.create( + try subscribe(complete) + ) + } catch let error { + complete(.error(error)) + return Disposables.create() + } + } + } +} + +extension Completable { + static func createThrows(subscribe: @escaping (@escaping CompletableObserver) throws -> Disposable) -> Completable { + return .create { complete in + do { + return Disposables.create( + try subscribe(complete) + ) + } catch let error { + complete(.error(error)) + return Disposables.create() + } + } + } + + static func workItem(_ block: @escaping () throws -> Void) -> Completable { + return .create { complete in + do { + try block() + complete(.completed) + return Disposables.create() + } catch let error { + complete(.error(error)) + return Disposables.create() + } + } + } +} + +enum ElementChange { + case initial(Element) + case change(oldValue: Element, newValue: Element) + + var change: (oldValue: Element, newValue: Element)? { + guard case let .change(oldValue, newValue) = self else { + return nil + } + return (oldValue, newValue) + } +} + +extension ObservableType { + // with latest + + func combineWithLatestFrom(_ second: Source) -> Observable<(Element, Source.Element)> + where Source: ObservableConvertibleType { + return self.withLatestFrom(second) { ($0, $1) } + } + + func combineWithLatestFrom(_ second: Source) + -> Observable<(First, Second, Source.Element)> + where Element == (First, Second), Source: ObservableConvertibleType { + return self.withLatestFrom(second) { ($0.0, $0.1, $1) } + } + + func combineWithLatestFrom(_ second: Source) + -> Observable<(First, Second, Third, Source.Element)> + where Element == (First, Second, Third), Source: ObservableConvertibleType { + return self.withLatestFrom(second) { ($0.0, $0.1, $0.2, $1) } + } + + func combineWithLatestFrom(_ second: Source) + -> Observable<(First, Second, Third, Fourth, Source.Element)> + where Element == (First, Second, Third, Fourth), Source: ObservableConvertibleType { + return self.withLatestFrom(second) { ($0.0, $0.1, $0.2, $0.3, $1) } + } + + // Flat map + + func combineFlatMapLatest(_ second: Source, scheduler: SchedulerType) + -> Observable<(Element, Source.Element)> + where Source: ObservableConvertibleType { + return self.flatMapLatest { element in second.asObservable().take(1).map { (element, $0) } } + .observeOn(scheduler) + } + + func combineFlatMapLatest(_ second: Source) -> Observable<(Element, Source.Element)> + where Source: ObservableConvertibleType { + return combineFlatMapLatest(second, resultSelector: { ($0, $1) }) + } + + func combineFlatMapLatest(_ second: Source, + resultSelector: @escaping (Element, Source.Element) -> T) + -> Observable + where Source: ObservableConvertibleType { + return flatMapLatest { element in + second.asObservable().take(1).map { resultSelector(element, $0) } + } + } + + func combineFlatMapLatest(_ second: Source) + -> Observable<(First, Second, Source.Element)> + where Source: ObservableConvertibleType, Element == (First, Second) { + return self.flatMapLatest { element in second.asObservable().take(1).map { (element.0, element.1, $0) } } + } + + func combineFlatMapLatest(_ second: Source) + -> Observable<(First, Second, Third, Source.Element)> + where Source: ObservableConvertibleType, Element == (First, Second, Third) { + return self.flatMapLatest { element in + second.asObservable().take(1).map { + (element.0, element.1, element.2, $0) + } + } + } + + func combineFlatMapLatest(_ second: Source) + -> Observable<(First, Second, Third, Fourth, Source.Element)> + where Source: ObservableConvertibleType, Element == (First, Second, Third, Fourth) { + return self.flatMapLatest { element in + second.asObservable().take(1).map { + (element.0, element.1, element.2, element.3, $0) + } + } + } + + func combineFlatMapLatest(_ second: Source, + scheduler: SchedulerType) -> Observable + where Source: ObservableConvertibleType, Element == Void { + return self.flatMapLatest { _ in second.asObservable().take(1) }.observeOn(scheduler) + } + + // .. TBD +} + +final class BagContainer { + final class DisposeTracker: Disposable { + func dispose() { + print("DISPOSED: \(String(describing: T.self))") + } + } + + private(set) var bag = createBag() + + func resetBag() { + bag = BagContainer.createBag() + } + + private static func createBag() -> DisposeBag { + let bag = DisposeBag() + #if DEBUG + bag.insert(DisposeTracker()) + #endif + return bag + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/UIStoryboard+ViewControllers.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/UIStoryboard+ViewControllers.swift new file mode 100644 index 0000000..c791069 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/UIStoryboard+ViewControllers.swift @@ -0,0 +1,45 @@ +// +// UIStoryboard+ViewControllers.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 27/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import UIKit + +extension UIStoryboard { + static var main: UIStoryboard { + return UIStoryboard(name: "Main", bundle: nil) + } +} + +extension UIStoryboard { + var loginViewController: LoginViewController { + guard let vc = UIStoryboard.main.instantiateViewController(withIdentifier: "LoginViewController") as? LoginViewController else { + fatalError("LoginViewController couldn't be found in Storyboard file") + } + return vc + } + + var discoverViewController: DiscoverViewController { + guard let vc = UIStoryboard.main.instantiateViewController(withIdentifier: "DiscoverViewController") as? DiscoverViewController else { + fatalError("DiscoverViewController couldn't be found in Storyboard file") + } + return vc + } + + var movieDetailViewController: MovieDetailViewController { + guard let vc = UIStoryboard.main.instantiateViewController(withIdentifier: "MovieDetailViewController") as? MovieDetailViewController else { + fatalError("MovieDetailViewController couldn't be found in Storyboard file") + } + return vc + } + + var searchViewController: SearchViewController { + guard let vc = UIStoryboard.main.instantiateViewController(withIdentifier: "SearchViewController") as? SearchViewController else { + fatalError("SearchViewController couldn't be found in Storyboard file") + } + return vc + } +} diff --git a/tmdb-rx-driver/tmdb-rx-driver/Utils/ViewModelType.swift b/tmdb-rx-driver/tmdb-rx-driver/Utils/ViewModelType.swift new file mode 100644 index 0000000..60a7666 --- /dev/null +++ b/tmdb-rx-driver/tmdb-rx-driver/Utils/ViewModelType.swift @@ -0,0 +1,16 @@ +// +// ViewModelType.swift +// tmdb-mvvm-pure +// +// Created by krawiecp-home on 27/01/2019. +// Copyright © 2019 tailec. All rights reserved. +// + +import Foundation + +protocol ViewModelType { + associatedtype Input + associatedtype Output + + func transform(input: Input) -> Output +}