├── .github └── FUNDING.yml ├── .gitignore ├── .swift-version ├── .swiftlint.yml ├── CHANGES.md ├── Design ├── screensaver │ ├── amiantos-screenshots.pxm │ ├── lifesaver-screenshots-1.1.png │ ├── lifesaver-screenshots.png │ └── screenshots.pxm └── tvos │ ├── tvos-ss-1.png │ └── tvos-ss-2.png ├── Installer Resources ├── Life Saver.pkgproj ├── introduction.rtf ├── post-install.sh └── signing.sh ├── LICENSE ├── Life Saver.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ ├── Life Saver Screensaver.xcscheme │ │ ├── Life Saver macOS Debug.xcscheme │ │ ├── Life Saver tvOS Debug.xcscheme │ │ └── Life Saver tvOS.xcscheme └── xcuserdata │ └── bradroot.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── Life Saver.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Podfile ├── Podfile.lock ├── Pods ├── Manifest.lock ├── Pods.xcodeproj │ └── project.pbxproj ├── SwiftLint │ ├── LICENSE │ └── swiftlint └── Target Support Files │ ├── Pods-Life Saver Screensaver │ ├── Pods-Life Saver Screensaver-Info.plist │ ├── Pods-Life Saver Screensaver-acknowledgements.markdown │ ├── Pods-Life Saver Screensaver-acknowledgements.plist │ ├── Pods-Life Saver Screensaver-dummy.m │ ├── Pods-Life Saver Screensaver-umbrella.h │ ├── Pods-Life Saver Screensaver.debug.xcconfig │ ├── Pods-Life Saver Screensaver.modulemap │ └── Pods-Life Saver Screensaver.release.xcconfig │ ├── Pods-Life Saver macOS │ ├── Pods-Life Saver macOS-Info.plist │ ├── Pods-Life Saver macOS-acknowledgements.markdown │ ├── Pods-Life Saver macOS-acknowledgements.plist │ ├── Pods-Life Saver macOS-dummy.m │ ├── Pods-Life Saver macOS-umbrella.h │ ├── Pods-Life Saver macOS.debug.xcconfig │ ├── Pods-Life Saver macOS.modulemap │ └── Pods-Life Saver macOS.release.xcconfig │ ├── Pods-Life Saver tvOS │ ├── Pods-Life Saver tvOS-Info.plist │ ├── Pods-Life Saver tvOS-acknowledgements.markdown │ ├── Pods-Life Saver tvOS-acknowledgements.plist │ ├── Pods-Life Saver tvOS-dummy.m │ ├── Pods-Life Saver tvOS-umbrella.h │ ├── Pods-Life Saver tvOS.debug.xcconfig │ ├── Pods-Life Saver tvOS.modulemap │ └── Pods-Life Saver tvOS.release.xcconfig │ ├── SwiftLint-macOS │ └── SwiftLint-macOS.xcconfig │ └── SwiftLint-tvOS │ └── SwiftLint-tvOS.xcconfig ├── README.md ├── Shared ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── github.imageset │ │ ├── Contents.json │ │ ├── github_dark-1.png │ │ ├── github_dark.png │ │ ├── github_light-1.png │ │ └── github_light.png │ ├── square.imageset │ │ ├── Contents.json │ │ └── aeonSquare.pdf │ ├── thumbnail.imageset │ │ ├── Contents.json │ │ ├── thumbnail.png │ │ └── thumbnail@2x.png │ └── twitter.imageset │ │ ├── Contents.json │ │ ├── twitter_dark-1.png │ │ ├── twitter_dark.png │ │ ├── twitter_light-1.png │ │ └── twitter_light.png ├── LifeDatabase.swift ├── LifeManager.swift ├── LifeNode.swift ├── LifePreset.swift ├── LifeScene.swift └── Utilities │ ├── Extensions.swift │ ├── FileGrabber.swift │ ├── ToroidalMatrix.swift │ └── URLType.swift ├── macOS Screensaver ├── Configuration │ ├── ConfigureSheet.xib │ └── ConfigureSheetController.swift ├── Info.plist ├── LifeDatabase.swift └── LifeScreenSaverView.swift ├── macOS ├── AppDelegate.swift ├── Base.lproj │ └── Main.storyboard ├── Info.plist ├── Life_Saver.entitlements └── ViewController.swift └── tvOS ├── AppDelegate.swift ├── Assets.xcassets ├── Brand Assets.brandassets │ ├── App Icon - App Store.imagestack │ │ ├── Back.imagestacklayer │ │ │ ├── Content.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── TVA - Bottom Layer.png │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Front.imagestacklayer │ │ │ ├── Content.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── TVA - Top Layer.png │ │ │ └── Contents.json │ │ └── Middle.imagestacklayer │ │ │ ├── Content.imageset │ │ │ ├── Contents.json │ │ │ └── TVA - Middle Layer.png │ │ │ └── Contents.json │ ├── App Icon.imagestack │ │ ├── Back.imagestacklayer │ │ │ ├── Content.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── TV - Bottom Layer.png │ │ │ │ └── TV - Bottom Layer@2x.png │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Front.imagestacklayer │ │ │ ├── Content.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── TV - Top Layer.png │ │ │ │ └── TV - Top Layer@2x.png │ │ │ └── Contents.json │ │ └── Middle.imagestacklayer │ │ │ ├── Content.imageset │ │ │ ├── Contents.json │ │ │ ├── TV - Middle Layer.png │ │ │ └── TV - Middle Layer@2x.png │ │ │ └── Contents.json │ ├── Contents.json │ ├── Top Shelf Image Wide.imageset │ │ ├── Contents.json │ │ ├── Top Shelf.png │ │ └── Top Shelf@2x.png │ └── Top Shelf Image.imageset │ │ └── Contents.json ├── Contents.json └── LaunchImage.launchimage │ ├── Contents.json │ ├── LaunchImage.png │ └── LaunchImage@2x.png ├── Base.lproj └── Main.storyboard ├── Info.plist ├── LifeViewController.swift └── Menus ├── ColorPresetTableViewCell.swift └── MenuTableViewController.swift /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [amiantos] 4 | patreon: amiantos 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | custom: # Replace with a single custom sponsorship URL 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | *.pkg 9 | *.saver 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata/ 21 | 22 | ## Other 23 | *.moved-aside 24 | *.xccheckout 25 | *.xcscmblueprint 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | .build/ 44 | 45 | # CocoaPods 46 | # 47 | # We recommend against adding the Pods directory to your .gitignore. However 48 | # you should judge for yourself, the pros and cons are mentioned at: 49 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 50 | # 51 | # Pods/ 52 | 53 | # Carthage 54 | # 55 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 56 | # Carthage/Checkouts 57 | 58 | Carthage/Build 59 | 60 | # fastlane 61 | # 62 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 63 | # screenshots whenever they are needed. 64 | # For more information about the recommended setup visit: 65 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 66 | 67 | fastlane/report.xml 68 | fastlane/Preview.html 69 | fastlane/screenshots/**/*.png 70 | fastlane/test_output 71 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - trailing_comma 3 | 4 | excluded: # paths to ignore during linting. Takes precedence over `included`. 5 | - Carthage 6 | - Pods 7 | 8 | line_length: 9 | - 150 # warning 10 | - 200 # error 11 | 12 | type_body_length: 13 | - 250 14 | 15 | cyclomatic_complexity: 16 | ignores_case_statements: true -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Screensaver 2 | 3 | ## 1.2 4 | - Added two smaller size options ((#18 by lordlycastle)[https://www.youtube.com/watch?v=A3pq9xL3kIs]) 5 | 6 | ## 1.1 7 | - Config sheet detects if colors match existing preset and selects it in picker 8 | - Added option for selecting a random color preset on launch 9 | - Improvements to "extra" cell death animation 10 | - Many optimizations to reduce memory, CPU, and energy usage 11 | - Neighbor generation is much faster due to usage of ToroidalMatrix 12 | - Reduced preferred FPS to 30 fps 13 | - Uses shared SKTexture to reduce per-frame draw count to 1 14 | 15 | ## 1.0 16 | - Initial release -------------------------------------------------------------------------------- /Design/screensaver/amiantos-screenshots.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/Design/screensaver/amiantos-screenshots.pxm -------------------------------------------------------------------------------- /Design/screensaver/lifesaver-screenshots-1.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/Design/screensaver/lifesaver-screenshots-1.1.png -------------------------------------------------------------------------------- /Design/screensaver/lifesaver-screenshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/Design/screensaver/lifesaver-screenshots.png -------------------------------------------------------------------------------- /Design/screensaver/screenshots.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/Design/screensaver/screenshots.pxm -------------------------------------------------------------------------------- /Design/tvos/tvos-ss-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/Design/tvos/tvos-ss-1.png -------------------------------------------------------------------------------- /Design/tvos/tvos-ss-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/Design/tvos/tvos-ss-2.png -------------------------------------------------------------------------------- /Installer Resources/Life Saver.pkgproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PACKAGES 6 | 7 | 8 | MUST-CLOSE-APPLICATION-ITEMS 9 | 10 | 11 | APPLICATION_ID 12 | x-apple.systempreferences 13 | STATE 14 | 15 | 16 | 17 | MUST-CLOSE-APPLICATIONS 18 | 19 | PACKAGE_FILES 20 | 21 | DEFAULT_INSTALL_LOCATION 22 | / 23 | HIERARCHY 24 | 25 | CHILDREN 26 | 27 | 28 | CHILDREN 29 | 30 | GID 31 | 80 32 | PATH 33 | Applications 34 | PATH_TYPE 35 | 0 36 | PERMISSIONS 37 | 509 38 | TYPE 39 | 1 40 | UID 41 | 0 42 | 43 | 44 | CHILDREN 45 | 46 | 47 | CHILDREN 48 | 49 | GID 50 | 80 51 | PATH 52 | Application Support 53 | PATH_TYPE 54 | 0 55 | PERMISSIONS 56 | 493 57 | TYPE 58 | 1 59 | UID 60 | 0 61 | 62 | 63 | CHILDREN 64 | 65 | GID 66 | 0 67 | PATH 68 | Automator 69 | PATH_TYPE 70 | 0 71 | PERMISSIONS 72 | 493 73 | TYPE 74 | 1 75 | UID 76 | 0 77 | 78 | 79 | CHILDREN 80 | 81 | GID 82 | 0 83 | PATH 84 | Documentation 85 | PATH_TYPE 86 | 0 87 | PERMISSIONS 88 | 493 89 | TYPE 90 | 1 91 | UID 92 | 0 93 | 94 | 95 | CHILDREN 96 | 97 | GID 98 | 0 99 | PATH 100 | Extensions 101 | PATH_TYPE 102 | 0 103 | PERMISSIONS 104 | 493 105 | TYPE 106 | 1 107 | UID 108 | 0 109 | 110 | 111 | CHILDREN 112 | 113 | GID 114 | 0 115 | PATH 116 | Filesystems 117 | PATH_TYPE 118 | 0 119 | PERMISSIONS 120 | 493 121 | TYPE 122 | 1 123 | UID 124 | 0 125 | 126 | 127 | CHILDREN 128 | 129 | GID 130 | 0 131 | PATH 132 | Frameworks 133 | PATH_TYPE 134 | 0 135 | PERMISSIONS 136 | 493 137 | TYPE 138 | 1 139 | UID 140 | 0 141 | 142 | 143 | CHILDREN 144 | 145 | GID 146 | 0 147 | PATH 148 | Input Methods 149 | PATH_TYPE 150 | 0 151 | PERMISSIONS 152 | 493 153 | TYPE 154 | 1 155 | UID 156 | 0 157 | 158 | 159 | CHILDREN 160 | 161 | GID 162 | 0 163 | PATH 164 | Internet Plug-Ins 165 | PATH_TYPE 166 | 0 167 | PERMISSIONS 168 | 493 169 | TYPE 170 | 1 171 | UID 172 | 0 173 | 174 | 175 | CHILDREN 176 | 177 | GID 178 | 0 179 | PATH 180 | LaunchAgents 181 | PATH_TYPE 182 | 0 183 | PERMISSIONS 184 | 493 185 | TYPE 186 | 1 187 | UID 188 | 0 189 | 190 | 191 | CHILDREN 192 | 193 | GID 194 | 0 195 | PATH 196 | LaunchDaemons 197 | PATH_TYPE 198 | 0 199 | PERMISSIONS 200 | 493 201 | TYPE 202 | 1 203 | UID 204 | 0 205 | 206 | 207 | CHILDREN 208 | 209 | GID 210 | 0 211 | PATH 212 | PreferencePanes 213 | PATH_TYPE 214 | 0 215 | PERMISSIONS 216 | 493 217 | TYPE 218 | 1 219 | UID 220 | 0 221 | 222 | 223 | CHILDREN 224 | 225 | GID 226 | 0 227 | PATH 228 | Preferences 229 | PATH_TYPE 230 | 0 231 | PERMISSIONS 232 | 493 233 | TYPE 234 | 1 235 | UID 236 | 0 237 | 238 | 239 | CHILDREN 240 | 241 | GID 242 | 80 243 | PATH 244 | Printers 245 | PATH_TYPE 246 | 0 247 | PERMISSIONS 248 | 493 249 | TYPE 250 | 1 251 | UID 252 | 0 253 | 254 | 255 | CHILDREN 256 | 257 | GID 258 | 0 259 | PATH 260 | PrivilegedHelperTools 261 | PATH_TYPE 262 | 0 263 | PERMISSIONS 264 | 1005 265 | TYPE 266 | 1 267 | UID 268 | 0 269 | 270 | 271 | CHILDREN 272 | 273 | GID 274 | 0 275 | PATH 276 | QuickLook 277 | PATH_TYPE 278 | 0 279 | PERMISSIONS 280 | 493 281 | TYPE 282 | 1 283 | UID 284 | 0 285 | 286 | 287 | CHILDREN 288 | 289 | GID 290 | 0 291 | PATH 292 | QuickTime 293 | PATH_TYPE 294 | 0 295 | PERMISSIONS 296 | 493 297 | TYPE 298 | 1 299 | UID 300 | 0 301 | 302 | 303 | CHILDREN 304 | 305 | 306 | BUNDLE_CAN_DOWNGRADE 307 | 308 | BUNDLE_POSTINSTALL_PATH 309 | 310 | PATH_TYPE 311 | 0 312 | 313 | BUNDLE_PREINSTALL_PATH 314 | 315 | PATH_TYPE 316 | 0 317 | 318 | CHILDREN 319 | 320 | GID 321 | 0 322 | PATH 323 | Life Saver.saver 324 | PATH_TYPE 325 | 1 326 | PERMISSIONS 327 | 493 328 | TYPE 329 | 3 330 | UID 331 | 0 332 | 333 | 334 | GID 335 | 0 336 | PATH 337 | Screen Savers 338 | PATH_TYPE 339 | 0 340 | PERMISSIONS 341 | 493 342 | TYPE 343 | 1 344 | UID 345 | 0 346 | 347 | 348 | CHILDREN 349 | 350 | GID 351 | 0 352 | PATH 353 | Scripts 354 | PATH_TYPE 355 | 0 356 | PERMISSIONS 357 | 493 358 | TYPE 359 | 1 360 | UID 361 | 0 362 | 363 | 364 | CHILDREN 365 | 366 | GID 367 | 0 368 | PATH 369 | Services 370 | PATH_TYPE 371 | 0 372 | PERMISSIONS 373 | 493 374 | TYPE 375 | 1 376 | UID 377 | 0 378 | 379 | 380 | CHILDREN 381 | 382 | GID 383 | 0 384 | PATH 385 | Widgets 386 | PATH_TYPE 387 | 0 388 | PERMISSIONS 389 | 493 390 | TYPE 391 | 1 392 | UID 393 | 0 394 | 395 | 396 | GID 397 | 0 398 | PATH 399 | Library 400 | PATH_TYPE 401 | 0 402 | PERMISSIONS 403 | 493 404 | TYPE 405 | 1 406 | UID 407 | 0 408 | 409 | 410 | CHILDREN 411 | 412 | 413 | CHILDREN 414 | 415 | GID 416 | 0 417 | PATH 418 | Shared 419 | PATH_TYPE 420 | 0 421 | PERMISSIONS 422 | 1023 423 | TYPE 424 | 1 425 | UID 426 | 0 427 | 428 | 429 | GID 430 | 80 431 | PATH 432 | Users 433 | PATH_TYPE 434 | 0 435 | PERMISSIONS 436 | 493 437 | TYPE 438 | 1 439 | UID 440 | 0 441 | 442 | 443 | GID 444 | 0 445 | PATH 446 | / 447 | PATH_TYPE 448 | 0 449 | PERMISSIONS 450 | 493 451 | TYPE 452 | 1 453 | UID 454 | 0 455 | 456 | PAYLOAD_TYPE 457 | 0 458 | SHOW_INVISIBLE 459 | 460 | SPLIT_FORKS 461 | 462 | TREAT_MISSING_FILES_AS_WARNING 463 | 464 | VERSION 465 | 5 466 | 467 | PACKAGE_SCRIPTS 468 | 469 | POSTINSTALL_PATH 470 | 471 | PATH 472 | /Users/bradroot/Coding/Life Saver/Installer Resources/post-install.sh 473 | PATH_TYPE 474 | 0 475 | 476 | PREINSTALL_PATH 477 | 478 | PATH_TYPE 479 | 0 480 | 481 | RESOURCES 482 | 483 | 484 | PACKAGE_SETTINGS 485 | 486 | AUTHENTICATION 487 | 1 488 | CONCLUSION_ACTION 489 | 0 490 | FOLLOW_SYMBOLIC_LINKS 491 | 492 | IDENTIFIER 493 | net.amiantos.pkg.LifeSaver11 494 | LOCATION 495 | 0 496 | NAME 497 | Life Saver 1.1 498 | OVERWRITE_PERMISSIONS 499 | 500 | PAYLOAD_SIZE 501 | -1 502 | REFERENCE_PATH 503 | 504 | RELOCATABLE 505 | 506 | USE_HFS+_COMPRESSION 507 | 508 | VERSION 509 | 1.1 510 | 511 | TYPE 512 | 0 513 | UUID 514 | A2078BF3-F182-4A46-8691-00485201F940 515 | 516 | 517 | PROJECT 518 | 519 | PROJECT_COMMENTS 520 | 521 | NOTES 522 | 523 | PCFET0NUWVBFIGh0bWwgUFVCTElDICItLy9XM0MvL0RURCBIVE1M 524 | IDQuMDEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvVFIvaHRtbDQv 525 | c3RyaWN0LmR0ZCI+CjxodG1sPgo8aGVhZD4KPG1ldGEgaHR0cC1l 526 | cXVpdj0iQ29udGVudC1UeXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7 527 | IGNoYXJzZXQ9VVRGLTgiPgo8bWV0YSBodHRwLWVxdWl2PSJDb250 528 | ZW50LVN0eWxlLVR5cGUiIGNvbnRlbnQ9InRleHQvY3NzIj4KPHRp 529 | dGxlPjwvdGl0bGU+CjxtZXRhIG5hbWU9IkdlbmVyYXRvciIgY29u 530 | dGVudD0iQ29jb2EgSFRNTCBXcml0ZXIiPgo8bWV0YSBuYW1lPSJD 531 | b2NvYVZlcnNpb24iIGNvbnRlbnQ9IjE2NzEuNSI+CjxzdHlsZSB0 532 | eXBlPSJ0ZXh0L2NzcyI+Cjwvc3R5bGU+CjwvaGVhZD4KPGJvZHk+ 533 | CjwvYm9keT4KPC9odG1sPgo= 534 | 535 | 536 | PROJECT_PRESENTATION 537 | 538 | BACKGROUND 539 | 540 | APPAREANCES 541 | 542 | DARK_AQUA 543 | 544 | LIGHT_AQUA 545 | 546 | 547 | SHARED_SETTINGS_FOR_ALL_APPAREANCES 548 | 549 | 550 | INSTALLATION_STEPS 551 | 552 | 553 | ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS 554 | ICPresentationViewIntroductionController 555 | INSTALLER_PLUGIN 556 | Introduction 557 | LIST_TITLE_KEY 558 | InstallerSectionTitle 559 | 560 | 561 | ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS 562 | ICPresentationViewReadMeController 563 | INSTALLER_PLUGIN 564 | ReadMe 565 | LIST_TITLE_KEY 566 | InstallerSectionTitle 567 | 568 | 569 | ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS 570 | ICPresentationViewLicenseController 571 | INSTALLER_PLUGIN 572 | License 573 | LIST_TITLE_KEY 574 | InstallerSectionTitle 575 | 576 | 577 | ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS 578 | ICPresentationViewDestinationSelectController 579 | INSTALLER_PLUGIN 580 | TargetSelect 581 | LIST_TITLE_KEY 582 | InstallerSectionTitle 583 | 584 | 585 | ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS 586 | ICPresentationViewInstallationTypeController 587 | INSTALLER_PLUGIN 588 | PackageSelection 589 | LIST_TITLE_KEY 590 | InstallerSectionTitle 591 | 592 | 593 | ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS 594 | ICPresentationViewInstallationController 595 | INSTALLER_PLUGIN 596 | Install 597 | LIST_TITLE_KEY 598 | InstallerSectionTitle 599 | 600 | 601 | ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS 602 | ICPresentationViewSummaryController 603 | INSTALLER_PLUGIN 604 | Summary 605 | LIST_TITLE_KEY 606 | InstallerSectionTitle 607 | 608 | 609 | INTRODUCTION 610 | 611 | LOCALIZATIONS 612 | 613 | 614 | LANGUAGE 615 | English 616 | VALUE 617 | 618 | PATH 619 | /Users/bradroot/Coding/Life Saver/Installer Resources/introduction.rtf 620 | PATH_TYPE 621 | 1 622 | 623 | 624 | 625 | 626 | LICENSE 627 | 628 | LOCALIZATIONS 629 | 630 | MODE 631 | 0 632 | 633 | README 634 | 635 | LOCALIZATIONS 636 | 637 | 638 | SUMMARY 639 | 640 | LOCALIZATIONS 641 | 642 | 643 | TITLE 644 | 645 | LOCALIZATIONS 646 | 647 | 648 | LANGUAGE 649 | English 650 | VALUE 651 | Life Saver 1.1 652 | 653 | 654 | 655 | 656 | PROJECT_REQUIREMENTS 657 | 658 | LIST 659 | 660 | RESOURCES 661 | 662 | ROOT_VOLUME_ONLY 663 | 664 | 665 | PROJECT_SETTINGS 666 | 667 | BUILD_FORMAT 668 | 0 669 | BUILD_PATH 670 | 671 | PATH 672 | . 673 | PATH_TYPE 674 | 1 675 | 676 | EXCLUDED_FILES 677 | 678 | 679 | PATTERNS_ARRAY 680 | 681 | 682 | REGULAR_EXPRESSION 683 | 684 | STRING 685 | .DS_Store 686 | TYPE 687 | 0 688 | 689 | 690 | PROTECTED 691 | 692 | PROXY_NAME 693 | Remove .DS_Store files 694 | PROXY_TOOLTIP 695 | Remove ".DS_Store" files created by the Finder. 696 | STATE 697 | 698 | 699 | 700 | PATTERNS_ARRAY 701 | 702 | 703 | REGULAR_EXPRESSION 704 | 705 | STRING 706 | .pbdevelopment 707 | TYPE 708 | 0 709 | 710 | 711 | PROTECTED 712 | 713 | PROXY_NAME 714 | Remove .pbdevelopment files 715 | PROXY_TOOLTIP 716 | Remove ".pbdevelopment" files created by ProjectBuilder or Xcode. 717 | STATE 718 | 719 | 720 | 721 | PATTERNS_ARRAY 722 | 723 | 724 | REGULAR_EXPRESSION 725 | 726 | STRING 727 | CVS 728 | TYPE 729 | 1 730 | 731 | 732 | REGULAR_EXPRESSION 733 | 734 | STRING 735 | .cvsignore 736 | TYPE 737 | 0 738 | 739 | 740 | REGULAR_EXPRESSION 741 | 742 | STRING 743 | .cvspass 744 | TYPE 745 | 0 746 | 747 | 748 | REGULAR_EXPRESSION 749 | 750 | STRING 751 | .svn 752 | TYPE 753 | 1 754 | 755 | 756 | REGULAR_EXPRESSION 757 | 758 | STRING 759 | .git 760 | TYPE 761 | 1 762 | 763 | 764 | REGULAR_EXPRESSION 765 | 766 | STRING 767 | .gitignore 768 | TYPE 769 | 0 770 | 771 | 772 | PROTECTED 773 | 774 | PROXY_NAME 775 | Remove SCM metadata 776 | PROXY_TOOLTIP 777 | Remove helper files and folders used by the CVS, SVN or Git Source Code Management systems. 778 | STATE 779 | 780 | 781 | 782 | PATTERNS_ARRAY 783 | 784 | 785 | REGULAR_EXPRESSION 786 | 787 | STRING 788 | classes.nib 789 | TYPE 790 | 0 791 | 792 | 793 | REGULAR_EXPRESSION 794 | 795 | STRING 796 | designable.db 797 | TYPE 798 | 0 799 | 800 | 801 | REGULAR_EXPRESSION 802 | 803 | STRING 804 | info.nib 805 | TYPE 806 | 0 807 | 808 | 809 | PROTECTED 810 | 811 | PROXY_NAME 812 | Optimize nib files 813 | PROXY_TOOLTIP 814 | Remove "classes.nib", "info.nib" and "designable.nib" files within .nib bundles. 815 | STATE 816 | 817 | 818 | 819 | PATTERNS_ARRAY 820 | 821 | 822 | REGULAR_EXPRESSION 823 | 824 | STRING 825 | Resources Disabled 826 | TYPE 827 | 1 828 | 829 | 830 | PROTECTED 831 | 832 | PROXY_NAME 833 | Remove Resources Disabled folders 834 | PROXY_TOOLTIP 835 | Remove "Resources Disabled" folders. 836 | STATE 837 | 838 | 839 | 840 | SEPARATOR 841 | 842 | 843 | 844 | NAME 845 | lifesaver-1.1-unsigned 846 | PAYLOAD_ONLY 847 | 848 | TREAT_MISSING_PRESENTATION_DOCUMENTS_AS_WARNING 849 | 850 | 851 | 852 | TYPE 853 | 0 854 | VERSION 855 | 2 856 | 857 | 858 | -------------------------------------------------------------------------------- /Installer Resources/introduction.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1671\cocoasubrtf500 2 | {\fonttbl\f0\fswiss\fcharset0 Helvetica-Bold;\f1\fswiss\fcharset0 Helvetica;} 3 | {\colortbl;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;} 5 | \margl1440\margr1440\vieww12600\viewh7800\viewkind0 6 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 7 | 8 | \f0\b\fs36 \cf0 Life Saver 1.1\ 9 | 10 | \fs28 A Conway's Game of Life Screensaver 11 | \fs36 \ 12 | \ 13 | 14 | \f1\b0\fs26 Thanks for downloading Life Saver!\ 15 | \ 16 | The installer will install the screensaver into the \ul /Library/Screen Savers\ulnone folder on your computer. 17 | \fs24 \ 18 | \ 19 | 20 | \f0\b\fs26 After installation completes, your screen saver configuration panel will open. Select "Life Saver" and you're good to go.\ 21 | 22 | \f1\b0\fs24 \ 23 | {\field{\*\fldinst{HYPERLINK "https://www.amiantos.net/lifesaver"}}{\fldrslt 24 | \fs28 https://www.amiantos.net/lifesaver}}} -------------------------------------------------------------------------------- /Installer Resources/post-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | open /System/Library/PreferencePanes/DesktopScreenEffectsPref.prefPane -------------------------------------------------------------------------------- /Installer Resources/signing.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | /usr/bin/productsign --sign "Developer ID Installer: Brad Root (2Y9M69QJKZ)" lifesaver-1.1-unsigned.pkg lifesaver-1.1.pkg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /Life Saver.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Life Saver.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Life Saver.xcodeproj/xcshareddata/xcschemes/Life Saver Screensaver.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 71 | 72 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /Life Saver.xcodeproj/xcshareddata/xcschemes/Life Saver macOS Debug.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Life Saver.xcodeproj/xcshareddata/xcschemes/Life Saver tvOS Debug.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Life Saver.xcodeproj/xcshareddata/xcschemes/Life Saver tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Life Saver.xcodeproj/xcuserdata/bradroot.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Life Saver Screensaver.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | Life Saver macOS Debug.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 1 16 | 17 | Life Saver tvOS Debug.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 7 21 | 22 | Life Saver tvOS.xcscheme_^#shared#^_ 23 | 24 | orderHint 25 | 6 26 | 27 | 28 | SuppressBuildableAutocreation 29 | 30 | B492215E2291F59C00D5DEA4 31 | 32 | primary 33 | 34 | 35 | B4FD88C32290B9DB00AE066A 36 | 37 | primary 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Life Saver.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Life Saver.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'Life Saver macOS' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for Life Saver macOS 9 | pod 'SwiftLint' 10 | 11 | end 12 | 13 | target 'Life Saver Screensaver' do 14 | # Comment the next line if you don't want to use dynamic frameworks 15 | use_frameworks! 16 | 17 | # Pods for Life Saver Screensaver 18 | 19 | end 20 | 21 | target 'Life Saver tvOS' do 22 | # Comment the next line if you don't want to use dynamic frameworks 23 | use_frameworks! 24 | 25 | # Pods for Life Saver tvOS 26 | pod 'SwiftLint' 27 | end 28 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SwiftLint (0.33.0) 3 | 4 | DEPENDENCIES: 5 | - SwiftLint 6 | 7 | SPEC REPOS: 8 | https://github.com/cocoapods/specs.git: 9 | - SwiftLint 10 | 11 | SPEC CHECKSUMS: 12 | SwiftLint: fed9c66336e41fc74dc48a73678380718f0c8b0e 13 | 14 | PODFILE CHECKSUM: 4e098b7aa804dffdefce9b1afb1a86d385d687ae 15 | 16 | COCOAPODS: 1.7.2 17 | -------------------------------------------------------------------------------- /Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SwiftLint (0.33.0) 3 | 4 | DEPENDENCIES: 5 | - SwiftLint 6 | 7 | SPEC REPOS: 8 | https://github.com/cocoapods/specs.git: 9 | - SwiftLint 10 | 11 | SPEC CHECKSUMS: 12 | SwiftLint: fed9c66336e41fc74dc48a73678380718f0c8b0e 13 | 14 | PODFILE CHECKSUM: 4e098b7aa804dffdefce9b1afb1a86d385d687ae 15 | 16 | COCOAPODS: 1.7.2 17 | -------------------------------------------------------------------------------- /Pods/SwiftLint/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Realm Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pods/SwiftLint/swiftlint: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/Pods/SwiftLint/swiftlint -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver Screensaver/Pods-Life Saver Screensaver-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver Screensaver/Pods-Life Saver Screensaver-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | Generated by CocoaPods - https://cocoapods.org 4 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver Screensaver/Pods-Life Saver Screensaver-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Generated by CocoaPods - https://cocoapods.org 18 | Title 19 | 20 | Type 21 | PSGroupSpecifier 22 | 23 | 24 | StringsTable 25 | Acknowledgements 26 | Title 27 | Acknowledgements 28 | 29 | 30 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver Screensaver/Pods-Life Saver Screensaver-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_Life_Saver_Screensaver : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_Life_Saver_Screensaver 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver Screensaver/Pods-Life Saver Screensaver-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_Life_Saver_ScreensaverVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_Life_Saver_ScreensaverVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver Screensaver/Pods-Life Saver Screensaver.debug.xcconfig: -------------------------------------------------------------------------------- 1 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 2 | PODS_BUILD_DIR = ${BUILD_DIR} 3 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 4 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 5 | PODS_ROOT = ${SRCROOT}/Pods 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver Screensaver/Pods-Life Saver Screensaver.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_Life_Saver_Screensaver { 2 | umbrella header "Pods-Life Saver Screensaver-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver Screensaver/Pods-Life Saver Screensaver.release.xcconfig: -------------------------------------------------------------------------------- 1 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 2 | PODS_BUILD_DIR = ${BUILD_DIR} 3 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 4 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 5 | PODS_ROOT = ${SRCROOT}/Pods 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver macOS/Pods-Life Saver macOS-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver macOS/Pods-Life Saver macOS-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## SwiftLint 5 | 6 | The MIT License (MIT) 7 | 8 | Copyright (c) 2015 Realm Inc. 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | 28 | Generated by CocoaPods - https://cocoapods.org 29 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver macOS/Pods-Life Saver macOS-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | The MIT License (MIT) 18 | 19 | Copyright (c) 2015 Realm Inc. 20 | 21 | Permission is hereby granted, free of charge, to any person obtaining a copy 22 | of this software and associated documentation files (the "Software"), to deal 23 | in the Software without restriction, including without limitation the rights 24 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | copies of the Software, and to permit persons to whom the Software is 26 | furnished to do so, subject to the following conditions: 27 | 28 | The above copyright notice and this permission notice shall be included in all 29 | copies or substantial portions of the Software. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 36 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 37 | SOFTWARE. 38 | 39 | License 40 | MIT 41 | Title 42 | SwiftLint 43 | Type 44 | PSGroupSpecifier 45 | 46 | 47 | FooterText 48 | Generated by CocoaPods - https://cocoapods.org 49 | Title 50 | 51 | Type 52 | PSGroupSpecifier 53 | 54 | 55 | StringsTable 56 | Acknowledgements 57 | Title 58 | Acknowledgements 59 | 60 | 61 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver macOS/Pods-Life Saver macOS-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_Life_Saver_macOS : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_Life_Saver_macOS 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver macOS/Pods-Life Saver macOS-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_Life_Saver_macOSVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_Life_Saver_macOSVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver macOS/Pods-Life Saver macOS.debug.xcconfig: -------------------------------------------------------------------------------- 1 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 2 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/Frameworks' 3 | PODS_BUILD_DIR = ${BUILD_DIR} 4 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 5 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 6 | PODS_ROOT = ${SRCROOT}/Pods 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver macOS/Pods-Life Saver macOS.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_Life_Saver_macOS { 2 | umbrella header "Pods-Life Saver macOS-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver macOS/Pods-Life Saver macOS.release.xcconfig: -------------------------------------------------------------------------------- 1 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 2 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/../Frameworks' '@loader_path/Frameworks' 3 | PODS_BUILD_DIR = ${BUILD_DIR} 4 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 5 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 6 | PODS_ROOT = ${SRCROOT}/Pods 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver tvOS/Pods-Life Saver tvOS-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver tvOS/Pods-Life Saver tvOS-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## SwiftLint 5 | 6 | The MIT License (MIT) 7 | 8 | Copyright (c) 2015 Realm Inc. 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | 28 | Generated by CocoaPods - https://cocoapods.org 29 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver tvOS/Pods-Life Saver tvOS-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | The MIT License (MIT) 18 | 19 | Copyright (c) 2015 Realm Inc. 20 | 21 | Permission is hereby granted, free of charge, to any person obtaining a copy 22 | of this software and associated documentation files (the "Software"), to deal 23 | in the Software without restriction, including without limitation the rights 24 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | copies of the Software, and to permit persons to whom the Software is 26 | furnished to do so, subject to the following conditions: 27 | 28 | The above copyright notice and this permission notice shall be included in all 29 | copies or substantial portions of the Software. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 36 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 37 | SOFTWARE. 38 | 39 | License 40 | MIT 41 | Title 42 | SwiftLint 43 | Type 44 | PSGroupSpecifier 45 | 46 | 47 | FooterText 48 | Generated by CocoaPods - https://cocoapods.org 49 | Title 50 | 51 | Type 52 | PSGroupSpecifier 53 | 54 | 55 | StringsTable 56 | Acknowledgements 57 | Title 58 | Acknowledgements 59 | 60 | 61 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver tvOS/Pods-Life Saver tvOS-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_Life_Saver_tvOS : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_Life_Saver_tvOS 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver tvOS/Pods-Life Saver tvOS-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_Life_Saver_tvOSVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_Life_Saver_tvOSVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver tvOS/Pods-Life Saver tvOS.debug.xcconfig: -------------------------------------------------------------------------------- 1 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 2 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 3 | PODS_BUILD_DIR = ${BUILD_DIR} 4 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 5 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 6 | PODS_ROOT = ${SRCROOT}/Pods 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver tvOS/Pods-Life Saver tvOS.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_Life_Saver_tvOS { 2 | umbrella header "Pods-Life Saver tvOS-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Life Saver tvOS/Pods-Life Saver tvOS.release.xcconfig: -------------------------------------------------------------------------------- 1 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 2 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 3 | PODS_BUILD_DIR = ${BUILD_DIR} 4 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 5 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 6 | PODS_ROOT = ${SRCROOT}/Pods 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SwiftLint-macOS/SwiftLint-macOS.xcconfig: -------------------------------------------------------------------------------- 1 | CODE_SIGN_IDENTITY = 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftLint-macOS 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | PODS_BUILD_DIR = ${BUILD_DIR} 5 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 6 | PODS_ROOT = ${SRCROOT} 7 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftLint 8 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 9 | SKIP_INSTALL = YES 10 | -------------------------------------------------------------------------------- /Pods/Target Support Files/SwiftLint-tvOS/SwiftLint-tvOS.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftLint-tvOS 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | PODS_BUILD_DIR = ${BUILD_DIR} 4 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 5 | PODS_ROOT = ${SRCROOT} 6 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftLint 7 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 8 | SKIP_INSTALL = YES 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Life Saver 2 | 3 | Life Saver is an abstract, artistic implementation of [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) built with SpriteKit. With a variety of settings and color schemes, Life Saver should satisfy designers and geeks alike. Comes in two flavors: a macOS screensaver, and an Apple TV app. 4 | 5 | ## 🖥  Screensaver 6 | 7 | 📼 [Watch a YouTube video to see it in action](https://www.youtube.com/watch?v=N4nCFUVThgg). 8 | 9 | ![Life Saver Screensaver](/Design/screensaver/lifesaver-screenshots-1.1.png?raw=true) 10 | 11 | ### 👉 [Download Life Saver v1.2 for macOS](https://amiantos.s3.amazonaws.com/lifesaver-1.2.zip) 12 | 13 | ## 📺  Apple TV 14 | 15 | ![Life Saver tvOS Main menu](/Design/tvos/tvos-ss-1.png?raw=true) 16 | ![Life Saver tvOS Preset menu](/Design/tvos/tvos-ss-2.png?raw=true) 17 | 18 | ### 👉 [Get Life Saver TV on the App Store](https://apps.apple.com/us/app/life-saver-tv/id1470667717) 19 | 20 | ## To Install from Source 21 | 22 | 1. `git clone https://github.com/amiantos/lifesaver.git` 23 | 2. Open `Life Saver.xcworkspace` 24 | 3. Pick a target and run! 25 | 26 | 27 | -------------------------------------------------------------------------------- /Shared/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /Shared/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Shared/Assets.xcassets/github.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "filename" : "github_dark.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "filename" : "github_light.png", 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "dark" 15 | } 16 | ], 17 | "scale" : "1x" 18 | }, 19 | { 20 | "idiom" : "mac", 21 | "filename" : "github_dark-1.png", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "idiom" : "mac", 26 | "filename" : "github_light-1.png", 27 | "appearances" : [ 28 | { 29 | "appearance" : "luminosity", 30 | "value" : "dark" 31 | } 32 | ], 33 | "scale" : "2x" 34 | } 35 | ], 36 | "info" : { 37 | "version" : 1, 38 | "author" : "xcode" 39 | } 40 | } -------------------------------------------------------------------------------- /Shared/Assets.xcassets/github.imageset/github_dark-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/Shared/Assets.xcassets/github.imageset/github_dark-1.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/github.imageset/github_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/Shared/Assets.xcassets/github.imageset/github_dark.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/github.imageset/github_light-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/Shared/Assets.xcassets/github.imageset/github_light-1.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/github.imageset/github_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/Shared/Assets.xcassets/github.imageset/github_light.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/square.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "aeonSquare.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Shared/Assets.xcassets/square.imageset/aeonSquare.pdf: -------------------------------------------------------------------------------- 1 | %PDF-1.7 2 | 3 | 1 0 obj 4 | << >> 5 | endobj 6 | 7 | 2 0 obj 8 | << /Length 3 0 R >> 9 | stream 10 | /DeviceRGB CS 11 | /DeviceRGB cs 12 | q 13 | 1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm 14 | 0.000000 60.000000 m 15 | 60.000000 60.000000 l 16 | 60.000000 0.000000 l 17 | 0.000000 0.000000 l 18 | 0.000000 60.000000 l 19 | h 20 | 1.000000 1.000000 1.000000 scn 21 | f 22 | n 23 | Q 24 | 25 | endstream 26 | endobj 27 | 28 | 3 0 obj 29 | 232 30 | endobj 31 | 32 | 4 0 obj 33 | << /MediaBox [ 0.000000 0.000000 60.000000 60.000000 ] 34 | /Resources 1 0 R 35 | /Contents 2 0 R 36 | /Parent 5 0 R 37 | /Type /Page 38 | >> 39 | endobj 40 | 41 | 5 0 obj 42 | << /Kids [ 4 0 R ] 43 | /Count 1 44 | /Type /Pages 45 | >> 46 | endobj 47 | 48 | 6 0 obj 49 | << /Type /Catalog 50 | /Pages 5 0 R 51 | >> 52 | endobj 53 | 54 | xref 55 | 0 7 56 | 0000000000 65535 f 57 | 0000000010 00000 n 58 | 0000000034 00000 n 59 | 0000000322 00000 n 60 | 0000000344 00000 n 61 | 0000000501 00000 n 62 | 0000000575 00000 n 63 | trailer 64 | << /ID [ (some) (id) ] 65 | /Root 6 0 R 66 | /Size 7 67 | >> 68 | startxref 69 | 634 70 | %%EOF -------------------------------------------------------------------------------- /Shared/Assets.xcassets/thumbnail.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "filename" : "thumbnail.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "filename" : "thumbnail@2x.png", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /Shared/Assets.xcassets/thumbnail.imageset/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/Shared/Assets.xcassets/thumbnail.imageset/thumbnail.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/thumbnail.imageset/thumbnail@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/Shared/Assets.xcassets/thumbnail.imageset/thumbnail@2x.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/twitter.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "filename" : "twitter_dark.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "filename" : "twitter_light.png", 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "dark" 15 | } 16 | ], 17 | "scale" : "1x" 18 | }, 19 | { 20 | "idiom" : "mac", 21 | "filename" : "twitter_dark-1.png", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "idiom" : "mac", 26 | "filename" : "twitter_light-1.png", 27 | "appearances" : [ 28 | { 29 | "appearance" : "luminosity", 30 | "value" : "dark" 31 | } 32 | ], 33 | "scale" : "2x" 34 | } 35 | ], 36 | "info" : { 37 | "version" : 1, 38 | "author" : "xcode" 39 | } 40 | } -------------------------------------------------------------------------------- /Shared/Assets.xcassets/twitter.imageset/twitter_dark-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/Shared/Assets.xcassets/twitter.imageset/twitter_dark-1.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/twitter.imageset/twitter_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/Shared/Assets.xcassets/twitter.imageset/twitter_dark.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/twitter.imageset/twitter_light-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/Shared/Assets.xcassets/twitter.imageset/twitter_light-1.png -------------------------------------------------------------------------------- /Shared/Assets.xcassets/twitter.imageset/twitter_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/Shared/Assets.xcassets/twitter.imageset/twitter_light.png -------------------------------------------------------------------------------- /Shared/LifeDatabase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LifeDatabase.swift 3 | // Life Saver 4 | // 5 | // Created by Bradley Root on 6/23/19. 6 | // Copyright © 2019 Brad Root. All rights reserved. 7 | // 8 | // This Source Code Form is subject to the terms of the Mozilla Public 9 | // License, v. 2.0. If a copy of the MPL was not distributed with this 10 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 11 | 12 | import SpriteKit 13 | 14 | struct LifeDatabase { 15 | fileprivate enum Key { 16 | static let appearanceMode = "appearanceMode" 17 | static let squareSize = "squareSize" 18 | static let blurAmount = "blurAmount" 19 | static let animationSpeed = "animationSpeed" 20 | static let color1 = "color1" 21 | static let color2 = "color2" 22 | static let color3 = "color3" 23 | static let randomColorPreset = "randomColorPreset" 24 | static let selectedPresetTitle = "selectedPresetTitle" 25 | static let deathFade = "deathFade" 26 | static let shiftingColors = "shiftingColors" 27 | static let hasPressedMenuButton = "hasPressedMenuButton" 28 | } 29 | 30 | static var standard: UserDefaults { 31 | let database = UserDefaults.standard 32 | 33 | database.register(defaults: 34 | [Key.appearanceMode: Appearance.dark.rawValue, 35 | Key.animationSpeed: AnimationSpeed.normal.rawValue, 36 | Key.squareSize: SquareSize.medium.rawValue, 37 | Key.color1: archiveData(SKColor.defaultColor1), 38 | Key.color2: archiveData(SKColor.defaultColor2), 39 | Key.color3: archiveData(SKColor.defaultColor3), 40 | Key.randomColorPreset: false, 41 | Key.deathFade: true, 42 | Key.shiftingColors: false, 43 | Key.selectedPresetTitle: "Santa Fe", 44 | Key.hasPressedMenuButton: false]) 45 | 46 | return database 47 | } 48 | } 49 | 50 | extension UserDefaults { 51 | var appearanceMode: Appearance { 52 | return Appearance(rawValue: integer(forKey: LifeDatabase.Key.appearanceMode))! 53 | } 54 | 55 | func set(appearanceMode: Appearance) { 56 | set(appearanceMode.rawValue, for: LifeDatabase.Key.appearanceMode) 57 | } 58 | 59 | var squareSize: SquareSize { 60 | return SquareSize(rawValue: integer(forKey: LifeDatabase.Key.squareSize))! 61 | } 62 | 63 | func set(squareSize: SquareSize) { 64 | set(squareSize.rawValue, for: LifeDatabase.Key.squareSize) 65 | } 66 | 67 | var animationSpeed: AnimationSpeed { 68 | return AnimationSpeed(rawValue: integer(forKey: LifeDatabase.Key.animationSpeed))! 69 | } 70 | 71 | func set(animationSpeed: AnimationSpeed) { 72 | set(animationSpeed.rawValue, for: LifeDatabase.Key.animationSpeed) 73 | } 74 | 75 | var randomColorPreset: Bool { 76 | return bool(forKey: LifeDatabase.Key.randomColorPreset) 77 | } 78 | 79 | func set(randomColorPreset: Bool) { 80 | set(randomColorPreset, for: LifeDatabase.Key.randomColorPreset) 81 | } 82 | 83 | var shiftingColors: Bool { 84 | return bool(forKey: LifeDatabase.Key.shiftingColors) 85 | } 86 | 87 | func set(shiftingColors: Bool) { 88 | set(shiftingColors, for: LifeDatabase.Key.shiftingColors) 89 | } 90 | 91 | var deathFade: Bool { 92 | return bool(forKey: LifeDatabase.Key.deathFade) 93 | } 94 | 95 | func set(deathFade: Bool) { 96 | set(deathFade, for: LifeDatabase.Key.deathFade) 97 | } 98 | 99 | var hasPressedMenuButton: Bool { 100 | return bool(forKey: LifeDatabase.Key.hasPressedMenuButton) 101 | } 102 | 103 | func set(hasPressedMenuButton: Bool) { 104 | set(hasPressedMenuButton, for: LifeDatabase.Key.hasPressedMenuButton) 105 | } 106 | 107 | var selectedPresetTitle: String { 108 | return string(forKey: LifeDatabase.Key.selectedPresetTitle) ?? "" 109 | } 110 | 111 | func set(selectedPresetTitle: String) { 112 | set(selectedPresetTitle, for: LifeDatabase.Key.selectedPresetTitle) 113 | } 114 | 115 | func getColor(_ color: Colors) -> SKColor { 116 | switch color { 117 | case .color1: 118 | return unarchiveColor(data(forKey: LifeDatabase.Key.color1)!) 119 | case .color2: 120 | return unarchiveColor(data(forKey: LifeDatabase.Key.color2)!) 121 | case .color3: 122 | return unarchiveColor(data(forKey: LifeDatabase.Key.color3)!) 123 | } 124 | } 125 | 126 | func set(_ color: SKColor, for colors: Colors) { 127 | switch colors { 128 | case .color1: 129 | set(archiveData(color), for: LifeDatabase.Key.color1) 130 | case .color2: 131 | set(archiveData(color), for: LifeDatabase.Key.color2) 132 | case .color3: 133 | set(archiveData(color), for: LifeDatabase.Key.color3) 134 | } 135 | } 136 | } 137 | 138 | private extension UserDefaults { 139 | func set(_ object: Any?, for key: String) { 140 | set(object, forKey: key) 141 | synchronize() 142 | } 143 | } 144 | 145 | private func archiveData(_ data: Any) -> Data { 146 | do { 147 | let data = try NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: false) 148 | return data 149 | } catch { 150 | fatalError("Failed to archive data") 151 | } 152 | } 153 | 154 | private func unarchiveColor(_ data: Data) -> SKColor { 155 | do { 156 | let color = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? SKColor 157 | return color! 158 | } catch { 159 | fatalError("Failed to unarchive data") 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Shared/LifeManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LifeManager.swift 3 | // Life Saver 4 | // 5 | // Created by Brad Root on 5/21/19. 6 | // Copyright © 2019 Brad Root. All rights reserved. 7 | // 8 | // This Source Code Form is subject to the terms of the Mozilla Public 9 | // License, v. 2.0. If a copy of the MPL was not distributed with this 10 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 11 | 12 | import Foundation 13 | import SpriteKit 14 | 15 | protocol LifeManagerDelegate: AnyObject { 16 | func updatedSettings() 17 | } 18 | 19 | final class LifeManager { 20 | private(set) var appearanceMode: Appearance 21 | private(set) var squareSize: SquareSize 22 | private(set) var animationSpeed: AnimationSpeed 23 | private(set) var color1: SKColor 24 | private(set) var color2: SKColor 25 | private(set) var color3: SKColor 26 | private(set) var randomColorPreset: Bool 27 | private(set) var shiftingColors: Bool 28 | private(set) var deathFade: Bool 29 | private(set) var selectedPresetTitle: String 30 | private(set) var hasPressedMenuButton: Bool 31 | 32 | private var usingPreset: Bool = false 33 | 34 | weak var delegate: LifeManagerDelegate? 35 | weak var settingsDelegate: LifeManagerDelegate? 36 | 37 | init() { 38 | appearanceMode = LifeDatabase.standard.appearanceMode 39 | squareSize = LifeDatabase.standard.squareSize 40 | animationSpeed = LifeDatabase.standard.animationSpeed 41 | color1 = LifeDatabase.standard.getColor(.color1) 42 | color2 = LifeDatabase.standard.getColor(.color2) 43 | color3 = LifeDatabase.standard.getColor(.color3) 44 | randomColorPreset = LifeDatabase.standard.randomColorPreset 45 | deathFade = LifeDatabase.standard.deathFade 46 | shiftingColors = LifeDatabase.standard.shiftingColors 47 | selectedPresetTitle = LifeDatabase.standard.selectedPresetTitle 48 | hasPressedMenuButton = LifeDatabase.standard.hasPressedMenuButton 49 | } 50 | 51 | func configure(with preset: LifePreset) { 52 | usingPreset = true 53 | 54 | if let appearanceMode = preset.appearanceMode { 55 | setAppearanceMode(appearanceMode) 56 | } 57 | 58 | if let squareSize = preset.squareSize { 59 | setSquareSize(squareSize) 60 | } 61 | 62 | if let animationSpeed = preset.animationSpeed { 63 | setAnimationSpeed(animationSpeed) 64 | } 65 | 66 | if let deathFade = preset.deathFade { 67 | setDeathFade(deathFade) 68 | } 69 | 70 | if let shiftingColors = preset.shiftingColors { 71 | setShiftingColors(shiftingColors) 72 | } 73 | 74 | if let color1 = preset.color1 { 75 | setColor(color1, for: .color1) 76 | } 77 | 78 | if let color2 = preset.color2 { 79 | setColor(color2, for: .color2) 80 | } 81 | 82 | if let color3 = preset.color3 { 83 | setColor(color3, for: .color3) 84 | } 85 | 86 | selectedPresetTitle = preset.title 87 | LifeDatabase.standard.set(selectedPresetTitle: selectedPresetTitle) 88 | 89 | usingPreset = false 90 | sendUpdateMessage() 91 | } 92 | 93 | func setRandomColorPreset(_ randomColorPreset: Bool) { 94 | self.randomColorPreset = randomColorPreset 95 | LifeDatabase.standard.set(randomColorPreset: randomColorPreset) 96 | sendUpdateMessage() 97 | } 98 | 99 | func setShiftingColors(_ shiftingColors: Bool) { 100 | self.shiftingColors = shiftingColors 101 | LifeDatabase.standard.set(shiftingColors: shiftingColors) 102 | sendUpdateMessage() 103 | } 104 | 105 | func setDeathFade(_ deathFade: Bool) { 106 | self.deathFade = deathFade 107 | LifeDatabase.standard.set(deathFade: deathFade) 108 | sendUpdateMessage() 109 | } 110 | 111 | func setHasPressedMenuButton(_ hasPressedMenuButton: Bool) { 112 | self.hasPressedMenuButton = hasPressedMenuButton 113 | LifeDatabase.standard.set(hasPressedMenuButton: hasPressedMenuButton) 114 | } 115 | 116 | func setAppearanceMode(_ appearanceMode: Appearance) { 117 | self.appearanceMode = appearanceMode 118 | LifeDatabase.standard.set(appearanceMode: appearanceMode) 119 | sendUpdateMessage() 120 | } 121 | 122 | func setSquareSize(_ squareSize: SquareSize) { 123 | self.squareSize = squareSize 124 | LifeDatabase.standard.set(squareSize: squareSize) 125 | sendUpdateMessage() 126 | } 127 | 128 | func setAnimationSpeed(_ animationSpeed: AnimationSpeed) { 129 | self.animationSpeed = animationSpeed 130 | LifeDatabase.standard.set(animationSpeed: animationSpeed) 131 | sendUpdateMessage() 132 | } 133 | 134 | func setColor(_ color: SKColor, for colors: Colors) { 135 | switch colors { 136 | case .color1: 137 | color1 = color 138 | case .color2: 139 | color2 = color 140 | case .color3: 141 | color3 = color 142 | } 143 | LifeDatabase.standard.set(color, for: colors) 144 | 145 | if !usingPreset { 146 | selectedPresetTitle = "" 147 | LifeDatabase.standard.set(selectedPresetTitle: selectedPresetTitle) 148 | } 149 | 150 | sendUpdateMessage() 151 | } 152 | 153 | func sendUpdateMessage() { 154 | if !usingPreset { 155 | delegate?.updatedSettings() 156 | settingsDelegate?.updatedSettings() 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Shared/LifeNode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LifeNode.swift 3 | // Life Saver 4 | // 5 | // Created by Brad Root on 5/23/19. 6 | // Copyright © 2019 Brad Root. All rights reserved. 7 | // 8 | // This Source Code Form is subject to the terms of the Mozilla Public 9 | // License, v. 2.0. If a copy of the MPL was not distributed with this 10 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 11 | 12 | import SpriteKit 13 | 14 | let squareTexture = FileGrabber.shared.getSKTexture(named: "square") 15 | 16 | class LifeNode: SKSpriteNode { 17 | let relativePosition: CGPoint 18 | var alive: Bool 19 | var timeInState: Int = 0 20 | var aliveColor: SKColor 21 | var deadColor: SKColor 22 | var neighbors: [LifeNode] = [] 23 | 24 | init(relativePosition: CGPoint, alive: Bool, color: SKColor, size: CGSize) { 25 | self.relativePosition = relativePosition 26 | self.alive = alive 27 | aliveColor = color 28 | deadColor = color 29 | super.init(texture: squareTexture, color: aliveColor, size: size) 30 | isUserInteractionEnabled = false 31 | anchorPoint = CGPoint(x: 0, y: 0) 32 | colorBlendFactor = 1 33 | zPosition = 0 34 | } 35 | 36 | required init?(coder _: NSCoder) { 37 | fatalError("init(coder:) has not been implemented") 38 | } 39 | 40 | // MARK: - Lifecycle 41 | 42 | public func live(duration: TimeInterval) { 43 | if alive { 44 | timeInState += 1 45 | return 46 | } 47 | 48 | timeInState = 0 49 | alive = true 50 | 51 | if duration > 0 { 52 | removeAllActions() 53 | let fadeAction = SKAction.fadeAlpha(to: 1, duration: duration) 54 | let colorAction = SKAction.colorize(with: aliveColor, colorBlendFactor: 1, duration: duration) 55 | let actionGroup = SKAction.group([fadeAction, colorAction]) 56 | actionGroup.timingMode = .easeInEaseOut 57 | run(actionGroup) 58 | } else { 59 | alpha = 1 60 | color = aliveColor 61 | } 62 | } 63 | 64 | public func die(duration: TimeInterval, fade: Bool) { 65 | if !alive { 66 | timeInState += 1 67 | 68 | // 30 for slow modes... 120 for fast? 69 | if timeInState == 120, duration > 0, fade { 70 | removeAllActions() 71 | let fadeAction = SKAction.fadeAlpha(to: 0, duration: duration) 72 | let colorAction = SKAction.colorize(with: deadColor, colorBlendFactor: 1, duration: duration) 73 | let actionGroup = SKAction.group([fadeAction, colorAction]) 74 | actionGroup.timingMode = .easeIn 75 | run(actionGroup) 76 | } 77 | 78 | return 79 | } 80 | 81 | timeInState = 0 82 | alive = false 83 | 84 | if duration > 0, fade { 85 | removeAllActions() 86 | let fadeAction = SKAction.fadeAlpha(to: 0.2, duration: duration) 87 | fadeAction.timingMode = .easeInEaseOut 88 | run(fadeAction) 89 | } else if fade { 90 | alpha = 0.2 91 | } 92 | } 93 | 94 | public func remove(duration: TimeInterval) { 95 | removeAllActions() 96 | timeInState = 0 97 | alive = false 98 | 99 | let fadeAction = SKAction.fadeAlpha(to: 0, duration: duration) 100 | fadeAction.timingMode = .easeInEaseOut 101 | run(fadeAction) { 102 | self.removeFromParent() 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Shared/LifePreset.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LifePreset.swift 3 | // Life Saver Screensaver 4 | // 5 | // Created by Brad Root on 5/23/19. 6 | // Copyright © 2019 Brad Root. All rights reserved. 7 | // 8 | // This Source Code Form is subject to the terms of the Mozilla Public 9 | // License, v. 2.0. If a copy of the MPL was not distributed with this 10 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 11 | 12 | import SpriteKit 13 | 14 | struct LifePreset { 15 | let title: String 16 | let appearanceMode: Appearance? 17 | let squareSize: SquareSize? 18 | let animationSpeed: AnimationSpeed? 19 | let deathFade: Bool? 20 | let shiftingColors: Bool? 21 | let color1: SKColor? 22 | let color2: SKColor? 23 | let color3: SKColor? 24 | } 25 | 26 | enum Appearance: Int { 27 | case light = 0 28 | case dark = 1 29 | } 30 | 31 | enum SquareSize: Int { 32 | case superSmall = -2 33 | case verySmall = -1 34 | case small = 0 35 | case medium = 1 36 | case large = 2 37 | } 38 | 39 | enum AnimationSpeed: Int { 40 | case fast = 0 41 | case normal = 1 42 | case slow = 2 43 | case off = 3 44 | } 45 | 46 | enum Colors: Int { 47 | case color1 = 0 48 | case color2 = 1 49 | case color3 = 2 50 | } 51 | 52 | extension SKColor { 53 | static let defaultColor1 = SKColor(red: 172 / 255.0, green: 48 / 255.0, blue: 17 / 255.0, alpha: 1.00) 54 | static let defaultColor2 = SKColor(red: 6 / 255.0, green: 66 / 255.0, blue: 110 / 255.0, alpha: 1.00) 55 | static let defaultColor3 = SKColor(red: 174 / 255.0, green: 129 / 255.0, blue: 0 / 255.0, alpha: 1.00) 56 | } 57 | 58 | let settingsPresets = [ 59 | LifePreset( 60 | title: "Defaults", 61 | appearanceMode: .dark, 62 | squareSize: .medium, 63 | animationSpeed: .normal, 64 | deathFade: true, 65 | shiftingColors: false, 66 | color1: SKColor.defaultColor1, 67 | color2: SKColor.defaultColor2, 68 | color3: SKColor.defaultColor3 69 | ), 70 | LifePreset( 71 | title: "Meditation", 72 | appearanceMode: .light, 73 | squareSize: .large, 74 | animationSpeed: .normal, 75 | deathFade: false, 76 | shiftingColors: true, 77 | color1: SKColor(red: 237 / 255.0, green: 200 / 255.0, blue: 195 / 255.0, alpha: 1.00), 78 | color2: SKColor(red: 16 / 255.0, green: 103 / 255.0, blue: 110 / 255.0, alpha: 1.00), 79 | color3: SKColor(red: 247 / 255.0, green: 172 / 255.0, blue: 153 / 255.0, alpha: 1.00) 80 | ), 81 | LifePreset( 82 | title: "8-bit Fireplace", 83 | appearanceMode: .dark, 84 | squareSize: .medium, 85 | animationSpeed: .fast, 86 | deathFade: true, 87 | shiftingColors: false, 88 | color1: SKColor(red: 0.98, green: 0.75, blue: 0.00, alpha: 1.0), 89 | color2: SKColor(red: 1.00, green: 0.46, blue: 0.00, alpha: 1.0), 90 | color3: SKColor(red: 0.71, green: 0.13, blue: 0.01, alpha: 1.0) 91 | ), 92 | LifePreset( 93 | title: "Technicolor Dream", 94 | appearanceMode: .light, 95 | squareSize: .verySmall, 96 | animationSpeed: .off, 97 | deathFade: false, 98 | shiftingColors: true, 99 | color1: SKColor(red: 252 / 255.0, green: 98 / 255.0, blue: 101 / 255.0, alpha: 1.00), 100 | color2: SKColor(red: 88 / 255.0, green: 137 / 255.0, blue: 251 / 255.0, alpha: 1.00), 101 | color3: SKColor(red: 38 / 255.0, green: 205 / 255.0, blue: 105 / 255.0, alpha: 1.00) 102 | ), 103 | LifePreset( 104 | title: "Basic Life", 105 | appearanceMode: .dark, 106 | squareSize: .verySmall, 107 | animationSpeed: .off, 108 | deathFade: true, 109 | shiftingColors: false, 110 | color1: SKColor.white, 111 | color2: SKColor.gray, 112 | color3: SKColor.lightGray 113 | ), 114 | ] 115 | 116 | let colorPresets = [ 117 | LifePreset( 118 | title: "Santa Fe", 119 | appearanceMode: .dark, 120 | squareSize: nil, 121 | animationSpeed: nil, 122 | deathFade: nil, 123 | shiftingColors: nil, 124 | color1: SKColor.defaultColor1, 125 | color2: SKColor.defaultColor2, 126 | color3: SKColor.defaultColor3 127 | ), 128 | LifePreset( 129 | title: "Braineater", 130 | appearanceMode: .dark, 131 | squareSize: nil, 132 | animationSpeed: nil, 133 | deathFade: nil, 134 | shiftingColors: nil, 135 | color1: SKColor(red: 103 / 255.0, green: 22 / 255.0, blue: 169 / 255.0, alpha: 1.00), 136 | color2: SKColor(red: 13 / 255.0, green: 17 / 255.0, blue: 108 / 255.0, alpha: 1.00), 137 | color3: SKColor(red: 12 / 255.0, green: 67 / 255.0, blue: 108 / 255.0, alpha: 1.00) 138 | ), 139 | LifePreset( 140 | title: "Reign In Blood", 141 | appearanceMode: .dark, 142 | squareSize: nil, 143 | animationSpeed: nil, 144 | deathFade: nil, 145 | shiftingColors: nil, 146 | color1: SKColor(red: 113 / 255.0, green: 17 / 255.0, blue: 8 / 255.0, alpha: 1.00), 147 | color2: SKColor(red: 95 / 255.0, green: 7 / 255.0, blue: 0 / 255.0, alpha: 1.00), 148 | color3: SKColor(red: 55 / 255.0, green: 55 / 255.0, blue: 55 / 255.0, alpha: 1.00) 149 | ), 150 | LifePreset( 151 | title: "Swamp Girl", 152 | appearanceMode: .dark, 153 | squareSize: nil, 154 | animationSpeed: nil, 155 | deathFade: nil, 156 | shiftingColors: nil, 157 | color1: SKColor(red: 173 / 255.0, green: 255 / 255.0, blue: 14 / 255.0, alpha: 1.00), 158 | color2: SKColor(red: 174 / 255.0, green: 129 / 255.0, blue: 255 / 255.0, alpha: 1.00), 159 | color3: SKColor(red: 6 / 255.0, green: 66 / 255.0, blue: 110 / 255.0, alpha: 1.00) 160 | ), 161 | LifePreset( 162 | title: "This Is America", 163 | appearanceMode: .dark, 164 | squareSize: nil, 165 | animationSpeed: nil, 166 | deathFade: nil, 167 | shiftingColors: nil, 168 | color1: SKColor(red: 190 / 255.0, green: 14 / 255.0, blue: 19 / 255.0, alpha: 1.00), 169 | color2: SKColor(red: 39 / 255.0, green: 65 / 255.0, blue: 110 / 255.0, alpha: 1.00), 170 | color3: SKColor(red: 212 / 255.0, green: 205 / 255.0, blue: 196 / 255.0, alpha: 1.00) 171 | ), 172 | LifePreset( 173 | title: "The Noun Project", 174 | appearanceMode: .dark, 175 | squareSize: nil, 176 | animationSpeed: nil, 177 | deathFade: nil, 178 | shiftingColors: nil, 179 | color1: SKColor(red: 255 / 255.0, green: 255 / 255.0, blue: 255 / 255.0, alpha: 1.00), 180 | color2: SKColor(red: 255 / 255.0, green: 255 / 255.0, blue: 255 / 255.0, alpha: 1.00), 181 | color3: SKColor(red: 255 / 255.0, green: 255 / 255.0, blue: 255 / 255.0, alpha: 1.00) 182 | ), 183 | LifePreset( 184 | title: "Lingo", 185 | appearanceMode: .light, 186 | squareSize: nil, 187 | animationSpeed: nil, 188 | deathFade: nil, 189 | shiftingColors: nil, 190 | color1: SKColor(red: 252 / 255.0, green: 98 / 255.0, blue: 101 / 255.0, alpha: 1.00), 191 | color2: SKColor(red: 88 / 255.0, green: 137 / 255.0, blue: 251 / 255.0, alpha: 1.00), 192 | color3: SKColor(red: 38 / 255.0, green: 205 / 255.0, blue: 105 / 255.0, alpha: 1.00) 193 | ), 194 | LifePreset( 195 | title: "Deuteranopia", 196 | appearanceMode: .dark, 197 | squareSize: nil, 198 | animationSpeed: nil, 199 | deathFade: nil, 200 | shiftingColors: nil, 201 | color1: SKColor(red: 211 / 255.0, green: 208 / 255.0, blue: 203 / 255.0, alpha: 1.00), 202 | color2: SKColor(red: 88 / 255.0, green: 123 / 255.0, blue: 127 / 255.0, alpha: 1.00), 203 | color3: SKColor(red: 255 / 255.0, green: 173 / 255.0, blue: 105 / 255.0, alpha: 1.00) 204 | ), 205 | LifePreset( 206 | title: "Retro Pastel", 207 | appearanceMode: .light, 208 | squareSize: nil, 209 | animationSpeed: nil, 210 | deathFade: nil, 211 | shiftingColors: nil, 212 | color1: SKColor(red: 229 / 255.0, green: 167 / 255.0, blue: 177 / 255.0, alpha: 1.00), 213 | color2: SKColor(red: 244 / 255.0, green: 243 / 255.0, blue: 216 / 255.0, alpha: 1.00), 214 | color3: SKColor(red: 175 / 255.0, green: 211 / 255.0, blue: 213 / 255.0, alpha: 1.00) 215 | ), 216 | LifePreset( 217 | title: "Better Days", 218 | appearanceMode: .dark, 219 | squareSize: nil, 220 | animationSpeed: nil, 221 | deathFade: nil, 222 | shiftingColors: nil, 223 | color1: SKColor(red: 95 / 255.0, green: 67 / 255.0, blue: 107 / 255.0, alpha: 1.00), 224 | color2: SKColor(red: 205 / 255.0, green: 170 / 255.0, blue: 37 / 255.0, alpha: 1.00), 225 | color3: SKColor(red: 114 / 255.0, green: 100 / 255.0, blue: 87 / 255.0, alpha: 1.00) 226 | ), 227 | LifePreset( 228 | title: "Bubblegum", 229 | appearanceMode: .light, 230 | squareSize: nil, 231 | animationSpeed: nil, 232 | deathFade: nil, 233 | shiftingColors: nil, 234 | color1: SKColor(red: 1 / 255.0, green: 153 / 255.0, blue: 138 / 255.0, alpha: 1.00), 235 | color2: SKColor(red: 255 / 255.0, green: 203 / 255.0, blue: 213 / 255.0, alpha: 1.00), 236 | color3: SKColor(red: 191 / 255.0, green: 178 / 255.0, blue: 95 / 255.0, alpha: 1.00) 237 | ), 238 | LifePreset( 239 | title: "Teenage Chapstick", 240 | appearanceMode: .dark, 241 | squareSize: nil, 242 | animationSpeed: nil, 243 | deathFade: nil, 244 | shiftingColors: nil, 245 | color1: SKColor(red: 208 / 255.0, green: 117 / 255.0, blue: 126 / 255.0, alpha: 1.00), 246 | color2: SKColor(red: 44 / 255.0, green: 80 / 255.0, blue: 80 / 255.0, alpha: 1.00), 247 | color3: SKColor(red: 156 / 255.0, green: 154 / 255.0, blue: 23 / 255.0, alpha: 1.00) 248 | ), 249 | LifePreset( 250 | title: "Trial by Fire", 251 | appearanceMode: .dark, 252 | squareSize: nil, 253 | animationSpeed: nil, 254 | deathFade: nil, 255 | shiftingColors: nil, 256 | color1: SKColor(red: 0.98, green: 0.75, blue: 0.00, alpha: 1.0), 257 | color2: SKColor(red: 1.00, green: 0.46, blue: 0.00, alpha: 1.0), 258 | color3: SKColor(red: 0.71, green: 0.13, blue: 0.01, alpha: 1.0) 259 | ), 260 | LifePreset( 261 | title: "Georgia", 262 | appearanceMode: .light, 263 | squareSize: nil, 264 | animationSpeed: nil, 265 | deathFade: nil, 266 | shiftingColors: nil, 267 | color1: SKColor(red: 237 / 255.0, green: 200 / 255.0, blue: 195 / 255.0, alpha: 1.00), 268 | color2: SKColor(red: 16 / 255.0, green: 103 / 255.0, blue: 110 / 255.0, alpha: 1.00), 269 | color3: SKColor(red: 247 / 255.0, green: 172 / 255.0, blue: 153 / 255.0, alpha: 1.00) 270 | ), 271 | LifePreset( 272 | title: "Boysenberry", 273 | appearanceMode: .dark, 274 | squareSize: nil, 275 | animationSpeed: nil, 276 | deathFade: nil, 277 | shiftingColors: nil, 278 | color1: SKColor(red: 122 / 255.0, green: 55 / 255.0, blue: 100 / 255.0, alpha: 1.00), 279 | color2: SKColor(red: 56 / 255.0, green: 66 / 255.0, blue: 109 / 255.0, alpha: 1.00), 280 | color3: SKColor(red: 160 / 255.0, green: 121 / 255.0, blue: 72 / 255.0, alpha: 1.00) 281 | ), 282 | LifePreset( 283 | title: "Tal Véz", 284 | appearanceMode: .dark, 285 | squareSize: nil, 286 | animationSpeed: nil, 287 | deathFade: nil, 288 | shiftingColors: nil, 289 | color1: SKColor(red: 59 / 255.0, green: 67 / 255.0, blue: 72 / 255.0, alpha: 1.00), 290 | color2: SKColor(red: 247 / 255.0, green: 201 / 255.0, blue: 177 / 255.0, alpha: 1.00), 291 | color3: SKColor(red: 176 / 255.0, green: 197 / 255.0, blue: 223 / 255.0, alpha: 1.00) 292 | ), 293 | LifePreset( 294 | title: "Deep Forest", 295 | appearanceMode: .dark, 296 | squareSize: nil, 297 | animationSpeed: nil, 298 | deathFade: nil, 299 | shiftingColors: nil, 300 | color1: SKColor(red: 36 / 255.0, green: 55 / 255.0, blue: 13 / 255.0, alpha: 1.00), 301 | color2: SKColor(red: 55 / 255.0, green: 60 / 255.0, blue: 13 / 255.0, alpha: 1.00), 302 | color3: SKColor(red: 10 / 255.0, green: 44 / 255.0, blue: 24 / 255.0, alpha: 1.00) 303 | ), 304 | LifePreset( 305 | title: "Bite Me", 306 | appearanceMode: .dark, 307 | squareSize: nil, 308 | animationSpeed: nil, 309 | deathFade: nil, 310 | shiftingColors: nil, 311 | color1: SKColor(red: 243 / 255.0, green: 136 / 255.0, blue: 103 / 255.0, alpha: 1.00), 312 | color2: SKColor(red: 241 / 255.0, green: 188 / 255.0, blue: 151 / 255.0, alpha: 1.00), 313 | color3: SKColor(red: 252 / 255.0, green: 53 / 255.0, blue: 113 / 255.0, alpha: 1.00) 314 | ), 315 | LifePreset( 316 | title: "Monochrome Red", 317 | appearanceMode: .dark, 318 | squareSize: nil, 319 | animationSpeed: nil, 320 | deathFade: nil, 321 | shiftingColors: nil, 322 | color1: SKColor(red: 229 / 255, green: 0 / 255, blue: 0 / 255, alpha: 1.0), 323 | color2: SKColor(red: 204 / 255, green: 0 / 255, blue: 0 / 255, alpha: 1.0), 324 | color3: SKColor(red: 178 / 255, green: 0 / 255, blue: 0 / 255, alpha: 1.0) 325 | ), 326 | LifePreset( 327 | title: "Custom", 328 | appearanceMode: nil, 329 | squareSize: nil, 330 | animationSpeed: nil, 331 | deathFade: nil, 332 | shiftingColors: nil, 333 | color1: nil, 334 | color2: nil, 335 | color3: nil 336 | ), 337 | ] 338 | -------------------------------------------------------------------------------- /Shared/LifeScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LifeScene.swift 3 | // Life Saver 4 | // 5 | // Created by Bradley Root on 5/18/19. 6 | // Copyright © 2019 Brad Root. All rights reserved. 7 | // 8 | // This Source Code Form is subject to the terms of the Mozilla Public 9 | // License, v. 2.0. If a copy of the MPL was not distributed with this 10 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 11 | 12 | import GameplayKit 13 | import SpriteKit 14 | 15 | final class LifeScene: SKScene, LifeManagerDelegate { 16 | // MARK: - Settings 17 | 18 | var aliveColors: [SKColor] = [ 19 | SKColor.defaultColor1, 20 | SKColor.defaultColor2, 21 | SKColor.defaultColor3, 22 | ] 23 | 24 | var appearanceColor: SKColor = .black 25 | var appearanceMode: Appearance = .dark { 26 | didSet { 27 | switch appearanceMode { 28 | case .dark: 29 | appearanceColor = .black 30 | case .light: 31 | appearanceColor = .white 32 | } 33 | } 34 | } 35 | 36 | var deathFade: Bool = true 37 | var shiftingColors: Bool = false 38 | 39 | private var animationTime: TimeInterval = 2 40 | private var updateTime: TimeInterval = 2 41 | var animationSpeed: AnimationSpeed = .normal { 42 | didSet { 43 | switch animationSpeed { 44 | case .fast: 45 | animationTime = 0.6 46 | updateTime = 0.6 47 | case .normal: 48 | animationTime = 2 49 | updateTime = 2 50 | case .slow: 51 | animationTime = 5 52 | updateTime = 5 53 | case .off: 54 | animationTime = 0 55 | updateTime = 0.1 56 | } 57 | } 58 | } 59 | 60 | var squareSize: SquareSize = .medium 61 | 62 | // MARK: - Manager 63 | 64 | var manager: LifeManager? { 65 | didSet { 66 | manager?.delegate = self 67 | } 68 | } 69 | 70 | func updatedSettings() { 71 | print("Updated Settings") 72 | isUpdating = false 73 | endLife() 74 | perform(#selector(createField), with: nil, afterDelay: 0.5) 75 | } 76 | 77 | // MARK: - Scene Lifecycle 78 | 79 | override func sceneDidLoad() { 80 | size.width = frame.size.width * 2 81 | size.height = frame.size.height * 2 82 | backgroundColor = .black 83 | } 84 | 85 | override func didMove(to _: SKView) { 86 | backgroundNode = SKSpriteNode(texture: squareTexture, color: appearanceColor, size: frame.size) 87 | backgroundNode.alpha = 0 88 | addChild(backgroundNode) 89 | backgroundNode.position = CGPoint(x: frame.width / 2, y: frame.height / 2) 90 | backgroundNode.zPosition = 0 91 | 92 | let fadeIn = SKAction.fadeIn(withDuration: 0.5) 93 | backgroundNode.run(fadeIn) 94 | 95 | createField() 96 | } 97 | 98 | private var lastUpdate: TimeInterval = 0 99 | 100 | private var isUpdating: Bool = true 101 | 102 | override func update(_ currentTime: TimeInterval) { 103 | if lastUpdate == 0 || currentTime - lastUpdate >= updateTime { 104 | lastUpdate = currentTime 105 | if isUpdating { 106 | updateLife() 107 | } 108 | } 109 | } 110 | 111 | // MARK: - Life Parameters 112 | 113 | private var backgroundNode: SKSpriteNode = SKSpriteNode() 114 | private var allNodes: [LifeNode] = [] 115 | private var aliveNodes: [LifeNode] = [] 116 | private var livingNodeHistory: [Int] = [] 117 | private var lengthSquares: CGFloat = 16 118 | private var heightSquares: CGFloat = 9 119 | private var matrix: ToroidalMatrix = ToroidalMatrix( 120 | rows: 0, 121 | columns: 0, 122 | defaultValue: LifeNode( 123 | relativePosition: .zero, 124 | alive: false, 125 | color: .black, 126 | size: .zero 127 | ) 128 | ) 129 | 130 | // MARK: - Life Creation 131 | 132 | @objc func createField() { 133 | if let manager = manager { 134 | appearanceMode = manager.appearanceMode 135 | squareSize = manager.squareSize 136 | animationSpeed = manager.animationSpeed 137 | aliveColors = [manager.color1, manager.color2, manager.color3] 138 | deathFade = manager.deathFade 139 | shiftingColors = manager.shiftingColors 140 | } 141 | 142 | if backgroundNode.color != appearanceColor { 143 | let colorize = SKAction.colorize(with: appearanceColor, colorBlendFactor: 1.0, duration: 0.5) 144 | backgroundNode.run(colorize) { 145 | self.isUpdating = true 146 | } 147 | } 148 | 149 | scaleMode = .aspectFill 150 | 151 | switch squareSize { 152 | case .large: 153 | lengthSquares = 7 154 | heightSquares = 4 155 | case .medium: 156 | lengthSquares = 16 157 | heightSquares = 9 158 | case .small: 159 | lengthSquares = 32 160 | heightSquares = 18 161 | case .verySmall: 162 | lengthSquares = 64 163 | heightSquares = 36 164 | case .superSmall: 165 | lengthSquares = 128 166 | heightSquares = 74 167 | } 168 | 169 | createLife() 170 | } 171 | 172 | fileprivate func createLife() { 173 | matrix = ToroidalMatrix( 174 | rows: Int(lengthSquares), 175 | columns: Int(heightSquares), 176 | defaultValue: LifeNode( 177 | relativePosition: .zero, 178 | alive: false, 179 | color: .black, 180 | size: .zero 181 | ) 182 | ) 183 | 184 | let totalSquares: CGFloat = lengthSquares * heightSquares 185 | let squareWidth: CGFloat = size.width / lengthSquares 186 | let squareHeight: CGFloat = size.height / heightSquares 187 | 188 | // Create Nodes 189 | var nextXValue: Int = 0 190 | var nextYValue: Int = 0 191 | var nextXPosition: CGFloat = 0 192 | var nextYPosition: CGFloat = 0 193 | for _ in 1 ... Int(totalSquares) { 194 | let actualPosition = CGPoint(x: nextXPosition, y: nextYPosition) 195 | let relativePosition = CGPoint(x: nextXValue, y: nextYValue) 196 | let squareSize = CGSize(width: squareWidth, height: squareHeight) 197 | 198 | createLifeSquare(relativePosition, squareSize, actualPosition) 199 | 200 | if nextXValue == Int(lengthSquares) - 1 { 201 | nextXValue = 0 202 | nextXPosition = 0 203 | nextYValue += 1 204 | nextYPosition += squareHeight 205 | } else { 206 | nextXValue += 1 207 | nextXPosition += squareWidth 208 | } 209 | } 210 | 211 | // Pre-fetch Neighbors 212 | for node in allNodes { 213 | createNeighbors(node) 214 | } 215 | } 216 | 217 | fileprivate func createLifeSquare(_ relativePosition: CGPoint, _ squareSize: CGSize, _ actualPosition: CGPoint) { 218 | let newSquare = LifeNode( 219 | relativePosition: relativePosition, 220 | alive: false, 221 | color: appearanceColor, 222 | size: squareSize 223 | ) 224 | addChild(newSquare) 225 | newSquare.position = actualPosition 226 | newSquare.alpha = 0 227 | 228 | if newSquare.alive { 229 | aliveNodes.append(newSquare) 230 | newSquare.color = aliveColors.randomElement()! 231 | } 232 | allNodes.append(newSquare) 233 | matrix[Int(relativePosition.x), Int(relativePosition.y)] = newSquare 234 | } 235 | 236 | fileprivate func createNeighbors(_ node: LifeNode) { 237 | var neighbors: [LifeNode] = [] 238 | neighbors.append(matrix[Int(node.relativePosition.x - 1), Int(node.relativePosition.y)]) 239 | neighbors.append(matrix[Int(node.relativePosition.x + 1), Int(node.relativePosition.y)]) 240 | neighbors.append(matrix[Int(node.relativePosition.x), Int(node.relativePosition.y + 1)]) 241 | neighbors.append(matrix[Int(node.relativePosition.x), Int(node.relativePosition.y - 1)]) 242 | neighbors.append(matrix[Int(node.relativePosition.x + 1), Int(node.relativePosition.y + 1)]) 243 | neighbors.append(matrix[Int(node.relativePosition.x - 1), Int(node.relativePosition.y - 1)]) 244 | neighbors.append(matrix[Int(node.relativePosition.x - 1), Int(node.relativePosition.y + 1)]) 245 | neighbors.append(matrix[Int(node.relativePosition.x + 1), Int(node.relativePosition.y - 1)]) 246 | node.neighbors = neighbors 247 | } 248 | 249 | // MARK: - Life Ending 250 | 251 | fileprivate func destroyField() { 252 | removeAllChildren() 253 | } 254 | 255 | fileprivate func endLife() { 256 | allNodes.forEach { $0.remove(duration: 0.5) } 257 | allNodes.removeAll() 258 | aliveNodes.removeAll() 259 | livingNodeHistory.removeAll() 260 | } 261 | 262 | // MARK: - Life Updates 263 | 264 | fileprivate func updateLife() { 265 | var dyingNodes: [LifeNode] = [] 266 | var livingNodes: [LifeNode] = [] 267 | for node in allNodes { 268 | // Get living neighbors... 269 | let livingNeighbors = node.neighbors.filter { $0.alive } 270 | 271 | if node.alive { 272 | if livingNeighbors.count > 3 || livingNeighbors.count < 2 { 273 | dyingNodes.append(node) 274 | } else { 275 | livingNodes.append(node) 276 | } 277 | } else if livingNeighbors.count == 3 { 278 | var livingColor = livingNeighbors.randomElement()!.color 279 | #if os(tvOS) 280 | if shiftingColors { 281 | livingColor = livingColor.modified(withAdditionalHue: 0.005, additionalSaturation: 0, additionalBrightness: 0) 282 | } 283 | #endif 284 | node.aliveColor = livingColor 285 | livingNodes.append(node) 286 | } else { 287 | dyingNodes.append(node) 288 | } 289 | } 290 | 291 | // If entire tank is dead, generate a new tank! 292 | if CGFloat(livingNodes.count) == 0 { 293 | createRandomShapes(&dyingNodes, &livingNodes) 294 | } 295 | 296 | // Static tank prevention 297 | if livingNodeHistory.count >= 10 { 298 | livingNodeHistory.removeFirst() 299 | livingNodeHistory.append(livingNodes.count) 300 | if 1 ... 2 ~= Set(livingNodeHistory).count { 301 | dyingNodes.append(contentsOf: livingNodes) 302 | livingNodes.removeAll() 303 | } 304 | } else { 305 | livingNodeHistory.append(livingNodes.count) 306 | } 307 | 308 | // Update nodes here 309 | dyingNodes.forEach { 310 | $0.die(duration: animationTime * 5, fade: deathFade) 311 | } 312 | 313 | livingNodes.forEach { 314 | $0.live(duration: animationTime) 315 | } 316 | 317 | aliveNodes = livingNodes 318 | } 319 | 320 | fileprivate func createRandomShapes(_: inout [LifeNode], _ livingNodes: inout [LifeNode]) { 321 | var totalShapes: Int = 0 322 | switch squareSize { 323 | case .superSmall: 324 | totalShapes = 500 325 | case .verySmall: 326 | totalShapes = 50 327 | case .small: 328 | totalShapes = 20 329 | case .medium: 330 | totalShapes = 10 331 | case .large: 332 | totalShapes = 4 333 | } 334 | for _ in 1 ... totalShapes { 335 | let nodeNumber = GKRandomSource.sharedRandom().nextInt(upperBound: allNodes.count) 336 | let color = aliveColors.randomElement()! 337 | let node = allNodes[nodeNumber] 338 | for neighborNode in node.neighbors where Int.random(in: 0 ... 1) == 1 { 339 | neighborNode.aliveColor = color 340 | livingNodes.append(neighborNode) 341 | } 342 | } 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /Shared/Utilities/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // Life Saver 4 | // 5 | // Created by Bradley Root on 6/29/19. 6 | // Copyright © 2019 Brad Root. All rights reserved. 7 | // 8 | 9 | import SpriteKit 10 | 11 | extension SKColor { 12 | func modified(withAdditionalHue hue: CGFloat, additionalSaturation: CGFloat, additionalBrightness: CGFloat) -> SKColor { 13 | var currentHue: CGFloat = 0.0 14 | var currentSaturation: CGFloat = 0.0 15 | var currentBrigthness: CGFloat = 0.0 16 | var currentAlpha: CGFloat = 0.0 17 | 18 | if getHue(¤tHue, saturation: ¤tSaturation, brightness: ¤tBrigthness, alpha: ¤tAlpha) { 19 | return SKColor( 20 | hue: currentHue + hue > 1 ? hue : currentHue + hue, 21 | saturation: currentSaturation + additionalSaturation, 22 | brightness: currentBrigthness + additionalBrightness, 23 | alpha: currentAlpha 24 | ) 25 | } else { 26 | return self 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Shared/Utilities/FileGrabber.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileGrabber.swift 3 | // Life Saver 4 | // 5 | // Created by Brad Root on 5/21/19. 6 | // Copyright © 2019 Brad Root. All rights reserved. 7 | // 8 | // This Source Code Form is subject to the terms of the Mozilla Public 9 | // License, v. 2.0. If a copy of the MPL was not distributed with this 10 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 11 | 12 | import SpriteKit 13 | 14 | #if os(macOS) 15 | import Cocoa 16 | 17 | // Step 1: Typealias UIImage to NSImage 18 | typealias UIImage = NSImage 19 | #endif 20 | 21 | class FileGrabber { 22 | let bundle: Bundle 23 | 24 | static let shared: FileGrabber = FileGrabber() 25 | 26 | init() { 27 | let bundle = Bundle(for: LifeScene.self) 28 | self.bundle = bundle 29 | } 30 | 31 | public func getSKTexture(named: String) -> SKTexture? { 32 | #if os(macOS) 33 | guard let image = bundle.image(forResource: named) else { return nil } 34 | #else 35 | guard let image = UIImage(named: named, in: bundle, compatibleWith: nil) else { return nil } 36 | #endif 37 | return SKTexture(image: image) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Shared/Utilities/ToroidalMatrix.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToroidalMatrix.swift 3 | // Derived from dimo hamdy https://stackoverflow.com/a/53421491/2117288 4 | // https://gist.github.com/amiantos/bb0f313da1ee686f4f69b8b44f3cd184 5 | // 6 | // This Source Code Form is subject to the terms of the Mozilla Public 7 | // License, v. 2.0. If a copy of the MPL was not distributed with this 8 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 9 | 10 | struct ToroidalMatrix { 11 | let rows: Int, columns: Int 12 | var grid: [T] 13 | 14 | init(rows: Int, columns: Int, defaultValue: T) { 15 | self.rows = rows 16 | self.columns = columns 17 | grid = Array(repeating: defaultValue, count: rows * columns) 18 | } 19 | 20 | func indexIsValid(row: Int, column: Int) -> Bool { 21 | return row >= 0 && row < rows && column >= 0 && column < columns 22 | } 23 | 24 | subscript(row: Int, column: Int) -> T { 25 | get { 26 | let safeRow = 0 ... rows - 1 ~= row ? row : row > rows - 1 ? 0 : row < 0 ? rows - 1 : -1 27 | let safeColumn = 0 ... columns - 1 ~= column ? column : column > columns - 1 ? 0 : column < 0 ? columns - 1 : -1 28 | assert(indexIsValid(row: safeRow, column: safeColumn), "Index out of range") 29 | return grid[(safeRow * columns) + safeColumn] 30 | } 31 | 32 | set { 33 | assert(indexIsValid(row: row, column: column), "Index out of range") 34 | grid[(row * columns) + column] = newValue 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Shared/Utilities/URLType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLType.swift 3 | // Life Saver Screensaver 4 | // 5 | // Created by Brad Root on 5/22/19. 6 | // Copyright © 2019 Brad Root. All rights reserved. 7 | // 8 | // This Source Code Form is subject to the terms of the Mozilla Public 9 | // License, v. 2.0. If a copy of the MPL was not distributed with this 10 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 11 | 12 | import Cocoa 13 | 14 | enum URLType: String { 15 | case brad = "https://amiantos.net" 16 | case github = "https://github.com/amiantos/lifesaver" 17 | case twitter = "https://twitter.com/amiantos" 18 | case website = "https://amiantos.net/lifesaver" 19 | } 20 | 21 | extension URLType { 22 | func open() { 23 | guard let url = URL(string: rawValue) else { return } 24 | NSWorkspace.shared.open(url) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /macOS Screensaver/Configuration/ConfigureSheetController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigureSheetController.swift 3 | // Life Saver Screensaver 4 | // 5 | // Created by Brad Root on 5/21/19. 6 | // Copyright © 2019 Brad Root. All rights reserved. 7 | // 8 | // This Source Code Form is subject to the terms of the Mozilla Public 9 | // License, v. 2.0. If a copy of the MPL was not distributed with this 10 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 11 | 12 | import Cocoa 13 | import SpriteKit 14 | 15 | final class ConfigureSheetController: NSObject { 16 | private let manager = LifeManager() 17 | 18 | // MARK: - Presets 19 | 20 | fileprivate let presets: [LifePreset] = colorPresets 21 | 22 | // MARK: - Config Actions and Outlets 23 | 24 | @IBOutlet var window: NSWindow? 25 | 26 | @IBOutlet var stylePresetsButton: NSSegmentedControl! 27 | @IBAction func stylePresetsAction(_ sender: NSSegmentedControl) { 28 | switch sender.selectedSegment { 29 | case 0: 30 | let simulationSettings = LifePreset( 31 | title: "Simulation", 32 | appearanceMode: nil, 33 | squareSize: .small, 34 | animationSpeed: .fast, 35 | deathFade: nil, 36 | shiftingColors: nil, 37 | color1: nil, 38 | color2: nil, 39 | color3: nil 40 | ) 41 | loadPreset(simulationSettings) 42 | case 2: 43 | let abstractSettings = LifePreset( 44 | title: "Abstract", 45 | appearanceMode: nil, 46 | squareSize: .large, 47 | animationSpeed: .slow, 48 | deathFade: nil, 49 | shiftingColors: nil, 50 | color1: nil, 51 | color2: nil, 52 | color3: nil 53 | ) 54 | loadPreset(abstractSettings) 55 | default: 56 | let defaultSettings = LifePreset( 57 | title: "Defaults", 58 | appearanceMode: nil, 59 | squareSize: .medium, 60 | animationSpeed: .normal, 61 | deathFade: nil, 62 | shiftingColors: nil, 63 | color1: nil, 64 | color2: nil, 65 | color3: nil 66 | ) 67 | loadPreset(defaultSettings) 68 | } 69 | } 70 | 71 | @IBOutlet var presetsButton: NSPopUpButton! 72 | @IBAction func presetsAction(_ sender: NSPopUpButton) { 73 | guard let title = sender.titleOfSelectedItem else { return } 74 | let soughtPreset = presets.filter { $0.title == title }.first 75 | if let preset = soughtPreset { 76 | loadPreset(preset) 77 | } 78 | } 79 | 80 | @IBOutlet var appearanceControl: NSSegmentedControl! 81 | @IBAction func appearanceAction(_ sender: NSSegmentedControl) { 82 | switch sender.selectedSegment { 83 | case 1: 84 | manager.setAppearanceMode(Appearance.light) 85 | default: 86 | manager.setAppearanceMode(Appearance.dark) 87 | } 88 | updateColorPresetsControl() 89 | } 90 | 91 | @IBOutlet var squareSizeControl: NSSegmentedControl! 92 | @IBAction func squareSizeAction(_ sender: NSSegmentedControl) { 93 | switch sender.selectedSegment { 94 | case 0: 95 | manager.setSquareSize(.superSmall) 96 | case 1: 97 | manager.setSquareSize(.verySmall) 98 | case 2: 99 | manager.setSquareSize(.small) 100 | case 3: 101 | manager.setSquareSize(.medium) 102 | case 4: 103 | manager.setSquareSize(.large) 104 | default: 105 | manager.setSquareSize(.medium) 106 | } 107 | updateStylePresetsControl() 108 | } 109 | 110 | @IBOutlet var animationSpeedControl: NSSegmentedControl! 111 | @IBAction func animationSpeedAction(_ sender: NSSegmentedControl) { 112 | switch sender.selectedSegment { 113 | case 0: 114 | manager.setAnimationSpeed(.fast) 115 | case 2: 116 | manager.setAnimationSpeed(.slow) 117 | default: 118 | manager.setAnimationSpeed(.normal) 119 | } 120 | updateStylePresetsControl() 121 | } 122 | 123 | @IBOutlet var color1Well: NSColorWell! 124 | @IBAction func color1Action(_ sender: NSColorWell) { 125 | manager.setColor(sender.color as SKColor, for: .color1) 126 | updateColorPresetsControl() 127 | } 128 | 129 | @IBOutlet var color2Well: NSColorWell! 130 | @IBAction func color2Action(_ sender: NSColorWell) { 131 | manager.setColor(sender.color as SKColor, for: .color2) 132 | updateColorPresetsControl() 133 | } 134 | 135 | @IBOutlet var color3Well: NSColorWell! 136 | @IBAction func color3Action(_ sender: NSColorWell) { 137 | manager.setColor(sender.color as SKColor, for: .color3) 138 | updateColorPresetsControl() 139 | } 140 | 141 | @IBOutlet var randomColorPresetCheck: NSButton! 142 | @IBAction func randomColorPresetAction(_ sender: NSButtonCell) { 143 | manager.setRandomColorPreset(sender.state == .on ? true : false) 144 | updateColorPresetsControl() 145 | } 146 | 147 | @IBAction func twitterAction(_: NSButton) { 148 | URLType.twitter.open() 149 | } 150 | 151 | @IBAction func gitHubAction(_: NSButton) { 152 | URLType.github.open() 153 | } 154 | 155 | @IBAction func bradAction(_: NSButton) { 156 | URLType.brad.open() 157 | } 158 | 159 | @IBAction func websiteAction(_: NSButton) { 160 | URLType.website.open() 161 | } 162 | 163 | @IBAction func closeConfigureSheet(sender _: AnyObject) { 164 | guard let window = window else { return } 165 | window.sheetParent?.endSheet(window) 166 | } 167 | 168 | // MARK: - View Setup 169 | 170 | override init() { 171 | super.init() 172 | let myBundle = Bundle(for: ConfigureSheetController.self) 173 | myBundle.loadNibNamed("ConfigureSheet", owner: self, topLevelObjects: nil) 174 | 175 | randomColorPresetCheck.toolTip = "Enable this to have a random color preset selected each time the screensaver loads." 176 | 177 | loadPresets() 178 | loadSettings() 179 | } 180 | 181 | fileprivate func loadSettings() { 182 | switch manager.appearanceMode { 183 | case .dark: 184 | appearanceControl.selectedSegment = 0 185 | case .light: 186 | appearanceControl.selectedSegment = 1 187 | } 188 | 189 | switch manager.squareSize { 190 | case .superSmall: 191 | squareSizeControl.selectedSegment = 0 192 | case .verySmall: 193 | squareSizeControl.selectedSegment = 1 194 | case .small: 195 | squareSizeControl.selectedSegment = 2 196 | case .medium: 197 | squareSizeControl.selectedSegment = 3 198 | case .large: 199 | squareSizeControl.selectedSegment = 4 200 | } 201 | 202 | switch manager.animationSpeed { 203 | case .normal: 204 | animationSpeedControl.selectedSegment = 1 205 | case .fast: 206 | animationSpeedControl.selectedSegment = 0 207 | case .slow: 208 | animationSpeedControl.selectedSegment = 2 209 | case .off: 210 | animationSpeedControl.selectedSegment = 1 211 | } 212 | 213 | color1Well.color = manager.color1 214 | color2Well.color = manager.color2 215 | color3Well.color = manager.color3 216 | 217 | randomColorPresetCheck.state = manager.randomColorPreset ? .on : .off 218 | 219 | updateStylePresetsControl() 220 | updateColorPresetsControl() 221 | } 222 | 223 | fileprivate func loadPresets() { 224 | presetsButton.removeAllItems() 225 | var presetTitles: [String] = [] 226 | for preset in presets { 227 | presetTitles.append(preset.title) 228 | } 229 | presetsButton.addItems(withTitles: presetTitles) 230 | } 231 | 232 | fileprivate func updateStylePresetsControl() { 233 | if manager.animationSpeed == .fast, manager.squareSize == .small { 234 | stylePresetsButton.selectSegment(withTag: 0) 235 | } else if manager.animationSpeed == .normal, manager.squareSize == .medium { 236 | stylePresetsButton.selectSegment(withTag: 1) 237 | } else if manager.animationSpeed == .slow, manager.squareSize == .large { 238 | stylePresetsButton.selectSegment(withTag: 2) 239 | } else { 240 | stylePresetsButton.setSelected(false, forSegment: stylePresetsButton.selectedSegment) 241 | } 242 | } 243 | 244 | fileprivate func updateColorPresetsControl() { 245 | let filteredPresets = presets.filter { $0.color1 == manager.color1 && $0.color2 == manager.color2 && $0.color3 == manager.color3 } 246 | presetsButton.selectItem(withTitle: filteredPresets.first?.title ?? "Custom") 247 | 248 | presetsButton.isEnabled = !manager.randomColorPreset 249 | color1Well.isEnabled = !manager.randomColorPreset 250 | color2Well.isEnabled = !manager.randomColorPreset 251 | color3Well.isEnabled = !manager.randomColorPreset 252 | appearanceControl.isEnabled = !manager.randomColorPreset 253 | } 254 | 255 | fileprivate func loadPreset(_ preset: LifePreset) { 256 | manager.configure(with: preset) 257 | loadSettings() 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /macOS Screensaver/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 | Life Saver 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2019 Brad Root. All rights reserved. 23 | NSPrincipalClass 24 | Life_Saver_Screensaver.LifeScreenSaverView 25 | 26 | 27 | -------------------------------------------------------------------------------- /macOS Screensaver/LifeDatabase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LifeDatabase.swift 3 | // Life Saver 4 | // 5 | // Created by Brad Root on 5/21/19. 6 | // Copyright © 2019 Brad Root. All rights reserved. 7 | // 8 | // This Source Code Form is subject to the terms of the Mozilla Public 9 | // License, v. 2.0. If a copy of the MPL was not distributed with this 10 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 11 | 12 | import ScreenSaver 13 | import SpriteKit 14 | 15 | struct LifeDatabase { 16 | fileprivate enum Key { 17 | static let appearanceMode = "appearanceMode" 18 | static let squareSize = "squareSize" 19 | static let blurAmount = "blurAmount" 20 | static let animationSpeed = "animationSpeed" 21 | static let color1 = "color1" 22 | static let color2 = "color2" 23 | static let color3 = "color3" 24 | static let randomColorPreset = "randomColorPreset" 25 | static let selectedPresetTitle = "selectedPresetTitle" 26 | static let deathFade = "deathFade" 27 | static let shiftingColors = "shiftingColors" 28 | static let hasPressedMenuButton = "hasPressedMenuButton" 29 | } 30 | 31 | static var standard: ScreenSaverDefaults { 32 | guard let bundleIdentifier = Bundle(for: LifeManager.self).bundleIdentifier, 33 | let database = ScreenSaverDefaults(forModuleWithName: bundleIdentifier) 34 | else { fatalError("Failed to retrieve database") } 35 | 36 | database.register(defaults: 37 | [Key.appearanceMode: Appearance.dark.rawValue, 38 | Key.animationSpeed: AnimationSpeed.normal.rawValue, 39 | Key.squareSize: SquareSize.medium.rawValue, 40 | Key.color1: archiveData(SKColor.defaultColor1), 41 | Key.color2: archiveData(SKColor.defaultColor2), 42 | Key.color3: archiveData(SKColor.defaultColor3), 43 | Key.randomColorPreset: false, 44 | Key.deathFade: true, 45 | Key.shiftingColors: false, 46 | Key.selectedPresetTitle: "Santa Fe", 47 | Key.hasPressedMenuButton: false]) 48 | 49 | return database 50 | } 51 | } 52 | 53 | extension ScreenSaverDefaults { 54 | var appearanceMode: Appearance { 55 | return Appearance(rawValue: integer(forKey: LifeDatabase.Key.appearanceMode))! 56 | } 57 | 58 | func set(appearanceMode: Appearance) { 59 | set(appearanceMode.rawValue, for: LifeDatabase.Key.appearanceMode) 60 | } 61 | 62 | var squareSize: SquareSize { 63 | return SquareSize(rawValue: integer(forKey: LifeDatabase.Key.squareSize))! 64 | } 65 | 66 | func set(squareSize: SquareSize) { 67 | set(squareSize.rawValue, for: LifeDatabase.Key.squareSize) 68 | } 69 | 70 | var animationSpeed: AnimationSpeed { 71 | return AnimationSpeed(rawValue: integer(forKey: LifeDatabase.Key.animationSpeed))! 72 | } 73 | 74 | func set(animationSpeed: AnimationSpeed) { 75 | set(animationSpeed.rawValue, for: LifeDatabase.Key.animationSpeed) 76 | } 77 | 78 | var randomColorPreset: Bool { 79 | return bool(forKey: LifeDatabase.Key.randomColorPreset) 80 | } 81 | 82 | func set(randomColorPreset: Bool) { 83 | set(randomColorPreset, for: LifeDatabase.Key.randomColorPreset) 84 | } 85 | 86 | var shiftingColors: Bool { 87 | return bool(forKey: LifeDatabase.Key.shiftingColors) 88 | } 89 | 90 | func set(shiftingColors: Bool) { 91 | set(shiftingColors, for: LifeDatabase.Key.shiftingColors) 92 | } 93 | 94 | var deathFade: Bool { 95 | return bool(forKey: LifeDatabase.Key.deathFade) 96 | } 97 | 98 | func set(deathFade: Bool) { 99 | set(deathFade, for: LifeDatabase.Key.deathFade) 100 | } 101 | 102 | var hasPressedMenuButton: Bool { 103 | return bool(forKey: LifeDatabase.Key.hasPressedMenuButton) 104 | } 105 | 106 | func set(hasPressedMenuButton: Bool) { 107 | set(hasPressedMenuButton, for: LifeDatabase.Key.hasPressedMenuButton) 108 | } 109 | 110 | var selectedPresetTitle: String { 111 | return string(forKey: LifeDatabase.Key.selectedPresetTitle) ?? "" 112 | } 113 | 114 | func set(selectedPresetTitle: String) { 115 | set(selectedPresetTitle, for: LifeDatabase.Key.selectedPresetTitle) 116 | } 117 | 118 | func getColor(_ color: Colors) -> SKColor { 119 | switch color { 120 | case .color1: 121 | return unarchiveColor(data(forKey: LifeDatabase.Key.color1)!) 122 | case .color2: 123 | return unarchiveColor(data(forKey: LifeDatabase.Key.color2)!) 124 | case .color3: 125 | return unarchiveColor(data(forKey: LifeDatabase.Key.color3)!) 126 | } 127 | } 128 | 129 | func set(_ color: SKColor, for colors: Colors) { 130 | switch colors { 131 | case .color1: 132 | set(archiveData(color), for: LifeDatabase.Key.color1) 133 | case .color2: 134 | set(archiveData(color), for: LifeDatabase.Key.color2) 135 | case .color3: 136 | set(archiveData(color), for: LifeDatabase.Key.color3) 137 | } 138 | } 139 | } 140 | 141 | private extension ScreenSaverDefaults { 142 | func set(_ object: Any, for key: String) { 143 | set(object, forKey: key) 144 | synchronize() 145 | } 146 | } 147 | 148 | private func archiveData(_ data: Any) -> Data { 149 | do { 150 | let data = try NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: false) 151 | return data 152 | } catch { 153 | fatalError("Failed to archive data") 154 | } 155 | } 156 | 157 | private func unarchiveColor(_ data: Data) -> SKColor { 158 | do { 159 | let color = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? SKColor 160 | return color! 161 | } catch { 162 | fatalError("Failed to unarchive data") 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /macOS Screensaver/LifeScreenSaverView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScreenSaverView.swift 3 | // Life Saver Screensaver 4 | // 5 | // Created by Bradley Root on 5/18/19. 6 | // Copyright © 2019 Brad Root. All rights reserved. 7 | // 8 | // This Source Code Form is subject to the terms of the Mozilla Public 9 | // License, v. 2.0. If a copy of the MPL was not distributed with this 10 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 11 | 12 | import Foundation 13 | import ScreenSaver 14 | import SpriteKit 15 | 16 | final class LifeScreenSaverView: ScreenSaverView { 17 | var spriteView: SKView? 18 | 19 | lazy var sheetController: ConfigureSheetController = ConfigureSheetController() 20 | 21 | override init?(frame: NSRect, isPreview: Bool) { 22 | super.init(frame: frame, isPreview: isPreview) 23 | animationTimeInterval = 1.0 24 | } 25 | 26 | required init?(coder: NSCoder) { 27 | super.init(coder: coder) 28 | animationTimeInterval = 1.0 29 | } 30 | 31 | override var frame: NSRect { 32 | didSet { 33 | self.spriteView?.frame = frame 34 | } 35 | } 36 | 37 | override var hasConfigureSheet: Bool { 38 | return true 39 | } 40 | 41 | override var configureSheet: NSWindow? { 42 | return sheetController.window 43 | } 44 | 45 | override func startAnimation() { 46 | if spriteView == nil { 47 | let manager = LifeManager() 48 | let spriteView = SKView(frame: frame) 49 | spriteView.ignoresSiblingOrder = true 50 | spriteView.showsFPS = false 51 | spriteView.showsNodeCount = false 52 | spriteView.preferredFramesPerSecond = 30 53 | let scene = LifeScene(size: frame.size) 54 | self.spriteView = spriteView 55 | addSubview(spriteView) 56 | 57 | if manager.randomColorPreset, let preset = colorPresets.randomElement() { 58 | manager.configure(with: preset) 59 | } 60 | 61 | scene.appearanceMode = manager.appearanceMode 62 | scene.squareSize = manager.squareSize 63 | scene.animationSpeed = manager.animationSpeed 64 | scene.aliveColors = [manager.color1, manager.color2, manager.color3] 65 | 66 | scene.isUserInteractionEnabled = false 67 | 68 | spriteView.presentScene(scene) 69 | } 70 | super.startAnimation() 71 | } 72 | 73 | override func stopAnimation() { 74 | super.stopAnimation() 75 | spriteView = nil 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /macOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Life Saver 4 | // 5 | // Created by Bradley Root on 5/18/19. 6 | // Copyright © 2019 Brad Root. All rights reserved. 7 | // 8 | // This Source Code Form is subject to the terms of the Mozilla Public 9 | // License, v. 2.0. If a copy of the MPL was not distributed with this 10 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 11 | 12 | import Cocoa 13 | 14 | @NSApplicationMain 15 | class AppDelegate: NSObject, NSApplicationDelegate { 16 | func applicationDidFinishLaunching(_: Notification) { 17 | // Insert code here to initialize your application 18 | } 19 | 20 | func applicationWillTerminate(_: Notification) { 21 | // Insert code here to tear down your application 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /macOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSApplicationCategoryType 24 | public.app-category.entertainment 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2019 Brad Root. All rights reserved. 29 | NSMainStoryboardFile 30 | Main 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /macOS/Life_Saver.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /macOS/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Life Saver 4 | // 5 | // Created by Bradley Root on 5/18/19. 6 | // Copyright © 2019 Brad Root. All rights reserved. 7 | // 8 | // This Source Code Form is subject to the terms of the Mozilla Public 9 | // License, v. 2.0. If a copy of the MPL was not distributed with this 10 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 11 | 12 | import Cocoa 13 | import GameplayKit 14 | import SpriteKit 15 | 16 | class ViewController: NSViewController { 17 | @IBOutlet var skView: SKView! 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | let scene = LifeScene(size: view.frame.size) 23 | 24 | scene.animationSpeed = .normal 25 | scene.squareSize = .medium 26 | 27 | if let preset = colorPresets.filter({ $0.title == "Santa Fe" }).first { 28 | if let appearanceMode = preset.appearanceMode { 29 | scene.appearanceMode = appearanceMode 30 | } 31 | if let color1 = preset.color1, let color2 = preset.color2, let color3 = preset.color3 { 32 | scene.aliveColors = [color1, color2, color3] 33 | } 34 | } 35 | 36 | let skView = view as? SKView 37 | skView?.preferredFramesPerSecond = 30 38 | skView?.presentScene(scene) 39 | skView?.showsFPS = true 40 | skView?.showsDrawCount = true 41 | skView?.showsNodeCount = true 42 | 43 | skView?.ignoresSiblingOrder = true 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tvOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Life Saver 4 | // 5 | // Created by Bradley Root on 6/25/19. 6 | // Copyright © 2019 Brad Root. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | var window: UIWindow? 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | application.isIdleTimerDisabled = true 18 | return true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "filename" : "TVA - Bottom Layer.png" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/TVA - Bottom Layer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/TVA - Bottom Layer.png -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon - App Store.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "layers" : [ 3 | { 4 | "filename" : "Front.imagestacklayer" 5 | }, 6 | { 7 | "filename" : "Middle.imagestacklayer" 8 | }, 9 | { 10 | "filename" : "Back.imagestacklayer" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | } 17 | } -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "filename" : "TVA - Top Layer.png" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/TVA - Top Layer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/TVA - Top Layer.png -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "filename" : "TVA - Middle Layer.png" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/TVA - Middle Layer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/TVA - Middle Layer.png -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "filename" : "TV - Bottom Layer.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "tv", 10 | "filename" : "TV - Bottom Layer@2x.png", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/TV - Bottom Layer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/TV - Bottom Layer.png -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/TV - Bottom Layer@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/TV - Bottom Layer@2x.png -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "layers" : [ 3 | { 4 | "filename" : "Front.imagestacklayer" 5 | }, 6 | { 7 | "filename" : "Middle.imagestacklayer" 8 | }, 9 | { 10 | "filename" : "Back.imagestacklayer" 11 | } 12 | ], 13 | "info" : { 14 | "version" : 1, 15 | "author" : "xcode" 16 | } 17 | } -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "filename" : "TV - Top Layer.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "tv", 10 | "filename" : "TV - Top Layer@2x.png", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/TV - Top Layer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/TV - Top Layer.png -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/TV - Top Layer@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/TV - Top Layer@2x.png -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "filename" : "TV - Middle Layer.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "tv", 10 | "filename" : "TV - Middle Layer@2x.png", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/TV - Middle Layer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/TV - Middle Layer.png -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/TV - Middle Layer@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/TV - Middle Layer@2x.png -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "size" : "1280x768", 5 | "idiom" : "tv", 6 | "filename" : "App Icon - App Store.imagestack", 7 | "role" : "primary-app-icon" 8 | }, 9 | { 10 | "size" : "400x240", 11 | "idiom" : "tv", 12 | "filename" : "App Icon.imagestack", 13 | "role" : "primary-app-icon" 14 | }, 15 | { 16 | "size" : "2320x720", 17 | "idiom" : "tv", 18 | "filename" : "Top Shelf Image Wide.imageset", 19 | "role" : "top-shelf-image-wide" 20 | }, 21 | { 22 | "size" : "1920x720", 23 | "idiom" : "tv", 24 | "filename" : "Top Shelf Image.imageset", 25 | "role" : "top-shelf-image" 26 | } 27 | ], 28 | "info" : { 29 | "version" : 1, 30 | "author" : "xcode" 31 | } 32 | } -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "filename" : "Top Shelf.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "tv", 10 | "filename" : "Top Shelf@2x.png", 11 | "scale" : "2x" 12 | } 13 | ], 14 | "info" : { 15 | "version" : 1, 16 | "author" : "xcode" 17 | } 18 | } -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/Top Shelf Image Wide.imageset/Top Shelf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/tvOS/Assets.xcassets/Brand Assets.brandassets/Top Shelf Image Wide.imageset/Top Shelf.png -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/Top Shelf Image Wide.imageset/Top Shelf@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/tvOS/Assets.xcassets/Brand Assets.brandassets/Top Shelf Image Wide.imageset/Top Shelf@2x.png -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Brand Assets.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "version" : 1, 14 | "author" : "xcode" 15 | } 16 | } -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "landscape", 5 | "idiom" : "tv", 6 | "filename" : "LaunchImage@2x.png", 7 | "extent" : "full-screen", 8 | "minimum-system-version" : "11.0", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "orientation" : "landscape", 13 | "idiom" : "tv", 14 | "filename" : "LaunchImage.png", 15 | "extent" : "full-screen", 16 | "minimum-system-version" : "9.0", 17 | "scale" : "1x" 18 | } 19 | ], 20 | "info" : { 21 | "version" : 1, 22 | "author" : "xcode" 23 | } 24 | } -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/LaunchImage.launchimage/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/tvOS/Assets.xcassets/LaunchImage.launchimage/LaunchImage.png -------------------------------------------------------------------------------- /tvOS/Assets.xcassets/LaunchImage.launchimage/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amiantos/lifesaver/158ac5378c0686480e57709d25d8488b79a26eeb/tvOS/Assets.xcassets/LaunchImage.launchimage/LaunchImage@2x.png -------------------------------------------------------------------------------- /tvOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Life Saver 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIcons 12 | 13 | CFBundleIcons~ipad 14 | 15 | CFBundleIdentifier 16 | $(PRODUCT_BUNDLE_IDENTIFIER) 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundleName 20 | $(PRODUCT_NAME) 21 | CFBundlePackageType 22 | APPL 23 | CFBundleShortVersionString 24 | 1.0 25 | CFBundleVersion 26 | 3 27 | LSRequiresIPhoneOS 28 | 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | arm64 34 | 35 | UIUserInterfaceStyle 36 | Dark 37 | 38 | 39 | -------------------------------------------------------------------------------- /tvOS/LifeViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LifeViewController.swift 3 | // Life Saver 4 | // 5 | // Created by Bradley Root on 6/25/19. 6 | // Copyright © 2019 Brad Root. All rights reserved. 7 | // 8 | 9 | import GameplayKit 10 | import SpriteKit 11 | import UIKit 12 | 13 | enum UIState { 14 | case colorPresets 15 | case mainMenu 16 | case allClosed 17 | } 18 | 19 | class LifeViewController: UIViewController, MenuTableDelegate { 20 | var scene: LifeScene? 21 | let manager = LifeManager() 22 | var state: UIState = .allClosed 23 | 24 | @IBOutlet var colorPresetsTableView: UITableView! 25 | @IBOutlet var colorPresetsView: UIVisualEffectView! 26 | @IBOutlet var mainMenuView: UIVisualEffectView! 27 | 28 | @IBOutlet var mainMenuLeadingConstraint: NSLayoutConstraint! 29 | @IBOutlet var colorMenuTrailingConstraint: NSLayoutConstraint! 30 | 31 | @IBOutlet var menuHintToast: UIVisualEffectView! 32 | @IBOutlet var menuHintToastConstraint: NSLayoutConstraint! 33 | var menuHintBounceAnimation: UIViewPropertyAnimator? 34 | @IBOutlet var colorMenuCloseToast: UIVisualEffectView! 35 | @IBOutlet var mainMenuCloseToast: UIVisualEffectView! 36 | 37 | @IBOutlet var kludgeButton: UIButton! 38 | @IBOutlet var initialOverlayView: UIView! 39 | 40 | let tableViewSource: [Int: [LifePreset]] = [0: settingsPresets, 1: colorPresets] 41 | 42 | var menuTableViewController: MenuTableViewController? 43 | var pressedMenuButtonRecognizer: UITapGestureRecognizer? 44 | 45 | var propertyAnimators: [UIViewPropertyAnimator] = [] 46 | 47 | // MARK: - View Lifecycle 48 | 49 | override func viewDidLoad() { 50 | super.viewDidLoad() 51 | view.backgroundColor = .black 52 | 53 | setupView() 54 | 55 | setupPresetMenu() 56 | 57 | setupGestureRecognizers() 58 | 59 | if !manager.hasPressedMenuButton { 60 | bounceOrHideMenuHintToast(reverse: false) 61 | } else { 62 | menuHintToast.alpha = 0 63 | } 64 | 65 | createScene() 66 | 67 | hideInitialOverlay() 68 | } 69 | 70 | override func viewDidAppear(_ animated: Bool) { 71 | super.viewDidAppear(animated) 72 | view.isUserInteractionEnabled = true 73 | } 74 | 75 | override var preferredFocusEnvironments: [UIFocusEnvironment] { 76 | if state == .colorPresets { 77 | return [colorPresetsView] 78 | } else { 79 | return [mainMenuView] 80 | } 81 | } 82 | 83 | override func prepare(for segue: UIStoryboardSegue, sender _: Any?) { 84 | if segue.identifier == "menuEmbedSegue" { 85 | if let tableViewController = segue.destination as? MenuTableViewController { 86 | menuTableViewController = tableViewController 87 | menuTableViewController?.manager = manager 88 | menuTableViewController?.delegate = self 89 | manager.settingsDelegate = tableViewController 90 | } 91 | } 92 | } 93 | 94 | // MARK: - UI Interactions 95 | 96 | fileprivate func setupGestureRecognizers() { 97 | let swipeFromRight = UISwipeGestureRecognizer(target: self, action: #selector(didSwipeLeft)) 98 | swipeFromRight.direction = .left 99 | swipeFromRight.allowedTouchTypes = [NSNumber(value: UITouch.TouchType.indirect.rawValue)] 100 | view.addGestureRecognizer(swipeFromRight) 101 | 102 | let swipeFromLeft = UISwipeGestureRecognizer(target: self, action: #selector(didSwipeRight)) 103 | swipeFromLeft.direction = .right 104 | swipeFromLeft.allowedTouchTypes = [NSNumber(value: UITouch.TouchType.indirect.rawValue)] 105 | view.addGestureRecognizer(swipeFromLeft) 106 | 107 | pressedMenuButtonRecognizer = UITapGestureRecognizer(target: self, action: #selector(didPressMenuButton)) 108 | pressedMenuButtonRecognizer!.allowedPressTypes = [NSNumber(value: UIPress.PressType.menu.rawValue)] 109 | view.addGestureRecognizer(pressedMenuButtonRecognizer!) 110 | } 111 | 112 | @objc func didPressMenuButton(gesture _: UIGestureRecognizer) { 113 | showMainMenu() 114 | } 115 | 116 | @objc func didSwipeLeft(gesture _: UIGestureRecognizer) { 117 | if state == .mainMenu { 118 | hideAllMenus() 119 | } 120 | 121 | if state == .allClosed { 122 | showColorPresets() 123 | } 124 | } 125 | 126 | @objc func didSwipeRight(gesture _: UIGestureRecognizer) { 127 | if state == .colorPresets { 128 | hideAllMenus() 129 | } 130 | 131 | if state == .allClosed { 132 | showMainMenu() 133 | } 134 | } 135 | 136 | func showColorPresets() { 137 | cancelRunningAnimators() 138 | DispatchQueue.main.async { 139 | self.colorMenuTrailingConstraint.constant = 0 140 | self.mainMenuLeadingConstraint.constant = -self.mainMenuView.frame.width 141 | self.state = .colorPresets 142 | self.pressedMenuButtonRecognizer?.isEnabled = true 143 | 144 | let propertyAnimator = UIViewPropertyAnimator(duration: 1, dampingRatio: 1, animations: { 145 | self.view.layoutIfNeeded() 146 | self.colorMenuCloseToast.alpha = 1 147 | self.mainMenuCloseToast.alpha = 0 148 | }) 149 | propertyAnimator.addCompletion { _ in 150 | self.kludgeButton.alpha = 0 151 | self.setNeedsFocusUpdate() 152 | self.updateFocusIfNeeded() 153 | } 154 | self.propertyAnimators.append(propertyAnimator) 155 | propertyAnimator.startAnimation() 156 | } 157 | } 158 | 159 | func showMainMenu() { 160 | cancelRunningAnimators() 161 | if !manager.hasPressedMenuButton { 162 | manager.setHasPressedMenuButton(true) 163 | } 164 | DispatchQueue.main.async { 165 | self.colorMenuTrailingConstraint.constant = -self.colorPresetsView.frame.width 166 | self.mainMenuLeadingConstraint.constant = 0 167 | self.state = .mainMenu 168 | self.pressedMenuButtonRecognizer?.isEnabled = false 169 | 170 | let propertyAnimator = UIViewPropertyAnimator(duration: 1, dampingRatio: 1, animations: { 171 | self.view.layoutIfNeeded() 172 | self.colorMenuCloseToast.alpha = 0 173 | self.mainMenuCloseToast.alpha = 1 174 | }) 175 | propertyAnimator.addCompletion { _ in 176 | self.kludgeButton.alpha = 0 177 | self.setNeedsFocusUpdate() 178 | self.updateFocusIfNeeded() 179 | } 180 | self.propertyAnimators.append(propertyAnimator) 181 | propertyAnimator.startAnimation() 182 | } 183 | } 184 | 185 | func hideAllMenus() { 186 | cancelRunningAnimators() 187 | DispatchQueue.main.async { 188 | self.colorMenuTrailingConstraint.constant = -self.colorPresetsView.frame.width 189 | self.mainMenuLeadingConstraint.constant = -self.mainMenuView.frame.width 190 | self.state = .allClosed 191 | self.pressedMenuButtonRecognizer?.isEnabled = true 192 | 193 | let propertyAnimator = UIViewPropertyAnimator(duration: 1, dampingRatio: 1, animations: { 194 | self.view.layoutIfNeeded() 195 | self.colorMenuCloseToast.alpha = 0 196 | self.mainMenuCloseToast.alpha = 0 197 | }) 198 | propertyAnimator.addCompletion { _ in 199 | self.kludgeButton.alpha = 1 200 | self.setNeedsFocusUpdate() 201 | self.updateFocusIfNeeded() 202 | } 203 | self.propertyAnimators.append(propertyAnimator) 204 | propertyAnimator.startAnimation() 205 | } 206 | } 207 | 208 | func cancelRunningAnimators() { 209 | propertyAnimators.forEach { 210 | $0.pauseAnimation() 211 | $0.stopAnimation(true) 212 | } 213 | propertyAnimators.removeAll() 214 | } 215 | 216 | // MARK: - View Setup 217 | 218 | fileprivate func setupView() { 219 | menuHintToast.layer.cornerRadius = menuHintToast.frame.height / 2 220 | menuHintToast.clipsToBounds = true 221 | menuHintToast.alpha = 1 222 | 223 | colorMenuCloseToast.layer.cornerRadius = colorMenuCloseToast.frame.height / 2 224 | colorMenuCloseToast.clipsToBounds = true 225 | colorMenuCloseToast.alpha = 0 226 | colorMenuTrailingConstraint.constant = -colorPresetsView.frame.width 227 | 228 | mainMenuCloseToast.layer.cornerRadius = colorMenuCloseToast.frame.height / 2 229 | mainMenuCloseToast.clipsToBounds = true 230 | mainMenuCloseToast.alpha = 0 231 | mainMenuLeadingConstraint.constant = -mainMenuView.frame.width 232 | 233 | updateViewConstraints() 234 | 235 | colorPresetsTableView.delegate = self 236 | colorPresetsTableView.dataSource = self 237 | } 238 | 239 | fileprivate func createScene() { 240 | if let view = self.view as? SKView { 241 | scene = LifeScene(size: view.bounds.size) 242 | scene!.scaleMode = .aspectFit 243 | 244 | scene!.manager = manager 245 | 246 | view.ignoresSiblingOrder = true 247 | view.preferredFramesPerSecond = 60 248 | view.presentScene(scene) 249 | } 250 | } 251 | 252 | fileprivate func setupPresetMenu() { 253 | let selectedPresetTitle = manager.selectedPresetTitle == "" ? "Default" : manager.selectedPresetTitle 254 | var filteredPresets = colorPresets.filter { $0.title == selectedPresetTitle } 255 | if filteredPresets.isEmpty { 256 | filteredPresets = settingsPresets.filter { $0.title == selectedPresetTitle } 257 | } 258 | if let selectedPreset = filteredPresets.first { 259 | var index = colorPresets.firstIndex(where: { $0.title == selectedPreset.title }) 260 | var section = 1 261 | if index == nil { 262 | section = 0 263 | index = settingsPresets.firstIndex(where: { $0.title == selectedPreset.title }) 264 | } 265 | colorPresetsTableView.selectRow( 266 | at: IndexPath( 267 | row: index ?? colorPresets.count - 1, 268 | section: section 269 | ), 270 | animated: false, 271 | scrollPosition: .top 272 | ) 273 | } 274 | } 275 | 276 | fileprivate func hideInitialOverlay() { 277 | let fadeAction = UIViewPropertyAnimator(duration: 2, curve: .easeInOut) { 278 | self.initialOverlayView.alpha = 0 279 | } 280 | fadeAction.addCompletion { state in 281 | switch state { 282 | case .end: 283 | self.initialOverlayView.removeFromSuperview() 284 | self.setNeedsFocusUpdate() 285 | default: 286 | return 287 | } 288 | } 289 | fadeAction.startAnimation(afterDelay: 1) 290 | } 291 | 292 | fileprivate func bounceOrHideMenuHintToast(reverse: Bool) { 293 | // TODO: - This code is problematic (infinitely repeats unnecessarily) and should be broken apart. 294 | if !manager.hasPressedMenuButton { 295 | menuHintToastConstraint.constant = reverse ? 35 : 15 296 | menuHintBounceAnimation = UIViewPropertyAnimator(duration: 1.5, curve: .easeInOut, animations: nil) 297 | } else { 298 | menuHintBounceAnimation?.stopAnimation(true) 299 | menuHintToastConstraint.constant = 35 300 | menuHintBounceAnimation = UIViewPropertyAnimator(duration: 1, curve: .easeIn, animations: nil) 301 | menuHintBounceAnimation?.addAnimations { 302 | self.menuHintToast.alpha = 0 303 | } 304 | } 305 | menuHintBounceAnimation?.addAnimations { 306 | self.view.layoutIfNeeded() 307 | } 308 | menuHintBounceAnimation?.addCompletion { state in 309 | if state == .end { 310 | self.bounceOrHideMenuHintToast(reverse: !reverse) 311 | } 312 | } 313 | menuHintBounceAnimation?.startAnimation() 314 | } 315 | } 316 | 317 | // MARK: - Preset Table View 318 | 319 | extension LifeViewController: UITableViewDelegate, UITableViewDataSource { 320 | func numberOfSections(in _: UITableView) -> Int { 321 | return 2 322 | } 323 | 324 | func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? { 325 | if section == 1 { 326 | return "Color Presets" 327 | } else { 328 | return "Presets" 329 | } 330 | } 331 | 332 | func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { 333 | if section == 1 { 334 | return colorPresets.count - 1 335 | } else { 336 | return settingsPresets.count 337 | } 338 | } 339 | 340 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 341 | if let cell = tableView.dequeueReusableCell(withIdentifier: "presetCell", for: indexPath) as? ColorPresetTableViewCell, 342 | let preset = tableViewSource[indexPath.section]?[indexPath.row] { 343 | cell.titleLabel.text = preset.title 344 | return cell 345 | } 346 | 347 | return UITableViewCell() 348 | } 349 | 350 | func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { 351 | if let preset = tableViewSource[indexPath.section]?[indexPath.row] { 352 | manager.configure(with: preset) 353 | } 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /tvOS/Menus/ColorPresetTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorPresetTableViewCell.swift 3 | // Life Saver tvOS 4 | // 5 | // Created by Bradley Root on 6/25/19. 6 | // Copyright © 2019 Brad Root. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ColorPresetTableViewCell: UITableViewCell { 12 | @IBOutlet var titleLabel: UILabel! 13 | 14 | override func awakeFromNib() { 15 | super.awakeFromNib() 16 | // Initialization code 17 | } 18 | 19 | override func setSelected(_ selected: Bool, animated: Bool) { 20 | super.setSelected(selected, animated: animated) 21 | 22 | // Configure the view for the selected state 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tvOS/Menus/MenuTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuTableViewController.swift 3 | // Life Saver tvOS 4 | // 5 | // Created by Bradley Root on 6/26/19. 6 | // Copyright © 2019 Brad Root. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol MenuTableDelegate: AnyObject { 12 | func showColorPresets() 13 | } 14 | 15 | class MenuTableViewController: UITableViewController, LifeManagerDelegate { 16 | @IBOutlet var squareSizeCell: UITableViewCell! 17 | @IBOutlet var speedCell: UITableViewCell! 18 | @IBOutlet var deathFadeCell: UITableViewCell! 19 | @IBOutlet var randomPresetColorCell: UITableViewCell! 20 | @IBOutlet var showColorPresetsCell: UITableViewCell! 21 | @IBOutlet var aboutCell: UITableViewCell! 22 | 23 | weak var manager: LifeManager? 24 | weak var delegate: MenuTableDelegate? 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | 29 | updatedSettings() 30 | } 31 | 32 | func updatedSettings() { 33 | if let manager = manager { 34 | switch manager.squareSize { 35 | case .superSmall: 36 | squareSizeCell.detailTextLabel?.text = "XX Small" 37 | case .verySmall: 38 | squareSizeCell.detailTextLabel?.text = "Tiny" 39 | case .small: 40 | squareSizeCell.detailTextLabel?.text = "Small" 41 | case .medium: 42 | squareSizeCell.detailTextLabel?.text = "Medium" 43 | case .large: 44 | squareSizeCell.detailTextLabel?.text = "Large" 45 | } 46 | 47 | switch manager.animationSpeed { 48 | case .normal: 49 | speedCell.detailTextLabel?.text = "Normal" 50 | case .fast: 51 | speedCell.detailTextLabel?.text = "Fast" 52 | case .slow: 53 | speedCell.detailTextLabel?.text = "Slow" 54 | case .off: 55 | speedCell.detailTextLabel?.text = "Instant" 56 | } 57 | 58 | let randomColorPresetTitle = manager.shiftingColors ? "On" : "Off" 59 | randomPresetColorCell.detailTextLabel?.text = randomColorPresetTitle 60 | 61 | let deathFadeTitle = manager.deathFade ? "On" : "Off" 62 | deathFadeCell.detailTextLabel?.text = deathFadeTitle 63 | } 64 | } 65 | 66 | override func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { 67 | print((indexPath.section, indexPath.row)) 68 | switch (indexPath.section, indexPath.row) { 69 | case (0, 0): 70 | showSquareSizePicker() 71 | case (0, 1): 72 | showSpeedPicker() 73 | case (0, 2): 74 | showDeathFadePicker() 75 | case (0, 3): 76 | showColorShiftingPicker() 77 | case (0, 4): 78 | delegate?.showColorPresets() 79 | case (1, 0): 80 | showAboutPage() 81 | default: 82 | return 83 | } 84 | } 85 | 86 | fileprivate func showColorShiftingPicker() { 87 | let alert = UIAlertController( 88 | title: "Shifting Color", 89 | message: "When this is enabled, the colors of newly born squares will be slightly mutated, leading to a color shift over time.", 90 | preferredStyle: .actionSheet 91 | ) 92 | 93 | let onAction = UIAlertAction(title: "On", style: .default) { _ in 94 | self.manager?.setShiftingColors(true) 95 | } 96 | alert.addAction(onAction) 97 | 98 | let offAction = UIAlertAction(title: "Off", style: .default) { _ in 99 | self.manager?.setShiftingColors(false) 100 | } 101 | alert.addAction(offAction) 102 | 103 | let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) 104 | alert.addAction(cancelAction) 105 | 106 | present(alert, animated: true, completion: nil) 107 | } 108 | 109 | fileprivate func showAboutPage() { 110 | let alert = UIAlertController(title: "About Life Saver", message: """ 111 | Life Saver is an artistic implementation of the Game of Life, a cellular automaton created by mathematician John Conway in 1972. 112 | 113 | Life Saver is open source software, which means you can see how it works and use it to make your own versions or modifications. 114 | 115 | For more information, visit https://amiantos.net/lifesaver 116 | """, preferredStyle: .alert) 117 | 118 | let cancelAction = UIAlertAction(title: "Close", style: .cancel, handler: nil) 119 | alert.addAction(cancelAction) 120 | 121 | present(alert, animated: true, completion: nil) 122 | } 123 | 124 | fileprivate func showSpeedPicker() { 125 | let alert = UIAlertController( 126 | title: "Animation Speed", 127 | message: """ 128 | This governs how quickly change animations occur. \ 129 | Slower speeds lead to more abstract, shifting colors. \ 130 | Faster speeds make the simulation easier to observe. 131 | """, 132 | preferredStyle: .actionSheet 133 | ) 134 | 135 | let defaultAction = UIAlertAction(title: "Normal", style: .default) { _ in 136 | self.manager?.setAnimationSpeed(.normal) 137 | } 138 | alert.addAction(defaultAction) 139 | 140 | let fastAction = UIAlertAction(title: "Fast", style: .default) { _ in 141 | self.manager?.setAnimationSpeed(.fast) 142 | } 143 | alert.addAction(fastAction) 144 | 145 | let offAction = UIAlertAction(title: "Instant", style: .default) { _ in 146 | self.manager?.setAnimationSpeed(.off) 147 | } 148 | alert.addAction(offAction) 149 | 150 | let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) 151 | alert.addAction(cancelAction) 152 | present(alert, animated: true, completion: nil) 153 | } 154 | 155 | fileprivate func showDeathFadePicker() { 156 | let alert = UIAlertController( 157 | title: "Death Fade", 158 | message: """ 159 | With death fade turned on, when a cell dies, it will fade into the background, \ 160 | and eventually fade out completely. With death fade turned off, the cell color \ 161 | will persist on the screen until it comes back to life as another color. 162 | """, 163 | preferredStyle: .actionSheet 164 | ) 165 | 166 | let onAction = UIAlertAction(title: "On", style: .default) { _ in 167 | self.manager?.setDeathFade(true) 168 | } 169 | alert.addAction(onAction) 170 | 171 | let offAction = UIAlertAction(title: "Off", style: .default) { _ in 172 | self.manager?.setDeathFade(false) 173 | } 174 | alert.addAction(offAction) 175 | 176 | let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) 177 | alert.addAction(cancelAction) 178 | 179 | present(alert, animated: true, completion: nil) 180 | } 181 | 182 | fileprivate func showSquareSizePicker() { 183 | // Square Size Picker 184 | let alert = UIAlertController( 185 | title: "Square Size", 186 | message: """ 187 | This governs the size of the squares on screen. \ 188 | Larger squares are more abstract, while smaller squares allow you to see the simulation easier. 189 | """, 190 | preferredStyle: .actionSheet 191 | ) 192 | let xSmallAction = UIAlertAction(title: "Tiny", style: .default) { _ in 193 | self.manager?.setSquareSize(.verySmall) 194 | } 195 | alert.addAction(xSmallAction) 196 | 197 | let smallAction = UIAlertAction(title: "Small", style: .default) { _ in 198 | self.manager?.setSquareSize(.small) 199 | } 200 | alert.addAction(smallAction) 201 | 202 | let mediumAction = UIAlertAction(title: "Medium", style: .default) { _ in 203 | self.manager?.setSquareSize(.medium) 204 | } 205 | alert.addAction(mediumAction) 206 | 207 | let largeAction = UIAlertAction(title: "Large", style: .default) { _ in 208 | self.manager?.setSquareSize(.large) 209 | } 210 | alert.addAction(largeAction) 211 | 212 | let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) 213 | alert.addAction(cancelAction) 214 | 215 | present(alert, animated: true, completion: nil) 216 | } 217 | } 218 | --------------------------------------------------------------------------------