├── .DS_Store ├── Minesweeper.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── staskochkin.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── staskochkin.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── Minesweeper ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── Extensions.swift ├── Host │ ├── AppDelegate.swift │ └── SceneDelegate.swift ├── Info.plist ├── Models │ ├── Board.swift │ ├── Gameplay.swift │ ├── Settings.swift │ ├── Tile.swift │ └── Timing.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── Views │ ├── BoardView.swift │ ├── GameView.swift │ ├── PlayButton.swift │ ├── SettingsView.swift │ ├── TImerView.swift │ └── TileView.swift ├── README.md └── Screenshot.png /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staskochkin/Minesweeper-SwiftUI/3a77a5c92f8b0f453bb1ecf3d8fb02cdabef7e0c/.DS_Store -------------------------------------------------------------------------------- /Minesweeper.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4206BA1222C1115E009CCA12 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4206BA1122C1115E009CCA12 /* AppDelegate.swift */; }; 11 | 4206BA1422C1115E009CCA12 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4206BA1322C1115E009CCA12 /* SceneDelegate.swift */; }; 12 | 4206BA1622C1115E009CCA12 /* GameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4206BA1522C1115E009CCA12 /* GameView.swift */; }; 13 | 4206BA1822C11160009CCA12 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4206BA1722C11160009CCA12 /* Assets.xcassets */; }; 14 | 4206BA1B22C11160009CCA12 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4206BA1A22C11160009CCA12 /* Preview Assets.xcassets */; }; 15 | 4206BA1E22C11160009CCA12 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4206BA1C22C11160009CCA12 /* LaunchScreen.storyboard */; }; 16 | 4206BA2922C111C9009CCA12 /* Gameplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4206BA2822C111C9009CCA12 /* Gameplay.swift */; }; 17 | 4206BA2B22C111D4009CCA12 /* Tile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4206BA2A22C111D4009CCA12 /* Tile.swift */; }; 18 | 4206BA2D22C11250009CCA12 /* Board.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4206BA2C22C11250009CCA12 /* Board.swift */; }; 19 | 4251DC7722C1224D00E34F1C /* BoardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4251DC7622C1224D00E34F1C /* BoardView.swift */; }; 20 | 4251DC7922C1639600E34F1C /* TileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4251DC7822C1639600E34F1C /* TileView.swift */; }; 21 | 427AC531236241CD00114315 /* PlayButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427AC530236241CD00114315 /* PlayButton.swift */; }; 22 | 42C6F41222C21C1300F52679 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C6F41122C21C1300F52679 /* Extensions.swift */; }; 23 | 42D9266722C938CD001B3DF1 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D9266622C938CD001B3DF1 /* SettingsView.swift */; }; 24 | 42D9266922C94497001B3DF1 /* TImerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D9266822C94497001B3DF1 /* TImerView.swift */; }; 25 | 42D9266B22C9460C001B3DF1 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D9266A22C9460C001B3DF1 /* Settings.swift */; }; 26 | 42D9266D22C94CEE001B3DF1 /* Timing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D9266C22C94CEE001B3DF1 /* Timing.swift */; }; 27 | /* End PBXBuildFile section */ 28 | 29 | /* Begin PBXFileReference section */ 30 | 4206BA0E22C1115E009CCA12 /* Minesweeper.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Minesweeper.app; sourceTree = BUILT_PRODUCTS_DIR; }; 31 | 4206BA1122C1115E009CCA12 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 32 | 4206BA1322C1115E009CCA12 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 33 | 4206BA1522C1115E009CCA12 /* GameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameView.swift; sourceTree = ""; }; 34 | 4206BA1722C11160009CCA12 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 35 | 4206BA1A22C11160009CCA12 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 36 | 4206BA1D22C11160009CCA12 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 37 | 4206BA1F22C11160009CCA12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 38 | 4206BA2822C111C9009CCA12 /* Gameplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Gameplay.swift; sourceTree = ""; }; 39 | 4206BA2A22C111D4009CCA12 /* Tile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tile.swift; sourceTree = ""; }; 40 | 4206BA2C22C11250009CCA12 /* Board.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Board.swift; sourceTree = ""; }; 41 | 4251DC7622C1224D00E34F1C /* BoardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoardView.swift; sourceTree = ""; }; 42 | 4251DC7822C1639600E34F1C /* TileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileView.swift; sourceTree = ""; }; 43 | 427AC530236241CD00114315 /* PlayButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayButton.swift; sourceTree = ""; }; 44 | 42C6F41122C21C1300F52679 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 45 | 42D9266622C938CD001B3DF1 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 46 | 42D9266822C94497001B3DF1 /* TImerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TImerView.swift; sourceTree = ""; }; 47 | 42D9266A22C9460C001B3DF1 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; 48 | 42D9266C22C94CEE001B3DF1 /* Timing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timing.swift; sourceTree = ""; }; 49 | /* End PBXFileReference section */ 50 | 51 | /* Begin PBXFrameworksBuildPhase section */ 52 | 4206BA0B22C1115E009CCA12 /* Frameworks */ = { 53 | isa = PBXFrameworksBuildPhase; 54 | buildActionMask = 2147483647; 55 | files = ( 56 | ); 57 | runOnlyForDeploymentPostprocessing = 0; 58 | }; 59 | /* End PBXFrameworksBuildPhase section */ 60 | 61 | /* Begin PBXGroup section */ 62 | 4206BA0522C1115E009CCA12 = { 63 | isa = PBXGroup; 64 | children = ( 65 | 4206BA1022C1115E009CCA12 /* Minesweeper */, 66 | 4206BA0F22C1115E009CCA12 /* Products */, 67 | ); 68 | sourceTree = ""; 69 | }; 70 | 4206BA0F22C1115E009CCA12 /* Products */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 4206BA0E22C1115E009CCA12 /* Minesweeper.app */, 74 | ); 75 | name = Products; 76 | sourceTree = ""; 77 | }; 78 | 4206BA1022C1115E009CCA12 /* Minesweeper */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 4206BA2522C111A3009CCA12 /* Host */, 82 | 4206BA2622C111AE009CCA12 /* Models */, 83 | 4206BA2722C111B9009CCA12 /* Views */, 84 | 4206BA1722C11160009CCA12 /* Assets.xcassets */, 85 | 4206BA1C22C11160009CCA12 /* LaunchScreen.storyboard */, 86 | 4206BA1F22C11160009CCA12 /* Info.plist */, 87 | 4206BA1922C11160009CCA12 /* Preview Content */, 88 | 42C6F41122C21C1300F52679 /* Extensions.swift */, 89 | ); 90 | path = Minesweeper; 91 | sourceTree = ""; 92 | }; 93 | 4206BA1922C11160009CCA12 /* Preview Content */ = { 94 | isa = PBXGroup; 95 | children = ( 96 | 4206BA1A22C11160009CCA12 /* Preview Assets.xcassets */, 97 | ); 98 | path = "Preview Content"; 99 | sourceTree = ""; 100 | }; 101 | 4206BA2522C111A3009CCA12 /* Host */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | 4206BA1122C1115E009CCA12 /* AppDelegate.swift */, 105 | 4206BA1322C1115E009CCA12 /* SceneDelegate.swift */, 106 | ); 107 | path = Host; 108 | sourceTree = ""; 109 | }; 110 | 4206BA2622C111AE009CCA12 /* Models */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 4206BA2822C111C9009CCA12 /* Gameplay.swift */, 114 | 42D9266A22C9460C001B3DF1 /* Settings.swift */, 115 | 42D9266C22C94CEE001B3DF1 /* Timing.swift */, 116 | 4206BA2C22C11250009CCA12 /* Board.swift */, 117 | 4206BA2A22C111D4009CCA12 /* Tile.swift */, 118 | ); 119 | path = Models; 120 | sourceTree = ""; 121 | }; 122 | 4206BA2722C111B9009CCA12 /* Views */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 4206BA1522C1115E009CCA12 /* GameView.swift */, 126 | 427AC530236241CD00114315 /* PlayButton.swift */, 127 | 42D9266622C938CD001B3DF1 /* SettingsView.swift */, 128 | 4251DC7622C1224D00E34F1C /* BoardView.swift */, 129 | 4251DC7822C1639600E34F1C /* TileView.swift */, 130 | 42D9266822C94497001B3DF1 /* TImerView.swift */, 131 | ); 132 | path = Views; 133 | sourceTree = ""; 134 | }; 135 | /* End PBXGroup section */ 136 | 137 | /* Begin PBXNativeTarget section */ 138 | 4206BA0D22C1115E009CCA12 /* Minesweeper */ = { 139 | isa = PBXNativeTarget; 140 | buildConfigurationList = 4206BA2222C11160009CCA12 /* Build configuration list for PBXNativeTarget "Minesweeper" */; 141 | buildPhases = ( 142 | 4206BA0A22C1115E009CCA12 /* Sources */, 143 | 4206BA0B22C1115E009CCA12 /* Frameworks */, 144 | 4206BA0C22C1115E009CCA12 /* Resources */, 145 | ); 146 | buildRules = ( 147 | ); 148 | dependencies = ( 149 | ); 150 | name = Minesweeper; 151 | productName = Minesweeper; 152 | productReference = 4206BA0E22C1115E009CCA12 /* Minesweeper.app */; 153 | productType = "com.apple.product-type.application"; 154 | }; 155 | /* End PBXNativeTarget section */ 156 | 157 | /* Begin PBXProject section */ 158 | 4206BA0622C1115E009CCA12 /* Project object */ = { 159 | isa = PBXProject; 160 | attributes = { 161 | LastSwiftUpdateCheck = 1100; 162 | LastUpgradeCheck = 1100; 163 | ORGANIZATIONNAME = appodeal; 164 | TargetAttributes = { 165 | 4206BA0D22C1115E009CCA12 = { 166 | CreatedOnToolsVersion = 11.0; 167 | }; 168 | }; 169 | }; 170 | buildConfigurationList = 4206BA0922C1115E009CCA12 /* Build configuration list for PBXProject "Minesweeper" */; 171 | compatibilityVersion = "Xcode 9.3"; 172 | developmentRegion = en; 173 | hasScannedForEncodings = 0; 174 | knownRegions = ( 175 | en, 176 | Base, 177 | ); 178 | mainGroup = 4206BA0522C1115E009CCA12; 179 | productRefGroup = 4206BA0F22C1115E009CCA12 /* Products */; 180 | projectDirPath = ""; 181 | projectRoot = ""; 182 | targets = ( 183 | 4206BA0D22C1115E009CCA12 /* Minesweeper */, 184 | ); 185 | }; 186 | /* End PBXProject section */ 187 | 188 | /* Begin PBXResourcesBuildPhase section */ 189 | 4206BA0C22C1115E009CCA12 /* Resources */ = { 190 | isa = PBXResourcesBuildPhase; 191 | buildActionMask = 2147483647; 192 | files = ( 193 | 4206BA1E22C11160009CCA12 /* LaunchScreen.storyboard in Resources */, 194 | 4206BA1B22C11160009CCA12 /* Preview Assets.xcassets in Resources */, 195 | 4206BA1822C11160009CCA12 /* Assets.xcassets in Resources */, 196 | ); 197 | runOnlyForDeploymentPostprocessing = 0; 198 | }; 199 | /* End PBXResourcesBuildPhase section */ 200 | 201 | /* Begin PBXSourcesBuildPhase section */ 202 | 4206BA0A22C1115E009CCA12 /* Sources */ = { 203 | isa = PBXSourcesBuildPhase; 204 | buildActionMask = 2147483647; 205 | files = ( 206 | 42D9266922C94497001B3DF1 /* TImerView.swift in Sources */, 207 | 427AC531236241CD00114315 /* PlayButton.swift in Sources */, 208 | 4251DC7722C1224D00E34F1C /* BoardView.swift in Sources */, 209 | 42C6F41222C21C1300F52679 /* Extensions.swift in Sources */, 210 | 4206BA2922C111C9009CCA12 /* Gameplay.swift in Sources */, 211 | 42D9266D22C94CEE001B3DF1 /* Timing.swift in Sources */, 212 | 4251DC7922C1639600E34F1C /* TileView.swift in Sources */, 213 | 4206BA1222C1115E009CCA12 /* AppDelegate.swift in Sources */, 214 | 42D9266B22C9460C001B3DF1 /* Settings.swift in Sources */, 215 | 42D9266722C938CD001B3DF1 /* SettingsView.swift in Sources */, 216 | 4206BA2D22C11250009CCA12 /* Board.swift in Sources */, 217 | 4206BA2B22C111D4009CCA12 /* Tile.swift in Sources */, 218 | 4206BA1422C1115E009CCA12 /* SceneDelegate.swift in Sources */, 219 | 4206BA1622C1115E009CCA12 /* GameView.swift in Sources */, 220 | ); 221 | runOnlyForDeploymentPostprocessing = 0; 222 | }; 223 | /* End PBXSourcesBuildPhase section */ 224 | 225 | /* Begin PBXVariantGroup section */ 226 | 4206BA1C22C11160009CCA12 /* LaunchScreen.storyboard */ = { 227 | isa = PBXVariantGroup; 228 | children = ( 229 | 4206BA1D22C11160009CCA12 /* Base */, 230 | ); 231 | name = LaunchScreen.storyboard; 232 | sourceTree = ""; 233 | }; 234 | /* End PBXVariantGroup section */ 235 | 236 | /* Begin XCBuildConfiguration section */ 237 | 4206BA2022C11160009CCA12 /* Debug */ = { 238 | isa = XCBuildConfiguration; 239 | buildSettings = { 240 | ALWAYS_SEARCH_USER_PATHS = NO; 241 | CLANG_ANALYZER_NONNULL = YES; 242 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 243 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 244 | CLANG_CXX_LIBRARY = "libc++"; 245 | CLANG_ENABLE_MODULES = YES; 246 | CLANG_ENABLE_OBJC_ARC = YES; 247 | CLANG_ENABLE_OBJC_WEAK = YES; 248 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 249 | CLANG_WARN_BOOL_CONVERSION = YES; 250 | CLANG_WARN_COMMA = YES; 251 | CLANG_WARN_CONSTANT_CONVERSION = YES; 252 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 253 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 254 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 255 | CLANG_WARN_EMPTY_BODY = YES; 256 | CLANG_WARN_ENUM_CONVERSION = YES; 257 | CLANG_WARN_INFINITE_RECURSION = YES; 258 | CLANG_WARN_INT_CONVERSION = YES; 259 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 260 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 261 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 262 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 263 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 264 | CLANG_WARN_STRICT_PROTOTYPES = YES; 265 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 266 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 267 | CLANG_WARN_UNREACHABLE_CODE = YES; 268 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 269 | COPY_PHASE_STRIP = NO; 270 | DEBUG_INFORMATION_FORMAT = dwarf; 271 | ENABLE_STRICT_OBJC_MSGSEND = YES; 272 | ENABLE_TESTABILITY = YES; 273 | GCC_C_LANGUAGE_STANDARD = gnu11; 274 | GCC_DYNAMIC_NO_PIC = NO; 275 | GCC_NO_COMMON_BLOCKS = YES; 276 | GCC_OPTIMIZATION_LEVEL = 0; 277 | GCC_PREPROCESSOR_DEFINITIONS = ( 278 | "DEBUG=1", 279 | "$(inherited)", 280 | ); 281 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 282 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 283 | GCC_WARN_UNDECLARED_SELECTOR = YES; 284 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 285 | GCC_WARN_UNUSED_FUNCTION = YES; 286 | GCC_WARN_UNUSED_VARIABLE = YES; 287 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 288 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 289 | MTL_FAST_MATH = YES; 290 | ONLY_ACTIVE_ARCH = YES; 291 | SDKROOT = iphoneos; 292 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 293 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 294 | }; 295 | name = Debug; 296 | }; 297 | 4206BA2122C11160009CCA12 /* Release */ = { 298 | isa = XCBuildConfiguration; 299 | buildSettings = { 300 | ALWAYS_SEARCH_USER_PATHS = NO; 301 | CLANG_ANALYZER_NONNULL = YES; 302 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 303 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 304 | CLANG_CXX_LIBRARY = "libc++"; 305 | CLANG_ENABLE_MODULES = YES; 306 | CLANG_ENABLE_OBJC_ARC = YES; 307 | CLANG_ENABLE_OBJC_WEAK = YES; 308 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 309 | CLANG_WARN_BOOL_CONVERSION = YES; 310 | CLANG_WARN_COMMA = YES; 311 | CLANG_WARN_CONSTANT_CONVERSION = YES; 312 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 313 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 314 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 315 | CLANG_WARN_EMPTY_BODY = YES; 316 | CLANG_WARN_ENUM_CONVERSION = YES; 317 | CLANG_WARN_INFINITE_RECURSION = YES; 318 | CLANG_WARN_INT_CONVERSION = YES; 319 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 320 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 321 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 322 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 323 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 324 | CLANG_WARN_STRICT_PROTOTYPES = YES; 325 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 326 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 327 | CLANG_WARN_UNREACHABLE_CODE = YES; 328 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 329 | COPY_PHASE_STRIP = NO; 330 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 331 | ENABLE_NS_ASSERTIONS = NO; 332 | ENABLE_STRICT_OBJC_MSGSEND = YES; 333 | GCC_C_LANGUAGE_STANDARD = gnu11; 334 | GCC_NO_COMMON_BLOCKS = YES; 335 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 336 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 337 | GCC_WARN_UNDECLARED_SELECTOR = YES; 338 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 339 | GCC_WARN_UNUSED_FUNCTION = YES; 340 | GCC_WARN_UNUSED_VARIABLE = YES; 341 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 342 | MTL_ENABLE_DEBUG_INFO = NO; 343 | MTL_FAST_MATH = YES; 344 | SDKROOT = iphoneos; 345 | SWIFT_COMPILATION_MODE = wholemodule; 346 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 347 | VALIDATE_PRODUCT = YES; 348 | }; 349 | name = Release; 350 | }; 351 | 4206BA2322C11160009CCA12 /* Debug */ = { 352 | isa = XCBuildConfiguration; 353 | buildSettings = { 354 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 355 | CODE_SIGN_STYLE = Automatic; 356 | DEVELOPMENT_ASSET_PATHS = "Minesweeper/Preview\\ Content"; 357 | DEVELOPMENT_TEAM = NZLK3N5NR4; 358 | ENABLE_PREVIEWS = YES; 359 | INFOPLIST_FILE = Minesweeper/Info.plist; 360 | LD_RUNPATH_SEARCH_PATHS = ( 361 | "$(inherited)", 362 | "@executable_path/Frameworks", 363 | ); 364 | PRODUCT_BUNDLE_IDENTIFIER = com.appodeal.Minesweeper; 365 | PRODUCT_NAME = "$(TARGET_NAME)"; 366 | SWIFT_VERSION = 5.0; 367 | TARGETED_DEVICE_FAMILY = "1,2"; 368 | }; 369 | name = Debug; 370 | }; 371 | 4206BA2422C11160009CCA12 /* Release */ = { 372 | isa = XCBuildConfiguration; 373 | buildSettings = { 374 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 375 | CODE_SIGN_STYLE = Automatic; 376 | DEVELOPMENT_ASSET_PATHS = "Minesweeper/Preview\\ Content"; 377 | DEVELOPMENT_TEAM = NZLK3N5NR4; 378 | ENABLE_PREVIEWS = YES; 379 | INFOPLIST_FILE = Minesweeper/Info.plist; 380 | LD_RUNPATH_SEARCH_PATHS = ( 381 | "$(inherited)", 382 | "@executable_path/Frameworks", 383 | ); 384 | PRODUCT_BUNDLE_IDENTIFIER = com.appodeal.Minesweeper; 385 | PRODUCT_NAME = "$(TARGET_NAME)"; 386 | SWIFT_VERSION = 5.0; 387 | TARGETED_DEVICE_FAMILY = "1,2"; 388 | }; 389 | name = Release; 390 | }; 391 | /* End XCBuildConfiguration section */ 392 | 393 | /* Begin XCConfigurationList section */ 394 | 4206BA0922C1115E009CCA12 /* Build configuration list for PBXProject "Minesweeper" */ = { 395 | isa = XCConfigurationList; 396 | buildConfigurations = ( 397 | 4206BA2022C11160009CCA12 /* Debug */, 398 | 4206BA2122C11160009CCA12 /* Release */, 399 | ); 400 | defaultConfigurationIsVisible = 0; 401 | defaultConfigurationName = Release; 402 | }; 403 | 4206BA2222C11160009CCA12 /* Build configuration list for PBXNativeTarget "Minesweeper" */ = { 404 | isa = XCConfigurationList; 405 | buildConfigurations = ( 406 | 4206BA2322C11160009CCA12 /* Debug */, 407 | 4206BA2422C11160009CCA12 /* Release */, 408 | ); 409 | defaultConfigurationIsVisible = 0; 410 | defaultConfigurationName = Release; 411 | }; 412 | /* End XCConfigurationList section */ 413 | }; 414 | rootObject = 4206BA0622C1115E009CCA12 /* Project object */; 415 | } 416 | -------------------------------------------------------------------------------- /Minesweeper.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Minesweeper.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Minesweeper.xcodeproj/project.xcworkspace/xcuserdata/staskochkin.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staskochkin/Minesweeper-SwiftUI/3a77a5c92f8b0f453bb1ecf3d8fb02cdabef7e0c/Minesweeper.xcodeproj/project.xcworkspace/xcuserdata/staskochkin.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Minesweeper.xcodeproj/xcuserdata/staskochkin.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Minesweeper.xcodeproj/xcuserdata/staskochkin.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Minesweeper.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Minesweeper/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 | } -------------------------------------------------------------------------------- /Minesweeper/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Minesweeper/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Minesweeper/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // Minesweeper 4 | // 5 | // Created by Stas Kochkin on 25/06/2019. 6 | // Copyright © 2019 appodeal. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | extension Array { 13 | mutating func forEach(body: (inout Element) throws -> Void) rethrows { 14 | for index in indices { 15 | try body(&self[index]) 16 | } 17 | } 18 | } 19 | 20 | func bind(_ x: T, _ closure:(T) -> U) -> U { 21 | closure(x) 22 | } 23 | -------------------------------------------------------------------------------- /Minesweeper/Host/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Minesweeper 4 | // 5 | // Created by Stas Kochkin on 24/06/2019. 6 | // Copyright © 2019 appodeal. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | func application(_ application: UIApplication, 14 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 15 | UINavigationBar.appearance().prefersLargeTitles = false 16 | return true 17 | } 18 | 19 | // MARK: UISceneSession Lifecycle 20 | func application(_ application: UIApplication, 21 | configurationForConnecting connectingSceneSession: UISceneSession, 22 | options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /Minesweeper/Host/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Minesweeper 4 | // 5 | // Created by Stas Kochkin on 24/06/2019. 6 | // Copyright © 2019 appodeal. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | private lazy var gameplay: Gameplay = { 14 | Gameplay(x: 10, y: 10) 15 | }() 16 | 17 | var window: UIWindow? 18 | 19 | func scene(_ scene: UIScene, 20 | willConnectTo session: UISceneSession, 21 | options connectionOptions: UIScene.ConnectionOptions) { 22 | guard let scene = scene as? UIWindowScene else { return } 23 | let window = UIWindow(windowScene: scene) 24 | window.rootViewController = UIHostingController(rootView: GameView().environmentObject(gameplay)) 25 | self.window = window 26 | window.makeKeyAndVisible() 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /Minesweeper/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 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UILaunchStoryboardName 33 | LaunchScreen 34 | UISceneConfigurationName 35 | Default Configuration 36 | UISceneDelegateClassName 37 | $(PRODUCT_MODULE_NAME).SceneDelegate 38 | 39 | 40 | 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UISupportedInterfaceOrientations~ipad 55 | 56 | UIInterfaceOrientationPortrait 57 | UIInterfaceOrientationPortraitUpsideDown 58 | UIInterfaceOrientationLandscapeLeft 59 | UIInterfaceOrientationLandscapeRight 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Minesweeper/Models/Board.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Board.swift 3 | // Minesweeper 4 | // 5 | // Created by Stas Kochkin on 24/06/2019. 6 | // Copyright © 2019 appodeal. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | typealias Index = (x: Int, y: Int) 13 | 14 | struct IndexedTile where T: BoardTile, T: Equatable { 15 | let id: UUID = UUID() 16 | let index: Index 17 | let value: T 18 | 19 | init(index: Index, value: T) { 20 | self.value = value 21 | self.index = index 22 | } 23 | } 24 | 25 | extension IndexedTile: Identifiable, Equatable { 26 | typealias ID = UUID 27 | 28 | static func == (lhs: IndexedTile, rhs: IndexedTile) -> Bool { 29 | return lhs.value == rhs.value 30 | } 31 | } 32 | 33 | 34 | final class Board where T: BoardTile, T: Equatable { 35 | typealias IndexedTileType = IndexedTile 36 | 37 | private(set) var tiles:[IndexedTileType] = [] 38 | let size: Index 39 | 40 | var isWin: Bool { tiles.reduce(true) { $0 && $1.value.isRevealed } } 41 | 42 | init(x: Int, y: Int) { 43 | size = (x, y) 44 | } 45 | 46 | subscript(index: Index) -> IndexedTileType? { 47 | guard index.x >= 0, index.y >= 0, index.x < size.x, index.y < size.y else { return nil } 48 | return tiles[index.y * size.x + index.x] 49 | } 50 | 51 | func indexed(tile: T?) -> IndexedTileType? { 52 | return tile.flatMap { tile in tiles.first { $0.value == tile } } 53 | } 54 | 55 | func arrange() { 56 | tiles.removeAll() 57 | (0..(index: (x, y), value: T()) 60 | tiles.append(tile) 61 | } 62 | } 63 | } 64 | 65 | func startGame(_ targetTile: T?) { 66 | var protectedTiles:[IndexedTileType] = [] 67 | if let targetTile = indexed(tile: targetTile) { 68 | protectedTiles.append(contentsOf: neighbors(of: targetTile)) 69 | protectedTiles.append(targetTile) 70 | } 71 | 72 | let maxMinesCount: Int = ((size.x * size.y) / 7) 73 | var posibleMines: [IndexedTileType] = tiles.filter { !protectedTiles.contains($0) } 74 | 75 | (0.. [IndexedTileType] { 86 | let dx:[Int] = [-1, 0, 1, -1, 1, -1, 0, 1] 87 | let dy:[Int] = [-1, -1, -1, 0, 0, 1, 1, 1] 88 | return dx 89 | .enumerated() 90 | .compactMap { 91 | let index: Index = (targetTile.index.x + $0.element, 92 | targetTile.index.y + dy[$0.offset]) 93 | return self[index] 94 | } 95 | } 96 | 97 | func endGame() { 98 | tiles.forEach { $0.value.reveal() } 99 | } 100 | } 101 | 102 | extension Board: CustomDebugStringConvertible { 103 | var debugDescription: String { "" } 104 | } 105 | -------------------------------------------------------------------------------- /Minesweeper/Models/Gameplay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GameBoard.swift 3 | // Minesweeper 4 | // 5 | // Created by Stas Kochkin on 24/06/2019. 6 | // Copyright © 2019 appodeal. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import Combine 12 | 13 | 14 | enum GameState { 15 | case idle 16 | case started 17 | case completed(Bool) 18 | } 19 | 20 | extension BoardTile { 21 | var isRevealed: Bool { 22 | switch state { 23 | case .revealed: return true 24 | case .marked: return isMine 25 | default: return false 26 | } 27 | } 28 | } 29 | 30 | final class Gameplay: ObservableObject { 31 | typealias BoardType = Board 32 | 33 | @Published private(set) var state: GameState = .idle 34 | @Published private(set) var board: BoardType 35 | 36 | private var settings: Settings 37 | private(set) var timing: Timing? = nil 38 | var isStarted: Bool { 39 | switch state { 40 | case .idle: return false 41 | default: return true 42 | } 43 | } 44 | 45 | init(x: Int, y: Int) { 46 | board = BoardType(x: x, y: y) 47 | settings = Settings(difficulty: 0, timerEnabled: true) 48 | } 49 | 50 | func reveal(_ tile: BoardTile) { 51 | switch state { 52 | case .started: _reveal(tile: tile) 53 | case .idle: startGame(tile) 54 | case .completed: break 55 | } 56 | } 57 | 58 | func mark(_ tile: BoardTile) { 59 | switch state { 60 | case .started: _mark(tile: tile) 61 | default: break 62 | } 63 | } 64 | 65 | func startGame(_ tile: BoardTile? = nil) { 66 | defer { state = .started } 67 | board.arrange() 68 | board.startGame(tile as? Tile) 69 | timing?.stop() 70 | startTimingIfNeeded() 71 | guard let tile = tile else { return } 72 | _reveal(tile: tile) 73 | } 74 | 75 | private func _reveal(tile: BoardTile, force: Bool = true) { 76 | guard tile.state != .marked, force || tile.state != .activated else { return } 77 | 78 | tile.reveal() 79 | 80 | if tile.state == .activated { 81 | endGame() 82 | state = .completed(false) 83 | } else if board.isWin { 84 | endGame() 85 | state = .completed(true) 86 | } else if tile.minesAround == 0 { 87 | board 88 | .indexed(tile: tile as? Tile) 89 | .map(board.neighbors)?.filter { !$0.value.isRevealed && !$0.value.isMine } 90 | .forEach { _reveal(tile: $0.value, force: false) } 91 | } 92 | } 93 | 94 | private func _mark(tile: BoardTile) { 95 | tile.mark() 96 | guard board.isWin else { return } 97 | endGame() 98 | state = .completed(true) 99 | } 100 | 101 | private func startTimingIfNeeded() { 102 | guard settings.timerEnabled else { return } 103 | timing = Timing() 104 | timing?.start() 105 | } 106 | 107 | private func endGame() { 108 | board.endGame() 109 | timing?.stop() 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Minesweeper/Models/Settings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Settings.swift 3 | // Minesweeper 4 | // 5 | // Created by Stas Kochkin on 30/06/2019. 6 | // Copyright © 2019 appodeal. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct Settings { 13 | var difficulty: Int 14 | var timerEnabled: Bool 15 | } 16 | -------------------------------------------------------------------------------- /Minesweeper/Models/Tile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Tile.swift 3 | // Minesweeper 4 | // 5 | // Created by Stas Kochkin on 24/06/2019. 6 | // Copyright © 2019 appodeal. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import Combine 12 | 13 | enum TileState: Equatable { 14 | case unrevealed 15 | case revealed(Int) 16 | case marked 17 | case activated 18 | case deactivated 19 | } 20 | 21 | 22 | protocol BoardTile { 23 | var state: TileState { get } 24 | var minesAround: Int { get } 25 | var isMine: Bool { get } 26 | 27 | init() 28 | 29 | func mark() 30 | func reveal() 31 | func mine() 32 | func incrementMinesAround() 33 | } 34 | 35 | 36 | final class Tile: BoardTile, ObservableObject, Identifiable { 37 | internal let id: UUID = UUID() 38 | 39 | @Published private(set) var state: TileState = .unrevealed 40 | 41 | private(set) var minesAround = 0, isMine = false 42 | 43 | func mine() { 44 | isMine = true 45 | } 46 | 47 | func incrementMinesAround() { 48 | minesAround += 1 49 | } 50 | 51 | func mark() { 52 | if state == .marked { 53 | state = .unrevealed 54 | } else if state == .unrevealed { 55 | state = .marked 56 | } 57 | } 58 | 59 | func reveal() { 60 | if state == .marked && isMine { 61 | state = .deactivated 62 | } else if isMine { 63 | state = .activated 64 | } else if state != .marked { 65 | state = .revealed(minesAround) 66 | } 67 | } 68 | } 69 | 70 | extension Tile: Equatable { 71 | static func == (lhs: Tile, rhs: Tile) -> Bool { 72 | return lhs.id == rhs.id 73 | } 74 | } 75 | 76 | 77 | extension Tile: CustomDebugStringConvertible { 78 | var debugDescription: String { 79 | return "Tile, mines around: \(minesAround), is mine: \(isMine), state: \(state)" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Minesweeper/Models/Timing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Timing.swift 3 | // Minesweeper 4 | // 5 | // Created by Stas Kochkin on 30/06/2019. 6 | // Copyright © 2019 appodeal. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | import SwiftUI 12 | 13 | 14 | final class Timing: ObservableObject { 15 | @Published private(set) var playingTime: TimeInterval = 0 16 | 17 | private var timer: Timer? 18 | 19 | func start() { 20 | playingTime = 0 21 | timer = Timer.scheduledTimer(withTimeInterval: 1, 22 | repeats: true) { [weak self] _ in self?.playingTime += 1 } 23 | timer?.fire() 24 | } 25 | 26 | func stop() { 27 | timer?.invalidate() 28 | timer = nil 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Minesweeper/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Minesweeper/Views/BoardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoardView.swift 3 | // Minesweeper 4 | // 5 | // Created by Stas Kochkin on 24/06/2019. 6 | // Copyright © 2019 appodeal. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import Combine 12 | 13 | 14 | struct BoardView: View { 15 | typealias TileType = IndexedTile 16 | 17 | @EnvironmentObject var gameplay: Gameplay 18 | 19 | private let offset: CGFloat = 12 20 | 21 | var body: some View { 22 | ZStack(alignment: .center) { 23 | GeometryReader() { proxy in 24 | ForEach(self.gameplay.board.tiles, id: \.id) { tile in 25 | self.view( 26 | for: tile, 27 | board: self.gameplay.board, 28 | size: proxy.size) 29 | } 30 | .clipped() 31 | } 32 | } 33 | } 34 | 35 | private func preferredSize(tiles: Index, size: CGSize) -> CGFloat { 36 | let boardWidth: CGFloat = CGFloat(gameplay.board.size.x) 37 | let boardHeight: CGFloat = CGFloat(gameplay.board.size.y) 38 | 39 | let width: CGFloat = (size.width - (boardWidth + 1) * offset) / boardWidth 40 | let height: CGFloat = (size.height - (boardHeight + 1) * offset) / boardHeight 41 | 42 | return min(width, height) 43 | } 44 | 45 | private func view(for tile:TileType, 46 | board: Board , 47 | size: CGSize) -> some View { 48 | let tileSize: CGFloat = preferredSize(tiles: board.size, size: size) 49 | let x: CGFloat = CGFloat(tile.index.x) * (tileSize + offset) + tileSize / 2 + offset 50 | let y: CGFloat = CGFloat(tile.index.y) * (tileSize + offset) + tileSize / 2 + offset 51 | 52 | return TileView(tile: tile.value, gameplay: gameplay) 53 | .frame(width: tileSize, height: tileSize, alignment: .center) 54 | .position(x: x, y: y) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Minesweeper/Views/GameView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Minesweeper 4 | // 5 | // Created by Stas Kochkin on 24/06/2019. 6 | // Copyright © 2019 appodeal. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct GameView : View { 12 | @EnvironmentObject var gameplay: Gameplay 13 | 14 | var body: some View { 15 | NavigationView { 16 | VStack { 17 | if gameplay.isStarted { 18 | Spacer() 19 | gameplay.timing.map(TimerView.init) 20 | GeometryReader { proxy in 21 | self.boardView 22 | .frame( 23 | width: proxy.boardSize, 24 | height: proxy.boardSize, 25 | alignment: .center 26 | ) 27 | } 28 | .transition(self.transition) 29 | .animation(self.animation) 30 | } 31 | 32 | PlayButton(action: self.startNewGame) 33 | .environmentObject(self.gameplay) 34 | } 35 | .transition(transition) 36 | .navigationBarTitle("Minesweeper", displayMode: .inline) 37 | .navigationBarItems(trailing: SettingsButton()) 38 | .font(Font.system(.headline, design: Font.Design.monospaced)) 39 | .foregroundColor(.primary) 40 | } 41 | } 42 | 43 | func startNewGame() { 44 | gameplay.startGame() 45 | } 46 | 47 | private var boardView: some View { 48 | return BoardView() 49 | .environmentObject(self.gameplay) 50 | } 51 | 52 | private var transition: AnyTransition { 53 | let insertion = AnyTransition 54 | .move(edge: .top) 55 | .combined(with: .opacity) 56 | let removal = AnyTransition 57 | .move(edge: .bottom) 58 | .combined(with: .opacity) 59 | return .asymmetric(insertion: insertion, removal: removal) 60 | } 61 | 62 | private var animation: Animation { 63 | Animation.spring().speed(2) 64 | } 65 | } 66 | 67 | private extension GeometryProxy { 68 | var boardSize: CGFloat { 69 | return min(size.width, size.height) - 16 70 | } 71 | } 72 | 73 | private struct SettingsButton: View { 74 | @State private var isShown: Bool = false 75 | var body: some View { 76 | return Button(action: { 77 | self.isShown.toggle() 78 | }) { 79 | Image(systemName: "gear") 80 | .renderingMode(.template) 81 | .foregroundColor(Color.secondary) 82 | .imageScale(.medium) 83 | } 84 | .sheet(isPresented: self.$isShown) { SettingsView() } 85 | } 86 | 87 | var animation: Animation { 88 | Animation 89 | .spring() 90 | .speed(2) 91 | } 92 | } 93 | 94 | #if DEBUG 95 | struct ContentView_Previews : PreviewProvider { 96 | static let gameplay: Gameplay = { 97 | let gameplay = Gameplay(x: 3, y: 3) 98 | gameplay.startGame() 99 | return gameplay 100 | }() 101 | 102 | static var previews: some View { 103 | GameView().environmentObject(gameplay) 104 | } 105 | } 106 | #endif 107 | -------------------------------------------------------------------------------- /Minesweeper/Views/PlayButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayButton.swift 3 | // Minesweeper 4 | // 5 | // Created by Stas Kochkin on 24.10.2019. 6 | // Copyright © 2019 appodeal. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct PlayButton: View { 12 | typealias ViewProps = (text: Text, gradient: Gradient) 13 | 14 | @EnvironmentObject var gameplay: Gameplay 15 | var action: () -> () 16 | var state: GameState { return gameplay.state } 17 | 18 | var body: some View { 19 | let props: ViewProps 20 | switch state { 21 | case .idle: props = idle() 22 | case .started: props = started() 23 | case .completed(let isWin):props = completed(isWin) 24 | } 25 | 26 | let background = LinearGradient( 27 | gradient: props.gradient, 28 | startPoint: UnitPoint(x: 0.5, y: 0), 29 | endPoint: UnitPoint(x: 1, y: 0.5) 30 | ) 31 | 32 | return Button(action: withAnimation { action }) { 33 | props.text.frame( 34 | width: 320, 35 | height: 33, 36 | alignment: .center 37 | )} 38 | .background(background) 39 | .cornerRadius(16) 40 | .transition(transition) 41 | } 42 | 43 | private var transition: AnyTransition { 44 | let insertion = AnyTransition 45 | .scale(scale: 1.2) 46 | .combined(with: .opacity) 47 | .animation(animation) 48 | let removal = AnyTransition 49 | .scale(scale: 0.8) 50 | .combined(with: .opacity) 51 | .animation(animation) 52 | return .asymmetric( 53 | insertion: insertion, 54 | removal: removal 55 | ) 56 | } 57 | 58 | private var animation: Animation { 59 | Animation 60 | .easeInOut 61 | .speed(2) 62 | .delay(0.1) 63 | } 64 | 65 | private func idle() -> ViewProps { 66 | return ( 67 | Text("New Game") 68 | .font(Font.system(.callout)) 69 | .foregroundColor(.white), 70 | Gradient(colors: [ 71 | Color.blue.opacity(0.6), 72 | Color.blue.opacity(0.9) 73 | ]) 74 | ) 75 | } 76 | 77 | private func started() -> ViewProps { 78 | return ( 79 | Text("Skip") 80 | .font(Font.system(size: 12, design: .rounded)) 81 | .foregroundColor(.gray), 82 | Gradient(colors: [ 83 | Color.gray.opacity(0.3), 84 | Color.black.opacity(0.2) 85 | ]) 86 | ) 87 | } 88 | 89 | private func completed(_ isWin: Bool) -> ViewProps { 90 | return ( 91 | Text(isWin ? "Your Win!. Start new game?" : "Your Lose. Try again?") 92 | .font(Font.system(size: 12, design: .monospaced)) 93 | .foregroundColor(.primary), 94 | isWin ? 95 | Gradient(colors: [Color.yellow.opacity(0.8), Color.orange.opacity(0.8)]) : 96 | Gradient(colors: [Color.purple.opacity(0.3), Color.red.opacity(0.5)]) 97 | ) 98 | } 99 | } 100 | 101 | #if DEBUG 102 | struct PlayButton_Previews : PreviewProvider { 103 | static let idleGameplay: Gameplay = { 104 | return Gameplay(x: 1, y: 1) 105 | }() 106 | 107 | static let startedGameplay: Gameplay = { 108 | let gameplay = Gameplay(x: 1, y: 1) 109 | gameplay.startGame() 110 | return gameplay 111 | }() 112 | 113 | static var previews: some View { 114 | Group { 115 | PlayButton(action: {}).environmentObject(idleGameplay) 116 | PlayButton(action: {}).environmentObject(startedGameplay) 117 | }.previewLayout(.fixed(width: 350, height: 50)) 118 | 119 | } 120 | } 121 | #endif 122 | -------------------------------------------------------------------------------- /Minesweeper/Views/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // Minesweeper 4 | // 5 | // Created by Stas Kochkin on 30/06/2019. 6 | // Copyright © 2019 appodeal. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | 13 | struct SettingsView: View { 14 | var body: some View { 15 | Text("Settings") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Minesweeper/Views/TImerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TImerView.swift 3 | // Minesweeper 4 | // 5 | // Created by Stas Kochkin on 30/06/2019. 6 | // Copyright © 2019 appodeal. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct TimerView: View { 12 | @ObservedObject var timing: Timing 13 | 14 | var body: some View { 15 | Text(timing.playingTime.str) 16 | .foregroundColor(Color.secondary) 17 | .fontWeight(.semibold) 18 | .font(Font.system(size: 14)) 19 | .shadow(radius: 5) 20 | } 21 | } 22 | 23 | private extension TimeInterval { 24 | var str: String { 25 | return String(Int(self / 60)) + 26 | ":" + 27 | String(Int(truncatingRemainder(dividingBy: 60))) 28 | } 29 | } 30 | 31 | #if DEBUG 32 | struct TimerView_Previews : PreviewProvider { 33 | static var previews: some View { 34 | TimerView(timing: Timing()) 35 | } 36 | } 37 | #endif 38 | -------------------------------------------------------------------------------- /Minesweeper/Views/TileView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TileView.swift 3 | // Minesweeper 4 | // 5 | // Created by Stas Kochkin on 24/06/2019. 6 | // Copyright © 2019 appodeal. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | 13 | struct TileView: View { 14 | private struct Constants { 15 | static func font(_ height: CGFloat) -> Font { 16 | return Font.system( 17 | size: height, 18 | design: Font.Design.rounded 19 | ) 20 | } 21 | 22 | static var unrevealedView: Text { Text("?").foregroundColor(Color.white) } 23 | static var activatedView: Text { Text("💣") } 24 | static var deactivatedView: Text { Text("🎖") } 25 | static var markedView: Text { Text("🙈") } 26 | 27 | static func revealedView(_ count: Int) -> Text { 28 | let text = count > 0 ? "\(count)" : " " 29 | return Text(text).foregroundColor(Color.white) 30 | } 31 | } 32 | 33 | @ObservedObject var tile: Tile 34 | @GestureState var isLongPressed = false 35 | 36 | let gameplay: Gameplay 37 | 38 | var body: some View { 39 | GeometryReader { geometry in 40 | Group { 41 | GeometryReader { groupGeometry in 42 | self.contentBody 43 | .font(Constants.font(groupGeometry.size.height / 3 * 2)) 44 | .shadow(radius: 5) 45 | .frame( 46 | width: groupGeometry.size.height, 47 | height: groupGeometry.size.width, 48 | alignment: .center 49 | ) 50 | } 51 | } 52 | .saturation(1.5) 53 | .background(self.backgroundStyle) 54 | .cornerRadius(CGFloat.greatestFiniteMagnitude) 55 | .shadow(radius: 5) 56 | .scaleEffect(self.isLongPressed ? 1.3 : 1) 57 | .frame( 58 | width: geometry.size.height * 0.95, 59 | height: geometry.size.width * 0.95, 60 | alignment: .center 61 | ) 62 | .gesture(self.tapGesture) 63 | .simultaneousGesture(self.longPressGesture) 64 | .animation(.spring()) 65 | } 66 | .transition(AnyTransition.tile) 67 | .animation(Animation.tile) 68 | } 69 | 70 | func reveal() { 71 | withAnimation { 72 | gameplay.reveal(tile.self) 73 | } 74 | } 75 | 76 | func mark() { 77 | withAnimation { 78 | gameplay.mark(tile.self) 79 | } 80 | } 81 | } 82 | 83 | 84 | private extension TileView { 85 | var backgroundStyle: some View { 86 | let gradient: Gradient 87 | switch tile.state { 88 | case .revealed(let count): gradient = revealedBacgroundGradient(count) 89 | case .unrevealed: gradient = Gradient(colors: [ 90 | Color.accentColor.opacity(0.6), 91 | Color.accentColor.opacity(0.9)]) 92 | case .activated: gradient = Gradient(colors: [ 93 | Color.red.opacity(0.6), 94 | Color.red.opacity(0.9) 95 | ]) 96 | case .deactivated: gradient = Gradient(colors: [ 97 | Color.blue.opacity(0.6), 98 | Color.green.opacity(0.9) 99 | ]) 100 | case .marked: gradient = Gradient(colors: [ 101 | Color.green.opacity(0.6), 102 | Color.purple.opacity(0.9) 103 | ]) 104 | } 105 | 106 | return AngularGradient(gradient: gradient, 107 | center: UnitPoint(x: 0, y: 0)) 108 | } 109 | 110 | var borderStyle: some ShapeStyle { 111 | let gradient = Gradient(colors: [ 112 | Color.accentColor.opacity(0.6), 113 | Color.accentColor.opacity(0.9) 114 | ]) 115 | return AngularGradient( 116 | gradient: gradient, 117 | center: UnitPoint(x: 0, y: 0) 118 | ) 119 | } 120 | 121 | var tapGesture: some Gesture { 122 | TapGesture() 123 | .onEnded(self.reveal) 124 | } 125 | 126 | var longPressGesture: some Gesture { 127 | LongPressGesture( 128 | minimumDuration: 0.2, 129 | maximumDistance: 5 130 | ) 131 | .onEnded { _ in self.mark() } 132 | .updating($isLongPressed) { value, state, transcation in state = value } 133 | } 134 | 135 | var contentBody: some View { 136 | let view: Text 137 | switch tile.state { 138 | case .unrevealed: view = Constants.unrevealedView 139 | case .revealed(let count): view = Constants.revealedView(count) 140 | case .activated: view = Constants.activatedView 141 | case .deactivated: view = Constants.deactivatedView 142 | case .marked: view = Constants.markedView 143 | } 144 | return view 145 | } 146 | 147 | func revealedBacgroundGradient(_ count: Int) -> Gradient { 148 | guard count > 0 else { return Gradient(colors: [Color.clear]) } 149 | let colors: [Color] = [ 150 | Color.yellow.opacity(0.5), 151 | Color.yellow.opacity(0.85), 152 | Color.orange.opacity(0.5), 153 | Color.orange.opacity(0.85), 154 | Color.red.opacity(0.5), 155 | Color.red.opacity(0.85) 156 | ] 157 | return Gradient(colors: [ 158 | colors[count - 1], 159 | colors[min(colors.count - 1, count + 1)] 160 | ]) 161 | } 162 | } 163 | 164 | private extension AnyTransition { 165 | static var tile: AnyTransition { 166 | let insertion = AnyTransition 167 | .scale(scale: 1.3) 168 | .combined(with: .opacity) 169 | let removal = AnyTransition 170 | .scale(scale: 0.6) 171 | .combined(with: .opacity) 172 | return .asymmetric( 173 | insertion: insertion, 174 | removal: removal 175 | ) 176 | } 177 | } 178 | 179 | private extension Animation { 180 | static var tile: Animation { 181 | return Animation 182 | .easeIn 183 | .delay(Double.random(in: 0.1...0.5)) 184 | } 185 | } 186 | 187 | #if DEBUG 188 | struct TitleView_Previews : PreviewProvider { 189 | static let gameplay = Gameplay(x: 1, y: 1) 190 | static let markedTile: Tile = { 191 | var tile = Tile() 192 | tile.mark() 193 | return tile 194 | }() 195 | 196 | static let mineTile: Tile = { 197 | var tile = Tile() 198 | tile.mine() 199 | tile.reveal() 200 | return tile 201 | }() 202 | 203 | static let unrevealedTile: Tile = { 204 | var tile = Tile() 205 | tile.incrementMinesAround() 206 | return tile 207 | }() 208 | 209 | static let revealedTile: Tile = { 210 | var tile = Tile() 211 | tile.incrementMinesAround() 212 | tile.incrementMinesAround() 213 | tile.reveal() 214 | return tile 215 | }() 216 | 217 | static var previews: some View { 218 | Group { 219 | TileView(tile: markedTile, gameplay: gameplay) 220 | TileView(tile: mineTile, gameplay: gameplay) 221 | TileView(tile: unrevealedTile, gameplay: gameplay) 222 | TileView(tile: revealedTile, gameplay: gameplay) 223 | } 224 | .previewLayout(.fixed(width: 200, height: 200)) 225 | } 226 | } 227 | #endif 228 | 229 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 💣 Minesweeper SwiftUI 2 | 3 | Simple Minesweeper game written in SwiftUI and Combine: 4 | 5 | - One tap to reveal tile 6 | - Long press to mark tile as bomb 7 | 8 | 9 | ![Screenshot](Screenshot.png) -------------------------------------------------------------------------------- /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staskochkin/Minesweeper-SwiftUI/3a77a5c92f8b0f453bb1ecf3d8fb02cdabef7e0c/Screenshot.png --------------------------------------------------------------------------------