├── .gitignore ├── Example ├── Images │ ├── detail.png │ └── search.png ├── PrexSample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── PrexSample.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── PrexSample │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── GitHub │ │ ├── GitHub.Pagination.swift │ │ ├── GitHub.Repository.swift │ │ ├── GitHub.Response.swift │ │ ├── GitHub.Session.swift │ │ ├── GitHub.User.swift │ │ └── GitHub.swift │ ├── Info.plist │ └── View │ │ ├── Detail │ │ ├── DetailViewController.Prex.swift │ │ ├── DetailViewController.swift │ │ └── DetailViewController.xib │ │ └── Search │ │ ├── SearchCell.swift │ │ ├── SearchCell.xib │ │ ├── SearchViewController.Prex.swift │ │ ├── SearchViewController.swift │ │ └── SearchViewController.xib ├── PrexSampleTests │ ├── Info.plist │ ├── Mock │ │ └── MockGitHubSession.swift │ └── TestCase │ │ └── Search │ │ └── SearchPresenterTestCase.swift └── README.md ├── Images ├── Prex.sketch ├── data-flow.png ├── func-test.png ├── playground.png ├── prex.png └── reflection-test.png ├── LICENSE ├── Package.swift ├── Prex.playground ├── Contents.swift └── contents.xcplayground ├── Prex.podspec ├── Prex.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── Prex-iOS.xcscheme │ ├── Prex-macOS.xcscheme │ ├── Prex-tvOS.xcscheme │ ├── Prex-watchOS.xcscheme │ └── PrexTests.xcscheme ├── Prex.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── README.md ├── Resources ├── Info-iOS.plist ├── Info-macOS.plist ├── Info-tvOS.plist ├── Info-watchOS.plist └── PrexTests-Info.plist ├── Sources └── Prex │ ├── Flux │ ├── Action.swift │ ├── Dispatcher.swift │ ├── Flux.swift │ ├── Store.swift │ └── View.swift │ ├── Mutation.swift │ ├── Presenter.swift │ ├── Prex.h │ ├── State.swift │ ├── StateChange.swift │ ├── Subscription.swift │ └── internal │ └── PubSub.swift ├── Tests ├── LinuxMain.swift └── PrexTests │ ├── Components.swift │ ├── DispatcherTests.swift │ ├── PresenterTests.swift │ ├── PubSubTests.swift │ ├── StoreTests.swift │ ├── ValueChangeTests.swift │ └── XCTestManifests.swift └── Tools ├── Prex.xctemplate ├── Common │ └── ___FILEBASENAME___.Prex.swift ├── Default │ ├── ___FILEBASENAME___Presenter.swift │ └── ___FILEBASENAME___ViewController.swift ├── PresenterSubclass │ ├── ___FILEBASENAME___Presenter.swift │ └── ___FILEBASENAME___ViewController.swift ├── TemplateIcon.png ├── TemplateIcon@2x.png ├── TemplateInfo.plist ├── WithStoryboard │ └── ___FILEBASENAME___ViewController.storyboard └── WithXIB │ └── ___FILEBASENAME___ViewController.xib ├── README.md └── install-xcode-template.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | .DS_Store 6 | 7 | ## Build generated 8 | build/ 9 | DerivedData/ 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata/ 21 | 22 | ## Other 23 | *.moved-aside 24 | *.xccheckout 25 | *.xcscmblueprint 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | .build/ 44 | 45 | # CocoaPods 46 | # 47 | # We recommend against adding the Pods directory to your .gitignore. However 48 | # you should judge for yourself, the pros and cons are mentioned at: 49 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 50 | # 51 | # Pods/ 52 | 53 | # Carthage 54 | # 55 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 56 | # Carthage/Checkouts 57 | 58 | Carthage/Build 59 | 60 | # fastlane 61 | # 62 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 63 | # screenshots whenever they are needed. 64 | # For more information about the recommended setup visit: 65 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 66 | 67 | fastlane/report.xml 68 | fastlane/Preview.html 69 | fastlane/screenshots/**/*.png 70 | fastlane/test_output 71 | -------------------------------------------------------------------------------- /Example/Images/detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marty-suzuki/Prex/d6c198fc96e0155f658be55a1798aaa287acdc1b/Example/Images/detail.png -------------------------------------------------------------------------------- /Example/Images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marty-suzuki/Prex/d6c198fc96e0155f658be55a1798aaa287acdc1b/Example/Images/search.png -------------------------------------------------------------------------------- /Example/PrexSample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 370656F0215F19B1003852EF /* Prex.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 370656EE215F19AC003852EF /* Prex.framework */; }; 11 | 370656F1215F19B1003852EF /* Prex.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 370656EE215F19AC003852EF /* Prex.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 12 | 370656F5215F1A57003852EF /* GitHub.Repository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370656F4215F1A57003852EF /* GitHub.Repository.swift */; }; 13 | 370656F7215F1A5F003852EF /* GitHub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370656F6215F1A5F003852EF /* GitHub.swift */; }; 14 | 370656F9215F1A68003852EF /* GitHub.User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370656F8215F1A68003852EF /* GitHub.User.swift */; }; 15 | 370656FB215F2026003852EF /* GitHub.Pagination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370656FA215F2026003852EF /* GitHub.Pagination.swift */; }; 16 | 370656FD215F2082003852EF /* GitHub.Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370656FC215F2082003852EF /* GitHub.Response.swift */; }; 17 | 370656FF215F20C4003852EF /* GitHub.Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370656FE215F20C4003852EF /* GitHub.Session.swift */; }; 18 | 37065702215F2419003852EF /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37065700215F2419003852EF /* SearchViewController.swift */; }; 19 | 37065703215F2419003852EF /* SearchViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 37065701215F2419003852EF /* SearchViewController.xib */; }; 20 | 37065707215F2C0C003852EF /* SearchViewController.Prex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37065706215F2C0C003852EF /* SearchViewController.Prex.swift */; }; 21 | 3706570A215F317E003852EF /* SearchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37065708215F317E003852EF /* SearchCell.swift */; }; 22 | 3706570B215F317E003852EF /* SearchCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 37065709215F317E003852EF /* SearchCell.xib */; }; 23 | 3706570F215F4D80003852EF /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3706570D215F4D80003852EF /* DetailViewController.swift */; }; 24 | 37065710215F4D80003852EF /* DetailViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3706570E215F4D80003852EF /* DetailViewController.xib */; }; 25 | 37065712215F4E1E003852EF /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 37065711215F4E1E003852EF /* WebKit.framework */; }; 26 | 37065714215F54A5003852EF /* DetailViewController.Prex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37065713215F54A5003852EF /* DetailViewController.Prex.swift */; }; 27 | 37932E12215F19640072DB23 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37932E11215F19640072DB23 /* AppDelegate.swift */; }; 28 | 37932E17215F19640072DB23 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 37932E15215F19640072DB23 /* Main.storyboard */; }; 29 | 37932E19215F19650072DB23 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 37932E18215F19650072DB23 /* Assets.xcassets */; }; 30 | 37932E1C215F19650072DB23 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 37932E1A215F19650072DB23 /* LaunchScreen.storyboard */; }; 31 | 37DEBE89215F8446008FF17B /* SearchPresenterTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DEBE88215F8446008FF17B /* SearchPresenterTestCase.swift */; }; 32 | 37DEBE8B215F847E008FF17B /* Prex.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 37DEBE8A215F847E008FF17B /* Prex.framework */; }; 33 | 37DEBE90215F8A75008FF17B /* MockGitHubSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DEBE8F215F8A75008FF17B /* MockGitHubSession.swift */; }; 34 | /* End PBXBuildFile section */ 35 | 36 | /* Begin PBXContainerItemProxy section */ 37 | 37932E23215F19650072DB23 /* PBXContainerItemProxy */ = { 38 | isa = PBXContainerItemProxy; 39 | containerPortal = 37932E06215F19640072DB23 /* Project object */; 40 | proxyType = 1; 41 | remoteGlobalIDString = 37932E0D215F19640072DB23; 42 | remoteInfo = PrexSample; 43 | }; 44 | /* End PBXContainerItemProxy section */ 45 | 46 | /* Begin PBXCopyFilesBuildPhase section */ 47 | 370656F2215F19B1003852EF /* Embed Frameworks */ = { 48 | isa = PBXCopyFilesBuildPhase; 49 | buildActionMask = 2147483647; 50 | dstPath = ""; 51 | dstSubfolderSpec = 10; 52 | files = ( 53 | 370656F1215F19B1003852EF /* Prex.framework in Embed Frameworks */, 54 | ); 55 | name = "Embed Frameworks"; 56 | runOnlyForDeploymentPostprocessing = 0; 57 | }; 58 | /* End PBXCopyFilesBuildPhase section */ 59 | 60 | /* Begin PBXFileReference section */ 61 | 370656EE215F19AC003852EF /* Prex.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Prex.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 62 | 370656F4215F1A57003852EF /* GitHub.Repository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHub.Repository.swift; sourceTree = ""; }; 63 | 370656F6215F1A5F003852EF /* GitHub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHub.swift; sourceTree = ""; }; 64 | 370656F8215F1A68003852EF /* GitHub.User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHub.User.swift; sourceTree = ""; }; 65 | 370656FA215F2026003852EF /* GitHub.Pagination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHub.Pagination.swift; sourceTree = ""; }; 66 | 370656FC215F2082003852EF /* GitHub.Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHub.Response.swift; sourceTree = ""; }; 67 | 370656FE215F20C4003852EF /* GitHub.Session.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHub.Session.swift; sourceTree = ""; }; 68 | 37065700215F2419003852EF /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; 69 | 37065701215F2419003852EF /* SearchViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SearchViewController.xib; sourceTree = ""; }; 70 | 37065706215F2C0C003852EF /* SearchViewController.Prex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.Prex.swift; sourceTree = ""; }; 71 | 37065708215F317E003852EF /* SearchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCell.swift; sourceTree = ""; }; 72 | 37065709215F317E003852EF /* SearchCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SearchCell.xib; sourceTree = ""; }; 73 | 3706570D215F4D80003852EF /* DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; 74 | 3706570E215F4D80003852EF /* DetailViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DetailViewController.xib; sourceTree = ""; }; 75 | 37065711215F4E1E003852EF /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; 76 | 37065713215F54A5003852EF /* DetailViewController.Prex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.Prex.swift; sourceTree = ""; }; 77 | 37932E0E215F19640072DB23 /* PrexSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PrexSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 78 | 37932E11215F19640072DB23 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 79 | 37932E16215F19640072DB23 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 80 | 37932E18215F19650072DB23 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 81 | 37932E1B215F19650072DB23 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 82 | 37932E1D215F19650072DB23 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 83 | 37932E22215F19650072DB23 /* PrexSampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PrexSampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 84 | 37932E28215F19650072DB23 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 85 | 37DEBE88215F8446008FF17B /* SearchPresenterTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchPresenterTestCase.swift; sourceTree = ""; }; 86 | 37DEBE8A215F847E008FF17B /* Prex.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Prex.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 87 | 37DEBE8F215F8A75008FF17B /* MockGitHubSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGitHubSession.swift; sourceTree = ""; }; 88 | /* End PBXFileReference section */ 89 | 90 | /* Begin PBXFrameworksBuildPhase section */ 91 | 37932E0B215F19640072DB23 /* Frameworks */ = { 92 | isa = PBXFrameworksBuildPhase; 93 | buildActionMask = 2147483647; 94 | files = ( 95 | 37065712215F4E1E003852EF /* WebKit.framework in Frameworks */, 96 | 370656F0215F19B1003852EF /* Prex.framework in Frameworks */, 97 | ); 98 | runOnlyForDeploymentPostprocessing = 0; 99 | }; 100 | 37932E1F215F19650072DB23 /* Frameworks */ = { 101 | isa = PBXFrameworksBuildPhase; 102 | buildActionMask = 2147483647; 103 | files = ( 104 | 37DEBE8B215F847E008FF17B /* Prex.framework in Frameworks */, 105 | ); 106 | runOnlyForDeploymentPostprocessing = 0; 107 | }; 108 | /* End PBXFrameworksBuildPhase section */ 109 | 110 | /* Begin PBXGroup section */ 111 | 370656ED215F19AC003852EF /* Frameworks */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | 37DEBE8A215F847E008FF17B /* Prex.framework */, 115 | 37065711215F4E1E003852EF /* WebKit.framework */, 116 | 370656EE215F19AC003852EF /* Prex.framework */, 117 | ); 118 | name = Frameworks; 119 | sourceTree = ""; 120 | }; 121 | 370656F3215F1A45003852EF /* GitHub */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 370656F6215F1A5F003852EF /* GitHub.swift */, 125 | 370656F4215F1A57003852EF /* GitHub.Repository.swift */, 126 | 370656F8215F1A68003852EF /* GitHub.User.swift */, 127 | 370656FA215F2026003852EF /* GitHub.Pagination.swift */, 128 | 370656FC215F2082003852EF /* GitHub.Response.swift */, 129 | 370656FE215F20C4003852EF /* GitHub.Session.swift */, 130 | ); 131 | path = GitHub; 132 | sourceTree = ""; 133 | }; 134 | 37065704215F2426003852EF /* Search */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 37065700215F2419003852EF /* SearchViewController.swift */, 138 | 37065701215F2419003852EF /* SearchViewController.xib */, 139 | 37065706215F2C0C003852EF /* SearchViewController.Prex.swift */, 140 | 37065708215F317E003852EF /* SearchCell.swift */, 141 | 37065709215F317E003852EF /* SearchCell.xib */, 142 | ); 143 | path = Search; 144 | sourceTree = ""; 145 | }; 146 | 37065705215F2436003852EF /* View */ = { 147 | isa = PBXGroup; 148 | children = ( 149 | 3706570C215F4D6A003852EF /* Detail */, 150 | 37065704215F2426003852EF /* Search */, 151 | ); 152 | path = View; 153 | sourceTree = ""; 154 | }; 155 | 3706570C215F4D6A003852EF /* Detail */ = { 156 | isa = PBXGroup; 157 | children = ( 158 | 3706570D215F4D80003852EF /* DetailViewController.swift */, 159 | 3706570E215F4D80003852EF /* DetailViewController.xib */, 160 | 37065713215F54A5003852EF /* DetailViewController.Prex.swift */, 161 | ); 162 | path = Detail; 163 | sourceTree = ""; 164 | }; 165 | 37932E05215F19640072DB23 = { 166 | isa = PBXGroup; 167 | children = ( 168 | 37932E10215F19640072DB23 /* PrexSample */, 169 | 37932E25215F19650072DB23 /* PrexSampleTests */, 170 | 37932E0F215F19640072DB23 /* Products */, 171 | 370656ED215F19AC003852EF /* Frameworks */, 172 | ); 173 | sourceTree = ""; 174 | }; 175 | 37932E0F215F19640072DB23 /* Products */ = { 176 | isa = PBXGroup; 177 | children = ( 178 | 37932E0E215F19640072DB23 /* PrexSample.app */, 179 | 37932E22215F19650072DB23 /* PrexSampleTests.xctest */, 180 | ); 181 | name = Products; 182 | sourceTree = ""; 183 | }; 184 | 37932E10215F19640072DB23 /* PrexSample */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 370656F3215F1A45003852EF /* GitHub */, 188 | 37065705215F2436003852EF /* View */, 189 | 37932E11215F19640072DB23 /* AppDelegate.swift */, 190 | 37932E15215F19640072DB23 /* Main.storyboard */, 191 | 37932E18215F19650072DB23 /* Assets.xcassets */, 192 | 37932E1A215F19650072DB23 /* LaunchScreen.storyboard */, 193 | 37932E1D215F19650072DB23 /* Info.plist */, 194 | ); 195 | path = PrexSample; 196 | sourceTree = ""; 197 | }; 198 | 37932E25215F19650072DB23 /* PrexSampleTests */ = { 199 | isa = PBXGroup; 200 | children = ( 201 | 37B329AA215FF60B00287935 /* TestCase */, 202 | 37DEBE8C215F8A54008FF17B /* Mock */, 203 | 37932E28215F19650072DB23 /* Info.plist */, 204 | ); 205 | path = PrexSampleTests; 206 | sourceTree = ""; 207 | }; 208 | 37B329AA215FF60B00287935 /* TestCase */ = { 209 | isa = PBXGroup; 210 | children = ( 211 | 37DEBE87215F8316008FF17B /* Search */, 212 | ); 213 | path = TestCase; 214 | sourceTree = ""; 215 | }; 216 | 37DEBE87215F8316008FF17B /* Search */ = { 217 | isa = PBXGroup; 218 | children = ( 219 | 37DEBE88215F8446008FF17B /* SearchPresenterTestCase.swift */, 220 | ); 221 | path = Search; 222 | sourceTree = ""; 223 | }; 224 | 37DEBE8C215F8A54008FF17B /* Mock */ = { 225 | isa = PBXGroup; 226 | children = ( 227 | 37DEBE8F215F8A75008FF17B /* MockGitHubSession.swift */, 228 | ); 229 | path = Mock; 230 | sourceTree = ""; 231 | }; 232 | /* End PBXGroup section */ 233 | 234 | /* Begin PBXNativeTarget section */ 235 | 37932E0D215F19640072DB23 /* PrexSample */ = { 236 | isa = PBXNativeTarget; 237 | buildConfigurationList = 37932E2B215F19650072DB23 /* Build configuration list for PBXNativeTarget "PrexSample" */; 238 | buildPhases = ( 239 | 37932E0A215F19640072DB23 /* Sources */, 240 | 37932E0B215F19640072DB23 /* Frameworks */, 241 | 37932E0C215F19640072DB23 /* Resources */, 242 | 370656F2215F19B1003852EF /* Embed Frameworks */, 243 | ); 244 | buildRules = ( 245 | ); 246 | dependencies = ( 247 | ); 248 | name = PrexSample; 249 | productName = PrexSample; 250 | productReference = 37932E0E215F19640072DB23 /* PrexSample.app */; 251 | productType = "com.apple.product-type.application"; 252 | }; 253 | 37932E21215F19650072DB23 /* PrexSampleTests */ = { 254 | isa = PBXNativeTarget; 255 | buildConfigurationList = 37932E2E215F19650072DB23 /* Build configuration list for PBXNativeTarget "PrexSampleTests" */; 256 | buildPhases = ( 257 | 37932E1E215F19650072DB23 /* Sources */, 258 | 37932E1F215F19650072DB23 /* Frameworks */, 259 | 37932E20215F19650072DB23 /* Resources */, 260 | ); 261 | buildRules = ( 262 | ); 263 | dependencies = ( 264 | 37932E24215F19650072DB23 /* PBXTargetDependency */, 265 | ); 266 | name = PrexSampleTests; 267 | productName = PrexSampleTests; 268 | productReference = 37932E22215F19650072DB23 /* PrexSampleTests.xctest */; 269 | productType = "com.apple.product-type.bundle.unit-test"; 270 | }; 271 | /* End PBXNativeTarget section */ 272 | 273 | /* Begin PBXProject section */ 274 | 37932E06215F19640072DB23 /* Project object */ = { 275 | isa = PBXProject; 276 | attributes = { 277 | LastSwiftUpdateCheck = 1000; 278 | LastUpgradeCheck = 1000; 279 | ORGANIZATIONNAME = "marty-suzuki"; 280 | TargetAttributes = { 281 | 37932E0D215F19640072DB23 = { 282 | CreatedOnToolsVersion = 10.0; 283 | }; 284 | 37932E21215F19650072DB23 = { 285 | CreatedOnToolsVersion = 10.0; 286 | TestTargetID = 37932E0D215F19640072DB23; 287 | }; 288 | }; 289 | }; 290 | buildConfigurationList = 37932E09215F19640072DB23 /* Build configuration list for PBXProject "PrexSample" */; 291 | compatibilityVersion = "Xcode 9.3"; 292 | developmentRegion = en; 293 | hasScannedForEncodings = 0; 294 | knownRegions = ( 295 | en, 296 | Base, 297 | ); 298 | mainGroup = 37932E05215F19640072DB23; 299 | productRefGroup = 37932E0F215F19640072DB23 /* Products */; 300 | projectDirPath = ""; 301 | projectRoot = ""; 302 | targets = ( 303 | 37932E0D215F19640072DB23 /* PrexSample */, 304 | 37932E21215F19650072DB23 /* PrexSampleTests */, 305 | ); 306 | }; 307 | /* End PBXProject section */ 308 | 309 | /* Begin PBXResourcesBuildPhase section */ 310 | 37932E0C215F19640072DB23 /* Resources */ = { 311 | isa = PBXResourcesBuildPhase; 312 | buildActionMask = 2147483647; 313 | files = ( 314 | 3706570B215F317E003852EF /* SearchCell.xib in Resources */, 315 | 37065710215F4D80003852EF /* DetailViewController.xib in Resources */, 316 | 37932E1C215F19650072DB23 /* LaunchScreen.storyboard in Resources */, 317 | 37932E19215F19650072DB23 /* Assets.xcassets in Resources */, 318 | 37065703215F2419003852EF /* SearchViewController.xib in Resources */, 319 | 37932E17215F19640072DB23 /* Main.storyboard in Resources */, 320 | ); 321 | runOnlyForDeploymentPostprocessing = 0; 322 | }; 323 | 37932E20215F19650072DB23 /* Resources */ = { 324 | isa = PBXResourcesBuildPhase; 325 | buildActionMask = 2147483647; 326 | files = ( 327 | ); 328 | runOnlyForDeploymentPostprocessing = 0; 329 | }; 330 | /* End PBXResourcesBuildPhase section */ 331 | 332 | /* Begin PBXSourcesBuildPhase section */ 333 | 37932E0A215F19640072DB23 /* Sources */ = { 334 | isa = PBXSourcesBuildPhase; 335 | buildActionMask = 2147483647; 336 | files = ( 337 | 3706570F215F4D80003852EF /* DetailViewController.swift in Sources */, 338 | 37065707215F2C0C003852EF /* SearchViewController.Prex.swift in Sources */, 339 | 3706570A215F317E003852EF /* SearchCell.swift in Sources */, 340 | 370656F9215F1A68003852EF /* GitHub.User.swift in Sources */, 341 | 370656F5215F1A57003852EF /* GitHub.Repository.swift in Sources */, 342 | 37065702215F2419003852EF /* SearchViewController.swift in Sources */, 343 | 370656FD215F2082003852EF /* GitHub.Response.swift in Sources */, 344 | 370656FF215F20C4003852EF /* GitHub.Session.swift in Sources */, 345 | 370656F7215F1A5F003852EF /* GitHub.swift in Sources */, 346 | 37065714215F54A5003852EF /* DetailViewController.Prex.swift in Sources */, 347 | 37932E12215F19640072DB23 /* AppDelegate.swift in Sources */, 348 | 370656FB215F2026003852EF /* GitHub.Pagination.swift in Sources */, 349 | ); 350 | runOnlyForDeploymentPostprocessing = 0; 351 | }; 352 | 37932E1E215F19650072DB23 /* Sources */ = { 353 | isa = PBXSourcesBuildPhase; 354 | buildActionMask = 2147483647; 355 | files = ( 356 | 37DEBE89215F8446008FF17B /* SearchPresenterTestCase.swift in Sources */, 357 | 37DEBE90215F8A75008FF17B /* MockGitHubSession.swift in Sources */, 358 | ); 359 | runOnlyForDeploymentPostprocessing = 0; 360 | }; 361 | /* End PBXSourcesBuildPhase section */ 362 | 363 | /* Begin PBXTargetDependency section */ 364 | 37932E24215F19650072DB23 /* PBXTargetDependency */ = { 365 | isa = PBXTargetDependency; 366 | target = 37932E0D215F19640072DB23 /* PrexSample */; 367 | targetProxy = 37932E23215F19650072DB23 /* PBXContainerItemProxy */; 368 | }; 369 | /* End PBXTargetDependency section */ 370 | 371 | /* Begin PBXVariantGroup section */ 372 | 37932E15215F19640072DB23 /* Main.storyboard */ = { 373 | isa = PBXVariantGroup; 374 | children = ( 375 | 37932E16215F19640072DB23 /* Base */, 376 | ); 377 | name = Main.storyboard; 378 | sourceTree = ""; 379 | }; 380 | 37932E1A215F19650072DB23 /* LaunchScreen.storyboard */ = { 381 | isa = PBXVariantGroup; 382 | children = ( 383 | 37932E1B215F19650072DB23 /* Base */, 384 | ); 385 | name = LaunchScreen.storyboard; 386 | sourceTree = ""; 387 | }; 388 | /* End PBXVariantGroup section */ 389 | 390 | /* Begin XCBuildConfiguration section */ 391 | 37932E29215F19650072DB23 /* Debug */ = { 392 | isa = XCBuildConfiguration; 393 | buildSettings = { 394 | ALWAYS_SEARCH_USER_PATHS = NO; 395 | CLANG_ANALYZER_NONNULL = YES; 396 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 397 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 398 | CLANG_CXX_LIBRARY = "libc++"; 399 | CLANG_ENABLE_MODULES = YES; 400 | CLANG_ENABLE_OBJC_ARC = YES; 401 | CLANG_ENABLE_OBJC_WEAK = YES; 402 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 403 | CLANG_WARN_BOOL_CONVERSION = YES; 404 | CLANG_WARN_COMMA = YES; 405 | CLANG_WARN_CONSTANT_CONVERSION = YES; 406 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 407 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 408 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 409 | CLANG_WARN_EMPTY_BODY = YES; 410 | CLANG_WARN_ENUM_CONVERSION = YES; 411 | CLANG_WARN_INFINITE_RECURSION = YES; 412 | CLANG_WARN_INT_CONVERSION = YES; 413 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 414 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 415 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 416 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 417 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 418 | CLANG_WARN_STRICT_PROTOTYPES = YES; 419 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 420 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 421 | CLANG_WARN_UNREACHABLE_CODE = YES; 422 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 423 | CODE_SIGN_IDENTITY = "iPhone Developer"; 424 | COPY_PHASE_STRIP = NO; 425 | DEBUG_INFORMATION_FORMAT = dwarf; 426 | ENABLE_STRICT_OBJC_MSGSEND = YES; 427 | ENABLE_TESTABILITY = YES; 428 | GCC_C_LANGUAGE_STANDARD = gnu11; 429 | GCC_DYNAMIC_NO_PIC = NO; 430 | GCC_NO_COMMON_BLOCKS = YES; 431 | GCC_OPTIMIZATION_LEVEL = 0; 432 | GCC_PREPROCESSOR_DEFINITIONS = ( 433 | "DEBUG=1", 434 | "$(inherited)", 435 | ); 436 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 437 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 438 | GCC_WARN_UNDECLARED_SELECTOR = YES; 439 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 440 | GCC_WARN_UNUSED_FUNCTION = YES; 441 | GCC_WARN_UNUSED_VARIABLE = YES; 442 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 443 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 444 | MTL_FAST_MATH = YES; 445 | ONLY_ACTIVE_ARCH = YES; 446 | SDKROOT = iphoneos; 447 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 448 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 449 | }; 450 | name = Debug; 451 | }; 452 | 37932E2A215F19650072DB23 /* Release */ = { 453 | isa = XCBuildConfiguration; 454 | buildSettings = { 455 | ALWAYS_SEARCH_USER_PATHS = NO; 456 | CLANG_ANALYZER_NONNULL = YES; 457 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 458 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 459 | CLANG_CXX_LIBRARY = "libc++"; 460 | CLANG_ENABLE_MODULES = YES; 461 | CLANG_ENABLE_OBJC_ARC = YES; 462 | CLANG_ENABLE_OBJC_WEAK = YES; 463 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 464 | CLANG_WARN_BOOL_CONVERSION = YES; 465 | CLANG_WARN_COMMA = YES; 466 | CLANG_WARN_CONSTANT_CONVERSION = YES; 467 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 468 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 469 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 470 | CLANG_WARN_EMPTY_BODY = YES; 471 | CLANG_WARN_ENUM_CONVERSION = YES; 472 | CLANG_WARN_INFINITE_RECURSION = YES; 473 | CLANG_WARN_INT_CONVERSION = YES; 474 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 475 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 476 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 477 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 478 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 479 | CLANG_WARN_STRICT_PROTOTYPES = YES; 480 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 481 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 482 | CLANG_WARN_UNREACHABLE_CODE = YES; 483 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 484 | CODE_SIGN_IDENTITY = "iPhone Developer"; 485 | COPY_PHASE_STRIP = NO; 486 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 487 | ENABLE_NS_ASSERTIONS = NO; 488 | ENABLE_STRICT_OBJC_MSGSEND = YES; 489 | GCC_C_LANGUAGE_STANDARD = gnu11; 490 | GCC_NO_COMMON_BLOCKS = YES; 491 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 492 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 493 | GCC_WARN_UNDECLARED_SELECTOR = YES; 494 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 495 | GCC_WARN_UNUSED_FUNCTION = YES; 496 | GCC_WARN_UNUSED_VARIABLE = YES; 497 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 498 | MTL_ENABLE_DEBUG_INFO = NO; 499 | MTL_FAST_MATH = YES; 500 | SDKROOT = iphoneos; 501 | SWIFT_COMPILATION_MODE = wholemodule; 502 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 503 | VALIDATE_PRODUCT = YES; 504 | }; 505 | name = Release; 506 | }; 507 | 37932E2C215F19650072DB23 /* Debug */ = { 508 | isa = XCBuildConfiguration; 509 | buildSettings = { 510 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 511 | CODE_SIGN_STYLE = Manual; 512 | DEVELOPMENT_TEAM = ""; 513 | INFOPLIST_FILE = PrexSample/Info.plist; 514 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 515 | LD_RUNPATH_SEARCH_PATHS = ( 516 | "$(inherited)", 517 | "@executable_path/Frameworks", 518 | ); 519 | PRODUCT_BUNDLE_IDENTIFIER = "jp.marty-suzuki.PrexSample"; 520 | PRODUCT_NAME = "$(TARGET_NAME)"; 521 | PROVISIONING_PROFILE_SPECIFIER = ""; 522 | SWIFT_VERSION = 4.2; 523 | TARGETED_DEVICE_FAMILY = "1,2"; 524 | }; 525 | name = Debug; 526 | }; 527 | 37932E2D215F19650072DB23 /* Release */ = { 528 | isa = XCBuildConfiguration; 529 | buildSettings = { 530 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 531 | CODE_SIGN_STYLE = Manual; 532 | DEVELOPMENT_TEAM = ""; 533 | INFOPLIST_FILE = PrexSample/Info.plist; 534 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 535 | LD_RUNPATH_SEARCH_PATHS = ( 536 | "$(inherited)", 537 | "@executable_path/Frameworks", 538 | ); 539 | PRODUCT_BUNDLE_IDENTIFIER = "jp.marty-suzuki.PrexSample"; 540 | PRODUCT_NAME = "$(TARGET_NAME)"; 541 | PROVISIONING_PROFILE_SPECIFIER = ""; 542 | SWIFT_VERSION = 4.2; 543 | TARGETED_DEVICE_FAMILY = "1,2"; 544 | }; 545 | name = Release; 546 | }; 547 | 37932E2F215F19650072DB23 /* Debug */ = { 548 | isa = XCBuildConfiguration; 549 | buildSettings = { 550 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 551 | BUNDLE_LOADER = "$(TEST_HOST)"; 552 | CODE_SIGN_STYLE = Manual; 553 | DEVELOPMENT_TEAM = ""; 554 | INFOPLIST_FILE = PrexSampleTests/Info.plist; 555 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 556 | LD_RUNPATH_SEARCH_PATHS = ( 557 | "$(inherited)", 558 | "@executable_path/Frameworks", 559 | "@loader_path/Frameworks", 560 | ); 561 | PRODUCT_BUNDLE_IDENTIFIER = "jp.marty-suzuki.PrexSampleTests"; 562 | PRODUCT_NAME = "$(TARGET_NAME)"; 563 | PROVISIONING_PROFILE_SPECIFIER = ""; 564 | SWIFT_VERSION = 4.2; 565 | TARGETED_DEVICE_FAMILY = "1,2"; 566 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PrexSample.app/PrexSample"; 567 | }; 568 | name = Debug; 569 | }; 570 | 37932E30215F19650072DB23 /* Release */ = { 571 | isa = XCBuildConfiguration; 572 | buildSettings = { 573 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 574 | BUNDLE_LOADER = "$(TEST_HOST)"; 575 | CODE_SIGN_STYLE = Manual; 576 | DEVELOPMENT_TEAM = ""; 577 | INFOPLIST_FILE = PrexSampleTests/Info.plist; 578 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 579 | LD_RUNPATH_SEARCH_PATHS = ( 580 | "$(inherited)", 581 | "@executable_path/Frameworks", 582 | "@loader_path/Frameworks", 583 | ); 584 | PRODUCT_BUNDLE_IDENTIFIER = "jp.marty-suzuki.PrexSampleTests"; 585 | PRODUCT_NAME = "$(TARGET_NAME)"; 586 | PROVISIONING_PROFILE_SPECIFIER = ""; 587 | SWIFT_VERSION = 4.2; 588 | TARGETED_DEVICE_FAMILY = "1,2"; 589 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PrexSample.app/PrexSample"; 590 | }; 591 | name = Release; 592 | }; 593 | /* End XCBuildConfiguration section */ 594 | 595 | /* Begin XCConfigurationList section */ 596 | 37932E09215F19640072DB23 /* Build configuration list for PBXProject "PrexSample" */ = { 597 | isa = XCConfigurationList; 598 | buildConfigurations = ( 599 | 37932E29215F19650072DB23 /* Debug */, 600 | 37932E2A215F19650072DB23 /* Release */, 601 | ); 602 | defaultConfigurationIsVisible = 0; 603 | defaultConfigurationName = Release; 604 | }; 605 | 37932E2B215F19650072DB23 /* Build configuration list for PBXNativeTarget "PrexSample" */ = { 606 | isa = XCConfigurationList; 607 | buildConfigurations = ( 608 | 37932E2C215F19650072DB23 /* Debug */, 609 | 37932E2D215F19650072DB23 /* Release */, 610 | ); 611 | defaultConfigurationIsVisible = 0; 612 | defaultConfigurationName = Release; 613 | }; 614 | 37932E2E215F19650072DB23 /* Build configuration list for PBXNativeTarget "PrexSampleTests" */ = { 615 | isa = XCConfigurationList; 616 | buildConfigurations = ( 617 | 37932E2F215F19650072DB23 /* Debug */, 618 | 37932E30215F19650072DB23 /* Release */, 619 | ); 620 | defaultConfigurationIsVisible = 0; 621 | defaultConfigurationName = Release; 622 | }; 623 | /* End XCConfigurationList section */ 624 | }; 625 | rootObject = 37932E06215F19640072DB23 /* Project object */; 626 | } 627 | -------------------------------------------------------------------------------- /Example/PrexSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/PrexSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/PrexSample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/PrexSample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/PrexSample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PrexSample 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | #if swift(>=4.2) 17 | typealias UIApplicationLaunchOptionsKey = UIApplication.LaunchOptionsKey 18 | #endif 19 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 20 | 21 | guard let navigationController = window?.rootViewController as? UINavigationController else { 22 | return false 23 | } 24 | 25 | navigationController.setViewControllers([SearchViewController()], animated: false) 26 | 27 | return true 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /Example/PrexSample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Example/PrexSample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/PrexSample/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 | -------------------------------------------------------------------------------- /Example/PrexSample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/PrexSample/GitHub/GitHub.Pagination.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitHub.Pagination.swift 3 | // PrexSample 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension GitHub { 12 | struct Pagination { 13 | var next: Int? 14 | var last: Int? 15 | var first: Int? 16 | var prev: Int? 17 | } 18 | } 19 | 20 | extension GitHub.Pagination { 21 | private enum Const { 22 | static let regex = try! NSRegularExpression(pattern: ".*; rel=\"(.*)\"", options: []) 23 | } 24 | 25 | init(link: String) { 26 | let values = link.split(separator: ",") 27 | 28 | self = values.reduce(GitHub.Pagination(next: nil, last: nil, first: nil, prev: nil)) { pagination, value in 29 | let string = String(value) 30 | let results = Const.regex.matches(in: string, 31 | options: [], 32 | range: NSRange(location: 0, length: (string as NSString).length)) 33 | 34 | let values = results.compactMap { result -> (Int, String)? in 35 | let values = (0.. (Int?, String?) in 36 | if index == 0 { 37 | return values 38 | } 39 | let range = result.range(at: index) 40 | let str = (string as NSString).substring(with: range) 41 | 42 | if index == 1, let page = Int(str) { 43 | return (page, values.1) 44 | } else if index == 2 { 45 | return (values.0, str) 46 | } else { 47 | return values 48 | } 49 | } 50 | 51 | if case let (page?, name?) = values { 52 | return (page, name) 53 | } else { 54 | return nil 55 | } 56 | } 57 | 58 | return values.reduce(pagination) { pagination, value in 59 | var newPagination = pagination 60 | switch value.1 { 61 | case "next": 62 | newPagination.next = value.0 63 | case "last": 64 | newPagination.last = value.0 65 | case "first": 66 | newPagination.first = value.0 67 | case "prev": 68 | newPagination.prev = value.0 69 | default: 70 | break 71 | } 72 | return newPagination 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Example/PrexSample/GitHub/GitHub.Repository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitHub.Repository.swift 3 | // PrexSample 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension GitHub { 12 | struct Repository: Codable { 13 | let id: Int 14 | let nodeID: String 15 | let name: String 16 | let fullName: String 17 | let owner: User 18 | let isPrivate: Bool 19 | let htmlURL: URL 20 | let description: String? 21 | let isFork: Bool 22 | let url: URL 23 | let createdAt: String 24 | let updatedAt: String 25 | let pushedAt: String? 26 | let homepage: String? 27 | let size: Int 28 | let stargazersCount: Int 29 | let watchersCount: Int 30 | let language: String? 31 | let forksCount: Int 32 | let openIssuesCount: Int 33 | let defaultBranch: String 34 | 35 | private enum CodingKeys: String, CodingKey { 36 | case id 37 | case nodeID = "node_id" 38 | case name 39 | case fullName = "full_name" 40 | case owner 41 | case isPrivate = "private" 42 | case htmlURL = "html_url" 43 | case description 44 | case isFork = "fork" 45 | case url 46 | case createdAt = "created_at" 47 | case updatedAt = "updated_at" 48 | case pushedAt = "pushed_at" 49 | case homepage 50 | case size 51 | case stargazersCount = "stargazers_count" 52 | case watchersCount = "watchers_count" 53 | case language 54 | case forksCount = "forks_count" 55 | case openIssuesCount = "open_issues_count" 56 | case defaultBranch = "default_branch" 57 | } 58 | } 59 | } 60 | 61 | extension GitHub.Repository: Equatable { 62 | static func == (lhs: GitHub.Repository, rhs: GitHub.Repository) -> Bool { 63 | return lhs.id == rhs.id && lhs.updatedAt == rhs.updatedAt 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Example/PrexSample/GitHub/GitHub.Response.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitHub.Response.swift 3 | // PrexSample 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension GitHub { 12 | struct Response: Decodable { 13 | let totalCount: Int 14 | let incompleteResults: Bool 15 | let items: [Item] 16 | 17 | private enum CodingKeys: String, CodingKey { 18 | case totalCount = "total_count" 19 | case incompleteResults = "incomplete_results" 20 | case items 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Example/PrexSample/GitHub/GitHub.Session.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitHub.Session.swift 3 | // PrexSample 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | typealias GitHubSearchResult = (GitHub.Session.Result<([GitHub.Repository], GitHub.Pagination)>) 12 | 13 | protocol GitHubSessionProtocol: AnyObject { 14 | @discardableResult 15 | func searchRepositories(query: String, page: Int, completion: @escaping (GitHubSearchResult) -> ()) -> URLSessionTask? 16 | } 17 | 18 | extension GitHub { 19 | 20 | final class Session { 21 | 22 | private let session: URLSession 23 | private let baseURL: URL = URL(string: "https://api.github.com")! 24 | 25 | init(session: URLSession = .shared) { 26 | self.session = session 27 | } 28 | 29 | private func sendRequest(path: String, 30 | method: HttpMethod, 31 | queryItems: [URLQueryItem]?, 32 | completion: @escaping (Result<(T, Pagination)>) -> ()) -> URLSessionTask? { 33 | let url = baseURL.appendingPathComponent(path) 34 | 35 | guard var componets = URLComponents(url: url, resolvingAgainstBaseURL: true) else { 36 | completion(.failure(Error(message: "failed to generate URLComponents from \(url)"))) 37 | return nil 38 | } 39 | componets.queryItems = queryItems?.compactMap { $0 } 40 | 41 | guard var urlRequest = componets.url.map({ URLRequest(url: $0) }) else { 42 | completion(.failure(Error(message: "failed to generate URLRequest from \(componets)"))) 43 | return nil 44 | } 45 | 46 | urlRequest.httpMethod = method.rawValue 47 | urlRequest.allHTTPHeaderFields = ["Accept": "application/json"] 48 | 49 | let task = session.dataTask(with: urlRequest) { data, response, error in 50 | if let error = error { 51 | completion(.failure(error)) 52 | return 53 | } 54 | 55 | guard let response = response as? HTTPURLResponse else { 56 | completion(.failure(Error(message: "no response"))) 57 | return 58 | } 59 | 60 | guard let data = data else { 61 | completion(.failure(Error(message: "no data"))) 62 | return 63 | } 64 | 65 | guard 200..<300 ~= response.statusCode else { 66 | completion(.failure(Error(message: "status code is \(response.statusCode)"))) 67 | return 68 | } 69 | 70 | let pagination: Pagination 71 | if let link = response.allHeaderFields["Link"] as? String { 72 | pagination = Pagination(link: link) 73 | } else { 74 | pagination = Pagination(next: nil, last: nil, first: nil, prev: nil) 75 | } 76 | 77 | do { 78 | let object = try JSONDecoder().decode(T.self, from: data) 79 | completion(.success((object, pagination))) 80 | } catch { 81 | completion(.failure(error)) 82 | } 83 | } 84 | 85 | task.resume() 86 | 87 | return task 88 | } 89 | } 90 | } 91 | 92 | extension GitHub.Session: GitHubSessionProtocol { 93 | @discardableResult 94 | func searchRepositories(query: String, page: Int, completion: @escaping (GitHubSearchResult) -> ()) -> URLSessionTask? { 95 | let queryItems: [URLQueryItem] = [ 96 | URLQueryItem(name: "q", value: query), 97 | URLQueryItem(name: "page", value: "\(page)"), 98 | URLQueryItem(name: "order", value: "desc"), 99 | URLQueryItem(name: "sort", value: "stars") 100 | ] 101 | 102 | typealias _Result = (GitHub.Session.Result<(GitHub.Response, GitHub.Pagination)>) 103 | return sendRequest(path: "/search/repositories", method: .get, queryItems: queryItems) { (result: _Result) in 104 | switch result { 105 | case let .success(response, pagination): 106 | completion(.success((response.items, pagination))) 107 | case let .failure(error): 108 | completion(.failure(error)) 109 | } 110 | } 111 | } 112 | } 113 | 114 | extension GitHub.Session { 115 | enum Result { 116 | case success(T) 117 | case failure(Swift.Error) 118 | } 119 | 120 | 121 | enum HttpMethod: String { 122 | case get = "GET" 123 | case post = "POST" 124 | } 125 | 126 | struct Error: Swift.Error { 127 | let message: String 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Example/PrexSample/GitHub/GitHub.User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitHub.User.swift 3 | // PrexSample 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension GitHub { 12 | struct User: Codable { 13 | let login: String 14 | let id: Int 15 | let nodeID: String 16 | let avatarURL: URL 17 | let gravatarID: String 18 | let url: URL 19 | let receivedEventsURL: URL 20 | let type: String 21 | 22 | private enum CodingKeys: String, CodingKey { 23 | case login, id 24 | case nodeID = "node_id" 25 | case avatarURL = "avatar_url" 26 | case gravatarID = "gravatar_id" 27 | case url 28 | case receivedEventsURL = "received_events_url" 29 | case type 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Example/PrexSample/GitHub/GitHub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GitHub.swift 3 | // PrexSample 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum GitHub {} 12 | -------------------------------------------------------------------------------- /Example/PrexSample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Example/PrexSample/View/Detail/DetailViewController.Prex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailViewController.Prex.swift 3 | // PrexSample 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import Prex 10 | import UIKit 11 | 12 | enum DetailAction: Action { 13 | case setHTMLURL(URL?) 14 | case setObservation(NSKeyValueObservation?) 15 | case setProgress(Double) 16 | case setName(String) 17 | } 18 | 19 | struct DetailState: State { 20 | fileprivate(set) var htmlURL: URL? 21 | fileprivate(set) var observation: NSKeyValueObservation? 22 | fileprivate(set) var progress: Double = 0 23 | fileprivate(set) var name = "" 24 | } 25 | 26 | struct DetailMutation: Mutation { 27 | func mutate(action: DetailAction, state: inout DetailState) { 28 | switch action { 29 | case let .setHTMLURL(url): 30 | state.htmlURL = url 31 | 32 | case let .setObservation(observation): 33 | state.observation = observation 34 | 35 | case let .setProgress(progress): 36 | state.progress = progress 37 | 38 | case let .setName(name): 39 | state.name = name 40 | } 41 | } 42 | } 43 | 44 | final class DetailPresenter: Presenter { 45 | 46 | init(view: View) where View.State == DetailState { 47 | let flux = Flux(state: DetailState(), mutation: DetailMutation()) 48 | super.init(view: view, flux: flux) 49 | } 50 | 51 | func progressUpdateParams(from progress: Double) -> ProgressUpdateParams { 52 | let isShown = 0.0..<1.0 ~= progress 53 | return ProgressUpdateParams(animated: isShown, 54 | alpha: isShown ? 1 : 0, 55 | progress: Float(progress)) 56 | } 57 | 58 | func observeProgress(of object: Root, for keyPath: KeyPath) { 59 | let observation = object.observe(keyPath, options: .new) { [weak self] _, change in 60 | guard let progress = change.newValue else { 61 | return 62 | } 63 | self?.dispatch(.setProgress(progress)) 64 | } 65 | dispatch(.setObservation(observation)) 66 | } 67 | 68 | func setRepository(_ repository: GitHub.Repository) { 69 | dispatch(.setHTMLURL(repository.htmlURL)) 70 | dispatch(.setName(repository.name)) 71 | } 72 | } 73 | 74 | extension DetailPresenter { 75 | struct ProgressUpdateParams { 76 | let animated: Bool 77 | let alpha: CGFloat 78 | let progress: Float 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Example/PrexSample/View/Detail/DetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailViewController.swift 3 | // PrexSample 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import Prex 10 | import UIKit 11 | import WebKit 12 | 13 | final class DetailViewController: UIViewController { 14 | 15 | @IBOutlet private(set) weak var progressView: UIProgressView! 16 | @IBOutlet private(set) weak var webviewContainer: UIView! { 17 | didSet { 18 | webviewContainer.addSubview(webview) 19 | webview.frame = webviewContainer.bounds 20 | webview.autoresizingMask = [.flexibleWidth, .flexibleHeight] 21 | } 22 | } 23 | 24 | let webview = WKWebView(frame: .zero, configuration: .init()) 25 | 26 | private(set) lazy var presenter = DetailPresenter(view: self) 27 | 28 | init(repository: GitHub.Repository) { 29 | super.init(nibName: nil, bundle: nil) 30 | 31 | presenter.setRepository(repository) 32 | } 33 | 34 | required init?(coder aDecoder: NSCoder) { 35 | fatalError("init(coder:) has not been implemented") 36 | } 37 | 38 | override func viewDidLoad() { 39 | super.viewDidLoad() 40 | 41 | presenter.observeProgress(of: webview, for: \.estimatedProgress) 42 | } 43 | } 44 | 45 | extension DetailViewController: View { 46 | func reflect(change: StateChange) { 47 | 48 | if let url = change.htmlURL?.value { 49 | webview.load(URLRequest(url: url)) 50 | } 51 | 52 | if let progress = change.progress?.value { 53 | let params = presenter.progressUpdateParams(from: progress) 54 | UIView.animate(withDuration: 0.3) { 55 | self.progressView.alpha = params.alpha 56 | self.progressView.setProgress(params.progress, animated: params.animated) 57 | } 58 | } 59 | 60 | if let name = change.changedProperty(for: \.name)?.value { 61 | navigationItem.title = name 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Example/PrexSample/View/Detail/DetailViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /Example/PrexSample/View/Search/SearchCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchCell.swift 3 | // PrexSample 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SearchCell: UITableViewCell { 12 | 13 | class var identifier: String { 14 | return String(describing: self) 15 | } 16 | 17 | class var nib: UINib { 18 | return UINib(nibName: identifier, bundle: Bundle(for: self)) 19 | } 20 | 21 | @IBOutlet private(set) weak var containerView: UIView! { 22 | didSet { 23 | containerView.layer.cornerRadius = 8 24 | containerView.layer.masksToBounds = true 25 | containerView.layer.borderWidth = 1 26 | containerView.layer.borderColor = UIColor.lightGray.cgColor 27 | } 28 | } 29 | 30 | @IBOutlet private(set) weak var repositoryNameLabel: UILabel! 31 | @IBOutlet private(set) weak var descriptionLabel: UILabel! 32 | 33 | @IBOutlet private(set) weak var languageContainerView: UIView! 34 | @IBOutlet private(set) weak var languageLabel: UILabel! 35 | 36 | @IBOutlet private(set) weak var starLabel: UILabel! 37 | 38 | func configure(with repository: GitHub.Repository) { 39 | repositoryNameLabel.text = repository.fullName 40 | 41 | if let description = repository.description { 42 | descriptionLabel.isHidden = false 43 | descriptionLabel.text = description 44 | } else { 45 | descriptionLabel.isHidden = true 46 | descriptionLabel.text = nil 47 | } 48 | 49 | if let language = repository.language { 50 | languageContainerView.isHidden = false 51 | languageLabel.text = language 52 | } else { 53 | languageContainerView.isHidden = true 54 | languageLabel.text = nil 55 | } 56 | 57 | starLabel.text = "★ \(repository.stargazersCount)" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Example/PrexSample/View/Search/SearchCell.xib: -------------------------------------------------------------------------------- 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 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /Example/PrexSample/View/Search/SearchViewController.Prex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchViewController.Prex.swift 3 | // PrexSample 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import Prex 10 | 11 | enum SearchAction: Action { 12 | case setQuery(String?) 13 | case setPagination(GitHub.Pagination?) 14 | case addRepositories([GitHub.Repository]) 15 | case clearRepositories 16 | case setIsFetching(Bool) 17 | case setIsEditing(Bool) 18 | case setError(Error?) 19 | case setFetchDate(Date) 20 | case setSelectedRepository(GitHub.Repository?) 21 | } 22 | 23 | struct SearchState: State { 24 | fileprivate(set) var query: String? 25 | fileprivate(set) var repositories: [GitHub.Repository] = [] 26 | fileprivate(set) var pagination: GitHub.Pagination? 27 | fileprivate(set) var isEditing = false 28 | fileprivate(set) var isFetching = false 29 | fileprivate(set) var error: Error? 30 | fileprivate(set) var fetchDate: Date? 31 | fileprivate(set) var selectedRepository: GitHub.Repository? 32 | } 33 | 34 | struct SearchMutation: Mutation { 35 | func mutate(action: SearchAction, state: inout SearchState) { 36 | switch action { 37 | case let .addRepositories(repositories): 38 | state.repositories.append(contentsOf: repositories) 39 | 40 | case .clearRepositories: 41 | state.repositories.removeAll() 42 | 43 | case let .setPagination(pagination): 44 | state.pagination = pagination 45 | 46 | case let .setIsFetching(isFetching): 47 | state.isFetching = isFetching 48 | 49 | case let .setIsEditing(isEditing): 50 | state.isEditing = isEditing 51 | 52 | case let .setError(error): 53 | state.error = error 54 | 55 | case let .setQuery(query): 56 | state.query = query 57 | 58 | case let .setFetchDate(date): 59 | state.fetchDate = date 60 | 61 | case let .setSelectedRepository(repository): 62 | state.selectedRepository = repository 63 | } 64 | } 65 | } 66 | 67 | extension Presenter where Action == SearchAction, State == SearchState { 68 | func fetchRepositories(query: String, 69 | page: Int = 1, 70 | session: GitHubSessionProtocol = GitHub.Session(), 71 | makeDate: @escaping () -> Date = { Date() }) { 72 | dispatch(.setQuery(query)) 73 | dispatch(.setIsFetching(true)) 74 | session.searchRepositories(query: query, page: page) { [weak self] result in 75 | switch result { 76 | case let .success(repositories, pagination): 77 | self?.dispatch(.addRepositories(repositories)) 78 | self?.dispatch(.setPagination(pagination)) 79 | case let .failure(error): 80 | self?.dispatch(.setError(error)) 81 | } 82 | self?.dispatch(.setIsFetching(false)) 83 | self?.dispatch(.setFetchDate(makeDate())) 84 | } 85 | } 86 | 87 | func fetchMoreRepositories(session: GitHubSessionProtocol = GitHub.Session()) { 88 | guard 89 | let query = state.query, 90 | let next = state.pagination?.next, 91 | state.pagination?.last != nil && !state.isFetching, 92 | let fetchDate = state.fetchDate, 93 | fetchDate.timeIntervalSinceNow < -1 94 | else { 95 | return 96 | } 97 | 98 | fetchRepositories(query: query, page: next, session: session) 99 | } 100 | 101 | func selectedIndexPath(_ indexPath: IndexPath?) { 102 | let repository = indexPath.map { state.repositories[$0.row] } 103 | dispatch(.setSelectedRepository(repository)) 104 | } 105 | 106 | func setIsEditing(_ isEditing: Bool) { 107 | dispatch(.setIsEditing(isEditing)) 108 | } 109 | 110 | func clearRepositories() { 111 | dispatch(.clearRepositories) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Example/PrexSample/View/Search/SearchViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchViewController.swift 3 | // PrexSample 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import Prex 10 | import UIKit 11 | 12 | final class SearchViewController: UIViewController { 13 | 14 | private(set) lazy var searchBar: UISearchBar = { 15 | let searchBar = UISearchBar(frame: .zero) 16 | searchBar.delegate = self 17 | return searchBar 18 | }() 19 | 20 | @IBOutlet private(set) weak var tableView: UITableView! { 21 | didSet { 22 | tableView.register(SearchCell.nib, forCellReuseIdentifier: SearchCell.identifier) 23 | tableView.dataSource = self 24 | tableView.delegate = self 25 | } 26 | } 27 | 28 | private(set) lazy var presenter = Presenter(view: self, 29 | state: SearchState(), 30 | mutation: SearchMutation()) 31 | 32 | init() { 33 | super.init(nibName: nil, bundle: nil) 34 | } 35 | 36 | required init?(coder aDecoder: NSCoder) { 37 | fatalError("init(coder:) has not been implemented") 38 | } 39 | 40 | override func viewDidLoad() { 41 | super.viewDidLoad() 42 | 43 | navigationItem.titleView = searchBar 44 | } 45 | 46 | override func viewDidAppear(_ animated: Bool) { 47 | super.viewDidAppear(animated) 48 | 49 | presenter.selectedIndexPath(nil) 50 | } 51 | 52 | private func refrectEditing(isEditing: Bool) { 53 | UIView.animate(withDuration: 0.3) { 54 | if isEditing { 55 | self.view.backgroundColor = .black 56 | self.tableView.isUserInteractionEnabled = false 57 | self.tableView.alpha = 0.5 58 | self.searchBar.setShowsCancelButton(true, animated: true) 59 | } else { 60 | self.searchBar.resignFirstResponder() 61 | self.view.backgroundColor = .white 62 | self.tableView.isUserInteractionEnabled = true 63 | self.tableView.alpha = 1 64 | self.searchBar.setShowsCancelButton(false, animated: true) 65 | } 66 | } 67 | } 68 | 69 | private func showDetail(repository: GitHub.Repository) { 70 | let vc = DetailViewController(repository: repository) 71 | navigationController?.pushViewController(vc, animated: true) 72 | } 73 | } 74 | 75 | extension SearchViewController: View { 76 | func reflect(change: StateChange) { 77 | 78 | if let isEditing = change.isEditing?.value { 79 | refrectEditing(isEditing: isEditing) 80 | } 81 | 82 | if let new = change.repositories?.value { 83 | tableView.reloadData() 84 | 85 | if new.count > 0, let old = change.old?.repositories, old.count > 0 { 86 | let lastIndexPath = IndexPath(row: old.count - 1, section: 0) 87 | tableView.scrollToRow(at: lastIndexPath, at: .bottom, animated: false) 88 | } 89 | } 90 | 91 | if let repository = change.selectedRepository?.value { 92 | showDetail(repository: repository) 93 | } 94 | } 95 | } 96 | 97 | extension SearchViewController: UISearchBarDelegate { 98 | func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool { 99 | presenter.setIsEditing(true) 100 | return true 101 | } 102 | 103 | func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { 104 | presenter.setIsEditing(false) 105 | } 106 | 107 | func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { 108 | if let text = searchBar.text, !text.isEmpty { 109 | presenter.clearRepositories() 110 | presenter.fetchRepositories(query: text) 111 | presenter.setIsEditing(false) 112 | } 113 | } 114 | } 115 | 116 | extension SearchViewController: UITableViewDataSource { 117 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 118 | return presenter.state.repositories.count 119 | } 120 | 121 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 122 | let cell = tableView.dequeueReusableCell(withIdentifier: SearchCell.identifier, for: indexPath) 123 | 124 | if let cell = cell as? SearchCell { 125 | let repository = presenter.state.repositories[indexPath.row] 126 | cell.configure(with: repository) 127 | } 128 | 129 | return cell 130 | } 131 | } 132 | 133 | extension SearchViewController: UITableViewDelegate { 134 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 135 | tableView.deselectRow(at: indexPath, animated: false) 136 | presenter.selectedIndexPath(indexPath) 137 | } 138 | 139 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 140 | if scrollView.contentSize.height > 0 && 141 | (scrollView.contentSize.height - scrollView.bounds.size.height) <= scrollView.contentOffset.y { 142 | presenter.fetchMoreRepositories() 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Example/PrexSample/View/Search/SearchViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Example/PrexSampleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Example/PrexSampleTests/Mock/MockGitHubSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockGitHubSession.swift 3 | // PrexSampleTests 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import PrexSample 11 | 12 | final class MockGitHubSession: GitHubSessionProtocol { 13 | var searchRepositoriesResult: GitHubSearchResult? 14 | var searchRepositoriesParams: ((String, Int) -> ())? 15 | 16 | func searchRepositories(query: String, page: Int, completion: @escaping (GitHubSearchResult) -> ()) -> URLSessionTask? { 17 | searchRepositoriesParams?(query, page) 18 | if let result = searchRepositoriesResult { 19 | completion(result) 20 | } 21 | return nil 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Example/PrexSampleTests/TestCase/Search/SearchPresenterTestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchPresenterTestCase.swift 3 | // PrexSampleTests 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import Prex 10 | import XCTest 11 | @testable import PrexSample 12 | 13 | final class SearchPresenterTestCase: XCTestCase { 14 | private var dependency: Dependency! 15 | 16 | override func setUp() { 17 | dependency = Dependency(state: .init()) 18 | } 19 | 20 | func testFetchRepositories() { 21 | let pagination = GitHub.Pagination(next: nil, last: nil, first: nil, prev: nil) 22 | let repositories = [makeRepository(), makeRepository()] 23 | dependency.session.searchRepositoriesResult = GitHubSearchResult.success((repositories, pagination)) 24 | 25 | var actions: [SearchAction] = [] 26 | let subscription = dependency.dispatcher.register { action in 27 | actions.append(action) 28 | } 29 | 30 | let date = Date() 31 | let query = "test-query" 32 | let page: Int = 1 33 | dependency.presenter.fetchRepositories(query: query, 34 | page: page, 35 | session: dependency.session, 36 | makeDate: { date }) 37 | dependency.dispatcher.unregister(subscription) 38 | 39 | XCTAssertEqual(actions.count, 6) 40 | 41 | if case let .setQuery(_query) = actions[0] { 42 | XCTAssertEqual(_query, query) 43 | } else { 44 | XCTFail("actions[0] must be .setQuery, but it is \(actions[0])") 45 | } 46 | 47 | if case let .setIsFetching(isFetching) = actions[1] { 48 | XCTAssertTrue(isFetching) 49 | } else { 50 | XCTFail("actions[1] must be .setIsFetching, but it is \(actions[1])") 51 | } 52 | 53 | if case let .addRepositories(_repos) = actions[2] { 54 | XCTAssertEqual(_repos, repositories) 55 | } else { 56 | XCTFail("actions[2] must be .addRepositories, but it is \(actions[2])") 57 | } 58 | 59 | if case let .setPagination(_pagination) = actions[3] { 60 | XCTAssertEqual(_pagination?.first, pagination.first) 61 | XCTAssertEqual(_pagination?.last, pagination.last) 62 | XCTAssertEqual(_pagination?.next, pagination.next) 63 | XCTAssertEqual(_pagination?.prev, pagination.prev) 64 | } else { 65 | XCTFail("actions[3] must be .setPagination, but it is \(actions[3])") 66 | } 67 | 68 | if case let .setIsFetching(isFetching) = actions[4] { 69 | XCTAssertFalse(isFetching) 70 | } else { 71 | XCTFail("actions[4] must be .setIsFetching, but it is \(actions[4])") 72 | } 73 | 74 | if case let .setFetchDate(_date) = actions[5] { 75 | XCTAssertEqual(_date, date) 76 | } else { 77 | XCTFail("actions[5] must be .setFetchDate, but is is \(actions[5])") 78 | } 79 | } 80 | 81 | func testSetIsFetching() { 82 | var _change: ValueChange? 83 | dependency.view.refrectHandler = { _change = $0 } 84 | 85 | dependency.dispatcher.dispatch(.setIsFetching(true)) 86 | 87 | guard let change = _change else { 88 | XCTFail("change is nil") 89 | return 90 | } 91 | 92 | let isFetching = change.valueIfChanged(for: \.isFetching) 93 | XCTAssertNotNil(isFetching) 94 | XCTAssertTrue(isFetching ?? false) 95 | } 96 | } 97 | 98 | extension SearchPresenterTestCase { 99 | private struct Dependency { 100 | 101 | let view = MockView() 102 | let dispatcher: Dispatcher 103 | let session = MockGitHubSession() 104 | let presenter: Presenter 105 | 106 | init(state: SearchState) { 107 | let flux = Flux(state: state, mutation: SearchMutation()) 108 | self.dispatcher = flux.dispatcher 109 | self.presenter = Presenter(view: view, flux: flux) 110 | } 111 | } 112 | 113 | private final class MockView: View { 114 | var refrectHandler: ((ValueChange) -> ())? 115 | 116 | func refrect(change: ValueChange) { 117 | refrectHandler?(change) 118 | } 119 | } 120 | 121 | private func makeUser() -> GitHub.User { 122 | return GitHub.User(login: "marty-suzuki", 123 | id: 1, 124 | nodeID: "nodeID", 125 | avatarURL: URL(string: "https://avatars1.githubusercontent.com")!, 126 | gravatarID: "", 127 | url: URL(string: "https://github.com/marty-suzuki")!, 128 | receivedEventsURL: URL(string: "https://github.com/marty-suzuki")!, 129 | type: "User") 130 | } 131 | 132 | private func makeRepository() -> GitHub.Repository { 133 | return GitHub.Repository(id: 1, 134 | nodeID: "nodeID", 135 | name: "URLEmbeddedView", 136 | fullName: "marty-suzuki/URLEmbeddedView", 137 | owner: makeUser(), 138 | isPrivate: false, 139 | htmlURL: URL(string: "https://github.com/marty-suzuki/URLEmbeddedView")!, 140 | description: "URLEmbeddedView automatically caches the object that is confirmed the Open Graph Protocol.", 141 | isFork: false, 142 | url: URL(string: "https://github.com/marty-suzuki/URLEmbeddedView")!, 143 | createdAt: "2016-03-06T03:45:39Z", 144 | updatedAt: "2018-08-28T04:50:22Z", 145 | pushedAt: "2018-07-18T10:04:10Z", 146 | homepage: nil, 147 | size: 1, 148 | stargazersCount: 479, 149 | watchersCount: 479, 150 | language: "Swift", 151 | forksCount: 52, 152 | openIssuesCount: 0, 153 | defaultBranch: "master") 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Example/README.md: -------------------------------------------------------------------------------- 1 | # GitHub Repository Search App Sample 2 | 3 | | SearchViewController | DetailViewController | 4 | | :-: | :-: | 5 | | ![](./Images/search.png) | ![](./Images/detail.png) | 6 | -------------------------------------------------------------------------------- /Images/Prex.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marty-suzuki/Prex/d6c198fc96e0155f658be55a1798aaa287acdc1b/Images/Prex.sketch -------------------------------------------------------------------------------- /Images/data-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marty-suzuki/Prex/d6c198fc96e0155f658be55a1798aaa287acdc1b/Images/data-flow.png -------------------------------------------------------------------------------- /Images/func-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marty-suzuki/Prex/d6c198fc96e0155f658be55a1798aaa287acdc1b/Images/func-test.png -------------------------------------------------------------------------------- /Images/playground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marty-suzuki/Prex/d6c198fc96e0155f658be55a1798aaa287acdc1b/Images/playground.png -------------------------------------------------------------------------------- /Images/prex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marty-suzuki/Prex/d6c198fc96e0155f658be55a1798aaa287acdc1b/Images/prex.png -------------------------------------------------------------------------------- /Images/reflection-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marty-suzuki/Prex/d6c198fc96e0155f658be55a1798aaa287acdc1b/Images/reflection-test.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Taiki Suzuki 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.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Prex", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "Prex", 12 | targets: ["Prex"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "Prex", 23 | dependencies: []), 24 | .testTarget( 25 | name: "PrexTests", 26 | dependencies: ["Prex"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /Prex.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | import PlaygroundSupport 2 | import Prex 3 | import UIKit 4 | 5 | // MARK: - ViewController 6 | 7 | final class CounterViewController: UIViewController { 8 | 9 | private let counterLabel: UILabel = { 10 | let label = UILabel() 11 | label.numberOfLines = 1 12 | label.font = .boldSystemFont(ofSize: 30) 13 | label.translatesAutoresizingMaskIntoConstraints = false 14 | return label 15 | }() 16 | 17 | private(set) lazy var incrementButton: UIButton = { 18 | let button = UIButton(type: .system) 19 | button.setTitle("Increment +", for: .normal) 20 | button.titleLabel?.font = .boldSystemFont(ofSize: 20) 21 | button.addTarget(self, action: #selector(self.incrementButtonTap(_:)), for: .touchUpInside) 22 | button.translatesAutoresizingMaskIntoConstraints = false 23 | return button 24 | }() 25 | 26 | private(set) lazy var decrementButton: UIButton = { 27 | let button = UIButton(type: .system) 28 | button.setTitle("Decrement -", for: .normal) 29 | button.titleLabel?.font = .boldSystemFont(ofSize: 20) 30 | button.addTarget(self, action: #selector(self.decrementButtonTap(_:)), for: .touchUpInside) 31 | button.translatesAutoresizingMaskIntoConstraints = false 32 | return button 33 | }() 34 | 35 | private lazy var presenter = Presenter(view: self, 36 | state: CounterState(), 37 | mutation: CounterMutation()) 38 | 39 | override func viewDidLoad() { 40 | super.viewDidLoad() 41 | 42 | view.backgroundColor = .white 43 | 44 | presenter.reflect() 45 | 46 | // setup stackView 47 | do { 48 | let rootStackView = UIStackView(arrangedSubviews: [counterLabel]) 49 | rootStackView.translatesAutoresizingMaskIntoConstraints = false 50 | rootStackView.alignment = .center 51 | rootStackView.axis = .vertical 52 | rootStackView.spacing = 10 53 | 54 | let buttonStackView = UIStackView(arrangedSubviews: [incrementButton, decrementButton]) 55 | buttonStackView.alignment = .center 56 | buttonStackView.axis = .vertical 57 | buttonStackView.translatesAutoresizingMaskIntoConstraints = false 58 | rootStackView.addArrangedSubview(buttonStackView) 59 | 60 | view.addSubview(rootStackView) 61 | view.centerXAnchor.constraint(equalTo: rootStackView.centerXAnchor).isActive = true 62 | view.centerYAnchor.constraint(equalTo: rootStackView.centerYAnchor).isActive = true 63 | } 64 | } 65 | 66 | @objc private func incrementButtonTap(_ button: UIButton) { 67 | presenter.increment() 68 | } 69 | 70 | @objc private func decrementButtonTap(_ button: UIButton) { 71 | presenter.decrement() 72 | } 73 | } 74 | 75 | // MARK: - Prex implementation 76 | 77 | extension CounterViewController: View { 78 | func reflect(change: StateChange) { 79 | 80 | if let count = change.count?.value { 81 | counterLabel.text = "\(count)" 82 | } 83 | } 84 | } 85 | 86 | enum CounterAction: Action { 87 | case increment 88 | case decrement 89 | } 90 | 91 | struct CounterState: State { 92 | var count: Int = 0 93 | } 94 | 95 | struct CounterMutation: Mutation { 96 | func mutate(action: CounterAction, state: inout CounterState) { 97 | switch action { 98 | case .increment: 99 | state.count += 1 100 | 101 | case .decrement: 102 | state.count -= 1 103 | } 104 | } 105 | } 106 | 107 | extension Presenter where Action == CounterAction, State == CounterState { 108 | func increment() { 109 | dispatch(.increment) 110 | } 111 | 112 | func decrement() { 113 | if state.count > 0 { 114 | dispatch(.decrement) 115 | } 116 | } 117 | } 118 | 119 | // MARK: - Playground 120 | 121 | PlaygroundPage.current.needsIndefiniteExecution = true 122 | PlaygroundPage.current.liveView = CounterViewController() 123 | -------------------------------------------------------------------------------- /Prex.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Prex.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod spec lint Sica.podspec' to ensure this is a 3 | # valid spec and to remove all comments including this before submitting the spec. 4 | # 5 | # To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html 6 | # To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/ 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = "Prex" 11 | s.version = "0.3.0" 12 | s.summary = "Prex is unidirectional data flow architecture with MVP and Flux" 13 | s.homepage = "https://github.com/marty-suzuki/Prex" 14 | s.license = { :type => "MIT", :file => "LICENSE" } 15 | s.author = { "Taiki Suzuki" => "s1180183@gmail.com" } 16 | s.ios.deployment_target = "10.0" 17 | s.tvos.deployment_target = "10.0" 18 | s.osx.deployment_target = "10.10" 19 | s.watchos.deployment_target = "3.0" 20 | s.source = { :git => "https://github.com/marty-suzuki/Prex.git", :tag => "#{s.version}" } 21 | s.social_media_url = 'https://twitter.com/marty_suzuki' 22 | s.source_files = "Sources/**/*.{swift}" 23 | s.swift_version = '4.2' 24 | end 25 | -------------------------------------------------------------------------------- /Prex.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Prex.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Prex.xcodeproj/xcshareddata/xcschemes/Prex-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Prex.xcodeproj/xcshareddata/xcschemes/Prex-macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Prex.xcodeproj/xcshareddata/xcschemes/Prex-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Prex.xcodeproj/xcshareddata/xcschemes/Prex-watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Prex.xcodeproj/xcshareddata/xcschemes/PrexTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 39 | 40 | 41 | 42 | 48 | 49 | 51 | 52 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Prex.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Prex.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 |

