Skip to content

Instantly share code, notes, and snippets.

@Cosmo
Last active July 13, 2022 13:26
Show Gist options
  • Save Cosmo/bd7720bf42e3bc5e7822010ba8aa350c to your computer and use it in GitHub Desktop.
Save Cosmo/bd7720bf42e3bc5e7822010ba8aa350c to your computer and use it in GitHub Desktop.
Take screenshot with Xcode Test Plans
#!/usr/bin/env xcrun swift
import Foundation
// Todos
// [ ] Create missing simulators if needed
// [ ] Test on fresh new install of macOS / Xcode
// [ ] Get scheme and testPlan from command line argument
// Ideas
// [ ] What about tvOS, watchOS, ..?
// Add devices for your screenshots.
let devices: [Device] = [
Device(name: "iPhone 11 Pro Max",
idiom: .phone,
homeStyle: .indicator,
displaySize: "6.5"),
Device(name: "iPhone 8 Plus",
idiom: .phone,
homeStyle: .button,
displaySize: "5.5"),
Device(name: "iPad Pro (12.9-inch) (5th generation)",
idiom: .pad,
homeStyle: .indicator,
displaySize: "12.9"),
Device(name: "iPad Pro (12.9-inch) (2nd generation)",
idiom: .pad,
homeStyle: .button,
displaySize: "12.9"),
]
// Replace the placeholders with your scheme and test plan.
let scheme = "SCHEME_NAME"
let testPlan = "TESTPLAN_NAME"
// Do not edit anything starting from here.
func start(devices: [Device], scheme: String, testPlan: String) {
let targetFolder = "\(Project.path)/Screenshots"
let derivedDataPath = "/private/tmp/\(Project.name)/DerivedData"
for device in devices {
let screenshotsPath = "\(targetFolder)/\(device.description)/\(device.name)"
print("Starting Simulator: \(device.name)")
Shell.run("xcrun simctl boot \(device.name.quoted())")
print("Simulator started.")
print("Setting Status Bar …")
device.setStatusBar()
print("Status Bar set.")
print("Deleting Derived Data …")
Shell.run("rm -rf \(derivedDataPath.quoted())")
print("Creating Screenshots Directory …")
Shell.run("mkdir -p \(screenshotsPath.quoted())")
print("Running Tests …")
Shell.run("xcodebuild test -scheme \(scheme) -destination \"platform=iOS Simulator,name=\(device.name)\" -testPlan \(testPlan) -derivedDataPath \(derivedDataPath.quoted())")
print("Copying Screenshots …")
Shell.run("find \"\(derivedDataPath)/Logs/Test\" -maxdepth 1 -type d -exec xcparse screenshots {} \(screenshotsPath.quoted()) \\;")
// run("xcrun simctl shutdown \"\(device.name)\"")
}
}
extension String {
func quoted() -> String {
return "\u{22}\(self)\u{22}"
}
}
struct Shell {
@discardableResult
static func safeShell(_ command: String) throws -> String {
let task = Process()
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
task.arguments = ["-c", command]
task.executableURL = URL(fileURLWithPath: "/bin/zsh")
task.standardInput = nil
try task.run()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!
return output.replacingOccurrences(of: "\n", with: "")
}
@discardableResult
static func run(_ command: String) -> String? {
do {
return try safeShell(command)
}
catch {
print("\(error)")
}
return nil
}
}
struct Project {
static var path: String {
return Shell.run("pwd") ?? ""
}
static var name: String {
return String(path.split(separator: "/").last ?? "project")
}
}
struct Device: CustomStringConvertible {
let name: String
let idiom: Idiom
let homeStyle: HomeStyle
let displaySize: String
enum Idiom: CustomStringConvertible {
case pad
case phone
var description: String {
switch self {
case .pad:
return "iPad"
case .phone:
return "iPhone"
}
}
}
enum HomeStyle: CustomStringConvertible {
case indicator
case button
var description: String {
switch self {
case .indicator:
return "Home Indicator"
case .button:
return "Home Button"
}
}
}
func setStatusBar() {
switch self.idiom {
case .pad:
Device.setStatusBarPad(name: self.name)
case .phone:
switch self.homeStyle {
case .button:
Device.setStatusBarPhoneWithHomeButton(name: self.name)
case .indicator:
Device.setStatusBarPhoneWithHomeIndicator(name: self.name)
}
}
}
static func setStatusBarPhoneWithHomeButton(name: String) {
Shell.run("xcrun simctl status_bar \"\(name)\" clear")
Shell.run("xcrun simctl status_bar \"\(name)\" override --time \"9:41 AM\" --wifiBars 3 --cellularBars 4 --operatorName \"\"")
Shell.run("xcrun simctl spawn \"\(name)\" defaults write com.apple.springboard SBShowBatteryPercentage 1")
}
static func setStatusBarPhoneWithHomeIndicator(name: String) {
Shell.run("xcrun simctl status_bar \"\(name)\" clear")
Shell.run("xcrun simctl status_bar \"\(name)\" override --time \"9:41\" --wifiBars 3 --cellularBars 4")
}
static func setStatusBarPad(name: String) {
Shell.run("xcrun simctl status_bar \"\(name)\" clear")
Shell.run("xcrun simctl status_bar \"\(name)\" override --time \"9:41 AM\" --wifiBars 3 --wifiMode active")
Shell.run("xcrun simctl spawn \"\(name)\" defaults write com.apple.springboard SBShowBatteryPercentage 1")
Shell.run("xcrun simctl spawn \"\(name)\" defaults write com.apple.UIKit StatusBarHidesDate 1")
}
var description: String {
return "\(idiom.description) \(displaySize)-inch with \(homeStyle.description)"
}
}
start(devices: devices, scheme: scheme, testPlan: testPlan)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment