diff --git a/SimDirs.xcodeproj/project.pbxproj b/SimDirs.xcodeproj/project.pbxproj index 2ca3007..1a05d63 100644 --- a/SimDirs.xcodeproj/project.pbxproj +++ b/SimDirs.xcodeproj/project.pbxproj @@ -31,6 +31,8 @@ C982F877283D020C00D491F4 /* SimProductFamily.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F876283D020C00D491F4 /* SimProductFamily.swift */; }; C982F879283D042E00D491F4 /* SimModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F878283D042E00D491F4 /* SimModel.swift */; }; C982F87B283E40C800D491F4 /* SimCtl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C982F87A283E40C800D491F4 /* SimCtl.swift */; }; + C9BF5232289FE95D00BDDC91 /* DescriptiveToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9BF5231289FE95D00BDDC91 /* DescriptiveToggle.swift */; }; + C9BF5234289FE99600BDDC91 /* DescriptiveToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9BF5233289FE99600BDDC91 /* DescriptiveToggleStyle.swift */; }; C9D73C25285C8C0C0044A279 /* SourceItemData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D73C24285C8C0C0044A279 /* SourceItemData.swift */; }; C9D73C29285C8C4B0044A279 /* SourceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D73C28285C8C4B0044A279 /* SourceItem.swift */; }; C9DD54C32860936D00D46AB3 /* SourceItemGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9DD54C22860936D00D46AB3 /* SourceItemGroup.swift */; }; @@ -72,6 +74,8 @@ C982F876283D020C00D491F4 /* SimProductFamily.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimProductFamily.swift; sourceTree = ""; }; C982F878283D042E00D491F4 /* SimModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimModel.swift; sourceTree = ""; }; C982F87A283E40C800D491F4 /* SimCtl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimCtl.swift; sourceTree = ""; }; + C9BF5231289FE95D00BDDC91 /* DescriptiveToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptiveToggle.swift; sourceTree = ""; }; + C9BF5233289FE99600BDDC91 /* DescriptiveToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptiveToggleStyle.swift; sourceTree = ""; }; C9D73C24285C8C0C0044A279 /* SourceItemData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceItemData.swift; sourceTree = ""; }; C9D73C28285C8C4B0044A279 /* SourceItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceItem.swift; sourceTree = ""; }; C9DD54C22860936D00D46AB3 /* SourceItemGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceItemGroup.swift; sourceTree = ""; }; @@ -159,6 +163,8 @@ C9DD54CC2860992200D46AB3 /* Model Views */, C90DCC152896B0370072E403 /* AppearancePicker.swift */, C90DCC132896AAAA0072E403 /* ContentHeader.swift */, + C9BF5231289FE95D00BDDC91 /* DescriptiveToggle.swift */, + C9BF5233289FE99600BDDC91 /* DescriptiveToggleStyle.swift */, C90BCE472861D70500C2EF35 /* ErrorView.swift */, C927A0DA2846502300533D66 /* PathActions.swift */, C9EE0CD128478FDB00E9B97A /* PathRow.swift */, @@ -281,6 +287,7 @@ C927A0DB2846502300533D66 /* PathActions.swift in Sources */, C90BCE442861D3C500C2EF35 /* DeviceContent.swift in Sources */, C90DCC142896AAAA0072E403 /* ContentHeader.swift in Sources */, + C9BF5232289FE95D00BDDC91 /* DescriptiveToggle.swift in Sources */, C90BCE502861E9D000C2EF35 /* SourceState.swift in Sources */, C90BCE4A2861DA6700C2EF35 /* RuntimeHeader.swift in Sources */, C9EE0CD42847B79E00E9B97A /* SimApp.swift in Sources */, @@ -295,6 +302,7 @@ C982F85B283B9F9000D491F4 /* ContentView.swift in Sources */, C982F875283CEEBB00D491F4 /* SimDevice.swift in Sources */, C9DD54C52860938C00D46AB3 /* SourceItemLink.swift in Sources */, + C9BF5234289FE99600BDDC91 /* DescriptiveToggleStyle.swift in Sources */, C982F873283CE9AD00D491F4 /* SimDeviceType.swift in Sources */, C9DD54CB2860948600D46AB3 /* SourceItemLabel.swift in Sources */, C90BCE482861D70500C2EF35 /* ErrorView.swift in Sources */, diff --git a/SimDirs/Assets.xcassets/Colors/CircleSymbolBkgOff.colorset/Contents.json b/SimDirs/Assets.xcassets/Colors/CircleSymbolBkgOff.colorset/Contents.json new file mode 100644 index 0000000..48f315f --- /dev/null +++ b/SimDirs/Assets.xcassets/Colors/CircleSymbolBkgOff.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.804", + "green" : "0.804", + "red" : "0.804" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.310", + "green" : "0.310", + "red" : "0.310" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SimDirs/Assets.xcassets/Colors/CircleSymbolOff.colorset/Contents.json b/SimDirs/Assets.xcassets/Colors/CircleSymbolOff.colorset/Contents.json new file mode 100644 index 0000000..143f72a --- /dev/null +++ b/SimDirs/Assets.xcassets/Colors/CircleSymbolOff.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.306", + "green" : "0.306", + "red" : "0.306" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.796", + "green" : "0.796", + "red" : "0.796" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SimDirs/Assets.xcassets/Colors/RoundedBackground.colorset/Contents.json b/SimDirs/Assets.xcassets/Colors/RoundedBackground.colorset/Contents.json new file mode 100644 index 0000000..2ba8c3d --- /dev/null +++ b/SimDirs/Assets.xcassets/Colors/RoundedBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.902", + "green" : "0.902", + "red" : "0.902" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.173", + "green" : "0.173", + "red" : "0.173" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SimDirs/Assets.xcassets/Colors/RoundedBorder.colorset/Contents.json b/SimDirs/Assets.xcassets/Colors/RoundedBorder.colorset/Contents.json new file mode 100644 index 0000000..8009850 --- /dev/null +++ b/SimDirs/Assets.xcassets/Colors/RoundedBorder.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.902", + "green" : "0.902", + "red" : "0.902" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.294", + "green" : "0.294", + "red" : "0.294" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SimDirs/Model/SimCtl.swift b/SimDirs/Model/SimCtl.swift index 8b51ac9..683bdbb 100644 --- a/SimDirs/Model/SimCtl.swift +++ b/SimDirs/Model/SimCtl.swift @@ -105,4 +105,14 @@ struct SimCtl { func setDeviceContentSize(_ device: SimDevice, contentSize: SimDevice.ContentSize) throws { try runAsync(args: ["ui", device.udid, "content_size", contentSize.rawValue]) } + + func getDeviceIncreaseContrast(_ device: SimDevice) async throws -> SimDevice.IncreaseContrast { + let increaseContrast : String = try await runAsync(args: ["ui", device.udid, "increase_contrast"]).trimmingCharacters(in: .whitespacesAndNewlines) + + return SimDevice.IncreaseContrast(rawValue: increaseContrast) ?? .unknown + } + + func setDeviceIncreaseContrast(_ device: SimDevice, increaseContrast: SimDevice.IncreaseContrast) throws { + try runAsync(args: ["ui", device.udid, "increase_contrast", increaseContrast.rawValue]) + } } diff --git a/SimDirs/Model/SimDevice.swift b/SimDirs/Model/SimDevice.swift index f094cdf..5bb2e28 100644 --- a/SimDirs/Model/SimDevice.swift +++ b/SimDirs/Model/SimDevice.swift @@ -26,6 +26,7 @@ class SimDevice: ObservableObject, Decodable { @Published var availabilityError : String? @Published var appearance = Appearance.unknown @Published var contentSize = ContentSize.unknown + @Published var increaseContrast = IncreaseContrast.unknown var isTransitioning : Bool { state == .booting || state == .shuttingDown } var isBooted : Bool { get { state.showBooted == true } @@ -143,6 +144,13 @@ class SimDevice: ObservableObject, Decodable { await MainActor.run { contentSize = result } } } + if increaseContrast == .unknown { + Task { + let result = try await SimCtl().getDeviceIncreaseContrast(self) + + await MainActor.run { increaseContrast = result } + } + } } func setAppearance(_ appearance: Appearance) { @@ -164,6 +172,16 @@ class SimDevice: ObservableObject, Decodable { print("Failed to set device content size: \(error)") } } + + func setIncreaseContrast(_ increaseContrast: IncreaseContrast) { + self.increaseContrast = increaseContrast // optimistic + + do { + try SimCtl().setDeviceIncreaseContrast(self, increaseContrast: increaseContrast) + } catch { + print("Failed to set device increase contrast: \(error)") + } + } } extension SimDevice { @@ -244,11 +262,21 @@ extension SimDevice { } } } + + enum IncreaseContrast: String { + case enabled = "enabled" + case disabled = "disabled" + case unsupported = "unsupported" + case unknown = "unknown" + + var isOn : Bool { self == .enabled } + } } extension SimDevice: SourceItemData { var title : String { return name } var headerTitle : String { "Device: \(title)" } + var isEnabled : Bool { isBooted } var imageDesc : SourceImageDesc { .symbol(systemName: "questionmark.circle", color: isAvailable ? .green : .red) } } diff --git a/SimDirs/Views/DescriptiveToggle.swift b/SimDirs/Views/DescriptiveToggle.swift new file mode 100644 index 0000000..3b5b2d5 --- /dev/null +++ b/SimDirs/Views/DescriptiveToggle.swift @@ -0,0 +1,50 @@ +// +// DescriptiveToggle.swift +// SimDirs +// +// Created by Casey Fleser on 8/7/22. +// + +import SwiftUI + +protocol ToggleDescriptor { + var isOn : Bool { get } + var titleKey : LocalizedStringKey { get } + var text : String { get } + var image : Image { get } +} + +extension ToggleDescriptor { + var circleColor : Color { isOn ? .accentColor : Color("CircleSymbolBkgOff") } +} + +struct DescriptiveToggle: View { + @Binding var isOn : Bool + var descriptor : T + + init(_ descriptor: T, isOn: Binding) { + self._isOn = isOn + self.descriptor = descriptor + } + + var body: some View { + Toggle(descriptor.titleKey, isOn: _isOn) + .toggleStyle(DescriptiveToggleStyle(descriptor)) + } +} + +struct DescriptiveToggle_Previews: PreviewProvider { + struct DarkMode: ToggleDescriptor { + var isOn : Bool = true + var titleKey : LocalizedStringKey { "Dark Mode" } + var text : String { isOn ? "On" : "Off" } + var image : Image { Image(systemName: "circle.circle") } + } + + @State static var toggle = DarkMode() + + static var previews: some View { + DescriptiveToggle(DarkMode(), isOn: $toggle.isOn) + .disabled(true) + } +} diff --git a/SimDirs/Views/DescriptiveToggleStyle.swift b/SimDirs/Views/DescriptiveToggleStyle.swift new file mode 100644 index 0000000..6265971 --- /dev/null +++ b/SimDirs/Views/DescriptiveToggleStyle.swift @@ -0,0 +1,46 @@ +// +// DescriptiveToggleStyle.swift +// SimDirs +// +// Created by Casey Fleser on 8/7/22. +// + +import SwiftUI + +struct DescriptiveToggleStyle: ToggleStyle { + var descriptor : T + + init(_ descriptor: T) { + self.descriptor = descriptor + } + + func makeBody(configuration: Configuration) -> some View { + Button(action: { configuration.isOn.toggle() }) { + VStack(spacing: 0) { + ZStack { + Circle() + .foregroundColor(descriptor.circleColor) + descriptor.image + .resizable() + .foregroundColor(descriptor.isOn ? .white : Color("CircleSymbolOff")) + .aspectRatio(contentMode: .fit) + .padding(9) + } + .frame(width: 36, height: 36) + .padding(.bottom, 4) + + Group { + Text(descriptor.titleKey) + .fontWeight(.semibold) + Text(descriptor.text) + .foregroundColor(.secondary) + } + .font(.system(size: 11)) + .allowsTightening(true) + .minimumScaleFactor(0.5) + .multilineTextAlignment(.center) + } + } + .buttonStyle(.plain) + } +} diff --git a/SimDirs/Views/Model Views/DeviceContent.swift b/SimDirs/Views/Model Views/DeviceContent.swift index 172fe78..b4c66ba 100644 --- a/SimDirs/Views/Model Views/DeviceContent.swift +++ b/SimDirs/Views/Model Views/DeviceContent.swift @@ -8,8 +8,9 @@ import SwiftUI extension SimDevice { - public var content : some View { DeviceContent(device: self) } - var scheme : ColorScheme? { + public var content : some View { DeviceContent(self) } + + var scheme : ColorScheme? { get { switch appearance { case .light: return .light @@ -25,17 +26,36 @@ extension SimDevice { } } } - var contentSizeVal : Double { + + var contentSizeVal : Double { get { Double(contentSize.intValue) } set { setContenSize(ContentSize(intValue: Int(newValue))) } } + + var isIncreaseContrast : Bool { + get { increaseContrast.isOn } + set { setIncreaseContrast(newValue ? .enabled : .disabled) } + } +} + +extension SimDevice.IncreaseContrast: ToggleDescriptor { + var titleKey : LocalizedStringKey { "Increase Contrast" } + var text : String { rawValue.capitalized } + var image : Image { Image(systemName: "circle.lefthalf.filled") } } struct DeviceContent: View { @ObservedObject var device : SimDevice + @State var isBooted : Bool + + init(_ device: SimDevice) { + self.device = device + self.isBooted = device.isBooted + } var body: some View { VStack(alignment: .leading, spacing: 3.0) { + ContentHeader("Paths") Group { if !device.isAvailable { ErrorView( @@ -43,37 +63,50 @@ struct DeviceContent: View { description: device.availabilityError ?? "Unknown Error") } - ContentHeader("Paths") PathRow(title: "Data Path", path: device.dataPath) PathRow(title: "Log Path", path: device.logPath) - - ContentHeader("UI") - HStack(spacing: 32) { - if device.appearance != .unsupported { - AppearancePicker(scheme: $device.scheme) - } - if device.contentSize != .unsupported { - VStack { - HStack { - Image(systemName: "textformat.size") - .imageScale(.small) - Slider(value: $device.contentSizeVal, in: SimDevice.ContentSize.range, step: 1) - Image(systemName: "textformat.size") - .imageScale(.large) - } - Text("Content Size") - } - } - } - .disabled(!device.isBooted) } .font(.subheadline) .textSelection(.enabled) - .lineLimit(1) + + ContentHeader("UI") + HStack(spacing: 16) { + if device.appearance != .unsupported { + AppearancePicker(scheme: $device.scheme) + } + if device.appearance != .unsupported { + DescriptiveToggle(device.increaseContrast, isOn: $device.isIncreaseContrast) + } + if device.contentSize != .unsupported { + VStack { + HStack { + Image(systemName: "textformat.size") + .imageScale(.small) + Slider(value: $device.contentSizeVal, in: SimDevice.ContentSize.range, step: 1) + Image(systemName: "textformat.size") + .imageScale(.large) + } + Text("Content Size") + } + .opacity(isBooted ? 1.0 : 0.5) + } + } } + .environment(\.isEnabled, isBooted) .onAppear { device.discoverUI() } + .onChange(of: device.state) { state in + let trulyBooted = state == .booted + + if isBooted != trulyBooted { + isBooted = trulyBooted + + if isBooted { + device.discoverUI() + } + } + } } } @@ -82,9 +115,9 @@ struct DeviceContent_Previews: PreviewProvider { static var previews: some View { if !devices.isEmpty { - DeviceContent(device: devices[0]) + DeviceContent(devices[0]) .preferredColorScheme(.light) - DeviceContent(device: devices.randomElement() ?? devices[0]) + DeviceContent(devices.randomElement() ?? devices[0]) .preferredColorScheme(.dark) } else {