From 7bfb94d75a596312e19f4599e9acacdb8280b3bb Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Thu, 31 Dec 2020 12:36:31 -0600 Subject: [PATCH 1/3] Uninstall a xcode version --- Xcodes/Backend/AppState.swift | 28 ++++++++- Xcodes/Backend/HelperClient.swift | 24 ++++++++ Xcodes/Backend/XcodeCommands.swift | 60 ++++++++++++++----- Xcodes/Frontend/XcodeList/InfoPane.swift | 20 +++++++ .../Frontend/XcodeList/XcodeListViewRow.swift | 8 +-- 5 files changed, 117 insertions(+), 23 deletions(-) diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 42b91b8..527ccfa 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -12,6 +12,7 @@ class AppState: ObservableObject { private let helperClient = HelperClient() private var cancellables = Set() private var selectPublisher: AnyCancellable? + private var uninstallPublisher: AnyCancellable? @Published var authenticationState: AuthenticationState = .unauthenticated @Published var availableXcodes: [AvailableXcode] = [] { @@ -32,7 +33,6 @@ class AppState: ObservableObject { @Published var presentingSignInAlert = false @Published var isProcessingAuthRequest = false @Published var secondFactorData: SecondFactorData? - @Published var xcodeBeingConfirmedForUninstallation: Xcode? @Published var helperInstallState: HelperInstallState = .notInstalled init() { @@ -200,14 +200,36 @@ class AppState: ObservableObject { .store(in: &cancellables) } - // MARK: - + // MARK: - Install func install(id: Xcode.ID) { // TODO: } + // MARK: - Uninstall func uninstall(id: Xcode.ID) { - // TODO: + if helperInstallState == .notInstalled { + installHelper() + } + + guard + let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version == id }), + uninstallPublisher == nil + else { return } + + uninstallPublisher = HelperClient().uninstallXcode(installedXcode.path) + .flatMap { [unowned self] _ in + self.updateSelectedXcodePath() + } + .sink( + receiveCompletion: { [unowned self] completion in + if case let .failure(error) = completion { + self.error = AlertContent(title: "Error uninstalling Xcode", message: error.legibleLocalizedDescription) + } + self.uninstallPublisher = nil + }, + receiveValue: { _ in } + ) } func reveal(id: Xcode.ID) { diff --git a/Xcodes/Backend/HelperClient.swift b/Xcodes/Backend/HelperClient.swift index 5ab5c7b..0c9d246 100644 --- a/Xcodes/Backend/HelperClient.swift +++ b/Xcodes/Backend/HelperClient.swift @@ -1,5 +1,6 @@ import Combine import Foundation +import Path final class HelperClient { private var connection: NSXPCConnection? @@ -103,4 +104,27 @@ final class HelperClient { .map { $0.0 } .eraseToAnyPublisher() } + + func uninstallXcode(_ path: Path) -> AnyPublisher { + let connectionErrorSubject = PassthroughSubject() + + return Deferred { + Future { promise in + do { + try Current.files.trashItem(at: path.url) + promise(.success(())) + } catch { + promise(.failure(error)) + } + } + } + // Take values, but fail when connectionErrorSubject fails + .zip( + connectionErrorSubject + .prepend("") + .map { _ in Void() } + ) + .map { $0.0 } + .eraseToAnyPublisher() + } } diff --git a/Xcodes/Backend/XcodeCommands.swift b/Xcodes/Backend/XcodeCommands.swift index 9a4b1c3..d9c75e1 100644 --- a/Xcodes/Backend/XcodeCommands.swift +++ b/Xcodes/Backend/XcodeCommands.swift @@ -9,6 +9,7 @@ struct XcodeCommands: Commands { var body: some Commands { CommandMenu("Xcode") { Group { + InstallCommand() Divider() @@ -17,6 +18,7 @@ struct XcodeCommands: Commands { OpenCommand() RevealCommand() CopyPathCommand() + UninstallCommand() } .environmentObject(appState) } @@ -31,24 +33,15 @@ struct InstallButton: View { let xcode: Xcode? var body: some View { - Button(action: uninstallOrInstall) { - if let xcode = xcode { - Text(xcode.installed == true ? "Uninstall" : "Install") - .help(xcode.installed == true ? "Uninstall" : "Install") - } else { - Text("Install") - .help("Install") - } + Button(action: install) { + Text("Install") + .help("Install") } } - private func uninstallOrInstall() { + private func install() { guard let xcode = xcode else { return } - if xcode.installed { - appState.xcodeBeingConfirmedForUninstallation = xcode - } else { - appState.install(id: xcode.id) - } + appState.install(id: xcode.id) } } @@ -91,6 +84,30 @@ struct OpenButton: View { } } +struct UninstallButton: View { + @EnvironmentObject var appState: AppState + let xcode: Xcode? + + @State private var showingAlert = false + var alert: Alert { + 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: xcode!.id) }), + secondaryButton: .cancel(Text("Cancel"))) + } + + var body: some View { + Button(action: { + self.showingAlert = true + }) { + Text("Uninstall") + } + .foregroundColor(.red) + .help("Uninstall") + .alert(isPresented:$showingAlert, content: { self.alert }) + } +} + struct RevealButton: View { @EnvironmentObject var appState: AppState let xcode: Xcode? @@ -133,8 +150,8 @@ struct InstallCommand: View { var body: some View { InstallButton(xcode: selectedXcode.unwrapped) - .keyboardShortcut(selectedXcode.unwrapped?.installed == true ? "u" : "i", modifiers: [.command, .option]) - .disabled(selectedXcode.unwrapped == nil) + .keyboardShortcut("i", modifiers: [.command, .option]) + .disabled(selectedXcode.unwrapped?.installed == true) } } @@ -181,3 +198,14 @@ struct CopyPathCommand: View { .disabled(selectedXcode.unwrapped?.installed != true) } } + +struct UninstallCommand: View { + @EnvironmentObject var appState: AppState + @FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode? + + var body: some View { + UninstallButton(xcode: selectedXcode.unwrapped) + .keyboardShortcut("u", modifiers: [.command, .option]) + .disabled(selectedXcode.unwrapped?.installed != true) + } +} diff --git a/Xcodes/Frontend/XcodeList/InfoPane.swift b/Xcodes/Frontend/XcodeList/InfoPane.swift index 9395fab..15bfd24 100644 --- a/Xcodes/Frontend/XcodeList/InfoPane.swift +++ b/Xcodes/Frontend/XcodeList/InfoPane.swift @@ -50,6 +50,11 @@ struct InfoPane: View { sdks(for: xcode) compilers(for: xcode) + if xcode.path != nil { + VStack(alignment: .leading) { + UninstallButton(xcode: xcode) + } + } Spacer() } } else { @@ -250,6 +255,21 @@ struct InfoPane_Previews: PreviewProvider { }) .previewDisplayName("Populated, Uninstalled") + InfoPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 0)) + .environmentObject(configure(AppState()) { + $0.allXcodes = [ + .init( + version: Version(major: 12, minor: 3, patch: 0), + installState: .installed, + selected: false, + path: "/Applications/Xcode-12.3.0.app", + icon: nil, + sdks: nil, + compilers: nil) + ] + }) + .previewDisplayName("Basic, installed") + InfoPane(selectedXcodeID: nil) .environmentObject(configure(AppState()) { $0.allXcodes = [ diff --git a/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift b/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift index 1b1b16c..ad8e2ec 100644 --- a/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift +++ b/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift @@ -26,15 +26,15 @@ struct XcodeListViewRow: View { installControl(for: xcode) } .contextMenu { - InstallButton(xcode: xcode) - - Divider() - if xcode.installed { SelectButton(xcode: xcode) OpenButton(xcode: xcode) RevealButton(xcode: xcode) CopyPathButton(xcode: xcode) + Divider() + UninstallButton(xcode: xcode) + } else { + InstallButton(xcode: xcode) } } } From 72bdadb796b8eccd407a19db2ac93f8ec428a6ec Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Thu, 31 Dec 2020 16:06:03 -0600 Subject: [PATCH 2/3] PR Cleanup --- Xcodes/Backend/AppState.swift | 28 ++++++++++++++++++++++-- Xcodes/Backend/HelperClient.swift | 24 -------------------- Xcodes/Backend/XcodeCommands.swift | 14 ++++-------- Xcodes/Frontend/MainWindow.swift | 18 ++++++--------- Xcodes/Frontend/XcodeList/InfoPane.swift | 10 ++++----- 5 files changed, 41 insertions(+), 53 deletions(-) diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 527ccfa..1620445 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -4,7 +4,7 @@ import Combine import Path import LegibleError import KeychainAccess -import SwiftUI +import Path import Version class AppState: ObservableObject { @@ -33,6 +33,7 @@ class AppState: ObservableObject { @Published var presentingSignInAlert = false @Published var isProcessingAuthRequest = false @Published var secondFactorData: SecondFactorData? + @Published var xcodeBeingConfirmedForUninstallation: Xcode? @Published var helperInstallState: HelperInstallState = .notInstalled init() { @@ -217,7 +218,7 @@ class AppState: ObservableObject { uninstallPublisher == nil else { return } - uninstallPublisher = HelperClient().uninstallXcode(installedXcode.path) + uninstallPublisher = uninstallXcode(path: installedXcode.path) .flatMap { [unowned self] _ in self.updateSelectedXcodePath() } @@ -315,6 +316,29 @@ class AppState: ObservableObject { } } + + private func uninstallXcode(path: Path) -> AnyPublisher { + let connectionErrorSubject = PassthroughSubject() + + return Deferred { + Future { promise in + do { + try Current.files.trashItem(at: path.url) + promise(.success(())) + } catch { + promise(.failure(error)) + } + } + } + // Take values, but fail when connectionErrorSubject fails + .zip( + connectionErrorSubject + .prepend("") + .map { _ in Void() } + ) + .map { $0.0 } + .eraseToAnyPublisher() + } // MARK: - Nested Types diff --git a/Xcodes/Backend/HelperClient.swift b/Xcodes/Backend/HelperClient.swift index 0c9d246..5ab5c7b 100644 --- a/Xcodes/Backend/HelperClient.swift +++ b/Xcodes/Backend/HelperClient.swift @@ -1,6 +1,5 @@ import Combine import Foundation -import Path final class HelperClient { private var connection: NSXPCConnection? @@ -104,27 +103,4 @@ final class HelperClient { .map { $0.0 } .eraseToAnyPublisher() } - - func uninstallXcode(_ path: Path) -> AnyPublisher { - let connectionErrorSubject = PassthroughSubject() - - return Deferred { - Future { promise in - do { - try Current.files.trashItem(at: path.url) - promise(.success(())) - } catch { - promise(.failure(error)) - } - } - } - // Take values, but fail when connectionErrorSubject fails - .zip( - connectionErrorSubject - .prepend("") - .map { _ in Void() } - ) - .map { $0.0 } - .eraseToAnyPublisher() - } } diff --git a/Xcodes/Backend/XcodeCommands.swift b/Xcodes/Backend/XcodeCommands.swift index d9c75e1..4cbee0b 100644 --- a/Xcodes/Backend/XcodeCommands.swift +++ b/Xcodes/Backend/XcodeCommands.swift @@ -18,6 +18,9 @@ struct XcodeCommands: Commands { OpenCommand() RevealCommand() CopyPathCommand() + + Divider() + UninstallCommand() } .environmentObject(appState) @@ -88,23 +91,14 @@ struct UninstallButton: View { @EnvironmentObject var appState: AppState let xcode: Xcode? - @State private var showingAlert = false - var alert: Alert { - 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: xcode!.id) }), - secondaryButton: .cancel(Text("Cancel"))) - } - var body: some View { Button(action: { - self.showingAlert = true + appState.xcodeBeingConfirmedForUninstallation = xcode }) { Text("Uninstall") } .foregroundColor(.red) .help("Uninstall") - .alert(isPresented:$showingAlert, content: { self.alert }) } } diff --git a/Xcodes/Frontend/MainWindow.swift b/Xcodes/Frontend/MainWindow.swift index fe78e1b..5052e77 100644 --- a/Xcodes/Frontend/MainWindow.swift +++ b/Xcodes/Frontend/MainWindow.swift @@ -7,13 +7,18 @@ struct MainWindow: View { @AppStorage("lastUpdated") private var lastUpdated: Double? @SceneStorage("isShowingInfoPane") private var isShowingInfoPane = false @SceneStorage("xcodeListCategory") private var category: XcodeListCategory = .all - + var body: some View { HSplitView { XcodeListView(selectedXcodeID: $selectedXcodeID, searchText: searchText, category: category) .frame(minWidth: 300) .layoutPriority(1) - + .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: xcode.id) }), + secondaryButton: .cancel(Text("Cancel"))) + } InfoPane(selectedXcodeID: selectedXcodeID) .frame(minWidth: 300, maxWidth: .infinity) .frame(width: isShowingInfoPane ? nil : 0) @@ -31,15 +36,6 @@ struct MainWindow: View { message: Text(verbatim: error.message), dismissButton: .default(Text("OK"))) } - /* - Removing this for now, because it's overriding the error alert that's being worked on above. - .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: xcode.id) }), - secondaryButton: .cancel(Text("Cancel"))) - } - **/ .sheet(isPresented: $appState.secondFactorData.isNotNil) { secondFactorView(appState.secondFactorData!) .environmentObject(appState) diff --git a/Xcodes/Frontend/XcodeList/InfoPane.swift b/Xcodes/Frontend/XcodeList/InfoPane.swift index 15bfd24..0c2f29b 100644 --- a/Xcodes/Frontend/XcodeList/InfoPane.swift +++ b/Xcodes/Frontend/XcodeList/InfoPane.swift @@ -37,6 +37,9 @@ struct InfoPane: View { OpenButton(xcode: xcode) .help("Open") + + Spacer() + UninstallButton(xcode: xcode) } } else { InstallButton(xcode: xcode) @@ -49,12 +52,7 @@ struct InfoPane: View { compatibility(for: xcode) sdks(for: xcode) compilers(for: xcode) - - if xcode.path != nil { - VStack(alignment: .leading) { - UninstallButton(xcode: xcode) - } - } + Spacer() } } else { From 7d1e22e6689cd165e9ec2af0f61a1c7063662476 Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Fri, 1 Jan 2021 13:01:07 -0600 Subject: [PATCH 3/3] Remove some unused code --- Xcodes/Backend/AppState.swift | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 1620445..4e4c1fc 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -209,10 +209,6 @@ class AppState: ObservableObject { // MARK: - Uninstall func uninstall(id: Xcode.ID) { - if helperInstallState == .notInstalled { - installHelper() - } - guard let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version == id }), uninstallPublisher == nil @@ -318,8 +314,6 @@ class AppState: ObservableObject { private func uninstallXcode(path: Path) -> AnyPublisher { - let connectionErrorSubject = PassthroughSubject() - return Deferred { Future { promise in do { @@ -330,13 +324,6 @@ class AppState: ObservableObject { } } } - // Take values, but fail when connectionErrorSubject fails - .zip( - connectionErrorSubject - .prepend("") - .map { _ in Void() } - ) - .map { $0.0 } .eraseToAnyPublisher() }