5 |

6 | Platform 7 | 8 | Language 9 | 10 | 11 | Carthage 12 | 13 | 14 | Version 15 | 16 | 17 | License 18 | 19 | 20 | CI Status 21 | 22 |

23 | 24 | Prex is a framework which makes an unidirectional data flow application possible with MVP architecture. 25 | 26 | ## Concept 27 | 28 | Prex represents **Pre**senter + Flu**x**, therefore it is a combination of Flux and MVP architecture. 29 | In addition, Reactive frameworks are not used in Prex. 30 | To reflect a state to a view, using **Passive View Pattern**. 31 | Flux are used behind of the Presenter. 32 | Data flow is unidirectional that like a below figure. 33 | 34 | ![](./Images/data-flow.png) 35 | 36 | If you use Prex, you have to implement those components. 37 | 38 | - [State](#state) 39 | - [Action](#action) 40 | - [Mutation](#mutation) 41 | - [Presenter](#presenter) 42 | - [View](#view) 43 | 44 | ### State 45 | 46 | The State has properties to use in the View and the Presenter. 47 | 48 | ```swift 49 | struct CounterState: State { 50 | var count: Int = 0 51 | } 52 | ``` 53 | 54 | ### Action 55 | 56 | The Action represents internal API of your application. 57 | For example, if you want to increment the count of CounterState, dispatch Action.increment to Dispatcher. 58 | 59 | ```swift 60 | enum CounterAction: Action { 61 | case increment 62 | case decrement 63 | } 64 | ``` 65 | 66 | ### Mutation 67 | 68 | The Mutation is allowed to mutate the State with the Action. 69 | 70 | ```swift 71 | struct CounterMutation: Mutation { 72 | func mutate(action: CounterAction, state: inout CounterState) { 73 | switch action { 74 | case .increment: 75 | state.count += 1 76 | 77 | case .decrement: 78 | state.count -= 1 79 | } 80 | } 81 | } 82 | ``` 83 | 84 | ### Presenter 85 | 86 | The Presenter has a role to connect between View and Flux components. 87 | If you want to access side effect (API access and so on), you must access them in the Presenter. 88 | Finally, you dispatch those results with `Presenter.dispatch(_:)`. 89 | 90 | ```swift 91 | extension Presenter where Action == CounterAction, State == CounterState { 92 | func increment() { 93 | dispatch(.increment) 94 | } 95 | 96 | func decrement() { 97 | if state.count > 0 { 98 | dispatch(.decrement) 99 | } 100 | } 101 | } 102 | ``` 103 | 104 | ### View 105 | 106 | The View displays the State with `View.reflect(change:)`. 107 | It is called by the Presenter when the State has changed. 108 | In addition, it calls the Presenter methods by User interactions. 109 | 110 | ```swift 111 | final class CounterViewController: UIViewController { 112 | private let counterLabel: UILabel 113 | private lazy var presenter = Presenter(view: self, 114 | state: CounterState(), 115 | mutation: CounterMutation()) 116 | 117 | @objc private func incrementButtonTap(_ button: UIButton) { 118 | presenter.increment() 119 | } 120 | 121 | @objc private func decrementButtonTap(_ button: UIButton) { 122 | presenter.decrement() 123 | } 124 | } 125 | 126 | extension CounterViewController: View { 127 | func reflect(change: StateChange) { 128 | if let count = change.count?.value { 129 | counterLabel.text = "\(count)" 130 | } 131 | } 132 | } 133 | ``` 134 | 135 | You can get only specified value that has changed in the State from `StateChange.changedProperty(for:)`. 136 | 137 | ## Advanced Usage 138 | 139 | ### Shared Store 140 | 141 | Initializers of the Store and the Dispatcher are not public access level. 142 | But you can initialize them with `Flux` and inject them with `Presenter.init(view:flux:)`. 143 | 144 | This is shared Flux components example. 145 | 146 | ```swift 147 | extension Flux where Action == CounterAction, State == CounterState { 148 | static let shared = Flux(state: CounterState(), mutation: CounterMutation()) 149 | } 150 | ``` 151 | 152 | or 153 | 154 | ```swift 155 | enum SharedFlux { 156 | static let counter = Flux(state: CounterState(), mutation: CounterMutation()) 157 | } 158 | ``` 159 | 160 | Inject `Flux` like this. 161 | 162 | ```swift 163 | final class CounterViewController: UIViewController { 164 | private lazy var presenter = { 165 | let flux = Flux.shared 166 | return Presenter(view: self, flux: flux) 167 | }() 168 | } 169 | ``` 170 | 171 | ### Presenter Subclass 172 | 173 | The Presenter is class that has generic parameters. 174 | You can create the Presenter subclass like this. 175 | 176 | ```swift 177 | final class CounterPresenter: Presenter { 178 | init(view: T) where T.State == CounterState { 179 | let flux = Flux(state: CounterState(), mutation: CounterMutation()) 180 | super.init(view: view, flux: flux) 181 | } 182 | 183 | func increment() { 184 | dispatch(.increment) 185 | } 186 | 187 | func decrement() { 188 | if state.count > 0 { 189 | dispatch(.decrement) 190 | } 191 | } 192 | } 193 | ``` 194 | 195 | ### Testing 196 | 197 | I'll explain how to test with Prex. 198 | Focus on two test cases in this document. 199 | 200 | | 1. Reflection state testing | 2. Create actions testing | 201 | | :-: | :-: | 202 | | ![](./Images/reflection-test.png) | ![](./Images/func-test.png) | 203 | 204 | Both tests need the View to initialize a Presenter. 205 | You can create **MockView** like this. 206 | 207 | ```swift 208 | final class MockView: View { 209 | var refrectParameters: ((StateChange) -> ())? 210 | 211 | func reflect(change: StateChange) { 212 | refrectParameters?(change) 213 | } 214 | } 215 | ``` 216 | 217 | 218 | 219 | #### 1. Reflection state testing 220 | 221 | This test starts with dispatching an Action. 222 | An action is passed to Mutation, and Mutation mutates state with a received action. 223 | The Store notifies changes of state, and the Presenter calls reflect method of the View to reflects state. 224 | Finally, receives state via reflect method parameters of the View. 225 | 226 | This is a sample test code. 227 | 228 | ```swift 229 | func test_presenter_calls_reflect_of_view_when_state_changed() { 230 | let view = MockView() 231 | let flux = Flux(state: CounterState(), mutation: CounterMutation()) 232 | let presenter = Presenter(view: view, flux: flux) 233 | 234 | let expect = expectation(description: "wait receiving ValueChange") 235 | view.refrectParameters = { change in 236 | let count = change.changedProperty(for: \.count)?.value 237 | XCTAssertEqual(count, 1) 238 | expect.fulfill() 239 | } 240 | 241 | flux.dispatcher.dispatch(.increment) 242 | wait(for: [expect], timeout: 0.1) 243 | } 244 | ``` 245 | 246 | #### 2. Create actions testing 247 | 248 | This test starts with calling the Presenter method as dummy user interaction. 249 | The Presenter accesses side-effect and finally creates an action from that result. 250 | That action is dispatched to the Dispatcher. 251 | Finally, receives action via register callback of the Dispatcher. 252 | 253 | This is a sample test code. 254 | 255 | ```swift 256 | func test_increment_method_of_presenter() { 257 | let view = MockView() 258 | let flux = Flux(state: CounterState(), mutation: CounterMutation()) 259 | let presenter = Presenter(view: view, flux: flux) 260 | 261 | let expect = expectation(description: "wait receiving ValueChange") 262 | let subscription = flux.dispatcher.register { action in 263 | XCTAssertEqual(action, .increment) 264 | expect.fulfill() 265 | } 266 | 267 | presenter.increment() 268 | wait(for: [expect], timeout: 0.1) 269 | flux.dispatcher.unregister(subscription) 270 | } 271 | ``` 272 | 273 | #### An addition 274 | 275 | You can test mutating state like this. 276 | 277 | ```swift 278 | func test_mutation() { 279 | var state = CounterState() 280 | let mutation = CounterMutation() 281 | 282 | mutation.mutate(action: .increment, state: &state) 283 | XCTAssertEqual(state.count, 1) 284 | 285 | mutation.mutate(action: .decrement, state: &state) 286 | XCTAssertEqual(state.count, 0) 287 | } 288 | ``` 289 | 290 | ## Example 291 | 292 | ### Project 293 | 294 | You can try Prex with GitHub Repository Search Application [Example](./Example). 295 | Open PrexSample.xcworkspace and run it! 296 | 297 | ### Playground 298 | 299 | You can try Prex counter sample with Playground! 300 | Open Prex.xcworkspace and build `Prex-iOS`. 301 | Finally, you can run manually in Playground. 302 | 303 | ![](./Images/playground.png) 304 | 305 | ## Requirements 306 | - Xcode 9.4.1 or greater 307 | - iOS 10.0 or greater 308 | - tvOS 10.0 or greater 309 | - macOS 10.10 or greater 310 | - watchOS 3.0 or greater 311 | - Swift 4.1 or greater 312 | 313 | ## Installation 314 | 315 | ### Carthage 316 | 317 | If you’re using [Carthage](https://github.com/Carthage/Carthage), simply add Prex to your `Cartfile`: 318 | 319 | ```ruby 320 | github "marty-suzuki/Prex" 321 | ``` 322 | 323 | ### CocoaPods 324 | 325 | Prex is available through [CocoaPods](https://cocoapods.org). To install it, simply add the following line to your Podfile: 326 | 327 | ```ruby 328 | pod 'Prex' 329 | ``` 330 | 331 | ### Swift Package Manager 332 | 333 | Prex is available through `Swift Package Manager`. Just add the url of this repository to your `Package.swift`. 334 | 335 | ```Package.swift 336 | dependencies: [ 337 | .package(url: "https://github.com/marty-suzuki/Prex.git", from: "0.2.0") 338 | ] 339 | ``` 340 | 341 | ## Inspired by these unidirectional data flow frameworks 342 | 343 | - [VueFlux](https://github.com/ra1028/VueFlux) by [@ra1028](https://github.com/ra1028/VueFlux) 344 | - [ReactorKit](https://github.com/ReactorKit/ReactorKit) by [@devxoul](https://github.com/devxoul) 345 | 346 | ## Author 347 | 348 | marty-suzuki, s1180183@gmail.com 349 | 350 | ## License 351 | 352 | Prex is available under the MIT license. See the [LICENSE](./LICENSE) file for more info. 353 | -------------------------------------------------------------------------------- /Resources/Info-iOS.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Resources/Info-macOS.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2018年 marty-suzuki. All rights reserved. 23 | 24 | 25 | -------------------------------------------------------------------------------- /Resources/Info-tvOS.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Resources/Info-watchOS.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Resources/PrexTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/Prex/Flux/Action.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Action.swift 3 | // Prex 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | 10 | /// Represents user actions 11 | public protocol Action {} 12 | -------------------------------------------------------------------------------- /Sources/Prex/Flux/Dispatcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dispatcher.swift 3 | // Prex 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | /// Dispatches actions to registerd handlers 10 | public final class Dispatcher { 11 | 12 | private let pubsub = PubSub() 13 | 14 | internal init() {} 15 | 16 | /// Registers a actoin dispatch handler 17 | public func register(handler: @escaping (Action) -> ()) -> Subscription { 18 | let token = pubsub.subscribe(handler) 19 | return Subscription(token: token) 20 | } 21 | 22 | /// Unregisters a handler with a subscription 23 | public func unregister(_ subscription: Subscription) { 24 | pubsub.unsubscribe(subscription.token) 25 | } 26 | 27 | /// Dispatches an action 28 | public func dispatch(_ action: Action) { 29 | pubsub.publish(action) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Prex/Flux/Flux.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flux.swift 3 | // Prex 4 | // 5 | // Created by marty-suzuki on 2018/10/02. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | 10 | /// A Flux component container 11 | /// 12 | /// - note: Initializers of Dispatcher and Store are not open to public. 13 | /// But this class resolves dependencies of store and initialize both componet, finally holds them. 14 | public class Flux { 15 | 16 | public let dispatcher = Dispatcher() 17 | public let store: Store 18 | 19 | public init(state: State, mutation: Mutation) where Action == Mutation.Action, State == Mutation.State { 20 | self.store = Store(dispatcher: dispatcher, state: state, mutation: mutation) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Prex/Flux/Store.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Store.swift 3 | // Prex 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - Store 12 | 13 | /// Manages state and mutates state with actions recieved from Dispatcher 14 | public final class Store { 15 | 16 | /// Current state 17 | public private(set) var state: State { 18 | set { 19 | defer { lock.unlock() }; lock.lock() 20 | _state = newValue 21 | } 22 | get { 23 | defer { lock.unlock() }; lock.lock() 24 | return _state 25 | } 26 | } 27 | 28 | private var _state: State { 29 | didSet { 30 | pubsub.publish(StateChange(new: _state, old: oldValue)) 31 | } 32 | } 33 | 34 | private let lock: NSLocking = NSRecursiveLock() 35 | private let pubsub = PubSub>() 36 | private let dispatcher: _AnyDispatcher 37 | private lazy var subscription: _AnySubscription = { fatalError("subscription has not initialized yet") }() 38 | 39 | deinit { 40 | dispatcher.unregister(subscription) 41 | } 42 | 43 | internal init(dispatcher: Dispatcher, state: State, mutation: Mutation) where Action == Mutation.Action, State == Mutation.State { 44 | self._state = state 45 | self.dispatcher = _AnyDispatcher(dispatcher) 46 | let subscription = dispatcher.register { [mutation, weak self] action in 47 | guard let me = self else { 48 | return 49 | } 50 | mutation.mutate(action: action, state: &me.state) 51 | } 52 | self.subscription = _AnySubscription(subscription) 53 | } 54 | 55 | 56 | /// Registers listeners as callback 57 | /// 58 | /// - Parameter callback: Notifies changes of state 59 | public func addListener(callback: @escaping (StateChange) -> ()) -> Subscription { 60 | let token = pubsub.subscribe(callback) 61 | return Subscription(token: token) 62 | } 63 | 64 | 65 | /// Removes listenners with a subscription 66 | public func removeListener(with subscription: Subscription) { 67 | pubsub.unsubscribe(subscription.token) 68 | } 69 | } 70 | 71 | // MARK: - Type Erasure 72 | 73 | private struct _AnySubscription { 74 | 75 | let token: Token 76 | 77 | init(_ subscription: Subscription) { 78 | self.token = subscription.token 79 | } 80 | } 81 | 82 | private struct _AnyDispatcher { 83 | 84 | private let _unregister: (_AnySubscription) -> () 85 | 86 | init(_ dispatcher: Dispatcher) { 87 | self._unregister = { [weak dispatcher] in 88 | let subscription = Subscription(token: $0.token) 89 | dispatcher?.unregister(subscription) 90 | } 91 | } 92 | 93 | func unregister(_ subscription: _AnySubscription) { 94 | _unregister(subscription) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/Prex/Flux/View.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View.swift 3 | // Prex 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | 10 | /// Represent Flux View component 11 | public protocol View: AnyObject { 12 | associatedtype State: Prex.State 13 | 14 | /// This method called when state has changed 15 | /// 16 | /// - Parameter change: Contains new state and old state 17 | func reflect(change: StateChange) 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Prex/Mutation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mutation.swift 3 | // Prex 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | /// Represents mutation that mutate state by actions 10 | public protocol Mutation { 11 | associatedtype Action: Prex.Action 12 | associatedtype State: Prex.State 13 | 14 | func mutate(action: Action, state: inout State) 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Prex/Presenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Presenter.swift 3 | // Prex 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - Presenter 12 | 13 | /// Presenter recieves user actions and dispatchs them, finally notifies changes of state to `View` 14 | open class Presenter { 15 | 16 | /// Current state 17 | open var state: State { 18 | return store.state 19 | } 20 | 21 | private let dispatcher: Dispatcher 22 | private let store: Store 23 | private let reflectInMain: (StateChange) -> () 24 | private lazy var subscription: Subscription = { fatalError("canceller has not initialized yet") }() 25 | 26 | deinit { 27 | store.removeListener(with: subscription) 28 | } 29 | 30 | /// Automatically creates Flux components with `State` and `Mutaion` 31 | public convenience init(view: View, state: State, mutation: Mutation) where Mutation.State == State, Mutation.Action == Action, View.State == State { 32 | let flux = Flux(state: state, mutation: mutation) 33 | self.init(view: view, flux: flux) 34 | } 35 | 36 | /// Use this initializer when injects Flux components 37 | public init(view: View, flux: Flux) where View.State == State { 38 | self.dispatcher = flux.dispatcher 39 | 40 | let _view = _WeakView(view) 41 | self.reflectInMain = { [_view] change in 42 | if Thread.isMainThread { 43 | _view.reflect(change: change) 44 | } else { 45 | DispatchQueue.main.async { 46 | _view.reflect(change: change) 47 | } 48 | } 49 | } 50 | 51 | self.store = flux.store 52 | self.subscription = store.addListener(callback: { [reflectInMain] in reflectInMain($0) }) 53 | } 54 | 55 | /// Dispatches an action 56 | public func dispatch(_ action: Action) { 57 | dispatcher.dispatch(action) 58 | } 59 | 60 | /// Reflects current state to `View` forcefully 61 | public func reflect() { 62 | reflectInMain(StateChange(new: state, old: nil)) 63 | } 64 | } 65 | 66 | // MARK: - _WeakView 67 | 68 | private struct _WeakView { 69 | 70 | private weak var view: View? 71 | 72 | init(_ view: View) { 73 | self.view = view 74 | } 75 | 76 | func reflect(change: StateChange) { 77 | view?.reflect(change: change) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/Prex/Prex.h: -------------------------------------------------------------------------------- 1 | // 2 | // Prex.h 3 | // Prex 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Prex. 12 | FOUNDATION_EXPORT double PrexVersionNumber; 13 | 14 | //! Project version string for Prex. 15 | FOUNDATION_EXPORT const unsigned char PrexVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/Prex/State.swift: -------------------------------------------------------------------------------- 1 | // 2 | // State.swift 3 | // Prex 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | /// Represents state 10 | public protocol State {} 11 | -------------------------------------------------------------------------------- /Sources/Prex/StateChange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StateChange.swift 3 | // Prex 4 | // 5 | // Created by marty-suzuki on 2018/09/29. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | /// Contains new and old value 10 | @dynamicMemberLookup 11 | public struct StateChange { 12 | 13 | /// New value of changes 14 | public let new: State 15 | 16 | /// Old value of changes 17 | public let old: State? 18 | 19 | /// Returns specified value when it has changed 20 | public func changedProperty(for keyPath: KeyPath) -> PropertyChange? { 21 | self[dynamicMember: keyPath] 22 | } 23 | 24 | public subscript(dynamicMember keyPath: KeyPath) -> PropertyChange? { 25 | let newValue = new[keyPath: keyPath] 26 | return newValue == old?[keyPath: keyPath] ? nil : PropertyChange(value: newValue) 27 | } 28 | } 29 | 30 | extension StateChange { 31 | 32 | /// Contains a changed value of specified property 33 | public struct PropertyChange { 34 | public let value: T 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Prex/Subscription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Subscription.swift 3 | // Prex 4 | // 5 | // Created by marty-suzuki on 2018/10/02. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | /// Represents subscription 10 | public struct Subscription { 11 | 12 | let token: Token 13 | 14 | init(token: Token) { 15 | self.token = token 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Prex/internal/PubSub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PubSub.swift 3 | // Prex 4 | // 5 | // Created by marty-suzuki on 2018/10/02. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class PubSub { 12 | public typealias Handler = (T) -> () 13 | 14 | private let lock: NSLocking = NSRecursiveLock() 15 | private var next: UInt = 0 16 | private var handlers: [UInt: Handler] = [:] 17 | 18 | deinit { 19 | defer { lock.unlock() }; lock.lock() 20 | handlers.removeAll() 21 | } 22 | 23 | init() {} 24 | 25 | public func subscribe(_ handler: @escaping Handler) -> Token { 26 | lock.lock(); defer { lock.unlock() } 27 | next += 1 28 | handlers[next] = handler 29 | return Token(next) 30 | } 31 | 32 | public func unsubscribe(_ token: Token) { 33 | lock.lock(); defer { lock.unlock() } 34 | handlers.removeValue(forKey: token.rawValue) 35 | } 36 | 37 | func publish(_ topic: T) { 38 | lock.lock(); defer { lock.unlock() } 39 | handlers.forEach { $0.value(topic) } 40 | } 41 | } 42 | 43 | struct Token { 44 | fileprivate let rawValue: UInt 45 | fileprivate init(_ rawValue: UInt) { 46 | self.rawValue = rawValue 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import PrexTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += PrexTests.allTests() 7 | XCTMain(tests) -------------------------------------------------------------------------------- /Tests/PrexTests/Components.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Components.swift 3 | // PrexTests 4 | // 5 | // Created by marty-suzuki on 2018/10/08. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | @testable import Prex 10 | 11 | enum TestAction: Action { 12 | case test 13 | case test2 14 | } 15 | 16 | struct TestMutation: Mutation { 17 | let mutateCalled = PubSub() 18 | 19 | func mutate(action: TestAction, state: inout TestState) { 20 | switch action { 21 | case .test: 22 | state.testCalledCount += 1 23 | case .test2: 24 | state.test2CalledCount += 1 25 | } 26 | mutateCalled.publish(action) 27 | } 28 | } 29 | 30 | struct TestState: State, Equatable { 31 | fileprivate(set) var testCalledCount: Int = 0 32 | fileprivate(set) var test2CalledCount: Int = 0 33 | fileprivate(set) var testString: String? = nil 34 | } 35 | 36 | final class TestView: View { 37 | let refrectCalled = PubSub>() 38 | 39 | func refrect(change: ValueChange) { 40 | refrectCalled.publish(change) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/PrexTests/DispatcherTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DispatcherTests.swift 3 | // PrexTests 4 | // 5 | // Created by marty-suzuki on 2018/10/08. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Prex 11 | 12 | final class DispatcherTests: XCTestCase { 13 | 14 | private var dispatcher: Dispatcher! 15 | 16 | override func setUp() { 17 | dispatcher = Dispatcher() 18 | } 19 | 20 | func testDispatch() { 21 | let action: TestAction = .test 22 | 23 | let expect = expectation(description: "wait action") 24 | let subscription = dispatcher.register { [action] in 25 | XCTAssertEqual(action, $0) 26 | expect.fulfill() 27 | } 28 | 29 | dispatcher.dispatch(action) 30 | wait(for: [expect], timeout: 0.1) 31 | dispatcher.unregister(subscription) 32 | } 33 | 34 | func testDispatchMultiTimes() { 35 | let actions: [TestAction] = [.test, .test2, .test, .test, .test2] 36 | 37 | let expect = expectation(description: "wait action") 38 | expect.expectedFulfillmentCount = actions.count 39 | 40 | var calledActions: [TestAction] = [] 41 | let subscription = dispatcher.register { 42 | calledActions.append($0) 43 | expect.fulfill() 44 | } 45 | 46 | actions.forEach { 47 | dispatcher.dispatch($0) 48 | } 49 | wait(for: [expect], timeout: 0.1) 50 | dispatcher.unregister(subscription) 51 | 52 | XCTAssertEqual(actions, calledActions) 53 | } 54 | 55 | func testUnregister() { 56 | let expect = expectation(description: "wait action") 57 | expect.isInverted = true 58 | 59 | let subscription = dispatcher.register { action in 60 | expect.fulfill() 61 | XCTFail("dispatcher.register should not call, but it received \(action)") 62 | } 63 | dispatcher.unregister(subscription) 64 | 65 | dispatcher.dispatch(.test) 66 | wait(for: [expect], timeout: 0.1) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/PrexTests/PresenterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PresenterTests.swift 3 | // PrexTests 4 | // 5 | // Created by marty-suzuki on 2018/10/09. 6 | // Copyright © 2018年 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Prex 11 | 12 | final class PresenterTests: XCTestCase { 13 | 14 | private var dependency: Dependency! 15 | 16 | override func setUp() { 17 | dependency = Dependency() 18 | } 19 | 20 | func testDispatch() { 21 | let action: TestAction = .test 22 | 23 | let expect = expectation(description: "wait action") 24 | let subscription = dependency.dispatcher.register { [action] in 25 | XCTAssertEqual(action, $0) 26 | expect.fulfill() 27 | } 28 | 29 | dependency.presenter.dispatch(action) 30 | wait(for: [expect], timeout: 0.1) 31 | dependency.dispatcher.unregister(subscription) 32 | } 33 | 34 | func testReflect() { 35 | let state = dependency.presenter.state 36 | 37 | let expect = expectation(description: "wait refrect") 38 | let token = dependency.view.refrectCalled.subscribe { [state] changes in 39 | XCTAssertNil(changes.old) 40 | XCTAssertEqual(changes.new, state) 41 | expect.fulfill() 42 | } 43 | 44 | dependency.presenter.refrect() 45 | wait(for: [expect], timeout: 0.1) 46 | dependency.view.refrectCalled.unsubscribe(token) 47 | } 48 | 49 | func testStateChanges() { 50 | let state = dependency.presenter.state 51 | let expectState = TestState(testCalledCount: state.testCalledCount + 1, 52 | test2CalledCount: state.test2CalledCount, 53 | testString: state.testString) 54 | 55 | let expect = expectation(description: "wait refrect") 56 | let token = dependency.view.refrectCalled.subscribe { [expectState] changes in 57 | XCTAssertNotNil(changes.old) 58 | XCTAssertEqual(changes.new, expectState) 59 | expect.fulfill() 60 | } 61 | 62 | dependency.presenter.dispatch(.test) 63 | wait(for: [expect], timeout: 0.1) 64 | dependency.view.refrectCalled.unsubscribe(token) 65 | } 66 | 67 | func testDeinit() { 68 | let dispatcher = dependency.dispatcher 69 | let view = dependency.view 70 | 71 | let action: TestAction = .test 72 | 73 | let expect = expectation(description: "wait action") 74 | expect.isInverted = true 75 | let token = view.refrectCalled.subscribe { changes in 76 | expect.fulfill() 77 | XCTFail("view.refrectCalled.subscribe should not call, but it received \(changes)") 78 | } 79 | dependency = nil 80 | 81 | dispatcher.dispatch(action) 82 | wait(for: [expect], timeout: 0.1) 83 | view.refrectCalled.unsubscribe(token) 84 | } 85 | } 86 | 87 | extension PresenterTests { 88 | 89 | private struct Dependency { 90 | 91 | let store: Store 92 | let dispatcher: Dispatcher 93 | let view = TestView() 94 | let presenter: Presenter 95 | 96 | init() { 97 | let flux = Flux(state: TestState(), mutation: TestMutation()) 98 | self.store = flux.store 99 | self.dispatcher = flux.dispatcher 100 | self.presenter = Presenter(view: view, flux: flux) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Tests/PrexTests/PubSubTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PubSubTests.swift 3 | // PrexTests 4 | // 5 | // Created by marty-suzuki on 2018/10/08. 6 | // Copyright © 2018年 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Prex 11 | 12 | final class PubSubTests: XCTestCase { 13 | 14 | private var pubsub: PubSub! 15 | 16 | override func setUp() { 17 | pubsub = PubSub() 18 | } 19 | 20 | private func rand() -> Int { 21 | return Int(arc4random() % 1000) 22 | } 23 | 24 | func testPublish() { 25 | let value = rand() 26 | 27 | let expect = expectation(description: "wait topic") 28 | let token = pubsub.subscribe { [value] in 29 | XCTAssertEqual(value, $0) 30 | expect.fulfill() 31 | } 32 | 33 | pubsub.publish(value) 34 | wait(for: [expect], timeout: 0.1) 35 | pubsub.unsubscribe(token) 36 | } 37 | 38 | func testPublishMultiTimes() { 39 | let values = [rand(), rand(), rand(), rand(), rand()] 40 | 41 | let expect = expectation(description: "wait topic") 42 | expect.expectedFulfillmentCount = values.count 43 | 44 | var calledValues: [Int] = [] 45 | let token = pubsub.subscribe { 46 | calledValues.append($0) 47 | expect.fulfill() 48 | } 49 | 50 | values.forEach { 51 | pubsub.publish($0) 52 | } 53 | wait(for: [expect], timeout: 0.1) 54 | pubsub.unsubscribe(token) 55 | 56 | XCTAssertEqual(values, calledValues) 57 | } 58 | 59 | func testUnsubscribe() { 60 | let expect = expectation(description: "wait topic") 61 | expect.isInverted = true 62 | 63 | let token = pubsub.subscribe { value in 64 | expect.fulfill() 65 | XCTFail("pubsub.subscribe should not call, but it received \(value)") 66 | } 67 | pubsub.unsubscribe(token) 68 | 69 | pubsub.publish(rand()) 70 | wait(for: [expect], timeout: 0.1) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tests/PrexTests/StoreTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoreTests.swift 3 | // PrexTests 4 | // 5 | // Created by marty-suzuki on 2018/10/08. 6 | // Copyright © 2018 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Prex 11 | 12 | final class StoreTests: XCTestCase { 13 | 14 | private var dependency: Dependency! 15 | 16 | override func setUp() { 17 | dependency = Dependency() 18 | } 19 | 20 | func testStoreChanges() { 21 | XCTAssertEqual(dependency.store.state.testCalledCount, 0) 22 | dependency.dispatcher.dispatch(.test) 23 | XCTAssertEqual(dependency.store.state.testCalledCount, 1) 24 | } 25 | 26 | func testMultiStoreChanges() { 27 | let count: Int = 10 28 | XCTAssertEqual(dependency.store.state.testCalledCount, 0) 29 | (0.. 106 | let dispatcher: Dispatcher 107 | 108 | init() { 109 | let flux = Flux(state: TestState(), mutation: mutation) 110 | dispatcher = flux.dispatcher 111 | store = flux.store 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Tests/PrexTests/ValueChangeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValueChangeTests.swift 3 | // PrexTests 4 | // 5 | // Created by marty-suzuki on 2018/10/08. 6 | // Copyright © 2018年 marty-suzuki. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Prex 11 | 12 | final class ValueChangeTests: XCTestCase { 13 | 14 | func testOldIsNil() { 15 | let new = TestState() 16 | let changes = ValueChange(new: new, old: nil) 17 | 18 | let testCalledCount = changes.valueIfChanged(for: \.testCalledCount) 19 | XCTAssertNotNil(testCalledCount) 20 | XCTAssertEqual(testCalledCount, new.testCalledCount) 21 | 22 | let test2CalledCount = changes.valueIfChanged(for: \.test2CalledCount) 23 | XCTAssertNotNil(test2CalledCount) 24 | XCTAssertEqual(test2CalledCount, new.test2CalledCount) 25 | 26 | let changed = changes.valueIfChanged(for: \.testString) 27 | XCTAssertNotNil(changed) 28 | XCTAssertEqual(changed?.value, new.testString) 29 | } 30 | 31 | func testOldIsNotNilAndSameAsNew() { 32 | let new = TestState() 33 | let changes = ValueChange(new: new, old: new) 34 | 35 | let testCalledCount = changes.valueIfChanged(for: \.testCalledCount) 36 | XCTAssertNil(testCalledCount) 37 | 38 | let test2CalledCount = changes.valueIfChanged(for: \.test2CalledCount) 39 | XCTAssertNil(test2CalledCount) 40 | 41 | let changed = changes.valueIfChanged(for: \.testString) 42 | XCTAssertNil(changed) 43 | } 44 | 45 | func testOldIsNotNilAndTestCalledCountChanged() { 46 | let old = TestState() 47 | let new = TestState(testCalledCount: old.testCalledCount + 1, 48 | test2CalledCount: old.test2CalledCount, 49 | testString: old.testString) 50 | let changes = ValueChange(new: new, old: old) 51 | 52 | let testCalledCount = changes.valueIfChanged(for: \.testCalledCount) 53 | XCTAssertNotNil(testCalledCount) 54 | XCTAssertEqual(testCalledCount, new.testCalledCount) 55 | 56 | let test2CalledCount = changes.valueIfChanged(for: \.test2CalledCount) 57 | XCTAssertNil(test2CalledCount) 58 | 59 | let changed = changes.valueIfChanged(for: \.testString) 60 | XCTAssertNil(changed) 61 | } 62 | 63 | func testOldIsNotNilAndTestCalledCount2Changed() { 64 | let old = TestState() 65 | let new = TestState(testCalledCount: old.testCalledCount, 66 | test2CalledCount: old.test2CalledCount + 2, 67 | testString: old.testString) 68 | let changes = ValueChange(new: new, old: old) 69 | 70 | let testCalledCount = changes.valueIfChanged(for: \.testCalledCount) 71 | XCTAssertNil(testCalledCount) 72 | 73 | let test2CalledCount = changes.valueIfChanged(for: \.test2CalledCount) 74 | XCTAssertNotNil(test2CalledCount) 75 | XCTAssertEqual(test2CalledCount, new.test2CalledCount) 76 | 77 | let changed = changes.valueIfChanged(for: \.testString) 78 | XCTAssertNil(changed) 79 | } 80 | 81 | func testOldIsNotNilAndTestStringChangedToNonOptional() { 82 | let old = TestState() 83 | let new = TestState(testCalledCount: old.testCalledCount, 84 | test2CalledCount: old.test2CalledCount, 85 | testString: "test") 86 | let changes = ValueChange(new: new, old: old) 87 | 88 | let testCalledCount = changes.valueIfChanged(for: \.testCalledCount) 89 | XCTAssertNil(testCalledCount) 90 | 91 | let test2CalledCount = changes.valueIfChanged(for: \.test2CalledCount) 92 | XCTAssertNil(test2CalledCount) 93 | 94 | let changed = changes.valueIfChanged(for: \.testString) 95 | XCTAssertNotNil(changed) 96 | XCTAssertEqual(changed?.value, new.testString) 97 | } 98 | 99 | func testOldIsNotNilAndTestStringChangedToOptional() { 100 | let old = TestState(testCalledCount: 0, 101 | test2CalledCount: 0, 102 | testString: "test") 103 | let new = TestState(testCalledCount: old.testCalledCount, 104 | test2CalledCount: old.test2CalledCount, 105 | testString: nil) 106 | let changes = ValueChange(new: new, old: old) 107 | 108 | let testCalledCount = changes.valueIfChanged(for: \.testCalledCount) 109 | XCTAssertNil(testCalledCount) 110 | 111 | let test2CalledCount = changes.valueIfChanged(for: \.test2CalledCount) 112 | XCTAssertNil(test2CalledCount) 113 | 114 | let changed = changes.valueIfChanged(for: \.testString) 115 | XCTAssertNotNil(changed) 116 | XCTAssertEqual(changed?.value, new.testString) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Tests/PrexTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !os(macOS) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(PrexTests.allTests), 7 | ] 8 | } 9 | #endif -------------------------------------------------------------------------------- /Tools/Prex.xctemplate/Common/___FILEBASENAME___.Prex.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import Prex 4 | 5 | enum ___VARIABLE_productName___Action: Action { 6 | 7 | // - FIXME: This is sample case. Please replace this. 8 | case fixme(Int) 9 | } 10 | 11 | struct ___VARIABLE_productName___State: State { 12 | 13 | // - FIXME: This is sample property. Please replace this. 14 | fileprivate(set) var fixme: Int = 0 15 | } 16 | 17 | struct ___VARIABLE_productName___Mutation: Mutation { 18 | 19 | func mutate(action: ___VARIABLE_productName___Action, state: inout ___VARIABLE_productName___State) { 20 | switch action { 21 | 22 | // - FIXME: This is sample case. Please replace this. 23 | case let .fixme(value): 24 | state.fixme = value 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tools/Prex.xctemplate/Default/___FILEBASENAME___Presenter.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import Prex 4 | 5 | extension Presenter where Action == ___VARIABLE_productName___Action, State == ___VARIABLE_productName___State { 6 | 7 | // - FIXME: This is sample function. Please replace this. 8 | func setFixme(_ value: Int) { 9 | dispatch(.fixme(value)) 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /Tools/Prex.xctemplate/Default/___FILEBASENAME___ViewController.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import Prex 4 | import UIKit 5 | 6 | final class ___VARIABLE_productName___ViewController: UIViewController { 7 | 8 | private(set) lazy var presenter = Presenter(view: self, 9 | state: ___VARIABLE_productName___State(), 10 | mutation: ___VARIABLE_productName___Mutation()) 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | // - FIXME: This is sample function call. Please replace this. 16 | presenter.setFixme(12345) 17 | } 18 | } 19 | 20 | extension ___VARIABLE_productName___ViewController: View { 21 | 22 | func reflect(change: StateChange<___VARIABLE_productName___State>) { 23 | 24 | // - FIXME: This is sample if statement. Please replace this. 25 | if let value = change.changedProperty(for: \.fixme)?.value { 26 | print(value) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tools/Prex.xctemplate/PresenterSubclass/___FILEBASENAME___Presenter.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import Prex 4 | 5 | final class ___VARIABLE_productName___Presenter: Presenter<___VARIABLE_productName___Action, ___VARIABLE_productName___State> { 6 | 7 | init(view: View) where View.State == ___VARIABLE_productName___State { 8 | let flux = Flux(state: ___VARIABLE_productName___State(), mutation: ___VARIABLE_productName___Mutation()) 9 | super.init(view: view, flux: flux) 10 | } 11 | 12 | // - FIXME: This is sample function. Please replace this. 13 | func setFixme(_ value: Int) { 14 | dispatch(.fixme(value)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Tools/Prex.xctemplate/PresenterSubclass/___FILEBASENAME___ViewController.swift: -------------------------------------------------------------------------------- 1 | //___FILEHEADER___ 2 | 3 | import Prex 4 | import UIKit 5 | 6 | final class ___VARIABLE_productName___ViewController: UIViewController { 7 | 8 | private(set) lazy var presenter = ___VARIABLE_productName___Presenter(view: self) 9 | 10 | override func viewDidLoad() { 11 | super.viewDidLoad() 12 | 13 | // - FIXME: This is sample function call. Please replace this. 14 | presenter.setFixme(12345) 15 | } 16 | } 17 | 18 | extension ___VARIABLE_productName___ViewController: View { 19 | 20 | func reflect(change: StateChange<___VARIABLE_productName___State>) { 21 | 22 | // - FIXME: This is sample if statement. Please replace this. 23 | if let value = change.changedProperty(for: \.fixme)?.value { 24 | print(value) 25 | } 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /Tools/Prex.xctemplate/TemplateIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marty-suzuki/Prex/d6c198fc96e0155f658be55a1798aaa287acdc1b/Tools/Prex.xctemplate/TemplateIcon.png -------------------------------------------------------------------------------- /Tools/Prex.xctemplate/TemplateIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marty-suzuki/Prex/d6c198fc96e0155f658be55a1798aaa287acdc1b/Tools/Prex.xctemplate/TemplateIcon@2x.png -------------------------------------------------------------------------------- /Tools/Prex.xctemplate/TemplateInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kind 6 | Xcode.IDEFoundation.TextSubstitutionFileTemplateKind 7 | Description 8 | Prex components. 9 | Summary 10 | Prex components. 11 | SortOrder 12 | 1 13 | DefaultCompletionName 14 | Prex 15 | Platforms 16 | 17 | com.apple.platform.iphoneos 18 | 19 | Options 20 | 21 | 22 | Identifier 23 | productName 24 | Required 25 | true 26 | Name 27 | Prex name: 28 | Description 29 | The name of the Prex to create 30 | Type 31 | text 32 | NotPersisted 33 | true 34 | 35 | 36 | Identifier 37 | PresenterSubclass 38 | Required 39 | true 40 | Name 41 | Use Presenter subclass 42 | Description 43 | Whether this Prex uses a subclass of Presenter 44 | Type 45 | checkbox 46 | Default 47 | true 48 | NotPersisted 49 | true 50 | 51 | 52 | Identifier 53 | WithXIB 54 | Name 55 | Also create XIB file for user interface 56 | Description 57 | Whether to create a XIB file with the same name 58 | Type 59 | checkbox 60 | RequiredOptions 61 | 62 | withStoryboard 63 | false 64 | 65 | Default 66 | true 67 | NotPersisted 68 | true 69 | 70 | 71 | Identifier 72 | WithStoryboard 73 | Required 74 | true 75 | Name 76 | Also create Storyboard file for user interface 77 | Description 78 | Whether to create a Storyboard file with the same name 79 | Type 80 | checkbox 81 | Default 82 | false 83 | NotPersisted 84 | true 85 | RequiredOptions 86 | 87 | withXIB 88 | false 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /Tools/Prex.xctemplate/WithStoryboard/___FILEBASENAME___ViewController.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Tools/Prex.xctemplate/WithXIB/___FILEBASENAME___ViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Tools/README.md: -------------------------------------------------------------------------------- 1 | # Prex Xcode Templete 2 | -------------------------------------------------------------------------------- /Tools/install-xcode-template.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Configuration 4 | XCODE_TEMPLATE_DIR=$HOME'/Library/Developer/Xcode/Templates/File Templates/Prex' 5 | PREX_TEMPLATE_DIR="$XCODE_TEMPLATE_DIR"/Prex.xctemplate 6 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 7 | 8 | # Copy Prex file templates into the local Prex template directory 9 | xcodeTemplate () { 10 | echo "==> Copying up Prex Xcode file templates..." 11 | 12 | if [ -d "$XCODE_TEMPLATE_DIR" ]; then 13 | rm -R "$XCODE_TEMPLATE_DIR" 14 | fi 15 | mkdir -p "$XCODE_TEMPLATE_DIR" 16 | 17 | cp -R $SCRIPT_DIR/Prex.xctemplate "$XCODE_TEMPLATE_DIR" 18 | cp -R "$PREX_TEMPLATE_DIR"/Common/* "$PREX_TEMPLATE_DIR"/Default 19 | cp -R "$PREX_TEMPLATE_DIR"/Common/* "$PREX_TEMPLATE_DIR"/PresenterSubclass 20 | rm -R "$PREX_TEMPLATE_DIR"/Common 21 | mkdir -p "$PREX_TEMPLATE_DIR"/PresenterSubclassWithXIB 22 | mkdir -p "$PREX_TEMPLATE_DIR"/PresenterSubclassWithStoryboard 23 | cp -R "$PREX_TEMPLATE_DIR"/WithXIB/* "$PREX_TEMPLATE_DIR"/PresenterSubclassWithXIB/ 24 | cp -R "$PREX_TEMPLATE_DIR"/WithStoryboard/* "$PREX_TEMPLATE_DIR"/PresenterSubclassWithStoryboard/ 25 | cp -R "$PREX_TEMPLATE_DIR"/PresenterSubclass/* "$PREX_TEMPLATE_DIR"/PresenterSubclassWithXIB/ 26 | cp -R "$PREX_TEMPLATE_DIR"/PresenterSubclass/* "$PREX_TEMPLATE_DIR"/PresenterSubclassWithStoryboard/ 27 | cp -R "$PREX_TEMPLATE_DIR"/Default/* "$PREX_TEMPLATE_DIR"/WithXIB/ 28 | cp -R "$PREX_TEMPLATE_DIR"/Default/* "$PREX_TEMPLATE_DIR"/WithStoryboard/ 29 | } 30 | 31 | xcodeTemplate 32 | 33 | echo "==> ... success!" 34 | echo "==> Prex have been set up. In Xcode, select 'New File...' to use Prex templates." 35 | --------------------------------------------------------------------------------