├── .gitignore ├── README.md ├── Reversi.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── Reversi ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── CellColor.colorset │ │ └── Contents.json │ ├── Contents.json │ ├── DarkColor.colorset │ │ └── Contents.json │ └── LightColor.colorset │ │ └── Contents.json ├── BoardView.swift ├── ColorExtensions.swift ├── Computer.swift ├── ContentView.swift ├── DiskView.swift ├── EnvironmentExtensions.swift ├── GameView.swift ├── Info.plist ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── ReversiApp.swift ├── Saver.swift └── UIKit │ ├── _BoardView.swift │ ├── _CellView.swift │ └── _DiskView.swift ├── ReversiTests ├── Info.plist └── ReversiTests.swift ├── ReversiUITests ├── Info.plist └── ReversiUITests.swift └── img ├── data-flow.png ├── dependencies.png ├── reversi.png └── states.png /.gitignore: -------------------------------------------------------------------------------- 1 | /*.xcodeproj/xcuserdata 2 | /*.xcodeproj/project.xcworkspace/xcuserdata 3 | /*.xcworkspace/xcuserdata 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftUIで作るリバーシアプリ 2 | 3 | 本リポジトリは[リバーシ](https://ja.wikipedia.org/wiki/%E3%82%AA%E3%82%BB%E3%83%AD_(%E3%83%9C%E3%83%BC%E3%83%89%E3%82%B2%E3%83%BC%E3%83%A0))の iOS アプリを SwiftUI で実装したものです。 4 | 5 | ![](img/reversi.png) 6 | 7 | ## アプリの構成 8 | 9 | 本アプリはコードの依存関係を適切に扱うために、三つのモジュールに分けれられています。 10 | 11 | | モジュール | リポジトリ | 12 | |:--|:--| 13 | | `SwiftyReversi` | [koher/swifty-reversi](https://github.com/koher/swifty-reversi) | 14 | | `ReversiLogics` | [koher/reversi-logics-swift](https://github.com/koher/reversi-logics-swift) | 15 | | `Reversi` | 本リポジトリ | 16 | 17 | `SwiftyReversi` および `ReversiLogics` は SwiftPM を用いて本リポジトリの Xcode プロジェクトに組み込まれています。モジュールの依存関係は次の通りです。 18 | 19 | ![](img/dependencies.png) 20 | 21 | ### `SwiftyReversi` 22 | 23 | Swift で実装された汎用的リバーシライブラリです。本アプリの仕様から独立しており、 [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) の Entity に該当します。 24 | 25 | ### `ReversiLogics` 26 | 27 | `SwiftyReversi` と違い、アプリ特有のロジックを実装するレイヤーです。ただし、 UI やファイル I/O などからは独立しているため、テストが記述しやすくなっています。 28 | 29 | 内部的には `UseCases` と `Presenters` の二つのモジュールに分かれています。前者は `GameManager` が、後者は `GamePresenter` が中心的な役割を果たします。 30 | 31 | `GameManager` はアプリの状態遷移を扱っており、たとえば下図は `playState` で表される状態の遷移を表したものです。 32 | 33 | ![](img/states.png) 34 | 35 | `GamePresenter` は `GameManager` と UI を仲介し、必要に応じてデータを変換します。 36 | 37 | ### `Reversi` 38 | 39 | SwiftUI で実装されたアプリ本体です。 `ReversiApp` がエントリーポイントで、 `GameView` がメインの `View` です。 `GameView` が `GamePresenter` を介して `GameManager` を保持し、それによって UI と状態管理のロジックのインタラクションを実現しています。 40 | 41 | `GamePresenter` は `GameView` からの入力と `GameManager` からの出力を仲介しますが、入力と出力は干渉しないため一方向のデータフローが実現されています。 42 | 43 | ![](img/data-flow.png) 44 | 45 | ## UIKit との連携 46 | 47 | 本アプリは [refactoring-challenge/reversi-ios](https://github.com/refactoring-challenge/reversi-ios) の実装例の一つです。 refactoring-challenge/reversi-ios は UIKit で実装された `BoardView`, `CellView`, `DiskView` を提供しています。本アプリではそれぞれ `_BoardView`, `_CellView`, `_DiskView` としてリネームした上で取り込み、 `UIViewRepresentable` を用いて SwiftUI 用のラッパーを提供することで `GameView` から利用しています。 -------------------------------------------------------------------------------- /Reversi.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | D61778FB24F2D03E0028C846 /* ColorExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61778FA24F2D03E0028C846 /* ColorExtensions.swift */; }; 11 | D672EFA424F24090001C7E91 /* ReversiApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D672EFA324F24090001C7E91 /* ReversiApp.swift */; }; 12 | D672EFA824F24091001C7E91 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D672EFA724F24091001C7E91 /* Assets.xcassets */; }; 13 | D672EFAB24F24091001C7E91 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D672EFAA24F24091001C7E91 /* Preview Assets.xcassets */; }; 14 | D672EFB624F24091001C7E91 /* ReversiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D672EFB524F24091001C7E91 /* ReversiTests.swift */; }; 15 | D672EFC124F24091001C7E91 /* ReversiUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D672EFC024F24091001C7E91 /* ReversiUITests.swift */; }; 16 | D672EFCF24F24122001C7E91 /* DiskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D672EFCE24F24122001C7E91 /* DiskView.swift */; }; 17 | D672EFD224F2415A001C7E91 /* ReversiLogics in Frameworks */ = {isa = PBXBuildFile; productRef = D672EFD124F2415A001C7E91 /* ReversiLogics */; }; 18 | D672EFD824F24E8C001C7E91 /* BoardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D672EFD724F24E8C001C7E91 /* BoardView.swift */; }; 19 | D672EFDB24F25257001C7E91 /* GameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D672EFDA24F25257001C7E91 /* GameView.swift */; }; 20 | D6B461B224F5E5F700392128 /* Saver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B461B124F5E5F700392128 /* Saver.swift */; }; 21 | D6BD0DC224F7F1C900851290 /* Computer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BD0DC124F7F1C900851290 /* Computer.swift */; }; 22 | D6CAAF2F2515D784004DAE45 /* _DiskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CAAF2C2515D784004DAE45 /* _DiskView.swift */; }; 23 | D6CAAF302515D784004DAE45 /* _BoardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CAAF2D2515D784004DAE45 /* _BoardView.swift */; }; 24 | D6CAAF312515D784004DAE45 /* _CellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CAAF2E2515D784004DAE45 /* _CellView.swift */; }; 25 | D6CD6A1124FA485500509101 /* EnvironmentExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CD6A1024FA485500509101 /* EnvironmentExtensions.swift */; }; 26 | D6CD6A1324FA4B6700509101 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CD6A1224FA4B6700509101 /* ContentView.swift */; }; 27 | /* End PBXBuildFile section */ 28 | 29 | /* Begin PBXContainerItemProxy section */ 30 | D672EFB224F24091001C7E91 /* PBXContainerItemProxy */ = { 31 | isa = PBXContainerItemProxy; 32 | containerPortal = D672EF9824F24090001C7E91 /* Project object */; 33 | proxyType = 1; 34 | remoteGlobalIDString = D672EF9F24F24090001C7E91; 35 | remoteInfo = Reversi; 36 | }; 37 | D672EFBD24F24091001C7E91 /* PBXContainerItemProxy */ = { 38 | isa = PBXContainerItemProxy; 39 | containerPortal = D672EF9824F24090001C7E91 /* Project object */; 40 | proxyType = 1; 41 | remoteGlobalIDString = D672EF9F24F24090001C7E91; 42 | remoteInfo = Reversi; 43 | }; 44 | /* End PBXContainerItemProxy section */ 45 | 46 | /* Begin PBXFileReference section */ 47 | D61778FA24F2D03E0028C846 /* ColorExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtensions.swift; sourceTree = ""; }; 48 | D672EFA024F24090001C7E91 /* Reversi.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Reversi.app; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | D672EFA324F24090001C7E91 /* ReversiApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReversiApp.swift; sourceTree = ""; }; 50 | D672EFA724F24091001C7E91 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 51 | D672EFAA24F24091001C7E91 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 52 | D672EFAC24F24091001C7E91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 53 | D672EFB124F24091001C7E91 /* ReversiTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReversiTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | D672EFB524F24091001C7E91 /* ReversiTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReversiTests.swift; sourceTree = ""; }; 55 | D672EFB724F24091001C7E91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56 | D672EFBC24F24091001C7E91 /* ReversiUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReversiUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 57 | D672EFC024F24091001C7E91 /* ReversiUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReversiUITests.swift; sourceTree = ""; }; 58 | D672EFC224F24091001C7E91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 59 | D672EFCE24F24122001C7E91 /* DiskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskView.swift; sourceTree = ""; }; 60 | D672EFD724F24E8C001C7E91 /* BoardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoardView.swift; sourceTree = ""; }; 61 | D672EFDA24F25257001C7E91 /* GameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameView.swift; sourceTree = ""; }; 62 | D6B461B124F5E5F700392128 /* Saver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Saver.swift; sourceTree = ""; }; 63 | D6BD0DC124F7F1C900851290 /* Computer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Computer.swift; sourceTree = ""; }; 64 | D6CAAF2C2515D784004DAE45 /* _DiskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _DiskView.swift; sourceTree = ""; }; 65 | D6CAAF2D2515D784004DAE45 /* _BoardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _BoardView.swift; sourceTree = ""; }; 66 | D6CAAF2E2515D784004DAE45 /* _CellView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _CellView.swift; sourceTree = ""; }; 67 | D6CD6A1024FA485500509101 /* EnvironmentExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentExtensions.swift; sourceTree = ""; }; 68 | D6CD6A1224FA4B6700509101 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 69 | /* End PBXFileReference section */ 70 | 71 | /* Begin PBXFrameworksBuildPhase section */ 72 | D672EF9D24F24090001C7E91 /* Frameworks */ = { 73 | isa = PBXFrameworksBuildPhase; 74 | buildActionMask = 2147483647; 75 | files = ( 76 | D672EFD224F2415A001C7E91 /* ReversiLogics in Frameworks */, 77 | ); 78 | runOnlyForDeploymentPostprocessing = 0; 79 | }; 80 | D672EFAE24F24091001C7E91 /* Frameworks */ = { 81 | isa = PBXFrameworksBuildPhase; 82 | buildActionMask = 2147483647; 83 | files = ( 84 | ); 85 | runOnlyForDeploymentPostprocessing = 0; 86 | }; 87 | D672EFB924F24091001C7E91 /* Frameworks */ = { 88 | isa = PBXFrameworksBuildPhase; 89 | buildActionMask = 2147483647; 90 | files = ( 91 | ); 92 | runOnlyForDeploymentPostprocessing = 0; 93 | }; 94 | /* End PBXFrameworksBuildPhase section */ 95 | 96 | /* Begin PBXGroup section */ 97 | D672EF9724F24090001C7E91 = { 98 | isa = PBXGroup; 99 | children = ( 100 | D672EFA224F24090001C7E91 /* Reversi */, 101 | D672EFB424F24091001C7E91 /* ReversiTests */, 102 | D672EFBF24F24091001C7E91 /* ReversiUITests */, 103 | D672EFA124F24090001C7E91 /* Products */, 104 | ); 105 | sourceTree = ""; 106 | }; 107 | D672EFA124F24090001C7E91 /* Products */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | D672EFA024F24090001C7E91 /* Reversi.app */, 111 | D672EFB124F24091001C7E91 /* ReversiTests.xctest */, 112 | D672EFBC24F24091001C7E91 /* ReversiUITests.xctest */, 113 | ); 114 | name = Products; 115 | sourceTree = ""; 116 | }; 117 | D672EFA224F24090001C7E91 /* Reversi */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | D672EFD724F24E8C001C7E91 /* BoardView.swift */, 121 | D61778FA24F2D03E0028C846 /* ColorExtensions.swift */, 122 | D6BD0DC124F7F1C900851290 /* Computer.swift */, 123 | D6CD6A1224FA4B6700509101 /* ContentView.swift */, 124 | D672EFCE24F24122001C7E91 /* DiskView.swift */, 125 | D6CD6A1024FA485500509101 /* EnvironmentExtensions.swift */, 126 | D672EFDA24F25257001C7E91 /* GameView.swift */, 127 | D672EFA324F24090001C7E91 /* ReversiApp.swift */, 128 | D6B461B124F5E5F700392128 /* Saver.swift */, 129 | D6CAAF2B2515D765004DAE45 /* UIKit */, 130 | D672EFA724F24091001C7E91 /* Assets.xcassets */, 131 | D672EFAC24F24091001C7E91 /* Info.plist */, 132 | D672EFA924F24091001C7E91 /* Preview Content */, 133 | ); 134 | path = Reversi; 135 | sourceTree = ""; 136 | }; 137 | D672EFA924F24091001C7E91 /* Preview Content */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | D672EFAA24F24091001C7E91 /* Preview Assets.xcassets */, 141 | ); 142 | path = "Preview Content"; 143 | sourceTree = ""; 144 | }; 145 | D672EFB424F24091001C7E91 /* ReversiTests */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | D672EFB524F24091001C7E91 /* ReversiTests.swift */, 149 | D672EFB724F24091001C7E91 /* Info.plist */, 150 | ); 151 | path = ReversiTests; 152 | sourceTree = ""; 153 | }; 154 | D672EFBF24F24091001C7E91 /* ReversiUITests */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | D672EFC024F24091001C7E91 /* ReversiUITests.swift */, 158 | D672EFC224F24091001C7E91 /* Info.plist */, 159 | ); 160 | path = ReversiUITests; 161 | sourceTree = ""; 162 | }; 163 | D6CAAF2B2515D765004DAE45 /* UIKit */ = { 164 | isa = PBXGroup; 165 | children = ( 166 | D6CAAF2D2515D784004DAE45 /* _BoardView.swift */, 167 | D6CAAF2E2515D784004DAE45 /* _CellView.swift */, 168 | D6CAAF2C2515D784004DAE45 /* _DiskView.swift */, 169 | ); 170 | path = UIKit; 171 | sourceTree = ""; 172 | }; 173 | /* End PBXGroup section */ 174 | 175 | /* Begin PBXNativeTarget section */ 176 | D672EF9F24F24090001C7E91 /* Reversi */ = { 177 | isa = PBXNativeTarget; 178 | buildConfigurationList = D672EFC524F24091001C7E91 /* Build configuration list for PBXNativeTarget "Reversi" */; 179 | buildPhases = ( 180 | D672EF9C24F24090001C7E91 /* Sources */, 181 | D672EF9D24F24090001C7E91 /* Frameworks */, 182 | D672EF9E24F24090001C7E91 /* Resources */, 183 | ); 184 | buildRules = ( 185 | ); 186 | dependencies = ( 187 | ); 188 | name = Reversi; 189 | packageProductDependencies = ( 190 | D672EFD124F2415A001C7E91 /* ReversiLogics */, 191 | ); 192 | productName = Reversi; 193 | productReference = D672EFA024F24090001C7E91 /* Reversi.app */; 194 | productType = "com.apple.product-type.application"; 195 | }; 196 | D672EFB024F24091001C7E91 /* ReversiTests */ = { 197 | isa = PBXNativeTarget; 198 | buildConfigurationList = D672EFC824F24091001C7E91 /* Build configuration list for PBXNativeTarget "ReversiTests" */; 199 | buildPhases = ( 200 | D672EFAD24F24091001C7E91 /* Sources */, 201 | D672EFAE24F24091001C7E91 /* Frameworks */, 202 | D672EFAF24F24091001C7E91 /* Resources */, 203 | ); 204 | buildRules = ( 205 | ); 206 | dependencies = ( 207 | D672EFB324F24091001C7E91 /* PBXTargetDependency */, 208 | ); 209 | name = ReversiTests; 210 | productName = ReversiTests; 211 | productReference = D672EFB124F24091001C7E91 /* ReversiTests.xctest */; 212 | productType = "com.apple.product-type.bundle.unit-test"; 213 | }; 214 | D672EFBB24F24091001C7E91 /* ReversiUITests */ = { 215 | isa = PBXNativeTarget; 216 | buildConfigurationList = D672EFCB24F24091001C7E91 /* Build configuration list for PBXNativeTarget "ReversiUITests" */; 217 | buildPhases = ( 218 | D672EFB824F24091001C7E91 /* Sources */, 219 | D672EFB924F24091001C7E91 /* Frameworks */, 220 | D672EFBA24F24091001C7E91 /* Resources */, 221 | ); 222 | buildRules = ( 223 | ); 224 | dependencies = ( 225 | D672EFBE24F24091001C7E91 /* PBXTargetDependency */, 226 | ); 227 | name = ReversiUITests; 228 | productName = ReversiUITests; 229 | productReference = D672EFBC24F24091001C7E91 /* ReversiUITests.xctest */; 230 | productType = "com.apple.product-type.bundle.ui-testing"; 231 | }; 232 | /* End PBXNativeTarget section */ 233 | 234 | /* Begin PBXProject section */ 235 | D672EF9824F24090001C7E91 /* Project object */ = { 236 | isa = PBXProject; 237 | attributes = { 238 | LastSwiftUpdateCheck = 1200; 239 | LastUpgradeCheck = 1200; 240 | TargetAttributes = { 241 | D672EF9F24F24090001C7E91 = { 242 | CreatedOnToolsVersion = 12.0; 243 | }; 244 | D672EFB024F24091001C7E91 = { 245 | CreatedOnToolsVersion = 12.0; 246 | TestTargetID = D672EF9F24F24090001C7E91; 247 | }; 248 | D672EFBB24F24091001C7E91 = { 249 | CreatedOnToolsVersion = 12.0; 250 | TestTargetID = D672EF9F24F24090001C7E91; 251 | }; 252 | }; 253 | }; 254 | buildConfigurationList = D672EF9B24F24090001C7E91 /* Build configuration list for PBXProject "Reversi" */; 255 | compatibilityVersion = "Xcode 9.3"; 256 | developmentRegion = en; 257 | hasScannedForEncodings = 0; 258 | knownRegions = ( 259 | en, 260 | Base, 261 | ); 262 | mainGroup = D672EF9724F24090001C7E91; 263 | packageReferences = ( 264 | D672EFD024F2415A001C7E91 /* XCRemoteSwiftPackageReference "reversi-logics-swift" */, 265 | ); 266 | productRefGroup = D672EFA124F24090001C7E91 /* Products */; 267 | projectDirPath = ""; 268 | projectRoot = ""; 269 | targets = ( 270 | D672EF9F24F24090001C7E91 /* Reversi */, 271 | D672EFB024F24091001C7E91 /* ReversiTests */, 272 | D672EFBB24F24091001C7E91 /* ReversiUITests */, 273 | ); 274 | }; 275 | /* End PBXProject section */ 276 | 277 | /* Begin PBXResourcesBuildPhase section */ 278 | D672EF9E24F24090001C7E91 /* Resources */ = { 279 | isa = PBXResourcesBuildPhase; 280 | buildActionMask = 2147483647; 281 | files = ( 282 | D672EFAB24F24091001C7E91 /* Preview Assets.xcassets in Resources */, 283 | D672EFA824F24091001C7E91 /* Assets.xcassets in Resources */, 284 | ); 285 | runOnlyForDeploymentPostprocessing = 0; 286 | }; 287 | D672EFAF24F24091001C7E91 /* Resources */ = { 288 | isa = PBXResourcesBuildPhase; 289 | buildActionMask = 2147483647; 290 | files = ( 291 | ); 292 | runOnlyForDeploymentPostprocessing = 0; 293 | }; 294 | D672EFBA24F24091001C7E91 /* Resources */ = { 295 | isa = PBXResourcesBuildPhase; 296 | buildActionMask = 2147483647; 297 | files = ( 298 | ); 299 | runOnlyForDeploymentPostprocessing = 0; 300 | }; 301 | /* End PBXResourcesBuildPhase section */ 302 | 303 | /* Begin PBXSourcesBuildPhase section */ 304 | D672EF9C24F24090001C7E91 /* Sources */ = { 305 | isa = PBXSourcesBuildPhase; 306 | buildActionMask = 2147483647; 307 | files = ( 308 | D6BD0DC224F7F1C900851290 /* Computer.swift in Sources */, 309 | D672EFD824F24E8C001C7E91 /* BoardView.swift in Sources */, 310 | D672EFCF24F24122001C7E91 /* DiskView.swift in Sources */, 311 | D672EFDB24F25257001C7E91 /* GameView.swift in Sources */, 312 | D672EFA424F24090001C7E91 /* ReversiApp.swift in Sources */, 313 | D6CD6A1324FA4B6700509101 /* ContentView.swift in Sources */, 314 | D61778FB24F2D03E0028C846 /* ColorExtensions.swift in Sources */, 315 | D6CD6A1124FA485500509101 /* EnvironmentExtensions.swift in Sources */, 316 | D6CAAF312515D784004DAE45 /* _CellView.swift in Sources */, 317 | D6CAAF302515D784004DAE45 /* _BoardView.swift in Sources */, 318 | D6B461B224F5E5F700392128 /* Saver.swift in Sources */, 319 | D6CAAF2F2515D784004DAE45 /* _DiskView.swift in Sources */, 320 | ); 321 | runOnlyForDeploymentPostprocessing = 0; 322 | }; 323 | D672EFAD24F24091001C7E91 /* Sources */ = { 324 | isa = PBXSourcesBuildPhase; 325 | buildActionMask = 2147483647; 326 | files = ( 327 | D672EFB624F24091001C7E91 /* ReversiTests.swift in Sources */, 328 | ); 329 | runOnlyForDeploymentPostprocessing = 0; 330 | }; 331 | D672EFB824F24091001C7E91 /* Sources */ = { 332 | isa = PBXSourcesBuildPhase; 333 | buildActionMask = 2147483647; 334 | files = ( 335 | D672EFC124F24091001C7E91 /* ReversiUITests.swift in Sources */, 336 | ); 337 | runOnlyForDeploymentPostprocessing = 0; 338 | }; 339 | /* End PBXSourcesBuildPhase section */ 340 | 341 | /* Begin PBXTargetDependency section */ 342 | D672EFB324F24091001C7E91 /* PBXTargetDependency */ = { 343 | isa = PBXTargetDependency; 344 | target = D672EF9F24F24090001C7E91 /* Reversi */; 345 | targetProxy = D672EFB224F24091001C7E91 /* PBXContainerItemProxy */; 346 | }; 347 | D672EFBE24F24091001C7E91 /* PBXTargetDependency */ = { 348 | isa = PBXTargetDependency; 349 | target = D672EF9F24F24090001C7E91 /* Reversi */; 350 | targetProxy = D672EFBD24F24091001C7E91 /* PBXContainerItemProxy */; 351 | }; 352 | /* End PBXTargetDependency section */ 353 | 354 | /* Begin XCBuildConfiguration section */ 355 | D672EFC324F24091001C7E91 /* Debug */ = { 356 | isa = XCBuildConfiguration; 357 | buildSettings = { 358 | ALWAYS_SEARCH_USER_PATHS = NO; 359 | CLANG_ANALYZER_NONNULL = YES; 360 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 361 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 362 | CLANG_CXX_LIBRARY = "libc++"; 363 | CLANG_ENABLE_MODULES = YES; 364 | CLANG_ENABLE_OBJC_ARC = YES; 365 | CLANG_ENABLE_OBJC_WEAK = YES; 366 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 367 | CLANG_WARN_BOOL_CONVERSION = YES; 368 | CLANG_WARN_COMMA = YES; 369 | CLANG_WARN_CONSTANT_CONVERSION = YES; 370 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 371 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 372 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 373 | CLANG_WARN_EMPTY_BODY = YES; 374 | CLANG_WARN_ENUM_CONVERSION = YES; 375 | CLANG_WARN_INFINITE_RECURSION = YES; 376 | CLANG_WARN_INT_CONVERSION = YES; 377 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 378 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 379 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 380 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 381 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 382 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 383 | CLANG_WARN_STRICT_PROTOTYPES = YES; 384 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 385 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 386 | CLANG_WARN_UNREACHABLE_CODE = YES; 387 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 388 | COPY_PHASE_STRIP = NO; 389 | DEBUG_INFORMATION_FORMAT = dwarf; 390 | ENABLE_STRICT_OBJC_MSGSEND = YES; 391 | ENABLE_TESTABILITY = YES; 392 | GCC_C_LANGUAGE_STANDARD = gnu11; 393 | GCC_DYNAMIC_NO_PIC = NO; 394 | GCC_NO_COMMON_BLOCKS = YES; 395 | GCC_OPTIMIZATION_LEVEL = 0; 396 | GCC_PREPROCESSOR_DEFINITIONS = ( 397 | "DEBUG=1", 398 | "$(inherited)", 399 | ); 400 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 401 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 402 | GCC_WARN_UNDECLARED_SELECTOR = YES; 403 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 404 | GCC_WARN_UNUSED_FUNCTION = YES; 405 | GCC_WARN_UNUSED_VARIABLE = YES; 406 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 407 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 408 | MTL_FAST_MATH = YES; 409 | ONLY_ACTIVE_ARCH = YES; 410 | SDKROOT = iphoneos; 411 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 412 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 413 | }; 414 | name = Debug; 415 | }; 416 | D672EFC424F24091001C7E91 /* Release */ = { 417 | isa = XCBuildConfiguration; 418 | buildSettings = { 419 | ALWAYS_SEARCH_USER_PATHS = NO; 420 | CLANG_ANALYZER_NONNULL = YES; 421 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 422 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 423 | CLANG_CXX_LIBRARY = "libc++"; 424 | CLANG_ENABLE_MODULES = YES; 425 | CLANG_ENABLE_OBJC_ARC = YES; 426 | CLANG_ENABLE_OBJC_WEAK = YES; 427 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 428 | CLANG_WARN_BOOL_CONVERSION = YES; 429 | CLANG_WARN_COMMA = YES; 430 | CLANG_WARN_CONSTANT_CONVERSION = YES; 431 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 432 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 433 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 434 | CLANG_WARN_EMPTY_BODY = YES; 435 | CLANG_WARN_ENUM_CONVERSION = YES; 436 | CLANG_WARN_INFINITE_RECURSION = YES; 437 | CLANG_WARN_INT_CONVERSION = YES; 438 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 439 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 440 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 441 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 442 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 443 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 444 | CLANG_WARN_STRICT_PROTOTYPES = YES; 445 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 446 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 447 | CLANG_WARN_UNREACHABLE_CODE = YES; 448 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 449 | COPY_PHASE_STRIP = NO; 450 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 451 | ENABLE_NS_ASSERTIONS = NO; 452 | ENABLE_STRICT_OBJC_MSGSEND = YES; 453 | GCC_C_LANGUAGE_STANDARD = gnu11; 454 | GCC_NO_COMMON_BLOCKS = YES; 455 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 456 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 457 | GCC_WARN_UNDECLARED_SELECTOR = YES; 458 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 459 | GCC_WARN_UNUSED_FUNCTION = YES; 460 | GCC_WARN_UNUSED_VARIABLE = YES; 461 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 462 | MTL_ENABLE_DEBUG_INFO = NO; 463 | MTL_FAST_MATH = YES; 464 | SDKROOT = iphoneos; 465 | SWIFT_COMPILATION_MODE = wholemodule; 466 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 467 | VALIDATE_PRODUCT = YES; 468 | }; 469 | name = Release; 470 | }; 471 | D672EFC624F24091001C7E91 /* Debug */ = { 472 | isa = XCBuildConfiguration; 473 | buildSettings = { 474 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 475 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 476 | CODE_SIGN_STYLE = Automatic; 477 | DEVELOPMENT_ASSET_PATHS = "\"Reversi/Preview Content\""; 478 | DEVELOPMENT_TEAM = AQ3JC8F3HL; 479 | ENABLE_PREVIEWS = YES; 480 | INFOPLIST_FILE = Reversi/Info.plist; 481 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 482 | LD_RUNPATH_SEARCH_PATHS = ( 483 | "$(inherited)", 484 | "@executable_path/Frameworks", 485 | ); 486 | PRODUCT_BUNDLE_IDENTIFIER = org.koherent.Reversi; 487 | PRODUCT_NAME = "$(TARGET_NAME)"; 488 | SWIFT_VERSION = 5.0; 489 | TARGETED_DEVICE_FAMILY = "1,2"; 490 | }; 491 | name = Debug; 492 | }; 493 | D672EFC724F24091001C7E91 /* Release */ = { 494 | isa = XCBuildConfiguration; 495 | buildSettings = { 496 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 497 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 498 | CODE_SIGN_STYLE = Automatic; 499 | DEVELOPMENT_ASSET_PATHS = "\"Reversi/Preview Content\""; 500 | DEVELOPMENT_TEAM = AQ3JC8F3HL; 501 | ENABLE_PREVIEWS = YES; 502 | INFOPLIST_FILE = Reversi/Info.plist; 503 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 504 | LD_RUNPATH_SEARCH_PATHS = ( 505 | "$(inherited)", 506 | "@executable_path/Frameworks", 507 | ); 508 | PRODUCT_BUNDLE_IDENTIFIER = org.koherent.Reversi; 509 | PRODUCT_NAME = "$(TARGET_NAME)"; 510 | SWIFT_VERSION = 5.0; 511 | TARGETED_DEVICE_FAMILY = "1,2"; 512 | }; 513 | name = Release; 514 | }; 515 | D672EFC924F24091001C7E91 /* Debug */ = { 516 | isa = XCBuildConfiguration; 517 | buildSettings = { 518 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 519 | BUNDLE_LOADER = "$(TEST_HOST)"; 520 | CODE_SIGN_STYLE = Automatic; 521 | DEVELOPMENT_TEAM = AQ3JC8F3HL; 522 | INFOPLIST_FILE = ReversiTests/Info.plist; 523 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 524 | LD_RUNPATH_SEARCH_PATHS = ( 525 | "$(inherited)", 526 | "@executable_path/Frameworks", 527 | "@loader_path/Frameworks", 528 | ); 529 | PRODUCT_BUNDLE_IDENTIFIER = org.koherent.ReversiTests; 530 | PRODUCT_NAME = "$(TARGET_NAME)"; 531 | SWIFT_VERSION = 5.0; 532 | TARGETED_DEVICE_FAMILY = "1,2"; 533 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Reversi.app/Reversi"; 534 | }; 535 | name = Debug; 536 | }; 537 | D672EFCA24F24091001C7E91 /* Release */ = { 538 | isa = XCBuildConfiguration; 539 | buildSettings = { 540 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 541 | BUNDLE_LOADER = "$(TEST_HOST)"; 542 | CODE_SIGN_STYLE = Automatic; 543 | DEVELOPMENT_TEAM = AQ3JC8F3HL; 544 | INFOPLIST_FILE = ReversiTests/Info.plist; 545 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 546 | LD_RUNPATH_SEARCH_PATHS = ( 547 | "$(inherited)", 548 | "@executable_path/Frameworks", 549 | "@loader_path/Frameworks", 550 | ); 551 | PRODUCT_BUNDLE_IDENTIFIER = org.koherent.ReversiTests; 552 | PRODUCT_NAME = "$(TARGET_NAME)"; 553 | SWIFT_VERSION = 5.0; 554 | TARGETED_DEVICE_FAMILY = "1,2"; 555 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Reversi.app/Reversi"; 556 | }; 557 | name = Release; 558 | }; 559 | D672EFCC24F24091001C7E91 /* Debug */ = { 560 | isa = XCBuildConfiguration; 561 | buildSettings = { 562 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 563 | CODE_SIGN_STYLE = Automatic; 564 | DEVELOPMENT_TEAM = AQ3JC8F3HL; 565 | INFOPLIST_FILE = ReversiUITests/Info.plist; 566 | LD_RUNPATH_SEARCH_PATHS = ( 567 | "$(inherited)", 568 | "@executable_path/Frameworks", 569 | "@loader_path/Frameworks", 570 | ); 571 | PRODUCT_BUNDLE_IDENTIFIER = org.koherent.ReversiUITests; 572 | PRODUCT_NAME = "$(TARGET_NAME)"; 573 | SWIFT_VERSION = 5.0; 574 | TARGETED_DEVICE_FAMILY = "1,2"; 575 | TEST_TARGET_NAME = Reversi; 576 | }; 577 | name = Debug; 578 | }; 579 | D672EFCD24F24091001C7E91 /* Release */ = { 580 | isa = XCBuildConfiguration; 581 | buildSettings = { 582 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 583 | CODE_SIGN_STYLE = Automatic; 584 | DEVELOPMENT_TEAM = AQ3JC8F3HL; 585 | INFOPLIST_FILE = ReversiUITests/Info.plist; 586 | LD_RUNPATH_SEARCH_PATHS = ( 587 | "$(inherited)", 588 | "@executable_path/Frameworks", 589 | "@loader_path/Frameworks", 590 | ); 591 | PRODUCT_BUNDLE_IDENTIFIER = org.koherent.ReversiUITests; 592 | PRODUCT_NAME = "$(TARGET_NAME)"; 593 | SWIFT_VERSION = 5.0; 594 | TARGETED_DEVICE_FAMILY = "1,2"; 595 | TEST_TARGET_NAME = Reversi; 596 | }; 597 | name = Release; 598 | }; 599 | /* End XCBuildConfiguration section */ 600 | 601 | /* Begin XCConfigurationList section */ 602 | D672EF9B24F24090001C7E91 /* Build configuration list for PBXProject "Reversi" */ = { 603 | isa = XCConfigurationList; 604 | buildConfigurations = ( 605 | D672EFC324F24091001C7E91 /* Debug */, 606 | D672EFC424F24091001C7E91 /* Release */, 607 | ); 608 | defaultConfigurationIsVisible = 0; 609 | defaultConfigurationName = Release; 610 | }; 611 | D672EFC524F24091001C7E91 /* Build configuration list for PBXNativeTarget "Reversi" */ = { 612 | isa = XCConfigurationList; 613 | buildConfigurations = ( 614 | D672EFC624F24091001C7E91 /* Debug */, 615 | D672EFC724F24091001C7E91 /* Release */, 616 | ); 617 | defaultConfigurationIsVisible = 0; 618 | defaultConfigurationName = Release; 619 | }; 620 | D672EFC824F24091001C7E91 /* Build configuration list for PBXNativeTarget "ReversiTests" */ = { 621 | isa = XCConfigurationList; 622 | buildConfigurations = ( 623 | D672EFC924F24091001C7E91 /* Debug */, 624 | D672EFCA24F24091001C7E91 /* Release */, 625 | ); 626 | defaultConfigurationIsVisible = 0; 627 | defaultConfigurationName = Release; 628 | }; 629 | D672EFCB24F24091001C7E91 /* Build configuration list for PBXNativeTarget "ReversiUITests" */ = { 630 | isa = XCConfigurationList; 631 | buildConfigurations = ( 632 | D672EFCC24F24091001C7E91 /* Debug */, 633 | D672EFCD24F24091001C7E91 /* Release */, 634 | ); 635 | defaultConfigurationIsVisible = 0; 636 | defaultConfigurationName = Release; 637 | }; 638 | /* End XCConfigurationList section */ 639 | 640 | /* Begin XCRemoteSwiftPackageReference section */ 641 | D672EFD024F2415A001C7E91 /* XCRemoteSwiftPackageReference "reversi-logics-swift" */ = { 642 | isa = XCRemoteSwiftPackageReference; 643 | repositoryURL = "https://github.com/koher/reversi-logics-swift.git"; 644 | requirement = { 645 | branch = master; 646 | kind = branch; 647 | }; 648 | }; 649 | /* End XCRemoteSwiftPackageReference section */ 650 | 651 | /* Begin XCSwiftPackageProductDependency section */ 652 | D672EFD124F2415A001C7E91 /* ReversiLogics */ = { 653 | isa = XCSwiftPackageProductDependency; 654 | package = D672EFD024F2415A001C7E91 /* XCRemoteSwiftPackageReference "reversi-logics-swift" */; 655 | productName = ReversiLogics; 656 | }; 657 | /* End XCSwiftPackageProductDependency section */ 658 | }; 659 | rootObject = D672EF9824F24090001C7E91 /* Project object */; 660 | } 661 | -------------------------------------------------------------------------------- /Reversi.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Reversi.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Reversi.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "ReversiLogics", 6 | "repositoryURL": "https://github.com/koher/reversi-logics-swift.git", 7 | "state": { 8 | "branch": "master", 9 | "revision": "da02322ca94dc2d76f5f6d6e148739ba8dbde836", 10 | "version": null 11 | } 12 | }, 13 | { 14 | "package": "SwiftyReversi", 15 | "repositoryURL": "https://github.com/koher/swifty-reversi.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "b8b904ad881bcbbc1d3a9c6e0a697085a0e9dfa6", 19 | "version": "0.2.0-beta" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Reversi/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Reversi/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Reversi/Assets.xcassets/CellColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.424", 9 | "green" : "0.498", 10 | "red" : "0.424" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Reversi/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Reversi/Assets.xcassets/DarkColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.122", 9 | "green" : "0.122", 10 | "red" : "0.122" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Reversi/Assets.xcassets/LightColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.937", 9 | "green" : "0.937", 10 | "red" : "0.937" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Reversi/BoardView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UIKit 3 | import SwiftyReversi 4 | import ReversiLogics 5 | 6 | struct BoardView: UIViewRepresentable { 7 | let board: Board 8 | let action: (Int, Int) -> Void 9 | let animationCompletion: (() -> Void)? 10 | 11 | init(_ board: Board, action: @escaping (Int, Int) -> Void, animationCompletion: (() -> Void)?) { 12 | self.board = board 13 | self.action = action 14 | self.animationCompletion = animationCompletion 15 | } 16 | 17 | func makeUIView(context: Context) -> _BoardView { 18 | let view: _BoardView = .init() 19 | view.delegate = context.coordinator 20 | view.setBoard(board, animated: false, completion: nil) 21 | return view 22 | } 23 | 24 | func updateUIView(_ uiView: _BoardView, context: Context) { 25 | context.coordinator.action = action 26 | uiView.setBoard(board, animated: animationCompletion != nil) { _ in 27 | animationCompletion?() 28 | } 29 | } 30 | 31 | func makeCoordinator() -> Coordinator { 32 | Coordinator(action: action) 33 | } 34 | 35 | final class Coordinator: _BoardViewDelegate { 36 | var action: (Int, Int) -> Void 37 | init(action: @escaping (Int, Int) -> Void) { 38 | self.action = action 39 | } 40 | func boardView(_ boardView: _BoardView, didSelectCellAtX x: Int, y: Int) { 41 | action(x, y) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Reversi/ColorExtensions.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Color { 4 | static var dark: Color { Color("DarkColor") } 5 | static var light: Color { Color("LightColor") } 6 | static var cell: Color { Color("CellColor") } 7 | } 8 | -------------------------------------------------------------------------------- /Reversi/Computer.swift: -------------------------------------------------------------------------------- 1 | import SwiftyReversi 2 | import Dispatch 3 | 4 | final class Computer { 5 | private var board: Board? 6 | private var workItem: DispatchWorkItem? 7 | 8 | func move(for board: Board, completion: @escaping (Int, Int) -> Void) { 9 | if board == self.board { return } 10 | self.board = board 11 | let workItem = DispatchWorkItem { 12 | guard let (x, y) = board.validMoves(for: .dark).randomElement() else { return } 13 | completion(x, y) 14 | } 15 | self.workItem = workItem 16 | DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: workItem) 17 | } 18 | 19 | func cancel() { 20 | workItem?.cancel() 21 | workItem = nil 22 | board = nil 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Reversi/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import ReversiLogics 3 | import SwiftyReversi 4 | 5 | struct ContentView: View { 6 | private let presenter: GamePresenter 7 | private let saver: Saver 8 | 9 | init() { 10 | saver = Saver() 11 | if let savedState = try? saver.load() { 12 | presenter = GamePresenter(savedState: savedState) 13 | } else { 14 | presenter = GamePresenter(manager: GameManager(game: Game(board: Board(width: 8, height: 8)), darkPlayer: .manual, lightPlayer: .manual)) 15 | } 16 | } 17 | 18 | var body: some View { 19 | GameView(presenter: presenter) 20 | .environment(\.saver, saver) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Reversi/DiskView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftyReversi 3 | 4 | struct DiskView: UIViewRepresentable { 5 | let disk: Disk 6 | 7 | init(_ disk: Disk) { 8 | self.disk = disk 9 | } 10 | 11 | func makeUIView(context: Context) -> _DiskView { 12 | let view = _DiskView() 13 | view.disk = disk 14 | return view 15 | } 16 | 17 | func updateUIView(_ uiView: _DiskView, context: Context) { 18 | uiView.disk = disk 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Reversi/EnvironmentExtensions.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ComputerKey: EnvironmentKey { 4 | static let defaultValue: Computer = .init() 5 | } 6 | 7 | struct SaverKey: EnvironmentKey { 8 | static let defaultValue: Saver = . init() 9 | } 10 | 11 | extension EnvironmentValues { 12 | var computer: Computer { 13 | get { self[ComputerKey.self] } 14 | set { self[ComputerKey.self] = newValue } 15 | } 16 | 17 | var saver: Saver { 18 | get { self[SaverKey.self] } 19 | set { self[SaverKey.self] = newValue } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Reversi/GameView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import SwiftyReversi 3 | import ReversiLogics 4 | 5 | struct GameView: View { 6 | @Environment(\.computer) private var computer: Computer 7 | @Environment(\.saver) private var saver: Saver 8 | @State var presenter: GamePresenter 9 | 10 | var body: some View { 11 | try? saver.save(presenter.savedState) 12 | if let board = presenter.boardForComputer { 13 | computer.move(for: board) { x, y in 14 | presenter.placeDiskAt(x: x, y: y) 15 | } 16 | } else { 17 | computer.cancel() 18 | } 19 | 20 | return VStack(spacing: 20) { 21 | MessageView(presenter.message) 22 | Spacer() 23 | PlayerStateView( 24 | side: .dark, 25 | count: presenter.count(of: .dark), 26 | player: $presenter.darkPlayer, 27 | isActivityIndicatorVisible: presenter.isPlayerActivityIndicatorVisible(of: .dark) 28 | ) 29 | .environment(\.layoutDirection, .leftToRight) 30 | BoardView(presenter.manager.game.board, action: { x, y in 31 | presenter.tryPlacingDiskAt(x: x, y: y) 32 | }, animationCompletion: presenter.needsAnimatingBoardChanges ? { 33 | presenter.completeFlippingDisks() 34 | } : nil) 35 | .aspectRatio(1, contentMode: .fit) 36 | PlayerStateView( 37 | side: .light, 38 | count: presenter.count(of: .light), 39 | player: $presenter.lightPlayer, 40 | isActivityIndicatorVisible: presenter.isPlayerActivityIndicatorVisible(of: .light) 41 | ) 42 | .environment(\.layoutDirection, .rightToLeft) 43 | Spacer() 44 | Button("Reset") { 45 | presenter.confirmToReset() 46 | } 47 | } 48 | .padding(20) 49 | .alert(isPresented: .constant(presenter.isResetAlertVisible || presenter.isPassingAlertVisible)) { 50 | if presenter.isResetAlertVisible { 51 | return Alert( 52 | title: Text("Confirmation"), 53 | message: Text("Do you really want to reset the game?"), 54 | primaryButton: .cancel(Text("Cancel")) { 55 | presenter.reset(false) 56 | }, 57 | secondaryButton: .destructive(Text("OK")) { 58 | presenter.reset(true) 59 | } 60 | ) 61 | } else if presenter.isPassingAlertVisible { 62 | return Alert( 63 | title: Text("Pass"), 64 | message: Text("Cannot place a disk."), 65 | dismissButton: .default(Text("Dismiss")) { 66 | presenter.pass() 67 | } 68 | ) 69 | } else { 70 | preconditionFailure("Never reaches here.") 71 | } 72 | } 73 | } 74 | } 75 | 76 | extension GameView { 77 | struct MessageView: View { 78 | let message: GamePresenter.Message 79 | 80 | init(_ message: GamePresenter.Message) { 81 | self.message = message 82 | } 83 | 84 | var body: some View { 85 | HStack { 86 | switch message { 87 | case .turn(let side): 88 | DiskView(side) 89 | .frame(width: 24, height: 24) 90 | Text("'s turn") 91 | .font(.system(size: 32)) 92 | case .result(.some(let side)): 93 | DiskView(side) 94 | .frame(width: 24, height: 24) 95 | Text("won") 96 | .font(.system(size: 32)) 97 | case .result(.none): 98 | Text("Tied") 99 | .font(.system(size: 32)) 100 | } 101 | } 102 | } 103 | } 104 | } 105 | 106 | extension GameView { 107 | struct PlayerStateView: View { 108 | let side: Disk 109 | let count: Int 110 | @Binding var player: Player 111 | let isActivityIndicatorVisible: Bool 112 | 113 | var body: some View { 114 | HStack(spacing: 16) { 115 | DiskView(side) 116 | .frame(width: 26, height: 26) 117 | PlayerPicker(selection: $player) 118 | .frame(width: 161) 119 | .environment(\.layoutDirection, .leftToRight) 120 | Text(count.description) 121 | .font(.system(size: 24)) 122 | if isActivityIndicatorVisible { 123 | ProgressView() 124 | } 125 | Spacer() 126 | } 127 | } 128 | } 129 | } 130 | 131 | extension GameView { 132 | struct PlayerPicker: View { 133 | @Binding var selection: Player 134 | 135 | var body: some View { 136 | Picker(selection: $selection, label: EmptyView(), content: { 137 | Text("Manual").tag(Player.manual) 138 | Text("Computer").tag(Player.computer) 139 | }) 140 | .pickerStyle(SegmentedPickerStyle()) 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Reversi/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | 28 | UIApplicationSupportsIndirectInputEvents 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Reversi/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Reversi/ReversiApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct ReversiApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Reversi/Saver.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Foundation 3 | import ReversiLogics 4 | 5 | final class Saver { 6 | private static let url: URL = .init(fileURLWithPath: (NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).first! as NSString).appendingPathComponent("Game")) 7 | 8 | private var state: SavedState? 9 | 10 | func load() throws -> SavedState { 11 | if let state = self.state { return state } 12 | self.state = try SavedState(data: Data(contentsOf: Saver.url)) 13 | return self.state! 14 | } 15 | 16 | func save(_ state: SavedState) throws { 17 | if state == self.state { return } 18 | self.state = state 19 | try state.data.write(to: Self.url, options: .atomic) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Reversi/UIKit/_BoardView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftyReversi 3 | import ReversiLogics 4 | 5 | private let lineWidth: CGFloat = 2 6 | 7 | public class _BoardView: UIView { 8 | private var cellViews: [_CellView] = [] 9 | private var actions: [CellSelectionAction] = [] 10 | 11 | public let width: Int = 8 12 | public let height: Int = 8 13 | 14 | public let xRange: Range 15 | public let yRange: Range 16 | 17 | public weak var delegate: _BoardViewDelegate? 18 | 19 | private var animationCanceller: AnimationCanceller? 20 | 21 | override public init(frame: CGRect) { 22 | xRange = 0 ..< width 23 | yRange = 0 ..< height 24 | super.init(frame: frame) 25 | setUp() 26 | } 27 | 28 | required public init?(coder: NSCoder) { 29 | xRange = 0 ..< width 30 | yRange = 0 ..< height 31 | super.init(coder: coder) 32 | setUp() 33 | } 34 | 35 | private func setUp() { 36 | self.backgroundColor = UIColor(named: "DarkColor")! 37 | 38 | let cellViews: [_CellView] = (0 ..< (width * height)).map { _ in 39 | let cellView = _CellView() 40 | cellView.translatesAutoresizingMaskIntoConstraints = false 41 | return cellView 42 | } 43 | self.cellViews = cellViews 44 | 45 | cellViews.forEach(self.addSubview(_:)) 46 | for i in cellViews.indices.dropFirst() { 47 | NSLayoutConstraint.activate([ 48 | cellViews[0].widthAnchor.constraint(equalTo: cellViews[i].widthAnchor), 49 | cellViews[0].heightAnchor.constraint(equalTo: cellViews[i].heightAnchor), 50 | ]) 51 | } 52 | 53 | NSLayoutConstraint.activate([ 54 | cellViews[0].widthAnchor.constraint(equalTo: cellViews[0].heightAnchor), 55 | ]) 56 | 57 | for y in yRange { 58 | for x in xRange { 59 | let topNeighborAnchor: NSLayoutYAxisAnchor 60 | if let cellView = cellViewAt(x: x, y: y - 1) { 61 | topNeighborAnchor = cellView.bottomAnchor 62 | } else { 63 | topNeighborAnchor = self.topAnchor 64 | } 65 | 66 | let leftNeighborAnchor: NSLayoutXAxisAnchor 67 | if let cellView = cellViewAt(x: x - 1, y: y) { 68 | leftNeighborAnchor = cellView.rightAnchor 69 | } else { 70 | leftNeighborAnchor = self.leftAnchor 71 | } 72 | 73 | let cellView = cellViewAt(x: x, y: y)! 74 | NSLayoutConstraint.activate([ 75 | cellView.topAnchor.constraint(equalTo: topNeighborAnchor, constant: lineWidth), 76 | cellView.leftAnchor.constraint(equalTo: leftNeighborAnchor, constant: lineWidth), 77 | ]) 78 | 79 | if y == height - 1 { 80 | NSLayoutConstraint.activate([ 81 | self.bottomAnchor.constraint(equalTo: cellView.bottomAnchor, constant: lineWidth), 82 | ]) 83 | } 84 | if x == width - 1 { 85 | NSLayoutConstraint.activate([ 86 | self.rightAnchor.constraint(equalTo: cellView.rightAnchor, constant: lineWidth), 87 | ]) 88 | } 89 | } 90 | } 91 | 92 | reset() 93 | 94 | for y in yRange { 95 | for x in xRange { 96 | let cellView: _CellView = cellViewAt(x: x, y: y)! 97 | let action = CellSelectionAction(boardView: self, x: x, y: y) 98 | actions.append(action) // To retain the `action` 99 | cellView.addTarget(action, action: #selector(action.selectCell), for: .touchUpInside) 100 | } 101 | } 102 | } 103 | 104 | public func reset() { 105 | for y in yRange { 106 | for x in xRange { 107 | setDisk(nil, atX: x, y: y, animated: false) 108 | } 109 | } 110 | 111 | setDisk(.light, atX: width / 2 - 1, y: height / 2 - 1, animated: false) 112 | setDisk(.dark, atX: width / 2, y: height / 2 - 1, animated: false) 113 | setDisk(.dark, atX: width / 2 - 1, y: height / 2, animated: false) 114 | setDisk(.light, atX: width / 2, y: height / 2, animated: false) 115 | } 116 | 117 | private func cellViewAt(x: Int, y: Int) -> _CellView? { 118 | guard xRange.contains(x) && yRange.contains(y) else { return nil } 119 | return cellViews[y * width + x] 120 | } 121 | 122 | public func diskAt(x: Int, y: Int) -> Disk? { 123 | cellViewAt(x: x, y: y)?.disk 124 | } 125 | 126 | public func setDisk(_ disk: Disk?, atX x: Int, y: Int, animated isAnimated: Bool, completion: ((Bool) -> Void)? = nil) { 127 | precondition(Thread.isMainThread) 128 | guard let cellView = cellViewAt(x: x, y: y) else { 129 | preconditionFailure() // FIXME: Add a message. 130 | } 131 | cellView.setDisk(disk, animated: isAnimated, completion: completion) 132 | } 133 | 134 | public var board: Board { 135 | var board: Board = .init(width: width, height: height) 136 | for y in yRange { 137 | for x in xRange { 138 | board[x, y] = cellViewAt(x: x, y: y)!.disk 139 | } 140 | } 141 | return board 142 | } 143 | 144 | public func setBoard(_ board: Board, animated isAnimated: Bool, completion: ((Bool) -> Void)? = nil) { 145 | precondition(Thread.isMainThread) 146 | precondition(board.width == width) 147 | precondition(board.height == height) 148 | 149 | animationCanceller?.cancel() 150 | let canceller: AnimationCanceller = .init() 151 | animationCanceller = canceller 152 | 153 | let boardDiff: BoardDiff = .init(from: self.board, to: board) 154 | applyBoardDiffResult(boardDiff.result[...], animated: isAnimated, canceller: canceller) { isFinished in 155 | DispatchQueue.main.async { 156 | completion?(isFinished) 157 | } 158 | } 159 | } 160 | 161 | private func applyBoardDiffResult(_ diff: ArraySlice<(disk: Disk?, x: Int, y: Int)>, animated isAnimated: Bool, canceller: AnimationCanceller, completion: ((Bool) -> Void)?) { 162 | if canceller.isCancelled { return } 163 | guard let (disk, x, y) = diff.first else { 164 | animationCanceller = nil 165 | completion?(true) 166 | return 167 | } 168 | setDisk(disk, atX: x, y: y, animated: isAnimated) { [weak self] isFinished in 169 | guard let self = self else { return } 170 | guard isFinished else { 171 | self.animationCanceller = nil 172 | completion?(isFinished) 173 | return 174 | } 175 | self.applyBoardDiffResult(diff[(diff.startIndex + 1)...], animated: isAnimated, canceller: canceller, completion: completion) 176 | } 177 | } 178 | } 179 | 180 | public protocol _BoardViewDelegate: AnyObject { 181 | func boardView(_ boardView: _BoardView, didSelectCellAtX x: Int, y: Int) 182 | } 183 | 184 | private final class CellSelectionAction: NSObject { 185 | private weak var boardView: _BoardView? 186 | let x: Int 187 | let y: Int 188 | 189 | init(boardView: _BoardView, x: Int, y: Int) { 190 | self.boardView = boardView 191 | self.x = x 192 | self.y = y 193 | } 194 | 195 | @objc func selectCell() { 196 | guard let boardView = boardView else { return } 197 | boardView.delegate?.boardView(boardView, didSelectCellAtX: x, y: y) 198 | } 199 | } 200 | 201 | private final class AnimationCanceller { 202 | private(set) var isCancelled: Bool = false 203 | func cancel() { 204 | isCancelled = true 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /Reversi/UIKit/_CellView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftyReversi 3 | 4 | private let animationDuration: TimeInterval = 0.25 5 | 6 | public class _CellView: UIView { 7 | private let button: UIButton = UIButton() 8 | private let diskView: _DiskView = _DiskView() 9 | 10 | private var _disk: Disk? 11 | public var disk: Disk? { 12 | get { _disk } 13 | set { setDisk(newValue, animated: true) } 14 | } 15 | 16 | override public init(frame: CGRect) { 17 | super.init(frame: frame) 18 | setUp() 19 | } 20 | 21 | required public init?(coder: NSCoder) { 22 | super.init(coder: coder) 23 | setUp() 24 | } 25 | 26 | private func setUp() { 27 | do { // button 28 | button.translatesAutoresizingMaskIntoConstraints = false 29 | do { // backgroundImage 30 | UIGraphicsBeginImageContext(CGSize(width: 1, height: 1)) 31 | defer { UIGraphicsEndImageContext() } 32 | 33 | let color: UIColor = UIColor(named: "CellColor")! 34 | color.set() 35 | UIRectFill(CGRect(x: 0, y: 0, width: 1, height: 1)) 36 | 37 | let backgroundImage = UIGraphicsGetImageFromCurrentImageContext()! 38 | button.setBackgroundImage(backgroundImage, for: .normal) 39 | button.setBackgroundImage(backgroundImage, for: .disabled) 40 | } 41 | self.addSubview(button) 42 | } 43 | 44 | do { // diskView 45 | diskView.translatesAutoresizingMaskIntoConstraints = false 46 | self.addSubview(diskView) 47 | } 48 | 49 | setNeedsLayout() 50 | } 51 | 52 | public override func layoutSubviews() { 53 | super.layoutSubviews() 54 | 55 | button.frame = bounds 56 | layoutDiskView() 57 | } 58 | 59 | private func layoutDiskView() { 60 | let cellSize = bounds.size 61 | let diskDiameter = Swift.min(cellSize.width, cellSize.height) * 0.8 62 | let diskSize: CGSize 63 | if _disk == nil || diskView.disk == _disk { 64 | diskSize = CGSize(width: diskDiameter, height: diskDiameter) 65 | } else { 66 | diskSize = CGSize(width: 0, height: diskDiameter) 67 | } 68 | diskView.frame = CGRect( 69 | origin: CGPoint(x: (cellSize.width - diskSize.width) / 2, y: (cellSize.height - diskSize.height) / 2), 70 | size: diskSize 71 | ) 72 | diskView.alpha = _disk == nil ? 0.0 : 1.0 73 | } 74 | 75 | public func setDisk(_ disk: Disk?, animated: Bool, completion: ((Bool) -> Void)? = nil) { 76 | let diskBefore: Disk? = _disk 77 | _disk = disk 78 | let diskAfter: Disk? = _disk 79 | if animated { 80 | switch (diskBefore, diskAfter) { 81 | case (.none, .none): 82 | completion?(true) 83 | case (.none, .some(let animationDisk)): 84 | diskView.disk = animationDisk 85 | fallthrough 86 | case (.some, .none): 87 | UIView.animate(withDuration: animationDuration, delay: 0, options: .curveEaseIn, animations: { [weak self] in 88 | self?.layoutDiskView() 89 | }, completion: { finished in 90 | completion?(finished) 91 | }) 92 | case (.some, .some): 93 | UIView.animate(withDuration: animationDuration / 2, delay: 0, options: .curveEaseOut, animations: { [weak self] in 94 | self?.layoutDiskView() 95 | }, completion: { [weak self] finished in 96 | guard let self = self else { return } 97 | if self.diskView.disk == self._disk { 98 | completion?(finished) 99 | } 100 | guard let diskAfter = self._disk else { 101 | completion?(finished) 102 | return 103 | } 104 | self.diskView.disk = diskAfter 105 | UIView.animate(withDuration: animationDuration / 2, animations: { [weak self] in 106 | self?.layoutDiskView() 107 | }, completion: { finished in 108 | completion?(finished) 109 | }) 110 | }) 111 | } 112 | } else { 113 | if let diskAfter = diskAfter { 114 | diskView.disk = diskAfter 115 | } 116 | completion?(true) 117 | setNeedsLayout() 118 | } 119 | } 120 | 121 | public func addTarget(_ target: Any?, action: Selector, for controlEvents: UIControl.Event) { 122 | button.addTarget(target, action: action, for: controlEvents) 123 | } 124 | 125 | public func removeTarget(_ target: Any?, action: Selector?, for controlEvents: UIControl.Event) { 126 | button.removeTarget(target, action: action, for: controlEvents) 127 | } 128 | 129 | public func actions(forTarget target: Any?, forControlEvent controlEvent: UIControl.Event) -> [String]? { 130 | button.actions(forTarget: target, forControlEvent: controlEvent) 131 | } 132 | 133 | public var allTargets: Set { 134 | button.allTargets 135 | } 136 | 137 | public var allControlEvents: UIControl.Event { 138 | button.allControlEvents 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Reversi/UIKit/_DiskView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftyReversi 3 | 4 | public class _DiskView: UIView { 5 | public var disk: Disk = .dark { 6 | didSet { setNeedsDisplay() } 7 | } 8 | 9 | @IBInspectable public var name: String { 10 | get { disk.name } 11 | set { disk = .init(name: newValue) } 12 | } 13 | 14 | override public init(frame: CGRect) { 15 | super.init(frame: frame) 16 | setUp() 17 | } 18 | 19 | required public init?(coder: NSCoder) { 20 | super.init(coder: coder) 21 | setUp() 22 | } 23 | 24 | private func setUp() { 25 | backgroundColor = .clear 26 | isUserInteractionEnabled = false 27 | } 28 | 29 | override public func draw(_ rect: CGRect) { 30 | guard let context = UIGraphicsGetCurrentContext() else { return } 31 | context.setFillColor(disk.cgColor) 32 | context.fillEllipse(in: bounds) 33 | } 34 | } 35 | 36 | extension Disk { 37 | fileprivate var uiColor: UIColor { 38 | switch self { 39 | case .dark: return UIColor(named: "DarkColor")! 40 | case .light: return UIColor(named: "LightColor")! 41 | } 42 | } 43 | 44 | fileprivate var cgColor: CGColor { 45 | uiColor.cgColor 46 | } 47 | 48 | fileprivate var name: String { 49 | switch self { 50 | case .dark: return "dark" 51 | case .light: return "light" 52 | } 53 | } 54 | 55 | fileprivate init(name: String) { 56 | switch name { 57 | case Disk.dark.name: 58 | self = .dark 59 | case Disk.light.name: 60 | self = .light 61 | default: 62 | preconditionFailure("Illegal name: \(name)") 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ReversiTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ReversiTests/ReversiTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Reversi 3 | 4 | class ReversiTests: XCTestCase { 5 | 6 | override func setUpWithError() throws { 7 | // Put setup code here. This method is called before the invocation of each test method in the class. 8 | } 9 | 10 | override func tearDownWithError() throws { 11 | // Put teardown code here. This method is called after the invocation of each test method in the class. 12 | } 13 | 14 | func testExample() throws { 15 | // This is an example of a functional test case. 16 | // Use XCTAssert and related functions to verify your tests produce the correct results. 17 | } 18 | 19 | func testPerformanceExample() throws { 20 | // This is an example of a performance test case. 21 | self.measure { 22 | // Put the code you want to measure the time of here. 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /ReversiUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ReversiUITests/ReversiUITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | class ReversiUITests: XCTestCase { 4 | 5 | override func setUpWithError() throws { 6 | // Put setup code here. This method is called before the invocation of each test method in the class. 7 | 8 | // In UI tests it is usually best to stop immediately when a failure occurs. 9 | continueAfterFailure = false 10 | 11 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 12 | } 13 | 14 | override func tearDownWithError() throws { 15 | // Put teardown code here. This method is called after the invocation of each test method in the class. 16 | } 17 | 18 | func testExample() throws { 19 | // UI tests must launch the application that they test. 20 | let app = XCUIApplication() 21 | app.launch() 22 | 23 | // Use recording to get started writing UI tests. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testLaunchPerformance() throws { 28 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 29 | // This measures how long it takes to launch your application. 30 | measure(metrics: [XCTApplicationLaunchMetric()]) { 31 | XCUIApplication().launch() 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /img/data-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koher/reversi-ios-swiftui/1b71722bd33a1001eb8ffe6d7e9423016aa4ad33/img/data-flow.png -------------------------------------------------------------------------------- /img/dependencies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koher/reversi-ios-swiftui/1b71722bd33a1001eb8ffe6d7e9423016aa4ad33/img/dependencies.png -------------------------------------------------------------------------------- /img/reversi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koher/reversi-ios-swiftui/1b71722bd33a1001eb8ffe6d7e9423016aa4ad33/img/reversi.png -------------------------------------------------------------------------------- /img/states.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koher/reversi-ios-swiftui/1b71722bd33a1001eb8ffe6d7e9423016aa4ad33/img/states.png --------------------------------------------------------------------------------