├── Gemfile ├── images ├── gif.gif ├── image1.png ├── image2.png ├── image3.png ├── image4.png └── image5.png ├── Snaptake ├── Assets.xcassets │ ├── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── ViewController.swift ├── Info.plist ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard └── AppDelegate.swift ├── fastlane ├── screenshots │ ├── en-US │ │ └── iPhone 8-testExample-final.mp4 │ └── screenshots.html ├── Appfile ├── README.md ├── Snapfile ├── report.xml ├── Fastfile └── SnapshotHelper.swift ├── Snaptake.xcodeproj ├── xcuserdata │ └── stephanlerner.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ ├── xcschememanagement.plist │ │ └── Snaptake.xcscheme ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ └── stephanlerner.xcuserdatad │ │ │ ├── UserInterfaceState.xcuserstate │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── xcshareddata │ └── xcschemes │ │ └── SnaptakeUITests.xcscheme └── project.pbxproj ├── SnaptakeUITests ├── Info.plist └── SnaptakeUITests.swift ├── bin ├── ww ├── dotenv ├── rougify ├── fastlane ├── xcpretty ├── bin-proxy ├── xcodeproj ├── httpclient ├── generate-api ├── terminal-notifier ├── xcpretty-travis-formatter └── bundle ├── Gemfile.lock └── README.md /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | gem "listen" 5 | -------------------------------------------------------------------------------- /images/gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lausbert/Snaptake/HEAD/images/gif.gif -------------------------------------------------------------------------------- /images/image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lausbert/Snaptake/HEAD/images/image1.png -------------------------------------------------------------------------------- /images/image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lausbert/Snaptake/HEAD/images/image2.png -------------------------------------------------------------------------------- /images/image3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lausbert/Snaptake/HEAD/images/image3.png -------------------------------------------------------------------------------- /images/image4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lausbert/Snaptake/HEAD/images/image4.png -------------------------------------------------------------------------------- /images/image5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lausbert/Snaptake/HEAD/images/image5.png -------------------------------------------------------------------------------- /Snaptake/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /fastlane/screenshots/en-US/iPhone 8-testExample-final.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lausbert/Snaptake/HEAD/fastlane/screenshots/en-US/iPhone 8-testExample-final.mp4 -------------------------------------------------------------------------------- /Snaptake.xcodeproj/xcuserdata/stephanlerner.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /Snaptake.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Snaptake.xcodeproj/project.xcworkspace/xcuserdata/stephanlerner.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lausbert/Snaptake/HEAD/Snaptake.xcodeproj/project.xcworkspace/xcuserdata/stephanlerner.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /fastlane/Appfile: -------------------------------------------------------------------------------- 1 | app_identifier("de.lausbert.Snaptake") # The bundle identifier of your app 2 | # apple_id("[[APPLE_ID]]") # Your Apple email address 3 | 4 | 5 | # For more information about the Appfile, see: 6 | # https://docs.fastlane.tools/advanced/#appfile 7 | -------------------------------------------------------------------------------- /Snaptake.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Snaptake.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildSystemType 6 | Latest 7 | 8 | 9 | -------------------------------------------------------------------------------- /Snaptake/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Snaptake 4 | // 5 | // Created by Stephan Lerner on 14.04.18. 6 | // Copyright © 2018 Stephan Lerner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view, typically from a nib. 16 | } 17 | 18 | override func didReceiveMemoryWarning() { 19 | super.didReceiveMemoryWarning() 20 | // Dispose of any resources that can be recreated. 21 | } 22 | 23 | 24 | } 25 | 26 | -------------------------------------------------------------------------------- /Snaptake.xcodeproj/project.xcworkspace/xcuserdata/stephanlerner.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | UseAppPreferences 7 | CustomBuildLocationType 8 | RelativeToDerivedData 9 | DerivedDataLocationStyle 10 | Default 11 | EnabledFullIndexStoreVisibility 12 | 13 | IssueFilterStyle 14 | ShowActiveSchemeOnly 15 | LiveSourceIssuesEnabled 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /SnaptakeUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Snaptake.xcodeproj/xcuserdata/stephanlerner.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Snaptake.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | SnaptakeUITests.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 1 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | 8D8A725F20824F840021D344 21 | 22 | primary 23 | 24 | 25 | 8D8A7288208252340021D344 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /bin/ww: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'ww' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("word_wrap", "ww") 30 | -------------------------------------------------------------------------------- /bin/dotenv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'dotenv' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("dotenv", "dotenv") 30 | -------------------------------------------------------------------------------- /bin/rougify: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rougify' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("rouge", "rougify") 30 | -------------------------------------------------------------------------------- /bin/fastlane: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'fastlane' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("fastlane", "fastlane") 30 | -------------------------------------------------------------------------------- /bin/xcpretty: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'xcpretty' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("xcpretty", "xcpretty") 30 | -------------------------------------------------------------------------------- /bin/bin-proxy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bin-proxy' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("fastlane", "bin-proxy") 30 | -------------------------------------------------------------------------------- /bin/xcodeproj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'xcodeproj' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("xcodeproj", "xcodeproj") 30 | -------------------------------------------------------------------------------- /bin/httpclient: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'httpclient' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("httpclient", "httpclient") 30 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | 5 | Make sure you have the latest version of the Xcode command line tools installed: 6 | 7 | ``` 8 | xcode-select --install 9 | ``` 10 | 11 | Install _fastlane_ using 12 | ``` 13 | [sudo] gem install fastlane -NV 14 | ``` 15 | or alternatively using `brew cask install fastlane` 16 | 17 | # Available Actions 18 | ## iOS 19 | ### ios screenshots 20 | ``` 21 | fastlane ios screenshots 22 | ``` 23 | Generate new localized screenshots 24 | ### ios videos 25 | ``` 26 | fastlane ios videos 27 | ``` 28 | Generate new localized videos 29 | 30 | ---- 31 | 32 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 33 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). 34 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 35 | -------------------------------------------------------------------------------- /bin/generate-api: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'generate-api' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("google-api-client", "generate-api") 30 | -------------------------------------------------------------------------------- /bin/terminal-notifier: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'terminal-notifier' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("terminal-notifier", "terminal-notifier") 30 | -------------------------------------------------------------------------------- /fastlane/Snapfile: -------------------------------------------------------------------------------- 1 | # Uncomment the lines below you want to change by removing the # in the beginning 2 | 3 | # A list of devices you want to take the screenshots from 4 | devices([ 5 | "iPhone 8", 6 | # "iPhone 8 Plus", 7 | # "iPhone SE", 8 | # "iPhone X", 9 | # "iPad Pro (12.9-inch)", 10 | # "iPad Pro (9.7-inch)", 11 | # "Apple TV 1080p" 12 | ]) 13 | 14 | languages([ 15 | "en-US", 16 | # "de-DE" 17 | # "it-IT", 18 | # ["pt", "pt_BR"] # Portuguese with Brazilian locale 19 | ]) 20 | 21 | # The name of the scheme which contains the UI Tests 22 | scheme("SnaptakeUITests") 23 | 24 | # remove the '#' to clear all previously generated screenshots before creating new ones 25 | clear_previous_screenshots(true) 26 | 27 | # Arguments to pass to the app on launch. See https://docs.fastlane.tools/actions/snapshot/#launch-arguments 28 | # launch_arguments(["-favColor red"]) 29 | 30 | # For more information about all available options run 31 | # fastlane action snapshot 32 | -------------------------------------------------------------------------------- /bin/xcpretty-travis-formatter: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'xcpretty-travis-formatter' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 150) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("xcpretty-travis-formatter", "xcpretty-travis-formatter") 30 | -------------------------------------------------------------------------------- /fastlane/report.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /SnaptakeUITests/SnaptakeUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnaptakeUITests.swift 3 | // SnaptakeUITests 4 | // 5 | // Created by Stephan Lerner on 14.04.18. 6 | // Copyright © 2018 Stephan Lerner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class SnaptakeUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | super.setUp() 15 | 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | 18 | // In UI tests it is usually best to stop immediately when a failure occurs. 19 | continueAfterFailure = false 20 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 21 | let app = XCUIApplication() 22 | setupSnapshot(app) 23 | app.launch() 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | super.tearDown() 29 | } 30 | 31 | func testExample() { 32 | snaptake("testExample") { 33 | XCUIApplication().buttons["button"].tap() 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Snaptake/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Snaptake/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Snaptake/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Snaptake/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Snaptake 4 | // 5 | // Created by Stephan Lerner on 14.04.18. 6 | // Copyright © 2018 Stephan Lerner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 || ">= 0.a" 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../../Gemfile", __FILE__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_version 64 | @bundler_version ||= begin 65 | env_var_version || cli_arg_version || 66 | lockfile_version || "#{Gem::Requirement.default}.a" 67 | end 68 | end 69 | 70 | def load_bundler! 71 | ENV["BUNDLE_GEMFILE"] ||= gemfile 72 | 73 | # must dup string for RG < 1.8 compatibility 74 | activate_bundler(bundler_version.dup) 75 | end 76 | 77 | def activate_bundler(bundler_version) 78 | if Gem::Version.correct?(bundler_version) && Gem::Version.new(bundler_version).release < Gem::Version.new("2.0") 79 | bundler_version = "< 2" 80 | end 81 | gem_error = activation_error_handling do 82 | gem "bundler", bundler_version 83 | end 84 | return if gem_error.nil? 85 | require_error = activation_error_handling do 86 | require "bundler/version" 87 | end 88 | return if require_error.nil? && Gem::Requirement.new(bundler_version).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 89 | warn "Activating bundler (#{bundler_version}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_version}'`" 90 | exit 42 91 | end 92 | 93 | def activation_error_handling 94 | yield 95 | nil 96 | rescue StandardError, LoadError => e 97 | e 98 | end 99 | end 100 | 101 | m.load_bundler! 102 | 103 | if m.invoked_as_script? 104 | load Gem.bin_path("bundler", "bundle") 105 | end 106 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | 9 | # Uncomment the line if you want fastlane to automatically update itself 10 | # update_fastlane 11 | 12 | default_platform(:ios) 13 | 14 | platform :ios do 15 | 16 | desc "Generate new localized screenshots" 17 | lane :screenshots do 18 | capture_screenshots(scheme: "SnaptakeUITests") 19 | end 20 | 21 | desc "Generate new localized videos" 22 | lane :videos do |options| 23 | 24 | ### RECORDING VIDEOS 25 | 26 | # Delete all existing videos 27 | mp4_file_paths = Find.find('screenshots').select { |p| /.*\.mp4$/ =~ p} 28 | for mp4_file_path in mp4_file_paths 29 | File.delete(mp4_file_path) 30 | end 31 | 32 | # Ensure that caching folder for screenshots and recording flags exists 33 | Dir.mkdir(File.expand_path('~/Library/Caches/tools.fastlane/screenshots')) unless Dir.exist?(File.expand_path('~/Library/Caches/tools.fastlane/screenshots')) 34 | 35 | # Setup listeners for starting and ending recording 36 | fastlane_require 'listen' 37 | path = nil 38 | process = nil 39 | trimming_time_dictionary = {} 40 | recordingListener = Listen.to(File.expand_path('~/Library/Caches/tools.fastlane/screenshots'), only: /\.txt$/) do |modified, added, removed| 41 | if (!added.empty?) && File.basename(added.first) == 'recordingFlag.txt' 42 | recording_flag_path = added.first 43 | path = File.read(recording_flag_path) 44 | process = IO.popen("xcrun simctl io booted recordVideo '#{path}'") # Start recording of current simulator to path determined in recordingFlag.txt 45 | end 46 | if (!removed.empty?) && File.basename(removed.first) == 'recordingFlag.txt' 47 | pid = process.pid 48 | Process.kill("INT", pid) # Stop recording by killing process with id pid 49 | trimming_flag_path = File.expand_path('~/Library/Caches/tools.fastlane/screenshots/trimmingFlag.txt') 50 | trimming_time = File.read(trimming_flag_path) 51 | trimming_time_dictionary[path] = trimming_time # Storing trimming time determined in trimmingFlag.txt for recorded video (necessary due to initial black simulator screen after starting recording) 52 | end 53 | end 54 | 55 | # Build SnaptakeUITests and Snaptake and run UITests 56 | recordingListener.start 57 | sh("cd .. && fastlane snapshot --concurrent_simulators false && cd fastlane") 58 | recordingListener.stop 59 | 60 | ### EDIT VIDEOS 61 | 62 | sleep(3) 63 | 64 | # Trim videos and reencode 65 | mp4_file_paths = Find.find('screenshots').select { |p| /.*\.mp4$/ =~ p} 66 | for mp4_file_path in mp4_file_paths 67 | 68 | trimmed_path = mp4_file_path.chomp('.mp4') + '-trimmed.mp4' 69 | trimming_time = trimming_time_dictionary[mp4_file_path] 70 | sh("ffmpeg -ss '#{trimming_time}' -i '#{mp4_file_path}' -c:v copy -r 30 '#{trimmed_path}'") # Trimming the Beginning of the Videos 71 | File.delete(mp4_file_path) 72 | 73 | final_path = trimmed_path.chomp('-trimmed.mp4') + '-final.mp4' 74 | sh("ffmpeg -i '#{trimmed_path}' -ar 44100 -ab 256k -r 30 -crf 22 -profile:v main -pix_fmt yuv420p -y -max_muxing_queue_size 1000 '#{final_path}'") 75 | File.delete(trimmed_path) 76 | end 77 | 78 | end 79 | 80 | end 81 | -------------------------------------------------------------------------------- /Snaptake.xcodeproj/xcshareddata/xcschemes/SnaptakeUITests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 55 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 74 | 80 | 81 | 82 | 83 | 85 | 86 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /Snaptake/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /Snaptake.xcodeproj/xcuserdata/stephanlerner.xcuserdatad/xcschemes/Snaptake.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.0) 5 | addressable (2.5.2) 6 | public_suffix (>= 2.0.2, < 4.0) 7 | atomos (0.1.2) 8 | babosa (1.0.2) 9 | claide (1.0.2) 10 | colored (1.2) 11 | colored2 (3.1.2) 12 | commander-fastlane (4.4.6) 13 | highline (~> 1.7.2) 14 | declarative (0.0.10) 15 | declarative-option (0.1.0) 16 | domain_name (0.5.20170404) 17 | unf (>= 0.0.5, < 1.0.0) 18 | dotenv (2.2.2) 19 | emoji_regex (0.1.1) 20 | excon (0.62.0) 21 | faraday (0.14.0) 22 | multipart-post (>= 1.2, < 3) 23 | faraday-cookie_jar (0.0.6) 24 | faraday (>= 0.7.4) 25 | http-cookie (~> 1.0.0) 26 | faraday_middleware (0.12.2) 27 | faraday (>= 0.7.4, < 1.0) 28 | fastimage (2.1.1) 29 | fastlane (2.91.0) 30 | CFPropertyList (>= 2.3, < 4.0.0) 31 | addressable (>= 2.3, < 3.0.0) 32 | babosa (>= 1.0.2, < 2.0.0) 33 | bundler (>= 1.12.0, < 2.0.0) 34 | colored 35 | commander-fastlane (>= 4.4.6, < 5.0.0) 36 | dotenv (>= 2.1.1, < 3.0.0) 37 | emoji_regex (~> 0.1) 38 | excon (>= 0.45.0, < 1.0.0) 39 | faraday (~> 0.9) 40 | faraday-cookie_jar (~> 0.0.6) 41 | faraday_middleware (~> 0.9) 42 | fastimage (>= 2.1.0, < 3.0.0) 43 | gh_inspector (>= 1.1.2, < 2.0.0) 44 | google-api-client (>= 0.13.1, < 0.14.0) 45 | highline (>= 1.7.2, < 2.0.0) 46 | json (< 3.0.0) 47 | mini_magick (~> 4.5.1) 48 | multi_json 49 | multi_xml (~> 0.5) 50 | multipart-post (~> 2.0.0) 51 | plist (>= 3.1.0, < 4.0.0) 52 | public_suffix (~> 2.0.0) 53 | rubyzip (>= 1.1.0, < 2.0.0) 54 | security (= 0.1.3) 55 | simctl (~> 1.6.3) 56 | slack-notifier (>= 2.0.0, < 3.0.0) 57 | terminal-notifier (>= 1.6.2, < 2.0.0) 58 | terminal-table (>= 1.4.5, < 2.0.0) 59 | tty-screen (>= 0.6.3, < 1.0.0) 60 | tty-spinner (>= 0.8.0, < 1.0.0) 61 | word_wrap (~> 1.0.0) 62 | xcodeproj (>= 1.5.7, < 2.0.0) 63 | xcpretty (>= 0.2.4, < 1.0.0) 64 | xcpretty-travis-formatter (>= 0.0.3) 65 | ffi (1.9.18) 66 | gh_inspector (1.1.3) 67 | google-api-client (0.13.6) 68 | addressable (~> 2.5, >= 2.5.1) 69 | googleauth (~> 0.5) 70 | httpclient (>= 2.8.1, < 3.0) 71 | mime-types (~> 3.0) 72 | representable (~> 3.0) 73 | retriable (>= 2.0, < 4.0) 74 | googleauth (0.6.2) 75 | faraday (~> 0.12) 76 | jwt (>= 1.4, < 3.0) 77 | logging (~> 2.0) 78 | memoist (~> 0.12) 79 | multi_json (~> 1.11) 80 | os (~> 0.9) 81 | signet (~> 0.7) 82 | highline (1.7.10) 83 | http-cookie (1.0.3) 84 | domain_name (~> 0.5) 85 | httpclient (2.8.3) 86 | json (2.1.0) 87 | jwt (2.1.0) 88 | listen (3.1.5) 89 | rb-fsevent (~> 0.9, >= 0.9.4) 90 | rb-inotify (~> 0.9, >= 0.9.7) 91 | ruby_dep (~> 1.2) 92 | little-plugger (1.1.4) 93 | logging (2.2.2) 94 | little-plugger (~> 1.1) 95 | multi_json (~> 1.10) 96 | memoist (0.16.0) 97 | mime-types (3.1) 98 | mime-types-data (~> 3.2015) 99 | mime-types-data (3.2016.0521) 100 | mini_magick (4.5.1) 101 | multi_json (1.13.1) 102 | multi_xml (0.6.0) 103 | multipart-post (2.0.0) 104 | nanaimo (0.2.5) 105 | naturally (2.1.0) 106 | os (0.9.6) 107 | plist (3.4.0) 108 | public_suffix (2.0.5) 109 | rb-fsevent (0.10.2) 110 | rb-inotify (0.9.10) 111 | ffi (>= 0.5.0, < 2) 112 | representable (3.0.4) 113 | declarative (< 0.1.0) 114 | declarative-option (< 0.2.0) 115 | uber (< 0.2.0) 116 | retriable (3.1.1) 117 | rouge (2.0.7) 118 | ruby_dep (1.5.0) 119 | rubyzip (1.2.1) 120 | security (0.1.3) 121 | signet (0.8.1) 122 | addressable (~> 2.3) 123 | faraday (~> 0.9) 124 | jwt (>= 1.5, < 3.0) 125 | multi_json (~> 1.10) 126 | simctl (1.6.3) 127 | CFPropertyList 128 | naturally 129 | slack-notifier (2.3.2) 130 | terminal-notifier (1.8.0) 131 | terminal-table (1.8.0) 132 | unicode-display_width (~> 1.1, >= 1.1.1) 133 | tty-cursor (0.5.0) 134 | tty-screen (0.6.4) 135 | tty-spinner (0.8.0) 136 | tty-cursor (>= 0.5.0) 137 | uber (0.1.0) 138 | unf (0.1.4) 139 | unf_ext 140 | unf_ext (0.0.7.5) 141 | unicode-display_width (1.3.0) 142 | word_wrap (1.0.0) 143 | xcodeproj (1.5.7) 144 | CFPropertyList (>= 2.3.3, < 4.0) 145 | atomos (~> 0.1.2) 146 | claide (>= 1.0.2, < 2.0) 147 | colored2 (~> 3.1) 148 | nanaimo (~> 0.2.4) 149 | xcpretty (0.2.8) 150 | rouge (~> 2.0.7) 151 | xcpretty-travis-formatter (1.0.0) 152 | xcpretty (~> 0.2, >= 0.0.7) 153 | 154 | PLATFORMS 155 | ruby 156 | 157 | DEPENDENCIES 158 | fastlane 159 | listen 160 | 161 | BUNDLED WITH 162 | 1.16.1 163 | -------------------------------------------------------------------------------- /fastlane/screenshots/screenshots.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | fastlane/snapshot 5 | 6 | 66 | 67 | 68 |
69 | 70 |
71 |
72 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | ### 🚀 Give My Budgeting App a Try! 4 | 5 | Try out my budgeting app! 🙂 → [monee-app.com](https://monee-app.com) 6 | 7 | Just like all the content here on the site, it’s completely **free**—so I’d really appreciate a rating and a review! 8 | 9 | --- 10 | 11 | # Snaptake 12 | 13 | There are already some tries (here and here) to automate creating videos with fastlane snapshot. Anyway Felix definitely does not want to have an http server in his code base :) 14 |

