Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions assets/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,22 @@
}
}
},
"Couldn’t Shut Down Simulator": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Couldn’t Shut Down Simulator"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "无法关闭模拟器"
}
}
}
},
"Couldn’t Start Simulator": {
"localizations": {
"en": {
Expand Down Expand Up @@ -769,6 +785,22 @@
}
}
},
"Couldn’t shut down %@.\n%@": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Couldn’t shut down %1$@.\n%2$@"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "无法关闭 %1$@。\n%2$@"
}
}
}
},
"Couldn’t start %@.\n%@": {
"localizations": {
"en": {
Expand Down Expand Up @@ -961,6 +993,22 @@
}
}
},
"Finish or cancel app operations before shutting down this simulator.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Finish or cancel app operations before shutting down this simulator."
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "请先完成或取消应用操作,再关闭此模拟器。"
}
}
}
},
"Games": {
"localizations": {
"en": {
Expand Down Expand Up @@ -1809,6 +1857,22 @@
}
}
},
"Shut Down": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Shut Down"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "关机"
}
}
}
},
"Shutdown": {
"localizations": {
"en": {
Expand Down Expand Up @@ -1841,6 +1905,22 @@
}
}
},
"Shutting Down…": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Shutting Down…"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "正在关机…"
}
}
}
},
"SimStore Is Up to Date": {
"localizations": {
"en": {
Expand Down
5 changes: 5 additions & 0 deletions src/AppInstallationStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ final class AppInstallationStore {
errors[Key(appID: appID, deviceID: deviceID)]
}

func hasOperations(deviceID: String) -> Bool {
phases.keys.contains { $0.deviceID == deviceID }
|| updateBatches[deviceID]?.isRunning == true
}

func install(app: CatalogApp, version: CatalogVersion, on device: SimulatorDevice) {
startInstallation(app: app, version: version, on: device, completion: didInstall)
}
Expand Down
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ add_executable(SimStore MACOSX_BUNDLE
SimulatorStore.swift
SimulatorDevicePresentation.swift
SimulatorSelectionViews.swift
SimulatorPowerButton.swift
ContentView.swift
LibrarySection.swift
AppCatalog.swift
Expand Down
16 changes: 9 additions & 7 deletions src/CatalogBrowserView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ struct CatalogBrowserView: View {
let installedApps: InstalledAppsStore
let installations: AppInstallationStore
let device: SimulatorDevice
let isStarting: Bool
let deviceManagerName: String?
let startSimulator: () -> Void
let shutdownSimulator: () -> Void
let openDeviceManager: () -> Void

@State private var query = ""
Expand Down Expand Up @@ -115,8 +115,9 @@ struct CatalogBrowserView: View {
}
.popover(isPresented: $showsSimulator) {
SimulatorDetailView(
device: device, isStarting: isStarting, deviceManagerName: deviceManagerName,
start: startSimulator, openDeviceManager: openDeviceManager
device: device, hasAppOperations: installations.hasOperations(deviceID: device.id),
deviceManagerName: deviceManagerName,
start: startSimulator, shutdown: shutdownSimulator, openDeviceManager: openDeviceManager
)
.frame(width: 480)
}
Expand Down Expand Up @@ -144,14 +145,15 @@ struct CatalogBrowserView: View {
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: 3) {
Text(verbatim: device.name).fontWeight(.medium)
Text(verbatim: "\(device.runtimeLabel) · \(device.stateLabel(isStarting: isStarting))")
Text(verbatim: "\(device.runtimeLabel) · \(device.stateLabel())")
.font(.system(size: 11)).foregroundStyle(.secondary)
}
.lineLimit(1)
Spacer(minLength: 12)
if device.canStart || isStarting {
SimulatorStartButton(
isStarting: isStarting || device.state == "Booting", action: startSimulator)
if device.canStart || device.canShutdown || device.isTransitioning {
SimulatorPowerButton(
device: device, hasAppOperations: installations.hasOperations(deviceID: device.id),
start: startSimulator, shutdown: shutdownSimulator)
}
Button {
showsSimulator = true
Expand Down
34 changes: 29 additions & 5 deletions src/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ struct ContentView: View {
boot: @escaping @Sendable (String) async throws -> Void = {
try await SimulatorControl.boot($0)
},
shutdown: @escaping @Sendable (String) async throws -> Void = {
try await SimulatorControl.shutdown($0)
},
catalogClient: CatalogClient = .live,
installer: AppInstaller = AppInstaller(),
showDevice: (@MainActor (SimulatorDevice) async throws -> Void)? = nil,
Expand All @@ -40,15 +43,17 @@ struct ContentView: View {
}
var installer = installer
var boot = boot
var shutdown = shutdown
var loadInstalledApps = loadInstalledApps
if case .success(.some(.incompatible)) = preview {
installer.download = { _, _, _ in try PreviewScenario.rejectDeviceAction() }
installer.command = { _, _ in try PreviewScenario.rejectDeviceAction() }
boot = { _ in try PreviewScenario.rejectDeviceAction() }
shutdown = { _ in try PreviewScenario.rejectDeviceAction() }
loadInstalledApps = { _ in [] }
}
#endif
let simulators = SimulatorStore(discover: discover, boot: boot)
let simulators = SimulatorStore(discover: discover, shutdown: shutdown, boot: boot)
_simulators = State(initialValue: simulators)
_catalog = State(initialValue: CatalogStore(client: catalogClient))
let installedApps = InstalledAppsStore(load: loadInstalledApps)
Expand Down Expand Up @@ -103,7 +108,7 @@ struct ContentView: View {
discovery: discovery, isChecking: simulators.isChecking, needsXcode: needsXcode,
section: $librarySection, updateCount: updateCount,
selectedDeviceID: simulators.selectedDeviceID,
startingDeviceIDs: simulators.startingDeviceIDs, selectDevice: simulators.selectDevice,
selectDevice: simulators.selectDevice,
openRepository: openRepository
)
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .topLeading)
Expand Down Expand Up @@ -181,6 +186,17 @@ struct ContentView: View {
} message: {
Text(simulators.startError ?? "")
}
.alert(
"Couldn’t Shut Down Simulator",
isPresented: Binding(
get: { simulators.shutdownError != nil },
set: { if !$0 { simulators.shutdownError = nil } }
)
) {
Button("OK", role: .cancel) { simulators.shutdownError = nil }
} message: {
Text(simulators.shutdownError ?? "")
}
}

@ViewBuilder
Expand All @@ -198,9 +214,9 @@ struct ContentView: View {
section: librarySection,
store: catalog, installedApps: installedApps, installations: installations,
device: device,
isStarting: simulators.isStartingSelectedDevice,
deviceManagerName: deviceManager?.name,
startSimulator: startSimulator,
shutdownSimulator: shutdownSimulator,
openDeviceManager: openDeviceManager
)
.id(librarySection)
Expand All @@ -223,17 +239,25 @@ struct ContentView: View {
}

private func startSimulator() {
let id = simulators.selectedDeviceID
guard let id = simulators.selectedDeviceID else { return }
Task {
let started = await simulators.startSelectedDevice()
let started = await simulators.startDevice(id)
if started, let device = simulators.selectedDevice, device.id == id {
openDeviceManager()
}
}
}

private func shutdownSimulator() {
guard let id = simulators.selectedDeviceID, !installations.hasOperations(deviceID: id) else {
return
}
simulators.shutdownDevice(id)
}

private var shouldMonitorState: Bool {
!simulators.startingDeviceIDs.isEmpty
|| !simulators.shuttingDownDeviceIDs.isEmpty
|| discovery?.devices.contains(where: \.isTransitioning) == true
}

Expand Down
7 changes: 6 additions & 1 deletion src/InstalledAppsStatusView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ struct InstalledAppsStatusView: View {

var body: some View {
Group {
if device.state != "Booted" {
if device.isTransitioning {
VStack(spacing: 14) {
ProgressView()
Text(verbatim: device.stateLabel()).foregroundStyle(.secondary)
}
} else if device.state != "Booted" {
ContentUnavailableView {
Label("Simulator Not Running", systemImage: "power")
} description: {
Expand Down
3 changes: 1 addition & 2 deletions src/SidebarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ struct SidebarView: View {
@Binding var section: LibrarySection
let updateCount: Int?
let selectedDeviceID: String?
let startingDeviceIDs: Set<String>
let selectDevice: (String) -> Void
let openRepository: () -> Void

Expand Down Expand Up @@ -53,7 +52,7 @@ struct SidebarView: View {
VStack(alignment: .leading, spacing: 4) {
Text(device.name).font(.system(size: 12, weight: .medium))
Text(
"\(device.runtimeLabel) · \(device.stateLabel(isStarting: startingDeviceIDs.contains(device.id)))"
"\(device.runtimeLabel) · \(device.stateLabel())"
)
.font(.system(size: 10))
.foregroundStyle(.secondary)
Expand Down
11 changes: 11 additions & 0 deletions src/SimulatorControl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,15 @@ enum SimulatorControl {
}.value
try Task.checkCancellation()
}

static func shutdown(
_ deviceID: String,
command: @escaping Command.Runner = Command.run
) async throws {
try Task.checkCancellation()
try await Task.detached(priority: .userInitiated) {
_ = try Command.simctl(["shutdown", deviceID], using: command)
}.value
try Task.checkCancellation()
}
}
4 changes: 2 additions & 2 deletions src/SimulatorDevicePresentation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ extension SimulatorDevice {

var runtimeLabel: String { String(localized: "iOS \(runtime)") }

func stateLabel(isStarting: Bool = false) -> String {
switch isStarting ? "Booting" : state {
func stateLabel() -> String {
switch state {
case "Booted": String(localized: "Booted")
case "Shutdown": String(localized: "Shutdown")
case "Booting": String(localized: "Booting")
Expand Down
2 changes: 2 additions & 0 deletions src/SimulatorDiscovery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ struct SimulatorDevice: Identifiable, Equatable, Sendable {

var canStart: Bool { state == "Shutdown" || state == "Booting" }

var canShutdown: Bool { state == "Booted" }

var isTransitioning: Bool { state == "Booting" || state == "Shutting Down" }
}

Expand Down
Loading