Add ability to record screen although not 100% satisfied with it.

This commit is contained in:
Casey Fleser 2022-08-21 07:24:49 -05:00
parent 7b02bd3da2
commit 4d67317ebb
4 changed files with 93 additions and 20 deletions

View file

@ -8,14 +8,24 @@
import Foundation import Foundation
struct SimCtl { struct SimCtl {
func run(args: [String]) throws -> Data { func run(args: [String], run: Bool = true) throws -> Process {
let process = Process() let process = Process()
let pipe = Pipe()
process.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun") process.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun")
process.arguments = ["simctl"] + args process.arguments = ["simctl"] + args
process.standardOutput = pipe
process.standardError = nil 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() try process.run()
return pipe.fileHandleForReading.readDataToEndOfFile() return pipe.fileHandleForReading.readDataToEndOfFile()
@ -119,4 +129,8 @@ struct SimCtl {
func saveScreen(_ device: SimDevice, url: URL) throws { func saveScreen(_ device: SimDevice, url: URL) throws {
try runAsync(args: ["io", device.udid, "screenshot", url.path]) 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])
}
} }

View file

@ -27,6 +27,8 @@ class SimDevice: ObservableObject, Decodable {
@Published var appearance = Appearance.unknown @Published var appearance = Appearance.unknown
@Published var contentSize = ContentSize.unknown @Published var contentSize = ContentSize.unknown
@Published var increaseContrast = IncreaseContrast.unknown @Published var increaseContrast = IncreaseContrast.unknown
@Published var isRecording = false
var recordingProcess : Process?
var isTransitioning : Bool { state == .booting || state == .shuttingDown } var isTransitioning : Bool { state == .booting || state == .shuttingDown }
var isBooted : Bool { var isBooted : Bool {
get { state.showBooted == true } get { state.showBooted == true }
@ -190,6 +192,28 @@ class SimDevice: ObservableObject, Decodable {
print("Failed to save screen: \(error)") 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 { extension SimDevice {

View file

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import UniformTypeIdentifiers
extension SimDevice { extension SimDevice {
public var content : some View { DeviceContent(self) } public var content : some View { DeviceContent(self) }
@ -45,6 +46,25 @@ extension SimDevice.IncreaseContrast: ToggleDescriptor {
} }
struct DeviceContent: View { 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 @ObservedObject var device : SimDevice
@State var isBooted : Bool @State var isBooted : Bool
@ -77,6 +97,23 @@ struct DeviceContent: View {
.font(.subheadline) .font(.subheadline)
.textSelection(.enabled) .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") ContentHeader("UI")
HStack(spacing: 16) { HStack(spacing: 16) {
if device.appearance != .unsupported { if device.appearance != .unsupported {
@ -99,14 +136,6 @@ struct DeviceContent: View {
.opacity(isBooted ? 1.0 : 0.5) .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) .environment(\.isEnabled, isBooted)
.onAppear { .onAppear {
@ -124,21 +153,24 @@ struct DeviceContent: View {
} }
} }
} }
func saveScreen() { func saveScreen(_ type: SaveType = .image) {
let savePanel = NSSavePanel() let savePanel = NSSavePanel()
savePanel.allowedContentTypes = [.png] savePanel.allowedContentTypes = type.allowedContentTypes
savePanel.canCreateDirectories = true savePanel.canCreateDirectories = true
savePanel.isExtensionHidden = false savePanel.isExtensionHidden = false
savePanel.title = "Save Screen" savePanel.title = type.title
savePanel.message = "Select destination" savePanel.message = "Select destination"
savePanel.nameFieldLabel = "Filename:" savePanel.nameFieldLabel = "Filename:"
savePanel.nameFieldStringValue = "\(device.name) - \(fileDateFormatter.string(from: Date()))" savePanel.nameFieldStringValue = "\(device.name) - \(fileDateFormatter.string(from: Date()))"
if savePanel.runModal() == .OK { if savePanel.runModal() == .OK {
if let url = savePanel.url { if let url = savePanel.url {
device.saveScreen(url) switch type {
case .image: device.saveScreen(url)
case .video: device.saveVideo(url)
}
} }
} }
} }

View file

@ -8,17 +8,19 @@
import SwiftUI import SwiftUI
extension ButtonStyle where Self == SystemIconButtonStyle { extension ButtonStyle where Self == SystemIconButtonStyle {
static func systemIcon(_ imageName: String) -> SystemIconButtonStyle { static func systemIcon(_ imageName: String, active: Bool = false) -> SystemIconButtonStyle {
SystemIconButtonStyle(imageName) SystemIconButtonStyle(imageName, active: active)
} }
} }
struct SystemIconButtonStyle: ButtonStyle { struct SystemIconButtonStyle: ButtonStyle {
@State var isFocused = false @State var isFocused = false
let isActive : Bool
let imageName : String let imageName : String
init(_ imageName: String) { init(_ imageName: String, active: Bool = false) {
self.imageName = imageName self.imageName = imageName
self.isActive = active
} }
func makeBody(configuration: Configuration) -> some View { func makeBody(configuration: Configuration) -> some View {
@ -32,11 +34,12 @@ struct SystemIconButtonStyle: ButtonStyle {
.resizable() .resizable()
.aspectRatio(contentMode: ContentMode.fit) .aspectRatio(contentMode: ContentMode.fit)
.frame(width: 24, height: 24) .frame(width: 24, height: 24)
.foregroundColor(isActive ? .accentColor : foregroundColor(pressed: configuration.isPressed))
} }
configuration.label configuration.label
.foregroundColor(foregroundColor(pressed: configuration.isPressed))
} }
.foregroundColor(foregroundColor(pressed: configuration.isPressed))
.padding(1.0) .padding(1.0)
.onHover { isFocused = $0 } .onHover { isFocused = $0 }
} }