15 | 16 |

17 | 18 | The following step by step guide shows an alternative way of solution. The outline corresponds to the commit history. 19 |

20 | 21 |

22 | 23 | ## Prerequisites 24 | 25 | - Fastlane for recording videos 26 | - FFmpeg for editing videos 27 | 28 | ## Creating project 29 | 30 | Just create a single view application in XCode without any tests. 31 |

32 | 33 |

34 | 35 | ## Setting up fastlane snapshot 36 | 37 | ```cd``` to your project folder and run ```fastlane init```. When fastlane is asking what you would like to use it for press "1". 38 |

39 | 40 |

41 | 42 | Do exactly what fastlane is telling you afterwards. After finishing the instructions open the newly created Snapfile in your projects fastlane folder. Uncomment at least one device and one language. Make sure ``` snapshot("0Launch") ``` is called in one of your UITests. Run ``` fastlane snapshot ``` in your project folder to verify everything is working fine. 43 | 44 | ## Setting up storyboard 45 | 46 | To test the later added video recording feature, we need something to record. Therefore just add a button to your first ViewController and add a second ViewController with a distinguishable background. Push the second ViewController, when the button is clicked. Also set the buttons accessibility identifier to ```"button"```. 47 |

48 | 49 |

50 | 51 | ## Setting up UITests 52 | 53 | Now that your storyboard is set up, let's add video related code to the SnapshotHelper file. To keep the original snapshot logic, add the following two functions to the SnapshotHelper file scope. 54 | 55 | ```swift 56 | func snaptake(_ name: String, waitForLoadingIndicator: Bool, plot: ()->()) { 57 | if waitForLoadingIndicator { 58 | Snapshot.snaptake(name, plot: plot) 59 | } else { 60 | Snapshot.snaptake(name, timeWaitingForIdle: 0, plot: plot) 61 | } 62 | } 63 | ``` 64 | 65 | ```swift 66 | /// - Parameters: 67 | /// - name: The name of the snaptake 68 | /// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait. 69 | /// - plot: Plot which should be recorded. 70 | func snaptake(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20, plot: ()->()) { 71 | Snapshot.snaptake(name, timeWaitingForIdle: timeout, plot: plot) 72 | } 73 | ``` 74 | These two functions are pretty similar to the already existing snapshot functions. The only difference lies in the additional argument ```plot: ()->()```, which is a closure with no parameters and return values. ```plot``` contains all the interface interactions you want to record. You will see how to use it later. 75 | 76 | Within your Snapshot class add the actual recording logic. ```snaptake``` takes ```plot``` as an argument and successively calls ```snaptakeStart()```, ```snaptakeSetTrimmingFlag()```, ```plot()``` and ```snaptakeStop()```. 77 | 78 | ```swift 79 | open class func snaptake(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20, plot: ()->()) { 80 | 81 | guard let recordingFlagPath = snaptakeStart(name, timeWaitingForIdle: timeout) else { return } 82 | 83 | snaptakeSetTrimmingFlag() 84 | 85 | plot() 86 | 87 | snaptakeStop(recordingFlagPath) 88 | } 89 | ``` 90 | 91 | Within ```snaptakeStart``` a recordingFlag is saved to your hard drive. This recordingFlag contains the path of the later recorded video. The saving of this recordingFlag is watched outside of XCode to start the actual recording process. You will see how this works later. 92 | 93 | ```swift 94 | class func snaptakeStart(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) -> URL? { 95 | if timeout > 0 { 96 | waitForLoadingIndicatorToDisappear(within: timeout) 97 | } 98 | 99 | print("snaptake: \(name)") 100 | 101 | sleep(1) // Waiting for the animation to be finished (kind of) 102 | 103 | #if os(OSX) 104 | XCUIApplication().typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: []) 105 | #else 106 | guard let simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return nil } 107 | 108 | let path = "screenshots/\(locale)/\(simulator)-\(name).mp4" 109 | let recordingFlagPath = screenshotsDir.appendingPathComponent("recordingFlag.txt") 110 | 111 | do { 112 | try path.write(to: recordingFlagPath, atomically: false, encoding: String.Encoding.utf8) 113 | } catch let error { 114 | print("Problem setting recording flag: \(recordingFlagPath)") 115 | print(error) 116 | } 117 | #endif 118 | return recordingFlagPath 119 | } 120 | ``` 121 | 122 | There is a pretty annoying bug, when recording videos via console: The first few frames appear black until somethings happens within your application. That's why we are going to rotate the device and save related duration in ```snaptakeSetTrimmingFlag```. Later we will trim the recorded video accordingly. 123 | 124 | ```swift 125 | class func snaptakeSetTrimmingFlag() { 126 | 127 | let start = Date() 128 | sleep(2) 129 | XCUIDevice.shared.orientation = .landscapeLeft 130 | sleep(2) 131 | XCUIDevice.shared.orientation = .portrait 132 | let trimmingTime = -start.timeIntervalSinceNow - 2 133 | 134 | let hours = Int(trimmingTime)/3600 135 | let minutes = (Int(trimmingTime)/60)%60 136 | let seconds = Int(trimmingTime)%60 137 | let milliseconds = Int((trimmingTime - Double(Int(trimmingTime))) * 1000) 138 | let trimmingTimeString = String(format:"%02i:%02i:%02i.%03i", hours, minutes, seconds, milliseconds) 139 | 140 | #if os(OSX) 141 | XCUIApplication().typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: []) 142 | #else 143 | guard let screenshotsDir = screenshotsDirectory else { return } 144 | 145 | let trimmingFlagPath = screenshotsDir.appendingPathComponent("trimmingFlag.txt") 146 | 147 | do { 148 | try trimmingTimeString.write(to: trimmingFlagPath, atomically: false, encoding: String.Encoding.utf8) 149 | } catch let error { 150 | print("Problem setting recording flag: \(trimmingFlagPath)") 151 | print(error) 152 | } 153 | 154 | #endif 155 | } 156 | ``` 157 | 158 | After we called ```plot``` in ```snaptake``` we finally are going to stop recording in ```snaptakeStop```. We are doing so by removing the ```recordingFlag``` we added earlier in ```snaptakeStart```. 159 | 160 | ```swift 161 | class func snaptakeStop(_ recordingFlagPath: URL) { 162 | let fileManager = FileManager.default 163 | 164 | do { 165 | try fileManager.removeItem(at: recordingFlagPath) 166 | } catch let error { 167 | print("Problem removing recording flag: \(recordingFlagPath)") 168 | print(error) 169 | } 170 | } 171 | ``` 172 | 173 | Finally add the following test function within SnaptakeUITests file. The function contains our plot where our button is simply tapped. 174 | 175 | ```swift 176 | func testExample() { 177 | snaptake("testExample") { 178 | XCUIApplication().buttons["button"].tap() 179 | } 180 | } 181 | ``` 182 | 183 | ## Setting up fastfile, gemfile and snapfile 184 | 185 | After your UITests are fully set up we need to add related logic outside of XCode. Within your Gemfile in your fastlane folder add ```gem "listen"```. Within your Snapfile remove ```output_directory("./screenshots")```. Now we are ready to create a videos lane in your Fastfile. The videos lane is more or less self-explaining. The most relevant part is the ```recordingListener```. Within its handlers the video reording process is started and stopped, when the recordingFlag is added or removed. When recording is stopped, the trimming time for the resulting video is read from our trimmingFlag and stored in ```trimming_time_dictionary```. ```sh("cd .. && fastlane snapshot --concurrent_simulators false && cd fastlane")``` builds Snaptake and runs SnaptakeUITests, so our ```recordingListener``` could actually be triggered. After recording any videos, they are trimmed and reencoded. 186 | 187 | ```ruby 188 | desc "Generate new localized videos" 189 | lane :videos do |options| 190 | 191 | ### RECORDING VIDEOS 192 | 193 | # Delete all existing videos 194 | mp4_file_paths = Find.find('screenshots').select { |p| /.*\.mp4$/ =~ p} 195 | for mp4_file_path in mp4_file_paths 196 | File.delete(mp4_file_path) 197 | end 198 | 199 | # Ensure that caching folder for screenshots and recording flags exists 200 | Dir.mkdir(File.expand_path('~/Library/Caches/tools.fastlane/screenshots')) unless Dir.exist?(File.expand_path('~/Library/Caches/tools.fastlane/screenshots')) 201 | 202 | # Setup listeners for starting and ending recording 203 | fastlane_require 'listen' 204 | path = nil 205 | process = nil 206 | trimming_time_dictionary = {} 207 | recordingListener = Listen.to(File.expand_path('~/Library/Caches/tools.fastlane/screenshots'), only: /\.txt$/) do |modified, added, removed| 208 | if (!added.empty?) && File.basename(added.first) == 'recordingFlag.txt' 209 | recording_flag_path = added.first 210 | path = File.read(recording_flag_path) 211 | process = IO.popen("xcrun simctl io booted recordVideo '#{path}'") # Start recording of current simulator to path determined in recordingFlag.txt 212 | end 213 | if (!removed.empty?) && File.basename(removed.first) == 'recordingFlag.txt' 214 | pid = process.pid 215 | Process.kill("INT", pid) # Stop recording by killing process with id pid 216 | trimming_flag_path = File.expand_path('~/Library/Caches/tools.fastlane/screenshots/trimmingFlag.txt') 217 | trimming_time = File.read(trimming_flag_path) 218 | trimming_time_dictionary[path] = trimming_time # Storing trimming time determined in trimmingFlag.txt for recorded video (necessary due to initial black simulator screen after starting recording) 219 | end 220 | end 221 | 222 | # Build SnaptakeUITests and Snaptake and run UITests 223 | recordingListener.start 224 | sh("cd .. && fastlane snapshot --concurrent_simulators false && cd fastlane") 225 | recordingListener.stop 226 | 227 | ### EDIT VIDEOS 228 | 229 | sleep(3) 230 | 231 | # Trim videos and reencode 232 | mp4_file_paths = Find.find('screenshots').select { |p| /.*\.mp4$/ =~ p} 233 | for mp4_file_path in mp4_file_paths 234 | 235 | trimmed_path = mp4_file_path.chomp('.mp4') + '-trimmed.mp4' 236 | trimming_time = trimming_time_dictionary[mp4_file_path] 237 | sh("ffmpeg -ss '#{trimming_time}' -i '#{mp4_file_path}' -c:v copy -r 30 '#{trimmed_path}'") # Trimming the Beginning of the Videos 238 | File.delete(mp4_file_path) 239 | 240 | final_path = trimmed_path.chomp('-trimmed.mp4') + '-final.mp4' 241 | sh("ffmpeg -i '#{trimmed_path}' -ar 44100 -ab 256k -r 30 -crf 22 -profile:v main -pix_fmt yuv420p -y -max_muxing_queue_size 1000 '#{final_path}'") 242 | File.delete(trimmed_path) 243 | end 244 | end 245 | ``` 246 | 247 | ## Running videos lane 248 | 249 | By calling ```fastlane videos``` we are creating our test video: 250 | 251 |

