diff --git a/GameOfLife/.swiftlint.yml b/GameOfLife/.swiftlint.yml new file mode 100644 index 000000000..b943658a3 --- /dev/null +++ b/GameOfLife/.swiftlint.yml @@ -0,0 +1,88 @@ +# SwiftLint configuration file +# +# For any changes to this file, tag all team members in the PR for quick approval or discussion +# +# See current configuration with: +# swiftlint rules +# +# See documentation for the latest (master branch) rules at: https://github.com/realm/SwiftLint/blob/master/Rules.md +# and https://github.com/realm/SwiftLint for an example of options in this file +# +disabled_rules: # rule identifiers to exclude from running + - class_delegate_protocol # This breaks if you have a delegate protocol that inherits from a class-bound protocol (see: SwiftyTables DataSourceDelegate) + - identifier_name + - redundant_objc_attribute + - todo + - trailing_comma + - first_where + - function_body_length + - force_cast + - line_length + - cyclomatic_complexity + - vertical_parameter_alignment # does not work with tab indentation: https://github.com/realm/SwiftLint/issues/1573 + +excluded: # paths to ignore during linting. + - Carthage + - Swift Style Guide.playground + - fastlane/* + - Pods + +opt_in_rules: + - anyobject_protocol + - closure_spacing + - collection_alignment + - conditional_returns_on_newline + - convenience_type + - empty_count + - empty_xctest_method + - explicit_init + - fatal_error_message + - first_where + - function_default_parameter_at_end + - identical_operands + - implicit_return + - joined_default_parameter + - last_where + - legacy_random + - let_var_whitespace + - modifier_order + - multiline_arguments + - multiline_parameters + - operator_usage_whitespace + - overridden_super_call + - private_outlet + - prohibited_super_call + - redundant_nil_coalescing + - static_operator + - toggle_bool + - unavailable_function + - unneeded_parentheses_in_closure_argument + - untyped_error_in_catch + - unused_control_flow_label + - vertical_parameter_alignment_on_call + - xct_specific_matcher + + # Experimental + - explicit_self + - unused_import + - unused_private_declaration + +# Rule options +conditional_returns_on_newline: + if_only: true +file_length: + warning: 800 + error: 1000 +line_length: + warning: 200 + ignores_comments: true + ignores_interpolated_strings: true +multiline_arguments: + only_enforce_after_first_closure_on_first_line: true +type_body_length: + warning: 400 + error: 700 +vertical_whitespace: + max_empty_lines: 2 +trailing_whitespace: + ignores_empty_lines: true diff --git a/GameOfLife/GameOfLife.xcodeproj/project.pbxproj b/GameOfLife/GameOfLife.xcodeproj/project.pbxproj new file mode 100644 index 000000000..83e0b8141 --- /dev/null +++ b/GameOfLife/GameOfLife.xcodeproj/project.pbxproj @@ -0,0 +1,560 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 63009C0F24807864008D9703 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63009C0E24807864008D9703 /* Notification.swift */; }; + 635CEFAA247F4E4700167BB7 /* Cell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 635CEFA9247F4E4700167BB7 /* Cell.swift */; }; + 63BCD153247D6E3B00E34EC4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63BCD152247D6E3B00E34EC4 /* AppDelegate.swift */; }; + 63BCD155247D6E3B00E34EC4 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63BCD154247D6E3B00E34EC4 /* SceneDelegate.swift */; }; + 63BCD15C247D6E3C00E34EC4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 63BCD15B247D6E3C00E34EC4 /* Assets.xcassets */; }; + 63BCD15F247D6E3C00E34EC4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 63BCD15D247D6E3C00E34EC4 /* LaunchScreen.storyboard */; }; + 63BCD16B247D6F3A00E34EC4 /* PreviewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63BCD16A247D6F3A00E34EC4 /* PreviewExtensions.swift */; }; + 63BCD16E247D745400E34EC4 /* GameOfLifeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63BCD16D247D745400E34EC4 /* GameOfLifeViewController.swift */; }; + 63C910CD247F51D900490508 /* GameOfLifeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63C910CC247F51D900490508 /* GameOfLifeTests.swift */; }; + 63C910D5247F534F00490508 /* World.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63C910D4247F534F00490508 /* World.swift */; }; + 63C910D7247F54A800490508 /* GameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63C910D6247F54A800490508 /* GameView.swift */; }; + 63FF0E60248062D20080CF20 /* AboutGameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63FF0E5F248062D20080CF20 /* AboutGameViewController.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 63C910CF247F51D900490508 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 63BCD147247D6E3B00E34EC4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 63BCD14E247D6E3B00E34EC4; + remoteInfo = GameOfLife; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 63009C0E24807864008D9703 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; + 635CEFA9247F4E4700167BB7 /* Cell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cell.swift; sourceTree = ""; }; + 63BCD14F247D6E3B00E34EC4 /* GameOfLife.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GameOfLife.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 63BCD152247D6E3B00E34EC4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 63BCD154247D6E3B00E34EC4 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 63BCD15B247D6E3C00E34EC4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 63BCD15E247D6E3C00E34EC4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 63BCD160247D6E3C00E34EC4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 63BCD16A247D6F3A00E34EC4 /* PreviewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewExtensions.swift; sourceTree = ""; }; + 63BCD16D247D745400E34EC4 /* GameOfLifeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameOfLifeViewController.swift; sourceTree = ""; }; + 63C910CA247F51D900490508 /* GameOfLifeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GameOfLifeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 63C910CC247F51D900490508 /* GameOfLifeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameOfLifeTests.swift; sourceTree = ""; }; + 63C910CE247F51D900490508 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 63C910D4247F534F00490508 /* World.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = World.swift; sourceTree = ""; }; + 63C910D6247F54A800490508 /* GameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameView.swift; sourceTree = ""; }; + 63FF0E5F248062D20080CF20 /* AboutGameViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutGameViewController.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 63BCD14C247D6E3B00E34EC4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 63C910C7247F51D900490508 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 635CEFA8247F4E3B00167BB7 /* Model */ = { + isa = PBXGroup; + children = ( + 635CEFA9247F4E4700167BB7 /* Cell.swift */, + 63C910D4247F534F00490508 /* World.swift */, + ); + path = Model; + sourceTree = ""; + }; + 63BCD146247D6E3B00E34EC4 = { + isa = PBXGroup; + children = ( + 63BCD151247D6E3B00E34EC4 /* GameOfLife */, + 63C910CB247F51D900490508 /* GameOfLifeTests */, + 63BCD150247D6E3B00E34EC4 /* Products */, + ); + sourceTree = ""; + }; + 63BCD150247D6E3B00E34EC4 /* Products */ = { + isa = PBXGroup; + children = ( + 63BCD14F247D6E3B00E34EC4 /* GameOfLife.app */, + 63C910CA247F51D900490508 /* GameOfLifeTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 63BCD151247D6E3B00E34EC4 /* GameOfLife */ = { + isa = PBXGroup; + children = ( + 635CEFA8247F4E3B00167BB7 /* Model */, + 63BCD167247D6EA600E34EC4 /* View */, + 63BCD16C247D743A00E34EC4 /* Controller */, + 63BCD169247D6F2900E34EC4 /* Extensions */, + 63BCD166247D6E4A00E34EC4 /* Support */, + ); + path = GameOfLife; + sourceTree = ""; + }; + 63BCD166247D6E4A00E34EC4 /* Support */ = { + isa = PBXGroup; + children = ( + 63BCD152247D6E3B00E34EC4 /* AppDelegate.swift */, + 63BCD154247D6E3B00E34EC4 /* SceneDelegate.swift */, + 63BCD15B247D6E3C00E34EC4 /* Assets.xcassets */, + 63BCD160247D6E3C00E34EC4 /* Info.plist */, + ); + path = Support; + sourceTree = ""; + }; + 63BCD167247D6EA600E34EC4 /* View */ = { + isa = PBXGroup; + children = ( + 63C910D8247F54B500490508 /* Custom Views */, + 63BCD168247D6EAD00E34EC4 /* Storyboards */, + ); + path = View; + sourceTree = ""; + }; + 63BCD168247D6EAD00E34EC4 /* Storyboards */ = { + isa = PBXGroup; + children = ( + 63BCD15D247D6E3C00E34EC4 /* LaunchScreen.storyboard */, + ); + path = Storyboards; + sourceTree = ""; + }; + 63BCD169247D6F2900E34EC4 /* Extensions */ = { + isa = PBXGroup; + children = ( + 63BCD16A247D6F3A00E34EC4 /* PreviewExtensions.swift */, + 63009C0E24807864008D9703 /* Notification.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 63BCD16C247D743A00E34EC4 /* Controller */ = { + isa = PBXGroup; + children = ( + 63BCD16D247D745400E34EC4 /* GameOfLifeViewController.swift */, + 63FF0E5F248062D20080CF20 /* AboutGameViewController.swift */, + ); + path = Controller; + sourceTree = ""; + }; + 63C910CB247F51D900490508 /* GameOfLifeTests */ = { + isa = PBXGroup; + children = ( + 63C910CC247F51D900490508 /* GameOfLifeTests.swift */, + 63C910CE247F51D900490508 /* Info.plist */, + ); + path = GameOfLifeTests; + sourceTree = ""; + }; + 63C910D8247F54B500490508 /* Custom Views */ = { + isa = PBXGroup; + children = ( + 63C910D6247F54A800490508 /* GameView.swift */, + ); + path = "Custom Views"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 63BCD14E247D6E3B00E34EC4 /* GameOfLife */ = { + isa = PBXNativeTarget; + buildConfigurationList = 63BCD163247D6E3C00E34EC4 /* Build configuration list for PBXNativeTarget "GameOfLife" */; + buildPhases = ( + 63BCD14B247D6E3B00E34EC4 /* Sources */, + 63BCD14C247D6E3B00E34EC4 /* Frameworks */, + 63BCD14D247D6E3B00E34EC4 /* Resources */, + 63C910C5247F508000490508 /* SwiftLint */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = GameOfLife; + productName = GameOfLife; + productReference = 63BCD14F247D6E3B00E34EC4 /* GameOfLife.app */; + productType = "com.apple.product-type.application"; + }; + 63C910C9247F51D900490508 /* GameOfLifeTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 63C910D1247F51D900490508 /* Build configuration list for PBXNativeTarget "GameOfLifeTests" */; + buildPhases = ( + 63C910C6247F51D900490508 /* Sources */, + 63C910C7247F51D900490508 /* Frameworks */, + 63C910C8247F51D900490508 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 63C910D0247F51D900490508 /* PBXTargetDependency */, + ); + name = GameOfLifeTests; + productName = GameOfLifeTests; + productReference = 63C910CA247F51D900490508 /* GameOfLifeTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 63BCD147247D6E3B00E34EC4 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1150; + LastUpgradeCheck = 1150; + ORGANIZATIONNAME = "Chad Rutherford"; + TargetAttributes = { + 63BCD14E247D6E3B00E34EC4 = { + CreatedOnToolsVersion = 11.5; + }; + 63C910C9247F51D900490508 = { + CreatedOnToolsVersion = 11.5; + TestTargetID = 63BCD14E247D6E3B00E34EC4; + }; + }; + }; + buildConfigurationList = 63BCD14A247D6E3B00E34EC4 /* Build configuration list for PBXProject "GameOfLife" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 63BCD146247D6E3B00E34EC4; + productRefGroup = 63BCD150247D6E3B00E34EC4 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 63BCD14E247D6E3B00E34EC4 /* GameOfLife */, + 63C910C9247F51D900490508 /* GameOfLifeTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 63BCD14D247D6E3B00E34EC4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 63BCD15F247D6E3C00E34EC4 /* LaunchScreen.storyboard in Resources */, + 63BCD15C247D6E3C00E34EC4 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 63C910C8247F51D900490508 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 63C910C5247F508000490508 /* SwiftLint */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = SwiftLint; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\n\tswiftlint\nelse\n\techo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 63BCD14B247D6E3B00E34EC4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 63C910D5247F534F00490508 /* World.swift in Sources */, + 63BCD153247D6E3B00E34EC4 /* AppDelegate.swift in Sources */, + 63009C0F24807864008D9703 /* Notification.swift in Sources */, + 63BCD155247D6E3B00E34EC4 /* SceneDelegate.swift in Sources */, + 635CEFAA247F4E4700167BB7 /* Cell.swift in Sources */, + 63FF0E60248062D20080CF20 /* AboutGameViewController.swift in Sources */, + 63BCD16B247D6F3A00E34EC4 /* PreviewExtensions.swift in Sources */, + 63BCD16E247D745400E34EC4 /* GameOfLifeViewController.swift in Sources */, + 63C910D7247F54A800490508 /* GameView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 63C910C6247F51D900490508 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 63C910CD247F51D900490508 /* GameOfLifeTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 63C910D0247F51D900490508 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 63BCD14E247D6E3B00E34EC4 /* GameOfLife */; + targetProxy = 63C910CF247F51D900490508 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 63BCD15D247D6E3C00E34EC4 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 63BCD15E247D6E3C00E34EC4 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 63BCD161247D6E3C00E34EC4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.5; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 63BCD162247D6E3C00E34EC4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.5; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 63BCD164247D6E3C00E34EC4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = P7DFWNNEL3; + INFOPLIST_FILE = GameOfLife/Support/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.github.chadarutherford.GameOfLife; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 63BCD165247D6E3C00E34EC4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = P7DFWNNEL3; + INFOPLIST_FILE = GameOfLife/Support/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.github.chadarutherford.GameOfLife; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 63C910D2247F51D900490508 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = P7DFWNNEL3; + INFOPLIST_FILE = GameOfLifeTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.github.chadarutherford.GameOfLifeTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GameOfLife.app/GameOfLife"; + }; + name = Debug; + }; + 63C910D3247F51D900490508 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = P7DFWNNEL3; + INFOPLIST_FILE = GameOfLifeTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.github.chadarutherford.GameOfLifeTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GameOfLife.app/GameOfLife"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 63BCD14A247D6E3B00E34EC4 /* Build configuration list for PBXProject "GameOfLife" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 63BCD161247D6E3C00E34EC4 /* Debug */, + 63BCD162247D6E3C00E34EC4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 63BCD163247D6E3C00E34EC4 /* Build configuration list for PBXNativeTarget "GameOfLife" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 63BCD164247D6E3C00E34EC4 /* Debug */, + 63BCD165247D6E3C00E34EC4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 63C910D1247F51D900490508 /* Build configuration list for PBXNativeTarget "GameOfLifeTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 63C910D2247F51D900490508 /* Debug */, + 63C910D3247F51D900490508 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 63BCD147247D6E3B00E34EC4 /* Project object */; +} diff --git a/GameOfLife/GameOfLife.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/GameOfLife/GameOfLife.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..672cfb935 --- /dev/null +++ b/GameOfLife/GameOfLife.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/GameOfLife/GameOfLife.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/GameOfLife/GameOfLife.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/GameOfLife/GameOfLife.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/GameOfLife/GameOfLife/Controller/AboutGameViewController.swift b/GameOfLife/GameOfLife/Controller/AboutGameViewController.swift new file mode 100644 index 000000000..450a720a3 --- /dev/null +++ b/GameOfLife/GameOfLife/Controller/AboutGameViewController.swift @@ -0,0 +1,144 @@ +// +// AboutGameViewController.swift +// GameOfLife +// +// Created by Chad Rutherford on 5/28/20. +// Copyright © 2020 Chad Rutherford. All rights reserved. +// + +import UIKit + +class AboutGameViewController: UIViewController { + + let rulesLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "Rules" + label.textColor = .label + label.font = UIFont.systemFont(ofSize: 40, weight: .bold) + return label + }() + + let rule1Label: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "• Any live cell with less than 2 neighbors will die" + label.numberOfLines = 0 + label.textColor = .label + label.font = UIFont.systemFont(ofSize: 20, weight: .semibold) + return label + }() + + let rule2Label: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "• Any live cell with 2 or 3 neighbors will live for the next generation" + label.numberOfLines = 0 + label.textColor = .label + label.font = UIFont.systemFont(ofSize: 20, weight: .semibold) + return label + }() + + let rule3Label: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "• Any live cell with more then 3 neighbors will die" + label.numberOfLines = 0 + label.textColor = .label + label.font = UIFont.systemFont(ofSize: 20, weight: .semibold) + return label + }() + + let rule4Label: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "• Any dead cell with exactly 3 neighbors will become a live cell" + label.numberOfLines = 0 + label.textColor = .label + label.font = UIFont.systemFont(ofSize: 20, weight: .semibold) + return label + }() + + let aboutAlgoLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "About this Algorithm" + label.textColor = .label + label.font = UIFont.systemFont(ofSize: 40, weight: .bold) + return label + }() + + let aboutAlgoTextView: UITextView = { + let textView = UITextView() + textView.translatesAutoresizingMaskIntoConstraints = false + textView.isUserInteractionEnabled = false + textView.isEditable = false + textView.text = + """ + I initially started out researching projects on this topic. I learned about the founder John Conway andnthat he was a British Mathematician. He came up with this simple concept, that given a set of simple rules, a cell can handle it's own state. The result of that process is an automated cell that when combined with other cells, creates some interesting geometric shapes. The concept of Turing-completeness is expecially interesting as it states that anything that's considered Turing-complete is able to recognize or decide other data-manipulation rule sets. + """ + textView.textColor = .label + textView.font = UIFont.systemFont(ofSize: 17, weight: .semibold) + return textView + }() + + let copyrightLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = "Copyright Chad Rutherford© May 2020" + label.numberOfLines = 0 + label.textColor = .label + label.font = UIFont.systemFont(ofSize: 20, weight: .semibold) + return label + }() + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + } + + private func setupUI() { + title = "About" + view.backgroundColor = .systemBackground + navigationController?.navigationBar.tintColor = .label + view.addSubview(rulesLabel) + view.addSubview(rule1Label) + view.addSubview(rule2Label) + view.addSubview(rule3Label) + view.addSubview(rule4Label) + view.addSubview(aboutAlgoLabel) + view.addSubview(aboutAlgoTextView) + view.addSubview(copyrightLabel) + NSLayoutConstraint.activate([ + rulesLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20), + rulesLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), + + rule1Label.leadingAnchor.constraint(equalTo: rulesLabel.leadingAnchor, constant: 16), + rule1Label.topAnchor.constraint(equalTo: rulesLabel.bottomAnchor, constant: 16), + rule1Label.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -8), + + rule2Label.leadingAnchor.constraint(equalTo: rulesLabel.leadingAnchor, constant: 16), + rule2Label.topAnchor.constraint(equalTo: rule1Label.bottomAnchor, constant: 16), + rule2Label.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -8), + + rule3Label.leadingAnchor.constraint(equalTo: rulesLabel.leadingAnchor, constant: 16), + rule3Label.topAnchor.constraint(equalTo: rule2Label.bottomAnchor, constant: 16), + rule3Label.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -8), + + rule4Label.leadingAnchor.constraint(equalTo: rulesLabel.leadingAnchor, constant: 16), + rule4Label.topAnchor.constraint(equalTo: rule3Label.bottomAnchor, constant: 16), + rule4Label.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -8), + + aboutAlgoLabel.leadingAnchor.constraint(equalTo: rulesLabel.leadingAnchor), + aboutAlgoLabel.topAnchor.constraint(equalTo: rule4Label.bottomAnchor, constant: 20), + + aboutAlgoTextView.topAnchor.constraint(equalTo: aboutAlgoLabel.bottomAnchor, constant: 20), + aboutAlgoTextView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16), + aboutAlgoTextView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16), + aboutAlgoTextView.bottomAnchor.constraint(equalTo: copyrightLabel.topAnchor, constant: -16), + + copyrightLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20), + copyrightLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) + ]) + } +} diff --git a/GameOfLife/GameOfLife/Controller/GameOfLifeViewController.swift b/GameOfLife/GameOfLife/Controller/GameOfLifeViewController.swift new file mode 100644 index 000000000..1161991a2 --- /dev/null +++ b/GameOfLife/GameOfLife/Controller/GameOfLifeViewController.swift @@ -0,0 +1,314 @@ +// +// GameOfLifeViewController.swift +// GameOfLife +// +// Created by Chad Rutherford on 5/26/20. +// Copyright © 2020 Chad Rutherford. All rights reserved. +// + +import SwiftUI +import UIKit + +class GameOfLifeViewController: UIViewController { + + lazy var gameView: GameView = { + let view = GameView(worldSize: 200, cellSize: 10) + view.translatesAutoresizingMaskIntoConstraints = false + view.isUserInteractionEnabled = true + view.backgroundColor = .label + view.layer.borderWidth = 8 + view.layer.borderColor = UIColor.systemGray.cgColor + view.clipsToBounds = false + return view + }() + + let stepButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.isUserInteractionEnabled = true + let attributes = [ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17, weight: .semibold), + NSAttributedString.Key.foregroundColor: UIColor.systemBackground + ] + button.setAttributedTitle(NSAttributedString(string: "Step", attributes: attributes), for: .normal) + button.addTarget(self, action: #selector(stepGame), for: .touchUpInside) + button.backgroundColor = .label + button.layer.cornerRadius = 8 + return button + }() + + let playButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.isUserInteractionEnabled = true + let attributes = [ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17, weight: .semibold), + NSAttributedString.Key.foregroundColor: UIColor.systemBackground + ] + button.setAttributedTitle(NSAttributedString(string: "Play", attributes: attributes), for: .normal) + button.addTarget(self, action: #selector(runGame), for: .touchUpInside) + button.backgroundColor = .label + button.layer.cornerRadius = 8 + return button + }() + + let resetButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.isUserInteractionEnabled = true + let attributes = [ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17, weight: .semibold), + NSAttributedString.Key.foregroundColor: UIColor.systemBackground + ] + button.setAttributedTitle(NSAttributedString(string: "Reset", attributes: attributes), for: .normal) + button.addTarget(self, action: #selector(resetGame), for: .touchUpInside) + button.backgroundColor = .label + button.layer.cornerRadius = 8 + return button + }() + + let stopButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.isUserInteractionEnabled = true + let attributes = [ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17, weight: .semibold), + NSAttributedString.Key.foregroundColor: UIColor.systemBackground + ] + button.setAttributedTitle(NSAttributedString(string: "Stop", attributes: attributes), for: .normal) + button.addTarget(self, action: #selector(stopGame), for: .touchUpInside) + button.backgroundColor = .label + button.layer.cornerRadius = 8 + return button + }() + + let blinkerButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.isUserInteractionEnabled = true + let attributes = [ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17, weight: .semibold), + NSAttributedString.Key.foregroundColor: UIColor.systemBackground + ] + button.setAttributedTitle(NSAttributedString(string: "Blinker", attributes: attributes), for: .normal) + button.addTarget(self, action: #selector(presentBlinker), for: .touchUpInside) + button.backgroundColor = .label + button.layer.cornerRadius = 8 + return button + }() + + let toadButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.isUserInteractionEnabled = true + let attributes = [ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17, weight: .semibold), + NSAttributedString.Key.foregroundColor: UIColor.systemBackground + ] + button.setAttributedTitle(NSAttributedString(string: "Toad", attributes: attributes), for: .normal) + button.addTarget(self, action: #selector(presentToad), for: .touchUpInside) + button.backgroundColor = .label + button.layer.cornerRadius = 8 + return button + }() + + let pulsarButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.isUserInteractionEnabled = true + let attributes = [ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17, weight: .semibold), + NSAttributedString.Key.foregroundColor: UIColor.systemBackground + ] + button.setAttributedTitle(NSAttributedString(string: "Pulsar", attributes: attributes), for: .normal) + button.addTarget(self, action: #selector(presentPulsar), for: .touchUpInside) + button.backgroundColor = .label + button.layer.cornerRadius = 8 + return button + }() + + let gliderButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.isUserInteractionEnabled = true + let attributes = [ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17, weight: .semibold), + NSAttributedString.Key.foregroundColor: UIColor.systemBackground + ] + button.setAttributedTitle(NSAttributedString(string: "Glider", attributes: attributes), for: .normal) + button.addTarget(self, action: #selector(presentGlider), for: .touchUpInside) + button.backgroundColor = .label + button.layer.cornerRadius = 8 + return button + }() + + let outerStackView: UIStackView = { + let stack = UIStackView() + stack.translatesAutoresizingMaskIntoConstraints = false + stack.axis = .vertical + stack.alignment = .fill + stack.distribution = .fillEqually + stack.spacing = 20 + return stack + }() + + let innerTopStackView: UIStackView = { + let stack = UIStackView() + stack.axis = .horizontal + stack.alignment = .fill + stack.distribution = .fillEqually + stack.spacing = 40 + return stack + }() + + let innerBottomStackView: UIStackView = { + let stack = UIStackView() + stack.axis = .horizontal + stack.alignment = .fill + stack.distribution = .fillEqually + stack.spacing = 40 + return stack + }() + + let generationLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.systemFont(ofSize: 17, weight: .semibold) + label.text = "Generation: 0" + label.textColor = .label + return label + }() + + var timer: CADisplayLink! + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + setupNavController() + NotificationCenter.default.addObserver(self, selector: #selector(updateGeneration), name: .generationChanged, object: nil) + } + + private func setupUI() { + view.backgroundColor = .systemBackground + view.addSubview(generationLabel) + view.addSubview(gameView) + view.addSubview(stepButton) + view.addSubview(playButton) + view.addSubview(resetButton) + view.addSubview(stopButton) + view.addSubview(outerStackView) + outerStackView.addArrangedSubview(innerTopStackView) + outerStackView.addArrangedSubview(innerBottomStackView) + innerTopStackView.addArrangedSubview(blinkerButton) + innerTopStackView.addArrangedSubview(toadButton) + innerBottomStackView.addArrangedSubview(pulsarButton) + innerBottomStackView.addArrangedSubview(gliderButton) + NSLayoutConstraint.activate([ + generationLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), + generationLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + + gameView.topAnchor.constraint(equalTo: generationLabel.bottomAnchor, constant: 16), + gameView.widthAnchor.constraint(equalToConstant: 300), + gameView.heightAnchor.constraint(equalTo: gameView.widthAnchor, multiplier: 1), + gameView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + + stepButton.topAnchor.constraint(equalTo: gameView.bottomAnchor, constant: 20), + stepButton.heightAnchor.constraint(equalToConstant: 40), + stepButton.leadingAnchor.constraint(equalTo: gameView.leadingAnchor), + stepButton.widthAnchor.constraint(equalTo: gameView.widthAnchor, multiplier: 0.3), + + resetButton.topAnchor.constraint(equalTo: gameView.bottomAnchor, constant: 20), + resetButton.heightAnchor.constraint(equalToConstant: 40), + resetButton.leadingAnchor.constraint(equalTo: stepButton.trailingAnchor, constant: 16), + resetButton.widthAnchor.constraint(equalTo: gameView.widthAnchor, multiplier: 0.3), + + playButton.topAnchor.constraint(equalTo: gameView.bottomAnchor, constant: 20), + playButton.heightAnchor.constraint(equalToConstant: 40), + playButton.trailingAnchor.constraint(equalTo: gameView.trailingAnchor), + playButton.widthAnchor.constraint(equalTo: gameView.widthAnchor, multiplier: 0.3), + + stopButton.topAnchor.constraint(equalTo: resetButton.bottomAnchor, constant: 20), + stopButton.leadingAnchor.constraint(equalTo: resetButton.leadingAnchor), + stopButton.heightAnchor.constraint(equalToConstant: 40), + stopButton.trailingAnchor.constraint(equalTo: resetButton.trailingAnchor), + + outerStackView.topAnchor.constraint(equalTo: stopButton.bottomAnchor, constant: 20), + outerStackView.leadingAnchor.constraint(equalTo: gameView.leadingAnchor), + outerStackView.trailingAnchor.constraint(equalTo: gameView.trailingAnchor) + ]) + } + + private func setupNavController() { + title = "Game Of Life" + navigationController?.navigationBar.prefersLargeTitles = true + navigationItem.rightBarButtonItem = UIBarButtonItem( + image: UIImage(systemName: "info.circle"), + style: .plain, + target: self, + action: #selector(presentAboutScreen) + ) + navigationItem.rightBarButtonItem?.tintColor = .label + } + + @objc private func stepGame() { + if gameView.world.cells == [] { + gameView.world.createRandom() + } + gameView.world.updateCells() + gameView.setNeedsDisplay() + } + + @objc private func runGame() { + gameView.state = .running + gameView.autoRun() + } + + @objc private func stopGame() { + gameView.state = .stopped + gameView.setNeedsDisplay() + } + + @objc private func resetGame() { + gameView.world.createRandom() + gameView.state = .stopped + gameView.setNeedsDisplay() + } + + @objc private func presentBlinker() { + gameView.world.createBlinker() + gameView.setNeedsDisplay() + } + + @objc private func presentToad() { + gameView.world.createToad() + gameView.setNeedsDisplay() + } + + @objc private func presentPulsar() { + gameView.world.createPulsar() + gameView.setNeedsDisplay() + } + + @objc private func presentGlider() { + gameView.world.createGlider() + gameView.setNeedsDisplay() + } + + @objc private func presentAboutScreen() { + let aboutGameVC = AboutGameViewController(nibName: nil, bundle: nil) + navigationController?.pushViewController(aboutGameVC, animated: true) + } + + @objc private func updateGeneration() { + generationLabel.text = "Generation: \(gameView.world.generation)" + } +} + +@available(iOS 13, *) +struct AboutGamePreview: PreviewProvider { + static var previews: some View { + GameOfLifeViewController().asPreview() + .edgesIgnoringSafeArea(.all) + .colorScheme(.dark) + } +} diff --git a/GameOfLife/GameOfLife/Extensions/Notification.swift b/GameOfLife/GameOfLife/Extensions/Notification.swift new file mode 100644 index 000000000..91450a9db --- /dev/null +++ b/GameOfLife/GameOfLife/Extensions/Notification.swift @@ -0,0 +1,13 @@ +// +// Notification.swift +// GameOfLife +// +// Created by Chad Rutherford on 5/28/20. +// Copyright © 2020 Chad Rutherford. All rights reserved. +// + +import Foundation + +extension Notification.Name { + static let generationChanged = Notification.Name("generationChangedNotification") +} diff --git a/GameOfLife/GameOfLife/Extensions/PreviewExtensions.swift b/GameOfLife/GameOfLife/Extensions/PreviewExtensions.swift new file mode 100644 index 000000000..2f3080ed3 --- /dev/null +++ b/GameOfLife/GameOfLife/Extensions/PreviewExtensions.swift @@ -0,0 +1,115 @@ +// +// PreviewExtensions.swift +// GameOfLife +// +// Created by Chad Rutherford on 5/26/20. +// Copyright © 2020 Chad Rutherford. All rights reserved. +// + +import SwiftUI +import UIKit + +extension Binding { + static func mock(_ value: Value) -> Self { + var value = value + return Binding(get: { value }, set: { value = $0 }) + } +} + +extension ColorScheme { + var previewName: String { + String(describing: self).capitalized + } +} + +extension ContentSizeCategory { + static let smallestAndLargest = [allCases.first!, allCases.last!] + + var previewName: String { + self == Self.smallestAndLargest.first ? "Small" : "Large" + } +} + +extension ForEach where Data.Element: Hashable, ID == Data.Element, Content: View { + init(values: Data, content: @escaping (Data.Element) -> Content) { + self.init(values, id: \.self, content: content) + } +} + +struct ComponentPreview: View { + var component: Component + + var body: some View { + ForEach(values: ColorScheme.allCases) { scheme in + ForEach(values: ContentSizeCategory.smallestAndLargest) { category in + self.component + .previewLayout(.sizeThatFits) + .background(Color(UIColor.systemBackground)) + .colorScheme(scheme) + .environment(\.sizeCategory, category) + .previewDisplayName("\(scheme.previewName) + \(category.previewName)") + } + } + } +} + +extension View { + func previewAsContent() -> some View { + ComponentPreview(component: self) + } + + func previewAsScreen() -> some View { + ScreenPreview(screen: self) + } +} + +struct ScreenPreview: View { + var screen: Screen + + var body: some View { + ForEach(values: deviceNames) { device in + ForEach(values: ColorScheme.allCases) { scheme in + NavigationView { + self.screen + .navigationBarTitle("") + .navigationBarHidden(true) + } + .previewDevice(PreviewDevice(rawValue: device)) + .colorScheme(scheme) + .previewDisplayName("\(scheme.previewName): \(device)") + .navigationViewStyle(StackNavigationViewStyle()) + } + } + } + + private var deviceNames: [String] { + [ + "iPhone 8", + "iPhone 11", + "iPhone 11 Pro", + "iPad (7th generation)", + "iPad Pro (12.9-inch) (4th generation)" + ] + } +} + + +extension UIViewController { + @available(iOS 13, *) + private struct Preview: UIViewControllerRepresentable { + var viewController: UIViewController + + func makeUIViewController(context: Context) -> UIViewController { + viewController + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { + // No-op + } + } + + @available(iOS 13, *) + func asPreview() -> some View { + Preview(viewController: self) + } +} diff --git a/GameOfLife/GameOfLife/Model/Cell.swift b/GameOfLife/GameOfLife/Model/Cell.swift new file mode 100644 index 000000000..d13e86488 --- /dev/null +++ b/GameOfLife/GameOfLife/Model/Cell.swift @@ -0,0 +1,51 @@ +// +// Cell.swift +// GameOfLife +// +// Created by Chad Rutherford on 5/27/20. +// Copyright © 2020 Chad Rutherford. All rights reserved. +// + +import Foundation + +enum State { + case alive, dead +} + +struct Cell: Hashable { + let x: Int + let y: Int + var state: State + + init(x: Int, y: Int, state: State) { + self.x = x + self.y = y + self.state = state + } + + func hash(into hasher: inout Hasher) { + let hash = x + y * 1_0000 + hasher.combine(hash) + } + + func isNeighbor(to cell: Cell) -> Bool { + let xDiff = abs(self.x - cell.x) + let yDiff = abs(self.y - cell.y) + + switch (xDiff, yDiff) { + case (1, 1), (0, 1), (1, 0): + return true + default: + return false + } + } + + mutating func toggle() { + switch state { + case .alive: + self.state = .dead + case .dead: + self.state = .alive + } + } +} diff --git a/GameOfLife/GameOfLife/Model/World.swift b/GameOfLife/GameOfLife/Model/World.swift new file mode 100644 index 000000000..17ea5bc27 --- /dev/null +++ b/GameOfLife/GameOfLife/Model/World.swift @@ -0,0 +1,292 @@ +// +// World.swift +// GameOfLife +// +// Created by Chad Rutherford on 5/27/20. +// Copyright © 2020 Chad Rutherford. All rights reserved. +// + +import Foundation + +class World { + + // --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- + // MARK: - Properties + var cells = [Cell]() + let size: Int + var generation = 0 { + didSet { + NotificationCenter.default.post(name: .generationChanged, object: self, userInfo: nil) + } + } + + // --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- + // MARK: - Computed Properties + + + lazy var cellNeighbors = { (cell: Cell) -> [Cell] in + let neighboringCells = { (left: Cell, right: Cell) -> Bool in + let diff = (abs(left.x - right.x), abs(left.y - right.y)) + switch diff { + case (1, 1), (1, 0), (0, 1): + return true + default: + return false + } + } + return self.cells.filter { neighboringCells(cell, $0) } + } + + init(size: Int) { + self.size = size + createRandom() + } + + func updateCells() { + var updatedCells = [Cell]() + let livingCells = cells.filter { $0.state == .alive } + + for cell in cells { + let aliveNeighbors = livingCells.filter { $0.isNeighbor(to: cell) } + switch aliveNeighbors.count { + case 2...3 where cell.state == .alive: + updatedCells.append(cell) + case 3 where cell.state == .dead: + let aliveCell = Cell(x: cell.x, y: cell.y, state: .alive) + updatedCells.append(aliveCell) + default: + let dyingCell = Cell(x: cell.x, y: cell.y, state: .dead) + updatedCells.append(dyingCell) + } + } + + cells = updatedCells + generation += 1 + } + + func createRandom() { + generation = 0 + cells = [] + for x in 0 ..< (size / 6) { + for y in 0 ..< (size / 6) { + let randomState = Int.random(in: 0...2) + let cell = Cell(x: x, y: y, state: randomState == 0 ? .alive : .dead) + cells.append(cell) + } + } + } + + func createBlinker() { + generation = 0 + cells = [] + let center = ((size / 6) / 2) - 2 + for x in 0 ..< (size / 6) { + for y in 0 ..< (size / 6) { + switch (x, y) { + case (center, center): + let centerCell = Cell(x: center, y: center, state: .alive) + cells.append(centerCell) + case (center - 1, center): + let leftCell = Cell(x: center - 1, y: center, state: .alive) + cells.append(leftCell) + case (center + 1, center): + let rightCell = Cell(x: center + 1, y: center, state: .alive) + cells.append(rightCell) + default: + let cell = Cell(x: x, y: y, state: .dead) + cells.append(cell) + } + } + } + } + + func createToad() { + generation = 0 + cells = [] + let center = ((size / 6) / 2) - 2 + for x in 0 ..< (size / 6) { + for y in 0 ..< (size / 6) { + switch (x, y) { + case (center, center): + let centerCell = Cell(x: center, y: center, state: .alive) + cells.append(centerCell) + case (center - 1, center): + let leftCell = Cell(x: center - 1, y: center, state: .alive) + cells.append(leftCell) + case (center + 1, center): + let rightCell = Cell(x: center + 1, y: center, state: .alive) + cells.append(rightCell) + case (center, center - 1): + let topLeftCell = Cell(x: center, y: center - 1, state: .alive) + cells.append(topLeftCell) + case (center + 1, center - 1): + let topMiddleCell = Cell(x: center + 1, y: center - 1, state: .alive) + cells.append(topMiddleCell) + case (center + 2, center - 1): + let topRightCell = Cell(x: center + 2, y: center - 1, state: .alive) + cells.append(topRightCell) + default: + let cell = Cell(x: x, y: y, state: .dead) + cells.append(cell) + } + } + } + } + + func createPulsar() { + generation = 0 + cells = [] + let center = ((size / 6) / 2) - 2 + for x in 0 ..< (size / 6) { + for y in 0 ..< (size / 6) { + switch (x, y) { + case (center - 1, center - 2), (center - 1, center - 3), (center - 1, center - 4): + let cell1 = Cell(x: center - 1, y: center - 2, state: .alive) + let cell2 = Cell(x: center - 1, y: center - 3, state: .alive) + let cell3 = Cell(x: center - 1, y: center - 4, state: .alive) + cells.append(cell1) + cells.append(cell2) + cells.append(cell3) + case (center - 2, center - 6), (center - 3, center - 6), (center - 4, center - 6): + let cell1 = Cell(x: center - 2, y: center - 6, state: .alive) + let cell2 = Cell(x: center - 3, y: center - 6, state: .alive) + let cell3 = Cell(x: center - 4, y: center - 6, state: .alive) + cells.append(cell1) + cells.append(cell2) + cells.append(cell3) + case (center - 2, center - 1), (center - 3, center - 1), (center - 4, center - 1): + let cell1 = Cell(x: center - 2, y: center - 1, state: .alive) + let cell2 = Cell(x: center - 3, y: center - 1, state: .alive) + let cell3 = Cell(x: center - 4, y: center - 1, state: .alive) + cells.append(cell1) + cells.append(cell2) + cells.append(cell3) + case (center - 6, center - 2), (center - 6, center - 3), (center - 6, center - 4): + let cell1 = Cell(x: center - 6, y: center - 2, state: .alive) + let cell2 = Cell(x: center - 6, y: center - 3, state: .alive) + let cell3 = Cell(x: center - 6, y: center - 4, state: .alive) + cells.append(cell1) + cells.append(cell2) + cells.append(cell3) + case (center + 1, center - 2), (center + 1, center - 3), (center + 1, center - 4): + let cell1 = Cell(x: center + 1, y: center - 2, state: .alive) + let cell2 = Cell(x: center + 1, y: center - 3, state: .alive) + let cell3 = Cell(x: center + 1, y: center - 4, state: .alive) + cells.append(cell1) + cells.append(cell2) + cells.append(cell3) + case (center + 2, center - 1), (center + 3, center - 1), (center + 4, center - 1): + let cell1 = Cell(x: center + 2, y: center - 1, state: .alive) + let cell2 = Cell(x: center + 3, y: center - 1, state: .alive) + let cell3 = Cell(x: center + 4, y: center - 1, state: .alive) + cells.append(cell1) + cells.append(cell2) + cells.append(cell3) + case (center + 6, center - 2), (center + 6, center - 3), (center + 6, center - 4): + let cell1 = Cell(x: center + 6, y: center - 2, state: .alive) + let cell2 = Cell(x: center + 6, y: center - 3, state: .alive) + let cell3 = Cell(x: center + 6, y: center - 4, state: .alive) + cells.append(cell1) + cells.append(cell2) + cells.append(cell3) + case (center + 2, center - 6), (center + 3, center - 6), (center + 4, center - 6): + let cell1 = Cell(x: center + 2, y: center - 6, state: .alive) + let cell2 = Cell(x: center + 3, y: center - 6, state: .alive) + let cell3 = Cell(x: center + 4, y: center - 6, state: .alive) + cells.append(cell1) + cells.append(cell2) + cells.append(cell3) + case (center - 2, center + 1), (center - 3, center + 1), (center - 4, center + 1): + let cell1 = Cell(x: center - 2, y: center + 1, state: .alive) + let cell2 = Cell(x: center - 3, y: center + 1, state: .alive) + let cell3 = Cell(x: center - 4, y: center + 1, state: .alive) + cells.append(cell1) + cells.append(cell2) + cells.append(cell3) + case (center - 1, center + 2), (center - 1, center + 3), (center - 1, center + 4): + let cell1 = Cell(x: center - 1, y: center + 2, state: .alive) + let cell2 = Cell(x: center - 1, y: center + 3, state: .alive) + let cell3 = Cell(x: center - 1, y: center + 4, state: .alive) + cells.append(cell1) + cells.append(cell2) + cells.append(cell3) + case (center + 2, center + 1), (center + 3, center + 1), (center + 4, center + 1): + let cell1 = Cell(x: center + 2, y: center + 1, state: .alive) + let cell2 = Cell(x: center + 3, y: center + 1, state: .alive) + let cell3 = Cell(x: center + 4, y: center + 1, state: .alive) + cells.append(cell1) + cells.append(cell2) + cells.append(cell3) + case (center + 1, center + 2), (center + 1, center + 3), (center + 1, center + 4): + let cell1 = Cell(x: center + 1, y: center + 2, state: .alive) + let cell2 = Cell(x: center + 1, y: center + 3, state: .alive) + let cell3 = Cell(x: center + 1, y: center + 4, state: .alive) + cells.append(cell1) + cells.append(cell2) + cells.append(cell3) + case (center + 2, center + 6), (center + 3, center + 6), (center + 4, center + 6): + let cell1 = Cell(x: center + 2, y: center + 6, state: .alive) + let cell2 = Cell(x: center + 3, y: center + 6, state: .alive) + let cell3 = Cell(x: center + 4, y: center + 6, state: .alive) + cells.append(cell1) + cells.append(cell2) + cells.append(cell3) + case (center + 6, center + 2), (center + 6, center + 3), (center + 6, center + 4): + let cell1 = Cell(x: center + 6, y: center + 2, state: .alive) + let cell2 = Cell(x: center + 6, y: center + 3, state: .alive) + let cell3 = Cell(x: center + 6, y: center + 4, state: .alive) + cells.append(cell1) + cells.append(cell2) + cells.append(cell3) + case (center - 2, center + 6), (center - 3, center + 6), (center - 4, center + 6): + let cell1 = Cell(x: center - 2, y: center + 6, state: .alive) + let cell2 = Cell(x: center - 3, y: center + 6, state: .alive) + let cell3 = Cell(x: center - 4, y: center + 6, state: .alive) + cells.append(cell1) + cells.append(cell2) + cells.append(cell3) + case (center - 6, center + 2), (center - 6, center + 3), (center - 6, center + 4): + let cell1 = Cell(x: center - 6, y: center + 2, state: .alive) + let cell2 = Cell(x: center - 6, y: center + 3, state: .alive) + let cell3 = Cell(x: center - 6, y: center + 4, state: .alive) + cells.append(cell1) + cells.append(cell2) + cells.append(cell3) + default: + let cell = Cell(x: x, y: y, state: .dead) + cells.append(cell) + } + } + } + } + + func createGlider() { + generation = 0 + cells = [] + let center = ((size / 6) / 2) - 2 + for x in 0 ..< (size / 6) { + for y in 0 ..< (size / 6) { + switch (x, y) { + case (center, center): + let centerCell = Cell(x: center, y: center, state: .alive) + cells.append(centerCell) + case (center - 1, center): + let leftCell = Cell(x: center - 1, y: center, state: .alive) + cells.append(leftCell) + case (center - 2, center - 1): + let spacedCell = Cell(x: center - 2, y: center - 1, state: .alive) + cells.append(spacedCell) + case (center, center - 1): + let midCell = Cell(x: center, y: center - 1, state: .alive) + cells.append(midCell) + case (center, center - 2): + let topCell = Cell(x: center, y: center - 2, state: .alive) + cells.append(topCell) + default: + let cell = Cell(x: x, y: y, state: .dead) + cells.append(cell) + } + } + } + } +} diff --git a/GameOfLife/GameOfLife/Support/AppDelegate.swift b/GameOfLife/GameOfLife/Support/AppDelegate.swift new file mode 100644 index 000000000..0f8bc5dc2 --- /dev/null +++ b/GameOfLife/GameOfLife/Support/AppDelegate.swift @@ -0,0 +1,34 @@ +// +// AppDelegate.swift +// GameOfLife +// +// Created by Chad Rutherford on 5/26/20. +// Copyright © 2020 Chad Rutherford. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } +} diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/Contents.json b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..b8b8c3af3 --- /dev/null +++ b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,158 @@ +{ + "images" : [ + { + "filename" : "icon-40.png", + "scale" : "1x", + "size" : "40x40", + "idiom" : "ipad" + }, + { + "idiom" : "ipad", + "filename" : "icon-40@2x.png", + "scale" : "2x", + "size" : "40x40" + }, + { + "size" : "60x60", + "scale" : "2x", + "idiom" : "iphone", + "filename" : "icon-60@2x.png" + }, + { + "scale" : "1x", + "idiom" : "ipad", + "filename" : "icon-72.png", + "size" : "72x72" + }, + { + "size" : "72x72", + "filename" : "icon-72@2x.png", + "scale" : "2x", + "idiom" : "ipad" + }, + { + "scale" : "1x", + "filename" : "icon-76.png", + "size" : "76x76", + "idiom" : "ipad" + }, + { + "idiom" : "ipad", + "filename" : "icon-76@2x.png", + "scale" : "2x", + "size" : "76x76" + }, + { + "scale" : "1x", + "idiom" : "ipad", + "size" : "50x50", + "filename" : "icon-small-50.png" + }, + { + "size" : "50x50", + "idiom" : "ipad", + "scale" : "2x", + "filename" : "icon-small-50@2x.png" + }, + { + "idiom" : "iphone", + "filename" : "icon-small.png", + "scale" : "1x", + "size" : "29x29" + }, + { + "scale" : "2x", + "filename" : "icon-small@2x.png", + "idiom" : "iphone", + "size" : "29x29" + }, + { + "filename" : "icon.png", + "size" : "57x57", + "idiom" : "iphone", + "scale" : "1x" + }, + { + "size" : "57x57", + "idiom" : "iphone", + "filename" : "icon@2x.png", + "scale" : "2x" + }, + { + "filename" : "icon-small@3x.png", + "size" : "29x29", + "scale" : "3x", + "idiom" : "iphone" + }, + { + "size" : "40x40", + "scale" : "3x", + "idiom" : "iphone", + "filename" : "icon-40@3x.png" + }, + { + "idiom" : "iphone", + "filename" : "icon-60@3x.png", + "size" : "60x60", + "scale" : "3x" + }, + { + "filename" : "icon-40@2x.png", + "size" : "40x40", + "scale" : "2x", + "idiom" : "iphone" + }, + { + "filename" : "icon-small.png", + "size" : "29x29", + "idiom" : "ipad", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x", + "filename" : "icon-small@2x.png" + }, + { + "filename" : "icon-83.5@2x.png", + "size" : "83.5x83.5", + "scale" : "2x", + "idiom" : "ipad" + }, + { + "filename" : "notification-icon@2x.png", + "size" : "20x20", + "scale" : "2x", + "idiom" : "iphone" + }, + { + "filename" : "notification-icon@3x.png", + "size" : "20x20", + "idiom" : "iphone", + "scale" : "3x" + }, + { + "filename" : "notification-icon~ipad.png", + "size" : "20x20", + "scale" : "1x", + "idiom" : "ipad" + }, + { + "size" : "20x20", + "filename" : "notification-icon~ipad@2x.png", + "idiom" : "ipad", + "scale" : "2x" + }, + { + "filename" : "ios-marketing.png", + "size" : "1024x1024", + "idiom" : "ios-marketing", + "scale" : "1x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-40.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-40.png new file mode 100644 index 000000000..3a598a68a Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-40.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png new file mode 100644 index 000000000..ff8385e01 Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png new file mode 100644 index 000000000..885129506 Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png new file mode 100644 index 000000000..885129506 Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png new file mode 100644 index 000000000..b718d7821 Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-72.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-72.png new file mode 100644 index 000000000..bc1eb17ae Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-72.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png new file mode 100644 index 000000000..0b6793853 Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-76.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-76.png new file mode 100644 index 000000000..c9f944caa Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-76.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png new file mode 100644 index 000000000..ffad97e5d Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png new file mode 100644 index 000000000..36c3f9e46 Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-small-50.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-small-50.png new file mode 100644 index 000000000..b0df431de Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-small-50.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-small-50@2x.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-small-50@2x.png new file mode 100644 index 000000000..448451a99 Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-small-50@2x.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-small.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-small.png new file mode 100644 index 000000000..ea1a0e4e5 Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-small.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-small@2x.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-small@2x.png new file mode 100644 index 000000000..f2143164c Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-small@2x.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-small@3x.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-small@3x.png new file mode 100644 index 000000000..9829e1f5e Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon-small@3x.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon.png new file mode 100644 index 000000000..9139878b6 Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon@2x.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon@2x.png new file mode 100644 index 000000000..45b504ede Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/icon@2x.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/ios-marketing.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/ios-marketing.png new file mode 100644 index 000000000..3d7da4c9c Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/ios-marketing.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/notification-icon@2x.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/notification-icon@2x.png new file mode 100644 index 000000000..3a598a68a Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/notification-icon@2x.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/notification-icon@3x.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/notification-icon@3x.png new file mode 100644 index 000000000..83cc3dbe2 Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/notification-icon@3x.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/notification-icon~ipad.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/notification-icon~ipad.png new file mode 100644 index 000000000..7cf98953c Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/notification-icon~ipad.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/notification-icon~ipad@2x.png b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/notification-icon~ipad@2x.png new file mode 100644 index 000000000..3a598a68a Binary files /dev/null and b/GameOfLife/GameOfLife/Support/Assets.xcassets/AppIcon.appiconset/notification-icon~ipad@2x.png differ diff --git a/GameOfLife/GameOfLife/Support/Assets.xcassets/Contents.json b/GameOfLife/GameOfLife/Support/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/GameOfLife/GameOfLife/Support/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GameOfLife/GameOfLife/Support/Info.plist b/GameOfLife/GameOfLife/Support/Info.plist new file mode 100644 index 000000000..9742bf0f4 --- /dev/null +++ b/GameOfLife/GameOfLife/Support/Info.plist @@ -0,0 +1,60 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/GameOfLife/GameOfLife/Support/SceneDelegate.swift b/GameOfLife/GameOfLife/Support/SceneDelegate.swift new file mode 100644 index 000000000..8dd0489b9 --- /dev/null +++ b/GameOfLife/GameOfLife/Support/SceneDelegate.swift @@ -0,0 +1,55 @@ +// +// SceneDelegate.swift +// GameOfLife +// +// Created by Chad Rutherford on 5/26/20. +// Copyright © 2020 Chad Rutherford. All rights reserved. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let windowScene = scene as? UIWindowScene else { return } + window = UIWindow(frame: windowScene.coordinateSpace.bounds) + window?.windowScene = windowScene + let golVC = GameOfLifeViewController(nibName: nil, bundle: nil) + let navController = UINavigationController(rootViewController: golVC) + window?.rootViewController = navController + window?.makeKeyAndVisible() + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } +} diff --git a/GameOfLife/GameOfLife/View/Custom Views/GameView.swift b/GameOfLife/GameOfLife/View/Custom Views/GameView.swift new file mode 100644 index 000000000..b41fba288 --- /dev/null +++ b/GameOfLife/GameOfLife/View/Custom Views/GameView.swift @@ -0,0 +1,67 @@ +// +// GameView.swift +// GameOfLife +// +// Created by Chad Rutherford on 5/27/20. +// Copyright © 2020 Chad Rutherford. All rights reserved. +// + +import UIKit + +enum GameViewState { + case running, stopped +} + +class GameView: UIView { + var world: World = World(size: 0) + var cellSize = 40 + + var state: GameViewState = .running + + convenience init(worldSize: Int, cellSize: Int) { + let frame = CGRect(x: 0, y: 0, width: worldSize * worldSize, height: worldSize * worldSize) + self.init(frame: frame) + self.world = World(size: worldSize) + self.cellSize = cellSize + contentMode = .redraw + } + + convenience init() { + let frame = CGRect(x: 0, y: 0, width: 1000, height: 1000) + self.init(frame: frame) + contentMode = .redraw + } + + override init(frame: CGRect) { + super.init(frame: frame) + contentMode = .redraw + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ rect: CGRect) { + let context = UIGraphicsGetCurrentContext() + context?.saveGState() + for cell in world.cells { + let rect = CGRect(x: cell.x * cellSize, y: cell.y * cellSize, width: cellSize, height: cellSize) + let color = cell.state == .alive ? UIColor.systemBackground.cgColor : UIColor.label.cgColor + context?.addRect(rect) + context?.setFillColor(color) + context?.fill(rect) + } + context?.restoreGState() + } + + func autoRun() { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + if self.state == .running { + self.world.updateCells() + self.setNeedsDisplay() + self.autoRun() + } + } + } +} diff --git a/GameOfLife/GameOfLife/View/Storyboards/Base.lproj/LaunchScreen.storyboard b/GameOfLife/GameOfLife/View/Storyboards/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000..865e9329f --- /dev/null +++ b/GameOfLife/GameOfLife/View/Storyboards/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GameOfLife/GameOfLifeTests/GameOfLifeTests.swift b/GameOfLife/GameOfLifeTests/GameOfLifeTests.swift new file mode 100644 index 000000000..2eca21665 --- /dev/null +++ b/GameOfLife/GameOfLifeTests/GameOfLifeTests.swift @@ -0,0 +1,42 @@ +// +// GameOfLifeTests.swift +// GameOfLifeTests +// +// Created by Chad Rutherford on 5/27/20. +// Copyright © 2020 Chad Rutherford. All rights reserved. +// + +import XCTest +@testable import GameOfLife + +class GameOfLifeTests: XCTestCase { + + func testNeighbors() { + var firstCell = Cell(x: 10, y: 10, state: .dead) + var secondCell = Cell(x: 11, y: 11, state: .dead) + XCTAssertTrue(firstCell.isNeighbor(to: secondCell)) + // True - Adjacent row, adjacent column. + + firstCell = Cell(x: 10, y: 10, state: .dead) + secondCell = Cell(x: 11, y: 10, state: .dead) + XCTAssertTrue(firstCell.isNeighbor(to: secondCell)) + // True - Same row, different (neighboring) column. + + firstCell = Cell(x: 10, y: 10, state: .dead) + secondCell = Cell(x: 20, y: 10, state: .dead) + XCTAssertFalse(firstCell.isNeighbor(to: secondCell)) + // False - Same row, very different (non-neighboring) column. + + firstCell = Cell(x: 10, y: 10, state: .dead) + secondCell = Cell(x: 10, y: 10, state: .dead) + XCTAssertFalse(firstCell.isNeighbor(to: secondCell)) + // false - Same row, same column. + } + + func testWorldInitializesCorrectAmountOfCells() { + let world = World(size: 4) + // world creates size^2 cells + // for a world of size 4 it should be 4^2 = 16 + XCTAssertEqual(16, world.cells.count) + } +} diff --git a/GameOfLife/GameOfLifeTests/Info.plist b/GameOfLife/GameOfLifeTests/Info.plist new file mode 100644 index 000000000..64d65ca49 --- /dev/null +++ b/GameOfLife/GameOfLifeTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/GameOfLife/autocorrect.yml b/GameOfLife/autocorrect.yml new file mode 100644 index 000000000..4aa474399 --- /dev/null +++ b/GameOfLife/autocorrect.yml @@ -0,0 +1,12 @@ +# This file will be run to auto-correct certain swiftlint rules before swiftlint is run with the standard .swiftlint.yml configuration file. +# +# An enhancement to do this has been requested here: https://github.com/realm/SwiftLint/issues/1451 +# Once done, that enhancement might obsolete this approach. + +# Only add rules here you want to auto-correct +whitelist_rules: +- trailing_whitespace +- trailing_semicolon +- trailing_newline +excluded: # paths to ignore during linting. + - Carthage