├── .gitignore ├── .travis.yml ├── Examples └── ArticleFeed │ ├── ArticleFeed.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata │ ├── ArticleFeed.xcworkspace │ └── contents.xcworkspacedata │ ├── ArticleFeed │ ├── Resources │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ └── Base.lproj │ │ │ └── LaunchScreen.storyboard │ ├── Sources │ │ ├── Entry │ │ │ ├── AppDelegate.swift │ │ │ └── AppDependency.swift │ │ ├── Models │ │ │ ├── Article.swift │ │ │ ├── Comment.swift │ │ │ └── User.swift │ │ ├── Rx │ │ │ ├── UICollectionViewFlexLayout+Rx.swift │ │ │ └── UIView+Rx.swift │ │ ├── Sections │ │ │ ├── ArticleSectionDelegate.swift │ │ │ ├── ArticleSectionReactor.swift │ │ │ ├── ArticleViewSetion.swift │ │ │ └── FeedViewSection.swift │ │ ├── Services │ │ │ └── ArticleService.swift │ │ ├── Utils │ │ │ ├── BorderedLayer.swift │ │ │ ├── Snap.swift │ │ │ └── String+BoundingRect.swift │ │ ├── ViewControllers │ │ │ ├── ArticleListViewController.swift │ │ │ ├── ArticleListViewReactor.swift │ │ │ ├── ArticleViewController.swift │ │ │ └── ArticleViewReactor.swift │ │ └── Views │ │ │ ├── ArticleCardAuthorCell.swift │ │ │ ├── ArticleCardAuthorCellReactor.swift │ │ │ ├── ArticleCardCommentCell.swift │ │ │ ├── ArticleCardCommentCellReactor.swift │ │ │ ├── ArticleCardReactionCell.swift │ │ │ ├── ArticleCardReactionCellReactor.swift │ │ │ ├── ArticleCardTextCell.swift │ │ │ ├── ArticleCardTextCellReactor.swift │ │ │ ├── BaseArticleCardSectionItemCell.swift │ │ │ └── CollectionBorderedBackgroundView.swift │ └── Supporting Files │ │ └── Info.plist │ ├── Podfile │ └── README.md ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── SectionReactor.podspec ├── Sources └── SectionReactor │ ├── Empty.swift │ ├── SectionDelegateType.swift │ └── SectionReactor.swift ├── Tests └── SectionReactorTests │ ├── Fixture.swift │ └── SectionReactorTests.swift └── codecov.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | **/xcuserdata 6 | **/xcshareddata 7 | Pods/ 8 | Carthage/ 9 | Examples/**/Podfile.lock 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | osx_image: xcode10.2 2 | language: objective-c 3 | sudo: required 4 | env: 5 | global: 6 | - PROJECT="SectionReactor.xcodeproj" 7 | - SCHEME="SectionReactor-Package" 8 | - IOS_SDK="iphonesimulator12.2" 9 | - TVOS_SDK="appletvsimulator12.2" 10 | matrix: 11 | - SDK="$IOS_SDK" TEST=1 DESTINATION="platform=iOS Simulator,name=iPhone 7,OS=12.2" 12 | - SDK="$TVOS_SDK" TEST=1 DESTINATION="OS=12.2,name=Apple TV 4K" 13 | 14 | install: 15 | - eval "$(curl -sL https://gist.githubusercontent.com/kylef/5c0475ff02b7c7671d2a/raw/9f442512a46d7a2af7b850d65a7e9bd31edfb09b/swiftenv-install.sh)" 16 | - swift --version 17 | 18 | before_script: 19 | - set -o pipefail 20 | - if [ $TEST == 1 ]; then 21 | TEST=1 swift package generate-xcodeproj; 22 | else 23 | swift package generate-xcodeproj; 24 | fi 25 | 26 | script: 27 | - if [ $TEST == 1 ]; then 28 | xcodebuild clean build test 29 | -project "$PROJECT" 30 | -scheme "$SCHEME" 31 | -sdk "$SDK" 32 | -destination "$DESTINATION" 33 | -configuration Debug 34 | -enableCodeCoverage YES 35 | CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO | xcpretty -c; 36 | else 37 | xcodebuild clean build 38 | -project "$PROJECT" 39 | -scheme "$SCHEME" 40 | -sdk "$SDK" 41 | -destination "$DESTINATION" 42 | -configuration Debug 43 | CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO | xcpretty -c; 44 | fi 45 | 46 | after_success: 47 | - if [ $TEST == 1 ]; then 48 | bash <(curl -s https://codecov.io/bash) -X xcodeplist -J 'SectionReactor'; 49 | fi 50 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0312544E1F639C3500953F94 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 031254261F639C3500953F94 /* Assets.xcassets */; }; 11 | 0312544F1F639C3500953F94 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 031254271F639C3500953F94 /* LaunchScreen.storyboard */; }; 12 | 031254521F639C3500953F94 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0312542E1F639C3500953F94 /* Comment.swift */; }; 13 | 031254531F639C3500953F94 /* Article.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0312542F1F639C3500953F94 /* Article.swift */; }; 14 | 031254541F639C3500953F94 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031254301F639C3500953F94 /* User.swift */; }; 15 | 031254551F639C3500953F94 /* UICollectionViewFlexLayout+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031254321F639C3500953F94 /* UICollectionViewFlexLayout+Rx.swift */; }; 16 | 031254561F639C3500953F94 /* UIView+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031254331F639C3500953F94 /* UIView+Rx.swift */; }; 17 | 031254571F639C3500953F94 /* FeedViewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031254351F639C3500953F94 /* FeedViewSection.swift */; }; 18 | 031254581F639C3500953F94 /* ArticleSectionReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031254361F639C3500953F94 /* ArticleSectionReactor.swift */; }; 19 | 031254591F639C3500953F94 /* ArticleViewSetion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031254371F639C3500953F94 /* ArticleViewSetion.swift */; }; 20 | 0312545A1F639C3500953F94 /* ArticleService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031254391F639C3500953F94 /* ArticleService.swift */; }; 21 | 0312545B1F639C3500953F94 /* Snap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0312543B1F639C3500953F94 /* Snap.swift */; }; 22 | 0312545C1F639C3500953F94 /* String+BoundingRect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0312543C1F639C3500953F94 /* String+BoundingRect.swift */; }; 23 | 0312545D1F639C3500953F94 /* ArticleListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0312543E1F639C3500953F94 /* ArticleListViewController.swift */; }; 24 | 0312545E1F639C3500953F94 /* ArticleListViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0312543F1F639C3500953F94 /* ArticleListViewReactor.swift */; }; 25 | 0312545F1F639C3500953F94 /* ArticleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031254401F639C3500953F94 /* ArticleViewController.swift */; }; 26 | 031254601F639C3500953F94 /* ArticleViewReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031254411F639C3500953F94 /* ArticleViewReactor.swift */; }; 27 | 031254611F639C3500953F94 /* BaseArticleCardSectionItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031254431F639C3500953F94 /* BaseArticleCardSectionItemCell.swift */; }; 28 | 031254621F639C3500953F94 /* ArticleCardAuthorCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031254441F639C3500953F94 /* ArticleCardAuthorCell.swift */; }; 29 | 031254631F639C3500953F94 /* ArticleCardAuthorCellReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031254451F639C3500953F94 /* ArticleCardAuthorCellReactor.swift */; }; 30 | 031254641F639C3500953F94 /* ArticleCardCommentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031254461F639C3500953F94 /* ArticleCardCommentCell.swift */; }; 31 | 031254651F639C3500953F94 /* ArticleCardCommentCellReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031254471F639C3500953F94 /* ArticleCardCommentCellReactor.swift */; }; 32 | 031254661F639C3500953F94 /* ArticleCardReactionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031254481F639C3500953F94 /* ArticleCardReactionCell.swift */; }; 33 | 031254671F639C3500953F94 /* ArticleCardReactionCellReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 031254491F639C3500953F94 /* ArticleCardReactionCellReactor.swift */; }; 34 | 031254681F639C3500953F94 /* ArticleCardTextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0312544A1F639C3500953F94 /* ArticleCardTextCell.swift */; }; 35 | 031254691F639C3500953F94 /* ArticleCardTextCellReactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0312544B1F639C3500953F94 /* ArticleCardTextCellReactor.swift */; }; 36 | 034463311FF7E5D100F2159A /* AppDependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0344632F1FF7E5D100F2159A /* AppDependency.swift */; }; 37 | 034463321FF7E5D100F2159A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034463301FF7E5D100F2159A /* AppDelegate.swift */; }; 38 | 037E406F1F6450D70005A54E /* ArticleSectionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037E406E1F6450D70005A54E /* ArticleSectionDelegate.swift */; }; 39 | 03F482C91F678598008F0950 /* BorderedLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F482C81F678598008F0950 /* BorderedLayer.swift */; }; 40 | 03F482CB1F678631008F0950 /* CollectionBorderedBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F482CA1F678631008F0950 /* CollectionBorderedBackgroundView.swift */; }; 41 | F2A46C59D3F8EC92AFBD04D5 /* Pods_ArticleFeed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6ED26994F9A3571B8C6B252 /* Pods_ArticleFeed.framework */; }; 42 | /* End PBXBuildFile section */ 43 | 44 | /* Begin PBXFileReference section */ 45 | 031253C91F639B1400953F94 /* ArticleFeed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ArticleFeed.app; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 031254261F639C3500953F94 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 47 | 031254281F639C3500953F94 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 48 | 0312542E1F639C3500953F94 /* Comment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; 49 | 0312542F1F639C3500953F94 /* Article.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Article.swift; sourceTree = ""; }; 50 | 031254301F639C3500953F94 /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 51 | 031254321F639C3500953F94 /* UICollectionViewFlexLayout+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UICollectionViewFlexLayout+Rx.swift"; sourceTree = ""; }; 52 | 031254331F639C3500953F94 /* UIView+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Rx.swift"; sourceTree = ""; }; 53 | 031254351F639C3500953F94 /* FeedViewSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedViewSection.swift; sourceTree = ""; }; 54 | 031254361F639C3500953F94 /* ArticleSectionReactor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleSectionReactor.swift; sourceTree = ""; }; 55 | 031254371F639C3500953F94 /* ArticleViewSetion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleViewSetion.swift; sourceTree = ""; }; 56 | 031254391F639C3500953F94 /* ArticleService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleService.swift; sourceTree = ""; }; 57 | 0312543B1F639C3500953F94 /* Snap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Snap.swift; sourceTree = ""; }; 58 | 0312543C1F639C3500953F94 /* String+BoundingRect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+BoundingRect.swift"; sourceTree = ""; }; 59 | 0312543E1F639C3500953F94 /* ArticleListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleListViewController.swift; sourceTree = ""; }; 60 | 0312543F1F639C3500953F94 /* ArticleListViewReactor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleListViewReactor.swift; sourceTree = ""; }; 61 | 031254401F639C3500953F94 /* ArticleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleViewController.swift; sourceTree = ""; }; 62 | 031254411F639C3500953F94 /* ArticleViewReactor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleViewReactor.swift; sourceTree = ""; }; 63 | 031254431F639C3500953F94 /* BaseArticleCardSectionItemCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseArticleCardSectionItemCell.swift; sourceTree = ""; }; 64 | 031254441F639C3500953F94 /* ArticleCardAuthorCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleCardAuthorCell.swift; sourceTree = ""; }; 65 | 031254451F639C3500953F94 /* ArticleCardAuthorCellReactor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleCardAuthorCellReactor.swift; sourceTree = ""; }; 66 | 031254461F639C3500953F94 /* ArticleCardCommentCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleCardCommentCell.swift; sourceTree = ""; }; 67 | 031254471F639C3500953F94 /* ArticleCardCommentCellReactor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleCardCommentCellReactor.swift; sourceTree = ""; }; 68 | 031254481F639C3500953F94 /* ArticleCardReactionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleCardReactionCell.swift; sourceTree = ""; }; 69 | 031254491F639C3500953F94 /* ArticleCardReactionCellReactor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleCardReactionCellReactor.swift; sourceTree = ""; }; 70 | 0312544A1F639C3500953F94 /* ArticleCardTextCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleCardTextCell.swift; sourceTree = ""; }; 71 | 0312544B1F639C3500953F94 /* ArticleCardTextCellReactor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleCardTextCellReactor.swift; sourceTree = ""; }; 72 | 0312544D1F639C3500953F94 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 73 | 0344632F1FF7E5D100F2159A /* AppDependency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDependency.swift; sourceTree = ""; }; 74 | 034463301FF7E5D100F2159A /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 75 | 037E406E1F6450D70005A54E /* ArticleSectionDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleSectionDelegate.swift; sourceTree = ""; }; 76 | 03F482C81F678598008F0950 /* BorderedLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BorderedLayer.swift; sourceTree = ""; }; 77 | 03F482CA1F678631008F0950 /* CollectionBorderedBackgroundView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionBorderedBackgroundView.swift; sourceTree = ""; }; 78 | 1459E30ECC2A914DBEE8DB4E /* Pods-ArticleFeed.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ArticleFeed.release.xcconfig"; path = "Pods/Target Support Files/Pods-ArticleFeed/Pods-ArticleFeed.release.xcconfig"; sourceTree = ""; }; 79 | C6ED26994F9A3571B8C6B252 /* Pods_ArticleFeed.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ArticleFeed.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 80 | FEC6C51D2B65CE2F484BF318 /* Pods-ArticleFeed.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ArticleFeed.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ArticleFeed/Pods-ArticleFeed.debug.xcconfig"; sourceTree = ""; }; 81 | /* End PBXFileReference section */ 82 | 83 | /* Begin PBXFrameworksBuildPhase section */ 84 | 031253C61F639B1400953F94 /* Frameworks */ = { 85 | isa = PBXFrameworksBuildPhase; 86 | buildActionMask = 2147483647; 87 | files = ( 88 | F2A46C59D3F8EC92AFBD04D5 /* Pods_ArticleFeed.framework in Frameworks */, 89 | ); 90 | runOnlyForDeploymentPostprocessing = 0; 91 | }; 92 | /* End PBXFrameworksBuildPhase section */ 93 | 94 | /* Begin PBXGroup section */ 95 | 031253C01F639B1400953F94 = { 96 | isa = PBXGroup; 97 | children = ( 98 | 031254241F639C3500953F94 /* ArticleFeed */, 99 | 031253CA1F639B1400953F94 /* Products */, 100 | 8759F2C9DC0AF951B0CFBC60 /* Pods */, 101 | E9C0DC86963E92330261B5E0 /* Frameworks */, 102 | ); 103 | sourceTree = ""; 104 | }; 105 | 031253CA1F639B1400953F94 /* Products */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 031253C91F639B1400953F94 /* ArticleFeed.app */, 109 | ); 110 | name = Products; 111 | sourceTree = ""; 112 | }; 113 | 031254241F639C3500953F94 /* ArticleFeed */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 0312542B1F639C3500953F94 /* Sources */, 117 | 031254251F639C3500953F94 /* Resources */, 118 | 0312544C1F639C3500953F94 /* Supporting Files */, 119 | ); 120 | path = ArticleFeed; 121 | sourceTree = ""; 122 | }; 123 | 031254251F639C3500953F94 /* Resources */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 031254261F639C3500953F94 /* Assets.xcassets */, 127 | 031254271F639C3500953F94 /* LaunchScreen.storyboard */, 128 | ); 129 | path = Resources; 130 | sourceTree = ""; 131 | }; 132 | 0312542B1F639C3500953F94 /* Sources */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 0344632E1FF7E5D100F2159A /* Entry */, 136 | 031254381F639C3500953F94 /* Services */, 137 | 0312542D1F639C3500953F94 /* Models */, 138 | 031254341F639C3500953F94 /* Sections */, 139 | 0312543D1F639C3500953F94 /* ViewControllers */, 140 | 031254421F639C3500953F94 /* Views */, 141 | 0312543A1F639C3500953F94 /* Utils */, 142 | 031254311F639C3500953F94 /* Rx */, 143 | ); 144 | path = Sources; 145 | sourceTree = ""; 146 | }; 147 | 0312542D1F639C3500953F94 /* Models */ = { 148 | isa = PBXGroup; 149 | children = ( 150 | 0312542E1F639C3500953F94 /* Comment.swift */, 151 | 0312542F1F639C3500953F94 /* Article.swift */, 152 | 031254301F639C3500953F94 /* User.swift */, 153 | ); 154 | path = Models; 155 | sourceTree = ""; 156 | }; 157 | 031254311F639C3500953F94 /* Rx */ = { 158 | isa = PBXGroup; 159 | children = ( 160 | 031254321F639C3500953F94 /* UICollectionViewFlexLayout+Rx.swift */, 161 | 031254331F639C3500953F94 /* UIView+Rx.swift */, 162 | ); 163 | path = Rx; 164 | sourceTree = ""; 165 | }; 166 | 031254341F639C3500953F94 /* Sections */ = { 167 | isa = PBXGroup; 168 | children = ( 169 | 031254351F639C3500953F94 /* FeedViewSection.swift */, 170 | 031254371F639C3500953F94 /* ArticleViewSetion.swift */, 171 | 031254361F639C3500953F94 /* ArticleSectionReactor.swift */, 172 | 037E406E1F6450D70005A54E /* ArticleSectionDelegate.swift */, 173 | ); 174 | path = Sections; 175 | sourceTree = ""; 176 | }; 177 | 031254381F639C3500953F94 /* Services */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | 031254391F639C3500953F94 /* ArticleService.swift */, 181 | ); 182 | path = Services; 183 | sourceTree = ""; 184 | }; 185 | 0312543A1F639C3500953F94 /* Utils */ = { 186 | isa = PBXGroup; 187 | children = ( 188 | 0312543B1F639C3500953F94 /* Snap.swift */, 189 | 0312543C1F639C3500953F94 /* String+BoundingRect.swift */, 190 | 03F482C81F678598008F0950 /* BorderedLayer.swift */, 191 | ); 192 | path = Utils; 193 | sourceTree = ""; 194 | }; 195 | 0312543D1F639C3500953F94 /* ViewControllers */ = { 196 | isa = PBXGroup; 197 | children = ( 198 | 0312543E1F639C3500953F94 /* ArticleListViewController.swift */, 199 | 0312543F1F639C3500953F94 /* ArticleListViewReactor.swift */, 200 | 031254401F639C3500953F94 /* ArticleViewController.swift */, 201 | 031254411F639C3500953F94 /* ArticleViewReactor.swift */, 202 | ); 203 | path = ViewControllers; 204 | sourceTree = ""; 205 | }; 206 | 031254421F639C3500953F94 /* Views */ = { 207 | isa = PBXGroup; 208 | children = ( 209 | 031254431F639C3500953F94 /* BaseArticleCardSectionItemCell.swift */, 210 | 031254441F639C3500953F94 /* ArticleCardAuthorCell.swift */, 211 | 031254451F639C3500953F94 /* ArticleCardAuthorCellReactor.swift */, 212 | 0312544A1F639C3500953F94 /* ArticleCardTextCell.swift */, 213 | 0312544B1F639C3500953F94 /* ArticleCardTextCellReactor.swift */, 214 | 031254481F639C3500953F94 /* ArticleCardReactionCell.swift */, 215 | 031254491F639C3500953F94 /* ArticleCardReactionCellReactor.swift */, 216 | 031254461F639C3500953F94 /* ArticleCardCommentCell.swift */, 217 | 031254471F639C3500953F94 /* ArticleCardCommentCellReactor.swift */, 218 | 03F482CA1F678631008F0950 /* CollectionBorderedBackgroundView.swift */, 219 | ); 220 | path = Views; 221 | sourceTree = ""; 222 | }; 223 | 0312544C1F639C3500953F94 /* Supporting Files */ = { 224 | isa = PBXGroup; 225 | children = ( 226 | 0312544D1F639C3500953F94 /* Info.plist */, 227 | ); 228 | path = "Supporting Files"; 229 | sourceTree = ""; 230 | }; 231 | 0344632E1FF7E5D100F2159A /* Entry */ = { 232 | isa = PBXGroup; 233 | children = ( 234 | 0344632F1FF7E5D100F2159A /* AppDependency.swift */, 235 | 034463301FF7E5D100F2159A /* AppDelegate.swift */, 236 | ); 237 | path = Entry; 238 | sourceTree = ""; 239 | }; 240 | 8759F2C9DC0AF951B0CFBC60 /* Pods */ = { 241 | isa = PBXGroup; 242 | children = ( 243 | FEC6C51D2B65CE2F484BF318 /* Pods-ArticleFeed.debug.xcconfig */, 244 | 1459E30ECC2A914DBEE8DB4E /* Pods-ArticleFeed.release.xcconfig */, 245 | ); 246 | name = Pods; 247 | sourceTree = ""; 248 | }; 249 | E9C0DC86963E92330261B5E0 /* Frameworks */ = { 250 | isa = PBXGroup; 251 | children = ( 252 | C6ED26994F9A3571B8C6B252 /* Pods_ArticleFeed.framework */, 253 | ); 254 | name = Frameworks; 255 | sourceTree = ""; 256 | }; 257 | /* End PBXGroup section */ 258 | 259 | /* Begin PBXNativeTarget section */ 260 | 031253C81F639B1400953F94 /* ArticleFeed */ = { 261 | isa = PBXNativeTarget; 262 | buildConfigurationList = 031253DB1F639B1400953F94 /* Build configuration list for PBXNativeTarget "ArticleFeed" */; 263 | buildPhases = ( 264 | 8C7EE2ABCA017B0207FF7CE6 /* [CP] Check Pods Manifest.lock */, 265 | 031253C51F639B1400953F94 /* Sources */, 266 | 031253C61F639B1400953F94 /* Frameworks */, 267 | 031253C71F639B1400953F94 /* Resources */, 268 | DBF47E7B32A0E029FEB611C3 /* [CP] Embed Pods Frameworks */, 269 | 16F2A2FE886F6F3732411820 /* [CP] Copy Pods Resources */, 270 | ); 271 | buildRules = ( 272 | ); 273 | dependencies = ( 274 | ); 275 | name = ArticleFeed; 276 | productName = ArticleFeed; 277 | productReference = 031253C91F639B1400953F94 /* ArticleFeed.app */; 278 | productType = "com.apple.product-type.application"; 279 | }; 280 | /* End PBXNativeTarget section */ 281 | 282 | /* Begin PBXProject section */ 283 | 031253C11F639B1400953F94 /* Project object */ = { 284 | isa = PBXProject; 285 | attributes = { 286 | LastSwiftUpdateCheck = 0830; 287 | LastUpgradeCheck = 0900; 288 | ORGANIZATIONNAME = "Suyeol Jeon"; 289 | TargetAttributes = { 290 | 031253C81F639B1400953F94 = { 291 | CreatedOnToolsVersion = 8.3.3; 292 | ProvisioningStyle = Automatic; 293 | }; 294 | }; 295 | }; 296 | buildConfigurationList = 031253C41F639B1400953F94 /* Build configuration list for PBXProject "ArticleFeed" */; 297 | compatibilityVersion = "Xcode 3.2"; 298 | developmentRegion = English; 299 | hasScannedForEncodings = 0; 300 | knownRegions = ( 301 | en, 302 | Base, 303 | ); 304 | mainGroup = 031253C01F639B1400953F94; 305 | productRefGroup = 031253CA1F639B1400953F94 /* Products */; 306 | projectDirPath = ""; 307 | projectRoot = ""; 308 | targets = ( 309 | 031253C81F639B1400953F94 /* ArticleFeed */, 310 | ); 311 | }; 312 | /* End PBXProject section */ 313 | 314 | /* Begin PBXResourcesBuildPhase section */ 315 | 031253C71F639B1400953F94 /* Resources */ = { 316 | isa = PBXResourcesBuildPhase; 317 | buildActionMask = 2147483647; 318 | files = ( 319 | 0312544E1F639C3500953F94 /* Assets.xcassets in Resources */, 320 | 0312544F1F639C3500953F94 /* LaunchScreen.storyboard in Resources */, 321 | ); 322 | runOnlyForDeploymentPostprocessing = 0; 323 | }; 324 | /* End PBXResourcesBuildPhase section */ 325 | 326 | /* Begin PBXShellScriptBuildPhase section */ 327 | 16F2A2FE886F6F3732411820 /* [CP] Copy Pods Resources */ = { 328 | isa = PBXShellScriptBuildPhase; 329 | buildActionMask = 2147483647; 330 | files = ( 331 | ); 332 | inputPaths = ( 333 | ); 334 | name = "[CP] Copy Pods Resources"; 335 | outputPaths = ( 336 | ); 337 | runOnlyForDeploymentPostprocessing = 0; 338 | shellPath = /bin/sh; 339 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ArticleFeed/Pods-ArticleFeed-resources.sh\"\n"; 340 | showEnvVarsInLog = 0; 341 | }; 342 | 8C7EE2ABCA017B0207FF7CE6 /* [CP] Check Pods Manifest.lock */ = { 343 | isa = PBXShellScriptBuildPhase; 344 | buildActionMask = 2147483647; 345 | files = ( 346 | ); 347 | inputPaths = ( 348 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 349 | "${PODS_ROOT}/Manifest.lock", 350 | ); 351 | name = "[CP] Check Pods Manifest.lock"; 352 | outputPaths = ( 353 | "$(DERIVED_FILE_DIR)/Pods-ArticleFeed-checkManifestLockResult.txt", 354 | ); 355 | runOnlyForDeploymentPostprocessing = 0; 356 | shellPath = /bin/sh; 357 | 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"; 358 | showEnvVarsInLog = 0; 359 | }; 360 | DBF47E7B32A0E029FEB611C3 /* [CP] Embed Pods Frameworks */ = { 361 | isa = PBXShellScriptBuildPhase; 362 | buildActionMask = 2147483647; 363 | files = ( 364 | ); 365 | inputPaths = ( 366 | "${SRCROOT}/Pods/Target Support Files/Pods-ArticleFeed/Pods-ArticleFeed-frameworks.sh", 367 | "${BUILT_PRODUCTS_DIR}/CGFloatLiteral/CGFloatLiteral.framework", 368 | "${BUILT_PRODUCTS_DIR}/Differentiator/Differentiator.framework", 369 | "${BUILT_PRODUCTS_DIR}/LoremIpsum/LoremIpsum.framework", 370 | "${BUILT_PRODUCTS_DIR}/ManualLayout/ManualLayout.framework", 371 | "${BUILT_PRODUCTS_DIR}/ReactorKit/ReactorKit.framework", 372 | "${BUILT_PRODUCTS_DIR}/ReusableKit/ReusableKit.framework", 373 | "${BUILT_PRODUCTS_DIR}/RxCocoa/RxCocoa.framework", 374 | "${BUILT_PRODUCTS_DIR}/RxDataSources/RxDataSources.framework", 375 | "${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework", 376 | "${BUILT_PRODUCTS_DIR}/RxViewController/RxViewController.framework", 377 | "${BUILT_PRODUCTS_DIR}/SectionReactor/SectionReactor.framework", 378 | "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", 379 | "${BUILT_PRODUCTS_DIR}/SwiftyColor/SwiftyColor.framework", 380 | "${BUILT_PRODUCTS_DIR}/Then/Then.framework", 381 | "${BUILT_PRODUCTS_DIR}/UICollectionViewFlexLayout/UICollectionViewFlexLayout.framework", 382 | "${BUILT_PRODUCTS_DIR}/URLNavigator/URLNavigator.framework", 383 | ); 384 | name = "[CP] Embed Pods Frameworks"; 385 | outputPaths = ( 386 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CGFloatLiteral.framework", 387 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Differentiator.framework", 388 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LoremIpsum.framework", 389 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ManualLayout.framework", 390 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactorKit.framework", 391 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReusableKit.framework", 392 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxCocoa.framework", 393 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxDataSources.framework", 394 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxSwift.framework", 395 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxViewController.framework", 396 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SectionReactor.framework", 397 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", 398 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyColor.framework", 399 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Then.framework", 400 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/UICollectionViewFlexLayout.framework", 401 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/URLNavigator.framework", 402 | ); 403 | runOnlyForDeploymentPostprocessing = 0; 404 | shellPath = /bin/sh; 405 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ArticleFeed/Pods-ArticleFeed-frameworks.sh\"\n"; 406 | showEnvVarsInLog = 0; 407 | }; 408 | /* End PBXShellScriptBuildPhase section */ 409 | 410 | /* Begin PBXSourcesBuildPhase section */ 411 | 031253C51F639B1400953F94 /* Sources */ = { 412 | isa = PBXSourcesBuildPhase; 413 | buildActionMask = 2147483647; 414 | files = ( 415 | 031254541F639C3500953F94 /* User.swift in Sources */, 416 | 031254601F639C3500953F94 /* ArticleViewReactor.swift in Sources */, 417 | 031254531F639C3500953F94 /* Article.swift in Sources */, 418 | 031254671F639C3500953F94 /* ArticleCardReactionCellReactor.swift in Sources */, 419 | 034463321FF7E5D100F2159A /* AppDelegate.swift in Sources */, 420 | 031254651F639C3500953F94 /* ArticleCardCommentCellReactor.swift in Sources */, 421 | 031254561F639C3500953F94 /* UIView+Rx.swift in Sources */, 422 | 03F482CB1F678631008F0950 /* CollectionBorderedBackgroundView.swift in Sources */, 423 | 031254551F639C3500953F94 /* UICollectionViewFlexLayout+Rx.swift in Sources */, 424 | 034463311FF7E5D100F2159A /* AppDependency.swift in Sources */, 425 | 031254571F639C3500953F94 /* FeedViewSection.swift in Sources */, 426 | 0312545A1F639C3500953F94 /* ArticleService.swift in Sources */, 427 | 031254611F639C3500953F94 /* BaseArticleCardSectionItemCell.swift in Sources */, 428 | 031254621F639C3500953F94 /* ArticleCardAuthorCell.swift in Sources */, 429 | 031254661F639C3500953F94 /* ArticleCardReactionCell.swift in Sources */, 430 | 031254581F639C3500953F94 /* ArticleSectionReactor.swift in Sources */, 431 | 03F482C91F678598008F0950 /* BorderedLayer.swift in Sources */, 432 | 031254641F639C3500953F94 /* ArticleCardCommentCell.swift in Sources */, 433 | 031254591F639C3500953F94 /* ArticleViewSetion.swift in Sources */, 434 | 031254631F639C3500953F94 /* ArticleCardAuthorCellReactor.swift in Sources */, 435 | 0312545F1F639C3500953F94 /* ArticleViewController.swift in Sources */, 436 | 0312545B1F639C3500953F94 /* Snap.swift in Sources */, 437 | 0312545C1F639C3500953F94 /* String+BoundingRect.swift in Sources */, 438 | 0312545E1F639C3500953F94 /* ArticleListViewReactor.swift in Sources */, 439 | 031254691F639C3500953F94 /* ArticleCardTextCellReactor.swift in Sources */, 440 | 0312545D1F639C3500953F94 /* ArticleListViewController.swift in Sources */, 441 | 031254681F639C3500953F94 /* ArticleCardTextCell.swift in Sources */, 442 | 037E406F1F6450D70005A54E /* ArticleSectionDelegate.swift in Sources */, 443 | 031254521F639C3500953F94 /* Comment.swift in Sources */, 444 | ); 445 | runOnlyForDeploymentPostprocessing = 0; 446 | }; 447 | /* End PBXSourcesBuildPhase section */ 448 | 449 | /* Begin PBXVariantGroup section */ 450 | 031254271F639C3500953F94 /* LaunchScreen.storyboard */ = { 451 | isa = PBXVariantGroup; 452 | children = ( 453 | 031254281F639C3500953F94 /* Base */, 454 | ); 455 | name = LaunchScreen.storyboard; 456 | sourceTree = ""; 457 | }; 458 | /* End PBXVariantGroup section */ 459 | 460 | /* Begin XCBuildConfiguration section */ 461 | 031253D91F639B1400953F94 /* Debug */ = { 462 | isa = XCBuildConfiguration; 463 | buildSettings = { 464 | ALWAYS_SEARCH_USER_PATHS = NO; 465 | CLANG_ANALYZER_NONNULL = YES; 466 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 467 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 468 | CLANG_CXX_LIBRARY = "libc++"; 469 | CLANG_ENABLE_MODULES = YES; 470 | CLANG_ENABLE_OBJC_ARC = YES; 471 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 472 | CLANG_WARN_BOOL_CONVERSION = YES; 473 | CLANG_WARN_COMMA = YES; 474 | CLANG_WARN_CONSTANT_CONVERSION = YES; 475 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 476 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 477 | CLANG_WARN_EMPTY_BODY = YES; 478 | CLANG_WARN_ENUM_CONVERSION = YES; 479 | CLANG_WARN_INFINITE_RECURSION = YES; 480 | CLANG_WARN_INT_CONVERSION = YES; 481 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 482 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 483 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 484 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 485 | CLANG_WARN_STRICT_PROTOTYPES = YES; 486 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 487 | CLANG_WARN_UNREACHABLE_CODE = YES; 488 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 489 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 490 | COPY_PHASE_STRIP = NO; 491 | DEBUG_INFORMATION_FORMAT = dwarf; 492 | ENABLE_STRICT_OBJC_MSGSEND = YES; 493 | ENABLE_TESTABILITY = YES; 494 | GCC_C_LANGUAGE_STANDARD = gnu99; 495 | GCC_DYNAMIC_NO_PIC = NO; 496 | GCC_NO_COMMON_BLOCKS = YES; 497 | GCC_OPTIMIZATION_LEVEL = 0; 498 | GCC_PREPROCESSOR_DEFINITIONS = ( 499 | "DEBUG=1", 500 | "$(inherited)", 501 | ); 502 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 503 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 504 | GCC_WARN_UNDECLARED_SELECTOR = YES; 505 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 506 | GCC_WARN_UNUSED_FUNCTION = YES; 507 | GCC_WARN_UNUSED_VARIABLE = YES; 508 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 509 | MTL_ENABLE_DEBUG_INFO = YES; 510 | ONLY_ACTIVE_ARCH = YES; 511 | SDKROOT = iphoneos; 512 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 513 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 514 | SWIFT_VERSION = 4.0; 515 | }; 516 | name = Debug; 517 | }; 518 | 031253DA1F639B1400953F94 /* Release */ = { 519 | isa = XCBuildConfiguration; 520 | buildSettings = { 521 | ALWAYS_SEARCH_USER_PATHS = NO; 522 | CLANG_ANALYZER_NONNULL = YES; 523 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 524 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 525 | CLANG_CXX_LIBRARY = "libc++"; 526 | CLANG_ENABLE_MODULES = YES; 527 | CLANG_ENABLE_OBJC_ARC = YES; 528 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 529 | CLANG_WARN_BOOL_CONVERSION = YES; 530 | CLANG_WARN_COMMA = YES; 531 | CLANG_WARN_CONSTANT_CONVERSION = YES; 532 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 533 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 534 | CLANG_WARN_EMPTY_BODY = YES; 535 | CLANG_WARN_ENUM_CONVERSION = YES; 536 | CLANG_WARN_INFINITE_RECURSION = YES; 537 | CLANG_WARN_INT_CONVERSION = YES; 538 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 539 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 540 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 541 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 542 | CLANG_WARN_STRICT_PROTOTYPES = YES; 543 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 544 | CLANG_WARN_UNREACHABLE_CODE = YES; 545 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 546 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 547 | COPY_PHASE_STRIP = NO; 548 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 549 | ENABLE_NS_ASSERTIONS = NO; 550 | ENABLE_STRICT_OBJC_MSGSEND = YES; 551 | GCC_C_LANGUAGE_STANDARD = gnu99; 552 | GCC_NO_COMMON_BLOCKS = YES; 553 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 554 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 555 | GCC_WARN_UNDECLARED_SELECTOR = YES; 556 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 557 | GCC_WARN_UNUSED_FUNCTION = YES; 558 | GCC_WARN_UNUSED_VARIABLE = YES; 559 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 560 | MTL_ENABLE_DEBUG_INFO = NO; 561 | SDKROOT = iphoneos; 562 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 563 | SWIFT_VERSION = 4.0; 564 | VALIDATE_PRODUCT = YES; 565 | }; 566 | name = Release; 567 | }; 568 | 031253DC1F639B1400953F94 /* Debug */ = { 569 | isa = XCBuildConfiguration; 570 | baseConfigurationReference = FEC6C51D2B65CE2F484BF318 /* Pods-ArticleFeed.debug.xcconfig */; 571 | buildSettings = { 572 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 573 | INFOPLIST_FILE = "$(SRCROOT)/ArticleFeed/Supporting Files/Info.plist"; 574 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 575 | PRODUCT_BUNDLE_IDENTIFIER = kr.xoul.ArticleFeed; 576 | PRODUCT_NAME = "$(TARGET_NAME)"; 577 | }; 578 | name = Debug; 579 | }; 580 | 031253DD1F639B1400953F94 /* Release */ = { 581 | isa = XCBuildConfiguration; 582 | baseConfigurationReference = 1459E30ECC2A914DBEE8DB4E /* Pods-ArticleFeed.release.xcconfig */; 583 | buildSettings = { 584 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 585 | INFOPLIST_FILE = "$(SRCROOT)/ArticleFeed/Supporting Files/Info.plist"; 586 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 587 | PRODUCT_BUNDLE_IDENTIFIER = kr.xoul.ArticleFeed; 588 | PRODUCT_NAME = "$(TARGET_NAME)"; 589 | }; 590 | name = Release; 591 | }; 592 | /* End XCBuildConfiguration section */ 593 | 594 | /* Begin XCConfigurationList section */ 595 | 031253C41F639B1400953F94 /* Build configuration list for PBXProject "ArticleFeed" */ = { 596 | isa = XCConfigurationList; 597 | buildConfigurations = ( 598 | 031253D91F639B1400953F94 /* Debug */, 599 | 031253DA1F639B1400953F94 /* Release */, 600 | ); 601 | defaultConfigurationIsVisible = 0; 602 | defaultConfigurationName = Release; 603 | }; 604 | 031253DB1F639B1400953F94 /* Build configuration list for PBXNativeTarget "ArticleFeed" */ = { 605 | isa = XCConfigurationList; 606 | buildConfigurations = ( 607 | 031253DC1F639B1400953F94 /* Debug */, 608 | 031253DD1F639B1400953F94 /* Release */, 609 | ); 610 | defaultConfigurationIsVisible = 0; 611 | defaultConfigurationName = Release; 612 | }; 613 | /* End XCConfigurationList section */ 614 | }; 615 | rootObject = 031253C11F639B1400953F94 /* Project object */; 616 | } 617 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Resources/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Entry/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 01/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | import CGFloatLiteral 12 | import ManualLayout 13 | import RxCocoa 14 | import RxSwift 15 | import RxViewController 16 | import SnapKit 17 | import SwiftyColor 18 | import Then 19 | 20 | @UIApplicationMain 21 | class AppDelegate: UIResponder, UIApplicationDelegate { 22 | 23 | var dependency: AppDependency! 24 | var window: UIWindow? 25 | 26 | func application( 27 | _ application: UIApplication, 28 | didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? 29 | ) -> Bool { 30 | self.dependency = self.dependency ?? AppDependency.resolve() 31 | 32 | self.dependency.window.frame = UIScreen.main.bounds 33 | self.dependency.window.backgroundColor = .white 34 | self.dependency.window.makeKeyAndVisible() 35 | self.dependency.window.rootViewController = self.dependency.rootViewController 36 | 37 | self.window = self.dependency.window 38 | return true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Entry/AppDependency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDependency.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 08/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | import URLNavigator 12 | 13 | struct AppDependency { 14 | let window: UIWindow 15 | let rootViewController: UIViewController 16 | } 17 | 18 | extension AppDependency { 19 | static func resolve() -> AppDependency { 20 | let articleService = ArticleService() 21 | let navigator = Navigator() 22 | 23 | let articleSectionReactorFactory: (Article) -> ArticleSectionReactor = { article in 24 | return ArticleSectionReactor( 25 | article: article, 26 | authorCellReactorFactory: ArticleCardAuthorCellReactor.init, 27 | textCellReactorFactory: ArticleCardTextCellReactor.init, 28 | reactionCellReactorFactory: ArticleCardReactionCellReactor.init, 29 | commentCellReactorFactory: ArticleCardCommentCellReactor.init 30 | ) 31 | } 32 | 33 | var articleViewControllerFactory: ((Article) -> ArticleViewController)! 34 | articleViewControllerFactory = { article in 35 | return ArticleViewController( 36 | reactor: ArticleViewReactor( 37 | article: article, 38 | articleSectionReactorFactory: articleSectionReactorFactory 39 | ), 40 | articleSectionDelegate: ArticleSectionDelegate( 41 | navigator: navigator, 42 | articleViewControllerFactory: articleViewControllerFactory, 43 | presentsArticleViewControllerWhenTaps: true 44 | ) 45 | ) 46 | } 47 | 48 | let articleListViewReactor = ArticleListViewReactor( 49 | articleService: articleService, 50 | articleSectionReactorFactory: articleSectionReactorFactory 51 | ) 52 | let articleListViewController = ArticleListViewController( 53 | reactor: articleListViewReactor, 54 | articleSectionDelegate: ArticleSectionDelegate( 55 | navigator: navigator, 56 | articleViewControllerFactory: articleViewControllerFactory, 57 | presentsArticleViewControllerWhenTaps: false 58 | ) 59 | ) 60 | 61 | return .init( 62 | window: UIWindow(), 63 | rootViewController: UINavigationController(rootViewController: articleListViewController) 64 | ) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Models/Article.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Article.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 01/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import LoremIpsum 10 | 11 | struct Article { 12 | var id: String 13 | var author: User 14 | var text: String? 15 | var comments: [Comment] 16 | 17 | static func random() -> Article { 18 | let id = UUID().uuidString 19 | let comments: [Comment] = (0..() 17 | 18 | var id: String 19 | var articleID: String 20 | var author: User 21 | var text: String 22 | 23 | static func random(articleID: String) -> Comment { 24 | return Comment( 25 | id: UUID().uuidString, 26 | articleID: articleID, 27 | author: .random(), 28 | text: LoremIpsum.sentence() 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Models/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Author.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 01/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import LoremIpsum 10 | 11 | struct User { 12 | var id: String 13 | var name: String 14 | 15 | static func random() -> User { 16 | return User(id: UUID().uuidString, name: LoremIpsum.name()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Rx/UICollectionViewFlexLayout+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionViewFlexLayout+Rx.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 01/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import RxCocoa 10 | import UICollectionViewFlexLayout 11 | 12 | extension RxCollectionViewDelegateProxy: UICollectionViewDelegateFlexLayout { 13 | } 14 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Rx/UIView+Rx.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Rx.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 01/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | import RxCocoa 12 | import RxSwift 13 | 14 | extension Reactive where Base: UIView { 15 | var setNeedsLayout: Binder { 16 | return Binder(self.base) { view, _ in 17 | view.setNeedsLayout() 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Sections/ArticleSectionDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleSectionDelegate.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 09/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import ReusableKit 10 | import SectionReactor 11 | import UICollectionViewFlexLayout 12 | import URLNavigator 13 | 14 | final class ArticleSectionDelegate: SectionDelegateType { 15 | typealias SectionReactor = ArticleSectionReactor 16 | 17 | fileprivate enum Reusable { 18 | static let authorCell = ReusableCell() 19 | static let textCell = ReusableCell() 20 | static let reactionCell = ReusableCell() 21 | static let commentCell = ReusableCell() 22 | static let sectionBackgroundView = ReusableView() 23 | static let itemBackgroundView = ReusableView() 24 | } 25 | 26 | private let navigator: NavigatorType 27 | private let articleViewControllerFactory: (Article) -> ArticleViewController 28 | private let presentsArticleViewControllerWhenTaps: Bool 29 | 30 | init( 31 | navigator: NavigatorType, 32 | articleViewControllerFactory: @escaping (Article) -> ArticleViewController, 33 | presentsArticleViewControllerWhenTaps: Bool 34 | ) { 35 | self.navigator = navigator 36 | self.articleViewControllerFactory = articleViewControllerFactory 37 | self.presentsArticleViewControllerWhenTaps = presentsArticleViewControllerWhenTaps 38 | } 39 | 40 | func registerReusables(to collectionView: UICollectionView) { 41 | collectionView.register(Reusable.authorCell) 42 | collectionView.register(Reusable.textCell) 43 | collectionView.register(Reusable.reactionCell) 44 | collectionView.register(Reusable.commentCell) 45 | collectionView.register(Reusable.sectionBackgroundView, kind: UICollectionElementKindSectionBackground) 46 | collectionView.register(Reusable.itemBackgroundView, kind: UICollectionElementKindItemBackground) 47 | } 48 | 49 | func cell( 50 | collectionView: UICollectionView, 51 | indexPath: IndexPath, 52 | sectionReactor: SectionReactor, 53 | sectionItem: SectionItem 54 | ) -> UICollectionViewCell { 55 | switch sectionItem { 56 | case let .author(cellReactor): 57 | let cell = collectionView.dequeue(Reusable.authorCell, for: indexPath) 58 | if cell.reactor !== cellReactor { 59 | cell.reactor = cellReactor 60 | self.subscribeTapToPresentArticleViewController(cell: cell, sectionReactor: sectionReactor) 61 | } 62 | return cell 63 | 64 | case let .text(cellReactor): 65 | let cell = collectionView.dequeue(Reusable.textCell, for: indexPath) 66 | if cell.reactor !== cellReactor { 67 | cell.reactor = cellReactor 68 | self.subscribeTapToPresentArticleViewController(cell: cell, sectionReactor: sectionReactor) 69 | } 70 | return cell 71 | 72 | case let .reaction(cellReactor): 73 | let cell = collectionView.dequeue(Reusable.reactionCell, for: indexPath) 74 | if cell.reactor !== cellReactor { 75 | cell.reactor = cellReactor 76 | self.subscribeTapToPresentArticleViewController(cell: cell, sectionReactor: sectionReactor) 77 | } 78 | return cell 79 | 80 | case let .comment(cellReactor): 81 | let cell = collectionView.dequeue(Reusable.commentCell, for: indexPath) 82 | if cell.reactor !== cellReactor { 83 | cell.reactor = cellReactor 84 | } 85 | return cell 86 | } 87 | } 88 | 89 | func supplementaryView( 90 | collectionView: UICollectionView, 91 | kind: String, 92 | indexPath: IndexPath, 93 | sectionReactor: SectionReactor, 94 | sectionItem: SectionItem 95 | ) -> UICollectionReusableView { 96 | switch kind { 97 | case UICollectionElementKindSectionBackground: 98 | let view = collectionView.dequeue(Reusable.sectionBackgroundView, kind: kind, for: indexPath) 99 | view.backgroundColor = .white 100 | view.borderedLayer?.borders = [.top, .bottom] 101 | return view 102 | 103 | case UICollectionElementKindItemBackground: 104 | switch sectionItem { 105 | case .comment: 106 | let view = collectionView.dequeue(Reusable.itemBackgroundView, kind: kind, for: indexPath) 107 | view.backgroundColor = 0xFAFAFA.color 108 | if self.isFirstComment(indexPath, in: sectionReactor.currentState.sectionItems) { 109 | view.borderedLayer?.borders = [.top] 110 | } else if self.isLast(indexPath, in: collectionView) { 111 | view.borderedLayer?.borders = [.bottom] 112 | } else { 113 | view.borderedLayer?.borders = [] 114 | } 115 | return view 116 | 117 | default: 118 | let view = collectionView.dequeue(Reusable.itemBackgroundView, kind: kind, for: indexPath) 119 | view.backgroundColor = .white 120 | view.borderedLayer?.borders = [] 121 | return view 122 | } 123 | 124 | default: 125 | return collectionView.emptyView(for: indexPath, kind: kind) 126 | } 127 | } 128 | 129 | func cellSize( 130 | collectionView: UICollectionView, 131 | layout: UICollectionViewFlexLayout, 132 | indexPath: IndexPath, 133 | sectionItem: SectionItem 134 | ) -> CGSize { 135 | let maxWidth = layout.maximumWidth(forItemAt: indexPath) 136 | switch sectionItem { 137 | case let .author(cellReactor): 138 | return Reusable.authorCell.class.size(width: maxWidth, reactor: cellReactor) 139 | 140 | case let .text(cellReactor): 141 | return Reusable.textCell.class.size(width: maxWidth, reactor: cellReactor) 142 | 143 | case let .reaction(cellReactor): 144 | return Reusable.reactionCell.class.size(width: maxWidth, reactor: cellReactor) 145 | 146 | case let .comment(cellReactor): 147 | return Reusable.commentCell.class.size(width: maxWidth, reactor: cellReactor) 148 | } 149 | } 150 | 151 | func cellMargin( 152 | collectionView: UICollectionView, 153 | layout: UICollectionViewFlexLayout, 154 | indexPath: IndexPath, 155 | sectionItem: SectionItem 156 | ) -> UIEdgeInsets { 157 | switch sectionItem { 158 | case .comment: 159 | let sectionPadding = layout.padding(forSectionAt: indexPath.section) 160 | let isLast = self.isLast(indexPath, in: collectionView) 161 | return UIEdgeInsets( 162 | top: 0, 163 | left: -sectionPadding.left, 164 | bottom: isLast ? -sectionPadding.bottom : 0, 165 | right: -sectionPadding.right 166 | ) 167 | 168 | default: 169 | return .zero 170 | } 171 | } 172 | 173 | func cellPadding( 174 | collectionView: UICollectionView, 175 | layout: UICollectionViewFlexLayout, 176 | indexPath: IndexPath, 177 | sectionItem: SectionItem 178 | ) -> UIEdgeInsets { 179 | switch sectionItem { 180 | case .comment: 181 | let sectionPadding = layout.padding(forSectionAt: indexPath.section) 182 | return UIEdgeInsets( 183 | top: 10, 184 | left: sectionPadding.left, 185 | bottom: 10, 186 | right: sectionPadding.right 187 | ) 188 | 189 | default: 190 | return .zero 191 | } 192 | } 193 | 194 | func cellVerticalSpacing( 195 | collectionView: UICollectionView, 196 | layout: UICollectionViewFlexLayout, 197 | sectionItem: SectionItem, 198 | nextSectionItem: SectionItem 199 | ) -> CGFloat { 200 | switch (sectionItem, nextSectionItem) { 201 | case (.comment, .comment): return 0 202 | case (_, .comment): return 15 203 | case (.author, _): return 10 204 | case (.text, _): return 10 205 | case (.reaction, _): return 10 206 | case (.comment, _): return 10 207 | } 208 | } 209 | 210 | 211 | // MARK: Utils 212 | 213 | private func subscribeTapToPresentArticleViewController( 214 | cell: BaseArticleCardSectionItemCell, 215 | sectionReactor: SectionReactor 216 | ) { 217 | guard self.presentsArticleViewControllerWhenTaps else { return } 218 | cell.rx.tap 219 | .subscribe(onNext: { [weak self, weak sectionReactor] in 220 | guard let `self` = self else { return } 221 | guard let article = sectionReactor?.currentState.article else { return } 222 | let articleViewController = self.articleViewControllerFactory(article) 223 | self.navigator.push(articleViewController) 224 | }) 225 | .disposed(by: cell.disposeBag) 226 | } 227 | 228 | private func isFirstComment(_ indexPath: IndexPath, in sectionItems: [SectionItem]) -> Bool { 229 | let prevItemIndex = indexPath.item - 1 230 | guard sectionItems.indices.contains(prevItemIndex) else { return true } 231 | if case .comment = sectionItems[prevItemIndex] { 232 | return false 233 | } else { 234 | return true 235 | } 236 | } 237 | 238 | private func isLast(_ indexPath: IndexPath, in collectionView: UICollectionView) -> Bool { 239 | let lastItem = collectionView.numberOfItems(inSection: indexPath.section) - 1 240 | return indexPath.item == lastItem 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Sections/ArticleSectionReactor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleSectionReactor.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 01/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import ReactorKit 10 | import RxSwift 11 | import SectionReactor 12 | 13 | final class ArticleSectionReactor: SectionReactor { 14 | enum SectionItem { 15 | case author(ArticleCardAuthorCellReactor) 16 | case text(ArticleCardTextCellReactor) 17 | case reaction(ArticleCardReactionCellReactor) 18 | case comment(ArticleCardCommentCellReactor) 19 | } 20 | 21 | enum Action { 22 | } 23 | 24 | enum Mutation { 25 | case appendComment(Comment) 26 | } 27 | 28 | struct State: SectionReactorState { 29 | var article: Article 30 | var sectionItems: [SectionItem] 31 | } 32 | 33 | let initialState: State 34 | let authorCellReactorFactory: (User) -> ArticleCardAuthorCellReactor 35 | let textCellReactorFactory: (String) -> ArticleCardTextCellReactor 36 | let reactionCellReactorFactory: (String) -> ArticleCardReactionCellReactor 37 | let commentCellReactorFactory: (Comment) -> ArticleCardCommentCellReactor 38 | 39 | init( 40 | article: Article, 41 | authorCellReactorFactory: @escaping (User) -> ArticleCardAuthorCellReactor, 42 | textCellReactorFactory: @escaping (String) -> ArticleCardTextCellReactor, 43 | reactionCellReactorFactory: @escaping (String) -> ArticleCardReactionCellReactor, 44 | commentCellReactorFactory: @escaping (Comment) -> ArticleCardCommentCellReactor 45 | ) { 46 | defer { _ = self.state } 47 | self.authorCellReactorFactory = authorCellReactorFactory 48 | self.textCellReactorFactory = textCellReactorFactory 49 | self.reactionCellReactorFactory = reactionCellReactorFactory 50 | self.commentCellReactorFactory = commentCellReactorFactory 51 | 52 | var sectionItems: [SectionItem] = [ 53 | .author(ArticleCardAuthorCellReactor(user: article.author)) 54 | ] 55 | if let text = article.text { 56 | sectionItems.append(.text(ArticleCardTextCellReactor(text: text))) 57 | } 58 | sectionItems.append(.reaction(ArticleCardReactionCellReactor(articleID: article.id))) 59 | for comment in article.comments { 60 | sectionItems.append(.comment(ArticleCardCommentCellReactor(comment: comment))) 61 | } 62 | self.initialState = State(article: article, sectionItems: sectionItems) 63 | } 64 | 65 | func transform(mutation: Observable) -> Observable { 66 | let fromCommentEvent = Comment.event.flatMap { [weak self] event -> Observable in 67 | guard let `self` = self else { return .empty() } 68 | switch event { 69 | case let .create(comment): 70 | guard comment.articleID == self.currentState.article.id else { return .empty() } 71 | return .just(Mutation.appendComment(comment)) 72 | } 73 | } 74 | return Observable.merge(mutation, fromCommentEvent) 75 | } 76 | 77 | func reduce(state: State, mutation: Mutation) -> State { 78 | var state = state 79 | switch mutation { 80 | case let .appendComment(comment): 81 | state.article.comments.append(comment) 82 | state.sectionItems.append(.comment(ArticleCardCommentCellReactor(comment: comment))) 83 | return state 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Sections/ArticleViewSetion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleViewSetion.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 07/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import RxDataSources 10 | 11 | enum ArticleViewSection { 12 | case article(ArticleSectionReactor) 13 | } 14 | 15 | extension ArticleViewSection: SectionModelType { 16 | var items: [ArticleViewSectionItem] { 17 | switch self { 18 | case let .article(sectionReactor): 19 | return sectionReactor.currentState.sectionItems.map { 20 | ArticleViewSectionItem(sectionReactor: sectionReactor, sectionItem: $0) 21 | } 22 | } 23 | } 24 | 25 | init(original: ArticleViewSection, items: [ArticleViewSectionItem]) { 26 | self = original 27 | } 28 | } 29 | 30 | struct ArticleViewSectionItem { 31 | let sectionReactor: ArticleSectionReactor 32 | let sectionItem: ArticleSectionReactor.SectionItem 33 | } 34 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Sections/FeedViewSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleListViewSection.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 01/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import RxDataSources 10 | 11 | enum ArticleListViewSection { 12 | case article(ArticleSectionReactor) 13 | } 14 | 15 | extension ArticleListViewSection: SectionModelType { 16 | var items: [ArticleListViewSectionItem] { 17 | switch self { 18 | case let .article(sectionReactor): 19 | return sectionReactor.currentState.sectionItems.map { ArticleListViewSectionItem.articleCard(sectionReactor, $0) } 20 | } 21 | } 22 | 23 | init(original: ArticleListViewSection, items: [ArticleListViewSectionItem]) { 24 | self = original 25 | } 26 | } 27 | 28 | extension ArticleListViewSection { 29 | var articleCardSectionItems: [ArticleSectionReactor.SectionItem] { 30 | return self.items.flatMap { sectionItem in 31 | if case let .articleCard(_, item) = sectionItem { 32 | return item 33 | } else { 34 | return nil 35 | } 36 | } 37 | } 38 | } 39 | 40 | enum ArticleListViewSectionItem { 41 | case articleCard(ArticleSectionReactor, ArticleSectionReactor.SectionItem) 42 | } 43 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Services/ArticleService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleService.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 01/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import RxSwift 10 | 11 | protocol ArticleServiceType { 12 | func articles() -> Observable<[Article]> 13 | } 14 | 15 | final class ArticleService: ArticleServiceType { 16 | func articles() -> Observable<[Article]> { 17 | return Observable 18 | .just((0..<30).map { _ in Article.random() }) 19 | .delay(0.7, scheduler: ConcurrentDispatchQueueScheduler(qos: DispatchQoS.utility)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Utils/BorderedLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BorderedLayer.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 12/03/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | import SwiftyColor 12 | 13 | // MARK: - BorderedLayer 14 | 15 | final class BorderedLayer: CALayer { 16 | struct Border: OptionSet, Hashable { 17 | var rawValue: UInt 18 | var hashValue: Int 19 | 20 | init(rawValue: UInt) { 21 | self.rawValue = rawValue 22 | self.hashValue = Int(rawValue) 23 | } 24 | 25 | static let top = Border(rawValue: 1 << 0) 26 | static let left = Border(rawValue: 1 << 1) 27 | static let bottom = Border(rawValue: 1 << 2) 28 | static let right = Border(rawValue: 1 << 3) 29 | static let shadow = Border(rawValue: 1 << 4) 30 | } 31 | 32 | typealias Insets = (CGFloat, CGFloat) 33 | 34 | var borders: Border = [] { 35 | didSet { 36 | self.updateBordersHidden() 37 | } 38 | } 39 | 40 | let topBorder = CALayer() 41 | let leftBorder = CALayer() 42 | let bottomBorder = CALayer() 43 | let rightBorder = CALayer() 44 | 45 | let shadowLayers = [CALayer(), CALayer(), CALayer()] 46 | 47 | var borderColors = [Border: UIColor]() { 48 | didSet { 49 | self.updateBordersColor() 50 | } 51 | } 52 | var borderWidths = [Border: CGFloat]() { 53 | didSet { 54 | self.updateBordersFrame() 55 | } 56 | } 57 | var borderInsets = [Border: Insets]() { 58 | didSet { 59 | self.updateBordersFrame() 60 | } 61 | } 62 | 63 | override var frame: CGRect { 64 | didSet { 65 | self.updateBordersFrame() 66 | } 67 | } 68 | 69 | 70 | // MARK: Initializing 71 | 72 | override init() { 73 | super.init() 74 | 75 | self.borderColor = nil 76 | self.borderWidth = 0 77 | 78 | self.addSublayer(self.topBorder) 79 | self.addSublayer(self.leftBorder) 80 | self.addSublayer(self.bottomBorder) 81 | self.addSublayer(self.rightBorder) 82 | for shadow in self.shadowLayers { 83 | self.addSublayer(shadow) 84 | } 85 | 86 | self.updateBordersHidden() 87 | self.updateBordersColor() 88 | } 89 | 90 | override init(layer: Any) { 91 | super.init(layer: layer) 92 | } 93 | 94 | required init?(coder aDecoder: NSCoder) { 95 | fatalError("init(coder:) has not been implemented") 96 | } 97 | 98 | func updateBordersHidden() { 99 | self.topBorder.isHidden = !self.borders.contains(.top) 100 | self.leftBorder.isHidden = !self.borders.contains(.left) 101 | self.bottomBorder.isHidden = !self.borders.contains(.bottom) 102 | self.rightBorder.isHidden = !self.borders.contains(.right) 103 | 104 | let shadowHidden = !self.borders.contains(.shadow) 105 | for shadowLayer in self.shadowLayers { 106 | shadowLayer.isHidden = shadowHidden 107 | } 108 | } 109 | 110 | func updateBordersColor() { 111 | self.topBorder.backgroundColor = self.colorForBorder(.top).cgColor 112 | self.leftBorder.backgroundColor = self.colorForBorder(.left).cgColor 113 | self.bottomBorder.backgroundColor = self.colorForBorder(.bottom).cgColor 114 | self.rightBorder.backgroundColor = self.colorForBorder(.right).cgColor 115 | 116 | let color = self.colorForBorder(.shadow) 117 | for (i, shadow) in self.shadowLayers.enumerated() { 118 | let alpha = (CGFloat(self.shadowLayers.count - i) - 0.6) / CGFloat(self.shadowLayers.count) 119 | shadow.backgroundColor = (color~alpha).cgColor 120 | } 121 | } 122 | 123 | func updateBordersFrame() { 124 | CATransaction.begin() 125 | CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions) 126 | 127 | let topInsets = self.insetsForBorder(.top) 128 | self.topBorder.frame.size.width = self.frame.width - topInsets.0 - topInsets.1 129 | self.topBorder.frame.size.height = self.widthForBorder(.top) 130 | self.topBorder.frame.origin.x = topInsets.0 131 | 132 | let bottomInsets = self.insetsForBorder(.bottom) 133 | self.bottomBorder.frame.size.width = self.frame.width - bottomInsets.0 - bottomInsets.1 134 | self.bottomBorder.frame.size.height = self.widthForBorder(.bottom) 135 | self.bottomBorder.frame.origin.x = bottomInsets.0 136 | self.bottomBorder.frame.origin.y = self.frame.height - self.bottomBorder.frame.size.height 137 | 138 | let leftInsets = self.insetsForBorder(.left) 139 | self.leftBorder.frame.size.width = self.widthForBorder(.left) 140 | self.leftBorder.frame.size.height = self.frame.height - leftInsets.0 - leftInsets.1 141 | self.leftBorder.frame.origin.y = leftInsets.0 142 | 143 | let rightInsets = self.insetsForBorder(.right) 144 | self.rightBorder.frame.size.width = self.widthForBorder(.right) 145 | self.rightBorder.frame.size.height = self.frame.height - rightInsets.0 - rightInsets.1 146 | self.rightBorder.frame.origin.x = self.frame.width - self.rightBorder.frame.size.width 147 | self.rightBorder.frame.origin.y = rightInsets.0 148 | 149 | CATransaction.commit() 150 | } 151 | 152 | override func layoutSublayers() { 153 | super.layoutSublayers() 154 | self.topBorder.zPosition = CGFloat(self.sublayers?.count ?? 0) 155 | self.leftBorder.zPosition = self.topBorder.zPosition 156 | self.bottomBorder.zPosition = self.topBorder.zPosition 157 | self.rightBorder.zPosition = self.topBorder.zPosition 158 | 159 | CATransaction.begin() 160 | CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions) 161 | 162 | for (i, shadow) in self.shadowLayers.enumerated() { 163 | shadow.frame.origin.y = self.frame.size.height + (i.f / UIScreen.main.scale) 164 | shadow.frame.size.width = self.bottomBorder.frame.size.width 165 | shadow.frame.size.height = 1 / UIScreen.main.scale 166 | } 167 | 168 | CATransaction.commit() 169 | } 170 | 171 | 172 | // MARK: Utils 173 | 174 | func colorForBorder(_ border: Border) -> UIColor { 175 | if let value = self.borderColors[border] { 176 | return value 177 | } 178 | for (key, value) in self.borderColors { 179 | if key.contains(border) { 180 | return value 181 | } 182 | } 183 | return 0xDEDEDE.color 184 | } 185 | 186 | func widthForBorder(_ border: Border) -> CGFloat { 187 | if let value = self.borderWidths[border] { 188 | return value 189 | } 190 | for (key, value) in self.borderWidths { 191 | if key.contains(border) { 192 | return value 193 | } 194 | } 195 | return 1 / UIScreen.main.scale 196 | } 197 | 198 | func insetsForBorder(_ border: Border) -> Insets { 199 | if let value = self.borderInsets[border] { 200 | return value 201 | } 202 | for (key, value) in self.borderInsets { 203 | if key.contains(border) { 204 | return value 205 | } 206 | } 207 | return (0, 0) 208 | } 209 | } 210 | 211 | 212 | // MARK: - UIView Extension 213 | 214 | extension UIView { 215 | var borderedLayer: BorderedLayer? { 216 | return self.layer as? BorderedLayer 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Utils/Snap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Snap.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 01/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// Ceil to snap pixel 12 | func snap(_ x: CGFloat) -> CGFloat { 13 | let scale = UIScreen.main.scale 14 | return ceil(x * scale) / scale 15 | } 16 | 17 | func snap(_ point: CGPoint) -> CGPoint { 18 | return CGPoint(x: snap(point.x), y: snap(point.y)) 19 | } 20 | 21 | func snap(_ size: CGSize) -> CGSize { 22 | return CGSize(width: snap(size.width), height: snap(size.height)) 23 | } 24 | 25 | func snap(_ rect: CGRect) -> CGRect { 26 | return CGRect(origin: snap(rect.origin), size: snap(rect.size)) 27 | } 28 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Utils/String+BoundingRect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+BoundingRect.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 01/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension String { 12 | func boundingRect(with size: CGSize, attributes: [NSAttributedStringKey: Any]) -> CGRect { 13 | let options: NSStringDrawingOptions = [.usesLineFragmentOrigin, .usesFontLeading] 14 | let rect = self.boundingRect(with: size, options: options, attributes: attributes, context: nil) 15 | return snap(rect) 16 | } 17 | 18 | func size(thatFits size: CGSize, font: UIFont, maximumNumberOfLines: Int = 0) -> CGSize { 19 | let attributes: [NSAttributedStringKey: Any] = [.font: font] 20 | var size = self.boundingRect(with: size, attributes: attributes).size 21 | if maximumNumberOfLines > 0 { 22 | size.height = min(size.height, CGFloat(maximumNumberOfLines) * font.lineHeight) 23 | } 24 | return size 25 | } 26 | 27 | func width(with font: UIFont, maximumNumberOfLines: Int = 0) -> CGFloat { 28 | let size = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) 29 | return self.size(thatFits: size, font: font, maximumNumberOfLines: maximumNumberOfLines).width 30 | } 31 | 32 | func height(thatFitsWidth width: CGFloat, font: UIFont, maximumNumberOfLines: Int = 0) -> CGFloat { 33 | let size = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude) 34 | return self.size(thatFits: size, font: font, maximumNumberOfLines: maximumNumberOfLines).height 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/ViewControllers/ArticleListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 01/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | import ReactorKit 12 | import RxDataSources 13 | import RxSwift 14 | import UICollectionViewFlexLayout 15 | 16 | final class ArticleListViewController: UIViewController, View { 17 | 18 | // MARK: Properties 19 | 20 | var disposeBag = DisposeBag() 21 | lazy var dataSource = self.createDataSource() 22 | let articleSectionDelegate: ArticleSectionDelegate 23 | 24 | 25 | // MARK: UI 26 | 27 | let refreshControl: UIRefreshControl = UIRefreshControl().then { 28 | $0.layer.zPosition = -999 29 | } 30 | let collectionView: UICollectionView = UICollectionView( 31 | frame: .zero, 32 | collectionViewLayout: UICollectionViewFlexLayout() 33 | ).then { 34 | $0.backgroundColor = .clear 35 | $0.alwaysBounceVertical = true 36 | } 37 | 38 | 39 | // MARK: Initializing 40 | 41 | init(reactor: ArticleListViewReactor, articleSectionDelegate: ArticleSectionDelegate) { 42 | defer { self.reactor = reactor } 43 | self.articleSectionDelegate = articleSectionDelegate 44 | super.init(nibName: nil, bundle: nil) 45 | self.title = "Articles" 46 | self.articleSectionDelegate.registerReusables(to: self.collectionView) 47 | } 48 | 49 | required convenience init(coder aDecoder: NSCoder) { 50 | fatalError("init(coder:) has not been implemented") 51 | } 52 | 53 | private func createDataSource() -> RxCollectionViewSectionedReloadDataSource { 54 | return .init( 55 | configureCell: { [weak self] dataSource, collectionView, indexPath, sectionItem in 56 | guard let `self` = self else { return collectionView.emptyCell(for: indexPath) } 57 | switch sectionItem { 58 | case let .articleCard(sectionReactor, item): 59 | return self.articleSectionDelegate.cell( 60 | collectionView: collectionView, 61 | indexPath: indexPath, 62 | sectionReactor: sectionReactor, 63 | sectionItem: item 64 | ) 65 | } 66 | }, 67 | configureSupplementaryView: { [weak self] dataSource, collectionView, kind, indexPath in 68 | guard let `self` = self else { return collectionView.emptyView(for: indexPath, kind: kind) } 69 | switch dataSource[indexPath] { 70 | case let .articleCard(sectionReactor, item): 71 | return self.articleSectionDelegate.supplementaryView( 72 | collectionView: collectionView, 73 | kind: kind, 74 | indexPath: indexPath, 75 | sectionReactor: sectionReactor, 76 | sectionItem: item 77 | ) 78 | } 79 | } 80 | ) 81 | } 82 | 83 | 84 | // MARK: View Lifecycle 85 | 86 | override func viewDidLoad() { 87 | super.viewDidLoad() 88 | self.view.backgroundColor = 0xEDEDED.color 89 | self.view.addSubview(self.collectionView) 90 | self.collectionView.refreshControl = self.refreshControl 91 | 92 | self.collectionView.snp.makeConstraints { make in 93 | make.edges.equalToSuperview() 94 | } 95 | } 96 | 97 | 98 | // MARK: Binding 99 | 100 | func bind(reactor: ArticleListViewReactor) { 101 | // Action 102 | self.rx.viewDidLoad 103 | .map { Reactor.Action.refresh } 104 | .bind(to: reactor.action) 105 | .disposed(by: self.disposeBag) 106 | 107 | self.refreshControl.rx.controlEvent(.valueChanged) 108 | .map { Reactor.Action.refresh } 109 | .bind(to: reactor.action) 110 | .disposed(by: self.disposeBag) 111 | 112 | // State 113 | reactor.state.map { $0.isRefreshing } 114 | .distinctUntilChanged() 115 | .bind(to: self.refreshControl.rx.isRefreshing) 116 | .disposed(by: self.disposeBag) 117 | 118 | reactor.state.map { $0.sections } 119 | .bind(to: self.collectionView.rx.items(dataSource: self.dataSource)) 120 | .disposed(by: self.disposeBag) 121 | 122 | // View 123 | self.collectionView.rx 124 | .setDelegate(self) 125 | .disposed(by: self.disposeBag) 126 | } 127 | } 128 | 129 | extension ArticleListViewController: UICollectionViewDelegateFlexLayout { 130 | // section padding 131 | func collectionView( 132 | _ collectionView: UICollectionView, 133 | layout collectionViewLayout: UICollectionViewFlexLayout, 134 | paddingForSectionAt section: Int 135 | ) -> UIEdgeInsets { 136 | return .init(top: 10, left: 10, bottom: 10, right: 10) 137 | } 138 | 139 | // section spacing 140 | func collectionView( 141 | _ collectionView: UICollectionView, 142 | layout collectionViewLayout: UICollectionViewFlexLayout, 143 | verticalSpacingBetweenSectionAt section: Int, 144 | and nextSection: Int 145 | ) -> CGFloat { 146 | return 10 147 | } 148 | 149 | // item spacing 150 | func collectionView( 151 | _ collectionView: UICollectionView, 152 | layout collectionViewLayout: UICollectionViewFlexLayout, 153 | verticalSpacingBetweenItemAt indexPath: IndexPath, 154 | and nextIndexPath: IndexPath 155 | ) -> CGFloat { 156 | switch (self.dataSource[indexPath], self.dataSource[nextIndexPath]) { 157 | case let (.articleCard(_, item), .articleCard(_, nextItem)): 158 | return self.articleSectionDelegate.cellVerticalSpacing( 159 | collectionView: collectionView, 160 | layout: collectionViewLayout, 161 | sectionItem: item, 162 | nextSectionItem: nextItem 163 | ) 164 | } 165 | } 166 | 167 | // item margin 168 | func collectionView( 169 | _ collectionView: UICollectionView, 170 | layout collectionViewLayout: UICollectionViewFlexLayout, 171 | marginForItemAt indexPath: IndexPath 172 | ) -> UIEdgeInsets { 173 | switch self.dataSource[indexPath] { 174 | case let .articleCard(_, item): 175 | return self.articleSectionDelegate.cellMargin( 176 | collectionView: collectionView, 177 | layout: collectionViewLayout, 178 | indexPath: indexPath, 179 | sectionItem: item 180 | ) 181 | } 182 | } 183 | 184 | // item padding 185 | func collectionView( 186 | _ collectionView: UICollectionView, 187 | layout collectionViewLayout: UICollectionViewFlexLayout, 188 | paddingForItemAt indexPath: IndexPath 189 | ) -> UIEdgeInsets { 190 | switch self.dataSource[indexPath] { 191 | case let .articleCard(_, item): 192 | return self.articleSectionDelegate.cellPadding( 193 | collectionView: collectionView, 194 | layout: collectionViewLayout, 195 | indexPath: indexPath, 196 | sectionItem: item 197 | ) 198 | } 199 | } 200 | 201 | // item size 202 | func collectionView( 203 | _ collectionView: UICollectionView, 204 | layout collectionViewLayout: UICollectionViewFlexLayout, 205 | sizeForItemAt indexPath: IndexPath 206 | ) -> CGSize { 207 | switch self.dataSource[indexPath] { 208 | case let .articleCard(_, item): 209 | return self.articleSectionDelegate.cellSize( 210 | collectionView: collectionView, 211 | layout: collectionViewLayout, 212 | indexPath: indexPath, 213 | sectionItem: item 214 | ) 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/ViewControllers/ArticleListViewReactor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleListViewReactor.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 01/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import ReactorKit 10 | import RxSwift 11 | 12 | final class ArticleListViewReactor: Reactor { 13 | enum Action { 14 | case refresh 15 | } 16 | 17 | enum Mutation { 18 | case setRefreshing(Bool) 19 | case setArticles([Article]) 20 | } 21 | 22 | struct State { 23 | var isRefreshing: Bool = false 24 | fileprivate var articleSectionReactors: [ArticleSectionReactor] = [] 25 | var sections: [ArticleListViewSection] { 26 | return self.articleSectionReactors.map(ArticleListViewSection.article) 27 | } 28 | } 29 | 30 | let initialState: State 31 | fileprivate let articleService: ArticleServiceType 32 | fileprivate let articleSectionReactorFactory: (Article) -> ArticleSectionReactor 33 | 34 | init( 35 | articleService: ArticleServiceType, 36 | articleSectionReactorFactory: @escaping (Article) -> ArticleSectionReactor 37 | ) { 38 | defer { _ = self.state } 39 | self.articleService = articleService 40 | self.articleSectionReactorFactory = articleSectionReactorFactory 41 | self.initialState = State() 42 | } 43 | 44 | func mutate(action: Action) -> Observable { 45 | switch action { 46 | case .refresh: 47 | guard self.currentState.isRefreshing == false else { return .empty() } 48 | return .concat([ 49 | Observable.just(Mutation.setRefreshing(true)), 50 | self.articleService.articles().map(Mutation.setArticles), 51 | Observable.just(Mutation.setRefreshing(false)), 52 | ]) 53 | } 54 | } 55 | 56 | func reduce(state: State, mutation: Mutation) -> State { 57 | var state = state 58 | switch mutation { 59 | case let .setRefreshing(isRefreshing): 60 | state.isRefreshing = isRefreshing 61 | return state 62 | 63 | case let .setArticles(articles): 64 | state.articleSectionReactors = articles.map(self.articleSectionReactorFactory) 65 | return state 66 | } 67 | } 68 | 69 | func transform(state: Observable) -> Observable { 70 | return state.with(section: \.articleSectionReactors) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/ViewControllers/ArticleViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleViewController.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 07/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | import ReactorKit 12 | import RxDataSources 13 | import RxSwift 14 | import UICollectionViewFlexLayout 15 | 16 | final class ArticleViewController: UIViewController, View { 17 | 18 | // MARK: Properties 19 | 20 | var disposeBag = DisposeBag() 21 | lazy var dataSource = self.createDataSource() 22 | let articleSectionDelegate: ArticleSectionDelegate 23 | 24 | 25 | // MARK: UI 26 | 27 | let collectionView: UICollectionView = UICollectionView( 28 | frame: .zero, 29 | collectionViewLayout: UICollectionViewFlexLayout() 30 | ).then { 31 | $0.backgroundColor = .clear 32 | $0.alwaysBounceVertical = true 33 | } 34 | 35 | 36 | // MARK: Initializing 37 | 38 | init(reactor: ArticleViewReactor, articleSectionDelegate: ArticleSectionDelegate) { 39 | defer { self.reactor = reactor } 40 | self.articleSectionDelegate = articleSectionDelegate 41 | super.init(nibName: nil, bundle: nil) 42 | self.title = "Article" 43 | self.articleSectionDelegate.registerReusables(to: self.collectionView) 44 | } 45 | 46 | required convenience init(coder aDecoder: NSCoder) { 47 | fatalError("init(coder:) has not been implemented") 48 | } 49 | 50 | private func createDataSource() -> RxCollectionViewSectionedReloadDataSource { 51 | return .init( 52 | configureCell: { [weak self] dataSource, collectionView, indexPath, sectionItem in 53 | guard let `self` = self else { return collectionView.emptyCell(for: indexPath) } 54 | return self.articleSectionDelegate.cell( 55 | collectionView: collectionView, 56 | indexPath: indexPath, 57 | sectionReactor: sectionItem.sectionReactor, 58 | sectionItem: sectionItem.sectionItem 59 | ) 60 | }, 61 | configureSupplementaryView: { [weak self] dataSource, collectionView, kind, indexPath in 62 | guard let `self` = self else { return collectionView.emptyView(for: indexPath, kind: kind) } 63 | return self.articleSectionDelegate.supplementaryView( 64 | collectionView: collectionView, 65 | kind: kind, 66 | indexPath: indexPath, 67 | sectionReactor: dataSource[indexPath].sectionReactor, 68 | sectionItem: dataSource[indexPath].sectionItem 69 | ) 70 | } 71 | ) 72 | } 73 | 74 | 75 | // MARK: View Lifecycle 76 | 77 | override func viewDidLoad() { 78 | super.viewDidLoad() 79 | self.view.backgroundColor = 0xEDEDED.color 80 | self.view.addSubview(self.collectionView) 81 | 82 | self.collectionView.snp.makeConstraints { make in 83 | make.edges.equalToSuperview() 84 | } 85 | } 86 | 87 | 88 | // MARK: Binding 89 | 90 | func bind(reactor: ArticleViewReactor) { 91 | // State 92 | reactor.state.map { $0.authorName } 93 | .distinctUntilChanged() 94 | .map { $0.components(separatedBy: " ").first ?? $0 } 95 | .map { "\($0)'s Article" } 96 | .bind(to: self.rx.title) 97 | .disposed(by: self.disposeBag) 98 | 99 | reactor.state.map { $0.sections } 100 | .bind(to: self.collectionView.rx.items(dataSource: self.dataSource)) 101 | .disposed(by: self.disposeBag) 102 | 103 | // View 104 | self.collectionView.rx 105 | .setDelegate(self) 106 | .disposed(by: self.disposeBag) 107 | } 108 | } 109 | 110 | extension ArticleViewController: UICollectionViewDelegateFlexLayout { 111 | // section padding 112 | func collectionView( 113 | _ collectionView: UICollectionView, 114 | layout collectionViewLayout: UICollectionViewFlexLayout, 115 | paddingForSectionAt section: Int 116 | ) -> UIEdgeInsets { 117 | return .init(top: 10, left: 10, bottom: 10, right: 10) 118 | } 119 | 120 | // section spacing 121 | func collectionView( 122 | _ collectionView: UICollectionView, 123 | layout collectionViewLayout: UICollectionViewFlexLayout, 124 | verticalSpacingBetweenSectionAt section: Int, 125 | and nextSection: Int 126 | ) -> CGFloat { 127 | return 10 128 | } 129 | 130 | // item spacing 131 | func collectionView( 132 | _ collectionView: UICollectionView, 133 | layout collectionViewLayout: UICollectionViewFlexLayout, 134 | verticalSpacingBetweenItemAt indexPath: IndexPath, 135 | and nextIndexPath: IndexPath 136 | ) -> CGFloat { 137 | return self.articleSectionDelegate.cellVerticalSpacing( 138 | collectionView: collectionView, 139 | layout: collectionViewLayout, 140 | sectionItem: self.dataSource[indexPath].sectionItem, 141 | nextSectionItem: self.dataSource[nextIndexPath].sectionItem 142 | ) 143 | } 144 | 145 | // item margin 146 | func collectionView( 147 | _ collectionView: UICollectionView, 148 | layout collectionViewLayout: UICollectionViewFlexLayout, 149 | marginForItemAt indexPath: IndexPath 150 | ) -> UIEdgeInsets { 151 | return self.articleSectionDelegate.cellMargin( 152 | collectionView: collectionView, 153 | layout: collectionViewLayout, 154 | indexPath: indexPath, 155 | sectionItem: self.dataSource[indexPath].sectionItem 156 | ) 157 | } 158 | 159 | // item padding 160 | func collectionView( 161 | _ collectionView: UICollectionView, 162 | layout collectionViewLayout: UICollectionViewFlexLayout, 163 | paddingForItemAt indexPath: IndexPath 164 | ) -> UIEdgeInsets { 165 | return self.articleSectionDelegate.cellPadding( 166 | collectionView: collectionView, 167 | layout: collectionViewLayout, 168 | indexPath: indexPath, 169 | sectionItem: self.dataSource[indexPath].sectionItem 170 | ) 171 | } 172 | 173 | // item size 174 | func collectionView( 175 | _ collectionView: UICollectionView, 176 | layout collectionViewLayout: UICollectionViewFlexLayout, 177 | sizeForItemAt indexPath: IndexPath 178 | ) -> CGSize { 179 | return self.articleSectionDelegate.cellSize( 180 | collectionView: collectionView, 181 | layout: collectionViewLayout, 182 | indexPath: indexPath, 183 | sectionItem: self.dataSource[indexPath].sectionItem 184 | ) 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/ViewControllers/ArticleViewReactor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleViewReactor.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 07/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import ReactorKit 10 | import RxSwift 11 | 12 | final class ArticleViewReactor: Reactor { 13 | enum Action { 14 | } 15 | 16 | enum Mutation { 17 | } 18 | 19 | struct State { 20 | var authorName: String 21 | var articleSectionReactor: ArticleSectionReactor 22 | var sections: [ArticleViewSection] { 23 | return [.article(self.articleSectionReactor)] 24 | } 25 | } 26 | 27 | let initialState: State 28 | 29 | init( 30 | article: Article, 31 | articleSectionReactorFactory: (Article) -> ArticleSectionReactor 32 | ) { 33 | defer { _ = self.state } 34 | self.initialState = State( 35 | authorName: article.author.name, 36 | articleSectionReactor: articleSectionReactorFactory(article) 37 | ) 38 | } 39 | 40 | func transform(state: Observable) -> Observable { 41 | return state.with(section: \.articleSectionReactor) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Views/ArticleCardAuthorCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleCardAuthorCell.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 01/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | import ReactorKit 12 | import RxSwift 13 | 14 | final class ArticleCardAuthorCell: BaseArticleCardSectionItemCell, View { 15 | 16 | // MARK: Constants 17 | 18 | fileprivate enum Metric { 19 | static let avatarViewSize = 30.f 20 | static let nameLabelLeft = 10.f 21 | } 22 | 23 | fileprivate enum Font { 24 | static let nameLabel = UIFont.boldSystemFont(ofSize: 13) 25 | } 26 | 27 | fileprivate enum Color { 28 | } 29 | 30 | 31 | // MARK: UI 32 | 33 | let avatarView: UIImageView = UIImageView().then { 34 | $0.backgroundColor = 0xCCCCCC.color 35 | $0.layer.cornerRadius = Metric.avatarViewSize / 2 36 | $0.clipsToBounds = true 37 | } 38 | let nameLabel: UILabel = UILabel().then { 39 | $0.font = Font.nameLabel 40 | } 41 | 42 | 43 | // MARK: Initializing 44 | 45 | override init(frame: CGRect) { 46 | super.init(frame: frame) 47 | self.contentView.addSubview(self.avatarView) 48 | self.contentView.addSubview(self.nameLabel) 49 | } 50 | 51 | required init?(coder aDecoder: NSCoder) { 52 | fatalError("init(coder:) has not been implemented") 53 | } 54 | 55 | 56 | // MARK: Binding 57 | 58 | func bind(reactor: ArticleCardAuthorCellReactor) { 59 | // State 60 | reactor.state.map { $0.name } 61 | .distinctUntilChanged() 62 | .bind(to: self.nameLabel.rx.text) 63 | .disposed(by: self.disposeBag) 64 | 65 | // View 66 | reactor.state.map { _ in } 67 | .bind(to: self.rx.setNeedsLayout) 68 | .disposed(by: self.disposeBag) 69 | } 70 | 71 | 72 | // MARK: Layout 73 | 74 | override func layoutSubviews() { 75 | super.layoutSubviews() 76 | 77 | self.avatarView.width = Metric.avatarViewSize 78 | self.avatarView.height = Metric.avatarViewSize 79 | 80 | self.nameLabel.sizeToFit() 81 | self.nameLabel.left = self.avatarView.right + Metric.nameLabelLeft 82 | self.nameLabel.centerY = self.avatarView.centerY 83 | } 84 | 85 | class func size(width: CGFloat, reactor: ArticleCardAuthorCellReactor) -> CGSize { 86 | return CGSize(width: width, height: Metric.avatarViewSize) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Views/ArticleCardAuthorCellReactor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleCardAuthorCellReactor.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 01/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import ReactorKit 10 | import RxSwift 11 | 12 | final class ArticleCardAuthorCellReactor: Reactor { 13 | enum Action { 14 | } 15 | 16 | enum Mutation { 17 | } 18 | 19 | struct State { 20 | var name: String 21 | } 22 | 23 | let initialState: State 24 | 25 | init(user: User) { 26 | defer { _ = self.state } 27 | self.initialState = State(name: user.name) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Views/ArticleCardCommentCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleCardCommentCell.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 01/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | import ReactorKit 12 | import RxSwift 13 | 14 | final class ArticleCardCommentCell: BaseArticleCardSectionItemCell, View { 15 | 16 | // MARK: Constants 17 | 18 | fileprivate enum Metric { 19 | static let avatarViewSize = 20.f 20 | static let nameLabelLeft = 6.f 21 | static let textLabelLeft = 4.f 22 | } 23 | 24 | fileprivate enum Font { 25 | static let nameLabel = UIFont.boldSystemFont(ofSize: 12) 26 | static let textLabel = UIFont.systemFont(ofSize: 12) 27 | } 28 | 29 | fileprivate enum Color { 30 | } 31 | 32 | 33 | // MARK: UI 34 | 35 | let avatarView: UIImageView = UIImageView().then { 36 | $0.backgroundColor = 0xCCCCCC.color 37 | $0.layer.cornerRadius = Metric.avatarViewSize / 2 38 | $0.clipsToBounds = true 39 | } 40 | let nameLabel = UILabel().then { $0.font = Font.nameLabel } 41 | let textLabel = UILabel().then { $0.font = Font.textLabel } 42 | 43 | 44 | // MARK: Initializing 45 | 46 | override init(frame: CGRect) { 47 | super.init(frame: frame) 48 | self.contentView.addSubview(self.avatarView) 49 | self.contentView.addSubview(self.nameLabel) 50 | self.contentView.addSubview(self.textLabel) 51 | } 52 | 53 | required init?(coder aDecoder: NSCoder) { 54 | fatalError("init(coder:) has not been implemented") 55 | } 56 | 57 | 58 | // MARK: Binding 59 | 60 | func bind(reactor: ArticleCardCommentCellReactor) { 61 | // State 62 | reactor.state.map { $0.name } 63 | .distinctUntilChanged() 64 | .bind(to: self.nameLabel.rx.text) 65 | .disposed(by: self.disposeBag) 66 | 67 | reactor.state.map { $0.text } 68 | .distinctUntilChanged() 69 | .bind(to: self.textLabel.rx.text) 70 | .disposed(by: self.disposeBag) 71 | 72 | // View 73 | reactor.state.map { _ in } 74 | .bind(to: self.rx.setNeedsLayout) 75 | .disposed(by: self.disposeBag) 76 | } 77 | 78 | 79 | // MARK: Layout 80 | 81 | override func layoutSubviews() { 82 | super.layoutSubviews() 83 | self.avatarView.width = Metric.avatarViewSize 84 | self.avatarView.height = Metric.avatarViewSize 85 | 86 | self.nameLabel.sizeToFit() 87 | self.nameLabel.left = self.avatarView.right + Metric.nameLabelLeft 88 | self.nameLabel.centerY = self.avatarView.centerY - 1 89 | 90 | self.textLabel.sizeToFit() 91 | self.textLabel.left = self.nameLabel.right + Metric.textLabelLeft 92 | self.textLabel.centerY = self.nameLabel.centerY 93 | self.textLabel.width = min(self.textLabel.width, self.contentView.width - self.textLabel.left) 94 | } 95 | 96 | class func size(width: CGFloat, reactor: ArticleCardCommentCellReactor) -> CGSize { 97 | return CGSize(width: width, height: Metric.avatarViewSize) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Views/ArticleCardCommentCellReactor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleCardCommentCellReactor.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 01/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import ReactorKit 10 | import RxSwift 11 | 12 | final class ArticleCardCommentCellReactor: Reactor { 13 | enum Action { 14 | } 15 | 16 | enum Mutation { 17 | } 18 | 19 | struct State { 20 | var name: String 21 | var text: String 22 | } 23 | 24 | let initialState: State 25 | 26 | init(comment: Comment) { 27 | defer { _ = self.state } 28 | self.initialState = State(name: comment.author.name, text: comment.text) 29 | } 30 | 31 | func mutate(action: Action) -> Observable { 32 | return .empty() 33 | } 34 | 35 | func reduce(state: State, mutation: Mutation) -> State { 36 | return state 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Views/ArticleCardReactionCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleCardReactionCell.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 01/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | import ReactorKit 12 | import RxSwift 13 | 14 | final class ArticleCardReactionCell: BaseArticleCardSectionItemCell, View { 15 | 16 | // MARK: Constants 17 | 18 | fileprivate enum Metric { 19 | } 20 | 21 | fileprivate enum Font { 22 | static let commentButtonTitle = UIFont.systemFont(ofSize: 13) 23 | } 24 | 25 | fileprivate enum Color { 26 | } 27 | 28 | 29 | // MARK: UI 30 | 31 | let commentButton: UIButton = UIButton(type: .system).then { 32 | $0.titleLabel?.font = Font.commentButtonTitle 33 | $0.setTitle("Create a comment", for: .normal) 34 | } 35 | 36 | 37 | // MARK: Initializing 38 | 39 | override init(frame: CGRect) { 40 | super.init(frame: frame) 41 | self.contentView.addSubview(self.commentButton) 42 | } 43 | 44 | required init?(coder aDecoder: NSCoder) { 45 | fatalError("init(coder:) has not been implemented") 46 | } 47 | 48 | 49 | // MARK: Binding 50 | 51 | func bind(reactor: ArticleCardReactionCellReactor) { 52 | // Action 53 | self.commentButton.rx.tap 54 | .map { Reactor.Action.createComment } 55 | .bind(to: reactor.action) 56 | .disposed(by: self.disposeBag) 57 | 58 | // View 59 | reactor.state.map { _ in } 60 | .bind(to: self.rx.setNeedsLayout) 61 | .disposed(by: self.disposeBag) 62 | } 63 | 64 | 65 | // MARK: Layout 66 | 67 | override func layoutSubviews() { 68 | super.layoutSubviews() 69 | self.commentButton.sizeToFit() 70 | self.commentButton.centerY = self.contentView.height / 2 71 | } 72 | 73 | class func size(width: CGFloat, reactor: ArticleCardReactionCellReactor) -> CGSize { 74 | return CGSize(width: width, height: snap(Font.commentButtonTitle.lineHeight)) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Views/ArticleCardReactionCellReactor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleCardReactionCellReactor.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 01/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import ReactorKit 10 | import RxSwift 11 | 12 | final class ArticleCardReactionCellReactor: Reactor { 13 | enum Action { 14 | case createComment 15 | } 16 | 17 | enum Mutation { 18 | } 19 | 20 | struct State { 21 | var articleID: String 22 | } 23 | 24 | let initialState: State 25 | 26 | init(articleID: String) { 27 | defer { _ = self.state } 28 | self.initialState = State(articleID: articleID) 29 | } 30 | 31 | func mutate(action: Action) -> Observable { 32 | switch action { 33 | case .createComment: 34 | return Observable.create { [weak self] observer in 35 | if let articleID = self?.currentState.articleID { 36 | let comment = Comment.random(articleID: articleID) 37 | Comment.event.onNext(.create(comment)) 38 | } 39 | observer.onCompleted() 40 | return Disposables.create() 41 | } 42 | } 43 | } 44 | 45 | func reduce(state: State, mutation: Mutation) -> State { 46 | return state 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Views/ArticleCardTextCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleCardTextCell.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 01/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | import ReactorKit 12 | import RxSwift 13 | 14 | final class ArticleCardTextCell: BaseArticleCardSectionItemCell, View { 15 | 16 | // MARK: Constants 17 | 18 | fileprivate enum Metric { 19 | } 20 | 21 | fileprivate enum Font { 22 | static let textLabel = UIFont.systemFont(ofSize: 13) 23 | } 24 | 25 | fileprivate enum Color { 26 | } 27 | 28 | 29 | // MARK: UI 30 | 31 | let textLabel: UILabel = UILabel().then { 32 | $0.numberOfLines = 0 33 | $0.font = Font.textLabel 34 | } 35 | 36 | 37 | // MARK: Initializing 38 | 39 | override init(frame: CGRect) { 40 | super.init(frame: frame) 41 | self.contentView.addSubview(self.textLabel) 42 | } 43 | 44 | required init?(coder aDecoder: NSCoder) { 45 | fatalError("init(coder:) has not been implemented") 46 | } 47 | 48 | 49 | // MARK: Binding 50 | 51 | func bind(reactor: ArticleCardTextCellReactor) { 52 | // State 53 | reactor.state.map { $0.text } 54 | .distinctUntilChanged() 55 | .bind(to: self.textLabel.rx.text) 56 | .disposed(by: self.disposeBag) 57 | 58 | // View 59 | reactor.state.map { _ in } 60 | .bind(to: self.rx.setNeedsLayout) 61 | .disposed(by: self.disposeBag) 62 | } 63 | 64 | 65 | // MARK: Layout 66 | 67 | override func layoutSubviews() { 68 | super.layoutSubviews() 69 | self.textLabel.frame = self.contentView.bounds 70 | } 71 | 72 | class func size(width: CGFloat, reactor: ArticleCardTextCellReactor) -> CGSize { 73 | let height = reactor.currentState.text.height(thatFitsWidth: width, font: Font.textLabel) 74 | return CGSize(width: width, height: height) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Views/ArticleCardTextCellReactor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleCardTextCellReactor.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 01/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import ReactorKit 10 | import RxSwift 11 | 12 | final class ArticleCardTextCellReactor: Reactor { 13 | enum Action { 14 | } 15 | 16 | enum Mutation { 17 | } 18 | 19 | struct State { 20 | var text: String 21 | } 22 | 23 | let initialState: State 24 | 25 | init(text: String) { 26 | defer { _ = self.state } 27 | self.initialState = State(text: text) 28 | } 29 | 30 | func mutate(action: Action) -> Observable { 31 | return .empty() 32 | } 33 | 34 | func reduce(state: State, mutation: Mutation) -> State { 35 | return state 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Views/BaseArticleCardSectionItemCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArticleCardSectionItemCell.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 07/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | import RxCocoa 12 | import RxSwift 13 | 14 | class BaseArticleCardSectionItemCell: UICollectionViewCell { 15 | 16 | // MARK: Properties 17 | 18 | var disposeBag = DisposeBag() 19 | 20 | 21 | // MARK: UI 22 | 23 | let tapGestureRecognizer = UITapGestureRecognizer() 24 | 25 | 26 | // MARK: Initializing 27 | 28 | override init(frame: CGRect) { 29 | super.init(frame: frame) 30 | self.contentView.addGestureRecognizer(self.tapGestureRecognizer) 31 | } 32 | 33 | required init?(coder aDecoder: NSCoder) { 34 | fatalError("init(coder:) has not been implemented") 35 | } 36 | } 37 | 38 | extension Reactive where Base: BaseArticleCardSectionItemCell { 39 | var tap: ControlEvent { 40 | let source = self.base.tapGestureRecognizer.rx.event.map { _ in } 41 | return ControlEvent(events: source) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Sources/Views/CollectionBorderedBackgroundView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionBorderedBackgroundView.swift 3 | // ArticleFeed 4 | // 5 | // Created by Suyeol Jeon on 12/09/2017. 6 | // Copyright © 2017 Suyeol Jeon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class CollectionBorderedBackgroundView: UICollectionReusableView { 12 | override class var layerClass: AnyClass { 13 | return BorderedLayer.self 14 | } 15 | 16 | override func layoutSublayers(of layer: CALayer) { 17 | super.layoutSublayers(of: layer) 18 | self.layer.frame.size = self.bounds.size 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/ArticleFeed/Supporting Files/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '9.0' 2 | 3 | target 'ArticleFeed' do 4 | use_frameworks! 5 | 6 | # Architecture 7 | pod 'ReactorKit' 8 | pod 'SectionReactor', :path => '../../SectionReactor.podspec' 9 | 10 | # Rx 11 | pod 'RxSwift' 12 | pod 'RxCocoa' 13 | pod 'RxDataSources' 14 | pod 'Differentiator' 15 | pod 'RxViewController' 16 | 17 | # Misc. 18 | pod 'Then' 19 | pod 'ManualLayout' 20 | pod 'SnapKit' 21 | pod 'ReusableKit' 22 | pod 'UICollectionViewFlexLayout' 23 | pod 'CGFloatLiteral' 24 | pod 'SwiftyColor' 25 | pod 'LoremIpsum' 26 | pod 'URLNavigator' 27 | end 28 | -------------------------------------------------------------------------------- /Examples/ArticleFeed/README.md: -------------------------------------------------------------------------------- 1 | # ArticleFeed 2 | 3 | ![articlefeed](https://user-images.githubusercontent.com/931655/30236985-8c0c0db4-94f5-11e7-8598-f33e06bbc7c1.png) 4 | 5 | The each parts of the screenshot are separated cells. A single section item contains partial information of the article. A single section contains complete information of an article. Each cell and reactor can have its corresponding reactor. 6 | 7 | ``` 8 | .---- ArticleListViewController -----. ArticleListViewReactor ([Article]) 9 | | | | 10 | | .-----section 0------------------. | |- SectionReactor (Article) 11 | | | [0, 0] ArticleCardAuthorCell | | | |- CellReactor (Article.author) 12 | | | [0, 1] ArticleCardTextCell | | | |- CellReactor (Article.text) 13 | | | [0, 2] ArticleCardReactionCell | | | |- CellReactor (Article.id) 14 | | | [0, 3] ArticleCardCommentCell | | | |- CellReactor (Article.comments[0]) 15 | | | [0, 4] ArticleCardCommentCell | | | '- CellReactor (Article.comments[1]) 16 | | | | | | 17 | | |-----section 1------------------| | |- SectionReactor (Article) 18 | | | [1, 0] ArticleCardAuthorCell | | | |- CellReactor (Article.author) 19 | | | [1, 1] ArticleCardTextCell | | | |- CellReactor (Article.text) 20 | | | [1, 2] ArticleCardReactionCell | | | |- CellReactor (Article.id) 21 | | | [1, 3] ArticleCardCommentCell | | | |- CellReactor (Article.comments[0]) 22 | | | [1, 4] ArticleCardCommentCell | | | '- CellReactor (Article.comments[1]) 23 | | | | | | 24 | | |-----section 2------------------| | '- SectionReactor (Article) 25 | | | [2, 0] ArticleCardAuthorCell | | |- CellReactor (Article.author) 26 | | | [2, 1] ArticleCardTextCell | | '- CellReactor (Article.text) 27 | | | ... | | 28 | | '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' | 29 | | | 30 | '------------------------------------' 31 | ``` 32 | 33 | ## How to Run 34 | 35 | ```swift 36 | $ pod install 37 | $ open ArticleFeed.xcworkspace 38 | ``` 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Suyeol Jeon (xoul.kr) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "ReactorKit", 6 | "repositoryURL": "https://github.com/ReactorKit/ReactorKit.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "d960b9537a0fdc869340a2c1af17959bab832570", 10 | "version": "2.0.1" 11 | } 12 | }, 13 | { 14 | "package": "RxDataSources", 15 | "repositoryURL": "https://github.com/RxSwiftCommunity/RxDataSources.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "a18cee01c9ee55f04d0c7eb15c77a96c3648c88e", 19 | "version": "4.0.1" 20 | } 21 | }, 22 | { 23 | "package": "RxExpect", 24 | "repositoryURL": "https://github.com/devxoul/RxExpect.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "c3a3bb3d46ee831582c6619ecc48cda1cdbff890", 28 | "version": "2.0.0" 29 | } 30 | }, 31 | { 32 | "package": "RxSwift", 33 | "repositoryURL": "https://github.com/ReactiveX/RxSwift.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "b3e888b4972d9bc76495dd74d30a8c7fad4b9395", 37 | "version": "5.0.1" 38 | } 39 | } 40 | ] 41 | }, 42 | "version": 1 43 | } 44 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SectionReactor", 7 | platforms: [ 8 | .iOS(.v8), .tvOS(.v9) 9 | ], 10 | products: [ 11 | .library(name: "SectionReactor", targets: ["SectionReactor"]), 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/ReactorKit/ReactorKit.git", .upToNextMajor(from: "2.0.0")), 15 | .package(url: "https://github.com/RxSwiftCommunity/RxDataSources.git", .upToNextMajor(from: "4.0.0")), 16 | .package(url: "https://github.com/devxoul/RxExpect.git", .upToNextMajor(from: "2.0.0")), 17 | ], 18 | targets: [ 19 | .target(name: "SectionReactor", dependencies: ["ReactorKit", "RxDataSources"]), 20 | .testTarget(name: "SectionReactorTests", dependencies: ["SectionReactor", "RxExpect"]) 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SectionReactor 2 | 3 | ![Swift](https://img.shields.io/badge/Swift-5.0-orange.svg) 4 | [![CocoaPods](http://img.shields.io/cocoapods/v/SectionReactor.svg)](https://cocoapods.org/pods/SectionReactor) 5 | [![Build Status](https://travis-ci.org/devxoul/SectionReactor.svg?branch=master)](https://travis-ci.org/devxoul/SectionReactor) 6 | [![Codecov](https://img.shields.io/codecov/c/github/devxoul/SectionReactor.svg)](https://codecov.io/gh/devxoul/SectionReactor) 7 | 8 | SectionReactor is a ReactorKit extension for managing table view and collection view sections with RxDataSources. 9 | 10 | ## Getting Started 11 | 12 | This is a draft. I have no idea how would I explain this concept 🤦‍♂️ It would be better to see the [ArticleFeed](https://github.com/devxoul/SectionReactor/tree/master/Examples/ArticleFeed) example. 13 | 14 | **ArticleViewSection.swift** 15 | 16 | ```swift 17 | enum ArticleViewSection: SectionModelType { 18 | case article(ArticleSectionReactor) 19 | 20 | var items: [ArticleViewSection] { 21 | switch self { 22 | case let .article(sectionReactor): 23 | return sectionReactor.currentState.sectionItems 24 | } 25 | } 26 | } 27 | ``` 28 | 29 | **ArticleSectionReactor.swift** 30 | 31 | ```swift 32 | import SectionReactor 33 | 34 | final class ArticleSectionItem: SectionReactor { 35 | struct State: SectionReactorState { 36 | var sectionItems: [ArticleSectionItem] 37 | } 38 | } 39 | ``` 40 | 41 | **ArticleListViewReactor.swift** 42 | 43 | ```swift 44 | final class ArticleListViewReactor: Reactor { 45 | struct State { 46 | var articleSectionReactors: [ArticleSectionReactor] 47 | var sections: [ArticleViewSection] { 48 | return self.articleSectionReactors.map(ArticleViewSection.article) 49 | } 50 | } 51 | 52 | func transform(state: Observable) -> Observable { 53 | return state.merge(sections: [ 54 | { $0.articleSectionReactors }, 55 | ]) 56 | } 57 | } 58 | ``` 59 | 60 | ## Dependencies 61 | 62 | * [ReactorKit](https://github.com/ReactorKit/ReactorKit) >= 0 63 | * [RxDataSources](https://github.com/RxSwiftCommunity/RxDataSources) >= 2.0 64 | 65 | ## Installation 66 | 67 | ```ruby 68 | pod 'SectionReactor' 69 | ``` 70 | 71 | ## License 72 | 73 | SectionReactor is under MIT license. See the [LICENSE](LICENSE) for more info. 74 | -------------------------------------------------------------------------------- /SectionReactor.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "SectionReactor" 3 | s.version = "1.0.0" 4 | s.summary = "A ReactorKit extension for managing table view and collection view sections with RxDataSources." 5 | s.homepage = "https://github.com/devxoul/SectionReactor" 6 | s.license = { :type => "MIT", :file => "LICENSE" } 7 | s.author = { "Suyeol Jeon" => "devxoul@gmail.com" } 8 | s.source = { :git => "https://github.com/devxoul/SectionReactor.git", 9 | :tag => s.version.to_s } 10 | s.source_files = "Sources/**/*.{swift,h,m}" 11 | s.frameworks = "Foundation" 12 | s.dependency "ReactorKit", ">= 2.0.0" 13 | s.dependency "RxDataSources", ">= 4.0.0" 14 | s.swift_version = "5.0" 15 | 16 | s.ios.deployment_target = "8.0" 17 | s.tvos.deployment_target = "9.0" 18 | end 19 | -------------------------------------------------------------------------------- /Sources/SectionReactor/Empty.swift: -------------------------------------------------------------------------------- 1 | #if os(iOS) || os(tvOS) 2 | import UIKit 3 | 4 | public extension UICollectionView { 5 | func emptyCell(for indexPath: IndexPath) -> UICollectionViewCell { 6 | let identifier = "SectionReactor.UICollectionView.emptyCell" 7 | self.register(UICollectionViewCell.self, forCellWithReuseIdentifier: identifier) 8 | let cell = self.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) 9 | cell.isHidden = true 10 | return cell 11 | } 12 | 13 | func emptyView(for indexPath: IndexPath, kind: String) -> UICollectionReusableView { 14 | let identifier = "SectionReactor.UICollectionView.emptyView" 15 | self.register(UICollectionReusableView.self, forSupplementaryViewOfKind: kind, withReuseIdentifier: identifier) 16 | let view = self.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: identifier, for: indexPath) 17 | view.isHidden = true 18 | return view 19 | } 20 | } 21 | 22 | public extension UITableView { 23 | func emptyCell(for indexPath: IndexPath) -> UITableViewCell { 24 | let identifier = "SectionReactor.UITableView.emptyCell" 25 | self.register(UITableViewCell.self, forCellReuseIdentifier: identifier) 26 | let cell = self.dequeueReusableCell(withIdentifier: identifier, for: indexPath) 27 | cell.isHidden = true 28 | return cell 29 | } 30 | } 31 | #endif 32 | -------------------------------------------------------------------------------- /Sources/SectionReactor/SectionDelegateType.swift: -------------------------------------------------------------------------------- 1 | public protocol SectionDelegateType { 2 | associatedtype SectionReactor: _SectionReactor 3 | typealias SectionItem = SectionReactor.State.SectionItem 4 | } 5 | -------------------------------------------------------------------------------- /Sources/SectionReactor/SectionReactor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import ReactorKit 3 | import RxDataSources 4 | import RxSwift 5 | 6 | public protocol SectionReactorState { 7 | associatedtype SectionItem 8 | var sectionItems: [SectionItem] { get } 9 | } 10 | 11 | public typealias _SectionReactor = SectionReactor 12 | public protocol SectionReactor: Reactor where State: SectionReactorState { 13 | } 14 | 15 | public extension ObservableType { 16 | func with( 17 | section sectionReactorsClosure: @escaping (State) -> [Section]? 18 | ) -> Observable where Element == State, Section: SectionReactor { 19 | return self.flatMapLatest { state -> Observable in 20 | guard let sectionReactors = sectionReactorsClosure(state) else { return .just(state) } 21 | let sectionStates = Observable.merge(sectionReactors.map { $0.state.skip(1) }) 22 | return Observable.merge(.just(state), sectionStates.map { _ in state }) 23 | } 24 | } 25 | 26 | func with( 27 | section sectionReactorClosure: @escaping (State) -> Section? 28 | ) -> Observable where Element == State, Section: SectionReactor { 29 | return self.with(section: { state in sectionReactorClosure(state).map { [$0] } }) 30 | } 31 | } 32 | 33 | public extension ObservableType { 34 | func with( 35 | section sectionReactorsKeyPath: KeyPath 36 | ) -> Observable where Element == State, Section: SectionReactor { 37 | return self.with(section: { state in state[keyPath: sectionReactorsKeyPath] }) 38 | } 39 | 40 | func with( 41 | section sectionReactorsKeyPath: KeyPath 42 | ) -> Observable where Element == State, Section: SectionReactor { 43 | return self.with(section: { state in state[keyPath: sectionReactorsKeyPath] }) 44 | } 45 | } 46 | 47 | public extension ObservableType { 48 | func with( 49 | section sectionReactorKeyPath: KeyPath 50 | ) -> Observable where Element == State, Section: SectionReactor { 51 | return self.with(section: { state in state[keyPath: sectionReactorKeyPath] }) 52 | } 53 | 54 | func with( 55 | section sectionReactorKeyPath: KeyPath 56 | ) -> Observable where Element == State, Section: SectionReactor { 57 | return self.with(section: { state in state[keyPath: sectionReactorKeyPath] }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Tests/SectionReactorTests/Fixture.swift: -------------------------------------------------------------------------------- 1 | import ReactorKit 2 | import RxDataSources 3 | import RxSwift 4 | @testable import SectionReactor 5 | 6 | struct ArticleListSection: SectionModelType { 7 | var sectionReactor: ArticleSectionReactor 8 | var items: [ArticleListSectionItem] { 9 | return self.sectionReactor.currentState.sectionItems 10 | } 11 | 12 | init(sectionReactor: ArticleSectionReactor) { 13 | self.sectionReactor = sectionReactor 14 | } 15 | 16 | init(original: ArticleListSection, items: [ArticleListSectionItem]) { 17 | self = original 18 | self.sectionReactor = original.sectionReactor 19 | } 20 | } 21 | 22 | typealias ArticleListSectionItem = Void 23 | 24 | final class ArticleListViewReactor: Reactor { 25 | enum Action { 26 | case setTitle(String) 27 | case setSingleSectionReactor(ArticleSectionReactor) 28 | case appendMultipleSectionReactor(ArticleSectionReactor) 29 | } 30 | 31 | enum Mutation { 32 | case setTitle(String) 33 | case setSingleSectionReactor(ArticleSectionReactor) 34 | case appendMultipleSectionReactor(ArticleSectionReactor) 35 | } 36 | 37 | struct State { 38 | var title: String? 39 | 40 | var singleSectionReactor: ArticleSectionReactor? = nil { 41 | didSet { 42 | self.updateSections() 43 | } 44 | } 45 | var multipleSectionReactors: [ArticleSectionReactor]? { 46 | didSet { 47 | self.updateSections() 48 | } 49 | } 50 | var sections: [ArticleListSection] = [] 51 | 52 | private mutating func updateSections() { 53 | self.sections.removeAll() 54 | if let singleSectionReactor = self.singleSectionReactor { 55 | self.sections.append(ArticleListSection(sectionReactor: singleSectionReactor)) 56 | } 57 | self.sections += (self.multipleSectionReactors ?? []).map(ArticleListSection.init) 58 | } 59 | } 60 | 61 | let initialState: State 62 | 63 | init() { 64 | defer { _ = self.state } 65 | self.initialState = State() 66 | } 67 | 68 | func mutate(action: Action) -> Observable { 69 | switch action { 70 | case let .setTitle(title): 71 | return .just(.setTitle(title)) 72 | 73 | case let .setSingleSectionReactor(sectionReactor): 74 | return .just(.setSingleSectionReactor(sectionReactor)) 75 | 76 | case let .appendMultipleSectionReactor(sectionReactor): 77 | return .just(.appendMultipleSectionReactor(sectionReactor)) 78 | } 79 | } 80 | 81 | func reduce(state: State, mutation: Mutation) -> State { 82 | var newState = state 83 | switch mutation { 84 | case let .setTitle(title): 85 | newState.title = title 86 | 87 | case let .setSingleSectionReactor(sectionReactor): 88 | newState.singleSectionReactor = sectionReactor 89 | 90 | case let .appendMultipleSectionReactor(sectionReactor): 91 | var multipleSectionReactors = newState.multipleSectionReactors ?? [] 92 | multipleSectionReactors.append(sectionReactor) 93 | newState.multipleSectionReactors = multipleSectionReactors 94 | } 95 | return newState 96 | } 97 | 98 | func transform(state: Observable) -> Observable { 99 | return state 100 | .with(section: \State.singleSectionReactor) 101 | .with(section: \State.multipleSectionReactors) 102 | } 103 | } 104 | 105 | 106 | // MARK: - Section Reactor 107 | 108 | final class ArticleSectionReactor: SectionReactor { 109 | enum Action { 110 | case append 111 | } 112 | 113 | enum Mutation { 114 | case appendArticle 115 | } 116 | 117 | struct State: SectionReactorState { 118 | var sectionItems: [ArticleListSectionItem] = [] 119 | } 120 | 121 | let initialState: State 122 | 123 | init() { 124 | defer { _ = self.state } 125 | self.initialState = State() 126 | } 127 | 128 | func mutate(action: Action) -> Observable { 129 | switch action { 130 | case .append: 131 | return .just(.appendArticle) 132 | } 133 | } 134 | 135 | func reduce(state: State, mutation: Mutation) -> State { 136 | var state = state 137 | switch mutation { 138 | case .appendArticle: 139 | state.sectionItems.append(ArticleListSectionItem()) 140 | } 141 | return state 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Tests/SectionReactorTests/SectionReactorTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import ReactorKit 3 | import RxDataSources 4 | import RxExpect 5 | import RxSwift 6 | import RxTest 7 | @testable import SectionReactor 8 | 9 | final class SectionReactorTests: XCTestCase { 10 | func testInitialState() { 11 | let reactor = ArticleListViewReactor() 12 | XCTAssertEqual(reactor.currentState.sections.count, 0) 13 | } 14 | 15 | /// SectionReactors should not affect other mutation and state. 16 | func testTitle_changes_whenReceivesSetTitleAction() { 17 | // given 18 | let reactor = ArticleListViewReactor() 19 | 20 | // when 21 | reactor.action.onNext(.setTitle("Hello, Single!")) 22 | 23 | // then 24 | XCTAssertEqual(reactor.currentState.title, "Hello, Single!") 25 | } 26 | 27 | func testSections_containsSingleSectionReactor_whenReceivesSetSingleSectionReactorAction() { 28 | // given 29 | let reactor = ArticleListViewReactor() 30 | let sectionReactor = ArticleSectionReactor() 31 | 32 | // when 33 | reactor.action.onNext(.setSingleSectionReactor(sectionReactor)) 34 | 35 | // then 36 | XCTAssertEqual(reactor.currentState.sections.count, 1) 37 | XCTAssertEqual(reactor.currentState.sections.first?.items.count, 0) 38 | XCTAssertTrue(reactor.currentState.sections.first?.sectionReactor === sectionReactor) 39 | } 40 | 41 | func testSections_containsMultipleSectionReactors_whenReceivesAppendMultipleSectionReactorAction() { 42 | // given 43 | let reactor = ArticleListViewReactor() 44 | let sectionReactor = ArticleSectionReactor() 45 | 46 | // when 47 | reactor.action.onNext(.appendMultipleSectionReactor(sectionReactor)) 48 | 49 | // then 50 | XCTAssertEqual(reactor.currentState.sections.count, 1) 51 | XCTAssertEqual(reactor.currentState.sections.first?.items.count, 0) 52 | XCTAssertTrue(reactor.currentState.sections.first?.sectionReactor === sectionReactor) 53 | } 54 | 55 | func testSingleSectionReactor_createsSectionItems_whenReceivesAppendAction() { 56 | // given 57 | let test = RxExpect() 58 | let reactor = test.retain(ArticleListViewReactor()) 59 | let sectionReactor = ArticleSectionReactor() 60 | reactor.action.onNext(.setSingleSectionReactor(sectionReactor)) 61 | 62 | // when 63 | test.input(sectionReactor.action, [ 64 | .next(100, .append), 65 | .next(200, .append), 66 | ]) 67 | 68 | // then 69 | test.assert(reactor.state) { events in 70 | let events = events.in(100...) 71 | XCTAssertEqual(events.count, 2) 72 | XCTAssertEqual(events.last?.value.element?.sections.count, 1) 73 | XCTAssertEqual(events.last?.value.element?.sections.first?.items.count, 2) 74 | } 75 | } 76 | 77 | func testMultipleSectionReactors_createsSectionItems_whenReceivesAppendAction() { 78 | // given 79 | let test = RxExpect() 80 | let reactor = test.retain(ArticleListViewReactor()) 81 | let sectionReactors: [ArticleSectionReactor] = [.init(), .init()] 82 | sectionReactors.forEach { reactor.action.onNext(.appendMultipleSectionReactor($0)) } 83 | 84 | // when 85 | test.input(sectionReactors[0].action, [ 86 | .next(100, .append), 87 | ]) 88 | test.input(sectionReactors[1].action, [ 89 | .next(100, .append), 90 | .next(200, .append), 91 | .next(300, .append), 92 | ]) 93 | 94 | // then 95 | test.assert(reactor.state) { events in 96 | let events = events.in(100...) 97 | XCTAssertEqual(events.count, 4) 98 | XCTAssertEqual(events.last?.value.element?.sections.count, 2) 99 | XCTAssertEqual(events.last?.value.element?.sections[0].items.count, 1) 100 | XCTAssertEqual(events.last?.value.element?.sections[1].items.count, 3) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "Tests/" 3 | 4 | coverage: 5 | status: 6 | project: no 7 | patch: no 8 | changes: no 9 | --------------------------------------------------------------------------------