252 | 253 |

254 | 255 | ## Call to action 256 | 257 | You want to know what is possible with this procedure? 258 | 259 | **Checkout Bonprix with your iPhone in the (e.g. German) Appstore!** 260 | 261 | You want to solve similarly exciting technical questions? 262 | 263 | **Join us at apploft!** 264 | -------------------------------------------------------------------------------- /fastlane/SnapshotHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SnapshotHelper.swift 3 | // Example 4 | // 5 | // Created by Felix Krause on 10/8/15. 6 | // Copyright © 2015 Felix Krause. All rights reserved. 7 | // 8 | 9 | // ----------------------------------------------------- 10 | // IMPORTANT: When modifying this file, make sure to 11 | // increment the version number at the very 12 | // bottom of the file to notify users about 13 | // the new SnapshotHelper.swift 14 | // ----------------------------------------------------- 15 | 16 | import Foundation 17 | import XCTest 18 | 19 | var deviceLanguage = "" 20 | var locale = "" 21 | 22 | func setupSnapshot(_ app: XCUIApplication) { 23 | Snapshot.setupSnapshot(app) 24 | } 25 | 26 | func snapshot(_ name: String, waitForLoadingIndicator: Bool) { 27 | if waitForLoadingIndicator { 28 | Snapshot.snapshot(name) 29 | } else { 30 | Snapshot.snapshot(name, timeWaitingForIdle: 0) 31 | } 32 | } 33 | 34 | func snaptake(_ name: String, waitForLoadingIndicator: Bool, plot: ()->()) { 35 | if waitForLoadingIndicator { 36 | Snapshot.snaptake(name, plot: plot) 37 | } else { 38 | Snapshot.snaptake(name, timeWaitingForIdle: 0, plot: plot) 39 | } 40 | } 41 | 42 | /// - Parameters: 43 | /// - name: The name of the snapshot 44 | /// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait. 45 | func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { 46 | Snapshot.snapshot(name, timeWaitingForIdle: timeout) 47 | } 48 | 49 | /// - Parameters: 50 | /// - name: The name of the snaptake 51 | /// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait. 52 | /// - plot: Plot which should be recorded. 53 | func snaptake(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20, plot: ()->()) { 54 | Snapshot.snaptake(name, timeWaitingForIdle: timeout, plot: plot) 55 | } 56 | 57 | enum SnapshotError: Error, CustomDebugStringConvertible { 58 | case cannotDetectUser 59 | case cannotFindHomeDirectory 60 | case cannotFindSimulatorHomeDirectory 61 | case cannotAccessSimulatorHomeDirectory(String) 62 | case cannotRunOnPhysicalDevice 63 | 64 | var debugDescription: String { 65 | switch self { 66 | case .cannotDetectUser: 67 | return "Couldn't find Snapshot configuration files - can't detect current user " 68 | case .cannotFindHomeDirectory: 69 | return "Couldn't find Snapshot configuration files - can't detect `Users` dir" 70 | case .cannotFindSimulatorHomeDirectory: 71 | return "Couldn't find simulator home location. Please, check SIMULATOR_HOST_HOME env variable." 72 | case .cannotAccessSimulatorHomeDirectory(let simulatorHostHome): 73 | return "Can't prepare environment. Simulator home location is inaccessible. Does \(simulatorHostHome) exist?" 74 | case .cannotRunOnPhysicalDevice: 75 | return "Can't use Snapshot on a physical device." 76 | } 77 | } 78 | } 79 | 80 | @objcMembers 81 | open class Snapshot: NSObject { 82 | static var app: XCUIApplication? 83 | static var cacheDirectory: URL? 84 | static var screenshotsDirectory: URL? { 85 | return cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true) 86 | } 87 | 88 | open class func setupSnapshot(_ app: XCUIApplication) { 89 | 90 | Snapshot.app = app 91 | 92 | do { 93 | let cacheDir = try pathPrefix() 94 | Snapshot.cacheDirectory = cacheDir 95 | setLanguage(app) 96 | setLocale(app) 97 | setLaunchArguments(app) 98 | } catch let error { 99 | print(error) 100 | } 101 | } 102 | 103 | class func setLanguage(_ app: XCUIApplication) { 104 | guard let cacheDirectory = self.cacheDirectory else { 105 | print("CacheDirectory is not set - probably running on a physical device?") 106 | return 107 | } 108 | 109 | let path = cacheDirectory.appendingPathComponent("language.txt") 110 | 111 | do { 112 | let trimCharacterSet = CharacterSet.whitespacesAndNewlines 113 | deviceLanguage = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) 114 | app.launchArguments += ["-AppleLanguages", "(\(deviceLanguage))"] 115 | } catch { 116 | print("Couldn't detect/set language...") 117 | } 118 | } 119 | 120 | class func setLocale(_ app: XCUIApplication) { 121 | guard let cacheDirectory = self.cacheDirectory else { 122 | print("CacheDirectory is not set - probably running on a physical device?") 123 | return 124 | } 125 | 126 | let path = cacheDirectory.appendingPathComponent("locale.txt") 127 | 128 | do { 129 | let trimCharacterSet = CharacterSet.whitespacesAndNewlines 130 | locale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) 131 | } catch { 132 | print("Couldn't detect/set locale...") 133 | } 134 | if locale.isEmpty { 135 | locale = Locale(identifier: deviceLanguage).identifier 136 | } 137 | app.launchArguments += ["-AppleLocale", "\"\(locale)\""] 138 | } 139 | 140 | class func setLaunchArguments(_ app: XCUIApplication) { 141 | guard let cacheDirectory = self.cacheDirectory else { 142 | print("CacheDirectory is not set - probably running on a physical device?") 143 | return 144 | } 145 | 146 | let path = cacheDirectory.appendingPathComponent("snapshot-launch_arguments.txt") 147 | app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"] 148 | 149 | do { 150 | let launchArguments = try String(contentsOf: path, encoding: String.Encoding.utf8) 151 | let regex = try NSRegularExpression(pattern: "(\\\".+?\\\"|\\S+)", options: []) 152 | let matches = regex.matches(in: launchArguments, options: [], range: NSRange(location: 0, length: launchArguments.count)) 153 | let results = matches.map { result -> String in 154 | (launchArguments as NSString).substring(with: result.range) 155 | } 156 | app.launchArguments += results 157 | } catch { 158 | print("Couldn't detect/set launch_arguments...") 159 | } 160 | } 161 | 162 | open class func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { 163 | if timeout > 0 { 164 | waitForLoadingIndicatorToDisappear(within: timeout) 165 | } 166 | 167 | print("snapshot: \(name)") // more information about this, check out https://docs.fastlane.tools/actions/snapshot/#how-does-it-work 168 | 169 | sleep(1) // Waiting for the animation to be finished (kind of) 170 | 171 | #if os(OSX) 172 | XCUIApplication().typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: []) 173 | #else 174 | 175 | guard let app = self.app else { 176 | print("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") 177 | return 178 | } 179 | 180 | let screenshot = app.windows.firstMatch.screenshot() 181 | guard let simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return } 182 | let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png") 183 | do { 184 | try screenshot.pngRepresentation.write(to: path) 185 | } catch let error { 186 | print("Problem writing screenshot: \(name) to \(path)") 187 | print(error) 188 | } 189 | #endif 190 | } 191 | 192 | open class func snaptake(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20, plot: ()->()) { 193 | 194 | guard let recordingFlagPath = snaptakeStart(name, timeWaitingForIdle: timeout) else { return } 195 | 196 | snaptakeSetTrimmingFlag() 197 | 198 | plot() 199 | 200 | snaptakeStop(recordingFlagPath) 201 | 202 | } 203 | 204 | class func snaptakeStart(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) -> URL? { 205 | if timeout > 0 { 206 | waitForLoadingIndicatorToDisappear(within: timeout) 207 | } 208 | 209 | print("snaptake: \(name)") 210 | 211 | sleep(1) // Waiting for the animation to be finished (kind of) 212 | 213 | #if os(OSX) 214 | XCUIApplication().typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: []) 215 | #else 216 | guard let simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return nil } 217 | 218 | let path = "screenshots/\(locale)/\(simulator)-\(name).mp4" 219 | let recordingFlagPath = screenshotsDir.appendingPathComponent("recordingFlag.txt") 220 | 221 | do { 222 | try path.write(to: recordingFlagPath, atomically: false, encoding: String.Encoding.utf8) 223 | } catch let error { 224 | print("Problem setting recording flag: \(recordingFlagPath)") 225 | print(error) 226 | } 227 | #endif 228 | return recordingFlagPath 229 | } 230 | 231 | class func snaptakeSetTrimmingFlag() { 232 | 233 | let start = Date() 234 | sleep(2) 235 | XCUIDevice.shared.orientation = .landscapeLeft 236 | sleep(2) 237 | XCUIDevice.shared.orientation = .portrait 238 | let trimmingTime = -start.timeIntervalSinceNow - 2 239 | 240 | let hours = Int(trimmingTime)/3600 241 | let minutes = (Int(trimmingTime)/60)%60 242 | let seconds = Int(trimmingTime)%60 243 | let milliseconds = Int((trimmingTime - Double(Int(trimmingTime))) * 1000) 244 | let trimmingTimeString = String(format:"%02i:%02i:%02i.%03i", hours, minutes, seconds, milliseconds) 245 | 246 | #if os(OSX) 247 | XCUIApplication().typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: []) 248 | #else 249 | guard let screenshotsDir = screenshotsDirectory else { return } 250 | 251 | let trimmingFlagPath = screenshotsDir.appendingPathComponent("trimmingFlag.txt") 252 | 253 | do { 254 | try trimmingTimeString.write(to: trimmingFlagPath, atomically: false, encoding: String.Encoding.utf8) 255 | } catch let error { 256 | print("Problem setting recording flag: \(trimmingFlagPath)") 257 | print(error) 258 | } 259 | 260 | #endif 261 | } 262 | 263 | class func snaptakeStop(_ recordingFlagPath: URL) { 264 | let fileManager = FileManager.default 265 | 266 | do { 267 | try fileManager.removeItem(at: recordingFlagPath) 268 | } catch let error { 269 | print("Problem removing recording flag: \(recordingFlagPath)") 270 | print(error) 271 | } 272 | } 273 | 274 | class func waitForLoadingIndicatorToDisappear(within timeout: TimeInterval) { 275 | #if os(tvOS) 276 | return 277 | #endif 278 | 279 | let networkLoadingIndicator = XCUIApplication().otherElements.deviceStatusBars.networkLoadingIndicators.element 280 | let networkLoadingIndicatorDisappeared = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == false"), object: networkLoadingIndicator) 281 | _ = XCTWaiter.wait(for: [networkLoadingIndicatorDisappeared], timeout: timeout) 282 | } 283 | 284 | class func pathPrefix() throws -> URL? { 285 | let homeDir: URL 286 | // on OSX config is stored in /Users//Library 287 | // and on iOS/tvOS/WatchOS it's in simulator's home dir 288 | #if os(OSX) 289 | guard let user = ProcessInfo().environment["USER"] else { 290 | throw SnapshotError.cannotDetectUser 291 | } 292 | 293 | guard let usersDir = FileManager.default.urls(for: .userDirectory, in: .localDomainMask).first else { 294 | throw SnapshotError.cannotFindHomeDirectory 295 | } 296 | 297 | homeDir = usersDir.appendingPathComponent(user) 298 | #else 299 | #if arch(i386) || arch(x86_64) 300 | guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else { 301 | throw SnapshotError.cannotFindSimulatorHomeDirectory 302 | } 303 | guard let homeDirUrl = URL(string: simulatorHostHome) else { 304 | throw SnapshotError.cannotAccessSimulatorHomeDirectory(simulatorHostHome) 305 | } 306 | homeDir = URL(fileURLWithPath: homeDirUrl.path) 307 | #else 308 | throw SnapshotError.cannotRunOnPhysicalDevice 309 | #endif 310 | #endif 311 | return homeDir.appendingPathComponent("Library/Caches/tools.fastlane") 312 | } 313 | } 314 | 315 | private extension XCUIElementAttributes { 316 | var isNetworkLoadingIndicator: Bool { 317 | if hasWhiteListedIdentifier { return false } 318 | 319 | let hasOldLoadingIndicatorSize = frame.size == CGSize(width: 10, height: 20) 320 | let hasNewLoadingIndicatorSize = frame.size.width.isBetween(46, and: 47) && frame.size.height.isBetween(2, and: 3) 321 | 322 | return hasOldLoadingIndicatorSize || hasNewLoadingIndicatorSize 323 | } 324 | 325 | var hasWhiteListedIdentifier: Bool { 326 | let whiteListedIdentifiers = ["GeofenceLocationTrackingOn", "StandardLocationTrackingOn"] 327 | 328 | return whiteListedIdentifiers.contains(identifier) 329 | } 330 | 331 | func isStatusBar(_ deviceWidth: CGFloat) -> Bool { 332 | if elementType == .statusBar { return true } 333 | guard frame.origin == .zero else { return false } 334 | 335 | let oldStatusBarSize = CGSize(width: deviceWidth, height: 20) 336 | let newStatusBarSize = CGSize(width: deviceWidth, height: 44) 337 | 338 | return [oldStatusBarSize, newStatusBarSize].contains(frame.size) 339 | } 340 | } 341 | 342 | private extension XCUIElementQuery { 343 | var networkLoadingIndicators: XCUIElementQuery { 344 | let isNetworkLoadingIndicator = NSPredicate { (evaluatedObject, _) in 345 | guard let element = evaluatedObject as? XCUIElementAttributes else { return false } 346 | 347 | return element.isNetworkLoadingIndicator 348 | } 349 | 350 | return self.containing(isNetworkLoadingIndicator) 351 | } 352 | 353 | var deviceStatusBars: XCUIElementQuery { 354 | let deviceWidth = XCUIApplication().frame.width 355 | 356 | let isStatusBar = NSPredicate { (evaluatedObject, _) in 357 | guard let element = evaluatedObject as? XCUIElementAttributes else { return false } 358 | 359 | return element.isStatusBar(deviceWidth) 360 | } 361 | 362 | return self.containing(isStatusBar) 363 | } 364 | } 365 | 366 | private extension CGFloat { 367 | func isBetween(_ numberA: CGFloat, and numberB: CGFloat) -> Bool { 368 | return numberA...numberB ~= self 369 | } 370 | } 371 | 372 | // Please don't remove the lines below 373 | // They are used to detect outdated configuration files 374 | // SnapshotHelperVersion [1.10] 375 | -------------------------------------------------------------------------------- /Snaptake.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8D8A726420824F840021D344 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D8A726320824F840021D344 /* AppDelegate.swift */; }; 11 | 8D8A726620824F840021D344 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D8A726520824F840021D344 /* ViewController.swift */; }; 12 | 8D8A726920824F840021D344 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8D8A726720824F840021D344 /* Main.storyboard */; }; 13 | 8D8A726B20824F850021D344 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8D8A726A20824F850021D344 /* Assets.xcassets */; }; 14 | 8D8A726E20824F850021D344 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8D8A726C20824F850021D344 /* LaunchScreen.storyboard */; }; 15 | 8D8A728C208252340021D344 /* SnaptakeUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D8A728B208252340021D344 /* SnaptakeUITests.swift */; }; 16 | 8D8A7294208252440021D344 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D8A7293208252440021D344 /* SnapshotHelper.swift */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXContainerItemProxy section */ 20 | 8D8A728E208252340021D344 /* PBXContainerItemProxy */ = { 21 | isa = PBXContainerItemProxy; 22 | containerPortal = 8D8A725820824F840021D344 /* Project object */; 23 | proxyType = 1; 24 | remoteGlobalIDString = 8D8A725F20824F840021D344; 25 | remoteInfo = Snaptake; 26 | }; 27 | /* End PBXContainerItemProxy section */ 28 | 29 | /* Begin PBXFileReference section */ 30 | 8D8A726020824F840021D344 /* Snaptake.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Snaptake.app; sourceTree = BUILT_PRODUCTS_DIR; }; 31 | 8D8A726320824F840021D344 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 32 | 8D8A726520824F840021D344 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 33 | 8D8A726820824F840021D344 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 34 | 8D8A726A20824F850021D344 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 35 | 8D8A726D20824F850021D344 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 36 | 8D8A726F20824F850021D344 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 37 | 8D8A7289208252340021D344 /* SnaptakeUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SnaptakeUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 38 | 8D8A728B208252340021D344 /* SnaptakeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnaptakeUITests.swift; sourceTree = ""; }; 39 | 8D8A728D208252340021D344 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 40 | 8D8A7293208252440021D344 /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SnapshotHelper.swift; path = fastlane/SnapshotHelper.swift; sourceTree = SOURCE_ROOT; }; 41 | /* End PBXFileReference section */ 42 | 43 | /* Begin PBXFrameworksBuildPhase section */ 44 | 8D8A725D20824F840021D344 /* Frameworks */ = { 45 | isa = PBXFrameworksBuildPhase; 46 | buildActionMask = 2147483647; 47 | files = ( 48 | ); 49 | runOnlyForDeploymentPostprocessing = 0; 50 | }; 51 | 8D8A7286208252340021D344 /* Frameworks */ = { 52 | isa = PBXFrameworksBuildPhase; 53 | buildActionMask = 2147483647; 54 | files = ( 55 | ); 56 | runOnlyForDeploymentPostprocessing = 0; 57 | }; 58 | /* End PBXFrameworksBuildPhase section */ 59 | 60 | /* Begin PBXGroup section */ 61 | 8D8A725720824F840021D344 = { 62 | isa = PBXGroup; 63 | children = ( 64 | 8D8A726220824F840021D344 /* Snaptake */, 65 | 8D8A728A208252340021D344 /* SnaptakeUITests */, 66 | 8D8A726120824F840021D344 /* Products */, 67 | ); 68 | sourceTree = ""; 69 | }; 70 | 8D8A726120824F840021D344 /* Products */ = { 71 | isa = PBXGroup; 72 | children = ( 73 | 8D8A726020824F840021D344 /* Snaptake.app */, 74 | 8D8A7289208252340021D344 /* SnaptakeUITests.xctest */, 75 | ); 76 | name = Products; 77 | sourceTree = ""; 78 | }; 79 | 8D8A726220824F840021D344 /* Snaptake */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | 8D8A726320824F840021D344 /* AppDelegate.swift */, 83 | 8D8A726520824F840021D344 /* ViewController.swift */, 84 | 8D8A726720824F840021D344 /* Main.storyboard */, 85 | 8D8A726A20824F850021D344 /* Assets.xcassets */, 86 | 8D8A726C20824F850021D344 /* LaunchScreen.storyboard */, 87 | 8D8A726F20824F850021D344 /* Info.plist */, 88 | ); 89 | path = Snaptake; 90 | sourceTree = ""; 91 | }; 92 | 8D8A728A208252340021D344 /* SnaptakeUITests */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | 8D8A728B208252340021D344 /* SnaptakeUITests.swift */, 96 | 8D8A7293208252440021D344 /* SnapshotHelper.swift */, 97 | 8D8A728D208252340021D344 /* Info.plist */, 98 | ); 99 | path = SnaptakeUITests; 100 | sourceTree = ""; 101 | }; 102 | /* End PBXGroup section */ 103 | 104 | /* Begin PBXNativeTarget section */ 105 | 8D8A725F20824F840021D344 /* Snaptake */ = { 106 | isa = PBXNativeTarget; 107 | buildConfigurationList = 8D8A727220824F850021D344 /* Build configuration list for PBXNativeTarget "Snaptake" */; 108 | buildPhases = ( 109 | 8D8A725C20824F840021D344 /* Sources */, 110 | 8D8A725D20824F840021D344 /* Frameworks */, 111 | 8D8A725E20824F840021D344 /* Resources */, 112 | ); 113 | buildRules = ( 114 | ); 115 | dependencies = ( 116 | ); 117 | name = Snaptake; 118 | productName = Snaptake; 119 | productReference = 8D8A726020824F840021D344 /* Snaptake.app */; 120 | productType = "com.apple.product-type.application"; 121 | }; 122 | 8D8A7288208252340021D344 /* SnaptakeUITests */ = { 123 | isa = PBXNativeTarget; 124 | buildConfigurationList = 8D8A7292208252340021D344 /* Build configuration list for PBXNativeTarget "SnaptakeUITests" */; 125 | buildPhases = ( 126 | 8D8A7285208252340021D344 /* Sources */, 127 | 8D8A7286208252340021D344 /* Frameworks */, 128 | 8D8A7287208252340021D344 /* Resources */, 129 | ); 130 | buildRules = ( 131 | ); 132 | dependencies = ( 133 | 8D8A728F208252340021D344 /* PBXTargetDependency */, 134 | ); 135 | name = SnaptakeUITests; 136 | productName = SnaptakeUITests; 137 | productReference = 8D8A7289208252340021D344 /* SnaptakeUITests.xctest */; 138 | productType = "com.apple.product-type.bundle.ui-testing"; 139 | }; 140 | /* End PBXNativeTarget section */ 141 | 142 | /* Begin PBXProject section */ 143 | 8D8A725820824F840021D344 /* Project object */ = { 144 | isa = PBXProject; 145 | attributes = { 146 | LastSwiftUpdateCheck = 0930; 147 | LastUpgradeCheck = 0930; 148 | ORGANIZATIONNAME = "Stephan Lerner"; 149 | TargetAttributes = { 150 | 8D8A725F20824F840021D344 = { 151 | CreatedOnToolsVersion = 9.3; 152 | }; 153 | 8D8A7288208252340021D344 = { 154 | CreatedOnToolsVersion = 9.3; 155 | TestTargetID = 8D8A725F20824F840021D344; 156 | }; 157 | }; 158 | }; 159 | buildConfigurationList = 8D8A725B20824F840021D344 /* Build configuration list for PBXProject "Snaptake" */; 160 | compatibilityVersion = "Xcode 9.3"; 161 | developmentRegion = en; 162 | hasScannedForEncodings = 0; 163 | knownRegions = ( 164 | en, 165 | Base, 166 | ); 167 | mainGroup = 8D8A725720824F840021D344; 168 | productRefGroup = 8D8A726120824F840021D344 /* Products */; 169 | projectDirPath = ""; 170 | projectRoot = ""; 171 | targets = ( 172 | 8D8A725F20824F840021D344 /* Snaptake */, 173 | 8D8A7288208252340021D344 /* SnaptakeUITests */, 174 | ); 175 | }; 176 | /* End PBXProject section */ 177 | 178 | /* Begin PBXResourcesBuildPhase section */ 179 | 8D8A725E20824F840021D344 /* Resources */ = { 180 | isa = PBXResourcesBuildPhase; 181 | buildActionMask = 2147483647; 182 | files = ( 183 | 8D8A726E20824F850021D344 /* LaunchScreen.storyboard in Resources */, 184 | 8D8A726B20824F850021D344 /* Assets.xcassets in Resources */, 185 | 8D8A726920824F840021D344 /* Main.storyboard in Resources */, 186 | ); 187 | runOnlyForDeploymentPostprocessing = 0; 188 | }; 189 | 8D8A7287208252340021D344 /* Resources */ = { 190 | isa = PBXResourcesBuildPhase; 191 | buildActionMask = 2147483647; 192 | files = ( 193 | ); 194 | runOnlyForDeploymentPostprocessing = 0; 195 | }; 196 | /* End PBXResourcesBuildPhase section */ 197 | 198 | /* Begin PBXSourcesBuildPhase section */ 199 | 8D8A725C20824F840021D344 /* Sources */ = { 200 | isa = PBXSourcesBuildPhase; 201 | buildActionMask = 2147483647; 202 | files = ( 203 | 8D8A726620824F840021D344 /* ViewController.swift in Sources */, 204 | 8D8A726420824F840021D344 /* AppDelegate.swift in Sources */, 205 | ); 206 | runOnlyForDeploymentPostprocessing = 0; 207 | }; 208 | 8D8A7285208252340021D344 /* Sources */ = { 209 | isa = PBXSourcesBuildPhase; 210 | buildActionMask = 2147483647; 211 | files = ( 212 | 8D8A728C208252340021D344 /* SnaptakeUITests.swift in Sources */, 213 | 8D8A7294208252440021D344 /* SnapshotHelper.swift in Sources */, 214 | ); 215 | runOnlyForDeploymentPostprocessing = 0; 216 | }; 217 | /* End PBXSourcesBuildPhase section */ 218 | 219 | /* Begin PBXTargetDependency section */ 220 | 8D8A728F208252340021D344 /* PBXTargetDependency */ = { 221 | isa = PBXTargetDependency; 222 | target = 8D8A725F20824F840021D344 /* Snaptake */; 223 | targetProxy = 8D8A728E208252340021D344 /* PBXContainerItemProxy */; 224 | }; 225 | /* End PBXTargetDependency section */ 226 | 227 | /* Begin PBXVariantGroup section */ 228 | 8D8A726720824F840021D344 /* Main.storyboard */ = { 229 | isa = PBXVariantGroup; 230 | children = ( 231 | 8D8A726820824F840021D344 /* Base */, 232 | ); 233 | name = Main.storyboard; 234 | sourceTree = ""; 235 | }; 236 | 8D8A726C20824F850021D344 /* LaunchScreen.storyboard */ = { 237 | isa = PBXVariantGroup; 238 | children = ( 239 | 8D8A726D20824F850021D344 /* Base */, 240 | ); 241 | name = LaunchScreen.storyboard; 242 | sourceTree = ""; 243 | }; 244 | /* End PBXVariantGroup section */ 245 | 246 | /* Begin XCBuildConfiguration section */ 247 | 8D8A727020824F850021D344 /* Debug */ = { 248 | isa = XCBuildConfiguration; 249 | buildSettings = { 250 | ALWAYS_SEARCH_USER_PATHS = NO; 251 | CLANG_ANALYZER_NONNULL = YES; 252 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 253 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 254 | CLANG_CXX_LIBRARY = "libc++"; 255 | CLANG_ENABLE_MODULES = YES; 256 | CLANG_ENABLE_OBJC_ARC = YES; 257 | CLANG_ENABLE_OBJC_WEAK = YES; 258 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 259 | CLANG_WARN_BOOL_CONVERSION = YES; 260 | CLANG_WARN_COMMA = YES; 261 | CLANG_WARN_CONSTANT_CONVERSION = YES; 262 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 263 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 264 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 265 | CLANG_WARN_EMPTY_BODY = YES; 266 | CLANG_WARN_ENUM_CONVERSION = YES; 267 | CLANG_WARN_INFINITE_RECURSION = YES; 268 | CLANG_WARN_INT_CONVERSION = YES; 269 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 270 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 271 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 272 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 273 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 274 | CLANG_WARN_STRICT_PROTOTYPES = YES; 275 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 276 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 277 | CLANG_WARN_UNREACHABLE_CODE = YES; 278 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 279 | CODE_SIGN_IDENTITY = "iPhone Developer"; 280 | COPY_PHASE_STRIP = NO; 281 | DEBUG_INFORMATION_FORMAT = dwarf; 282 | ENABLE_STRICT_OBJC_MSGSEND = YES; 283 | ENABLE_TESTABILITY = YES; 284 | GCC_C_LANGUAGE_STANDARD = gnu11; 285 | GCC_DYNAMIC_NO_PIC = NO; 286 | GCC_NO_COMMON_BLOCKS = YES; 287 | GCC_OPTIMIZATION_LEVEL = 0; 288 | GCC_PREPROCESSOR_DEFINITIONS = ( 289 | "DEBUG=1", 290 | "$(inherited)", 291 | ); 292 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 293 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 294 | GCC_WARN_UNDECLARED_SELECTOR = YES; 295 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 296 | GCC_WARN_UNUSED_FUNCTION = YES; 297 | GCC_WARN_UNUSED_VARIABLE = YES; 298 | IPHONEOS_DEPLOYMENT_TARGET = 11.3; 299 | MTL_ENABLE_DEBUG_INFO = YES; 300 | ONLY_ACTIVE_ARCH = YES; 301 | SDKROOT = iphoneos; 302 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 303 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 304 | }; 305 | name = Debug; 306 | }; 307 | 8D8A727120824F850021D344 /* Release */ = { 308 | isa = XCBuildConfiguration; 309 | buildSettings = { 310 | ALWAYS_SEARCH_USER_PATHS = NO; 311 | CLANG_ANALYZER_NONNULL = YES; 312 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 313 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 314 | CLANG_CXX_LIBRARY = "libc++"; 315 | CLANG_ENABLE_MODULES = YES; 316 | CLANG_ENABLE_OBJC_ARC = YES; 317 | CLANG_ENABLE_OBJC_WEAK = YES; 318 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 319 | CLANG_WARN_BOOL_CONVERSION = YES; 320 | CLANG_WARN_COMMA = YES; 321 | CLANG_WARN_CONSTANT_CONVERSION = YES; 322 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 323 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 324 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 325 | CLANG_WARN_EMPTY_BODY = YES; 326 | CLANG_WARN_ENUM_CONVERSION = YES; 327 | CLANG_WARN_INFINITE_RECURSION = YES; 328 | CLANG_WARN_INT_CONVERSION = YES; 329 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 330 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 331 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 332 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 333 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 334 | CLANG_WARN_STRICT_PROTOTYPES = YES; 335 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 336 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 337 | CLANG_WARN_UNREACHABLE_CODE = YES; 338 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 339 | CODE_SIGN_IDENTITY = "iPhone Developer"; 340 | COPY_PHASE_STRIP = NO; 341 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 342 | ENABLE_NS_ASSERTIONS = NO; 343 | ENABLE_STRICT_OBJC_MSGSEND = YES; 344 | GCC_C_LANGUAGE_STANDARD = gnu11; 345 | GCC_NO_COMMON_BLOCKS = YES; 346 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 347 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 348 | GCC_WARN_UNDECLARED_SELECTOR = YES; 349 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 350 | GCC_WARN_UNUSED_FUNCTION = YES; 351 | GCC_WARN_UNUSED_VARIABLE = YES; 352 | IPHONEOS_DEPLOYMENT_TARGET = 11.3; 353 | MTL_ENABLE_DEBUG_INFO = NO; 354 | SDKROOT = iphoneos; 355 | SWIFT_COMPILATION_MODE = wholemodule; 356 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 357 | VALIDATE_PRODUCT = YES; 358 | }; 359 | name = Release; 360 | }; 361 | 8D8A727320824F850021D344 /* Debug */ = { 362 | isa = XCBuildConfiguration; 363 | buildSettings = { 364 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 365 | CODE_SIGN_STYLE = Automatic; 366 | DEVELOPMENT_TEAM = C6N3X97ZLM; 367 | INFOPLIST_FILE = Snaptake/Info.plist; 368 | LD_RUNPATH_SEARCH_PATHS = ( 369 | "$(inherited)", 370 | "@executable_path/Frameworks", 371 | ); 372 | PRODUCT_BUNDLE_IDENTIFIER = de.lausbert.Snaptake; 373 | PRODUCT_NAME = "$(TARGET_NAME)"; 374 | SWIFT_VERSION = 4.0; 375 | TARGETED_DEVICE_FAMILY = "1,2"; 376 | }; 377 | name = Debug; 378 | }; 379 | 8D8A727420824F850021D344 /* Release */ = { 380 | isa = XCBuildConfiguration; 381 | buildSettings = { 382 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 383 | CODE_SIGN_STYLE = Automatic; 384 | DEVELOPMENT_TEAM = C6N3X97ZLM; 385 | INFOPLIST_FILE = Snaptake/Info.plist; 386 | LD_RUNPATH_SEARCH_PATHS = ( 387 | "$(inherited)", 388 | "@executable_path/Frameworks", 389 | ); 390 | PRODUCT_BUNDLE_IDENTIFIER = de.lausbert.Snaptake; 391 | PRODUCT_NAME = "$(TARGET_NAME)"; 392 | SWIFT_VERSION = 4.0; 393 | TARGETED_DEVICE_FAMILY = "1,2"; 394 | }; 395 | name = Release; 396 | }; 397 | 8D8A7290208252340021D344 /* Debug */ = { 398 | isa = XCBuildConfiguration; 399 | buildSettings = { 400 | CODE_SIGN_STYLE = Automatic; 401 | DEVELOPMENT_TEAM = C6N3X97ZLM; 402 | INFOPLIST_FILE = SnaptakeUITests/Info.plist; 403 | LD_RUNPATH_SEARCH_PATHS = ( 404 | "$(inherited)", 405 | "@executable_path/Frameworks", 406 | "@loader_path/Frameworks", 407 | ); 408 | PRODUCT_BUNDLE_IDENTIFIER = de.lausbert.SnaptakeUITests; 409 | PRODUCT_NAME = "$(TARGET_NAME)"; 410 | SWIFT_VERSION = 4.0; 411 | TARGETED_DEVICE_FAMILY = "1,2"; 412 | TEST_TARGET_NAME = Snaptake; 413 | }; 414 | name = Debug; 415 | }; 416 | 8D8A7291208252340021D344 /* Release */ = { 417 | isa = XCBuildConfiguration; 418 | buildSettings = { 419 | CODE_SIGN_STYLE = Automatic; 420 | DEVELOPMENT_TEAM = C6N3X97ZLM; 421 | INFOPLIST_FILE = SnaptakeUITests/Info.plist; 422 | LD_RUNPATH_SEARCH_PATHS = ( 423 | "$(inherited)", 424 | "@executable_path/Frameworks", 425 | "@loader_path/Frameworks", 426 | ); 427 | PRODUCT_BUNDLE_IDENTIFIER = de.lausbert.SnaptakeUITests; 428 | PRODUCT_NAME = "$(TARGET_NAME)"; 429 | SWIFT_VERSION = 4.0; 430 | TARGETED_DEVICE_FAMILY = "1,2"; 431 | TEST_TARGET_NAME = Snaptake; 432 | }; 433 | name = Release; 434 | }; 435 | /* End XCBuildConfiguration section */ 436 | 437 | /* Begin XCConfigurationList section */ 438 | 8D8A725B20824F840021D344 /* Build configuration list for PBXProject "Snaptake" */ = { 439 | isa = XCConfigurationList; 440 | buildConfigurations = ( 441 | 8D8A727020824F850021D344 /* Debug */, 442 | 8D8A727120824F850021D344 /* Release */, 443 | ); 444 | defaultConfigurationIsVisible = 0; 445 | defaultConfigurationName = Release; 446 | }; 447 | 8D8A727220824F850021D344 /* Build configuration list for PBXNativeTarget "Snaptake" */ = { 448 | isa = XCConfigurationList; 449 | buildConfigurations = ( 450 | 8D8A727320824F850021D344 /* Debug */, 451 | 8D8A727420824F850021D344 /* Release */, 452 | ); 453 | defaultConfigurationIsVisible = 0; 454 | defaultConfigurationName = Release; 455 | }; 456 | 8D8A7292208252340021D344 /* Build configuration list for PBXNativeTarget "SnaptakeUITests" */ = { 457 | isa = XCConfigurationList; 458 | buildConfigurations = ( 459 | 8D8A7290208252340021D344 /* Debug */, 460 | 8D8A7291208252340021D344 /* Release */, 461 | ); 462 | defaultConfigurationIsVisible = 0; 463 | defaultConfigurationName = Release; 464 | }; 465 | /* End XCConfigurationList section */ 466 | }; 467 | rootObject = 8D8A725820824F840021D344 /* Project object */; 468 | } 469 | --------------------------------------------------------------------------------