├── .gitignore ├── Cartfile ├── Cartfile.resolved ├── LICENSE ├── Pagination.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── Pagination ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── DisposeBag+Bulk.swift ├── GitHubAPI.swift ├── GitHubRequest.swift ├── Info.plist ├── PaginationRequest.swift ├── PaginationResponse.swift ├── PaginationResponseType.swift ├── PaginationViewModel.swift ├── Repository.swift ├── RepositoryCell.swift ├── SearchRepositoriesViewController.swift ├── Session+Rx.swift ├── UIScrollView+Rx.swift ├── UITableView+Rx.swift └── UIViewController+Rx.swift ├── README.md ├── Tests ├── Fixture.swift ├── Info.plist ├── PaginationViewModelTests.swift ├── SearchRepositories.json └── TestSessionAdapter.swift └── screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/screenshots 64 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "ishkawa/APIKit" ~> 3.0 2 | github "ikesyo/Himotoki" ~> 3.0 3 | github "ReactiveX/RxSwift" ~> 3.0 4 | github "RxSwiftCommunity/Action" ~> 2.1 5 | 6 | github "kylef/WebLinking.swift" "master" 7 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "ikesyo/Himotoki" "3.0.0" 2 | github "antitypical/Result" "3.1.0" 3 | github "ReactiveX/RxSwift" "3.0.1" 4 | github "kylef/WebLinking.swift" "fddbacc30deab8afe12ce1d3b78bd27c593a0c29" 5 | github "ishkawa/APIKit" "3.1.0" 6 | github "RxSwiftCommunity/Action" "2.1.0" 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 try! Swift 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pagination.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7F4A6D811DEDD548005BC65E /* Action.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F4A6D7F1DEDD538005BC65E /* Action.framework */; }; 11 | 7F4A6D821DEDD548005BC65E /* Action.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7F4A6D7F1DEDD538005BC65E /* Action.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 12 | 7F4A6D831DEDD55F005BC65E /* RxBlocking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE41B9A1C868C8800E6E7EC /* RxBlocking.framework */; }; 13 | 7F4A6D841DEDD55F005BC65E /* RxBlocking.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE41B9A1C868C8800E6E7EC /* RxBlocking.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 14 | 7F4A6D8D1DEDDEDA005BC65E /* Action.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F4A6D7F1DEDD538005BC65E /* Action.framework */; }; 15 | 7F4A6D8E1DEDDEDA005BC65E /* APIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE41B8E1C868C8800E6E7EC /* APIKit.framework */; }; 16 | 7F4A6D8F1DEDDEDA005BC65E /* Himotoki.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE41B961C868C8800E6E7EC /* Himotoki.framework */; }; 17 | 7F4A6D901DEDDEDA005BC65E /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE41B981C868C8800E6E7EC /* Result.framework */; }; 18 | 7F4A6D911DEDDEDA005BC65E /* RxBlocking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE41B9A1C868C8800E6E7EC /* RxBlocking.framework */; }; 19 | 7F4A6D921DEDDEDA005BC65E /* RxCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE41B9C1C868C8800E6E7EC /* RxCocoa.framework */; }; 20 | 7F4A6D931DEDDEDA005BC65E /* RxSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE41B9E1C868C8800E6E7EC /* RxSwift.framework */; }; 21 | 7F4A6D941DEDDEDA005BC65E /* RxTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F4A6D851DEDDE3B005BC65E /* RxTest.framework */; }; 22 | 7F4A6D951DEDDEDA005BC65E /* WebLinking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE423281C86D4AF00E6E7EC /* WebLinking.framework */; }; 23 | 7F4A6D961DEDDEDC005BC65E /* Action.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7F4A6D7F1DEDD538005BC65E /* Action.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 24 | 7F4A6D971DEDDEDC005BC65E /* APIKit.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7FE41B8E1C868C8800E6E7EC /* APIKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 25 | 7F4A6D981DEDDEDC005BC65E /* Himotoki.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7FE41B961C868C8800E6E7EC /* Himotoki.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 26 | 7F4A6D991DEDDEDC005BC65E /* Result.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7FE41B981C868C8800E6E7EC /* Result.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 27 | 7F4A6D9A1DEDDEDC005BC65E /* RxBlocking.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7FE41B9A1C868C8800E6E7EC /* RxBlocking.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 28 | 7F4A6D9B1DEDDEDC005BC65E /* RxCocoa.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7FE41B9C1C868C8800E6E7EC /* RxCocoa.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 29 | 7F4A6D9C1DEDDEDC005BC65E /* RxSwift.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7FE41B9E1C868C8800E6E7EC /* RxSwift.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 30 | 7F4A6D9D1DEDDEDC005BC65E /* RxTest.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7F4A6D851DEDDE3B005BC65E /* RxTest.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 31 | 7F4A6D9E1DEDDEDC005BC65E /* WebLinking.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 7FE423281C86D4AF00E6E7EC /* WebLinking.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 32 | 7F6A1D3D1E4C626200FAA59D /* UIViewController+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F6A1D3C1E4C626200FAA59D /* UIViewController+Rx.swift */; }; 33 | 7F9B84031C8688F40076C46A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F9B84021C8688F40076C46A /* AppDelegate.swift */; }; 34 | 7F9B84081C8688F40076C46A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7F9B84061C8688F40076C46A /* Main.storyboard */; }; 35 | 7F9B840A1C8688F40076C46A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7F9B84091C8688F40076C46A /* Assets.xcassets */; }; 36 | 7F9B840D1C8688F40076C46A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7F9B840B1C8688F40076C46A /* LaunchScreen.storyboard */; }; 37 | 7FAC64B71CDCE36000F1BB45 /* Fixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FAC64B61CDCE36000F1BB45 /* Fixture.swift */; }; 38 | 7FAC64B91CDCE39F00F1BB45 /* SearchRepositories.json in Resources */ = {isa = PBXBuildFile; fileRef = 7FAC64B81CDCE39F00F1BB45 /* SearchRepositories.json */; }; 39 | 7FBBA9BF1CBF2691002C9B27 /* PaginationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBBA9BE1CBF2691002C9B27 /* PaginationViewModelTests.swift */; }; 40 | 7FBBA9D21CBF2793002C9B27 /* TestSessionAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FBBA9D11CBF2793002C9B27 /* TestSessionAdapter.swift */; }; 41 | 7FE41B7B1C868ADC00E6E7EC /* Repository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FE41B7A1C868ADC00E6E7EC /* Repository.swift */; }; 42 | 7FE41B7F1C868B7900E6E7EC /* PaginationResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FE41B7E1C868B7900E6E7EC /* PaginationResponse.swift */; }; 43 | 7FE41B811C868B9C00E6E7EC /* PaginationResponseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FE41B801C868B9C00E6E7EC /* PaginationResponseType.swift */; }; 44 | 7FE423071C868CCD00E6E7EC /* APIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE41B8E1C868C8800E6E7EC /* APIKit.framework */; }; 45 | 7FE423081C868CCD00E6E7EC /* APIKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE41B8E1C868C8800E6E7EC /* APIKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 46 | 7FE423091C868CCD00E6E7EC /* Himotoki.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE41B961C868C8800E6E7EC /* Himotoki.framework */; }; 47 | 7FE4230A1C868CCD00E6E7EC /* Himotoki.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE41B961C868C8800E6E7EC /* Himotoki.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 48 | 7FE4230B1C868CCD00E6E7EC /* Result.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE41B981C868C8800E6E7EC /* Result.framework */; }; 49 | 7FE4230C1C868CCD00E6E7EC /* Result.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE41B981C868C8800E6E7EC /* Result.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 50 | 7FE4230D1C868CCD00E6E7EC /* RxCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE41B9C1C868C8800E6E7EC /* RxCocoa.framework */; }; 51 | 7FE4230E1C868CCD00E6E7EC /* RxCocoa.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE41B9C1C868C8800E6E7EC /* RxCocoa.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 52 | 7FE4230F1C868CCD00E6E7EC /* RxSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE41B9E1C868C8800E6E7EC /* RxSwift.framework */; }; 53 | 7FE423101C868CCD00E6E7EC /* RxSwift.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE41B9E1C868C8800E6E7EC /* RxSwift.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 54 | 7FE423131C868CE400E6E7EC /* PaginationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FE423121C868CE400E6E7EC /* PaginationRequest.swift */; }; 55 | 7FE423161C868D1900E6E7EC /* GitHubRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FE423151C868D1900E6E7EC /* GitHubRequest.swift */; }; 56 | 7FE423181C8690A900E6E7EC /* GitHubAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FE423171C8690A900E6E7EC /* GitHubAPI.swift */; }; 57 | 7FE4231C1C86923B00E6E7EC /* PaginationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FE4231B1C86923B00E6E7EC /* PaginationViewModel.swift */; }; 58 | 7FE4231E1C8693CF00E6E7EC /* Session+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FE4231D1C8693CF00E6E7EC /* Session+Rx.swift */; }; 59 | 7FE423211C86BD9C00E6E7EC /* SearchRepositoriesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FE423201C86BD9C00E6E7EC /* SearchRepositoriesViewController.swift */; }; 60 | 7FE423251C86C09400E6E7EC /* UIScrollView+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FE423241C86C09400E6E7EC /* UIScrollView+Rx.swift */; }; 61 | 7FE4232A1C86D4C600E6E7EC /* WebLinking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE423281C86D4AF00E6E7EC /* WebLinking.framework */; }; 62 | 7FE4232B1C86D4C600E6E7EC /* WebLinking.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7FE423281C86D4AF00E6E7EC /* WebLinking.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 63 | 7FF3C9AC1E4C8D4E00D95251 /* RepositoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FF3C9AB1E4C8D4E00D95251 /* RepositoryCell.swift */; }; 64 | 7FF3C9AE1E4C9EF600D95251 /* UITableView+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FF3C9AD1E4C9EF600D95251 /* UITableView+Rx.swift */; }; 65 | 7FF3C9B01E4CA25900D95251 /* DisposeBag+Bulk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FF3C9AF1E4CA25900D95251 /* DisposeBag+Bulk.swift */; }; 66 | /* End PBXBuildFile section */ 67 | 68 | /* Begin PBXContainerItemProxy section */ 69 | 7FBBA9B91CBF266B002C9B27 /* PBXContainerItemProxy */ = { 70 | isa = PBXContainerItemProxy; 71 | containerPortal = 7F9B83F71C8688F40076C46A /* Project object */; 72 | proxyType = 1; 73 | remoteGlobalIDString = 7F9B83FE1C8688F40076C46A; 74 | remoteInfo = Pagination; 75 | }; 76 | /* End PBXContainerItemProxy section */ 77 | 78 | /* Begin PBXCopyFilesBuildPhase section */ 79 | 7FBBA9C81CBF270D002C9B27 /* CopyFiles */ = { 80 | isa = PBXCopyFilesBuildPhase; 81 | buildActionMask = 2147483647; 82 | dstPath = ""; 83 | dstSubfolderSpec = 10; 84 | files = ( 85 | 7F4A6D961DEDDEDC005BC65E /* Action.framework in CopyFiles */, 86 | 7F4A6D971DEDDEDC005BC65E /* APIKit.framework in CopyFiles */, 87 | 7F4A6D981DEDDEDC005BC65E /* Himotoki.framework in CopyFiles */, 88 | 7F4A6D991DEDDEDC005BC65E /* Result.framework in CopyFiles */, 89 | 7F4A6D9A1DEDDEDC005BC65E /* RxBlocking.framework in CopyFiles */, 90 | 7F4A6D9B1DEDDEDC005BC65E /* RxCocoa.framework in CopyFiles */, 91 | 7F4A6D9C1DEDDEDC005BC65E /* RxSwift.framework in CopyFiles */, 92 | 7F4A6D9D1DEDDEDC005BC65E /* RxTest.framework in CopyFiles */, 93 | 7F4A6D9E1DEDDEDC005BC65E /* WebLinking.framework in CopyFiles */, 94 | ); 95 | runOnlyForDeploymentPostprocessing = 0; 96 | }; 97 | 7FE423111C868CCD00E6E7EC /* Embed Frameworks */ = { 98 | isa = PBXCopyFilesBuildPhase; 99 | buildActionMask = 2147483647; 100 | dstPath = ""; 101 | dstSubfolderSpec = 10; 102 | files = ( 103 | 7FE423101C868CCD00E6E7EC /* RxSwift.framework in Embed Frameworks */, 104 | 7FE4230E1C868CCD00E6E7EC /* RxCocoa.framework in Embed Frameworks */, 105 | 7F4A6D821DEDD548005BC65E /* Action.framework in Embed Frameworks */, 106 | 7F4A6D841DEDD55F005BC65E /* RxBlocking.framework in Embed Frameworks */, 107 | 7FE4230C1C868CCD00E6E7EC /* Result.framework in Embed Frameworks */, 108 | 7FE4230A1C868CCD00E6E7EC /* Himotoki.framework in Embed Frameworks */, 109 | 7FE4232B1C86D4C600E6E7EC /* WebLinking.framework in Embed Frameworks */, 110 | 7FE423081C868CCD00E6E7EC /* APIKit.framework in Embed Frameworks */, 111 | ); 112 | name = "Embed Frameworks"; 113 | runOnlyForDeploymentPostprocessing = 0; 114 | }; 115 | /* End PBXCopyFilesBuildPhase section */ 116 | 117 | /* Begin PBXFileReference section */ 118 | 7F4A6D7F1DEDD538005BC65E /* Action.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Action.framework; sourceTree = ""; }; 119 | 7F4A6D851DEDDE3B005BC65E /* RxTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = RxTest.framework; sourceTree = ""; }; 120 | 7F6A1D3C1E4C626200FAA59D /* UIViewController+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Rx.swift"; sourceTree = ""; }; 121 | 7F9B83FF1C8688F40076C46A /* Pagination.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Pagination.app; sourceTree = BUILT_PRODUCTS_DIR; }; 122 | 7F9B84021C8688F40076C46A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 123 | 7F9B84071C8688F40076C46A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 124 | 7F9B84091C8688F40076C46A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 125 | 7F9B840C1C8688F40076C46A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 126 | 7F9B840E1C8688F40076C46A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 127 | 7FAC64B61CDCE36000F1BB45 /* Fixture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fixture.swift; sourceTree = ""; }; 128 | 7FAC64B81CDCE39F00F1BB45 /* SearchRepositories.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = SearchRepositories.json; sourceTree = ""; }; 129 | 7FBBA9B41CBF266B002C9B27 /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 130 | 7FBBA9B81CBF266B002C9B27 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 131 | 7FBBA9BE1CBF2691002C9B27 /* PaginationViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaginationViewModelTests.swift; sourceTree = ""; }; 132 | 7FBBA9D11CBF2793002C9B27 /* TestSessionAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestSessionAdapter.swift; sourceTree = ""; }; 133 | 7FE41B7A1C868ADC00E6E7EC /* Repository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Repository.swift; sourceTree = ""; }; 134 | 7FE41B7E1C868B7900E6E7EC /* PaginationResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaginationResponse.swift; sourceTree = ""; }; 135 | 7FE41B801C868B9C00E6E7EC /* PaginationResponseType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaginationResponseType.swift; sourceTree = ""; }; 136 | 7FE41B8E1C868C8800E6E7EC /* APIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = APIKit.framework; sourceTree = ""; }; 137 | 7FE41B961C868C8800E6E7EC /* Himotoki.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Himotoki.framework; sourceTree = ""; }; 138 | 7FE41B981C868C8800E6E7EC /* Result.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Result.framework; sourceTree = ""; }; 139 | 7FE41B9A1C868C8800E6E7EC /* RxBlocking.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = RxBlocking.framework; sourceTree = ""; }; 140 | 7FE41B9C1C868C8800E6E7EC /* RxCocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = RxCocoa.framework; sourceTree = ""; }; 141 | 7FE41B9E1C868C8800E6E7EC /* RxSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = RxSwift.framework; sourceTree = ""; }; 142 | 7FE423121C868CE400E6E7EC /* PaginationRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaginationRequest.swift; sourceTree = ""; }; 143 | 7FE423151C868D1900E6E7EC /* GitHubRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitHubRequest.swift; sourceTree = ""; }; 144 | 7FE423171C8690A900E6E7EC /* GitHubAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitHubAPI.swift; sourceTree = ""; }; 145 | 7FE4231B1C86923B00E6E7EC /* PaginationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaginationViewModel.swift; sourceTree = ""; }; 146 | 7FE4231D1C8693CF00E6E7EC /* Session+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Session+Rx.swift"; sourceTree = ""; }; 147 | 7FE423201C86BD9C00E6E7EC /* SearchRepositoriesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchRepositoriesViewController.swift; sourceTree = ""; }; 148 | 7FE423241C86C09400E6E7EC /* UIScrollView+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIScrollView+Rx.swift"; sourceTree = ""; }; 149 | 7FE423281C86D4AF00E6E7EC /* WebLinking.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = WebLinking.framework; sourceTree = ""; }; 150 | 7FF3C9AB1E4C8D4E00D95251 /* RepositoryCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepositoryCell.swift; sourceTree = ""; }; 151 | 7FF3C9AD1E4C9EF600D95251 /* UITableView+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+Rx.swift"; sourceTree = ""; }; 152 | 7FF3C9AF1E4CA25900D95251 /* DisposeBag+Bulk.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DisposeBag+Bulk.swift"; sourceTree = ""; }; 153 | /* End PBXFileReference section */ 154 | 155 | /* Begin PBXFrameworksBuildPhase section */ 156 | 7F9B83FC1C8688F40076C46A /* Frameworks */ = { 157 | isa = PBXFrameworksBuildPhase; 158 | buildActionMask = 2147483647; 159 | files = ( 160 | 7FE4230F1C868CCD00E6E7EC /* RxSwift.framework in Frameworks */, 161 | 7FE4230D1C868CCD00E6E7EC /* RxCocoa.framework in Frameworks */, 162 | 7F4A6D811DEDD548005BC65E /* Action.framework in Frameworks */, 163 | 7FE4230B1C868CCD00E6E7EC /* Result.framework in Frameworks */, 164 | 7FE423091C868CCD00E6E7EC /* Himotoki.framework in Frameworks */, 165 | 7FE4232A1C86D4C600E6E7EC /* WebLinking.framework in Frameworks */, 166 | 7FE423071C868CCD00E6E7EC /* APIKit.framework in Frameworks */, 167 | 7F4A6D831DEDD55F005BC65E /* RxBlocking.framework in Frameworks */, 168 | ); 169 | runOnlyForDeploymentPostprocessing = 0; 170 | }; 171 | 7FBBA9B11CBF266B002C9B27 /* Frameworks */ = { 172 | isa = PBXFrameworksBuildPhase; 173 | buildActionMask = 2147483647; 174 | files = ( 175 | 7F4A6D8D1DEDDEDA005BC65E /* Action.framework in Frameworks */, 176 | 7F4A6D8E1DEDDEDA005BC65E /* APIKit.framework in Frameworks */, 177 | 7F4A6D8F1DEDDEDA005BC65E /* Himotoki.framework in Frameworks */, 178 | 7F4A6D901DEDDEDA005BC65E /* Result.framework in Frameworks */, 179 | 7F4A6D911DEDDEDA005BC65E /* RxBlocking.framework in Frameworks */, 180 | 7F4A6D921DEDDEDA005BC65E /* RxCocoa.framework in Frameworks */, 181 | 7F4A6D931DEDDEDA005BC65E /* RxSwift.framework in Frameworks */, 182 | 7F4A6D941DEDDEDA005BC65E /* RxTest.framework in Frameworks */, 183 | 7F4A6D951DEDDEDA005BC65E /* WebLinking.framework in Frameworks */, 184 | ); 185 | runOnlyForDeploymentPostprocessing = 0; 186 | }; 187 | /* End PBXFrameworksBuildPhase section */ 188 | 189 | /* Begin PBXGroup section */ 190 | 7F9B83F61C8688F40076C46A = { 191 | isa = PBXGroup; 192 | children = ( 193 | 7F9B84011C8688F40076C46A /* Pagination */, 194 | 7FBBA9B51CBF266B002C9B27 /* Tests */, 195 | 7FE41B831C868C8800E6E7EC /* Carthage */, 196 | 7F9B84001C8688F40076C46A /* Products */, 197 | ); 198 | sourceTree = ""; 199 | }; 200 | 7F9B84001C8688F40076C46A /* Products */ = { 201 | isa = PBXGroup; 202 | children = ( 203 | 7F9B83FF1C8688F40076C46A /* Pagination.app */, 204 | 7FBBA9B41CBF266B002C9B27 /* Tests.xctest */, 205 | ); 206 | name = Products; 207 | sourceTree = ""; 208 | }; 209 | 7F9B84011C8688F40076C46A /* Pagination */ = { 210 | isa = PBXGroup; 211 | children = ( 212 | 7F9B84021C8688F40076C46A /* AppDelegate.swift */, 213 | 7FE423201C86BD9C00E6E7EC /* SearchRepositoriesViewController.swift */, 214 | 7FF3C9AB1E4C8D4E00D95251 /* RepositoryCell.swift */, 215 | 7FE4231B1C86923B00E6E7EC /* PaginationViewModel.swift */, 216 | 7FE423141C868CED00E6E7EC /* Request */, 217 | 7FE41B821C868C1C00E6E7EC /* Response */, 218 | 7F9B84061C8688F40076C46A /* Main.storyboard */, 219 | 7F9B840B1C8688F40076C46A /* LaunchScreen.storyboard */, 220 | 7FE4231F1C8693D500E6E7EC /* Extension */, 221 | 7F9B84091C8688F40076C46A /* Assets.xcassets */, 222 | 7F9B840E1C8688F40076C46A /* Info.plist */, 223 | ); 224 | path = Pagination; 225 | sourceTree = ""; 226 | }; 227 | 7FBBA9B51CBF266B002C9B27 /* Tests */ = { 228 | isa = PBXGroup; 229 | children = ( 230 | 7FBBA9BE1CBF2691002C9B27 /* PaginationViewModelTests.swift */, 231 | 7FBBA9D11CBF2793002C9B27 /* TestSessionAdapter.swift */, 232 | 7FAC64B61CDCE36000F1BB45 /* Fixture.swift */, 233 | 7FAC64B81CDCE39F00F1BB45 /* SearchRepositories.json */, 234 | 7FBBA9B81CBF266B002C9B27 /* Info.plist */, 235 | ); 236 | path = Tests; 237 | sourceTree = ""; 238 | }; 239 | 7FE41B821C868C1C00E6E7EC /* Response */ = { 240 | isa = PBXGroup; 241 | children = ( 242 | 7FE41B801C868B9C00E6E7EC /* PaginationResponseType.swift */, 243 | 7FE41B7E1C868B7900E6E7EC /* PaginationResponse.swift */, 244 | 7FE41B7A1C868ADC00E6E7EC /* Repository.swift */, 245 | ); 246 | name = Response; 247 | sourceTree = ""; 248 | }; 249 | 7FE41B831C868C8800E6E7EC /* Carthage */ = { 250 | isa = PBXGroup; 251 | children = ( 252 | 7FE41B841C868C8800E6E7EC /* Build */, 253 | ); 254 | path = Carthage; 255 | sourceTree = ""; 256 | }; 257 | 7FE41B841C868C8800E6E7EC /* Build */ = { 258 | isa = PBXGroup; 259 | children = ( 260 | 7FE41B851C868C8800E6E7EC /* iOS */, 261 | ); 262 | path = Build; 263 | sourceTree = ""; 264 | }; 265 | 7FE41B851C868C8800E6E7EC /* iOS */ = { 266 | isa = PBXGroup; 267 | children = ( 268 | 7F4A6D7F1DEDD538005BC65E /* Action.framework */, 269 | 7FE41B8E1C868C8800E6E7EC /* APIKit.framework */, 270 | 7FE41B961C868C8800E6E7EC /* Himotoki.framework */, 271 | 7FE41B981C868C8800E6E7EC /* Result.framework */, 272 | 7FE41B9A1C868C8800E6E7EC /* RxBlocking.framework */, 273 | 7FE41B9C1C868C8800E6E7EC /* RxCocoa.framework */, 274 | 7FE41B9E1C868C8800E6E7EC /* RxSwift.framework */, 275 | 7F4A6D851DEDDE3B005BC65E /* RxTest.framework */, 276 | 7FE423281C86D4AF00E6E7EC /* WebLinking.framework */, 277 | ); 278 | path = iOS; 279 | sourceTree = ""; 280 | }; 281 | 7FE423141C868CED00E6E7EC /* Request */ = { 282 | isa = PBXGroup; 283 | children = ( 284 | 7FE423171C8690A900E6E7EC /* GitHubAPI.swift */, 285 | 7FE423151C868D1900E6E7EC /* GitHubRequest.swift */, 286 | 7FE423121C868CE400E6E7EC /* PaginationRequest.swift */, 287 | ); 288 | name = Request; 289 | sourceTree = ""; 290 | }; 291 | 7FE4231F1C8693D500E6E7EC /* Extension */ = { 292 | isa = PBXGroup; 293 | children = ( 294 | 7FE4231D1C8693CF00E6E7EC /* Session+Rx.swift */, 295 | 7FE423241C86C09400E6E7EC /* UIScrollView+Rx.swift */, 296 | 7FF3C9AD1E4C9EF600D95251 /* UITableView+Rx.swift */, 297 | 7F6A1D3C1E4C626200FAA59D /* UIViewController+Rx.swift */, 298 | 7FF3C9AF1E4CA25900D95251 /* DisposeBag+Bulk.swift */, 299 | ); 300 | name = Extension; 301 | sourceTree = ""; 302 | }; 303 | /* End PBXGroup section */ 304 | 305 | /* Begin PBXNativeTarget section */ 306 | 7F9B83FE1C8688F40076C46A /* Pagination */ = { 307 | isa = PBXNativeTarget; 308 | buildConfigurationList = 7F9B84111C8688F40076C46A /* Build configuration list for PBXNativeTarget "Pagination" */; 309 | buildPhases = ( 310 | 7F9B83FB1C8688F40076C46A /* Sources */, 311 | 7F9B83FC1C8688F40076C46A /* Frameworks */, 312 | 7F9B83FD1C8688F40076C46A /* Resources */, 313 | 7FE423111C868CCD00E6E7EC /* Embed Frameworks */, 314 | ); 315 | buildRules = ( 316 | ); 317 | dependencies = ( 318 | ); 319 | name = Pagination; 320 | productName = Pagination; 321 | productReference = 7F9B83FF1C8688F40076C46A /* Pagination.app */; 322 | productType = "com.apple.product-type.application"; 323 | }; 324 | 7FBBA9B31CBF266B002C9B27 /* Tests */ = { 325 | isa = PBXNativeTarget; 326 | buildConfigurationList = 7FBBA9BD1CBF266B002C9B27 /* Build configuration list for PBXNativeTarget "Tests" */; 327 | buildPhases = ( 328 | 7FBBA9B01CBF266B002C9B27 /* Sources */, 329 | 7FBBA9B11CBF266B002C9B27 /* Frameworks */, 330 | 7FBBA9B21CBF266B002C9B27 /* Resources */, 331 | 7FBBA9C81CBF270D002C9B27 /* CopyFiles */, 332 | ); 333 | buildRules = ( 334 | ); 335 | dependencies = ( 336 | 7FBBA9BA1CBF266B002C9B27 /* PBXTargetDependency */, 337 | ); 338 | name = Tests; 339 | productName = Tests; 340 | productReference = 7FBBA9B41CBF266B002C9B27 /* Tests.xctest */; 341 | productType = "com.apple.product-type.bundle.unit-test"; 342 | }; 343 | /* End PBXNativeTarget section */ 344 | 345 | /* Begin PBXProject section */ 346 | 7F9B83F71C8688F40076C46A /* Project object */ = { 347 | isa = PBXProject; 348 | attributes = { 349 | LastSwiftUpdateCheck = 0730; 350 | LastUpgradeCheck = 0810; 351 | ORGANIZATIONNAME = "Yosuke Ishikawa"; 352 | TargetAttributes = { 353 | 7F9B83FE1C8688F40076C46A = { 354 | CreatedOnToolsVersion = 7.2.1; 355 | LastSwiftMigration = 0810; 356 | }; 357 | 7FBBA9B31CBF266B002C9B27 = { 358 | CreatedOnToolsVersion = 7.3; 359 | LastSwiftMigration = 0810; 360 | TestTargetID = 7F9B83FE1C8688F40076C46A; 361 | }; 362 | }; 363 | }; 364 | buildConfigurationList = 7F9B83FA1C8688F40076C46A /* Build configuration list for PBXProject "Pagination" */; 365 | compatibilityVersion = "Xcode 3.2"; 366 | developmentRegion = English; 367 | hasScannedForEncodings = 0; 368 | knownRegions = ( 369 | en, 370 | Base, 371 | ); 372 | mainGroup = 7F9B83F61C8688F40076C46A; 373 | productRefGroup = 7F9B84001C8688F40076C46A /* Products */; 374 | projectDirPath = ""; 375 | projectRoot = ""; 376 | targets = ( 377 | 7F9B83FE1C8688F40076C46A /* Pagination */, 378 | 7FBBA9B31CBF266B002C9B27 /* Tests */, 379 | ); 380 | }; 381 | /* End PBXProject section */ 382 | 383 | /* Begin PBXResourcesBuildPhase section */ 384 | 7F9B83FD1C8688F40076C46A /* Resources */ = { 385 | isa = PBXResourcesBuildPhase; 386 | buildActionMask = 2147483647; 387 | files = ( 388 | 7F9B840D1C8688F40076C46A /* LaunchScreen.storyboard in Resources */, 389 | 7F9B840A1C8688F40076C46A /* Assets.xcassets in Resources */, 390 | 7F9B84081C8688F40076C46A /* Main.storyboard in Resources */, 391 | ); 392 | runOnlyForDeploymentPostprocessing = 0; 393 | }; 394 | 7FBBA9B21CBF266B002C9B27 /* Resources */ = { 395 | isa = PBXResourcesBuildPhase; 396 | buildActionMask = 2147483647; 397 | files = ( 398 | 7FAC64B91CDCE39F00F1BB45 /* SearchRepositories.json in Resources */, 399 | ); 400 | runOnlyForDeploymentPostprocessing = 0; 401 | }; 402 | /* End PBXResourcesBuildPhase section */ 403 | 404 | /* Begin PBXSourcesBuildPhase section */ 405 | 7F9B83FB1C8688F40076C46A /* Sources */ = { 406 | isa = PBXSourcesBuildPhase; 407 | buildActionMask = 2147483647; 408 | files = ( 409 | 7FE423251C86C09400E6E7EC /* UIScrollView+Rx.swift in Sources */, 410 | 7FF3C9AE1E4C9EF600D95251 /* UITableView+Rx.swift in Sources */, 411 | 7FE41B7F1C868B7900E6E7EC /* PaginationResponse.swift in Sources */, 412 | 7FE423161C868D1900E6E7EC /* GitHubRequest.swift in Sources */, 413 | 7FE4231E1C8693CF00E6E7EC /* Session+Rx.swift in Sources */, 414 | 7FE423131C868CE400E6E7EC /* PaginationRequest.swift in Sources */, 415 | 7F9B84031C8688F40076C46A /* AppDelegate.swift in Sources */, 416 | 7FE41B811C868B9C00E6E7EC /* PaginationResponseType.swift in Sources */, 417 | 7FE41B7B1C868ADC00E6E7EC /* Repository.swift in Sources */, 418 | 7FE4231C1C86923B00E6E7EC /* PaginationViewModel.swift in Sources */, 419 | 7FE423211C86BD9C00E6E7EC /* SearchRepositoriesViewController.swift in Sources */, 420 | 7FF3C9AC1E4C8D4E00D95251 /* RepositoryCell.swift in Sources */, 421 | 7FF3C9B01E4CA25900D95251 /* DisposeBag+Bulk.swift in Sources */, 422 | 7F6A1D3D1E4C626200FAA59D /* UIViewController+Rx.swift in Sources */, 423 | 7FE423181C8690A900E6E7EC /* GitHubAPI.swift in Sources */, 424 | ); 425 | runOnlyForDeploymentPostprocessing = 0; 426 | }; 427 | 7FBBA9B01CBF266B002C9B27 /* Sources */ = { 428 | isa = PBXSourcesBuildPhase; 429 | buildActionMask = 2147483647; 430 | files = ( 431 | 7FBBA9D21CBF2793002C9B27 /* TestSessionAdapter.swift in Sources */, 432 | 7FBBA9BF1CBF2691002C9B27 /* PaginationViewModelTests.swift in Sources */, 433 | 7FAC64B71CDCE36000F1BB45 /* Fixture.swift in Sources */, 434 | ); 435 | runOnlyForDeploymentPostprocessing = 0; 436 | }; 437 | /* End PBXSourcesBuildPhase section */ 438 | 439 | /* Begin PBXTargetDependency section */ 440 | 7FBBA9BA1CBF266B002C9B27 /* PBXTargetDependency */ = { 441 | isa = PBXTargetDependency; 442 | target = 7F9B83FE1C8688F40076C46A /* Pagination */; 443 | targetProxy = 7FBBA9B91CBF266B002C9B27 /* PBXContainerItemProxy */; 444 | }; 445 | /* End PBXTargetDependency section */ 446 | 447 | /* Begin PBXVariantGroup section */ 448 | 7F9B84061C8688F40076C46A /* Main.storyboard */ = { 449 | isa = PBXVariantGroup; 450 | children = ( 451 | 7F9B84071C8688F40076C46A /* Base */, 452 | ); 453 | name = Main.storyboard; 454 | sourceTree = ""; 455 | }; 456 | 7F9B840B1C8688F40076C46A /* LaunchScreen.storyboard */ = { 457 | isa = PBXVariantGroup; 458 | children = ( 459 | 7F9B840C1C8688F40076C46A /* Base */, 460 | ); 461 | name = LaunchScreen.storyboard; 462 | sourceTree = ""; 463 | }; 464 | /* End PBXVariantGroup section */ 465 | 466 | /* Begin XCBuildConfiguration section */ 467 | 7F9B840F1C8688F40076C46A /* Debug */ = { 468 | isa = XCBuildConfiguration; 469 | buildSettings = { 470 | ALWAYS_SEARCH_USER_PATHS = NO; 471 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 472 | CLANG_CXX_LIBRARY = "libc++"; 473 | CLANG_ENABLE_MODULES = YES; 474 | CLANG_ENABLE_OBJC_ARC = YES; 475 | CLANG_WARN_BOOL_CONVERSION = YES; 476 | CLANG_WARN_CONSTANT_CONVERSION = YES; 477 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 478 | CLANG_WARN_EMPTY_BODY = YES; 479 | CLANG_WARN_ENUM_CONVERSION = YES; 480 | CLANG_WARN_INFINITE_RECURSION = YES; 481 | CLANG_WARN_INT_CONVERSION = YES; 482 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 483 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 484 | CLANG_WARN_UNREACHABLE_CODE = YES; 485 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 486 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 487 | COPY_PHASE_STRIP = NO; 488 | DEBUG_INFORMATION_FORMAT = dwarf; 489 | ENABLE_STRICT_OBJC_MSGSEND = YES; 490 | ENABLE_TESTABILITY = YES; 491 | GCC_C_LANGUAGE_STANDARD = gnu99; 492 | GCC_DYNAMIC_NO_PIC = NO; 493 | GCC_NO_COMMON_BLOCKS = YES; 494 | GCC_OPTIMIZATION_LEVEL = 0; 495 | GCC_PREPROCESSOR_DEFINITIONS = ( 496 | "DEBUG=1", 497 | "$(inherited)", 498 | ); 499 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 500 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 501 | GCC_WARN_UNDECLARED_SELECTOR = YES; 502 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 503 | GCC_WARN_UNUSED_FUNCTION = YES; 504 | GCC_WARN_UNUSED_VARIABLE = YES; 505 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 506 | MTL_ENABLE_DEBUG_INFO = YES; 507 | ONLY_ACTIVE_ARCH = YES; 508 | SDKROOT = iphoneos; 509 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 510 | TARGETED_DEVICE_FAMILY = "1,2"; 511 | }; 512 | name = Debug; 513 | }; 514 | 7F9B84101C8688F40076C46A /* Release */ = { 515 | isa = XCBuildConfiguration; 516 | buildSettings = { 517 | ALWAYS_SEARCH_USER_PATHS = NO; 518 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 519 | CLANG_CXX_LIBRARY = "libc++"; 520 | CLANG_ENABLE_MODULES = YES; 521 | CLANG_ENABLE_OBJC_ARC = YES; 522 | CLANG_WARN_BOOL_CONVERSION = YES; 523 | CLANG_WARN_CONSTANT_CONVERSION = YES; 524 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 525 | CLANG_WARN_EMPTY_BODY = YES; 526 | CLANG_WARN_ENUM_CONVERSION = YES; 527 | CLANG_WARN_INFINITE_RECURSION = YES; 528 | CLANG_WARN_INT_CONVERSION = YES; 529 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 530 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 531 | CLANG_WARN_UNREACHABLE_CODE = YES; 532 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 533 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 534 | COPY_PHASE_STRIP = NO; 535 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 536 | ENABLE_NS_ASSERTIONS = NO; 537 | ENABLE_STRICT_OBJC_MSGSEND = YES; 538 | GCC_C_LANGUAGE_STANDARD = gnu99; 539 | GCC_NO_COMMON_BLOCKS = YES; 540 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 541 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 542 | GCC_WARN_UNDECLARED_SELECTOR = YES; 543 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 544 | GCC_WARN_UNUSED_FUNCTION = YES; 545 | GCC_WARN_UNUSED_VARIABLE = YES; 546 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 547 | MTL_ENABLE_DEBUG_INFO = NO; 548 | SDKROOT = iphoneos; 549 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 550 | TARGETED_DEVICE_FAMILY = "1,2"; 551 | VALIDATE_PRODUCT = YES; 552 | }; 553 | name = Release; 554 | }; 555 | 7F9B84121C8688F40076C46A /* Debug */ = { 556 | isa = XCBuildConfiguration; 557 | buildSettings = { 558 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 559 | FRAMEWORK_SEARCH_PATHS = ( 560 | "$(inherited)", 561 | "$(PROJECT_DIR)/Carthage/Build/iOS", 562 | ); 563 | INFOPLIST_FILE = Pagination/Info.plist; 564 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 565 | PRODUCT_BUNDLE_IDENTIFIER = "-.Pagination"; 566 | PRODUCT_NAME = "$(TARGET_NAME)"; 567 | SWIFT_VERSION = 3.0; 568 | }; 569 | name = Debug; 570 | }; 571 | 7F9B84131C8688F40076C46A /* Release */ = { 572 | isa = XCBuildConfiguration; 573 | buildSettings = { 574 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 575 | FRAMEWORK_SEARCH_PATHS = ( 576 | "$(inherited)", 577 | "$(PROJECT_DIR)/Carthage/Build/iOS", 578 | ); 579 | INFOPLIST_FILE = Pagination/Info.plist; 580 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 581 | PRODUCT_BUNDLE_IDENTIFIER = "-.Pagination"; 582 | PRODUCT_NAME = "$(TARGET_NAME)"; 583 | SWIFT_VERSION = 3.0; 584 | }; 585 | name = Release; 586 | }; 587 | 7FBBA9BB1CBF266B002C9B27 /* Debug */ = { 588 | isa = XCBuildConfiguration; 589 | buildSettings = { 590 | BUNDLE_LOADER = "$(TEST_HOST)"; 591 | CLANG_ANALYZER_NONNULL = YES; 592 | FRAMEWORK_SEARCH_PATHS = ( 593 | "$(inherited)", 594 | "$(PROJECT_DIR)/Carthage/Build/iOS", 595 | ); 596 | INFOPLIST_FILE = Tests/Info.plist; 597 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 598 | PRODUCT_BUNDLE_IDENTIFIER = "-.Tests"; 599 | PRODUCT_NAME = "$(TARGET_NAME)"; 600 | SWIFT_VERSION = 3.0; 601 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Pagination.app/Pagination"; 602 | }; 603 | name = Debug; 604 | }; 605 | 7FBBA9BC1CBF266B002C9B27 /* Release */ = { 606 | isa = XCBuildConfiguration; 607 | buildSettings = { 608 | BUNDLE_LOADER = "$(TEST_HOST)"; 609 | CLANG_ANALYZER_NONNULL = YES; 610 | FRAMEWORK_SEARCH_PATHS = ( 611 | "$(inherited)", 612 | "$(PROJECT_DIR)/Carthage/Build/iOS", 613 | ); 614 | INFOPLIST_FILE = Tests/Info.plist; 615 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 616 | PRODUCT_BUNDLE_IDENTIFIER = "-.Tests"; 617 | PRODUCT_NAME = "$(TARGET_NAME)"; 618 | SWIFT_VERSION = 3.0; 619 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Pagination.app/Pagination"; 620 | }; 621 | name = Release; 622 | }; 623 | /* End XCBuildConfiguration section */ 624 | 625 | /* Begin XCConfigurationList section */ 626 | 7F9B83FA1C8688F40076C46A /* Build configuration list for PBXProject "Pagination" */ = { 627 | isa = XCConfigurationList; 628 | buildConfigurations = ( 629 | 7F9B840F1C8688F40076C46A /* Debug */, 630 | 7F9B84101C8688F40076C46A /* Release */, 631 | ); 632 | defaultConfigurationIsVisible = 0; 633 | defaultConfigurationName = Release; 634 | }; 635 | 7F9B84111C8688F40076C46A /* Build configuration list for PBXNativeTarget "Pagination" */ = { 636 | isa = XCConfigurationList; 637 | buildConfigurations = ( 638 | 7F9B84121C8688F40076C46A /* Debug */, 639 | 7F9B84131C8688F40076C46A /* Release */, 640 | ); 641 | defaultConfigurationIsVisible = 0; 642 | defaultConfigurationName = Release; 643 | }; 644 | 7FBBA9BD1CBF266B002C9B27 /* Build configuration list for PBXNativeTarget "Tests" */ = { 645 | isa = XCConfigurationList; 646 | buildConfigurations = ( 647 | 7FBBA9BB1CBF266B002C9B27 /* Debug */, 648 | 7FBBA9BC1CBF266B002C9B27 /* Release */, 649 | ); 650 | defaultConfigurationIsVisible = 0; 651 | defaultConfigurationName = Release; 652 | }; 653 | /* End XCConfigurationList section */ 654 | }; 655 | rootObject = 7F9B83F71C8688F40076C46A /* Project object */; 656 | } 657 | -------------------------------------------------------------------------------- /Pagination.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Pagination/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | class AppDelegate: UIResponder, UIApplicationDelegate { 5 | var window: UIWindow? 6 | 7 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 8 | return true 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /Pagination/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Pagination/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Pagination/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 49 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /Pagination/DisposeBag+Bulk.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RxSwift 3 | 4 | extension DisposeBag { 5 | func insert(_ disposables: [Disposable]) { 6 | disposables.forEach(insert) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Pagination/GitHubAPI.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import APIKit 3 | 4 | final class GitHubAPI { 5 | private init() { 6 | 7 | } 8 | 9 | struct SearchRepositoriesRequest: GitHubRequest, PaginationRequest { 10 | let query: String 11 | var page: Int 12 | 13 | init(query: String, page: Int = 1) { 14 | self.query = query 15 | self.page = page 16 | } 17 | 18 | // MARK: RequestType 19 | typealias Response = SearchResponse 20 | 21 | var method: HTTPMethod { 22 | return .get 23 | } 24 | 25 | var path: String { 26 | return "/search/repositories" 27 | } 28 | 29 | var parameters: Any? { 30 | return ["q": query, "page": page] 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Pagination/GitHubRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import APIKit 3 | import Himotoki 4 | import WebLinking 5 | 6 | protocol GitHubRequest: Request { 7 | 8 | } 9 | 10 | extension GitHubRequest { 11 | var baseURL: URL { 12 | return URL(string: "https://api.github.com")! 13 | } 14 | } 15 | 16 | extension GitHubRequest where Response: Decodable { 17 | func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response { 18 | return try decodeValue(object) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Pagination/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Pagination/PaginationRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import APIKit 3 | import Himotoki 4 | 5 | protocol PaginationRequest: Request { 6 | associatedtype Response: PaginationResponse 7 | var page: Int { get set } 8 | } 9 | 10 | extension PaginationRequest { 11 | func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response { 12 | let elements = try decodeArray(object, rootKeyPath: "items") as [Response.Element] 13 | 14 | let nextURI = urlResponse.findLink(relation: "next")?.uri 15 | let queryItems = nextURI.flatMap(URLComponents.init)?.queryItems 16 | let nextPage = queryItems? 17 | .filter { $0.name == "page" } 18 | .flatMap { $0.value } 19 | .flatMap { Int($0) } 20 | .first 21 | 22 | return Response(elements: elements, page: page, nextPage: nextPage) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Pagination/PaginationResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Himotoki 3 | 4 | struct SearchResponse: PaginationResponse { 5 | let elements: [Element] 6 | let page: Int 7 | let nextPage: Int? 8 | } 9 | -------------------------------------------------------------------------------- /Pagination/PaginationResponseType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Himotoki 3 | 4 | protocol PaginationResponse { 5 | associatedtype Element: Decodable 6 | var elements: [Element] { get } 7 | var page: Int { get } 8 | var nextPage: Int? { get } 9 | init(elements: [Element], page: Int, nextPage: Int?) 10 | } 11 | 12 | struct AnyPaginationResponse: PaginationResponse { 13 | let elements: [Element] 14 | let page: Int 15 | let nextPage: Int? 16 | 17 | init(response: Response) where Response.Element == Element { 18 | elements = response.elements 19 | page = response.page 20 | nextPage = response.nextPage 21 | } 22 | 23 | init(elements: [Element], page: Int, nextPage: Int?) { 24 | self.elements = elements 25 | self.page = page 26 | self.nextPage = nextPage 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /Pagination/PaginationViewModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RxSwift 3 | import RxCocoa 4 | import APIKit 5 | import Action 6 | import Himotoki 7 | 8 | class PaginationViewModel { 9 | let indicatorViewAnimating: Driver 10 | let elements: Driver<[Element]> 11 | let loadError: Driver 12 | 13 | private let loadAction: Action> 14 | private let disposeBag = DisposeBag() 15 | 16 | init( 17 | baseRequest: Request, 18 | session: Session = Session.shared, 19 | viewWillAppear: Driver, 20 | scrollViewDidReachBottom: Driver) where Request.Response.Element == Element { 21 | 22 | loadAction = Action { page in 23 | var request = baseRequest 24 | request.page = page 25 | 26 | return session.rx 27 | .response(request) 28 | .map(AnyPaginationResponse.init) 29 | } 30 | 31 | indicatorViewAnimating = loadAction.executing.asDriver(onErrorJustReturn: false) 32 | elements = loadAction.elements.asDriver(onErrorDriveWith: .empty()) 33 | .scan([]) { $1.page == 1 ? $1.elements : $0 + $1.elements } 34 | .startWith([]) 35 | 36 | loadError = loadAction.errors.asDriver(onErrorDriveWith: .empty()) 37 | .flatMap { error -> Driver in 38 | switch error { 39 | case .underlyingError(let error): 40 | return Driver.just(error) 41 | case .notEnabled: 42 | return Driver.empty() 43 | } 44 | } 45 | 46 | viewWillAppear.asObservable() 47 | .map { _ in 1 } 48 | .subscribe(loadAction.inputs) 49 | .addDisposableTo(disposeBag) 50 | 51 | scrollViewDidReachBottom.asObservable() 52 | .withLatestFrom(loadAction.elements) 53 | .flatMap { $0.nextPage.map { Observable.of($0) } ?? Observable.empty() } 54 | .subscribe(loadAction.inputs) 55 | .addDisposableTo(disposeBag) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Pagination/Repository.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Himotoki 3 | 4 | struct Repository: Decodable { 5 | let id: Int 6 | let fullName: String 7 | let stargazersCount: Int 8 | 9 | static func decode(_ e: Extractor) throws -> Repository { 10 | return try Repository( 11 | id: e <| "id", 12 | fullName: e <| "full_name", 13 | stargazersCount: e <| "stargazers_count" 14 | ) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Pagination/RepositoryCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class RepositoryCell: UITableViewCell, BindableCell { 4 | func bind(_ repository: Repository) { 5 | textLabel?.text = repository.fullName 6 | detailTextLabel?.text = "🌟\(repository.stargazersCount)" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Pagination/SearchRepositoriesViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import RxSwift 3 | 4 | class SearchRepositoriesViewController: UITableViewController { 5 | @IBOutlet weak var indicatorView: UIActivityIndicatorView! 6 | 7 | private let disposeBag = DisposeBag() 8 | private var viewModel: PaginationViewModel! 9 | 10 | override func viewDidLoad() { 11 | super.viewDidLoad() 12 | 13 | let baseRequest = GitHubAPI.SearchRepositoriesRequest(query: "Swift") 14 | 15 | viewModel = PaginationViewModel( 16 | baseRequest: baseRequest, 17 | viewWillAppear: rx.viewWillAppear.asDriver(), 18 | scrollViewDidReachBottom: tableView.rx.reachedBottom.asDriver()) 19 | 20 | disposeBag.insert([ 21 | viewModel.indicatorViewAnimating.drive(indicatorView.rx.isAnimating), 22 | viewModel.elements.drive(tableView.rx.items(cellIdentifier: "Cell", cellType: RepositoryCell.self)), 23 | viewModel.loadError.drive(onNext: { print($0) }), 24 | ]) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Pagination/Session+Rx.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RxSwift 3 | import APIKit 4 | 5 | extension Session: ReactiveCompatible { 6 | 7 | } 8 | 9 | extension Reactive where Base: Session { 10 | func response(_ request: T) -> Observable { 11 | return Observable.create { [weak base] observer in 12 | let task = base?.send(request) { result in 13 | switch result { 14 | case .success(let response): 15 | observer.on(.next(response)) 16 | observer.on(.completed) 17 | 18 | case .failure(let error): 19 | observer.onError(error) 20 | } 21 | } 22 | 23 | return Disposables.create { 24 | task?.cancel() 25 | } 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Pagination/UIScrollView+Rx.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import RxSwift 3 | import RxCocoa 4 | 5 | extension Reactive where Base: UIScrollView { 6 | var reachedBottom: ControlEvent { 7 | let observable = contentOffset 8 | .flatMap { [weak base] contentOffset -> Observable in 9 | guard let scrollView = base else { 10 | return Observable.empty() 11 | } 12 | 13 | let visibleHeight = scrollView.frame.height - scrollView.contentInset.top - scrollView.contentInset.bottom 14 | let y = contentOffset.y + scrollView.contentInset.top 15 | let threshold = max(0.0, scrollView.contentSize.height - visibleHeight) 16 | 17 | return y > threshold ? Observable.just() : Observable.empty() 18 | } 19 | 20 | return ControlEvent(events: observable) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Pagination/UITableView+Rx.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import RxSwift 3 | import RxCocoa 4 | 5 | protocol BindableCell { 6 | associatedtype Value 7 | func bind(_ value: Value) 8 | } 9 | 10 | extension Reactive where Base: UITableView { 11 | func items( 12 | cellIdentifier: String, 13 | cellType: Cell.Type) 14 | -> (O) 15 | -> (Disposable) 16 | where O.E == S, Cell: BindableCell, Cell.Value == S.Iterator.Element { 17 | return { source in 18 | let binder: (Int, Cell.Value, Cell) -> Void = { $2.bind($1) } 19 | return self.items(cellIdentifier: cellIdentifier, cellType: cellType)(source)(binder) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Pagination/UIViewController+Rx.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import RxSwift 3 | import RxCocoa 4 | 5 | extension Reactive where Base: UIViewController { 6 | private func controlEvent(for selector: Selector) -> ControlEvent { 7 | return ControlEvent(events: sentMessage(selector).map { _ in }) 8 | } 9 | 10 | var viewWillAppear: ControlEvent { 11 | return controlEvent(for: #selector(UIViewController.viewWillAppear)) 12 | } 13 | 14 | var viewDidAppear: ControlEvent { 15 | return controlEvent(for: #selector(UIViewController.viewDidAppear)) 16 | } 17 | 18 | var viewWillDisappear: ControlEvent { 19 | return controlEvent(for: #selector(UIViewController.viewWillDisappear)) 20 | } 21 | 22 | var viewDidDisappear: ControlEvent { 23 | return controlEvent(for: #selector(UIViewController.viewDidDisappear)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RxPagination 2 | 3 | This is the demo project for my presentation at try! Swift conference 2016. 4 | 5 | - Slides: https://speakerdeck.com/ishkawa/protocol-oriented-programming-in-networking 6 | - Video: https://news.realm.io/news/tryswift-yosuke-ishikawa-protocol-oriented-networking/ 7 | 8 | ## Set Up 9 | 10 | - `carthage bootstrap --platform iOS` 11 | 12 | ## Requirements 13 | 14 | - Swift 3.0.1 15 | - Xcode 8.1 16 | 17 | ## Summary 18 | 19 | This demo project illustrates how to use RxSwift, Action and APIKit. The demo app fetches repositories via GitHub search API and displays them using the libraries. 20 | 21 | 22 | 23 | ### ViewModel 24 | 25 | `PaginationViewModel` is a view model for pagination. It has an initializer with type parameter `Request`, which is constrained to conform to `PaginationRequest` protocol. When `PaginationViewModel` is instantiated via `init(baseRequest:)`, the type of its property that represents pagination elements will be inferred as `Observable<[Request.Response.Element]>`. 26 | 27 | ```swift 28 | class PaginationViewModel { 29 | let indicatorViewAnimating: Driver 30 | let elements: Driver<[Element]> 31 | let loadError: Driver 32 | 33 | init( 34 | baseRequest: Request, 35 | viewWillAppear: Driver, 36 | scrollViewDidReachBottom: Driver) where Request.Response.Element == Element {...} 37 | } 38 | ``` 39 | 40 | ### ViewController 41 | 42 | Once ViewModel is instantiated with a `Request` type parameter, remained task that ViewController have to do is binding input streams and output streams. 43 | 44 | 45 | ```swift 46 | class SearchRepositoriesViewController: UITableViewController { 47 | @IBOutlet weak var indicatorView: UIActivityIndicatorView! 48 | 49 | private let disposeBag = DisposeBag() 50 | private var viewModel: PaginationViewModel! 51 | 52 | override func viewDidLoad() { 53 | super.viewDidLoad() 54 | 55 | let baseRequest = GitHubAPI.SearchRepositoriesRequest(query: "Swift") 56 | 57 | viewModel = PaginationViewModel( 58 | baseRequest: baseRequest, 59 | viewWillAppear: rx.viewWillAppear.asDriver(), 60 | scrollViewDidReachBottom: tableView.rx.reachedBottom.asDriver()) 61 | 62 | disposeBag.insert([ 63 | viewModel.indicatorViewAnimating.drive(indicatorView.rx.isAnimating), 64 | viewModel.elements.drive(tableView.rx.items(cellIdentifier: "Cell", cellType: RepositoryCell.self)), 65 | viewModel.loadError.drive(onNext: { print($0) }), 66 | ]) 67 | } 68 | } 69 | ``` 70 | 71 | ## Contact 72 | 73 | Twitter: https://twitter.com/_ishkawa 74 | -------------------------------------------------------------------------------- /Tests/Fixture.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Fixture.swift 3 | // Pagination 4 | // 5 | // Created by Yosuke Ishikawa on 5/6/16. 6 | // Copyright © 2016 Yosuke Ishikawa. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum Fixture: String { 12 | case SearchRepositories 13 | 14 | var data: Data { 15 | guard let path = Bundle(for: Dummy.self).path(forResource: self.rawValue, ofType: "json") else { 16 | fatalError("Could not file named \(self.rawValue).json in test bundle.") 17 | } 18 | 19 | guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { 20 | fatalError("Could not read data from file at \(path).") 21 | } 22 | 23 | return data 24 | } 25 | 26 | private class Dummy { 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/PaginationViewModelTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import APIKit 4 | import RxSwift 5 | import RxCocoa 6 | import RxTest 7 | 8 | @testable import Pagination 9 | 10 | class PaginationViewModelTests: XCTestCase { 11 | var disposeBag: DisposeBag! 12 | var scheduler: TestScheduler! 13 | 14 | var sessionAdapter: TestSessionAdapter! 15 | var viewModel: PaginationViewModel! 16 | 17 | let viewWillAppear = PublishSubject() 18 | let scrollViewDidScroll = PublishSubject() 19 | 20 | override func setUp() { 21 | disposeBag = DisposeBag() 22 | scheduler = TestScheduler(initialClock: 0, resolution: 1, simulateProcessingDelay: false) 23 | 24 | driveOnScheduler(scheduler) { 25 | sessionAdapter = TestSessionAdapter() 26 | 27 | let session = Session(adapter: sessionAdapter, callbackQueue: .sessionQueue) 28 | let request = GitHubAPI.SearchRepositoriesRequest(query: "Swift") 29 | 30 | viewModel = PaginationViewModel( 31 | baseRequest: request, 32 | session: session, 33 | viewWillAppear: viewWillAppear.asDriver(onErrorDriveWith: .empty()), 34 | scrollViewDidReachBottom: scrollViewDidScroll.asDriver(onErrorDriveWith: .empty())) 35 | } 36 | } 37 | 38 | func test() { 39 | driveOnScheduler(scheduler) { 40 | let indicatorViewAnimating = scheduler.createObserver(Bool.self) 41 | let elementsCount = scheduler.createObserver(Int.self) 42 | 43 | let disposables = [ 44 | viewModel.indicatorViewAnimating.drive(indicatorViewAnimating), 45 | viewModel.elements.map({ $0.count }).drive(elementsCount), 46 | ] 47 | 48 | scheduler.scheduleAt(10) { self.viewWillAppear.onNext() } 49 | scheduler.scheduleAt(20) { self.sessionAdapter.return(data: Fixture.SearchRepositories.data) } 50 | scheduler.scheduleAt(30) { disposables.forEach { $0.dispose() } } 51 | scheduler.start() 52 | 53 | XCTAssertEqual(indicatorViewAnimating.events, [ 54 | next(0, false), 55 | next(10, true), 56 | next(20, false), 57 | ]) 58 | 59 | XCTAssertEqual(elementsCount.events, [ 60 | next(0, 0), 61 | next(20, 30), 62 | ]) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Tests/TestSessionAdapter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import APIKit 3 | 4 | class TestSessionAdapter: SessionAdapter { 5 | class Task: SessionTask { 6 | let handler: (Data?, URLResponse?, Error?) -> Void 7 | 8 | init(handler: @escaping (Data?, URLResponse?, Error?) -> Void) { 9 | self.handler = handler 10 | } 11 | 12 | func resume() {} 13 | func cancel() {} 14 | } 15 | 16 | var tasks = [Task]() 17 | 18 | func `return`(data: Data) { 19 | guard !tasks.isEmpty else { 20 | return 21 | } 22 | 23 | let task = tasks.removeFirst() 24 | let urlResponse = HTTPURLResponse( 25 | url: URL(string: "https://example.com")!, 26 | statusCode: 200, 27 | httpVersion: nil, 28 | headerFields: nil) 29 | 30 | task.handler(data, urlResponse, nil) 31 | } 32 | 33 | // MARK: SessionAdapterType 34 | func createTask(with URLRequest: URLRequest, handler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask { 35 | let task = Task(handler: handler) 36 | tasks.append(task) 37 | return task 38 | } 39 | 40 | func getTasks(with handler: @escaping ([SessionTask]) -> Void) { 41 | handler([]) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryswift/RxPagination/f46005f266afcfea993368cf971353020649cb1a/screenshot.png --------------------------------------------------------------------------------