Merge pull request #24 from RobotsAndPencils/list

Update existing Xcode list view functionality
This commit is contained in:
Brandon Evans 2020-12-28 10:36:15 -07:00 committed by GitHub
commit 018a9a75bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 358 additions and 73 deletions

View file

@ -8,10 +8,12 @@
/* Begin PBXBuildFile section */
63EAA4EB259944450046AB8F /* ProgressButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63EAA4EA259944450046AB8F /* ProgressButton.swift */; };
CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */; };
CA378F992466567600A58CE0 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA378F982466567600A58CE0 /* AppState.swift */; };
CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */; };
CA44901F2463AD34003D8213 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA44901E2463AD34003D8213 /* Tag.swift */; };
CA5D781E257365D6008EDE9D /* PinCodeTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */; };
CA61A6E0259835580008926E /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA61A6DF259835580008926E /* Xcode.swift */; };
CA735109257BF96D00EA9CF8 /* AttributedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA735108257BF96D00EA9CF8 /* AttributedText.swift */; };
CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA73510C257BFCEF00EA9CF8 /* NSAttributedString+.swift */; };
CA9FF83F2594FBC000E47BAF /* Licenses.rtf in Resources */ = {isa = PBXBuildFile; fileRef = CA9FF83E2594FBC000E47BAF /* Licenses.rtf */; };
@ -58,6 +60,8 @@
CAD2E7A62449575000113D76 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CAD2E7A52449575000113D76 /* Assets.xcassets */; };
CAD2E7A92449575000113D76 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CAD2E7A82449575000113D76 /* Preview Assets.xcassets */; };
CAD2E7B82449575100113D76 /* XcodesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD2E7B72449575100113D76 /* XcodesTests.swift */; };
CAFBDB912598FE80003DCC5A /* SelectedXcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDB902598FE80003DCC5A /* SelectedXcode.swift */; };
CAFBDB952598FE96003DCC5A /* FocusedValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDB942598FE96003DCC5A /* FocusedValues.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -72,11 +76,13 @@
/* Begin PBXFileReference section */
63EAA4EA259944450046AB8F /* ProgressButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressButton.swift; sourceTree = "<group>"; };
CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeCommands.swift; sourceTree = "<group>"; };
CA378F982466567600A58CE0 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreButtonStyle.swift; sourceTree = "<group>"; };
CA44901E2463AD34003D8213 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
CA538A0C255A4F1A00E64DD7 /* AppleAPI */ = {isa = PBXFileReference; lastKnownFileType = folder; name = AppleAPI; path = Xcodes/AppleAPI; sourceTree = "<group>"; };
CA5D781D257365D6008EDE9D /* PinCodeTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinCodeTextView.swift; sourceTree = "<group>"; };
CA61A6DF259835580008926E /* Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xcode.swift; sourceTree = "<group>"; };
CA735108257BF96D00EA9CF8 /* AttributedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedText.swift; sourceTree = "<group>"; };
CA73510C257BFCEF00EA9CF8 /* NSAttributedString+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+.swift"; sourceTree = "<group>"; };
CA8FB5F8256E0F9400469DA5 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
@ -127,6 +133,8 @@
CAD2E7B32449575100113D76 /* XcodesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = XcodesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
CAD2E7B72449575100113D76 /* XcodesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodesTests.swift; sourceTree = "<group>"; };
CAD2E7B92449575100113D76 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
CAFBDB902598FE80003DCC5A /* SelectedXcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedXcode.swift; sourceTree = "<group>"; };
CAFBDB942598FE96003DCC5A /* FocusedValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusedValues.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -219,16 +227,20 @@
CABFA9B22592EEEA00380FEE /* Entry+.swift */,
CABFA9A92592EEE900380FEE /* Environment.swift */,
CABFA9B82592EEEA00380FEE /* FileManager+.swift */,
CAFBDB942598FE96003DCC5A /* FocusedValues.swift */,
CABFA9AC2592EEE900380FEE /* Foundation.swift */,
CA9FF8862595607900E47BAF /* InstalledXcode.swift */,
CABFA9AE2592EEE900380FEE /* Path+.swift */,
CABFA9B42592EEEA00380FEE /* Process.swift */,
CABFA9B02592EEEA00380FEE /* Promise+.swift */,
CAFBDB902598FE80003DCC5A /* SelectedXcode.swift */,
CABFA9AB2592EEE900380FEE /* URLRequest+Apple.swift */,
CABFA9B32592EEEA00380FEE /* URLSession+Promise.swift */,
CABFA9A82592EEE900380FEE /* Version+.swift */,
CA9FF876259528CC00E47BAF /* Version+XcodeReleases.swift */,
CABFA9A62592EEE900380FEE /* Version+Xcode.swift */,
CA61A6DF259835580008926E /* Xcode.swift */,
CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */,
);
path = Backend;
sourceTree = "<group>";
@ -459,6 +471,7 @@
buildActionMask = 2147483647;
files = (
CA735109257BF96D00EA9CF8 /* AttributedText.swift in Sources */,
CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */,
CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */,
CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */,
CABFA9CA2592EEEA00380FEE /* AppState+Update.swift in Sources */,
@ -473,15 +486,18 @@
CABFA9C52592EEEA00380FEE /* FileManager+.swift in Sources */,
CABFA9CD2592EEEA00380FEE /* Foundation.swift in Sources */,
CA9FF8872595607900E47BAF /* InstalledXcode.swift in Sources */,
CA61A6E0259835580008926E /* Xcode.swift in Sources */,
CA9FF84E2595079F00E47BAF /* ScrollingTextView.swift in Sources */,
CABFA9C12592EEEA00380FEE /* Version+.swift in Sources */,
CA9FF8522595080100E47BAF /* AcknowledgementsView.swift in Sources */,
CABFA9CE2592EEEA00380FEE /* Version+Xcode.swift in Sources */,
CAFBDB912598FE80003DCC5A /* SelectedXcode.swift in Sources */,
CAA1CB49255A5C97003FD669 /* SignInSMSView.swift in Sources */,
CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */,
CA9FF877259528CC00E47BAF /* Version+XcodeReleases.swift in Sources */,
CABFAA2D2592FBFC00380FEE /* Configure.swift in Sources */,
CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */,
CAFBDB952598FE96003DCC5A /* FocusedValues.swift in Sources */,
CABFA9C22592EEEA00380FEE /* Promise+.swift in Sources */,
CAA1CB4D255A5CFD003FD669 /* SignInPhoneListView.swift in Sources */,
CABFA9CF2592EEEA00380FEE /* Process.swift in Sources */,

View file

@ -5,6 +5,7 @@ import Path
import LegibleError
import KeychainAccess
import SwiftUI
import Version
class AppState: ObservableObject {
private let client = AppleAPI.Client()
@ -13,10 +14,10 @@ class AppState: ObservableObject {
@Published var authenticationState: AuthenticationState = .unauthenticated
@Published var availableXcodes: [AvailableXcode] = [] {
willSet {
updateAllVersions(newValue)
updateAllXcodes(newValue)
}
}
var allVersions: [XcodeVersion] = []
var allXcodes: [Xcode] = []
@Published var updatePublisher: AnyCancellable?
var isUpdating: Bool { updatePublisher != nil }
@Published var error: AlertContent?
@ -24,6 +25,11 @@ class AppState: ObservableObject {
@Published var presentingSignInAlert = false
@Published var isProcessingAuthRequest = false
@Published var secondFactorData: SecondFactorData?
// Selected in the Xcode list, not in the xcode-select sense
// This probably belongs as private @State in XcodeListView,
// but we need it here instead so that it can be a focusedValue at the top level in XcodesApp instead of in a list row. The latter seems more like how the focusedValue API is supposed to work, but currently doesn't.
@Published var selectedXcodeID: Xcode.ID?
@Published var xcodeBeingConfirmedForUninstallation: Xcode?
init() {
try? loadCachedAvailableXcodes()
@ -171,27 +177,39 @@ class AppState: ObservableObject {
// MARK: -
func install(id: String) {
func install(id: Xcode.ID) {
// TODO:
}
func uninstall(id: String) {
func uninstall(id: Xcode.ID) {
// TODO:
}
func reveal(id: String) {
func reveal(id: Xcode.ID) {
// TODO: show error if not
guard let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version.xcodeDescription == id }) else { return }
guard let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version == id }) else { return }
NSWorkspace.shared.activateFileViewerSelecting([installedXcode.path.url])
}
func select(id: String) {
func select(id: Xcode.ID) {
// TODO:
}
func launch(id: Xcode.ID) {
guard let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version == id }) else { return }
NSWorkspace.shared.openApplication(at: installedXcode.path.url, configuration: .init())
}
func copyPath(id: Xcode.ID) {
guard let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version == id }) else { return }
NSPasteboard.general.declareTypes([.URL, .string], owner: nil)
NSPasteboard.general.writeObjects([installedXcode.path.url as NSURL])
NSPasteboard.general.setString(installedXcode.path.string, forType: .string)
}
// MARK: - Private
private func updateAllVersions(_ xcodes: [AvailableXcode]) {
private func updateAllXcodes(_ xcodes: [AvailableXcode]) {
let installedXcodes = Current.files.installedXcodes(Path.root/"Applications")
var allXcodeVersions = xcodes.map { $0.version }
for installedXcode in installedXcodes {
@ -210,37 +228,22 @@ class AppState: ObservableObject {
}
}
allVersions = allXcodeVersions
allXcodes = allXcodeVersions
.sorted(by: >)
.map { xcodeVersion in
let installedXcode = installedXcodes.first(where: { xcodeVersion.isEquivalentForDeterminingIfInstalled(toInstalled: $0.version) })
return XcodeVersion(
title: xcodeVersion.xcodeDescription,
return Xcode(
version: xcodeVersion,
installState: installedXcodes.contains(where: { xcodeVersion.isEquivalentForDeterminingIfInstalled(toInstalled: $0.version) }) ? .installed : .notInstalled,
selected: false,
path: installedXcode?.path.string
path: installedXcode?.path.string,
icon: (installedXcode?.path.string).map(NSWorkspace.shared.icon(forFile:))
)
}
}
// MARK: - Nested Types
/// A merging of AvailableXcode and InstalledXcode prepared for display
struct XcodeVersion: Identifiable {
let title: String
let installState: InstallState
let selected: Bool
let path: String?
var id: String { title }
var installed: Bool { installState == .installed }
}
enum InstallState: Equatable {
case notInstalled
case installing(Progress)
case installed
}
struct AlertContent: Identifiable {
var title: String

View file

@ -0,0 +1,16 @@
import SwiftUI
// MARK: - FocusedXcodeKey
struct FocusedXcodeKey : FocusedValueKey {
typealias Value = SelectedXcode
}
// MARK: - FocusedValues
extension FocusedValues {
var selectedXcode: FocusedXcodeKey.Value? {
get { self[FocusedXcodeKey.self] }
set { self[FocusedXcodeKey.self] = newValue }
}
}

View file

@ -0,0 +1,35 @@
import Foundation
/// As part of the unexpected way we have to use focusedValue in XcodesApp, we need to provide an `Optional<Xcode>` because there isn't always a selected Xcode in the focused window.
/// But FocusedValueKey.Value is already optional, because there might not be a focused UI element to begin with, so the type ends up being `Optional<Optional<Xcode>>`.
/// This is weird enough, but I wasn't able to find a way to have FocusedXcodeKey.Value be `Optional<Optional<Xcode>>` and still compile.
/// There was always an error somewhere in either the use of @FocusedValue or FocusedValues.xcode or .focusedValue, as if it is only ever expecting a single level of optionality.
/// But! If we make our own Optional replica like SelectedXcode, it _does_ compile, and there's some more noise required to turn it back into an `Optional<Xcode>`.
/// All this to say, maybe one day we don't need to have this type at all.
enum SelectedXcode {
case none
case some(Xcode)
init(_ optional: Optional<Xcode>) {
switch optional {
case .none: self = .none
case let .some(xcode): self = .some(xcode)
}
}
var asOptional: Xcode? {
switch self {
case .none: return .none
case let .some(xcode): return .some(xcode)
}
}
}
extension Optional where Wrapped == SelectedXcode {
var unwrapped: Xcode? {
switch self {
case Optional<SelectedXcode>.none: return Optional<Xcode>.none
case let .some(selectedXcode): return selectedXcode.asOptional
}
}
}

View file

@ -0,0 +1,24 @@
import AppKit
import Foundation
import Version
struct Xcode: Identifiable, CustomStringConvertible {
let version: Version
let installState: XcodeInstallState
let selected: Bool
let path: String?
let icon: NSImage?
var id: Version { version }
var installed: Bool { installState == .installed }
var description: String {
version.xcodeDescription
}
}
enum XcodeInstallState: Equatable {
case notInstalled
case installing(Progress)
case installed
}

View file

@ -0,0 +1,172 @@
import SwiftUI
// MARK: - CommandMenu
struct XcodeCommands: Commands {
// CommandMenus don't participate in the environment hierarchy, so we need to shuffle AppState along to the individual Commands manually.
let appState: AppState
var body: some Commands {
CommandMenu("Xcode") {
Group {
InstallCommand()
Divider()
SelectCommand()
LaunchCommand()
RevealCommand()
CopyPathCommand()
}
.environmentObject(appState)
}
}
}
// MARK: - Buttons
// These are used for both context menus and commands
struct InstallButton: View {
@EnvironmentObject var appState: AppState
let xcode: Xcode?
var body: some View {
Button(action: uninstallOrInstall) {
if let xcode = xcode {
Text(xcode.installed == true ? "Uninstall" : "Install")
} else {
Text("Install")
}
}
}
private func uninstallOrInstall() {
guard let xcode = xcode else { return }
if xcode.installed {
appState.xcodeBeingConfirmedForUninstallation = xcode
} else {
appState.install(id: xcode.id)
}
}
}
struct SelectButton: View {
@EnvironmentObject var appState: AppState
let xcode: Xcode?
var body: some View {
Button(action: select) {
Text("Select")
}
}
private func select() {
guard let xcode = xcode else { return }
appState.select(id: xcode.id)
}
}
struct LaunchButton: View {
@EnvironmentObject var appState: AppState
let xcode: Xcode?
var body: some View {
Button(action: launch) {
Text("Launch")
}
}
private func launch() {
guard let xcode = xcode else { return }
appState.launch(id: xcode.id)
}
}
struct RevealButton: View {
@EnvironmentObject var appState: AppState
let xcode: Xcode?
var body: some View {
Button(action: reveal) {
Text("Reveal in Finder")
}
}
private func reveal() {
guard let xcode = xcode else { return }
appState.reveal(id: xcode.id)
}
}
struct CopyPathButton: View {
@EnvironmentObject var appState: AppState
let xcode: Xcode?
var body: some View {
Button(action: copyPath) {
Text("Copy Path")
}
}
private func copyPath() {
guard let xcode = xcode else { return }
appState.copyPath(id: xcode.id)
}
}
// MARK: - Commands
struct InstallCommand: View {
@EnvironmentObject var appState: AppState
@FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode?
var body: some View {
InstallButton(xcode: selectedXcode.unwrapped)
.keyboardShortcut(selectedXcode.unwrapped?.installed == true ? "u" : "i", modifiers: [.command, .option])
.disabled(selectedXcode.unwrapped == nil)
}
}
struct SelectCommand: View {
@EnvironmentObject var appState: AppState
@FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode?
var body: some View {
SelectButton(xcode: selectedXcode.unwrapped)
.keyboardShortcut("s", modifiers: [.command, .option])
.disabled(selectedXcode.unwrapped?.installed != true)
}
}
struct LaunchCommand: View {
@EnvironmentObject var appState: AppState
@FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode?
var body: some View {
LaunchButton(xcode: selectedXcode.unwrapped)
.keyboardShortcut(KeyboardShortcut(.downArrow, modifiers: .command))
.disabled(selectedXcode.unwrapped?.installed != true)
}
}
struct RevealCommand: View {
@EnvironmentObject var appState: AppState
@FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode?
var body: some View {
RevealButton(xcode: selectedXcode.unwrapped)
.keyboardShortcut("r", modifiers: [.command, .option])
.disabled(selectedXcode.unwrapped?.installed != true)
}
}
struct CopyPathCommand: View {
@EnvironmentObject var appState: AppState
@FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode?
var body: some View {
CopyPathButton(xcode: selectedXcode.unwrapped)
.keyboardShortcut("c", modifiers: [.command, .option])
.disabled(selectedXcode.unwrapped?.installed != true)
}
}

View file

@ -4,27 +4,25 @@ import PromiseKit
struct XcodeListView: View {
@EnvironmentObject var appState: AppState
@State private var selection = Set<String>()
@State private var rowBeingConfirmedForUninstallation: AppState.XcodeVersion?
@State private var selection: Xcode.ID?
@State private var searchText: String = ""
@AppStorage("lastUpdated") private var lastUpdated: Double?
@AppStorage("xcodeListCategory") private var category: Category = .all
var visibleVersions: [AppState.XcodeVersion] {
var versions: [AppState.XcodeVersion]
var visibleXcodes: [Xcode] {
var xcodes: [Xcode]
switch category {
case .all:
versions = appState.allVersions
xcodes = appState.allXcodes
case .installed:
versions = appState.allVersions.filter { $0.installed }
xcodes = appState.allXcodes.filter { $0.installed }
}
if !searchText.isEmpty {
versions = versions.filter { $0.title.contains(searchText) }
xcodes = xcodes.filter { $0.description.contains(searchText) }
}
return versions
return xcodes
}
enum Category: String, CaseIterable, Identifiable, CustomStringConvertible {
@ -42,38 +40,43 @@ struct XcodeListView: View {
}
var body: some View {
List(visibleVersions, selection: $selection) { row in
VStack(alignment: .leading) {
HStack {
Text(row.title)
List(visibleXcodes, selection: $appState.selectedXcodeID) { xcode in
HStack {
appIconView(for: xcode)
VStack(alignment: .leading) {
Text(xcode.description)
.font(.body)
if row.selected {
Tag(text: "SELECTED")
.foregroundColor(.green)
}
Spacer()
Button(row.installed ? "INSTALLED" : "INSTALL") {
print("Installing...")
}
.buttonStyle(AppStoreButtonStyle(installed: row.installed,
highlighted: self.selection.contains(row.id)))
.disabled(row.installed)
Text(verbatim: xcode.path ?? "")
.font(.caption)
.foregroundColor(appState.selectedXcodeID == xcode.id ? Color(NSColor.selectedMenuItemTextColor) : Color(NSColor.secondaryLabelColor))
}
Text(verbatim: row.path ?? "")
.font(.caption)
.foregroundColor(self.selection.contains(row.id) ? Color(NSColor.selectedMenuItemTextColor) : Color(NSColor.secondaryLabelColor))
if xcode.selected {
Tag(text: "SELECTED")
.foregroundColor(.green)
}
Spacer()
Button(xcode.installed ? "INSTALLED" : "INSTALL") {
print("Installing...")
}
.buttonStyle(AppStoreButtonStyle(installed: xcode.installed,
highlighted: appState.selectedXcodeID == xcode.id))
.disabled(xcode.installed)
}
.contextMenu {
Button(action: { row.installed ? self.rowBeingConfirmedForUninstallation = row : self.appState.install(id: row.id) }) {
Text(row.installed ? "Uninstall" : "Install")
}
if row.installed {
Button(action: { self.appState.reveal(id: row.id) }) {
Text("Reveal in Finder")
}
Button(action: { self.appState.select(id: row.id) }) {
Text("Select")
}
InstallButton(xcode: xcode)
Divider()
if xcode.installed {
SelectButton(xcode: xcode)
LaunchButton(xcode: xcode)
RevealButton(xcode: xcode)
CopyPathButton(xcode: xcode)
}
}
}
@ -114,10 +117,10 @@ struct XcodeListView: View {
}
/*
Removing this for now, because it's overriding the error alert that's being worked on above.
.alert(item: self.$rowBeingConfirmedForUninstallation) { row in
Alert(title: Text("Uninstall Xcode \(row.title)?"),
.alert(item: $appState.xcodeBeingConfirmedForUninstallation) { xcode in
Alert(title: Text("Uninstall Xcode \(xcode.description)?"),
message: Text("It will be moved to the Trash, but won't be emptied."),
primaryButton: .destructive(Text("Uninstall"), action: { self.appState.uninstall(id: row.id) }),
primaryButton: .destructive(Text("Uninstall"), action: { self.appState.uninstall(id: xcode.id) }),
secondaryButton: .cancel(Text("Cancel")))
}
**/
@ -146,6 +149,17 @@ struct XcodeListView: View {
return Text("")
}
}
@ViewBuilder
func appIconView(for xcode: Xcode) -> some View {
if let icon = xcode.icon {
Image(nsImage: icon)
} else {
Color.clear
.frame(width: 32, height: 32)
.foregroundColor(.secondary)
}
}
}
struct XcodeListView_Previews: PreviewProvider {
@ -154,11 +168,11 @@ struct XcodeListView_Previews: PreviewProvider {
XcodeListView()
.environmentObject({ () -> AppState in
let a = AppState()
a.allVersions = [
AppState.XcodeVersion(title: "12.3", installState: .installed, selected: true, path: nil),
AppState.XcodeVersion(title: "12.2", installState: .notInstalled, selected: false, path: nil),
AppState.XcodeVersion(title: "12.1", installState: .notInstalled, selected: false, path: nil),
AppState.XcodeVersion(title: "12.0", installState: .installed, selected: false, path: nil),
a.allXcodes = [
Xcode(version: Version("12.3.0")!, installState: .installed, selected: true, path: nil, icon: nil),
Xcode(version: Version("12.2.0")!, installState: .notInstalled, selected: false, path: nil, icon: nil),
Xcode(version: Version("12.1.0")!, installState: .notInstalled, selected: false, path: nil, icon: nil),
Xcode(version: Version("12.0.0")!, installState: .installed, selected: false, path: nil, icon: nil),
]
return a
}())

View file

@ -20,6 +20,9 @@ struct XcodesApp: App {
appState.updateIfNeeded()
}
}
// I'm expecting to be able to use this modifier on a List row, but using it at the top level here is the only way that has made XcodeCommands work so far.
// FB8954571 focusedValue(_:_:) on List row doesn't propagate value to @FocusedValue
.focusedValue(\.selectedXcode, SelectedXcode(appState.allXcodes.first { $0.id == appState.selectedXcodeID }))
}
.commands {
CommandGroup(replacing: .appInfo) {
@ -34,6 +37,8 @@ struct XcodesApp: App {
.keyboardShortcut(KeyEquivalent("r"))
.disabled(appState.isUpdating)
}
XcodeCommands(appState: appState)
}
Settings {