diff --git a/SimDirs/Model/SimCtl.swift b/SimDirs/Model/SimCtl.swift index d2b4d6f..4fa4385 100644 --- a/SimDirs/Model/SimCtl.swift +++ b/SimDirs/Model/SimCtl.swift @@ -8,14 +8,24 @@ import Foundation struct SimCtl { - func run(args: [String]) throws -> Data { + func run(args: [String], run: Bool = true) throws -> Process { let process = Process() - let pipe = Pipe() process.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun") process.arguments = ["simctl"] + args - process.standardOutput = pipe process.standardError = nil + if run { + try process.run() + } + + return process + } + + func run(args: [String]) throws -> Data { + let process : Process = try run(args: args, run: false) + let pipe = Pipe() + + process.standardOutput = pipe try process.run() return pipe.fileHandleForReading.readDataToEndOfFile() @@ -119,4 +129,8 @@ struct SimCtl { func saveScreen(_ device: SimDevice, url: URL) throws { try runAsync(args: ["io", device.udid, "screenshot", url.path]) } + + func saveVideo(_ device: SimDevice, url: URL) throws -> Process { + return try run(args: ["io", device.udid, "recordVideo", "--force", url.path]) + } } diff --git a/SimDirs/Model/SimDevice.swift b/SimDirs/Model/SimDevice.swift index b0ae697..98e38a1 100644 --- a/SimDirs/Model/SimDevice.swift +++ b/SimDirs/Model/SimDevice.swift @@ -27,6 +27,8 @@ class SimDevice: ObservableObject, Decodable { @Published var appearance = Appearance.unknown @Published var contentSize = ContentSize.unknown @Published var increaseContrast = IncreaseContrast.unknown + @Published var isRecording = false + var recordingProcess : Process? var isTransitioning : Bool { state == .booting || state == .shuttingDown } var isBooted : Bool { get { state.showBooted == true } @@ -190,6 +192,28 @@ class SimDevice: ObservableObject, Decodable { print("Failed to save screen: \(error)") } } + + func saveVideo(_ url: URL) { + do { + recordingProcess = try SimCtl().saveVideo(self, url: url) + if recordingProcess != nil { + isRecording = true + } + } catch { + print("Failed to save video: \(error)") + } + } + + func endRecording() { + if let process = recordingProcess { + process.interrupt() + recordingProcess = nil + isRecording = false + } + else { + isRecording = false + } + } } extension SimDevice { diff --git a/SimDirs/Views/Model Views/DeviceContent.swift b/SimDirs/Views/Model Views/DeviceContent.swift index 35c780e..bb3874d 100644 --- a/SimDirs/Views/Model Views/DeviceContent.swift +++ b/SimDirs/Views/Model Views/DeviceContent.swift @@ -6,6 +6,7 @@ // import SwiftUI +import UniformTypeIdentifiers extension SimDevice { public var content : some View { DeviceContent(self) } @@ -45,6 +46,25 @@ extension SimDevice.IncreaseContrast: ToggleDescriptor { } struct DeviceContent: View { + enum SaveType { + case image + case video + + var allowedContentTypes : [UTType] { + switch self { + case .image: return [.png] + case .video: return [.mpeg4Movie] + } + } + + var title : String { + switch self { + case .image: return "Save Screen" + case .video: return "Save Recording" + } + } + } + @ObservedObject var device : SimDevice @State var isBooted : Bool @@ -77,6 +97,23 @@ struct DeviceContent: View { .font(.subheadline) .textSelection(.enabled) + ContentHeader("Actions") + HStack(spacing: 16) { + Button(action: { saveScreen(.image) }) { + Text("Save Screen") + .fontWeight(.semibold) + .font(.system(size: 11)) + } + .buttonStyle(.systemIcon("camera.on.rectangle")) + + Button(action: { device.isRecording ? device.endRecording() : saveScreen(.video) }) { + Text(device.isRecording ? "End Recording" : "Record Screen") + .fontWeight(.semibold) + .font(.system(size: 11)) + } + .buttonStyle(.systemIcon("record.circle", active: device.isRecording)) + } + ContentHeader("UI") HStack(spacing: 16) { if device.appearance != .unsupported { @@ -99,14 +136,6 @@ struct DeviceContent: View { .opacity(isBooted ? 1.0 : 0.5) } } - - ContentHeader("Actions") - Button(action: saveScreen) { - Text("Save Screen") - .fontWeight(.semibold) - .font(.system(size: 11)) - } - .buttonStyle(.systemIcon("camera.on.rectangle")) } .environment(\.isEnabled, isBooted) .onAppear { @@ -124,21 +153,24 @@ struct DeviceContent: View { } } } - - func saveScreen() { + + func saveScreen(_ type: SaveType = .image) { let savePanel = NSSavePanel() - savePanel.allowedContentTypes = [.png] + savePanel.allowedContentTypes = type.allowedContentTypes savePanel.canCreateDirectories = true savePanel.isExtensionHidden = false - savePanel.title = "Save Screen" + savePanel.title = type.title savePanel.message = "Select destination" savePanel.nameFieldLabel = "Filename:" savePanel.nameFieldStringValue = "\(device.name) - \(fileDateFormatter.string(from: Date()))" if savePanel.runModal() == .OK { if let url = savePanel.url { - device.saveScreen(url) + switch type { + case .image: device.saveScreen(url) + case .video: device.saveVideo(url) + } } } } diff --git a/SimDirs/Views/Styles/SystemIconButtonStyle.swift b/SimDirs/Views/Styles/SystemIconButtonStyle.swift index b1a0123..48be02e 100644 --- a/SimDirs/Views/Styles/SystemIconButtonStyle.swift +++ b/SimDirs/Views/Styles/SystemIconButtonStyle.swift @@ -8,17 +8,19 @@ import SwiftUI extension ButtonStyle where Self == SystemIconButtonStyle { - static func systemIcon(_ imageName: String) -> SystemIconButtonStyle { - SystemIconButtonStyle(imageName) + static func systemIcon(_ imageName: String, active: Bool = false) -> SystemIconButtonStyle { + SystemIconButtonStyle(imageName, active: active) } } struct SystemIconButtonStyle: ButtonStyle { @State var isFocused = false + let isActive : Bool let imageName : String - init(_ imageName: String) { + init(_ imageName: String, active: Bool = false) { self.imageName = imageName + self.isActive = active } func makeBody(configuration: Configuration) -> some View { @@ -32,11 +34,12 @@ struct SystemIconButtonStyle: ButtonStyle { .resizable() .aspectRatio(contentMode: ContentMode.fit) .frame(width: 24, height: 24) + .foregroundColor(isActive ? .accentColor : foregroundColor(pressed: configuration.isPressed)) } configuration.label + .foregroundColor(foregroundColor(pressed: configuration.isPressed)) } - .foregroundColor(foregroundColor(pressed: configuration.isPressed)) .padding(1.0) .onHover { isFocused = $0 } }