├── .github └── workflows │ └── blank.yml ├── .gitignore ├── Podfile ├── README.md ├── RxMVVM+Texture.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── vingle.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── vingle.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── RxMVVM+Texture.xcworkspace ├── contents.xcworkspacedata └── xcuserdata │ └── vingle.xcuserdatad │ ├── UserInterfaceState.xcuserstate │ └── xcdebugger │ └── Breakpoints_v2.xcbkptlist ├── RxMVVM+Texture ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── controllers │ ├── RepositoryViewController.swift │ └── UserProfileViewController.swift ├── dataProvider │ └── RepoProvider.swift ├── extensions │ └── Decoder+extension.swift ├── models │ ├── Repository.swift │ └── User.swift ├── networks │ ├── Network.swift │ └── RepoService.swift ├── nodes │ └── RepositoryListCellNode.swift └── viewmodels │ └── RepositoryViewModel.swift ├── RxMVVM+TextureTests ├── Info.plist └── RxMVVM_TextureTests.swift └── resource ├── resource1.png ├── resource2.png ├── resource3.png └── resource4.png /.github/workflows/blank.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: macOS-latest 8 | 9 | steps: 10 | - name: Check out github repository 11 | uses: actions/checkout@v1 12 | - name: Install cocoapods 13 | run: gem install cocoapods 14 | - name: Install pod 15 | run: pod install --repo-update --project-directory=./ 16 | - name: Run xcode build 17 | run: | 18 | xcodebuild clean build test -workspace RxMVVM+Texture.xcworkspace -scheme RxMVVM+Texture -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 8,OS=12.0' -configuration Debug -enableCodeCoverage YES CODE_SIGNING_REQUIRED=NO | xcpretty 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .Trashes 3 | *.lock 4 | Pods/ 5 | build/**/* 6 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | 3 | target 'RxMVVM+Texture' do 4 | pod 'Texture' 5 | pod 'SnapKit' 6 | pod 'RxSwift' 7 | pod 'RxCocoa' 8 | pod 'RxAlamofire' 9 | pod 'MBProgressHUD' 10 | pod 'RxCocoa-Texture', :git => 'https://github.com/GeekTree0101/RxCocoa-Texture.git', :branch => 'Texture-2.7' 11 | end 12 | 13 | post_install do |installer| 14 | installer.pods_project.targets.each do |target| 15 | target.build_configurations.each do |config| 16 | config.build_settings['CONFIGURATION_BUILD_DIR'] = '$PODS_CONFIGURATION_BUILD_DIR' 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RxMVVM-Texture best practice 2 | 3 | ## RxSwift MVVM pattern best practice built on Texture(AsyncDisplayKit) and written in Swift 4 | 5 | ![alt text](https://github.com/GeekTree0101/RxMVVM-Texture/blob/master/resource/resource1.png) 6 | 7 | ### [ Model ] 8 | 9 | ```swift 10 | class Repository: Decodable { 11 | var id: Int 12 | var user: User? 13 | var repositoryName: String? 14 | var desc: String? 15 | var isPrivate: Bool = false 16 | var isForked: Bool = false 17 | 18 | enum CodingKeys: String, CodingKey { 19 | case id = "id" 20 | case user = "owner" 21 | case repositoryName = "full_name" 22 | case desc = "description" 23 | case isPrivate = "private" 24 | case isForked = "fork" 25 | } 26 | 27 | func merge(_ repo: Repository?) { 28 | guard let repo = repo else { return } 29 | user?.merge(repo.user) 30 | repositoryName = repo.repositoryName 31 | desc = repo.desc 32 | isPrivate = repo.isPrivate 33 | isForked = repo.isForked 34 | } 35 | ``` 36 | 37 | ### [ ViewModel ] 38 | 39 | ```swift 40 | class RepositoryViewModel { 41 | 42 | // @INPUT 43 | let didTapUserProfile = PublishRelay() 44 | let updateRepository = PublishRelay() 45 | let updateUsername = PublishRelay() 46 | let updateDescription = PublishRelay() 47 | 48 | // @OUTPUT 49 | var openUserProfile: Observable 50 | var username: Driver 51 | var profileURL: Driver 52 | var desc: Driver 53 | var status: Driver 54 | 55 | let id: Int 56 | 57 | private let disposeBag = DisposeBag() 58 | 59 | deinit { 60 | // release Model from DataProvider 61 | RepoProvider.release(id: id) 62 | } 63 | 64 | init(repository: Repository) { 65 | self.id = repository.id 66 | 67 | // retain Model to DataProvider 68 | RepoProvider.addAndUpdate(repository) 69 | 70 | // load Model Observer from ModelProvider 71 | let repoObserver = RepoProvider.observable(id: id) 72 | .asObservable() 73 | .share(replay: 1, scope: .whileConnected) 74 | 75 | self.username = repoObserver 76 | .map { $0?.user?.username } 77 | .asDriver(onErrorJustReturn: nil) 78 | 79 | self.profileURL = repoObserver 80 | .map { $0?.user?.profileURL } 81 | .asDriver(onErrorJustReturn: nil) 82 | 83 | self.desc = repoObserver 84 | .map { $0?.desc } 85 | .asDriver(onErrorJustReturn: nil) 86 | ``` 87 | 88 | ### [ View ] 89 | 90 | ```swift 91 | class RepositoryListCellNode: ASCellNode { 92 | 93 | init(viewModel: RepositoryViewModel) { 94 | 95 | ... 96 | 97 | // ViewModel Binding 98 | 99 | userProfileNode.rx 100 | .tap(to: viewModel.didTapUserProfile) 101 | .disposed(by: disposeBag) 102 | 103 | viewModel.profileURL.asObservable() 104 | .bind(to: userProfileNode.rx.url) 105 | .disposed(by: disposeBag) 106 | 107 | viewModel.username.asObservable() 108 | .bind(to: usernameNode.rx.text(Node.usernameAttributes), 109 | setNeedsLayout: self) 110 | .disposed(by: disposeBag) 111 | 112 | viewModel.desc.asObservable() 113 | .bind(to: descriptionNode.rx.text(Node.descAttributes), 114 | setNeedsLayout: self) 115 | .disposed(by: disposeBag) 116 | 117 | viewModel.status.asObservable() 118 | .bind(to: statusNode.rx.text(Node.statusAttributes), 119 | setNeedsLayout: self) 120 | .disposed(by: disposeBag) 121 | } 122 | 123 | ``` 124 | 125 | ![alt text](https://github.com/GeekTree0101/RxMVVM-Texture/blob/master/resource/resource2.png) 126 | 127 | ### Open Profile 128 | 129 | ```swift 130 | class RepositoryListCellNode: ASCellNode { 131 | 132 | ... 133 | 134 | init(viewModel: RepositoryViewModel) { 135 | 136 | ... 137 | 138 | // HERE! 139 | userProfileNode.rx 140 | .tap(to: viewModel.didTapUserProfile) 141 | .disposed(by: disposeBag) 142 | } 143 | 144 | ``` 145 | 146 | ```swift 147 | class RepositoryViewModel { 148 | // @INPUT 149 | let didTapUserProfile = PublishRelay() 150 | 151 | // @OUTPUT 152 | var openUserProfile: Observable 153 | 154 | ... 155 | 156 | init(repository: Repository) { 157 | 158 | ... 159 | 160 | // HERE! 161 | self.openUserProfile = self.didTapUserProfile.asObservable() 162 | } 163 | 164 | } 165 | 166 | ``` 167 | 168 | ```swift 169 | 170 | class RepositoryViewController: ASViewController { 171 | 172 | ... 173 | 174 | func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) 175 | -> ASCellNodeBlock { 176 | return { 177 | guard self.items.count > indexPath.row else { return ASCellNode() } 178 | let viewModel = self.items[indexPath.row] 179 | let cellNode = RepositoryListCellNode(viewModel: viewModel) 180 | 181 | // HERE! 182 | viewModel.openUserProfile 183 | .observeOn(MainScheduler.asyncInstance) 184 | .subscribe(onNext: { [weak self] _ in 185 | self?.openUserProfile(indexPath: indexPath) 186 | }).disposed(by: self.disposeBag) 187 | 188 | return cellNode 189 | } 190 | } 191 | 192 | } 193 | 194 | ``` 195 | 196 | ![alt text](https://github.com/GeekTree0101/RxMVVM-Texture/blob/master/resource/resource3.png) 197 | 198 | 199 | ### Update description 200 | ```swift 201 | 202 | class UserProfileViewController: ASViewController { 203 | 204 | ... 205 | 206 | 207 | init(viewModel: ...) { 208 | 209 | ... 210 | 211 | 212 | // HERE! 213 | self.descriptionNode.textView.rx.text 214 | .bind(to: self.viewModel.updateDescription, 215 | setNeedsLayout: self.node) 216 | .disposed(by: self.disposeBag) 217 | } 218 | } 219 | 220 | ``` 221 | 222 | ```swift 223 | 224 | class RepositoryViewModel { 225 | 226 | // @INPUT 227 | let updateDescription = PublishRelay() 228 | 229 | // @OUTPUT 230 | var desc: Driver 231 | 232 | init( ... ) { 233 | 234 | ... 235 | 236 | let repoObserver = RepoProvider.observable(id: id) 237 | .asObservable() 238 | .share(replay: 1, scope: .whileConnected) 239 | 240 | self.desc = repoObserver 241 | .map { $0?.desc } 242 | .asDriver(onErrorJustReturn: nil) 243 | 244 | updateDescription.withLatestFrom(repoObserver) { ($0, $1) } 245 | .subscribe(onNext: { text, repo in 246 | guard let repo = repo else { return } 247 | repo.desc = text 248 | RepoProvider.update(repo) 249 | }).disposed(by: disposeBag) 250 | } 251 | } 252 | ``` 253 | 254 | ![alt text](https://github.com/GeekTree0101/RxMVVM-Texture/blob/master/resource/resource4.png) 255 | 256 | ### Example Video 257 | [Example Video Link](https://youtu.be/qFu2hJG-OyE) 258 | -------------------------------------------------------------------------------- /RxMVVM+Texture.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 9B0E3BB320A6C6D0007346CF /* RepoProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B0E3BB220A6C6D0007346CF /* RepoProvider.swift */; }; 11 | 9B3895ED201C252A007DB34C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B3895EC201C252A007DB34C /* AppDelegate.swift */; }; 12 | 9B3895F4201C252A007DB34C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9B3895F3201C252A007DB34C /* Assets.xcassets */; }; 13 | 9B389602201C252A007DB34C /* RxMVVM_TextureTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B389601201C252A007DB34C /* RxMVVM_TextureTests.swift */; }; 14 | 9B389613201C2810007DB34C /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B389612201C2810007DB34C /* Network.swift */; }; 15 | 9B389615201C3434007DB34C /* Repository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B389614201C3434007DB34C /* Repository.swift */; }; 16 | 9B389617201C350B007DB34C /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B389616201C350B007DB34C /* User.swift */; }; 17 | 9B38961A201C4325007DB34C /* Decoder+extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B389619201C4325007DB34C /* Decoder+extension.swift */; }; 18 | 9B38961C201C5118007DB34C /* RepositoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B38961B201C5118007DB34C /* RepositoryViewModel.swift */; }; 19 | 9B38961E201C554E007DB34C /* RepositoryListCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B38961D201C554E007DB34C /* RepositoryListCellNode.swift */; }; 20 | 9B389620201C5863007DB34C /* RepositoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B38961F201C5863007DB34C /* RepositoryViewController.swift */; }; 21 | 9B389622201C5AD2007DB34C /* RepoService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B389621201C5AD2007DB34C /* RepoService.swift */; }; 22 | 9B389626201C7655007DB34C /* UserProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B389625201C7655007DB34C /* UserProfileViewController.swift */; }; 23 | A0D7B788D2E3FF4F453FE758 /* Pods_RxMVVM_Texture.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8F442244C164AA9492BBD3C6 /* Pods_RxMVVM_Texture.framework */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXContainerItemProxy section */ 27 | 9B3895FE201C252A007DB34C /* PBXContainerItemProxy */ = { 28 | isa = PBXContainerItemProxy; 29 | containerPortal = 9B3895E1201C252A007DB34C /* Project object */; 30 | proxyType = 1; 31 | remoteGlobalIDString = 9B3895E8201C252A007DB34C; 32 | remoteInfo = "RxMVVM+Texture"; 33 | }; 34 | /* End PBXContainerItemProxy section */ 35 | 36 | /* Begin PBXFileReference section */ 37 | 1DAC9B1E50E19AE16F5BEBE9 /* Pods-RxMVVM+Texture.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RxMVVM+Texture.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RxMVVM+Texture/Pods-RxMVVM+Texture.debug.xcconfig"; sourceTree = ""; }; 38 | 6C13371F9279357D9A5031FD /* Pods-RxMVVM+Texture.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RxMVVM+Texture.release.xcconfig"; path = "Pods/Target Support Files/Pods-RxMVVM+Texture/Pods-RxMVVM+Texture.release.xcconfig"; sourceTree = ""; }; 39 | 8F442244C164AA9492BBD3C6 /* Pods_RxMVVM_Texture.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RxMVVM_Texture.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | 9B0E3BB220A6C6D0007346CF /* RepoProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepoProvider.swift; sourceTree = ""; }; 41 | 9B3895E9201C252A007DB34C /* RxMVVM+Texture.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "RxMVVM+Texture.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | 9B3895EC201C252A007DB34C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 43 | 9B3895F3201C252A007DB34C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 44 | 9B3895F8201C252A007DB34C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | 9B3895FD201C252A007DB34C /* RxMVVM+TextureTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "RxMVVM+TextureTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 9B389601201C252A007DB34C /* RxMVVM_TextureTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RxMVVM_TextureTests.swift; sourceTree = ""; }; 47 | 9B389603201C252A007DB34C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | 9B389612201C2810007DB34C /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; 49 | 9B389614201C3434007DB34C /* Repository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Repository.swift; sourceTree = ""; }; 50 | 9B389616201C350B007DB34C /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 51 | 9B389619201C4325007DB34C /* Decoder+extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decoder+extension.swift"; sourceTree = ""; }; 52 | 9B38961B201C5118007DB34C /* RepositoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryViewModel.swift; sourceTree = ""; }; 53 | 9B38961D201C554E007DB34C /* RepositoryListCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryListCellNode.swift; sourceTree = ""; }; 54 | 9B38961F201C5863007DB34C /* RepositoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryViewController.swift; sourceTree = ""; }; 55 | 9B389621201C5AD2007DB34C /* RepoService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepoService.swift; sourceTree = ""; }; 56 | 9B389625201C7655007DB34C /* UserProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileViewController.swift; sourceTree = ""; }; 57 | /* End PBXFileReference section */ 58 | 59 | /* Begin PBXFrameworksBuildPhase section */ 60 | 9B3895E6201C252A007DB34C /* Frameworks */ = { 61 | isa = PBXFrameworksBuildPhase; 62 | buildActionMask = 2147483647; 63 | files = ( 64 | A0D7B788D2E3FF4F453FE758 /* Pods_RxMVVM_Texture.framework in Frameworks */, 65 | ); 66 | runOnlyForDeploymentPostprocessing = 0; 67 | }; 68 | 9B3895FA201C252A007DB34C /* Frameworks */ = { 69 | isa = PBXFrameworksBuildPhase; 70 | buildActionMask = 2147483647; 71 | files = ( 72 | ); 73 | runOnlyForDeploymentPostprocessing = 0; 74 | }; 75 | /* End PBXFrameworksBuildPhase section */ 76 | 77 | /* Begin PBXGroup section */ 78 | 6F372DC6261C869B07A0BF1E /* Frameworks */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 8F442244C164AA9492BBD3C6 /* Pods_RxMVVM_Texture.framework */, 82 | ); 83 | name = Frameworks; 84 | sourceTree = ""; 85 | }; 86 | 7F0C91DAEAF80E02F522A8B6 /* Pods */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 1DAC9B1E50E19AE16F5BEBE9 /* Pods-RxMVVM+Texture.debug.xcconfig */, 90 | 6C13371F9279357D9A5031FD /* Pods-RxMVVM+Texture.release.xcconfig */, 91 | ); 92 | name = Pods; 93 | sourceTree = ""; 94 | }; 95 | 9B0E3BB120A6C6C0007346CF /* dataProvider */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | 9B0E3BB220A6C6D0007346CF /* RepoProvider.swift */, 99 | ); 100 | path = dataProvider; 101 | sourceTree = ""; 102 | }; 103 | 9B3895E0201C252A007DB34C = { 104 | isa = PBXGroup; 105 | children = ( 106 | 9B3895EB201C252A007DB34C /* RxMVVM+Texture */, 107 | 9B389600201C252A007DB34C /* RxMVVM+TextureTests */, 108 | 9B3895EA201C252A007DB34C /* Products */, 109 | 7F0C91DAEAF80E02F522A8B6 /* Pods */, 110 | 6F372DC6261C869B07A0BF1E /* Frameworks */, 111 | ); 112 | sourceTree = ""; 113 | }; 114 | 9B3895EA201C252A007DB34C /* Products */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 9B3895E9201C252A007DB34C /* RxMVVM+Texture.app */, 118 | 9B3895FD201C252A007DB34C /* RxMVVM+TextureTests.xctest */, 119 | ); 120 | name = Products; 121 | sourceTree = ""; 122 | }; 123 | 9B3895EB201C252A007DB34C /* RxMVVM+Texture */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 9B0E3BB120A6C6C0007346CF /* dataProvider */, 127 | 9B389618201C430D007DB34C /* extensions */, 128 | 9B389611201C273A007DB34C /* viewmodels */, 129 | 9B389610201C26FD007DB34C /* networks */, 130 | 9B38960F201C26F4007DB34C /* nodes */, 131 | 9B38960E201C26EF007DB34C /* models */, 132 | 9B38960C201C26E2007DB34C /* controllers */, 133 | 9B3895EC201C252A007DB34C /* AppDelegate.swift */, 134 | 9B3895F3201C252A007DB34C /* Assets.xcassets */, 135 | 9B3895F8201C252A007DB34C /* Info.plist */, 136 | ); 137 | path = "RxMVVM+Texture"; 138 | sourceTree = ""; 139 | }; 140 | 9B389600201C252A007DB34C /* RxMVVM+TextureTests */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 9B389601201C252A007DB34C /* RxMVVM_TextureTests.swift */, 144 | 9B389603201C252A007DB34C /* Info.plist */, 145 | ); 146 | path = "RxMVVM+TextureTests"; 147 | sourceTree = ""; 148 | }; 149 | 9B38960C201C26E2007DB34C /* controllers */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | 9B38961F201C5863007DB34C /* RepositoryViewController.swift */, 153 | 9B389625201C7655007DB34C /* UserProfileViewController.swift */, 154 | ); 155 | path = controllers; 156 | sourceTree = ""; 157 | }; 158 | 9B38960E201C26EF007DB34C /* models */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | 9B389614201C3434007DB34C /* Repository.swift */, 162 | 9B389616201C350B007DB34C /* User.swift */, 163 | ); 164 | path = models; 165 | sourceTree = ""; 166 | }; 167 | 9B38960F201C26F4007DB34C /* nodes */ = { 168 | isa = PBXGroup; 169 | children = ( 170 | 9B38961D201C554E007DB34C /* RepositoryListCellNode.swift */, 171 | ); 172 | path = nodes; 173 | sourceTree = ""; 174 | }; 175 | 9B389610201C26FD007DB34C /* networks */ = { 176 | isa = PBXGroup; 177 | children = ( 178 | 9B389612201C2810007DB34C /* Network.swift */, 179 | 9B389621201C5AD2007DB34C /* RepoService.swift */, 180 | ); 181 | path = networks; 182 | sourceTree = ""; 183 | }; 184 | 9B389611201C273A007DB34C /* viewmodels */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 9B38961B201C5118007DB34C /* RepositoryViewModel.swift */, 188 | ); 189 | path = viewmodels; 190 | sourceTree = ""; 191 | }; 192 | 9B389618201C430D007DB34C /* extensions */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | 9B389619201C4325007DB34C /* Decoder+extension.swift */, 196 | ); 197 | path = extensions; 198 | sourceTree = ""; 199 | }; 200 | /* End PBXGroup section */ 201 | 202 | /* Begin PBXNativeTarget section */ 203 | 9B3895E8201C252A007DB34C /* RxMVVM+Texture */ = { 204 | isa = PBXNativeTarget; 205 | buildConfigurationList = 9B389606201C252A007DB34C /* Build configuration list for PBXNativeTarget "RxMVVM+Texture" */; 206 | buildPhases = ( 207 | 6BF72EF4937696EB02FF9903 /* [CP] Check Pods Manifest.lock */, 208 | 9B3895E5201C252A007DB34C /* Sources */, 209 | 9B3895E6201C252A007DB34C /* Frameworks */, 210 | 9B3895E7201C252A007DB34C /* Resources */, 211 | 7B778EAF26C910DDA1977486 /* [CP] Embed Pods Frameworks */, 212 | ); 213 | buildRules = ( 214 | ); 215 | dependencies = ( 216 | ); 217 | name = "RxMVVM+Texture"; 218 | productName = "RxMVVM+Texture"; 219 | productReference = 9B3895E9201C252A007DB34C /* RxMVVM+Texture.app */; 220 | productType = "com.apple.product-type.application"; 221 | }; 222 | 9B3895FC201C252A007DB34C /* RxMVVM+TextureTests */ = { 223 | isa = PBXNativeTarget; 224 | buildConfigurationList = 9B389609201C252A007DB34C /* Build configuration list for PBXNativeTarget "RxMVVM+TextureTests" */; 225 | buildPhases = ( 226 | 9B3895F9201C252A007DB34C /* Sources */, 227 | 9B3895FA201C252A007DB34C /* Frameworks */, 228 | 9B3895FB201C252A007DB34C /* Resources */, 229 | ); 230 | buildRules = ( 231 | ); 232 | dependencies = ( 233 | 9B3895FF201C252A007DB34C /* PBXTargetDependency */, 234 | ); 235 | name = "RxMVVM+TextureTests"; 236 | productName = "RxMVVM+TextureTests"; 237 | productReference = 9B3895FD201C252A007DB34C /* RxMVVM+TextureTests.xctest */; 238 | productType = "com.apple.product-type.bundle.unit-test"; 239 | }; 240 | /* End PBXNativeTarget section */ 241 | 242 | /* Begin PBXProject section */ 243 | 9B3895E1201C252A007DB34C /* Project object */ = { 244 | isa = PBXProject; 245 | attributes = { 246 | LastSwiftUpdateCheck = 0920; 247 | LastUpgradeCheck = 0920; 248 | ORGANIZATIONNAME = Geektree0101; 249 | TargetAttributes = { 250 | 9B3895E8201C252A007DB34C = { 251 | CreatedOnToolsVersion = 9.2; 252 | ProvisioningStyle = Automatic; 253 | }; 254 | 9B3895FC201C252A007DB34C = { 255 | CreatedOnToolsVersion = 9.2; 256 | ProvisioningStyle = Automatic; 257 | TestTargetID = 9B3895E8201C252A007DB34C; 258 | }; 259 | }; 260 | }; 261 | buildConfigurationList = 9B3895E4201C252A007DB34C /* Build configuration list for PBXProject "RxMVVM+Texture" */; 262 | compatibilityVersion = "Xcode 8.0"; 263 | developmentRegion = en; 264 | hasScannedForEncodings = 0; 265 | knownRegions = ( 266 | en, 267 | Base, 268 | ); 269 | mainGroup = 9B3895E0201C252A007DB34C; 270 | productRefGroup = 9B3895EA201C252A007DB34C /* Products */; 271 | projectDirPath = ""; 272 | projectRoot = ""; 273 | targets = ( 274 | 9B3895E8201C252A007DB34C /* RxMVVM+Texture */, 275 | 9B3895FC201C252A007DB34C /* RxMVVM+TextureTests */, 276 | ); 277 | }; 278 | /* End PBXProject section */ 279 | 280 | /* Begin PBXResourcesBuildPhase section */ 281 | 9B3895E7201C252A007DB34C /* Resources */ = { 282 | isa = PBXResourcesBuildPhase; 283 | buildActionMask = 2147483647; 284 | files = ( 285 | 9B3895F4201C252A007DB34C /* Assets.xcassets in Resources */, 286 | ); 287 | runOnlyForDeploymentPostprocessing = 0; 288 | }; 289 | 9B3895FB201C252A007DB34C /* Resources */ = { 290 | isa = PBXResourcesBuildPhase; 291 | buildActionMask = 2147483647; 292 | files = ( 293 | ); 294 | runOnlyForDeploymentPostprocessing = 0; 295 | }; 296 | /* End PBXResourcesBuildPhase section */ 297 | 298 | /* Begin PBXShellScriptBuildPhase section */ 299 | 6BF72EF4937696EB02FF9903 /* [CP] Check Pods Manifest.lock */ = { 300 | isa = PBXShellScriptBuildPhase; 301 | buildActionMask = 2147483647; 302 | files = ( 303 | ); 304 | inputPaths = ( 305 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 306 | "${PODS_ROOT}/Manifest.lock", 307 | ); 308 | name = "[CP] Check Pods Manifest.lock"; 309 | outputPaths = ( 310 | "$(DERIVED_FILE_DIR)/Pods-RxMVVM+Texture-checkManifestLockResult.txt", 311 | ); 312 | runOnlyForDeploymentPostprocessing = 0; 313 | shellPath = /bin/sh; 314 | 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"; 315 | showEnvVarsInLog = 0; 316 | }; 317 | 7B778EAF26C910DDA1977486 /* [CP] Embed Pods Frameworks */ = { 318 | isa = PBXShellScriptBuildPhase; 319 | buildActionMask = 2147483647; 320 | files = ( 321 | ); 322 | inputPaths = ( 323 | "${SRCROOT}/Pods/Target Support Files/Pods-RxMVVM+Texture/Pods-RxMVVM+Texture-frameworks.sh", 324 | "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", 325 | "${BUILT_PRODUCTS_DIR}/MBProgressHUD/MBProgressHUD.framework", 326 | "${BUILT_PRODUCTS_DIR}/PINCache/PINCache.framework", 327 | "${BUILT_PRODUCTS_DIR}/PINOperation/PINOperation.framework", 328 | "${BUILT_PRODUCTS_DIR}/PINRemoteImage/PINRemoteImage.framework", 329 | "${BUILT_PRODUCTS_DIR}/RxAlamofire/RxAlamofire.framework", 330 | "${BUILT_PRODUCTS_DIR}/RxCocoa/RxCocoa.framework", 331 | "${BUILT_PRODUCTS_DIR}/RxCocoa-Texture/RxCocoa_Texture.framework", 332 | "${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework", 333 | "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", 334 | "${BUILT_PRODUCTS_DIR}/Texture/AsyncDisplayKit.framework", 335 | ); 336 | name = "[CP] Embed Pods Frameworks"; 337 | outputPaths = ( 338 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", 339 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MBProgressHUD.framework", 340 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINCache.framework", 341 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINOperation.framework", 342 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PINRemoteImage.framework", 343 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxAlamofire.framework", 344 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxCocoa.framework", 345 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxCocoa_Texture.framework", 346 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxSwift.framework", 347 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", 348 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AsyncDisplayKit.framework", 349 | ); 350 | runOnlyForDeploymentPostprocessing = 0; 351 | shellPath = /bin/sh; 352 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-RxMVVM+Texture/Pods-RxMVVM+Texture-frameworks.sh\"\n"; 353 | showEnvVarsInLog = 0; 354 | }; 355 | /* End PBXShellScriptBuildPhase section */ 356 | 357 | /* Begin PBXSourcesBuildPhase section */ 358 | 9B3895E5201C252A007DB34C /* Sources */ = { 359 | isa = PBXSourcesBuildPhase; 360 | buildActionMask = 2147483647; 361 | files = ( 362 | 9B38961C201C5118007DB34C /* RepositoryViewModel.swift in Sources */, 363 | 9B0E3BB320A6C6D0007346CF /* RepoProvider.swift in Sources */, 364 | 9B389626201C7655007DB34C /* UserProfileViewController.swift in Sources */, 365 | 9B38961E201C554E007DB34C /* RepositoryListCellNode.swift in Sources */, 366 | 9B38961A201C4325007DB34C /* Decoder+extension.swift in Sources */, 367 | 9B389620201C5863007DB34C /* RepositoryViewController.swift in Sources */, 368 | 9B3895ED201C252A007DB34C /* AppDelegate.swift in Sources */, 369 | 9B389613201C2810007DB34C /* Network.swift in Sources */, 370 | 9B389617201C350B007DB34C /* User.swift in Sources */, 371 | 9B389615201C3434007DB34C /* Repository.swift in Sources */, 372 | 9B389622201C5AD2007DB34C /* RepoService.swift in Sources */, 373 | ); 374 | runOnlyForDeploymentPostprocessing = 0; 375 | }; 376 | 9B3895F9201C252A007DB34C /* Sources */ = { 377 | isa = PBXSourcesBuildPhase; 378 | buildActionMask = 2147483647; 379 | files = ( 380 | 9B389602201C252A007DB34C /* RxMVVM_TextureTests.swift in Sources */, 381 | ); 382 | runOnlyForDeploymentPostprocessing = 0; 383 | }; 384 | /* End PBXSourcesBuildPhase section */ 385 | 386 | /* Begin PBXTargetDependency section */ 387 | 9B3895FF201C252A007DB34C /* PBXTargetDependency */ = { 388 | isa = PBXTargetDependency; 389 | target = 9B3895E8201C252A007DB34C /* RxMVVM+Texture */; 390 | targetProxy = 9B3895FE201C252A007DB34C /* PBXContainerItemProxy */; 391 | }; 392 | /* End PBXTargetDependency section */ 393 | 394 | /* Begin XCBuildConfiguration section */ 395 | 9B389604201C252A007DB34C /* Debug */ = { 396 | isa = XCBuildConfiguration; 397 | buildSettings = { 398 | ALWAYS_SEARCH_USER_PATHS = NO; 399 | CLANG_ANALYZER_NONNULL = YES; 400 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 401 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 402 | CLANG_CXX_LIBRARY = "libc++"; 403 | CLANG_ENABLE_MODULES = YES; 404 | CLANG_ENABLE_OBJC_ARC = YES; 405 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 406 | CLANG_WARN_BOOL_CONVERSION = YES; 407 | CLANG_WARN_COMMA = YES; 408 | CLANG_WARN_CONSTANT_CONVERSION = YES; 409 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 410 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 411 | CLANG_WARN_EMPTY_BODY = YES; 412 | CLANG_WARN_ENUM_CONVERSION = YES; 413 | CLANG_WARN_INFINITE_RECURSION = YES; 414 | CLANG_WARN_INT_CONVERSION = YES; 415 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 416 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 417 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 418 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 419 | CLANG_WARN_STRICT_PROTOTYPES = YES; 420 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 421 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 422 | CLANG_WARN_UNREACHABLE_CODE = YES; 423 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 424 | CODE_SIGN_IDENTITY = "iPhone Developer"; 425 | COPY_PHASE_STRIP = NO; 426 | DEBUG_INFORMATION_FORMAT = dwarf; 427 | ENABLE_STRICT_OBJC_MSGSEND = YES; 428 | ENABLE_TESTABILITY = YES; 429 | GCC_C_LANGUAGE_STANDARD = gnu11; 430 | GCC_DYNAMIC_NO_PIC = NO; 431 | GCC_NO_COMMON_BLOCKS = YES; 432 | GCC_OPTIMIZATION_LEVEL = 0; 433 | GCC_PREPROCESSOR_DEFINITIONS = ( 434 | "DEBUG=1", 435 | "$(inherited)", 436 | ); 437 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 438 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 439 | GCC_WARN_UNDECLARED_SELECTOR = YES; 440 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 441 | GCC_WARN_UNUSED_FUNCTION = YES; 442 | GCC_WARN_UNUSED_VARIABLE = YES; 443 | IPHONEOS_DEPLOYMENT_TARGET = 11.2; 444 | MTL_ENABLE_DEBUG_INFO = YES; 445 | ONLY_ACTIVE_ARCH = YES; 446 | SDKROOT = iphoneos; 447 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 448 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 449 | }; 450 | name = Debug; 451 | }; 452 | 9B389605201C252A007DB34C /* Release */ = { 453 | isa = XCBuildConfiguration; 454 | buildSettings = { 455 | ALWAYS_SEARCH_USER_PATHS = NO; 456 | CLANG_ANALYZER_NONNULL = YES; 457 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 458 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 459 | CLANG_CXX_LIBRARY = "libc++"; 460 | CLANG_ENABLE_MODULES = YES; 461 | CLANG_ENABLE_OBJC_ARC = YES; 462 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 463 | CLANG_WARN_BOOL_CONVERSION = YES; 464 | CLANG_WARN_COMMA = YES; 465 | CLANG_WARN_CONSTANT_CONVERSION = YES; 466 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 467 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 468 | CLANG_WARN_EMPTY_BODY = YES; 469 | CLANG_WARN_ENUM_CONVERSION = YES; 470 | CLANG_WARN_INFINITE_RECURSION = YES; 471 | CLANG_WARN_INT_CONVERSION = YES; 472 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 473 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 474 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 475 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 476 | CLANG_WARN_STRICT_PROTOTYPES = YES; 477 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 478 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 479 | CLANG_WARN_UNREACHABLE_CODE = YES; 480 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 481 | CODE_SIGN_IDENTITY = "iPhone Developer"; 482 | COPY_PHASE_STRIP = NO; 483 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 484 | ENABLE_NS_ASSERTIONS = NO; 485 | ENABLE_STRICT_OBJC_MSGSEND = YES; 486 | GCC_C_LANGUAGE_STANDARD = gnu11; 487 | GCC_NO_COMMON_BLOCKS = YES; 488 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 489 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 490 | GCC_WARN_UNDECLARED_SELECTOR = YES; 491 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 492 | GCC_WARN_UNUSED_FUNCTION = YES; 493 | GCC_WARN_UNUSED_VARIABLE = YES; 494 | IPHONEOS_DEPLOYMENT_TARGET = 11.2; 495 | MTL_ENABLE_DEBUG_INFO = NO; 496 | SDKROOT = iphoneos; 497 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 498 | VALIDATE_PRODUCT = YES; 499 | }; 500 | name = Release; 501 | }; 502 | 9B389607201C252A007DB34C /* Debug */ = { 503 | isa = XCBuildConfiguration; 504 | baseConfigurationReference = 1DAC9B1E50E19AE16F5BEBE9 /* Pods-RxMVVM+Texture.debug.xcconfig */; 505 | buildSettings = { 506 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 507 | CODE_SIGN_STYLE = Automatic; 508 | DEVELOPMENT_TEAM = JTEXWEH4CZ; 509 | INFOPLIST_FILE = "RxMVVM+Texture/Info.plist"; 510 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 511 | PRODUCT_BUNDLE_IDENTIFIER = "Geektree0101.RxMVVM-Texture"; 512 | PRODUCT_NAME = "$(TARGET_NAME)"; 513 | SWIFT_VERSION = 4.0; 514 | TARGETED_DEVICE_FAMILY = 1; 515 | }; 516 | name = Debug; 517 | }; 518 | 9B389608201C252A007DB34C /* Release */ = { 519 | isa = XCBuildConfiguration; 520 | baseConfigurationReference = 6C13371F9279357D9A5031FD /* Pods-RxMVVM+Texture.release.xcconfig */; 521 | buildSettings = { 522 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 523 | CODE_SIGN_STYLE = Automatic; 524 | DEVELOPMENT_TEAM = JTEXWEH4CZ; 525 | INFOPLIST_FILE = "RxMVVM+Texture/Info.plist"; 526 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 527 | PRODUCT_BUNDLE_IDENTIFIER = "Geektree0101.RxMVVM-Texture"; 528 | PRODUCT_NAME = "$(TARGET_NAME)"; 529 | SWIFT_VERSION = 4.0; 530 | TARGETED_DEVICE_FAMILY = 1; 531 | }; 532 | name = Release; 533 | }; 534 | 9B38960A201C252A007DB34C /* Debug */ = { 535 | isa = XCBuildConfiguration; 536 | buildSettings = { 537 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 538 | BUNDLE_LOADER = "$(TEST_HOST)"; 539 | CODE_SIGN_STYLE = Automatic; 540 | DEVELOPMENT_TEAM = JTEXWEH4CZ; 541 | INFOPLIST_FILE = "RxMVVM+TextureTests/Info.plist"; 542 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 543 | PRODUCT_BUNDLE_IDENTIFIER = "Geektree0101.RxMVVM-TextureTests"; 544 | PRODUCT_NAME = "$(TARGET_NAME)"; 545 | SWIFT_VERSION = 4.0; 546 | TARGETED_DEVICE_FAMILY = "1,2"; 547 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RxMVVM+Texture.app/RxMVVM+Texture"; 548 | }; 549 | name = Debug; 550 | }; 551 | 9B38960B201C252A007DB34C /* Release */ = { 552 | isa = XCBuildConfiguration; 553 | buildSettings = { 554 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 555 | BUNDLE_LOADER = "$(TEST_HOST)"; 556 | CODE_SIGN_STYLE = Automatic; 557 | DEVELOPMENT_TEAM = JTEXWEH4CZ; 558 | INFOPLIST_FILE = "RxMVVM+TextureTests/Info.plist"; 559 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 560 | PRODUCT_BUNDLE_IDENTIFIER = "Geektree0101.RxMVVM-TextureTests"; 561 | PRODUCT_NAME = "$(TARGET_NAME)"; 562 | SWIFT_VERSION = 4.0; 563 | TARGETED_DEVICE_FAMILY = "1,2"; 564 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RxMVVM+Texture.app/RxMVVM+Texture"; 565 | }; 566 | name = Release; 567 | }; 568 | /* End XCBuildConfiguration section */ 569 | 570 | /* Begin XCConfigurationList section */ 571 | 9B3895E4201C252A007DB34C /* Build configuration list for PBXProject "RxMVVM+Texture" */ = { 572 | isa = XCConfigurationList; 573 | buildConfigurations = ( 574 | 9B389604201C252A007DB34C /* Debug */, 575 | 9B389605201C252A007DB34C /* Release */, 576 | ); 577 | defaultConfigurationIsVisible = 0; 578 | defaultConfigurationName = Release; 579 | }; 580 | 9B389606201C252A007DB34C /* Build configuration list for PBXNativeTarget "RxMVVM+Texture" */ = { 581 | isa = XCConfigurationList; 582 | buildConfigurations = ( 583 | 9B389607201C252A007DB34C /* Debug */, 584 | 9B389608201C252A007DB34C /* Release */, 585 | ); 586 | defaultConfigurationIsVisible = 0; 587 | defaultConfigurationName = Release; 588 | }; 589 | 9B389609201C252A007DB34C /* Build configuration list for PBXNativeTarget "RxMVVM+TextureTests" */ = { 590 | isa = XCConfigurationList; 591 | buildConfigurations = ( 592 | 9B38960A201C252A007DB34C /* Debug */, 593 | 9B38960B201C252A007DB34C /* Release */, 594 | ); 595 | defaultConfigurationIsVisible = 0; 596 | defaultConfigurationName = Release; 597 | }; 598 | /* End XCConfigurationList section */ 599 | }; 600 | rootObject = 9B3895E1201C252A007DB34C /* Project object */; 601 | } 602 | -------------------------------------------------------------------------------- /RxMVVM+Texture.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /RxMVVM+Texture.xcodeproj/project.xcworkspace/xcuserdata/vingle.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTree0101/RxMVVM-Texture/92269b340b6dda865167a0e862c2aeeedc4733fa/RxMVVM+Texture.xcodeproj/project.xcworkspace/xcuserdata/vingle.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /RxMVVM+Texture.xcodeproj/xcuserdata/vingle.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | RxMVVM+Texture.xcscheme 8 | 9 | orderHint 10 | 12 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /RxMVVM+Texture.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /RxMVVM+Texture.xcworkspace/xcuserdata/vingle.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTree0101/RxMVVM-Texture/92269b340b6dda865167a0e862c2aeeedc4733fa/RxMVVM+Texture.xcworkspace/xcuserdata/vingle.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /RxMVVM+Texture.xcworkspace/xcuserdata/vingle.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 20 | 21 | 35 | 36 | 50 | 51 | 52 | 53 | 54 | 56 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /RxMVVM+Texture/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // RxMVVM+Texture 4 | // 5 | // Created by Vingle on 2018. 1. 27.. 6 | // Copyright © 2018년 Geektree0101. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AsyncDisplayKit 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | 18 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 19 | // Override point for customization after application launch. 20 | window = UIWindow(frame: UIScreen.main.bounds) 21 | let rootViewController = RepositoryViewController() 22 | let navigationController = UINavigationController(rootViewController: rootViewController) 23 | if let window = window { 24 | window.rootViewController = navigationController 25 | window.makeKeyAndVisible() 26 | } 27 | // DEBUG 28 | //ASControlNode.enableHitTestDebug = true 29 | //ASDisplayNode.shouldShowRangeDebugOverlay = true 30 | return true 31 | } 32 | 33 | func applicationWillResignActive(_ application: UIApplication) { 34 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 35 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 36 | } 37 | 38 | func applicationDidEnterBackground(_ application: UIApplication) { 39 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 40 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 41 | } 42 | 43 | func applicationWillEnterForeground(_ application: UIApplication) { 44 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 45 | } 46 | 47 | func applicationDidBecomeActive(_ application: UIApplication) { 48 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 49 | } 50 | 51 | func applicationWillTerminate(_ application: UIApplication) { 52 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 53 | } 54 | 55 | 56 | } 57 | 58 | -------------------------------------------------------------------------------- /RxMVVM+Texture/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /RxMVVM+Texture/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /RxMVVM+Texture/controllers/RepositoryViewController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | import RxSwift 4 | import RxCocoa 5 | 6 | class RepositoryViewController: ASViewController { 7 | 8 | private var items: [RepositoryViewModel] = [] 9 | private var context: ASBatchContext? 10 | 11 | let disposeBag = DisposeBag() 12 | 13 | init() { 14 | let tableNode = ASTableNode(style: .plain) 15 | tableNode.backgroundColor = .white 16 | tableNode.automaticallyManagesSubnodes = true 17 | super.init(node: tableNode) 18 | 19 | self.title = "Reposivarvary" 20 | 21 | // main thread 22 | self.node.onDidLoad({ node in 23 | guard let `node` = node as? ASTableNode else { return } 24 | node.view.separatorStyle = .singleLine 25 | }) 26 | 27 | self.node.leadingScreensForBatching = 2.0 28 | self.node.dataSource = self 29 | self.node.delegate = self 30 | self.node.allowsSelectionDuringEditing = true 31 | } 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | self.loadMoreRepo(since: nil) 36 | } 37 | 38 | required init?(coder aDecoder: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | 42 | func loadMoreRepo(since: Int?) { 43 | _ = RepoService.loadRepository(params: [.since(since)]) 44 | .delay(0.5, scheduler: MainScheduler.asyncInstance) 45 | .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .default)) 46 | .map { $0.map { RepositoryViewModel(repository: $0) } } 47 | .observeOn(MainScheduler.instance) 48 | .retry(3) 49 | .subscribe(onSuccess: { [weak self] items in 50 | guard let `self` = self else { return } 51 | 52 | if since == nil { 53 | self.items = items 54 | self.node.reloadData() 55 | self.context?.completeBatchFetching(true) 56 | self.context = nil 57 | } else { 58 | // appending is good at table performance 59 | let updateIndexPaths = items.enumerated() 60 | .map { offset, _ -> IndexPath in 61 | return IndexPath(row: self.items.count - 1 + offset, section: 0) 62 | } 63 | 64 | self.items.append(contentsOf: items) 65 | self.node.performBatchUpdates({ 66 | self.node.insertRows(at: updateIndexPaths, 67 | with: .fade) 68 | }, completion: { finishied in 69 | self.context?.completeBatchFetching(finishied) 70 | self.context = nil 71 | }) 72 | } 73 | }, onError: { [weak self] error in 74 | guard let `self` = self else { return } 75 | self.context?.completeBatchFetching(true) 76 | self.context = nil 77 | }) 78 | } 79 | } 80 | 81 | extension RepositoryViewController: ASTableDataSource { 82 | func numberOfSections(in tableNode: ASTableNode) -> Int { 83 | return 1 84 | } 85 | 86 | func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { 87 | return self.items.count 88 | } 89 | 90 | /* 91 | Node Block Thread Safety Warning 92 | It is very important that node blocks be thread-safe. 93 | One aspect of that is ensuring that the data model is accessed outside of the node block. 94 | Therefore, it is unlikely that you should need to use the index inside of the block. 95 | */ 96 | func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { 97 | return { 98 | guard self.items.count > indexPath.row else { return ASCellNode() } 99 | let viewModel = self.items[indexPath.row] 100 | let cellNode = RepositoryListCellNode(viewModel: viewModel) 101 | 102 | viewModel.openProfile 103 | .subscribe(onNext: { [weak self] id in 104 | self?.openUserProfile(id: id) 105 | }).disposed(by: cellNode.disposeBag) 106 | 107 | return cellNode 108 | } 109 | } 110 | 111 | // func tableNode(_ tableNode: ASTableNode, nodeForRowAt indexPath: IndexPath) -> ASCellNode { 112 | // guard self.items.count > indexPath.row else { return ASCellNode() } 113 | // return RepositoryListCellNode(viewModel: self.items[indexPath.row]) 114 | // } 115 | 116 | } 117 | 118 | extension RepositoryViewController: ASTableDelegate { 119 | // block ASBatchContext active state 120 | func shouldBatchFetch(for tableNode: ASTableNode) -> Bool { 121 | return self.context == nil 122 | } 123 | 124 | // load more 125 | func tableNode(_ tableNode: ASTableNode, willBeginBatchFetchWith context: ASBatchContext) { 126 | self.context = context 127 | self.loadMoreRepo(since: self.items.last?.id) 128 | } 129 | 130 | // editable cell 131 | func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 132 | return true 133 | } 134 | 135 | func tableView(_ tableView: UITableView, 136 | commit editingStyle: UITableViewCellEditingStyle, 137 | forRowAt indexPath: IndexPath) { 138 | 139 | if editingStyle == .delete { 140 | self.node.performBatchUpdates({ 141 | self.items.remove(at: indexPath.row) 142 | self.node.deleteRows(at: [indexPath], with: .fade) 143 | }, completion: nil) 144 | } 145 | } 146 | } 147 | 148 | extension RepositoryViewController { 149 | func openUserProfile(id: Int) { 150 | guard let index = self.items.index(where: { $0.id == id }) else { return } 151 | let viewModel = self.items[index] 152 | let viewController = UserProfileViewController(viewModel: viewModel) 153 | self.navigationController?.pushViewController(viewController, animated: true) 154 | } 155 | } 156 | 157 | -------------------------------------------------------------------------------- /RxMVVM+Texture/controllers/UserProfileViewController.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | import RxSwift 4 | import RxCocoa 5 | import RxCocoa_Texture 6 | 7 | class UserProfileViewController: ASViewController { 8 | typealias Node = UserProfileViewController 9 | 10 | struct Attribute { 11 | static let placeHolderColor: UIColor = UIColor.gray.withAlphaComponent(0.2) 12 | } 13 | 14 | lazy var userProfileNode = { () -> ASNetworkImageNode in 15 | let node = ASNetworkImageNode() 16 | node.style.preferredSize = CGSize(width: 100.0, height: 100.0) 17 | node.cornerRadius = 50.0 18 | node.clipsToBounds = true 19 | node.placeholderColor = Attribute.placeHolderColor 20 | node.borderColor = UIColor.gray.withAlphaComponent(0.5).cgColor 21 | node.borderWidth = 0.5 22 | return node 23 | }() 24 | 25 | lazy var usernameNode = { () -> ASEditableTextNode in 26 | let node = ASEditableTextNode() 27 | node.style.flexGrow = 1.0 28 | node.attributedPlaceholderText = 29 | NSAttributedString(string: "Insert description", 30 | attributes: Node.usernamePlaceholderAttributes) 31 | node.typingAttributes = 32 | Node.convertTypingAttribute(Node.usernameAttributes) 33 | return node 34 | }() 35 | 36 | lazy var descriptionNode = { () -> ASEditableTextNode in 37 | let node = ASEditableTextNode() 38 | node.style.flexGrow = 1.0 39 | node.attributedPlaceholderText = 40 | NSAttributedString(string: "Insert description", 41 | attributes: Node.descPlaceholderAttributes) 42 | node.typingAttributes = 43 | Node.convertTypingAttribute(Node.descAttributes) 44 | return node 45 | }() 46 | 47 | lazy var statusNode = { () -> ASTextNode in 48 | let node = ASTextNode() 49 | node.placeholderColor = Attribute.placeHolderColor 50 | return node 51 | }() 52 | 53 | let viewModel: RepositoryViewModel 54 | private let disposeBag = DisposeBag() 55 | 56 | init(viewModel: RepositoryViewModel) { 57 | self.viewModel = viewModel 58 | super.init(node: ASDisplayNode()) 59 | 60 | node.backgroundColor = .white 61 | node.automaticallyManagesSubnodes = true 62 | node.layoutSpecBlock = { [weak self] (_, _) -> ASLayoutSpec in 63 | guard let `self` = self else { return ASLayoutSpec() } 64 | self.userProfileNode.style.spacingAfter = 10.0 65 | self.usernameNode.style.spacingAfter = 30.0 66 | self.descriptionNode.style.spacingAfter = 10.0 67 | 68 | let profileStackLayout = ASStackLayoutSpec(direction: .vertical, 69 | spacing: 0.0, 70 | justifyContent: .center, 71 | alignItems: .center, 72 | children: [ 73 | self.userProfileNode, 74 | self.usernameNode, 75 | self.descriptionNode, 76 | self.statusNode]) 77 | 78 | return ASInsetLayoutSpec(insets: .init(top: 100.0, 79 | left: 15.0, 80 | bottom: .infinity, 81 | right: 15.0), 82 | child: profileStackLayout) 83 | } 84 | 85 | // bind viewmodel 86 | viewModel.profileURL.asObservable() 87 | .bind(to: userProfileNode.rx.url) 88 | .disposed(by: disposeBag) 89 | 90 | viewModel.username.asObservable() 91 | .bind(to: usernameNode.rx.text(Node.usernameAttributes), 92 | setNeedsLayout: node) 93 | .disposed(by: disposeBag) 94 | 95 | viewModel.desc.asObservable() 96 | .bind(to: descriptionNode.rx.text(Node.descAttributes), 97 | setNeedsLayout: node) 98 | .disposed(by: disposeBag) 99 | 100 | viewModel.status.asObservable() 101 | .bind(to: statusNode.rx.text(Node.statusAttributes), 102 | setNeedsLayout: node) 103 | .disposed(by: disposeBag) 104 | 105 | node.onDidLoad({ [weak self] _ in 106 | guard let `self` = self else { return } 107 | 108 | self.descriptionNode.textView.rx.text 109 | .throttle(0.5, scheduler: MainScheduler.asyncInstance) 110 | .bind(to: self.viewModel.updateDescription, 111 | setNeedsLayout: self.node) 112 | .disposed(by: self.disposeBag) 113 | 114 | self.usernameNode.textView.rx.text 115 | .throttle(0.5, scheduler: MainScheduler.asyncInstance) 116 | .bind(to: self.viewModel.updateUsername, 117 | setNeedsLayout: self.node) 118 | .disposed(by: self.disposeBag) 119 | }) 120 | } 121 | 122 | required init?(coder aDecoder: NSCoder) { 123 | fatalError("init(coder:) has not been implemented") 124 | } 125 | } 126 | 127 | extension UserProfileViewController { 128 | static var usernameAttributes: [NSAttributedStringKey: Any] { 129 | return [NSAttributedStringKey.foregroundColor: UIColor.black, 130 | NSAttributedStringKey.font: UIFont.systemFont(ofSize: 20.0)] 131 | } 132 | 133 | static var descAttributes: [NSAttributedStringKey: Any] { 134 | let paragraphStyle = NSMutableParagraphStyle() 135 | paragraphStyle.alignment = .center 136 | return [NSAttributedStringKey.foregroundColor: UIColor.darkGray, 137 | NSAttributedStringKey.font: UIFont.systemFont(ofSize: 15.0), 138 | NSAttributedStringKey.paragraphStyle: paragraphStyle] 139 | } 140 | 141 | static var usernamePlaceholderAttributes: [NSAttributedStringKey: Any] { 142 | return [NSAttributedStringKey.foregroundColor: UIColor.black.withAlphaComponent(0.5), 143 | NSAttributedStringKey.font: UIFont.systemFont(ofSize: 20.0)] 144 | } 145 | 146 | static var descPlaceholderAttributes: [NSAttributedStringKey: Any] { 147 | let paragraphStyle = NSMutableParagraphStyle() 148 | paragraphStyle.alignment = .center 149 | return [NSAttributedStringKey.foregroundColor: UIColor.darkGray.withAlphaComponent(0.5), 150 | NSAttributedStringKey.font: UIFont.systemFont(ofSize: 15.0), 151 | NSAttributedStringKey.paragraphStyle: paragraphStyle] 152 | } 153 | 154 | static var statusAttributes: [NSAttributedStringKey: Any] { 155 | return [NSAttributedStringKey.foregroundColor: UIColor.gray, 156 | NSAttributedStringKey.font: UIFont.systemFont(ofSize: 12.0)] 157 | } 158 | 159 | static func convertTypingAttribute(_ attributes: [NSAttributedStringKey: Any]) -> [String: Any] { 160 | var typingAttribute: [String: Any] = [:] 161 | 162 | for key in attributes.keys { 163 | guard let attr = attributes[key] else { continue } 164 | typingAttribute[key.rawValue] = attr 165 | } 166 | 167 | return typingAttribute 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /RxMVVM+Texture/dataProvider/RepoProvider.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RxSwift 3 | import RxCocoa 4 | 5 | struct RepoProvider { 6 | private static let repoRelay = BehaviorRelay<[Int: (repo: Repository, count: Int, updatedAt: Date)]>(value: [:]) 7 | private static let repoObservable = repoRelay 8 | .asObservable() 9 | .subscribeOn(SerialDispatchQueueScheduler(queue: queue, internalSerialQueueName: UUID().uuidString)) 10 | .share(replay: 1, scope: .whileConnected) 11 | private static let queue = DispatchQueue(label: "RepoProvider.RxMVVMTexture.com", qos: .utility) 12 | 13 | static func addAndUpdate(_ repo: Repository) { 14 | queue.async { 15 | var repoValue = self.repoRelay.value 16 | if let record = repoValue[repo.id] { 17 | record.repo.merge(repo) 18 | repoValue[repo.id] = (repo: record.repo, count: record.count + 1, updatedAt: Date()) 19 | } else { 20 | repoValue[repo.id] = (repo: repo, count: 1, updatedAt: Date()) 21 | } 22 | self.repoRelay.accept(repoValue) 23 | } 24 | } 25 | 26 | 27 | static func update(_ repo: Repository) { 28 | queue.async { 29 | var repoValue = self.repoRelay.value 30 | if let record = repoValue[repo.id] { 31 | record.repo.merge(repo) 32 | repoValue[repo.id] = (repo: record.repo, count: record.count, updatedAt: Date()) 33 | } 34 | self.repoRelay.accept(repoValue) 35 | } 36 | } 37 | 38 | static func retain(id: Int) { 39 | queue.async { 40 | var repoValue = self.repoRelay.value 41 | var record = repoValue[id] 42 | guard record != nil else { return } 43 | 44 | record?.count += 1 45 | repoValue[id] = record 46 | self.repoRelay.accept(repoValue) 47 | } 48 | } 49 | 50 | static func release(id: Int) { 51 | queue.async { 52 | var repoValue = self.repoRelay.value 53 | var record = repoValue[id] 54 | guard record != nil else { return } 55 | 56 | record?.count -= 1 57 | if record?.count ?? 0 < 1 { 58 | record = nil 59 | } 60 | repoValue[id] = record 61 | self.repoRelay.accept(repoValue) 62 | } 63 | } 64 | 65 | static func repo(id: Int) -> Repository? { 66 | var repo: Repository? 67 | queue.sync { 68 | repo = self.repoRelay.value[id]?.repo 69 | } 70 | return repo 71 | } 72 | 73 | static func observable(id: Int) -> Observable { 74 | return repoObservable 75 | .map { $0[id] } 76 | .distinctUntilChanged { $0?.updatedAt == $1?.updatedAt } 77 | .map { $0?.repo } 78 | .share(replay: 1, scope: .whileConnected) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /RxMVVM+Texture/extensions/Decoder+extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RxSwift 3 | 4 | extension PrimitiveSequence where Element == Data { 5 | // .generateArrayModel(type: MODEL_CLASS_NAME.self).subscribe ... TODO 6 | func generateArrayModel() -> Single<[T]> { 7 | return self.asObservable() 8 | .flatMap({ data -> Observable<[T]> in 9 | let array = try? JSONDecoder().decode([T].self, from: data) 10 | return Observable.just(array ?? []) 11 | }) 12 | .asSingle() 13 | } 14 | 15 | func generateObjectModel() -> Single { 16 | return self.asObservable() 17 | .flatMap({ data -> Observable in 18 | let object = try? JSONDecoder().decode(T.self, from: data) 19 | return Observable.just(object ?? nil) 20 | }) 21 | .asSingle() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /RxMVVM+Texture/models/Repository.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class Repository: Decodable { 4 | var id: Int = -1 5 | var user: User? 6 | var repositoryName: String? 7 | var desc: String? 8 | var isPrivate: Bool = false 9 | var isForked: Bool = false 10 | 11 | enum CodingKeys: String, CodingKey { 12 | case id = "id" 13 | case user = "owner" 14 | case repositoryName = "full_name" 15 | case desc = "description" 16 | case isPrivate = "private" 17 | case isForked = "fork" 18 | } 19 | 20 | func merge(_ repo: Repository?) { 21 | guard let repo = repo else { return } 22 | user?.merge(repo.user) 23 | repositoryName = repo.repositoryName 24 | desc = repo.desc 25 | isPrivate = repo.isPrivate 26 | isForked = repo.isForked 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /RxMVVM+Texture/models/User.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class User: Decodable { 4 | var username: String = "" 5 | var profileURL: URL? 6 | 7 | enum CodingKeys: String, CodingKey { 8 | case username = "login" 9 | case profileURL = "avatar_url" 10 | } 11 | 12 | func merge(_ user: User?) { 13 | guard let user = user else { return } 14 | self.username = user.username 15 | self.profileURL = user.profileURL 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /RxMVVM+Texture/networks/Network.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RxAlamofire 3 | import Alamofire 4 | import RxSwift 5 | import RxCocoa 6 | 7 | class Network { 8 | static let shared = Network() 9 | 10 | class Network { 11 | static let shared = Network() 12 | 13 | func get(url: String, params: [String: Any]?) -> Single { 14 | return Observable.create({ operation in 15 | do { 16 | let convertedURL = try url.asURL() 17 | 18 | let nextHandler: (HTTPURLResponse, Any) -> Void = { res, data in 19 | do { 20 | let rawData = try JSONSerialization.data(withJSONObject: data, options: []) 21 | operation.onNext(rawData) 22 | operation.onCompleted() 23 | } catch { 24 | let error = NSError(domain: "failed JSONSerialization", 25 | code: 0, 26 | userInfo: nil) 27 | operation.onError(error) 28 | } 29 | } 30 | 31 | let errorHandler: (Error) -> Void = { error in 32 | operation.onError(error) 33 | } 34 | 35 | _ = RxAlamofire.requestJSON(.get, 36 | convertedURL, 37 | parameters: params, 38 | encoding: URLEncoding.default, 39 | headers: nil) 40 | .subscribe(onNext: nextHandler, 41 | onError: errorHandler) 42 | 43 | } catch { 44 | let error = NSError(domain: "failed convert url", 45 | code: 0, 46 | userInfo: nil) 47 | operation.onError(error) 48 | } 49 | 50 | return Disposables.create() 51 | }).asSingle() 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /RxMVVM+Texture/networks/RepoService.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RxSwift 3 | import RxCocoa 4 | 5 | class RepoService { 6 | enum Route { 7 | case basePath 8 | 9 | var path: String { 10 | let base = "https://api.github.com/repositories" 11 | 12 | switch self { 13 | case .basePath: return base 14 | } 15 | } 16 | 17 | enum Params { 18 | case since(Int?) 19 | 20 | var key: String { 21 | switch self { 22 | case .since: return "since" 23 | } 24 | } 25 | 26 | var value: Any? { 27 | switch self { 28 | case .since(let value): return value 29 | } 30 | } 31 | } 32 | 33 | static func parameters(_ params: [Params]?) -> [String: Any]? { 34 | guard let `params` = params else { return nil } 35 | var result: [String: Any] = [:] 36 | 37 | for param in params { 38 | result[param.key] = param.value 39 | } 40 | 41 | return result.isEmpty ? nil: result 42 | } 43 | } 44 | } 45 | 46 | extension RepoService { 47 | static func loadRepository(params: [RepoService.Route.Params]?) -> Single<[Repository]> { 48 | return Network.shared.get(url: Route.basePath.path, 49 | params: Route.parameters(params)) 50 | .generateArrayModel() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /RxMVVM+Texture/nodes/RepositoryListCellNode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncDisplayKit 3 | import RxSwift 4 | import RxCocoa 5 | import RxCocoa_Texture 6 | 7 | class RepositoryListCellNode: ASCellNode { 8 | typealias Node = RepositoryListCellNode 9 | 10 | struct Attribute { 11 | static let placeHolderColor: UIColor = UIColor.gray.withAlphaComponent(0.2) 12 | } 13 | 14 | // nodes 15 | lazy var userProfileNode = { () -> ASNetworkImageNode in 16 | let node = ASNetworkImageNode() 17 | node.style.preferredSize = CGSize(width: 50.0, height: 50.0) 18 | node.cornerRadius = 25.0 19 | node.clipsToBounds = true 20 | node.placeholderColor = Attribute.placeHolderColor 21 | node.borderColor = UIColor.gray.withAlphaComponent(0.5).cgColor 22 | node.borderWidth = 0.5 23 | return node 24 | }() 25 | 26 | lazy var usernameNode = { () -> ASTextNode in 27 | let node = ASTextNode() 28 | node.maximumNumberOfLines = 1 29 | node.placeholderColor = Attribute.placeHolderColor 30 | return node 31 | }() 32 | 33 | lazy var descriptionNode = { () -> ASTextNode in 34 | let node = ASTextNode() 35 | node.placeholderColor = Attribute.placeHolderColor 36 | node.maximumNumberOfLines = 2 37 | node.truncationAttributedText = NSAttributedString(string: " ...More", 38 | attributes: Node.moreSeeAttributes) 39 | node.delegate = self 40 | node.isUserInteractionEnabled = true 41 | return node 42 | }() 43 | 44 | lazy var statusNode = { () -> ASTextNode in 45 | let node = ASTextNode() 46 | node.placeholderColor = Attribute.placeHolderColor 47 | return node 48 | }() 49 | 50 | let disposeBag = DisposeBag() 51 | 52 | let id: Int 53 | 54 | init(viewModel: RepositoryViewModel) { 55 | self.id = viewModel.id 56 | super.init() 57 | self.selectionStyle = .none 58 | self.backgroundColor = .white 59 | self.automaticallyManagesSubnodes = true 60 | 61 | viewModel.profileURL 62 | .bind(to: userProfileNode.rx.url, 63 | setNeedsLayout: self) 64 | .disposed(by: disposeBag) 65 | 66 | viewModel.username 67 | .bind(to: usernameNode.rx.text(Node.usernameAttributes), 68 | setNeedsLayout: self) 69 | .disposed(by: disposeBag) 70 | 71 | viewModel.desc 72 | .bind(to: descriptionNode.rx.text(Node.descAttributes), 73 | setNeedsLayout: self) 74 | .disposed(by: disposeBag) 75 | 76 | viewModel.status 77 | .bind(to: statusNode.rx.text(Node.statusAttributes), 78 | setNeedsLayout: self) 79 | .disposed(by: disposeBag) 80 | 81 | userProfileNode.rx 82 | .tap(to: viewModel.openProfileRelay) 83 | .disposed(by: disposeBag) 84 | } 85 | } 86 | 87 | extension RepositoryListCellNode: ASTextNodeDelegate { 88 | func textNodeTappedTruncationToken(_ textNode: ASTextNode) { 89 | textNode.maximumNumberOfLines = 0 90 | self.setNeedsLayout() 91 | } 92 | } 93 | 94 | extension RepositoryListCellNode { 95 | // layout spec 96 | override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { 97 | let contentLayout = contentLayoutSpec() 98 | contentLayout.style.flexShrink = 1.0 99 | contentLayout.style.flexGrow = 1.0 100 | 101 | userProfileNode.style.flexShrink = 1.0 102 | userProfileNode.style.flexGrow = 0.0 103 | 104 | let stackLayout = ASStackLayoutSpec(direction: .horizontal, 105 | spacing: 10.0, 106 | justifyContent: .start, 107 | alignItems: .center, 108 | children: [userProfileNode, 109 | contentLayout]) 110 | return ASInsetLayoutSpec(insets: UIEdgeInsets(top: 10.0, 111 | left: 10.0, 112 | bottom: 10.0, 113 | right: 10.0), 114 | child: stackLayout) 115 | } 116 | 117 | private func contentLayoutSpec() -> ASLayoutSpec { 118 | let elements = [self.usernameNode, 119 | self.descriptionNode, 120 | self.statusNode].filter { $0.attributedText?.length ?? 0 > 0 } 121 | return ASStackLayoutSpec(direction: .vertical, 122 | spacing: 5.0, 123 | justifyContent: .start, 124 | alignItems: .stretch, 125 | children: elements) 126 | } 127 | } 128 | 129 | extension RepositoryListCellNode { 130 | static var usernameAttributes: [NSAttributedStringKey: Any] { 131 | return [NSAttributedStringKey.foregroundColor: UIColor.black, 132 | NSAttributedStringKey.font: UIFont.systemFont(ofSize: 20.0)] 133 | } 134 | 135 | static var descAttributes: [NSAttributedStringKey: Any] { 136 | return [NSAttributedStringKey.foregroundColor: UIColor.darkGray, 137 | NSAttributedStringKey.font: UIFont.systemFont(ofSize: 15.0)] 138 | } 139 | 140 | static var statusAttributes: [NSAttributedStringKey: Any] { 141 | return [NSAttributedStringKey.foregroundColor: UIColor.gray, 142 | NSAttributedStringKey.font: UIFont.systemFont(ofSize: 12.0)] 143 | } 144 | 145 | static var moreSeeAttributes: [NSAttributedStringKey: Any] { 146 | return [NSAttributedStringKey.foregroundColor: UIColor.darkGray, 147 | NSAttributedStringKey.font: UIFont.systemFont(ofSize: 15.0, weight: .medium)] 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /RxMVVM+Texture/viewmodels/RepositoryViewModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RxSwift 3 | import RxCocoa 4 | 5 | class RepositoryViewModel { 6 | 7 | // @INPUT 8 | let updateRepository = PublishRelay() 9 | let updateUsername = PublishRelay() 10 | let updateDescription = PublishRelay() 11 | let openProfileRelay = PublishRelay() 12 | 13 | // @OUTPUT 14 | var username: Observable 15 | var profileURL: Observable 16 | var desc: Observable 17 | var status: Observable 18 | var openProfile: Observable 19 | 20 | let id: Int 21 | let disposeBag = DisposeBag() 22 | 23 | deinit { 24 | RepoProvider.release(id: id) 25 | } 26 | 27 | init(repository: Repository) { 28 | self.id = repository.id 29 | 30 | RepoProvider.addAndUpdate(repository) 31 | 32 | let repoObserver = RepoProvider.observable(id: id) 33 | .asObservable() 34 | .share(replay: 1, scope: .whileConnected) 35 | 36 | openProfile = openProfileRelay 37 | .subscribeOn(MainScheduler.asyncInstance) 38 | .withLatestFrom(repoObserver) 39 | .map { $0?.id ?? -1 } 40 | 41 | self.username = repoObserver 42 | .map { $0?.user?.username } 43 | 44 | self.profileURL = repoObserver 45 | .map { $0?.user?.profileURL } 46 | 47 | self.desc = repoObserver 48 | .map { $0?.desc } 49 | 50 | self.status = repoObserver 51 | .map { item -> String? in 52 | var statusArray: [String] = [] 53 | if let isForked = item?.isForked, isForked { 54 | statusArray.append("Forked") 55 | } 56 | 57 | if let isPrivate = item?.isPrivate, isPrivate { 58 | statusArray.append("Private") 59 | } 60 | 61 | return statusArray.isEmpty ? nil: statusArray.joined(separator: " · ") 62 | } 63 | 64 | self.updateRepository.subscribe(onNext: { newRepo in 65 | RepoProvider.update(newRepo) 66 | }).disposed(by: disposeBag) 67 | 68 | updateUsername.withLatestFrom(repoObserver) { ($0, $1) } 69 | .subscribe(onNext: { text, repo in 70 | guard let repo = repo else { return } 71 | repo.user?.username = text ?? "" 72 | RepoProvider.update(repo) 73 | }).disposed(by: disposeBag) 74 | 75 | updateDescription.withLatestFrom(repoObserver) { ($0, $1) } 76 | .subscribe(onNext: { text, repo in 77 | guard let repo = repo else { return } 78 | repo.desc = text 79 | RepoProvider.update(repo) 80 | }).disposed(by: disposeBag) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /RxMVVM+TextureTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /RxMVVM+TextureTests/RxMVVM_TextureTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxMVVM_TextureTests.swift 3 | // RxMVVM+TextureTests 4 | // 5 | // Created by Vingle on 2018. 1. 27.. 6 | // Copyright © 2018년 Geektree0101. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import RxMVVM_Texture 11 | 12 | class RxMVVM_TextureTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /resource/resource1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTree0101/RxMVVM-Texture/92269b340b6dda865167a0e862c2aeeedc4733fa/resource/resource1.png -------------------------------------------------------------------------------- /resource/resource2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTree0101/RxMVVM-Texture/92269b340b6dda865167a0e862c2aeeedc4733fa/resource/resource2.png -------------------------------------------------------------------------------- /resource/resource3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTree0101/RxMVVM-Texture/92269b340b6dda865167a0e862c2aeeedc4733fa/resource/resource3.png -------------------------------------------------------------------------------- /resource/resource4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeekTree0101/RxMVVM-Texture/92269b340b6dda865167a0e862c2aeeedc4733fa/resource/resource4.png --------------------------------------------------------------------------------