├── Resource └── logo.png ├── Tests ├── RxDataSources-TextureTests │ └── DummyTests.swift └── LinuxMain.swift ├── Example ├── GithubRepos │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Repo.swift │ ├── AppDelegate.swift │ ├── GithubService.swift │ ├── MainSection.swift │ ├── Info.plist │ ├── RepoViewModel.swift │ ├── RepoCellNode.swift │ ├── CategoryViewController.swift │ ├── RepoTableViewController.swift │ ├── RepoCollectionViewController.swift │ └── Base.lproj │ │ └── LaunchScreen.storyboard ├── Podfile ├── GithubRepos.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── project.pbxproj └── GithubRepos.xcworkspace │ └── contents.xcworkspacedata ├── .gitignore ├── Podfile ├── Makefile ├── Sources └── RxDataSources-Texture │ ├── NodeTransition.swift │ ├── ASTableNode │ ├── RxASTableDataSourceType.swift │ ├── RxASTableSectionedReloadDataSource.swift │ ├── RxASTableDataSourceProxy.swift │ ├── RxASTableDelegateProxy.swift │ ├── RxASTableSectionedAnimatedDataSource.swift │ ├── ASTableSectionedDataSource.swift │ └── ASTableNode+Rx.swift │ ├── ASCollectionNode │ ├── RxASCollectionDataSourceType.swift │ ├── RxASCollectionSectionedReloadDataSource.swift │ ├── RxASCollectionDataSourceProxy.swift │ ├── RxASCollectionDelegateProxy.swift │ ├── RxASCollectionSectionedAnimatedDataSource.swift │ ├── ASCollectionSectionedDataSource.swift │ └── ASCollectionNode+Rx.swift │ ├── AnimationConfiguration.swift │ ├── Array+Extensions.swift │ ├── ASDelegateProxyType.swift │ ├── RxASErrorHandler.swift │ └── UI+SectionNodeType.swift ├── Package.resolved ├── Package.swift ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── RxDataSources-Texture.podspec ├── Podfile.lock └── README.md /Resource/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OhKanghoon/RxDataSources-Texture/HEAD/Resource/logo.png -------------------------------------------------------------------------------- /Tests/RxDataSources-TextureTests/DummyTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | final class DummyTests: XCTestCase { 4 | } 5 | -------------------------------------------------------------------------------- /Example/GithubRepos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import RxTextureTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += RxTextureTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.swiftpm 4 | /Packages 5 | /*.xcodeproj 6 | /*.xcworkspace 7 | **/xcuserdata 8 | **/xcshareddata 9 | Pods/ 10 | Carthage/ 11 | 12 | Example/**/Podfile.lock 13 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '9.0' 2 | inhibit_all_warnings! 3 | 4 | target 'RxDataSources-Texture' do 5 | use_frameworks! 6 | pod 'Texture', '~> 3.0' 7 | 8 | target 'RxDataSources-TextureTests' do 9 | end 10 | 11 | end -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | project: clean 2 | swift package generate-xcodeproj --enable-code-coverage 3 | ruby -e "require 'xcodeproj'; Xcodeproj::Project.open('RxDataSources-Texture.xcodeproj').save" || true 4 | pod install 5 | 6 | clean: 7 | rm -rf Pods -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '9.0' 2 | use_frameworks! 3 | inhibit_all_warnings! 4 | 5 | target 'GithubRepos' do 6 | pod 'RxDataSources-Texture', :path => '../' 7 | pod 'RxOptional' 8 | pod 'Alamofire' 9 | pod 'RxAlamofire' 10 | end -------------------------------------------------------------------------------- /Example/GithubRepos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/GithubRepos.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Sources/RxDataSources-Texture/NodeTransition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NodeTransition.swift 3 | // RxDataSources-Texture 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // 7 | 8 | /// Transition between two view states 9 | public enum NodeTransition { 10 | /// animated transition 11 | case animated 12 | /// refresh view without animations 13 | case reload 14 | } 15 | -------------------------------------------------------------------------------- /Example/GithubRepos/Repo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Repo.swift 3 | // GithubRepos 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Repo: Codable { 12 | 13 | let id: Int 14 | let name: String 15 | let fullName: String 16 | let owner: User 17 | 18 | enum CodingKeys: String, CodingKey { 19 | case id = "id" 20 | case name = "name" 21 | case fullName = "full_name" 22 | case owner = "owner" 23 | } 24 | } 25 | 26 | struct User: Codable { 27 | let avatarURL: String 28 | 29 | enum CodingKeys: String, CodingKey { 30 | case avatarURL = "avatar_url" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "RxDataSources", 6 | "repositoryURL": "https://github.com/RxSwiftCommunity/RxDataSources.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "241c62e7b578b2346c8b60efdf31b4eb2eab2966", 10 | "version": "5.0.0" 11 | } 12 | }, 13 | { 14 | "package": "RxSwift", 15 | "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "c8742ed97fc2f0c015a5ea5eddefb064cd7532d2", 19 | "version": "6.0.0" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "RxDataSources-Texture", 7 | platforms: [ 8 | .iOS(.v9) 9 | ], 10 | products: [ 11 | .library(name: "RxDataSources-Texture", targets: ["RxDataSources-Texture"]), 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/ReactiveX/RxSwift.git", .upToNextMajor(from: "6.0.0")), 15 | .package(url: "https://github.com/RxSwiftCommunity/RxDataSources.git", .upToNextMajor(from: "5.0.0")), 16 | ], 17 | targets: [ 18 | .target(name: "RxDataSources-Texture", dependencies: ["Differentiator", "RxSwift", "RxCocoa"]), 19 | .testTarget(name: "RxDataSources-TextureTests", dependencies: ["RxDataSources-Texture"]), 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /Sources/RxDataSources-Texture/ASTableNode/RxASTableDataSourceType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxASTableDataSourceType.swift 3 | // RxDataSources-Texture 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) 9 | 10 | import Foundation 11 | import AsyncDisplayKit 12 | import RxSwift 13 | 14 | /// Marks data source as `ASTableNode` reactive data source enabling it to be used with one of the `bindTo` methods. 15 | public protocol RxASTableDataSourceType /*: ASTableDataSource*/ { 16 | 17 | /// Type of elements that can be bound to table node. 18 | associatedtype Element 19 | 20 | /// New observable sequence event observed. 21 | /// 22 | /// - parameter tableView: Bound table node. 23 | /// - parameter observedEvent: Event 24 | func tableNode(_ tableNode: ASTableNode, observedEvent: Event) -> Void 25 | } 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /Example/GithubRepos/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // GithubRepos 4 | // 5 | // Created by ohkanghoon on 2020/03/31. 6 | // Copyright © 2020 kanghoon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | var window: UIWindow? 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 16 | window = UIWindow(frame: UIScreen.main.bounds) 17 | window?.backgroundColor = .white 18 | window?.makeKeyAndVisible() 19 | 20 | let viewController = CategoryViewController() 21 | 22 | let navigationController = UINavigationController(rootViewController: viewController) 23 | window?.rootViewController = navigationController 24 | return true 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /Sources/RxDataSources-Texture/ASCollectionNode/RxASCollectionDataSourceType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxASCollectionDataSourceType.swift 3 | // RxDataSources-Texture 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) 9 | 10 | import Foundation 11 | import AsyncDisplayKit 12 | import RxSwift 13 | 14 | /// Marks data source as `ASCollectionNode` reactive data source enabling it to be used with one of the `bindTo` methods. 15 | public protocol RxASCollectionDataSourceType /*: ASCollectionDataSource*/ { 16 | 17 | /// Type of elements that can be bound to table node. 18 | associatedtype Element 19 | 20 | /// New observable sequence event observed. 21 | /// 22 | /// - parameter collectionNode: Bound collection node. 23 | /// - parameter observedEvent: Event 24 | func collectionNode(_ collectionNode: ASCollectionNode, observedEvent: Event) -> Void 25 | } 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /Sources/RxDataSources-Texture/ASTableNode/RxASTableSectionedReloadDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxASTableSectionedReloadDataSource.swift 3 | // RxDataSources-Texture 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) 9 | import Foundation 10 | import AsyncDisplayKit 11 | #if !RX_NO_MODULE 12 | import RxSwift 13 | import RxCocoa 14 | #endif 15 | import Differentiator 16 | 17 | open class RxASTableSectionedReloadDataSource 18 | : ASTableSectionedDataSource 19 | , RxASTableDataSourceType { 20 | public typealias Element = [S] 21 | 22 | open func tableNode(_ tableNode: ASTableNode, observedEvent: Event) { 23 | Binder(self) { dataSource, element in 24 | #if DEBUG 25 | dataSource._dataSourceBound = true 26 | #endif 27 | dataSource.setSections(element) 28 | tableNode.reloadData() 29 | }.on(observedEvent) 30 | } 31 | } 32 | #endif 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: macOS-latest 12 | env: 13 | WORKSPACE: RxDataSources-Texture.xcworkspace 14 | SCHEME: RxDataSources-Texture-Package 15 | SDK: iphonesimulator 16 | DESTINATION: platform=iOS Simulator,name=iPhone 11 Pro,OS=latest 17 | 18 | steps: 19 | - uses: actions/checkout@v1 20 | 21 | - name: Generate Xcode Project 22 | run: make project 23 | 24 | - name: Build and Test 25 | run: | 26 | set -o pipefail && xcodebuild clean build test \ 27 | -workspace "$WORKSPACE" \ 28 | -scheme "$SCHEME" \ 29 | -sdk "$SDK" \ 30 | -destination "$DESTINATION" \ 31 | -configuration Debug \ 32 | -enableCodeCoverage YES \ 33 | CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO | xcpretty -c 34 | -------------------------------------------------------------------------------- /Example/GithubRepos/GithubService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubService.swift 3 | // GithubRepos 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | import RxAlamofire 12 | 13 | protocol GithubServiceProtocol { 14 | func repositories(since: Int?) -> Single<[Repo]> 15 | } 16 | 17 | final class GithubService: GithubServiceProtocol { 18 | 19 | init() {} 20 | 21 | func repositories(since: Int?) -> Single<[Repo]> { 22 | return RxAlamofire.request( 23 | .get, 24 | "https://api.github.com/repositories", 25 | parameters: since.map { ["since": $0] } 26 | ) 27 | .validate(statusCode: 200..<300) 28 | .responseData() 29 | .debug() 30 | .map { (_, data) -> [Repo] in 31 | let decoder = JSONDecoder() 32 | return try decoder.decode([Repo].self, from: data) 33 | } 34 | .asSingle() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/RxDataSources-Texture/ASCollectionNode/RxASCollectionSectionedReloadDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxASCollectionSectionedReloadDataSource.swift 3 | // RxDataSources-Texture 4 | // 5 | // Created by Kanghoon on 21/02/2019. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) 9 | import Foundation 10 | import AsyncDisplayKit 11 | #if !RX_NO_MODULE 12 | import RxSwift 13 | import RxCocoa 14 | #endif 15 | import Differentiator 16 | 17 | open class RxASCollectionSectionedReloadDataSource 18 | : ASCollectionSectionedDataSource 19 | , RxASCollectionDataSourceType { 20 | 21 | public typealias Element = [S] 22 | 23 | open func collectionNode(_ collectionNode: ASCollectionNode, observedEvent: Event<[S]>) { 24 | Binder(self) { dataSource, element in 25 | #if DEBUG 26 | dataSource._dataSourceBound = true 27 | #endif 28 | dataSource.setSections(element) 29 | collectionNode.reloadData() 30 | collectionNode.collectionViewLayout.invalidateLayout() 31 | }.on(observedEvent) 32 | } 33 | } 34 | #endif 35 | -------------------------------------------------------------------------------- /Sources/RxDataSources-Texture/AnimationConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimationConfiguration.swift 3 | // RxDataSources-Texture 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) 9 | import Foundation 10 | import UIKit 11 | 12 | /** 13 | Exposes custom animation styles for insertion, deletion and reloading behavior. 14 | */ 15 | public struct AnimationConfiguration { 16 | public let animated: Bool 17 | public let insertAnimation: UITableViewRowAnimation 18 | public let reloadAnimation: UITableViewRowAnimation 19 | public let deleteAnimation: UITableViewRowAnimation 20 | 21 | public init(animated: Bool = true, 22 | insertAnimation: UITableViewRowAnimation = .automatic, 23 | reloadAnimation: UITableViewRowAnimation = .automatic, 24 | deleteAnimation: UITableViewRowAnimation = .automatic) { 25 | self.animated = animated 26 | self.insertAnimation = insertAnimation 27 | self.reloadAnimation = reloadAnimation 28 | self.deleteAnimation = deleteAnimation 29 | } 30 | } 31 | #endif 32 | -------------------------------------------------------------------------------- /Sources/RxDataSources-Texture/Array+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Extensions.swift 3 | // RxDataSources-Texture 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) 9 | import Foundation 10 | import Differentiator 11 | 12 | extension Array where Element: SectionModelType { 13 | mutating func moveFromSourceIndexPath(_ sourceIndexPath: IndexPath, destinationIndexPath: IndexPath) { 14 | let sourceSection = self[sourceIndexPath.section] 15 | var sourceItems = sourceSection.items 16 | 17 | let sourceItem = sourceItems.remove(at: sourceIndexPath.item) 18 | 19 | let sourceSectionNew = Element(original: sourceSection, items: sourceItems) 20 | self[sourceIndexPath.section] = sourceSectionNew 21 | 22 | let destinationSection = self[destinationIndexPath.section] 23 | var destinationItems = destinationSection.items 24 | destinationItems.insert(sourceItem, at: destinationIndexPath.item) 25 | 26 | self[destinationIndexPath.section] = Element(original: destinationSection, items: destinationItems) 27 | } 28 | } 29 | #endif 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 OhKanghoon 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Example/GithubRepos/MainSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainSection.swift 3 | // GithubRepos 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import RxDataSources_Texture 10 | 11 | enum MainSection { 12 | case repo(repos: [MainSectionItem]) 13 | } 14 | 15 | extension MainSection: AnimatableSectionModelType { 16 | 17 | typealias Identity = String 18 | 19 | var identity: String { 20 | switch self { 21 | case .repo: return "repo" 22 | } 23 | } 24 | 25 | var items: [MainSectionItem] { 26 | switch self { 27 | case .repo(let items): return items 28 | } 29 | } 30 | 31 | init(original: MainSection, items: [MainSectionItem]) { 32 | switch original { 33 | case .repo: self = .repo(repos: items) 34 | } 35 | } 36 | } 37 | 38 | enum MainSectionItem { 39 | case repo(Repo) 40 | } 41 | 42 | extension MainSectionItem: IdentifiableType { 43 | typealias Identity = Int 44 | 45 | var identity: Int { 46 | switch self { 47 | case .repo(let repo): 48 | return repo.id 49 | } 50 | } 51 | } 52 | 53 | extension MainSectionItem: Equatable { 54 | static func == (lhs: MainSectionItem, rhs: MainSectionItem) -> Bool { 55 | return lhs.identity == rhs.identity 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /RxDataSources-Texture.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint RxTextureDataSources.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'RxDataSources-Texture' 11 | s.version = '1.4.0' 12 | s.summary = 'RxDataSources With Texture' 13 | s.description = <<-DESC 14 | This is a collection of reactive data sources for ASTableNode and ASCollectionNode 15 | DESC 16 | s.homepage = 'https://github.com/OhKanghoon/RxDataSources-Texture' 17 | s.license = { :type => 'MIT', :file => 'LICENSE' } 18 | s.author = { 'OhKanghoon' => 'ggaa96@naver.com' } 19 | s.source = { :git => 'https://github.com/OhKanghoon/RxDataSources-Texture.git', :tag => s.version.to_s } 20 | 21 | s.ios.deployment_target = '9.0' 22 | s.requires_arc = true 23 | s.swift_version = '5.0' 24 | 25 | s.source_files = 'Sources/**/*.{swift,h,m}' 26 | 27 | s.dependency 'RxSwift', '~> 6.0' 28 | s.dependency 'RxCocoa', '~> 6.0' 29 | s.dependency 'Differentiator', '~> 5.0' 30 | s.dependency 'Texture', '~> 3.0' 31 | end 32 | -------------------------------------------------------------------------------- /Example/GithubRepos/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 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 | -------------------------------------------------------------------------------- /Example/GithubRepos/RepoViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepoViewModel.swift 3 | // GithubRepos 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AsyncDisplayKit 11 | import RxSwift 12 | import RxRelay 13 | 14 | final class RepoViewModel { 15 | 16 | let refreshRelay = PublishRelay() 17 | let loadMoreRelay = PublishRelay() 18 | 19 | let repos = BehaviorRelay<[Repo]?>(value: nil) 20 | let since = BehaviorRelay(value: nil) 21 | let sections: Observable<[MainSection]> 22 | 23 | let disposeBag = DisposeBag() 24 | 25 | init(githubService: GithubServiceProtocol) { 26 | self.sections = self.repos 27 | .filterNil() 28 | .map { [MainSection.repo(repos: $0.map { MainSectionItem.repo($0) })] } 29 | .asObservable() 30 | 31 | self.repos.map { $0?.last?.id } 32 | .filterNil() 33 | .bind(to: since) 34 | .disposed(by: disposeBag) 35 | 36 | self.refreshRelay 37 | .flatMap { githubService.repositories(since: nil).catchErrorJustReturn([]) } 38 | .bind(to: repos) 39 | .disposed(by: disposeBag) 40 | 41 | self.loadMoreRelay 42 | .withLatestFrom(since) 43 | .filterNil() 44 | .distinctUntilChanged() 45 | .flatMap { githubService.repositories(since: $0).catchErrorJustReturn([]) } 46 | .withLatestFrom(repos) { (new, old) -> [Repo] in 47 | var sequence = old ?? [] 48 | sequence.append(contentsOf: new) 49 | return sequence 50 | } 51 | .bind(to: repos) 52 | .disposed(by: disposeBag) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - PINCache (3.0.3): 3 | - PINCache/Arc-exception-safe (= 3.0.3) 4 | - PINCache/Core (= 3.0.3) 5 | - PINCache/Arc-exception-safe (3.0.3): 6 | - PINCache/Core 7 | - PINCache/Core (3.0.3): 8 | - PINOperation (~> 1.2.1) 9 | - PINOperation (1.2.1) 10 | - PINRemoteImage/Core (3.0.3): 11 | - PINOperation 12 | - PINRemoteImage/iOS (3.0.3): 13 | - PINRemoteImage/Core 14 | - PINRemoteImage/PINCache (3.0.3): 15 | - PINCache (~> 3.0.3) 16 | - PINRemoteImage/Core 17 | - Texture (3.0.0): 18 | - Texture/AssetsLibrary (= 3.0.0) 19 | - Texture/Core (= 3.0.0) 20 | - Texture/MapKit (= 3.0.0) 21 | - Texture/Photos (= 3.0.0) 22 | - Texture/PINRemoteImage (= 3.0.0) 23 | - Texture/Video (= 3.0.0) 24 | - Texture/AssetsLibrary (3.0.0): 25 | - Texture/Core 26 | - Texture/Core (3.0.0) 27 | - Texture/MapKit (3.0.0): 28 | - Texture/Core 29 | - Texture/Photos (3.0.0): 30 | - Texture/Core 31 | - Texture/PINRemoteImage (3.0.0): 32 | - PINRemoteImage/iOS (~> 3.0.0) 33 | - PINRemoteImage/PINCache 34 | - Texture/Core 35 | - Texture/Video (3.0.0): 36 | - Texture/Core 37 | 38 | DEPENDENCIES: 39 | - Texture (~> 3.0) 40 | 41 | SPEC REPOS: 42 | trunk: 43 | - PINCache 44 | - PINOperation 45 | - PINRemoteImage 46 | - Texture 47 | 48 | SPEC CHECKSUMS: 49 | PINCache: 7a8fc1a691173d21dbddbf86cd515de6efa55086 50 | PINOperation: 00c935935f1e8cf0d1e2d6b542e75b88fc3e5e20 51 | PINRemoteImage: f1295b29f8c5e640e25335a1b2bd9d805171bd01 52 | Texture: 2f109e937850d94d1d07232041c9c7313ccddb81 53 | 54 | PODFILE CHECKSUM: 84c07afdd654394e2a5117c18b492819e1cc99d0 55 | 56 | COCOAPODS: 1.10.0 57 | -------------------------------------------------------------------------------- /Example/GithubRepos/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 | } -------------------------------------------------------------------------------- /Example/GithubRepos/RepoCellNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepoCellNode.swift 3 | // GithubRepos 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AsyncDisplayKit 11 | 12 | final class RepoCellNode: ASCellNode { 13 | 14 | enum Const { 15 | static let imageSize: CGSize = .init(width: 60, height: 60) 16 | static let spacing: CGFloat = 8 17 | static let placeholderColor: UIColor = .init( 18 | red: 233.0 / 255.0, 19 | green: 237.0 / 255.0, 20 | blue: 240.0 / 255.0, 21 | alpha: 1.0 22 | ) 23 | } 24 | 25 | enum ViewType { 26 | case table 27 | case collection 28 | } 29 | 30 | let imageNode: ASNetworkImageNode = { 31 | let node = ASNetworkImageNode() 32 | node.style.preferredSize = Const.imageSize 33 | node.placeholderFadeDuration = 0.3 34 | node.placeholderColor = Const.placeholderColor 35 | return node 36 | }() 37 | 38 | let titleNode: ASTextNode = { 39 | let node = ASTextNode() 40 | node.maximumNumberOfLines = 2 41 | node.style.flexShrink = 1.0 42 | return node 43 | }() 44 | 45 | let type: ViewType 46 | 47 | init(_ type: ViewType, repo: Repo) { 48 | self.type = type 49 | super.init() 50 | self.automaticallyManagesSubnodes = true 51 | self.backgroundColor = .white 52 | self.selectionStyle = .none 53 | 54 | imageNode.url = URL(string: repo.owner.avatarURL) 55 | titleNode.attributedText = NSAttributedString( 56 | string: repo.fullName, 57 | attributes: [.font: UIFont.systemFont(ofSize: 13), 58 | .foregroundColor: UIColor.black] 59 | ) 60 | } 61 | 62 | override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { 63 | let contentStackSpec = ASStackLayoutSpec( 64 | direction: .horizontal, 65 | spacing: Const.spacing, 66 | justifyContent: .start, 67 | alignItems: .center, 68 | children: [ 69 | imageNode, 70 | titleNode 71 | ] 72 | ) 73 | switch type { 74 | case .collection: 75 | contentStackSpec.style.preferredLayoutSize.width = ASDimensionMake(constrainedSize.max.width / 2 - 1) 76 | default: break 77 | } 78 | 79 | return ASInsetLayoutSpec(insets: .zero, child: contentStackSpec) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Example/GithubRepos/CategoryViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CategoryViewController.swift 3 | // GithubRepos 4 | // 5 | // Created by Kanghoon on 22/02/2019. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AsyncDisplayKit 11 | import RxSwift 12 | import RxCocoa 13 | import RxOptional 14 | 15 | final class CategoryViewController: ASDKViewController { 16 | 17 | enum Item: Int, CaseIterable { 18 | case table 19 | case collection 20 | 21 | var title: String { 22 | switch self { 23 | case .table: return "ASTableNode Example" 24 | case .collection: return "ASCollectionNode Example" 25 | } 26 | } 27 | } 28 | 29 | // MARK: - Initialization 30 | 31 | override init() { 32 | super.init(node: ASTableNode()) 33 | node.delegate = self 34 | node.dataSource = self 35 | title = "Categories" 36 | } 37 | 38 | required init?(coder aDecoder: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | 42 | 43 | // MARK: - View Life Cycle 44 | 45 | override func viewDidLoad() { 46 | super.viewDidLoad() 47 | self.node.view.alwaysBounceVertical = true 48 | } 49 | } 50 | 51 | extension CategoryViewController: ASTableDataSource { 52 | func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { 53 | return Item.allCases.count 54 | } 55 | 56 | func tableNode(_ tableNode: ASTableNode, nodeForRowAt indexPath: IndexPath) -> ASCellNode { 57 | guard let item = Item(rawValue: indexPath.row) else { return ASCellNode() } 58 | let cell = ASTextCellNode() 59 | cell.selectionStyle = .none 60 | cell.text = item.title 61 | return cell 62 | } 63 | } 64 | 65 | 66 | extension CategoryViewController: ASTableDelegate { 67 | func tableNode(_ tableNode: ASTableNode, didSelectRowAt indexPath: IndexPath) { 68 | guard let item = Item(rawValue: indexPath.row) else { return } 69 | 70 | let vc: UIViewController 71 | let viewModel = RepoViewModel(githubService: GithubService()) 72 | 73 | switch item { 74 | case .table: 75 | vc = RepoTableViewController(viewModel: viewModel) 76 | case .collection: 77 | vc = RepoCollectionViewController(viewModel: viewModel) 78 | } 79 | self.navigationController?.pushViewController(vc, animated: true) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ![Swift](https://img.shields.io/badge/Swift-5.1-orange.svg) 4 | [![Build Status](https://github.com/OhKanghoon/RxDataSources-Texture/workflows/CI/badge.svg)](https://github.com/OhKanghoon/RxDataSources-Texture/actions) 5 | [![Version](https://img.shields.io/cocoapods/v/RxDataSources-Texture.svg?style=flat)](https://cocoapods.org/pods/RxDataSources-Texture) 6 | [![License](https://img.shields.io/cocoapods/l/RxDataSources-Texture.svg?style=flat)](https://cocoapods.org/pods/RxDataSources-Texture) 7 | [![Platform](https://img.shields.io/cocoapods/p/RxDataSources-Texture.svg?style=flat)](https://cocoapods.org/pods/RxDataSources-Texture) 8 | 9 | ## Usage 10 | 11 | 1. Turn your data into an Observable sequence 12 | 2. Bind the data to the tableNode / collectionNode using : 13 | - rx.items(dataSource:protocol) 14 | 15 | ```swift 16 | let dataSource = RxASTableSectionedReloadDataSource>( 17 | configureCellBlock: { (_, _, _, num) in 18 | return { 19 | let cell = ASTextCellNode() 20 | cell.text = "\(num)" 21 | return cell 22 | } 23 | }) 24 | 25 | Observable.just([SectionModel(model: "title", items: [1, 2, 3])]) 26 | .bind(to: tableNode.rx.items(dataSource: dataSource)) 27 | .disposed(by: disposeBag) 28 | ``` 29 | 30 | ## Example 31 | 32 | To run the example project, clone the repo, and run `pod install` from the Example directory first. 33 | - [RxDataSources Example](https://github.com/OhKanghoon/RxDataSources-Texture/tree/master/Example) 34 | 35 | ## Requirements 36 | 37 | - Swift 5.2 38 | - [RxSwift](https://github.com/ReactiveX/RxSwift) (~> 6.0) 39 | - [RxCocoa](https://github.com/ReactiveX/RxSwift/tree/master/RxCocoa) (~> 6.0) 40 | - Differentiator (~> 5.0) 41 | - [Texture](https://github.com/TextureGroup/Texture) (~> 3.0) 42 | 43 | ## Installation 44 | 45 | RxDataSources-Texture is available through [CocoaPods](https://cocoapods.org). To install 46 | it, simply add the following line to your Podfile: 47 | 48 | ```ruby 49 | pod 'RxDataSources-Texture' 50 | ``` 51 | 52 | ## Development 53 | 54 | ```console 55 | $ make project 56 | $ open RxDataSources-Texture.xcworkspace 57 | ``` 58 | 59 | ## Author 60 | 61 | OhKanghoon, ggaa96@naver.com 62 | 63 | ## License 64 | 65 | RxDataSources-Texture is available under the MIT license. See the LICENSE file for more info. 66 | -------------------------------------------------------------------------------- /Sources/RxDataSources-Texture/ASDelegateProxyType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASDelegateProxyType.swift 3 | // RxDataSources-Texture 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) 9 | import UIKit 10 | import AsyncDisplayKit 11 | import RxSwift 12 | import RxCocoa 13 | 14 | extension ObservableType { 15 | func subscribeProxyDataSource(ofObject object: DelegateProxy.ParentObject, dataSource: DelegateProxy.Delegate, retainDataSource: Bool, binding: @escaping (DelegateProxy, Event) -> Void) 16 | -> Disposable 17 | where DelegateProxy.ParentObject: ASDisplayNode 18 | , DelegateProxy.Delegate: AnyObject { 19 | let proxy = DelegateProxy.proxy(for: object) 20 | 21 | // disposable needs to be disposed on the main thread 22 | let unregisterDelegate = ScheduledDisposable( 23 | scheduler: MainScheduler.instance, 24 | disposable: DelegateProxy.installForwardDelegate(dataSource, retainDelegate: retainDataSource, onProxyForObject: object) 25 | ) 26 | // this is needed to flush any delayed old state (https://github.com/RxSwiftCommunity/RxDataSources/pull/75) 27 | object.layoutIfNeeded() 28 | 29 | let subscription = self.asObservable() 30 | .observe(on: MainScheduler()) 31 | .catch { error in 32 | bindingError(error) 33 | return Observable.empty() 34 | } 35 | // source can never end, otherwise it would release the subscriber, and deallocate the data source 36 | .concat(Observable.never()) 37 | .take(until: object.rx.deallocated) 38 | .subscribe { [weak object] (event: Event) in 39 | 40 | if let object = object { 41 | assert(proxy === DelegateProxy.currentDelegate(for: object), "Proxy changed from the time it was first set.\nOriginal: \(proxy)\nExisting: \(String(describing: DelegateProxy.currentDelegate(for: object)))") 42 | } 43 | 44 | binding(proxy, event) 45 | 46 | switch event { 47 | case .error(let error): 48 | bindingError(error) 49 | unregisterDelegate.dispose() 50 | case .completed: 51 | unregisterDelegate.dispose() 52 | default: 53 | break 54 | } 55 | } 56 | 57 | return Disposables.create { [weak object] in 58 | subscription.dispose() 59 | object?.layoutIfNeeded() 60 | unregisterDelegate.dispose() 61 | } 62 | } 63 | } 64 | 65 | #endif 66 | -------------------------------------------------------------------------------- /Sources/RxDataSources-Texture/ASTableNode/RxASTableDataSourceProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxASTableDataSourceProxy.swift 3 | // RxDataSources-Texture 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) 9 | 10 | import UIKit 11 | import AsyncDisplayKit 12 | import RxSwift 13 | import RxCocoa 14 | 15 | extension ASTableNode: HasDataSource { 16 | public typealias DataSource = ASTableDataSource 17 | } 18 | 19 | fileprivate let tableDataSourceNotSet = ASTableDataSourceNotSet() 20 | 21 | final class ASTableDataSourceNotSet 22 | : NSObject 23 | , ASTableDataSource { 24 | 25 | func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { 26 | return 0 27 | } 28 | 29 | func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { 30 | rxAbstractMethod(message: dataSourceNotSet) 31 | } 32 | } 33 | 34 | /// For more information take a look at `DelegateProxyType`. 35 | final class RxASTableDataSourceProxy 36 | : DelegateProxy 37 | , DelegateProxyType 38 | , ASTableDataSource { 39 | 40 | /// Typed parent object. 41 | public weak private(set) var tableNode: ASTableNode? 42 | 43 | /// - parameter tableNode: Parent object for delegate proxy. 44 | public init(tableNode: ASTableNode) { 45 | self.tableNode = tableNode 46 | super.init(parentObject: tableNode, delegateProxy: RxASTableDataSourceProxy.self) 47 | } 48 | 49 | // Register known implementations 50 | public static func registerKnownImplementations() { 51 | self.register { RxASTableDataSourceProxy(tableNode: $0) } 52 | } 53 | 54 | fileprivate weak var _requiredMethodsDataSource: ASTableDataSource? = tableDataSourceNotSet 55 | 56 | // MARK: DataSource 57 | 58 | /// Required datasource method implementation. 59 | public func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { 60 | return (_requiredMethodsDataSource 61 | ?? tableDataSourceNotSet).tableNode!(tableNode, numberOfRowsInSection: section) 62 | } 63 | 64 | /// Required datasource method implementation. 65 | public func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { 66 | return (_requiredMethodsDataSource ?? tableDataSourceNotSet).tableNode!(tableNode, nodeBlockForRowAt: indexPath) 67 | } 68 | 69 | public override func setForwardToDelegate(_ forwardToDelegate: ASTableDataSource?, retainDelegate: Bool) { 70 | _requiredMethodsDataSource = forwardToDelegate ?? tableDataSourceNotSet 71 | super.setForwardToDelegate(forwardToDelegate, retainDelegate: retainDelegate) 72 | } 73 | } 74 | #endif 75 | -------------------------------------------------------------------------------- /Sources/RxDataSources-Texture/ASTableNode/RxASTableDelegateProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxASTableDelegateProxy.swift 3 | // RxDataSources-Texture 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) 9 | 10 | import Foundation 11 | import AsyncDisplayKit 12 | import RxSwift 13 | import RxCocoa 14 | 15 | extension ASTableNode: HasDelegate { 16 | public typealias Delegate = ASTableDelegate 17 | } 18 | 19 | /// For more information take a look at `DelegateProxyType`. 20 | open class RxASTableDelegateProxy 21 | : DelegateProxy 22 | , DelegateProxyType 23 | , ASTableDelegate { 24 | 25 | /// Typed parent object. 26 | public weak private(set) var tableNode: ASTableNode? 27 | 28 | /// - parameter tableNode: Parent object for delegate proxy. 29 | public init(tableNode: ASTableNode) { 30 | self.tableNode = tableNode 31 | super.init(parentObject: tableNode, delegateProxy: RxASTableDelegateProxy.self) 32 | } 33 | 34 | // Register known implementations 35 | public static func registerKnownImplementations() { 36 | self.register { RxASTableDelegateProxy(tableNode: $0) } 37 | } 38 | 39 | fileprivate var _contentOffsetBehaviorSubject: BehaviorSubject? 40 | fileprivate var _contentOffsetPublishSubject: PublishSubject<()>? 41 | 42 | /// Optimized version used for observing content offset changes. 43 | internal var contentOffsetBehaviorSubject: BehaviorSubject { 44 | if let subject = _contentOffsetBehaviorSubject { 45 | return subject 46 | } 47 | 48 | let subject = BehaviorSubject(value: self.tableNode?.contentOffset ?? CGPoint.zero) 49 | _contentOffsetBehaviorSubject = subject 50 | 51 | return subject 52 | } 53 | 54 | /// Optimized version used for observing content offset changes. 55 | internal var contentOffsetPublishSubject: PublishSubject<()> { 56 | if let subject = _contentOffsetPublishSubject { 57 | return subject 58 | } 59 | 60 | let subject = PublishSubject<()>() 61 | _contentOffsetPublishSubject = subject 62 | 63 | return subject 64 | } 65 | 66 | // MARK: delegate methods 67 | 68 | /// For more information take a look at `DelegateProxyType`. 69 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 70 | if let subject = _contentOffsetBehaviorSubject { 71 | subject.on(.next(scrollView.contentOffset)) 72 | } 73 | if let subject = _contentOffsetPublishSubject { 74 | subject.on(.next(())) 75 | } 76 | self._forwardToDelegate?.scrollViewDidScroll?(scrollView) 77 | } 78 | 79 | deinit { 80 | if let subject = _contentOffsetBehaviorSubject { 81 | subject.on(.completed) 82 | } 83 | 84 | if let subject = _contentOffsetPublishSubject { 85 | subject.on(.completed) 86 | } 87 | } 88 | } 89 | 90 | #endif 91 | -------------------------------------------------------------------------------- /Sources/RxDataSources-Texture/ASCollectionNode/RxASCollectionDataSourceProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxASCollectionDataSourceProxy.swift 3 | // RxDataSources-Texture 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) 9 | 10 | import UIKit 11 | import AsyncDisplayKit 12 | import RxSwift 13 | import RxCocoa 14 | 15 | extension ASCollectionNode: HasDataSource { 16 | public typealias DataSource = ASCollectionDataSource 17 | } 18 | 19 | fileprivate let collectionDataSourceNotSet = ASCollectionDataSourceNotSet() 20 | 21 | final class ASCollectionDataSourceNotSet 22 | : NSObject 23 | , ASCollectionDataSource { 24 | 25 | func collectionNode(_ collectionNode: ASCollectionNode, numberOfItemsInSection section: Int) -> Int { 26 | return 0 27 | } 28 | 29 | func collectionNode(_ collectionNode: ASCollectionNode, nodeBlockForItemAt indexPath: IndexPath) -> ASCellNodeBlock { 30 | rxAbstractMethod(message: dataSourceNotSet) 31 | } 32 | } 33 | 34 | /// For more information take a look at `DelegateProxyType`. 35 | final class RxASCollectionDataSourceProxy 36 | : DelegateProxy 37 | , DelegateProxyType 38 | , ASCollectionDataSource { 39 | 40 | /// Typed parent object. 41 | public weak private(set) var collectionNode: ASCollectionNode? 42 | 43 | /// - parameter tableNode: Parent object for delegate proxy. 44 | public init(collectionNode: ASCollectionNode) { 45 | self.collectionNode = collectionNode 46 | super.init(parentObject: collectionNode, delegateProxy: RxASCollectionDataSourceProxy.self) 47 | } 48 | 49 | // Register known implementations 50 | public static func registerKnownImplementations() { 51 | self.register { RxASCollectionDataSourceProxy(collectionNode: $0) } 52 | } 53 | 54 | fileprivate weak var _requiredMethodsDataSource: ASCollectionDataSource? = collectionDataSourceNotSet 55 | 56 | // MARK: DataSource 57 | 58 | /// Required datasource method implementation. 59 | public func collectionNode(_ collectionNode: ASCollectionNode, numberOfItemsInSection section: Int) -> Int { 60 | return (_requiredMethodsDataSource 61 | ?? collectionDataSourceNotSet).collectionNode!(collectionNode, numberOfItemsInSection: section) 62 | } 63 | 64 | /// Required datasource method implementation. 65 | 66 | public func collectionNode(_ collectionNode: ASCollectionNode, nodeBlockForItemAt indexPath: IndexPath) -> ASCellNodeBlock { 67 | return (_requiredMethodsDataSource ?? collectionDataSourceNotSet).collectionNode!(collectionNode, nodeBlockForItemAt: indexPath) 68 | } 69 | 70 | public override func setForwardToDelegate(_ forwardToDelegate: ASCollectionDataSource?, retainDelegate: Bool) { 71 | _requiredMethodsDataSource = forwardToDelegate ?? collectionDataSourceNotSet 72 | super.setForwardToDelegate(forwardToDelegate, retainDelegate: retainDelegate) 73 | } 74 | } 75 | #endif 76 | -------------------------------------------------------------------------------- /Sources/RxDataSources-Texture/ASCollectionNode/RxASCollectionDelegateProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxASCollectionDelegateProxy.swift 3 | // RxDataSources-Texture 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) 9 | 10 | import Foundation 11 | import AsyncDisplayKit 12 | import RxSwift 13 | import RxCocoa 14 | 15 | extension ASCollectionNode: HasDelegate { 16 | public typealias Delegate = ASCollectionDelegate 17 | } 18 | 19 | /// For more information take a look at `DelegateProxyType`. 20 | open class RxASCollectionDelegateProxy 21 | : DelegateProxy 22 | , DelegateProxyType 23 | , ASCollectionDelegate 24 | , ASCollectionDelegateFlowLayout 25 | , UICollectionViewDelegateFlowLayout { 26 | 27 | /// Typed parent object. 28 | public weak private(set) var collectionNode: ASCollectionNode? 29 | 30 | /// - parameter tableNode: Parent object for delegate proxy. 31 | public init(collectionNode: ASCollectionNode) { 32 | self.collectionNode = collectionNode 33 | super.init(parentObject: collectionNode, delegateProxy: RxASCollectionDelegateProxy.self) 34 | } 35 | 36 | // Register known implementations 37 | public static func registerKnownImplementations() { 38 | self.register { RxASCollectionDelegateProxy(collectionNode: $0) } 39 | } 40 | 41 | fileprivate var _contentOffsetBehaviorSubject: BehaviorSubject? 42 | fileprivate var _contentOffsetPublishSubject: PublishSubject<()>? 43 | 44 | /// Optimized version used for observing content offset changes. 45 | internal var contentOffsetBehaviorSubject: BehaviorSubject { 46 | if let subject = _contentOffsetBehaviorSubject { 47 | return subject 48 | } 49 | 50 | let subject = BehaviorSubject(value: self.collectionNode?.contentOffset ?? CGPoint.zero) 51 | _contentOffsetBehaviorSubject = subject 52 | 53 | return subject 54 | } 55 | 56 | /// Optimized version used for observing content offset changes. 57 | internal var contentOffsetPublishSubject: PublishSubject<()> { 58 | if let subject = _contentOffsetPublishSubject { 59 | return subject 60 | } 61 | 62 | let subject = PublishSubject<()>() 63 | _contentOffsetPublishSubject = subject 64 | 65 | return subject 66 | } 67 | 68 | // MARK: delegate methods 69 | 70 | /// For more information take a look at `DelegateProxyType`. 71 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 72 | if let subject = _contentOffsetBehaviorSubject { 73 | subject.on(.next(scrollView.contentOffset)) 74 | } 75 | if let subject = _contentOffsetPublishSubject { 76 | subject.on(.next(())) 77 | } 78 | self._forwardToDelegate?.scrollViewDidScroll?(scrollView) 79 | } 80 | 81 | deinit { 82 | if let subject = _contentOffsetBehaviorSubject { 83 | subject.on(.completed) 84 | } 85 | 86 | if let subject = _contentOffsetPublishSubject { 87 | subject.on(.completed) 88 | } 89 | } 90 | } 91 | 92 | #endif 93 | -------------------------------------------------------------------------------- /Example/GithubRepos/RepoTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepoTableViewController.swift 3 | // GithubRepos 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AsyncDisplayKit 11 | import RxDataSources_Texture 12 | import RxSwift 13 | import RxCocoa 14 | import RxOptional 15 | 16 | class RepoTableViewController: ASDKViewController { 17 | 18 | // MARK: - Properties 19 | 20 | private let animatedDataSource = RxASTableSectionedAnimatedDataSource( 21 | configureCellBlock: { _, _, _, sectionItem in 22 | switch sectionItem { 23 | case .repo(let repoItem): 24 | return { RepoCellNode(.table, repo: repoItem) } 25 | } 26 | }) 27 | 28 | private let dataSource = RxASTableSectionedReloadDataSource( 29 | configureCellBlock: { _, _, _, sectionItem in 30 | switch sectionItem { 31 | case .repo(let repoItem): 32 | return { RepoCellNode(.table, repo: repoItem) } 33 | } 34 | }) 35 | 36 | let tableNode = ASTableNode() 37 | 38 | var batchContext: ASBatchContext? 39 | let viewModel: RepoViewModel 40 | let disposeBag = DisposeBag() 41 | 42 | 43 | // MARK: - Initialization 44 | 45 | init(viewModel: RepoViewModel) { 46 | self.viewModel = viewModel 47 | super.init(node: ASDisplayNode()) 48 | self.node.automaticallyManagesSubnodes = true 49 | self.node.automaticallyRelayoutOnSafeAreaChanges = true 50 | self.node.layoutSpecBlock = { [weak self] (_, sizeRange) -> ASLayoutSpec in 51 | return self?.layoutSpecThatFits(sizeRange) ?? ASLayoutSpec() 52 | } 53 | 54 | self.title = "Table DataSources" 55 | self.bindViewModel() 56 | viewModel.refreshRelay.accept(()) 57 | } 58 | 59 | required init?(coder aDecoder: NSCoder) { 60 | fatalError("init(coder:) has not been implemented") 61 | } 62 | 63 | 64 | // MARK: - View Life Cycle 65 | 66 | override func viewDidLoad() { 67 | super.viewDidLoad() 68 | self.tableNode.view.separatorStyle = .none 69 | self.tableNode.view.alwaysBounceVertical = true 70 | } 71 | 72 | 73 | // MARK: - Binding 74 | 75 | private func bindViewModel() { 76 | self.tableNode.rx 77 | .setDelegate(self) 78 | .disposed(by: disposeBag) 79 | 80 | self.viewModel.sections 81 | .do(onNext: { [weak self] _ in 82 | self?.batchContext?.completeBatchFetching(true) 83 | }) 84 | .bind(to: tableNode.rx.items(dataSource: animatedDataSource)) 85 | .disposed(by: disposeBag) 86 | 87 | self.tableNode.rx.willBeginBatchFetch 88 | .asObservable() 89 | .do(onNext: { [weak self] context in 90 | self?.batchContext = context 91 | }).map { _ in return } 92 | .bind(to: viewModel.loadMoreRelay) 93 | .disposed(by: disposeBag) 94 | } 95 | 96 | 97 | // MARK: - LayoutSpec 98 | 99 | func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { 100 | return ASInsetLayoutSpec(insets: self.node.safeAreaInsets, child: self.tableNode) 101 | } 102 | } 103 | 104 | extension RepoTableViewController: ASTableDelegate { 105 | func shouldBatchFetch(for tableNode: ASTableNode) -> Bool { 106 | return self.viewModel.since.value != nil 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/RxDataSources-Texture/RxASErrorHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxASErrorHandler.swift 3 | // RxDataSources-Texture 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // 7 | 8 | import Foundation 9 | import RxCocoa 10 | 11 | @_exported import Differentiator 12 | 13 | enum RxDataSourceTextureError: Error { 14 | case outOfBounds(indexPath: IndexPath) 15 | } 16 | 17 | // MARK: Error binding policies 18 | 19 | func bindingError(_ error: Swift.Error) { 20 | let error = "Binding error: \(error)" 21 | #if DEBUG 22 | rxFatalError(error) 23 | #else 24 | print(error) 25 | #endif 26 | } 27 | 28 | /// Swift does not implement abstract methods. This method is used as a runtime check to ensure that methods which intended to be abstract (i.e., they should be implemented in subclasses) are not called directly on the superclass. 29 | func rxAbstractMethod(message: String = "Abstract method", file: StaticString = #file, line: UInt = #line) -> Swift.Never { 30 | rxFatalError(message, file: file, line: line) 31 | } 32 | 33 | func rxFatalError(_ lastMessage: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line) -> Swift.Never { 34 | // The temptation to comment this line is great, but please don't, it's for your own good. The choice is yours. 35 | fatalError(lastMessage(), file: file, line: line) 36 | } 37 | 38 | func rxFatalErrorInDebug(_ lastMessage: @autoclosure () -> String, file: StaticString = #file, line: UInt = #line) { 39 | #if DEBUG 40 | fatalError(lastMessage(), file: file, line: line) 41 | #else 42 | print("\(file):\(line): \(lastMessage())") 43 | #endif 44 | } 45 | 46 | func rxDebugFatalError(_ error: Error) { 47 | rxDebugFatalError("\(error)") 48 | } 49 | 50 | func rxDebugFatalError(_ message: String) { 51 | #if DEBUG 52 | fatalError(message) 53 | #else 54 | print(message) 55 | #endif 56 | } 57 | 58 | // MARK: casts or fatal error 59 | 60 | // workaround for Swift compiler bug, cheers compiler team :) 61 | func castOptionalOrFatalError(_ value: Any?) -> T? { 62 | if value == nil { 63 | return nil 64 | } 65 | let v: T = castOrFatalError(value) 66 | return v 67 | } 68 | 69 | func castOrThrow(_ resultType: T.Type, _ object: Any) throws -> T { 70 | guard let returnValue = object as? T else { 71 | throw RxCocoaError.castingError(object: object, targetType: resultType) 72 | } 73 | 74 | return returnValue 75 | } 76 | 77 | func castOptionalOrThrow(_ resultType: T.Type, _ object: AnyObject) throws -> T? { 78 | if NSNull().isEqual(object) { 79 | return nil 80 | } 81 | 82 | guard let returnValue = object as? T else { 83 | throw RxCocoaError.castingError(object: object, targetType: resultType) 84 | } 85 | 86 | return returnValue 87 | } 88 | 89 | func castOrFatalError(_ value: AnyObject!, message: String) -> T { 90 | let maybeResult: T? = value as? T 91 | guard let result = maybeResult else { 92 | rxFatalError(message) 93 | } 94 | 95 | return result 96 | } 97 | 98 | func castOrFatalError(_ value: Any!) -> T { 99 | let maybeResult: T? = value as? T 100 | guard let result = maybeResult else { 101 | rxFatalError("Failure converting from \(String(describing: value)) to \(T.self)") 102 | } 103 | 104 | return result 105 | } 106 | 107 | // MARK: Error messages 108 | 109 | let dataSourceNotSet = "DataSource not set" 110 | let delegateNotSet = "Delegate not set" 111 | -------------------------------------------------------------------------------- /Example/GithubRepos/RepoCollectionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepoCollectionViewController.swift 3 | // GithubRepos 4 | // 5 | // Created by Kanghoon on 22/02/2019. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AsyncDisplayKit 11 | import RxDataSources_Texture 12 | import RxSwift 13 | import RxCocoa 14 | import RxOptional 15 | 16 | class RepoCollectionViewController: ASDKViewController { 17 | 18 | // MARK: - Properties 19 | 20 | var flowLayout: UICollectionViewFlowLayout = { 21 | let layout = UICollectionViewFlowLayout() 22 | layout.scrollDirection = .vertical 23 | layout.minimumInteritemSpacing = 0 24 | layout.minimumLineSpacing = 0 25 | return layout 26 | }() 27 | 28 | lazy var collectionNode: ASCollectionNode = { 29 | let node = ASCollectionNode(collectionViewLayout: flowLayout) 30 | return node 31 | }() 32 | 33 | private let animatedDataSource = RxASCollectionSectionedAnimatedDataSource( 34 | configureCellBlock: { _, _, _, sectionItem in 35 | switch sectionItem { 36 | case .repo(let repoItem): 37 | return { RepoCellNode(.collection, repo: repoItem) } 38 | } 39 | }) 40 | private let dataSource = RxASCollectionSectionedReloadDataSource( 41 | configureCellBlock: { _, _, _, sectionItem in 42 | switch sectionItem { 43 | case .repo(let repoItem): 44 | return { RepoCellNode(.collection, repo: repoItem) } 45 | } 46 | }) 47 | 48 | var batchContext: ASBatchContext? 49 | let viewModel: RepoViewModel 50 | let disposeBag = DisposeBag() 51 | 52 | 53 | // MARK: Initialization 54 | 55 | init(viewModel: RepoViewModel) { 56 | self.viewModel = viewModel 57 | super.init(node: ASDisplayNode()) 58 | self.node.automaticallyManagesSubnodes = true 59 | self.node.automaticallyRelayoutOnSafeAreaChanges = true 60 | self.node.layoutSpecBlock = { [weak self] (_, sizeRange) -> ASLayoutSpec in 61 | return self?.layoutSpecThatFits(sizeRange) ?? ASLayoutSpec() 62 | } 63 | 64 | self.title = "Collection DataSources" 65 | self.bindViewModel() 66 | viewModel.refreshRelay.accept(()) 67 | } 68 | 69 | required init?(coder aDecoder: NSCoder) { 70 | fatalError("init(coder:) has not been implemented") 71 | } 72 | 73 | 74 | // MARK: - View Life Cycle 75 | 76 | override func viewDidLoad() { 77 | super.viewDidLoad() 78 | self.collectionNode.view.alwaysBounceVertical = true 79 | } 80 | 81 | 82 | // MARK: - Binding 83 | 84 | private func bindViewModel() { 85 | self.collectionNode.rx 86 | .setDelegate(self) 87 | .disposed(by: disposeBag) 88 | 89 | self.viewModel.sections 90 | .do(onNext: { [weak self] _ in 91 | self?.batchContext?.completeBatchFetching(true) 92 | }) 93 | .bind(to: collectionNode.rx.items(dataSource: animatedDataSource)) 94 | .disposed(by: disposeBag) 95 | 96 | self.collectionNode.rx.willBeginBatchFetch 97 | .do(onNext: { [weak self] context in 98 | self?.batchContext = context 99 | }).map { _ in return } 100 | .bind(to: viewModel.loadMoreRelay) 101 | .disposed(by: disposeBag) 102 | } 103 | 104 | 105 | // MARK: - LayoutSpec 106 | 107 | func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { 108 | return ASInsetLayoutSpec(insets: self.node.safeAreaInsets, 109 | child: self.collectionNode) 110 | } 111 | } 112 | 113 | extension RepoCollectionViewController: ASCollectionDelegate { 114 | func shouldBatchFetch(for collectionNode: ASCollectionNode) -> Bool { 115 | return self.viewModel.since.value != nil 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/RxDataSources-Texture/ASCollectionNode/RxASCollectionSectionedAnimatedDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxASCollectionSectionedAnimatedDataSource.swift 3 | // RxDataSources-Texture 4 | // 5 | // Created by Kanghoon on 21/02/2019. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) 9 | import Foundation 10 | import AsyncDisplayKit 11 | #if !RX_NO_MODULE 12 | import RxSwift 13 | import RxCocoa 14 | #endif 15 | import Differentiator 16 | 17 | open class RxASCollectionSectionedAnimatedDataSource 18 | : ASCollectionSectionedDataSource 19 | , RxASCollectionDataSourceType { 20 | 21 | public typealias Element = [S] 22 | public typealias DecideNodeTransition = (ASCollectionSectionedDataSource, ASCollectionNode, [Changeset]) -> NodeTransition 23 | 24 | /// Animation configuration for data source 25 | public var animationConfiguration: AnimationConfiguration 26 | 27 | /// Calculates view transition depending on type of changes 28 | public var decideNodeTransition: DecideNodeTransition 29 | 30 | public init( 31 | animationConfiguration: AnimationConfiguration = AnimationConfiguration(), 32 | decideNodeTransition: @escaping DecideNodeTransition = { _, _, _ in .animated }, 33 | configureCellBlock: @escaping ConfigureCellBlock, 34 | configureSupplementaryNode: ConfigureSupplementaryNode? = nil, 35 | moveItem: @escaping MoveItem = { _, _, _ in () }, 36 | canMoveItemWith: @escaping CanMoveItemWith = { _, _ in false } 37 | ) { 38 | self.animationConfiguration = animationConfiguration 39 | self.decideNodeTransition = decideNodeTransition 40 | super.init( 41 | configureCellBlock: configureCellBlock, 42 | configureSupplementaryNode: configureSupplementaryNode, 43 | moveItem: moveItem, 44 | canMoveItemWith: canMoveItemWith 45 | ) 46 | } 47 | 48 | // there is no longer limitation to load initial sections with reloadData 49 | // but it is kept as a feature everyone got used to 50 | var dataSet = false 51 | 52 | open func collectionNode(_ collectionNode: ASCollectionNode, observedEvent: Event) { 53 | Binder(self) { dataSource, newSections in 54 | #if DEBUG 55 | dataSource._dataSourceBound = true 56 | #endif 57 | if !dataSource.dataSet { 58 | dataSource.dataSet = true 59 | dataSource.setSections(newSections) 60 | collectionNode.reloadData() 61 | } 62 | else { 63 | let oldSections = dataSource.sectionModels 64 | do { 65 | let differences = try Diff.differencesForSectionedView(initialSections: oldSections, finalSections: newSections) 66 | 67 | switch dataSource.decideNodeTransition(dataSource, collectionNode, differences) { 68 | case .animated: 69 | // each difference must be run in a separate 'performBatchUpdates', otherwise it crashes. 70 | // this is a limitation of Diff tool 71 | for difference in differences { 72 | let updateBlock = { 73 | // sections must be set within updateBlock in 'performBatchUpdates' 74 | dataSource.setSections(difference.finalSections) 75 | 76 | collectionNode.batchUpdates(difference, animationConfiguration: dataSource.animationConfiguration) 77 | } 78 | collectionNode.performBatch(animated: dataSource.animationConfiguration.animated, 79 | updates: updateBlock, 80 | completion: nil) 81 | } 82 | 83 | case .reload: 84 | dataSource.setSections(newSections) 85 | collectionNode.reloadData() 86 | return 87 | } 88 | } 89 | catch let e { 90 | rxDebugFatalError(e) 91 | dataSource.setSections(newSections) 92 | collectionNode.reloadData() 93 | } 94 | } 95 | }.on(observedEvent) 96 | } 97 | } 98 | #endif 99 | -------------------------------------------------------------------------------- /Example/GithubRepos/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Sources/RxDataSources-Texture/ASTableNode/RxASTableSectionedAnimatedDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxASTableSectionedAnimatedDataSource.swift 3 | // RxDataSources-Texture 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) 9 | import Foundation 10 | import AsyncDisplayKit 11 | #if !RX_NO_MODULE 12 | import RxSwift 13 | import RxCocoa 14 | #endif 15 | import Differentiator 16 | 17 | open class RxASTableSectionedAnimatedDataSource 18 | : ASTableSectionedDataSource 19 | , RxASTableDataSourceType { 20 | 21 | public typealias Element = [S] 22 | public typealias DecideNodeTransition = (ASTableSectionedDataSource, ASTableNode, [Changeset]) -> NodeTransition 23 | 24 | /// Animation configuration for data source 25 | public var animationConfiguration: AnimationConfiguration 26 | 27 | /// Calculates view transition depending on type of changes 28 | public var decideNodeTransition: DecideNodeTransition 29 | private var dataSet = false 30 | 31 | #if os(iOS) 32 | public init( 33 | animationConfiguration: AnimationConfiguration = AnimationConfiguration(), 34 | decideNodeTransition: @escaping DecideNodeTransition = { _, _, _ in .animated }, 35 | configureCellBlock: @escaping ConfigureCellBlock, 36 | titleForHeaderInSection: @escaping TitleForHeaderInSection = { _, _ in nil }, 37 | titleForFooterInSection: @escaping TitleForFooterInSection = { _, _ in nil }, 38 | canEditRowAtIndexPath: @escaping CanEditRowAtIndexPath = { _, _ in false }, 39 | canMoveRowAtIndexPath: @escaping CanMoveRowAtIndexPath = { _, _ in false }, 40 | sectionIndexTitles: @escaping SectionIndexTitles = { _ in nil }, 41 | sectionForSectionIndexTitle: @escaping SectionForSectionIndexTitle = { _, _, index in index } 42 | ) { 43 | self.animationConfiguration = animationConfiguration 44 | self.decideNodeTransition = decideNodeTransition 45 | super.init( 46 | configureCellBlock: configureCellBlock, 47 | titleForHeaderInSection: titleForHeaderInSection, 48 | titleForFooterInSection: titleForFooterInSection, 49 | canEditRowAtIndexPath: canEditRowAtIndexPath, 50 | canMoveRowAtIndexPath: canMoveRowAtIndexPath, 51 | sectionIndexTitles: sectionIndexTitles, 52 | sectionForSectionIndexTitle: sectionForSectionIndexTitle 53 | ) 54 | } 55 | #else 56 | public init( 57 | animationConfiguration: AnimationConfiguration = AnimationConfiguration(), 58 | decideNodeTransition: @escaping DecideNodeTransition = { _, _, _ in .animated }, 59 | configureCellBlock: @escaping ConfigureCellBlock, 60 | titleForHeaderInSection: @escaping TitleForHeaderInSection = { _, _ in nil }, 61 | titleForFooterInSection: @escaping TitleForFooterInSection = { _, _ in nil }, 62 | canEditRowAtIndexPath: @escaping CanEditRowAtIndexPath = { _, _ in false }, 63 | canMoveRowAtIndexPath: @escaping CanMoveRowAtIndexPath = { _, _ in false } 64 | ) { 65 | self.animationConfiguration = animationConfiguration 66 | self.decideNodeTransition = decideNodeTransition 67 | super.init( 68 | configureCellBlock: configureCellBlock, 69 | titleForHeaderInSection: titleForHeaderInSection, 70 | titleForFooterInSection: titleForFooterInSection, 71 | canEditRowAtIndexPath: canEditRowAtIndexPath, 72 | canMoveRowAtIndexPath: canMoveRowAtIndexPath 73 | ) 74 | } 75 | #endif 76 | 77 | public func tableNode(_ tableNode: ASTableNode, observedEvent: Event<[S]>) { 78 | Binder(self) { dataSource, newSections in 79 | #if DEBUG 80 | dataSource._dataSourceBound = true 81 | #endif 82 | if !dataSource.dataSet { 83 | dataSource.dataSet = true 84 | dataSource.setSections(newSections) 85 | tableNode.reloadData() 86 | } 87 | else { 88 | let oldSections = dataSource.sectionModels 89 | do { 90 | let differences = try Diff.differencesForSectionedView(initialSections: oldSections, finalSections: newSections) 91 | 92 | switch dataSource.decideNodeTransition(dataSource, tableNode, differences) { 93 | case .animated: 94 | for difference in differences { 95 | let updateBlock = { 96 | // sections must be set within updateBlock in 'performBatchUpdates' 97 | dataSource.setSections(difference.finalSections) 98 | tableNode.batchUpdates(difference, animationConfiguration: dataSource.animationConfiguration) 99 | } 100 | tableNode.performBatch(animated: dataSource.animationConfiguration.animated, 101 | updates: updateBlock, 102 | completion: nil) 103 | } 104 | case .reload: 105 | dataSource.setSections(newSections) 106 | tableNode.reloadData() 107 | return 108 | } 109 | } 110 | catch let e { 111 | rxDebugFatalError(e) 112 | dataSource.setSections(newSections) 113 | tableNode.reloadData() 114 | } 115 | } 116 | }.on(observedEvent) 117 | } 118 | } 119 | #endif 120 | -------------------------------------------------------------------------------- /Sources/RxDataSources-Texture/UI+SectionNodeType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UI+SectionNodeType.swift 3 | // RxTextureDataSources 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) 9 | import Foundation 10 | import AsyncDisplayKit 11 | import Differentiator 12 | 13 | #if swift(>=4.2) 14 | public typealias UITableViewRowAnimation = UITableView.RowAnimation 15 | #endif 16 | 17 | func indexSet(_ values: [Int]) -> IndexSet { 18 | let indexSet = NSMutableIndexSet() 19 | for i in values { 20 | indexSet.add(i) 21 | } 22 | return indexSet as IndexSet 23 | } 24 | 25 | extension ASTableNode: SectionedNodeType { 26 | 27 | public func insertItemsAtIndexPaths(_ paths: [IndexPath], animationStyle: UITableViewRowAnimation) { 28 | self.insertRows(at: paths, with: animationStyle) 29 | } 30 | 31 | public func deleteItemsAtIndexPaths(_ paths: [IndexPath], animationStyle: UITableViewRowAnimation) { 32 | self.deleteRows(at: paths, with: animationStyle) 33 | } 34 | 35 | public func moveItemAtIndexPath(_ from: IndexPath, to: IndexPath) { 36 | self.moveRow(at: from, to: to) 37 | } 38 | 39 | public func reloadItemsAtIndexPaths(_ paths: [IndexPath], animationStyle: UITableViewRowAnimation) { 40 | self.reloadRows(at: paths, with: animationStyle) 41 | } 42 | 43 | public func insertSections(_ sections: [Int], animationStyle: UITableViewRowAnimation) { 44 | self.insertSections(indexSet(sections), with: animationStyle) 45 | } 46 | 47 | public func deleteSections(_ sections: [Int], animationStyle: UITableViewRowAnimation) { 48 | self.deleteSections(indexSet(sections), with: animationStyle) 49 | } 50 | 51 | public func moveSection(_ from: Int, to: Int) { 52 | self.moveSection(from, toSection: to) 53 | } 54 | 55 | public func reloadSections(_ sections: [Int], animationStyle: UITableViewRowAnimation) { 56 | self.reloadSections(indexSet(sections), with: animationStyle) 57 | } 58 | } 59 | 60 | extension ASCollectionNode: SectionedNodeType { 61 | 62 | public func insertItemsAtIndexPaths(_ paths: [IndexPath], animationStyle: UITableViewRowAnimation) { 63 | self.insertItems(at: paths) 64 | } 65 | 66 | public func deleteItemsAtIndexPaths(_ paths: [IndexPath], animationStyle: UITableViewRowAnimation) { 67 | self.deleteItems(at: paths) 68 | } 69 | 70 | public func moveItemAtIndexPath(_ from: IndexPath, to: IndexPath) { 71 | self.moveItem(at: from, to: to) 72 | } 73 | 74 | public func reloadItemsAtIndexPaths(_ paths: [IndexPath], animationStyle: UITableViewRowAnimation) { 75 | self.reloadItems(at: paths) 76 | } 77 | 78 | public func insertSections(_ sections: [Int], animationStyle: UITableViewRowAnimation) { 79 | self.insertSections(indexSet(sections)) 80 | } 81 | 82 | public func deleteSections(_ sections: [Int], animationStyle: UITableViewRowAnimation) { 83 | self.deleteSections(indexSet(sections)) 84 | } 85 | 86 | public func moveSection(_ from: Int, to: Int) { 87 | self.moveSection(from, toSection: to) 88 | } 89 | 90 | public func reloadSections(_ sections: [Int], animationStyle: UITableViewRowAnimation) { 91 | self.reloadSections(indexSet(sections)) 92 | } 93 | } 94 | 95 | public protocol SectionedNodeType { 96 | func insertItemsAtIndexPaths(_ paths: [IndexPath], animationStyle: UITableViewRowAnimation) 97 | func deleteItemsAtIndexPaths(_ paths: [IndexPath], animationStyle: UITableViewRowAnimation) 98 | func moveItemAtIndexPath(_ from: IndexPath, to: IndexPath) 99 | func reloadItemsAtIndexPaths(_ paths: [IndexPath], animationStyle: UITableViewRowAnimation) 100 | 101 | func insertSections(_ sections: [Int], animationStyle: UITableViewRowAnimation) 102 | func deleteSections(_ sections: [Int], animationStyle: UITableViewRowAnimation) 103 | func moveSection(_ from: Int, to: Int) 104 | func reloadSections(_ sections: [Int], animationStyle: UITableViewRowAnimation) 105 | } 106 | 107 | extension SectionedNodeType { 108 | 109 | public func batchUpdates(_ changes: Changeset, animationConfiguration: AnimationConfiguration) { 110 | typealias I = S.Item 111 | 112 | deleteSections(changes.deletedSections, animationStyle: animationConfiguration.deleteAnimation) 113 | // Updated sections doesn't mean reload entire section, somebody needs to update the section view manually 114 | // otherwise all cells will be reloaded for nothing. 115 | //view.reloadSections(changes.updatedSections, animationStyle: rowAnimation) 116 | insertSections(changes.insertedSections, animationStyle: animationConfiguration.insertAnimation) 117 | for (from, to) in changes.movedSections { 118 | moveSection(from, to: to) 119 | } 120 | 121 | deleteItemsAtIndexPaths( 122 | changes.deletedItems.map { IndexPath(item: $0.itemIndex, section: $0.sectionIndex) }, 123 | animationStyle: animationConfiguration.deleteAnimation 124 | ) 125 | insertItemsAtIndexPaths( 126 | changes.insertedItems.map { IndexPath(item: $0.itemIndex, section: $0.sectionIndex) }, 127 | animationStyle: animationConfiguration.insertAnimation 128 | ) 129 | reloadItemsAtIndexPaths( 130 | changes.updatedItems.map { IndexPath(item: $0.itemIndex, section: $0.sectionIndex) }, 131 | animationStyle: animationConfiguration.reloadAnimation 132 | ) 133 | 134 | for (from, to) in changes.movedItems { 135 | moveItemAtIndexPath( 136 | IndexPath(item: from.itemIndex, section: from.sectionIndex), 137 | to: IndexPath(item: to.itemIndex, section: to.sectionIndex) 138 | ) 139 | } 140 | } 141 | 142 | } 143 | #endif 144 | -------------------------------------------------------------------------------- /Sources/RxDataSources-Texture/ASCollectionNode/ASCollectionSectionedDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASCollectionSectionedDataSource.swift 3 | // RxDataSources-Texture 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) 9 | import Foundation 10 | import AsyncDisplayKit 11 | #if !RX_NO_MODULE 12 | import RxCocoa 13 | #endif 14 | import Differentiator 15 | 16 | open class ASCollectionSectionedDataSource 17 | : NSObject 18 | , ASCollectionDataSource 19 | , SectionedViewDataSourceType { 20 | 21 | public typealias I = S.Item 22 | public typealias Section = S 23 | 24 | public typealias ConfigureCellBlock = (ASCollectionSectionedDataSource, ASCollectionNode, IndexPath, I) -> ASCellNodeBlock 25 | public typealias ConfigureSupplementaryNode = (ASCollectionSectionedDataSource, ASCollectionNode, String, IndexPath) -> ASCellNode 26 | public typealias MoveItem = (ASCollectionSectionedDataSource, _ sourceIndexPath: IndexPath, _ destinationIndexPath:IndexPath) -> Void 27 | public typealias CanMoveItemWith = (ASCollectionSectionedDataSource, ASCellNode) -> Bool 28 | 29 | public init( 30 | configureCellBlock: @escaping ConfigureCellBlock, 31 | configureSupplementaryNode: ConfigureSupplementaryNode? = nil, 32 | moveItem: @escaping MoveItem = { _, _, _ in () }, 33 | canMoveItemWith: @escaping CanMoveItemWith = { _, _ in false } 34 | ) { 35 | self.configureCellBlock = configureCellBlock 36 | self.configureSupplementaryNode = configureSupplementaryNode 37 | self.moveItem = moveItem 38 | self.canMoveItemWith = canMoveItemWith 39 | } 40 | 41 | #if DEBUG 42 | // If data source has already been bound, then mutating it 43 | // afterwards isn't something desired. 44 | // This simulates immutability after binding 45 | var _dataSourceBound: Bool = false 46 | 47 | private func ensureNotMutatedAfterBinding() { 48 | assert(!_dataSourceBound, "Data source is already bound. Please write this line before binding call (`bindTo`, `drive`). Data source must first be completely configured, and then bound after that, otherwise there could be runtime bugs, glitches, or partial malfunctions.") 49 | } 50 | 51 | #endif 52 | 53 | // This structure exists because model can be mutable 54 | // In that case current state value should be preserved. 55 | // The state that needs to be preserved is ordering of items in section 56 | // and their relationship with section. 57 | // If particular item is mutable, that is irrelevant for this logic to function 58 | // properly. 59 | public typealias SectionModelSnapshot = SectionModel 60 | 61 | private var _sectionModels: [SectionModelSnapshot] = [] 62 | 63 | open var sectionModels: [S] { 64 | return _sectionModels.map { Section(original: $0.model, items: $0.items) } 65 | } 66 | 67 | open subscript(section: Int) -> S { 68 | let sectionModel = self._sectionModels[section] 69 | return S(original: sectionModel.model, items: sectionModel.items) 70 | } 71 | 72 | open subscript(indexPath: IndexPath) -> I { 73 | get { 74 | return self._sectionModels[indexPath.section].items[indexPath.item] 75 | } 76 | set(item) { 77 | var section = self._sectionModels[indexPath.section] 78 | section.items[indexPath.item] = item 79 | self._sectionModels[indexPath.section] = section 80 | } 81 | } 82 | 83 | open func model(at indexPath: IndexPath) throws -> Any { 84 | guard indexPath.section < self._sectionModels.count, 85 | indexPath.item < self._sectionModels[indexPath.section].items.count else { 86 | throw RxDataSourceTextureError.outOfBounds(indexPath: indexPath) 87 | } 88 | return self[indexPath] 89 | } 90 | 91 | open func setSections(_ sections: [S]) { 92 | self._sectionModels = sections.map { SectionModelSnapshot(model: $0, items: $0.items) } 93 | } 94 | 95 | open var configureCellBlock: ConfigureCellBlock { 96 | didSet { 97 | #if DEBUG 98 | ensureNotMutatedAfterBinding() 99 | #endif 100 | } 101 | } 102 | 103 | open var configureSupplementaryNode: ConfigureSupplementaryNode? { 104 | didSet { 105 | #if DEBUG 106 | ensureNotMutatedAfterBinding() 107 | #endif 108 | } 109 | } 110 | 111 | open var moveItem: MoveItem { 112 | didSet { 113 | #if DEBUG 114 | ensureNotMutatedAfterBinding() 115 | #endif 116 | } 117 | } 118 | 119 | open var canMoveItemWith: ((ASCollectionSectionedDataSource, ASCellNode) -> Bool)? { 120 | didSet { 121 | #if DEBUG 122 | ensureNotMutatedAfterBinding() 123 | #endif 124 | } 125 | } 126 | 127 | // ASCollectionDataSource 128 | 129 | open func numberOfSections(in collectionNode: ASCollectionNode) -> Int { 130 | return _sectionModels.count 131 | } 132 | 133 | open func collectionNode(_ collectionNode: ASCollectionNode, numberOfItemsInSection section: Int) -> Int { 134 | return _sectionModels[section].items.count 135 | } 136 | 137 | open func collectionNode(_ collectionNode: ASCollectionNode, nodeBlockForItemAt indexPath: IndexPath) -> ASCellNodeBlock { 138 | precondition(indexPath.item < _sectionModels[indexPath.section].items.count) 139 | 140 | return configureCellBlock(self, collectionNode, indexPath, self[indexPath]) 141 | } 142 | 143 | open func collectionNode(_ collectionNode: ASCollectionNode, nodeForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> ASCellNode { 144 | return configureSupplementaryNode!(self, collectionNode, kind, indexPath) 145 | } 146 | 147 | open func collectionNode(_ collectionNode: ASCollectionNode, canMoveItemWith node: ASCellNode) -> Bool { 148 | guard let canMoveItem = canMoveItemWith?(self, node) else { 149 | return false 150 | } 151 | return canMoveItem 152 | } 153 | 154 | open func collectionNode(_ collectionNode: ASCollectionNode, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { 155 | self._sectionModels.moveFromSourceIndexPath(sourceIndexPath, destinationIndexPath: destinationIndexPath) 156 | self.moveItem(self, sourceIndexPath, destinationIndexPath) 157 | } 158 | 159 | open override func responds(to aSelector: Selector!) -> Bool { 160 | if aSelector == #selector(ASCollectionDataSource.collectionNode(_:nodeForSupplementaryElementOfKind:at:)) { 161 | return configureSupplementaryNode != nil 162 | } 163 | else { 164 | return super.responds(to: aSelector) 165 | } 166 | } 167 | } 168 | #endif 169 | -------------------------------------------------------------------------------- /Sources/RxDataSources-Texture/ASTableNode/ASTableSectionedDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASTableSectionedDataSource.swift 3 | // RxDataSources-Texture 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) 9 | import Foundation 10 | import AsyncDisplayKit 11 | #if !RX_NO_MODULE 12 | import RxCocoa 13 | #endif 14 | import Differentiator 15 | 16 | open class ASTableSectionedDataSource 17 | : NSObject 18 | , ASTableDataSource 19 | , SectionedViewDataSourceType { 20 | 21 | public typealias I = S.Item 22 | public typealias Section = S 23 | 24 | public typealias ConfigureCellBlock = (ASTableSectionedDataSource, ASTableNode, IndexPath, I) -> ASCellNodeBlock 25 | public typealias TitleForHeaderInSection = (ASTableSectionedDataSource, Int) -> String? 26 | public typealias TitleForFooterInSection = (ASTableSectionedDataSource, Int) -> String? 27 | public typealias CanEditRowAtIndexPath = (ASTableSectionedDataSource, IndexPath) -> Bool 28 | public typealias CanMoveRowAtIndexPath = (ASTableSectionedDataSource, IndexPath) -> Bool 29 | 30 | #if os(iOS) 31 | public typealias SectionIndexTitles = (ASTableSectionedDataSource) -> [String]? 32 | public typealias SectionForSectionIndexTitle = (ASTableSectionedDataSource, _ title: String, _ index: Int) -> Int 33 | #endif 34 | 35 | #if os(iOS) 36 | public init( 37 | configureCellBlock: @escaping ConfigureCellBlock, 38 | titleForHeaderInSection: @escaping TitleForHeaderInSection = { _, _ in nil }, 39 | titleForFooterInSection: @escaping TitleForFooterInSection = { _, _ in nil }, 40 | canEditRowAtIndexPath: @escaping CanEditRowAtIndexPath = { _, _ in false }, 41 | canMoveRowAtIndexPath: @escaping CanMoveRowAtIndexPath = { _, _ in false }, 42 | sectionIndexTitles: @escaping SectionIndexTitles = { _ in nil }, 43 | sectionForSectionIndexTitle: @escaping SectionForSectionIndexTitle = { _, _, index in index } 44 | ) { 45 | self.configureCellBlock = configureCellBlock 46 | self.titleForHeaderInSection = titleForHeaderInSection 47 | self.titleForFooterInSection = titleForFooterInSection 48 | self.canEditRowAtIndexPath = canEditRowAtIndexPath 49 | self.canMoveRowAtIndexPath = canMoveRowAtIndexPath 50 | self.sectionIndexTitles = sectionIndexTitles 51 | self.sectionForSectionIndexTitle = sectionForSectionIndexTitle 52 | } 53 | 54 | #else 55 | public init( 56 | configureCellBlock: @escaping configureCellBlock, 57 | titleForHeaderInSection: @escaping TitleForHeaderInSection = { _, _ in nil }, 58 | titleForFooterInSection: @escaping TitleForFooterInSection = { _, _ in nil }, 59 | canEditRowAtIndexPath: @escaping CanEditRowAtIndexPath = { _, _ in false }, 60 | canMoveRowAtIndexPath: @escaping CanMoveRowAtIndexPath = { _, _ in false } 61 | ) { 62 | self.configureCellBlock = configureCellBlock 63 | self.titleForHeaderInSection = titleForHeaderInSection 64 | self.titleForFooterInSection = titleForFooterInSection 65 | self.canEditRowAtIndexPath = canEditRowAtIndexPath 66 | self.canMoveRowAtIndexPath = canMoveRowAtIndexPath 67 | } 68 | #endif 69 | 70 | #if DEBUG 71 | // If data source has already been bound, then mutating it 72 | // afterwards isn't something desired. 73 | // This simulates immutability after binding 74 | var _dataSourceBound: Bool = false 75 | 76 | private func ensureNotMutatedAfterBinding() { 77 | assert(!_dataSourceBound, "Data source is already bound. Please write this line before binding call (`bindTo`, `drive`). Data source must first be completely configured, and then bound after that, otherwise there could be runtime bugs, glitches, or partial malfunctions.") 78 | } 79 | 80 | #endif 81 | 82 | // This structure exists because model can be mutable 83 | // In that case current state value should be preserved. 84 | // The state that needs to be preserved is ordering of items in section 85 | // and their relationship with section. 86 | // If particular item is mutable, that is irrelevant for this logic to function 87 | // properly. 88 | public typealias SectionModelSnapshot = SectionModel 89 | 90 | private var _sectionModels: [SectionModelSnapshot] = [] 91 | 92 | open var sectionModels: [S] { 93 | return _sectionModels.map { Section(original: $0.model, items: $0.items) } 94 | } 95 | 96 | open subscript(section: Int) -> S { 97 | let sectionModel = self._sectionModels[section] 98 | return S(original: sectionModel.model, items: sectionModel.items) 99 | } 100 | 101 | open subscript(indexPath: IndexPath) -> I { 102 | get { 103 | return self._sectionModels[indexPath.section].items[indexPath.row] 104 | } 105 | set(item) { 106 | var section = self._sectionModels[indexPath.section] 107 | section.items[indexPath.row] = item 108 | self._sectionModels[indexPath.section] = section 109 | } 110 | } 111 | 112 | open func model(at indexPath: IndexPath) throws -> Any { 113 | guard indexPath.section < self._sectionModels.count, 114 | indexPath.row < self._sectionModels[indexPath.section].items.count else { 115 | throw RxDataSourceTextureError.outOfBounds(indexPath: indexPath) 116 | } 117 | return self[indexPath] 118 | } 119 | 120 | open func setSections(_ sections: [S]) { 121 | self._sectionModels = sections.map { SectionModelSnapshot(model: $0, items: $0.items) } 122 | } 123 | 124 | open var configureCellBlock: ConfigureCellBlock { 125 | didSet { 126 | #if DEBUG 127 | ensureNotMutatedAfterBinding() 128 | #endif 129 | } 130 | } 131 | 132 | open var titleForHeaderInSection: TitleForHeaderInSection { 133 | didSet { 134 | #if DEBUG 135 | ensureNotMutatedAfterBinding() 136 | #endif 137 | } 138 | } 139 | open var titleForFooterInSection: TitleForFooterInSection { 140 | didSet { 141 | #if DEBUG 142 | ensureNotMutatedAfterBinding() 143 | #endif 144 | } 145 | } 146 | 147 | open var canEditRowAtIndexPath: CanEditRowAtIndexPath { 148 | didSet { 149 | #if DEBUG 150 | ensureNotMutatedAfterBinding() 151 | #endif 152 | } 153 | } 154 | open var canMoveRowAtIndexPath: CanMoveRowAtIndexPath { 155 | didSet { 156 | #if DEBUG 157 | ensureNotMutatedAfterBinding() 158 | #endif 159 | } 160 | } 161 | 162 | open var rowAnimation: UITableViewRowAnimation = .automatic 163 | 164 | #if os(iOS) 165 | open var sectionIndexTitles: SectionIndexTitles { 166 | didSet { 167 | #if DEBUG 168 | ensureNotMutatedAfterBinding() 169 | #endif 170 | } 171 | } 172 | open var sectionForSectionIndexTitle: SectionForSectionIndexTitle { 173 | didSet { 174 | #if DEBUG 175 | ensureNotMutatedAfterBinding() 176 | #endif 177 | } 178 | } 179 | #endif 180 | 181 | // ASTableDataSource 182 | 183 | open func numberOfSections(in tableNode: ASTableNode) -> Int { 184 | return _sectionModels.count 185 | } 186 | 187 | open func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { 188 | guard _sectionModels.count > section else { return 0 } 189 | return _sectionModels[section].items.count 190 | } 191 | 192 | public func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { 193 | precondition(indexPath.row < _sectionModels[indexPath.section].items.count) 194 | 195 | return configureCellBlock(self, tableNode, indexPath, self[indexPath]) 196 | } 197 | 198 | open func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 199 | return titleForHeaderInSection(self, section) 200 | } 201 | 202 | open func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { 203 | return titleForFooterInSection(self, section) 204 | } 205 | 206 | open func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 207 | return canEditRowAtIndexPath(self, indexPath) 208 | } 209 | 210 | open func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { 211 | return canMoveRowAtIndexPath(self, indexPath) 212 | } 213 | 214 | open func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { 215 | self._sectionModels.moveFromSourceIndexPath(sourceIndexPath, destinationIndexPath: destinationIndexPath) 216 | } 217 | 218 | #if os(iOS) 219 | open func sectionIndexTitles(for tableView: UITableView) -> [String]? { 220 | return sectionIndexTitles(self) 221 | } 222 | 223 | open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int { 224 | return sectionForSectionIndexTitle(self, title, index) 225 | } 226 | #endif 227 | } 228 | #endif 229 | -------------------------------------------------------------------------------- /Sources/RxDataSources-Texture/ASCollectionNode/ASCollectionNode+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASCollectionNode+Rx.swift 3 | // RxDataSources-Texture 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) 9 | 10 | import Foundation 11 | import AsyncDisplayKit 12 | import RxSwift 13 | import RxCocoa 14 | 15 | // Items 16 | 17 | extension Reactive where Base: ASCollectionNode { 18 | 19 | /** 20 | Binds sequences of elements to collection node items using a custom reactive data used to perform the transformation. 21 | 22 | - parameter dataSource: Data source used to transform elements to cell nodes. 23 | - parameter source: Observable sequence of items. 24 | - returns: Disposable object that can be used to unbind. 25 | 26 | */ 27 | public func items< 28 | DataSource: RxASCollectionDataSourceType & ASCollectionDataSource, 29 | O: ObservableType> 30 | (dataSource: DataSource) 31 | -> (_ source: O) 32 | -> Disposable where DataSource.Element == O.Element { 33 | return { source in 34 | return source.subscribeProxyDataSource(ofObject: self.base, dataSource: dataSource, retainDataSource: true) { [weak collectionNode = self.base] (_: RxASCollectionDataSourceProxy, event) -> Void in 35 | guard let collectionNode = collectionNode else { 36 | return 37 | } 38 | dataSource.collectionNode(collectionNode, observedEvent: event) 39 | } 40 | } 41 | } 42 | } 43 | 44 | extension Reactive where Base: ASCollectionNode { 45 | 46 | /** 47 | Reactive wrapper for `delegate`. 48 | 49 | For more information take a look at `DelegateProxyType` protocol documentation. 50 | */ 51 | public var delegate: DelegateProxy { 52 | return RxASCollectionDelegateProxy.proxy(for: base) 53 | } 54 | 55 | /** 56 | Reactive wrapper for `dataSource`. 57 | 58 | For more information take a look at `DelegateProxyType` protocol documentation. 59 | */ 60 | public var dataSource: DelegateProxy { 61 | return RxASCollectionDataSourceProxy.proxy(for: base) 62 | } 63 | 64 | /** 65 | Installs data source as forwarding delegate on `rx.dataSource`. 66 | Data source won't be retained. 67 | 68 | It enables using normal delegate mechanism with reactive delegate mechanism. 69 | 70 | - parameter dataSource: Data source object. 71 | - returns: Disposable object that can be used to unbind the data source. 72 | */ 73 | public func setDataSource(_ dataSource: ASCollectionDataSource) -> Disposable { 74 | return ScheduledDisposable( 75 | scheduler: MainScheduler.instance, 76 | disposable: RxASCollectionDataSourceProxy.installForwardDelegate(dataSource, retainDelegate: false, onProxyForObject: self.base) 77 | ) 78 | } 79 | 80 | /** 81 | Installs delegate as forwarding delegate on `rx.delegate`. 82 | Data source won't be retained. 83 | 84 | It enables using normal delegate mechanism with reactive delegate mechanism. 85 | 86 | - parameter delegate: Delegate object 87 | - returns: Disposable object that can be used to unbind the delegate. 88 | */ 89 | public func setDelegate(_ delegate: ASCollectionDelegate) -> Disposable { 90 | return ScheduledDisposable( 91 | scheduler: MainScheduler.instance, 92 | disposable: RxASCollectionDelegateProxy.installForwardDelegate(delegate, retainDelegate: false, onProxyForObject: self.base) 93 | ) 94 | } 95 | 96 | /// Reactive wrapper for `contentOffset`. 97 | public var contentOffset: ControlProperty { 98 | let proxy = RxASCollectionDelegateProxy.proxy(for: base) 99 | 100 | let bindingObserver = Binder(self.base) { collectionNode, contentOffset in 101 | collectionNode.contentOffset = contentOffset 102 | } 103 | 104 | return ControlProperty(values: proxy.contentOffsetBehaviorSubject, valueSink: bindingObserver) 105 | } 106 | 107 | /// Reactive wrapper for delegate method `scrollViewDidScroll` 108 | public var didScroll: ControlEvent { 109 | let source = RxASCollectionDelegateProxy.proxy(for: base).contentOffsetPublishSubject 110 | return ControlEvent(events: source) 111 | } 112 | 113 | /// Reactive wrapper for delegate method `scrollViewWillBeginDecelerating` 114 | public var willBeginDecelerating: ControlEvent { 115 | let source = delegate.methodInvoked(#selector(ASCollectionDelegate.scrollViewWillBeginDecelerating(_:))).map { _ in } 116 | return ControlEvent(events: source) 117 | } 118 | 119 | /// Reactive wrapper for delegate method `scrollViewDidEndDecelerating` 120 | public var didEndDecelerating: ControlEvent { 121 | let source = delegate.methodInvoked(#selector(ASCollectionDelegate.scrollViewDidEndDecelerating(_:))).map { _ in } 122 | return ControlEvent(events: source) 123 | } 124 | 125 | /// Reactive wrapper for delegate method `scrollViewWillBeginDragging` 126 | public var willBeginDragging: ControlEvent { 127 | let source = delegate.methodInvoked(#selector(ASCollectionDelegate.scrollViewWillBeginDragging(_:))).map { _ in } 128 | return ControlEvent(events: source) 129 | } 130 | 131 | /// Reactive wrapper for delegate method `scrollViewWillEndDragging(_:withVelocity:targetContentOffset:)` 132 | public var willEndDragging: ControlEvent.WillEndDraggingEvent> { 133 | let source = delegate.methodInvoked(#selector(ASCollectionDelegate.scrollViewWillEndDragging(_:withVelocity:targetContentOffset:))) 134 | .map { value -> Reactive.WillEndDraggingEvent in 135 | let velocity = try castOrThrow(CGPoint.self, value[1]) 136 | let targetContentOffsetValue = try castOrThrow(NSValue.self, value[2]) 137 | 138 | guard let rawPointer = targetContentOffsetValue.pointerValue else { throw RxCocoaError.unknown } 139 | let typedPointer = rawPointer.bindMemory(to: CGPoint.self, capacity: MemoryLayout.size) 140 | 141 | return (velocity, typedPointer) 142 | } 143 | return ControlEvent(events: source) 144 | } 145 | 146 | /// Reactive wrapper for delegate method `scrollViewDidEndDragging(_:willDecelerate:)` 147 | public var didEndDragging: ControlEvent { 148 | let source = delegate.methodInvoked(#selector(ASCollectionDelegate.scrollViewDidEndDragging(_:willDecelerate:))).map { value -> Bool in 149 | return try castOrThrow(Bool.self, value[1]) 150 | } 151 | return ControlEvent(events: source) 152 | } 153 | 154 | /// Reactive wrapper for `delegate` message `collectionNode(_:didSelectItemAtIndexPath:)`. 155 | public var itemSelected: ControlEvent { 156 | let source = delegate.methodInvoked(#selector(ASCollectionDelegate.collectionNode(_:didSelectItemAt:))) 157 | .map { a in 158 | return try castOrThrow(IndexPath.self, a[1]) 159 | } 160 | 161 | return ControlEvent(events: source) 162 | } 163 | 164 | /// Reactive wrapper for `delegate` message `collectionNode(_:didSelectItemAtIndexPath:)`. 165 | public var itemDeselected: ControlEvent { 166 | let source = delegate.methodInvoked(#selector(ASCollectionDelegate.collectionNode(_:didDeselectItemAt:))) 167 | .map { a in 168 | return try castOrThrow(IndexPath.self, a[1]) 169 | } 170 | 171 | return ControlEvent(events: source) 172 | } 173 | 174 | /// Reactive wrapper for `delegate` message `ASCollectionDelegate(_:didHighlightItemAt:)`. 175 | public var itemHighlighted: ControlEvent { 176 | let source = delegate.methodInvoked(#selector(ASCollectionDelegate.collectionNode(_:didHighlightItemAt:))) 177 | .map { a in 178 | return try castOrThrow(IndexPath.self, a[1]) 179 | } 180 | 181 | return ControlEvent(events: source) 182 | } 183 | 184 | /// Reactive wrapper for `delegate` message `collectionNode(_:didUnhighlightItemAt:)`. 185 | public var itemUnhighlighted: ControlEvent { 186 | let source = delegate.methodInvoked(#selector(ASCollectionDelegate.collectionNode(_:didUnhighlightItemAt:))) 187 | .map { a in 188 | return try castOrThrow(IndexPath.self, a[1]) 189 | } 190 | 191 | return ControlEvent(events: source) 192 | } 193 | 194 | /// Reactive wrapper for `delegate` message `collectionNode:willDisplayItemWith:`. 195 | public var willDisplayItem: ControlEvent { 196 | let source: Observable = self.delegate.methodInvoked(#selector(ASCollectionDelegate.collectionNode(_:willDisplayItemWith:))) 197 | .map { a in 198 | return (try castOrThrow(ASCellNode.self, a[1])) 199 | } 200 | 201 | return ControlEvent(events: source) 202 | } 203 | 204 | /// Reactive wrapper for `delegate` message `collectionNode:willDisplaySupplementaryElementWith:`. 205 | public var willDisplaySupplementaryElement: ControlEvent { 206 | let source: Observable = self.delegate.methodInvoked(#selector(ASCollectionDelegate.collectionNode(_:willDisplaySupplementaryElementWith:))) 207 | .map { a in 208 | return (try castOrThrow(ASCellNode.self, a[1])) 209 | } 210 | 211 | return ControlEvent(events: source) 212 | } 213 | 214 | /// Reactive wrapper for `delegate` message `collectionView:didEndDisplaying:forItemAt:`. 215 | public var didEndDisplayingItem: ControlEvent { 216 | let source: Observable = self.delegate.methodInvoked(#selector(ASCollectionDelegate.collectionNode(_:didEndDisplayingItemWith:))) 217 | .map { a in 218 | return (try castOrThrow(ASCellNode.self, a[1])) 219 | } 220 | 221 | return ControlEvent(events: source) 222 | } 223 | 224 | /// Reactive wrapper for `delegate` message `collectionNode(_:didEndDisplayingSupplementaryElementWith:)`. 225 | public var didEndDisplayingSupplementaryElement: ControlEvent { 226 | let source: Observable = self.delegate.methodInvoked(#selector(ASCollectionDelegate.collectionNode(_:didEndDisplayingSupplementaryElementWith:))) 227 | .map { a in 228 | return (try castOrThrow(ASCellNode.self, a[1])) 229 | } 230 | 231 | return ControlEvent(events: source) 232 | } 233 | 234 | /// Reactive wrapper for `delegate` message `collectionNode(_:willBeginBatchFetchWith:)` 235 | public var willBeginBatchFetch: ControlEvent { 236 | let source: Observable = self.delegate.methodInvoked(#selector(ASCollectionDelegate.collectionNode(_:willBeginBatchFetchWith:))) 237 | .map { a in 238 | return try castOrThrow(ASBatchContext.self, a[1]) 239 | } 240 | 241 | return ControlEvent(events: source) 242 | } 243 | 244 | /// Reactive wrapper for `delegate` message `collectionNode(_:didSelectItemAtIndexPath:)`. 245 | /// 246 | /// It can be only used when one of the `rx.itemsWith*` methods is used to bind observable sequence, 247 | /// or any other data source conforming to `SectionedViewDataSourceType` protocol. 248 | /// 249 | /// ``` 250 | /// collectionNode.rx.modelSelected(MyModel.self) 251 | /// .map { ... 252 | /// ``` 253 | public func modelSelected(_ modelType: T.Type) -> ControlEvent { 254 | let source: Observable = itemSelected.flatMap { [weak node = self.base as ASCollectionNode] indexPath -> Observable in 255 | guard let node = node else { 256 | return Observable.empty() 257 | } 258 | 259 | return Observable.just(try node.rx.model(at: indexPath)) 260 | } 261 | 262 | return ControlEvent(events: source) 263 | } 264 | 265 | /// Reactive wrapper for `delegate` message `collectionNode(_:didSelectItemAtIndexPath:)`. 266 | /// 267 | /// It can be only used when one of the `rx.itemsWith*` methods is used to bind observable sequence, 268 | /// or any other data source conforming to `SectionedViewDataSourceType` protocol. 269 | /// 270 | /// ``` 271 | /// collectionNode.rx.modelDeselected(MyModel.self) 272 | /// .map { ... 273 | /// ``` 274 | public func modelDeselected(_ modelType: T.Type) -> ControlEvent { 275 | let source: Observable = itemDeselected.flatMap { [weak node = self.base as ASCollectionNode] indexPath -> Observable in 276 | guard let node = node else { 277 | return Observable.empty() 278 | } 279 | 280 | return Observable.just(try node.rx.model(at: indexPath)) 281 | } 282 | 283 | return ControlEvent(events: source) 284 | } 285 | 286 | /// Synchronous helper method for retrieving a model at indexPath through a reactive data source 287 | public func model(at indexPath: IndexPath) throws -> T { 288 | let dataSource: SectionedViewDataSourceType = castOrFatalError(self.dataSource.forwardToDelegate(), message: "This method only works in case one of the `rx.itemsWith*` methods was used.") 289 | 290 | let element = try dataSource.model(at: indexPath) 291 | 292 | return try castOrThrow(T.self, element) 293 | } 294 | } 295 | 296 | #endif 297 | -------------------------------------------------------------------------------- /Sources/RxDataSources-Texture/ASTableNode/ASTableNode+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASTableNode+Rx.swift 3 | // RxDataSources-Texture 4 | // 5 | // Created by Kanghoon on 19/02/2019. 6 | // 7 | 8 | #if os(iOS) || os(tvOS) 9 | 10 | import Foundation 11 | import AsyncDisplayKit 12 | import RxSwift 13 | import RxCocoa 14 | 15 | #if swift(>=4.2) 16 | public typealias UITableViewCellEditingStyle = UITableViewCell.EditingStyle 17 | #endif 18 | 19 | // Items 20 | 21 | extension Reactive where Base: ASTableNode { 22 | 23 | /** 24 | Binds sequences of elements to table node rows using a custom reactive data used to perform the transformation. 25 | This method will retain the data source for as long as the subscription isn't disposed (result `Disposable` 26 | being disposed). 27 | In case `source` observable sequence terminates successfully, the data source will present latest element 28 | until the subscription isn't disposed. 29 | 30 | - parameter dataSource: Data source used to transform elements to cell nodes. 31 | - parameter source: Observable sequence of items. 32 | - returns: Disposable object that can be used to unbind. 33 | */ 34 | public func items< 35 | DataSource: RxASTableDataSourceType & ASTableDataSource, 36 | O: ObservableType> 37 | (dataSource: DataSource) 38 | -> (_ source: O) 39 | -> Disposable 40 | where DataSource.Element == O.Element { 41 | return { source in 42 | // Strong reference is needed because data source is in use until result subscription is disposed 43 | return source.subscribeProxyDataSource(ofObject: self.base, dataSource: dataSource as ASTableDataSource, retainDataSource: true) { [weak tableNode = self.base] (_: RxASTableDataSourceProxy, event) -> Void in 44 | guard let tableNode = tableNode else { 45 | return 46 | } 47 | dataSource.tableNode(tableNode, observedEvent: event) 48 | } 49 | } 50 | } 51 | 52 | } 53 | 54 | extension Reactive where Base: ASTableNode { 55 | 56 | /** 57 | Reactive wrapper for `delegate`. 58 | 59 | For more information take a look at `DelegateProxyType` protocol documentation. 60 | */ 61 | public var delegate: DelegateProxy { 62 | return RxASTableDelegateProxy.proxy(for: base) 63 | } 64 | 65 | /** 66 | Reactive wrapper for `dataSource`. 67 | 68 | For more information take a look at `DelegateProxyType` protocol documentation. 69 | */ 70 | public var dataSource: DelegateProxy { 71 | return RxASTableDataSourceProxy.proxy(for: base) 72 | } 73 | 74 | /** 75 | Installs data source as forwarding delegate on `rx.dataSource`. 76 | Data source won't be retained. 77 | 78 | It enables using normal delegate mechanism with reactive delegate mechanism. 79 | 80 | - parameter dataSource: Data source object. 81 | - returns: Disposable object that can be used to unbind the data source. 82 | */ 83 | public func setDataSource(_ dataSource: ASTableDataSource) -> Disposable { 84 | return ScheduledDisposable( 85 | scheduler: MainScheduler.instance, 86 | disposable: RxASTableDataSourceProxy.installForwardDelegate(dataSource, retainDelegate: false, onProxyForObject: self.base) 87 | ) 88 | } 89 | 90 | /** 91 | Installs delegate as forwarding delegate on `rx.delegate`. 92 | Data source won't be retained. 93 | 94 | It enables using normal delegate mechanism with reactive delegate mechanism. 95 | 96 | - parameter delegate: Delegate object 97 | - returns: Disposable object that can be used to unbind the delegate. 98 | */ 99 | public func setDelegate(_ delegate: ASTableDelegate) -> Disposable { 100 | return ScheduledDisposable( 101 | scheduler: MainScheduler.instance, 102 | disposable: RxASTableDelegateProxy.installForwardDelegate(delegate, retainDelegate: false, onProxyForObject: self.base) 103 | ) 104 | } 105 | 106 | /// Reactive wrapper for `contentOffset`. 107 | public var contentOffset: ControlProperty { 108 | let proxy = RxASTableDelegateProxy.proxy(for: base) 109 | 110 | let bindingObserver = Binder(self.base) { tableNode, contentOffset in 111 | tableNode.contentOffset = contentOffset 112 | } 113 | 114 | return ControlProperty(values: proxy.contentOffsetBehaviorSubject, valueSink: bindingObserver) 115 | } 116 | 117 | /// Reactive wrapper for delegate method `scrollViewDidScroll` 118 | public var didScroll: ControlEvent { 119 | let source = RxASTableDelegateProxy.proxy(for: base).contentOffsetPublishSubject 120 | return ControlEvent(events: source) 121 | } 122 | 123 | /// Reactive wrapper for delegate method `scrollViewWillBeginDecelerating` 124 | public var willBeginDecelerating: ControlEvent { 125 | let source = delegate.methodInvoked(#selector(ASTableDelegate.scrollViewWillBeginDecelerating(_:))).map { _ in } 126 | return ControlEvent(events: source) 127 | } 128 | 129 | /// Reactive wrapper for delegate method `scrollViewDidEndDecelerating` 130 | public var didEndDecelerating: ControlEvent { 131 | let source = delegate.methodInvoked(#selector(ASTableDelegate.scrollViewDidEndDecelerating(_:))).map { _ in } 132 | return ControlEvent(events: source) 133 | } 134 | 135 | /// Reactive wrapper for delegate method `scrollViewWillBeginDragging` 136 | public var willBeginDragging: ControlEvent { 137 | let source = delegate.methodInvoked(#selector(ASTableDelegate.scrollViewWillBeginDragging(_:))).map { _ in } 138 | return ControlEvent(events: source) 139 | } 140 | 141 | /// Reactive wrapper for delegate method `scrollViewWillEndDragging(_:withVelocity:targetContentOffset:)` 142 | public var willEndDragging: ControlEvent.WillEndDraggingEvent> { 143 | let source = delegate.methodInvoked(#selector(ASTableDelegate.scrollViewWillEndDragging(_:withVelocity:targetContentOffset:))) 144 | .map { value -> Reactive.WillEndDraggingEvent in 145 | let velocity = try castOrThrow(CGPoint.self, value[1]) 146 | let targetContentOffsetValue = try castOrThrow(NSValue.self, value[2]) 147 | 148 | guard let rawPointer = targetContentOffsetValue.pointerValue else { throw RxCocoaError.unknown } 149 | let typedPointer = rawPointer.bindMemory(to: CGPoint.self, capacity: MemoryLayout.size) 150 | 151 | return (velocity, typedPointer) 152 | } 153 | return ControlEvent(events: source) 154 | } 155 | 156 | /// Reactive wrapper for delegate method `scrollViewDidEndDragging(_:willDecelerate:)` 157 | public var didEndDragging: ControlEvent { 158 | let source = delegate.methodInvoked(#selector(ASTableDelegate.scrollViewDidEndDragging(_:willDecelerate:))).map { value -> Bool in 159 | return try castOrThrow(Bool.self, value[1]) 160 | } 161 | return ControlEvent(events: source) 162 | } 163 | 164 | /** 165 | Reactive wrapper for `delegate` message `tableNode:didSelectRowAtIndexPath:`. 166 | */ 167 | public var itemSelected: ControlEvent { 168 | let source = self.delegate.methodInvoked(#selector(ASTableDelegate.tableNode(_:didSelectRowAt:))) 169 | .map { a in 170 | return try castOrThrow(IndexPath.self, a[1]) 171 | } 172 | 173 | return ControlEvent(events: source) 174 | } 175 | 176 | /** 177 | Reactive wrapper for `delegate` message `tableNode:didDeselectRowAtIndexPath:`. 178 | */ 179 | public var itemDeselected: ControlEvent { 180 | let source = self.delegate.methodInvoked(#selector(ASTableDelegate.tableNode(_:didDeselectRowAt:))) 181 | .map { a in 182 | return try castOrThrow(IndexPath.self, a[1]) 183 | } 184 | 185 | return ControlEvent(events: source) 186 | } 187 | 188 | /** 189 | Reactive wrapper for `delegate` message `tableView:commitEditingStyle:forRowAtIndexPath:`. 190 | */ 191 | public var itemInserted: ControlEvent { 192 | let source = self.dataSource.methodInvoked(#selector(ASTableDataSource.tableView(_:commit:forRowAt:))) 193 | .filter { a in 194 | return UITableViewCellEditingStyle(rawValue: (try castOrThrow(NSNumber.self, a[1])).intValue) == .insert 195 | } 196 | .map { a in 197 | return (try castOrThrow(IndexPath.self, a[2])) 198 | } 199 | 200 | return ControlEvent(events: source) 201 | } 202 | 203 | /** 204 | Reactive wrapper for `delegate` message `tableView:commitEditingStyle:forRowAtIndexPath:`. 205 | */ 206 | public var itemDeleted: ControlEvent { 207 | let source = self.dataSource.methodInvoked(#selector(ASTableDataSource.tableView(_:commit:forRowAt:))) 208 | .filter { a in 209 | return UITableViewCellEditingStyle(rawValue: (try castOrThrow(NSNumber.self, a[1])).intValue) == .delete 210 | } 211 | .map { a in 212 | return try castOrThrow(IndexPath.self, a[2]) 213 | } 214 | 215 | return ControlEvent(events: source) 216 | } 217 | 218 | /** 219 | Reactive wrapper for `delegate` message `tableView:moveRowAtIndexPath:toIndexPath:`. 220 | */ 221 | public var itemMoved: ControlEvent { 222 | let source: Observable = self.dataSource.methodInvoked(#selector(ASTableDataSource.tableView(_:moveRowAt:to:))) 223 | .map { a in 224 | return (try castOrThrow(IndexPath.self, a[1]), try castOrThrow(IndexPath.self, a[2])) 225 | } 226 | 227 | return ControlEvent(events: source) 228 | } 229 | 230 | /** 231 | Reactive wrapper for `delegate` message `tableNode:willDisplayCell:forRowAtIndexPath:`. 232 | */ 233 | public var willDisplayCell: ControlEvent { 234 | let source: Observable = self.delegate.methodInvoked(#selector(ASTableDelegate.tableNode(_:willDisplayRowWith:))) 235 | .map { a in 236 | return try castOrThrow(ASCellNode.self, a[1]) 237 | } 238 | 239 | return ControlEvent(events: source) 240 | } 241 | 242 | /** 243 | Reactive wrapper for `delegate` message `tableNode:didEndDisplayingCell:forRowAtIndexPath:`. 244 | */ 245 | public var didEndDisplayingCell: ControlEvent { 246 | let source: Observable = self.delegate.methodInvoked(#selector(ASTableDelegate.tableNode(_:didEndDisplayingRowWith:))) 247 | .map { a in 248 | return try castOrThrow(ASCellNode.self, a[1]) 249 | } 250 | 251 | return ControlEvent(events: source) 252 | } 253 | 254 | /** 255 | Reactive wrapper for `delegate` message `tableNode:willBeginBatchFetchWith` 256 | */ 257 | public var willBeginBatchFetch: ControlEvent { 258 | let source: Observable = self.delegate.methodInvoked(#selector(ASTableDelegate.tableNode(_:willBeginBatchFetchWith:))) 259 | .map { a in 260 | return try castOrThrow(ASBatchContext.self, a[1]) 261 | } 262 | 263 | return ControlEvent(events: source) 264 | } 265 | 266 | /** 267 | Reactive wrapper for `delegate` message `tableNode:didSelectRowAtIndexPath:`. 268 | 269 | It can be only used when one of the `rx.itemsWith*` methods is used to bind observable sequence, 270 | or any other data source conforming to `SectionedViewDataSourceType` protocol. 271 | 272 | ``` 273 | tableNode.rx.modelSelected(MyModel.self) 274 | .map { ... 275 | ``` 276 | */ 277 | public func modelSelected(_ modelType: T.Type) -> ControlEvent { 278 | let source: Observable = self.itemSelected.flatMap { [weak view = self.base as ASTableNode] indexPath -> Observable in 279 | guard let view = view else { 280 | return Observable.empty() 281 | } 282 | 283 | return Observable.just(try view.rx.model(at: indexPath)) 284 | } 285 | 286 | return ControlEvent(events: source) 287 | } 288 | 289 | /** 290 | Reactive wrapper for `delegate` message `tableNode:didDeselectRowAtIndexPath:`. 291 | 292 | It can be only used when one of the `rx.itemsWith*` methods is used to bind observable sequence, 293 | or any other data source conforming to `SectionedViewDataSourceType` protocol. 294 | 295 | ``` 296 | tableNode.rx.modelDeselected(MyModel.self) 297 | .map { ... 298 | ``` 299 | */ 300 | public func modelDeselected(_ modelType: T.Type) -> ControlEvent { 301 | let source: Observable = self.itemDeselected.flatMap { [weak view = self.base as ASTableNode] indexPath -> Observable in 302 | guard let view = view else { 303 | return Observable.empty() 304 | } 305 | 306 | return Observable.just(try view.rx.model(at: indexPath)) 307 | } 308 | 309 | return ControlEvent(events: source) 310 | } 311 | 312 | /** 313 | Reactive wrapper for `delegate` message `tableNode:commitEditingStyle:forRowAtIndexPath:`. 314 | 315 | It can be only used when one of the `rx.itemsWith*` methods is used to bind observable sequence, 316 | or any other data source conforming to `SectionedViewDataSourceType` protocol. 317 | 318 | ``` 319 | tableNode.rx.modelDeleted(MyModel.self) 320 | .map { ... 321 | ``` 322 | */ 323 | public func modelDeleted(_ modelType: T.Type) -> ControlEvent { 324 | let source: Observable = self.itemDeleted.flatMap { [weak view = self.base as ASTableNode] indexPath -> Observable in 325 | guard let view = view else { 326 | return Observable.empty() 327 | } 328 | 329 | return Observable.just(try view.rx.model(at: indexPath)) 330 | } 331 | 332 | return ControlEvent(events: source) 333 | } 334 | 335 | /** 336 | Synchronous helper method for retrieving a model at indexPath through a reactive data source. 337 | */ 338 | public func model(at indexPath: IndexPath) throws -> T { 339 | let dataSource: SectionedViewDataSourceType = castOrFatalError(self.dataSource.forwardToDelegate(), message: "This method only works in case one of the `rx.items*` methods was used.") 340 | 341 | let element = try dataSource.model(at: indexPath) 342 | 343 | return castOrFatalError(element) 344 | } 345 | } 346 | 347 | #endif 348 | -------------------------------------------------------------------------------- /Example/GithubRepos.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 51; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | BA49E5D0243310F600173DEB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA49E5CF243310F600173DEB /* AppDelegate.swift */; }; 11 | BA49E5D9243310F700173DEB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BA49E5D8243310F700173DEB /* Assets.xcassets */; }; 12 | BA49E5DC243310F700173DEB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BA49E5DA243310F700173DEB /* LaunchScreen.storyboard */; }; 13 | BA49E5FB2433158D00173DEB /* MainSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA49E5F32433158D00173DEB /* MainSection.swift */; }; 14 | BA49E5FC2433158D00173DEB /* RepoCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA49E5F42433158D00173DEB /* RepoCollectionViewController.swift */; }; 15 | BA49E5FD2433158D00173DEB /* Repo.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA49E5F52433158D00173DEB /* Repo.swift */; }; 16 | BA49E5FE2433158D00173DEB /* CategoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA49E5F62433158D00173DEB /* CategoryViewController.swift */; }; 17 | BA49E5FF2433158D00173DEB /* GithubService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA49E5F72433158D00173DEB /* GithubService.swift */; }; 18 | BA49E6002433158D00173DEB /* RepoTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA49E5F82433158D00173DEB /* RepoTableViewController.swift */; }; 19 | BA49E6012433158D00173DEB /* RepoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA49E5F92433158D00173DEB /* RepoViewModel.swift */; }; 20 | BA49E6022433158D00173DEB /* RepoCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA49E5FA2433158D00173DEB /* RepoCellNode.swift */; }; 21 | FE51C2432BFAFAA7BCDBBCF3 /* Pods_GithubRepos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BDB2D3249C3CFA839DC09BFD /* Pods_GithubRepos.framework */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXFileReference section */ 25 | 0D21F617731032C2765DE48E /* Pods-GithubRepos.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GithubRepos.release.xcconfig"; path = "Target Support Files/Pods-GithubRepos/Pods-GithubRepos.release.xcconfig"; sourceTree = ""; }; 26 | 89E31213DA9072326427B2AC /* Pods-GithubRepos.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GithubRepos.debug.xcconfig"; path = "Target Support Files/Pods-GithubRepos/Pods-GithubRepos.debug.xcconfig"; sourceTree = ""; }; 27 | BA49E5CC243310F600173DEB /* GithubRepos.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GithubRepos.app; sourceTree = BUILT_PRODUCTS_DIR; }; 28 | BA49E5CF243310F600173DEB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 29 | BA49E5D8243310F700173DEB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 30 | BA49E5DB243310F700173DEB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 31 | BA49E5DD243310F700173DEB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 32 | BA49E5F32433158D00173DEB /* MainSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainSection.swift; sourceTree = ""; }; 33 | BA49E5F42433158D00173DEB /* RepoCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepoCollectionViewController.swift; sourceTree = ""; }; 34 | BA49E5F52433158D00173DEB /* Repo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Repo.swift; sourceTree = ""; }; 35 | BA49E5F62433158D00173DEB /* CategoryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CategoryViewController.swift; sourceTree = ""; }; 36 | BA49E5F72433158D00173DEB /* GithubService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GithubService.swift; sourceTree = ""; }; 37 | BA49E5F82433158D00173DEB /* RepoTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepoTableViewController.swift; sourceTree = ""; }; 38 | BA49E5F92433158D00173DEB /* RepoViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepoViewModel.swift; sourceTree = ""; }; 39 | BA49E5FA2433158D00173DEB /* RepoCellNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepoCellNode.swift; sourceTree = ""; }; 40 | BDB2D3249C3CFA839DC09BFD /* Pods_GithubRepos.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GithubRepos.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | /* End PBXFileReference section */ 42 | 43 | /* Begin PBXFrameworksBuildPhase section */ 44 | BA49E5C9243310F600173DEB /* Frameworks */ = { 45 | isa = PBXFrameworksBuildPhase; 46 | buildActionMask = 2147483647; 47 | files = ( 48 | FE51C2432BFAFAA7BCDBBCF3 /* Pods_GithubRepos.framework in Frameworks */, 49 | ); 50 | runOnlyForDeploymentPostprocessing = 0; 51 | }; 52 | /* End PBXFrameworksBuildPhase section */ 53 | 54 | /* Begin PBXGroup section */ 55 | 651354D5072A9140EE4FCC8F /* Pods */ = { 56 | isa = PBXGroup; 57 | children = ( 58 | 89E31213DA9072326427B2AC /* Pods-GithubRepos.debug.xcconfig */, 59 | 0D21F617731032C2765DE48E /* Pods-GithubRepos.release.xcconfig */, 60 | ); 61 | path = Pods; 62 | sourceTree = ""; 63 | }; 64 | BA49E5C3243310F600173DEB = { 65 | isa = PBXGroup; 66 | children = ( 67 | BA49E5CE243310F600173DEB /* GithubRepos */, 68 | BA49E5CD243310F600173DEB /* Products */, 69 | 651354D5072A9140EE4FCC8F /* Pods */, 70 | F5A318878258C967C374CA2D /* Frameworks */, 71 | ); 72 | sourceTree = ""; 73 | }; 74 | BA49E5CD243310F600173DEB /* Products */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | BA49E5CC243310F600173DEB /* GithubRepos.app */, 78 | ); 79 | name = Products; 80 | sourceTree = ""; 81 | }; 82 | BA49E5CE243310F600173DEB /* GithubRepos */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | BA49E5F62433158D00173DEB /* CategoryViewController.swift */, 86 | BA49E5F72433158D00173DEB /* GithubService.swift */, 87 | BA49E5F32433158D00173DEB /* MainSection.swift */, 88 | BA49E5F52433158D00173DEB /* Repo.swift */, 89 | BA49E5FA2433158D00173DEB /* RepoCellNode.swift */, 90 | BA49E5F42433158D00173DEB /* RepoCollectionViewController.swift */, 91 | BA49E5F82433158D00173DEB /* RepoTableViewController.swift */, 92 | BA49E5F92433158D00173DEB /* RepoViewModel.swift */, 93 | BA49E5CF243310F600173DEB /* AppDelegate.swift */, 94 | BA49E5D8243310F700173DEB /* Assets.xcassets */, 95 | BA49E5DA243310F700173DEB /* LaunchScreen.storyboard */, 96 | BA49E5DD243310F700173DEB /* Info.plist */, 97 | ); 98 | path = GithubRepos; 99 | sourceTree = ""; 100 | }; 101 | F5A318878258C967C374CA2D /* Frameworks */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | BDB2D3249C3CFA839DC09BFD /* Pods_GithubRepos.framework */, 105 | ); 106 | name = Frameworks; 107 | sourceTree = ""; 108 | }; 109 | /* End PBXGroup section */ 110 | 111 | /* Begin PBXNativeTarget section */ 112 | BA49E5CB243310F600173DEB /* GithubRepos */ = { 113 | isa = PBXNativeTarget; 114 | buildConfigurationList = BA49E5E0243310F700173DEB /* Build configuration list for PBXNativeTarget "GithubRepos" */; 115 | buildPhases = ( 116 | 82D0BE1985141EE1B1F8C31C /* [CP] Check Pods Manifest.lock */, 117 | BA49E5C8243310F600173DEB /* Sources */, 118 | BA49E5C9243310F600173DEB /* Frameworks */, 119 | BA49E5CA243310F600173DEB /* Resources */, 120 | 8A53AC4E57C28518119D0262 /* [CP] Embed Pods Frameworks */, 121 | ); 122 | buildRules = ( 123 | ); 124 | dependencies = ( 125 | ); 126 | name = GithubRepos; 127 | productName = GithubUsers; 128 | productReference = BA49E5CC243310F600173DEB /* GithubRepos.app */; 129 | productType = "com.apple.product-type.application"; 130 | }; 131 | /* End PBXNativeTarget section */ 132 | 133 | /* Begin PBXProject section */ 134 | BA49E5C4243310F600173DEB /* Project object */ = { 135 | isa = PBXProject; 136 | attributes = { 137 | LastSwiftUpdateCheck = 1130; 138 | LastUpgradeCheck = 1130; 139 | ORGANIZATIONNAME = kanghoon; 140 | TargetAttributes = { 141 | BA49E5CB243310F600173DEB = { 142 | CreatedOnToolsVersion = 11.3.1; 143 | }; 144 | }; 145 | }; 146 | buildConfigurationList = BA49E5C7243310F600173DEB /* Build configuration list for PBXProject "GithubRepos" */; 147 | compatibilityVersion = "Xcode 9.3"; 148 | developmentRegion = en; 149 | hasScannedForEncodings = 0; 150 | knownRegions = ( 151 | en, 152 | Base, 153 | ); 154 | mainGroup = BA49E5C3243310F600173DEB; 155 | productRefGroup = BA49E5CD243310F600173DEB /* Products */; 156 | projectDirPath = ""; 157 | projectRoot = ""; 158 | targets = ( 159 | BA49E5CB243310F600173DEB /* GithubRepos */, 160 | ); 161 | }; 162 | /* End PBXProject section */ 163 | 164 | /* Begin PBXResourcesBuildPhase section */ 165 | BA49E5CA243310F600173DEB /* Resources */ = { 166 | isa = PBXResourcesBuildPhase; 167 | buildActionMask = 2147483647; 168 | files = ( 169 | BA49E5DC243310F700173DEB /* LaunchScreen.storyboard in Resources */, 170 | BA49E5D9243310F700173DEB /* Assets.xcassets in Resources */, 171 | ); 172 | runOnlyForDeploymentPostprocessing = 0; 173 | }; 174 | /* End PBXResourcesBuildPhase section */ 175 | 176 | /* Begin PBXShellScriptBuildPhase section */ 177 | 82D0BE1985141EE1B1F8C31C /* [CP] Check Pods Manifest.lock */ = { 178 | isa = PBXShellScriptBuildPhase; 179 | buildActionMask = 2147483647; 180 | files = ( 181 | ); 182 | inputFileListPaths = ( 183 | ); 184 | inputPaths = ( 185 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 186 | "${PODS_ROOT}/Manifest.lock", 187 | ); 188 | name = "[CP] Check Pods Manifest.lock"; 189 | outputFileListPaths = ( 190 | ); 191 | outputPaths = ( 192 | "$(DERIVED_FILE_DIR)/Pods-GithubRepos-checkManifestLockResult.txt", 193 | ); 194 | runOnlyForDeploymentPostprocessing = 0; 195 | shellPath = /bin/sh; 196 | 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"; 197 | showEnvVarsInLog = 0; 198 | }; 199 | 8A53AC4E57C28518119D0262 /* [CP] Embed Pods Frameworks */ = { 200 | isa = PBXShellScriptBuildPhase; 201 | buildActionMask = 2147483647; 202 | files = ( 203 | ); 204 | inputFileListPaths = ( 205 | "${PODS_ROOT}/Target Support Files/Pods-GithubRepos/Pods-GithubRepos-frameworks-${CONFIGURATION}-input-files.xcfilelist", 206 | ); 207 | name = "[CP] Embed Pods Frameworks"; 208 | outputFileListPaths = ( 209 | "${PODS_ROOT}/Target Support Files/Pods-GithubRepos/Pods-GithubRepos-frameworks-${CONFIGURATION}-output-files.xcfilelist", 210 | ); 211 | runOnlyForDeploymentPostprocessing = 0; 212 | shellPath = /bin/sh; 213 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-GithubRepos/Pods-GithubRepos-frameworks.sh\"\n"; 214 | showEnvVarsInLog = 0; 215 | }; 216 | /* End PBXShellScriptBuildPhase section */ 217 | 218 | /* Begin PBXSourcesBuildPhase section */ 219 | BA49E5C8243310F600173DEB /* Sources */ = { 220 | isa = PBXSourcesBuildPhase; 221 | buildActionMask = 2147483647; 222 | files = ( 223 | BA49E6012433158D00173DEB /* RepoViewModel.swift in Sources */, 224 | BA49E6002433158D00173DEB /* RepoTableViewController.swift in Sources */, 225 | BA49E5FE2433158D00173DEB /* CategoryViewController.swift in Sources */, 226 | BA49E5FC2433158D00173DEB /* RepoCollectionViewController.swift in Sources */, 227 | BA49E5FF2433158D00173DEB /* GithubService.swift in Sources */, 228 | BA49E6022433158D00173DEB /* RepoCellNode.swift in Sources */, 229 | BA49E5FB2433158D00173DEB /* MainSection.swift in Sources */, 230 | BA49E5D0243310F600173DEB /* AppDelegate.swift in Sources */, 231 | BA49E5FD2433158D00173DEB /* Repo.swift in Sources */, 232 | ); 233 | runOnlyForDeploymentPostprocessing = 0; 234 | }; 235 | /* End PBXSourcesBuildPhase section */ 236 | 237 | /* Begin PBXVariantGroup section */ 238 | BA49E5DA243310F700173DEB /* LaunchScreen.storyboard */ = { 239 | isa = PBXVariantGroup; 240 | children = ( 241 | BA49E5DB243310F700173DEB /* Base */, 242 | ); 243 | name = LaunchScreen.storyboard; 244 | sourceTree = ""; 245 | }; 246 | /* End PBXVariantGroup section */ 247 | 248 | /* Begin XCBuildConfiguration section */ 249 | BA49E5DE243310F700173DEB /* Debug */ = { 250 | isa = XCBuildConfiguration; 251 | buildSettings = { 252 | ALWAYS_SEARCH_USER_PATHS = NO; 253 | CLANG_ANALYZER_NONNULL = YES; 254 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 255 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 256 | CLANG_CXX_LIBRARY = "libc++"; 257 | CLANG_ENABLE_MODULES = YES; 258 | CLANG_ENABLE_OBJC_ARC = YES; 259 | CLANG_ENABLE_OBJC_WEAK = YES; 260 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 261 | CLANG_WARN_BOOL_CONVERSION = YES; 262 | CLANG_WARN_COMMA = YES; 263 | CLANG_WARN_CONSTANT_CONVERSION = YES; 264 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 265 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 266 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 267 | CLANG_WARN_EMPTY_BODY = YES; 268 | CLANG_WARN_ENUM_CONVERSION = YES; 269 | CLANG_WARN_INFINITE_RECURSION = YES; 270 | CLANG_WARN_INT_CONVERSION = YES; 271 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 272 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 273 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 274 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 275 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 276 | CLANG_WARN_STRICT_PROTOTYPES = YES; 277 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 278 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 279 | CLANG_WARN_UNREACHABLE_CODE = YES; 280 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 281 | COPY_PHASE_STRIP = NO; 282 | DEBUG_INFORMATION_FORMAT = dwarf; 283 | ENABLE_STRICT_OBJC_MSGSEND = YES; 284 | ENABLE_TESTABILITY = YES; 285 | GCC_C_LANGUAGE_STANDARD = gnu11; 286 | GCC_DYNAMIC_NO_PIC = NO; 287 | GCC_NO_COMMON_BLOCKS = YES; 288 | GCC_OPTIMIZATION_LEVEL = 0; 289 | GCC_PREPROCESSOR_DEFINITIONS = ( 290 | "DEBUG=1", 291 | "$(inherited)", 292 | ); 293 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 294 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 295 | GCC_WARN_UNDECLARED_SELECTOR = YES; 296 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 297 | GCC_WARN_UNUSED_FUNCTION = YES; 298 | GCC_WARN_UNUSED_VARIABLE = YES; 299 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 300 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 301 | MTL_FAST_MATH = YES; 302 | ONLY_ACTIVE_ARCH = YES; 303 | SDKROOT = iphoneos; 304 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 305 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 306 | }; 307 | name = Debug; 308 | }; 309 | BA49E5DF243310F700173DEB /* Release */ = { 310 | isa = XCBuildConfiguration; 311 | buildSettings = { 312 | ALWAYS_SEARCH_USER_PATHS = NO; 313 | CLANG_ANALYZER_NONNULL = YES; 314 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 315 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 316 | CLANG_CXX_LIBRARY = "libc++"; 317 | CLANG_ENABLE_MODULES = YES; 318 | CLANG_ENABLE_OBJC_ARC = YES; 319 | CLANG_ENABLE_OBJC_WEAK = YES; 320 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 321 | CLANG_WARN_BOOL_CONVERSION = YES; 322 | CLANG_WARN_COMMA = YES; 323 | CLANG_WARN_CONSTANT_CONVERSION = YES; 324 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 325 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 326 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 327 | CLANG_WARN_EMPTY_BODY = YES; 328 | CLANG_WARN_ENUM_CONVERSION = YES; 329 | CLANG_WARN_INFINITE_RECURSION = YES; 330 | CLANG_WARN_INT_CONVERSION = YES; 331 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 332 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 333 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 334 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 335 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 336 | CLANG_WARN_STRICT_PROTOTYPES = YES; 337 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 338 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 339 | CLANG_WARN_UNREACHABLE_CODE = YES; 340 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 341 | COPY_PHASE_STRIP = NO; 342 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 343 | ENABLE_NS_ASSERTIONS = NO; 344 | ENABLE_STRICT_OBJC_MSGSEND = YES; 345 | GCC_C_LANGUAGE_STANDARD = gnu11; 346 | GCC_NO_COMMON_BLOCKS = YES; 347 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 348 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 349 | GCC_WARN_UNDECLARED_SELECTOR = YES; 350 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 351 | GCC_WARN_UNUSED_FUNCTION = YES; 352 | GCC_WARN_UNUSED_VARIABLE = YES; 353 | IPHONEOS_DEPLOYMENT_TARGET = 13.2; 354 | MTL_ENABLE_DEBUG_INFO = NO; 355 | MTL_FAST_MATH = YES; 356 | SDKROOT = iphoneos; 357 | SWIFT_COMPILATION_MODE = wholemodule; 358 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 359 | VALIDATE_PRODUCT = YES; 360 | }; 361 | name = Release; 362 | }; 363 | BA49E5E1243310F700173DEB /* Debug */ = { 364 | isa = XCBuildConfiguration; 365 | baseConfigurationReference = 89E31213DA9072326427B2AC /* Pods-GithubRepos.debug.xcconfig */; 366 | buildSettings = { 367 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 368 | CODE_SIGN_STYLE = Automatic; 369 | INFOPLIST_FILE = "$(SRCROOT)/GithubRepos/Info.plist"; 370 | LD_RUNPATH_SEARCH_PATHS = ( 371 | "$(inherited)", 372 | "@executable_path/Frameworks", 373 | ); 374 | PRODUCT_BUNDLE_IDENTIFIER = com.kanghoon.GithubUsers; 375 | PRODUCT_NAME = "$(TARGET_NAME)"; 376 | SWIFT_VERSION = 5.0; 377 | TARGETED_DEVICE_FAMILY = "1,2"; 378 | }; 379 | name = Debug; 380 | }; 381 | BA49E5E2243310F700173DEB /* Release */ = { 382 | isa = XCBuildConfiguration; 383 | baseConfigurationReference = 0D21F617731032C2765DE48E /* Pods-GithubRepos.release.xcconfig */; 384 | buildSettings = { 385 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 386 | CODE_SIGN_STYLE = Automatic; 387 | INFOPLIST_FILE = "$(SRCROOT)/GithubRepos/Info.plist"; 388 | LD_RUNPATH_SEARCH_PATHS = ( 389 | "$(inherited)", 390 | "@executable_path/Frameworks", 391 | ); 392 | PRODUCT_BUNDLE_IDENTIFIER = com.kanghoon.GithubUsers; 393 | PRODUCT_NAME = "$(TARGET_NAME)"; 394 | SWIFT_VERSION = 5.0; 395 | TARGETED_DEVICE_FAMILY = "1,2"; 396 | }; 397 | name = Release; 398 | }; 399 | /* End XCBuildConfiguration section */ 400 | 401 | /* Begin XCConfigurationList section */ 402 | BA49E5C7243310F600173DEB /* Build configuration list for PBXProject "GithubRepos" */ = { 403 | isa = XCConfigurationList; 404 | buildConfigurations = ( 405 | BA49E5DE243310F700173DEB /* Debug */, 406 | BA49E5DF243310F700173DEB /* Release */, 407 | ); 408 | defaultConfigurationIsVisible = 0; 409 | defaultConfigurationName = Release; 410 | }; 411 | BA49E5E0243310F700173DEB /* Build configuration list for PBXNativeTarget "GithubRepos" */ = { 412 | isa = XCConfigurationList; 413 | buildConfigurations = ( 414 | BA49E5E1243310F700173DEB /* Debug */, 415 | BA49E5E2243310F700173DEB /* Release */, 416 | ); 417 | defaultConfigurationIsVisible = 0; 418 | defaultConfigurationName = Release; 419 | }; 420 | /* End XCConfigurationList section */ 421 | }; 422 | rootObject = BA49E5C4243310F600173DEB /* Project object */; 423 | } 424 | --------------------------------------------------------------------------------