From 8084f057fd9ca30f80a5f3d88fa5f447d8363f3d Mon Sep 17 00:00:00 2001 From: Brandon Evans Date: Mon, 28 Dec 2020 12:28:16 -0700 Subject: [PATCH] Create MainWindow to split up XcodeListView --- Xcodes.xcodeproj/project.pbxproj | 12 ++ Xcodes/Backend/Optional+IsNotNil.swift | 9 + Xcodes/Frontend/MainWindow.swift | 74 ++++++++ Xcodes/Frontend/XcodeList/MainToolbar.swift | 4 +- .../XcodeList/XcodeListCategory.swift | 15 ++ Xcodes/Frontend/XcodeList/XcodeListView.swift | 160 +++++------------- Xcodes/XcodesApp.swift | 3 +- 7 files changed, 156 insertions(+), 121 deletions(-) create mode 100644 Xcodes/Backend/Optional+IsNotNil.swift create mode 100644 Xcodes/Frontend/MainWindow.swift create mode 100644 Xcodes/Frontend/XcodeList/XcodeListCategory.swift diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 5a7bd11..b5e9823 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -69,6 +69,9 @@ 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 */; }; + CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAE4247E259A666100B8B246 /* MainWindow.swift */; }; + CAE42487259A68A300B8B246 /* XcodeListCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAE42486259A68A300B8B246 /* XcodeListCategory.swift */; }; + CAE4248C259A68B800B8B246 /* Optional+IsNotNil.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAE4248B259A68B800B8B246 /* Optional+IsNotNil.swift */; }; CAFBDB912598FE80003DCC5A /* SelectedXcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDB902598FE80003DCC5A /* SelectedXcode.swift */; }; CAFBDB952598FE96003DCC5A /* FocusedValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDB942598FE96003DCC5A /* FocusedValues.swift */; }; CAFBDC4E2599B33D003DCC5A /* MainToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDC4D2599B33D003DCC5A /* MainToolbar.swift */; }; @@ -189,6 +192,9 @@ 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 = ""; }; CAD2E7B92449575100113D76 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CAE4247E259A666100B8B246 /* MainWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainWindow.swift; sourceTree = ""; }; + CAE42486259A68A300B8B246 /* XcodeListCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeListCategory.swift; sourceTree = ""; }; + CAE4248B259A68B800B8B246 /* Optional+IsNotNil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+IsNotNil.swift"; sourceTree = ""; }; CAFBDB902598FE80003DCC5A /* SelectedXcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedXcode.swift; sourceTree = ""; }; CAFBDB942598FE96003DCC5A /* FocusedValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusedValues.swift; sourceTree = ""; }; CAFBDBA525990C76003DCC5A /* SimpleXPCApp.LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = SimpleXPCApp.LICENSE; sourceTree = ""; }; @@ -302,6 +308,7 @@ CAFBDC67259A308B003DCC5A /* InspectorPane.swift */, CAFBDC4D2599B33D003DCC5A /* MainToolbar.swift */, CA44901E2463AD34003D8213 /* Tag.swift */, + CAE42486259A68A300B8B246 /* XcodeListCategory.swift */, CAD2E7A32449574E00113D76 /* XcodeListView.swift */, ); path = XcodeList; @@ -326,6 +333,7 @@ CA9FF8F425959CE000E47BAF /* HelperInstaller.swift */, CA9FF9352595B44700E47BAF /* HelperClient.swift */, CA9FF8862595607900E47BAF /* InstalledXcode.swift */, + CAE4248B259A68B800B8B246 /* Optional+IsNotNil.swift */, CABFA9AE2592EEE900380FEE /* Path+.swift */, CABFA9B42592EEEA00380FEE /* Process.swift */, CABFA9B02592EEEA00380FEE /* Promise+.swift */, @@ -348,6 +356,7 @@ CA9FF8552595082000E47BAF /* About */, CAA1CB50255A5D16003FD669 /* SignIn */, CABFAA142592F73000380FEE /* XcodeList */, + CAE4247E259A666100B8B246 /* MainWindow.swift */, CABFAA2A2592FBFC00380FEE /* SettingsView.swift */, CAFBDC6B259A3098003DCC5A /* View+Conditional.swift */, CA9FF8652595130600E47BAF /* View+IsHidden.swift */, @@ -613,6 +622,7 @@ CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */, CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */, CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */, + CAE4248C259A68B800B8B246 /* Optional+IsNotNil.swift in Sources */, CA9FF9362595B44700E47BAF /* HelperClient.swift in Sources */, CABFA9CA2592EEEA00380FEE /* AppState+Update.swift in Sources */, CA44901F2463AD34003D8213 /* Tag.swift in Sources */, @@ -627,6 +637,7 @@ CABFA9CD2592EEEA00380FEE /* Foundation.swift in Sources */, CA9FF8872595607900E47BAF /* InstalledXcode.swift in Sources */, CA61A6E0259835580008926E /* Xcode.swift in Sources */, + CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */, CA9FF84E2595079F00E47BAF /* ScrollingTextView.swift in Sources */, CABFA9C12592EEEA00380FEE /* Version+.swift in Sources */, CA9FF8522595080100E47BAF /* AcknowledgementsView.swift in Sources */, @@ -645,6 +656,7 @@ CAFBDC6C259A3098003DCC5A /* View+Conditional.swift in Sources */, CABFA9CF2592EEEA00380FEE /* Process.swift in Sources */, CABFA9C72592EEEA00380FEE /* Entry+.swift in Sources */, + CAE42487259A68A300B8B246 /* XcodeListCategory.swift in Sources */, CABFAA2C2592FBFC00380FEE /* SettingsView.swift in Sources */, CA9FF87B2595293E00E47BAF /* DataSource.swift in Sources */, CABFA9C92592EEEA00380FEE /* URLRequest+Apple.swift in Sources */, diff --git a/Xcodes/Backend/Optional+IsNotNil.swift b/Xcodes/Backend/Optional+IsNotNil.swift new file mode 100644 index 0000000..9fdcd34 --- /dev/null +++ b/Xcodes/Backend/Optional+IsNotNil.swift @@ -0,0 +1,9 @@ +import Foundation + +extension Optional { + /// Note that this is lossy when setting, so you can really only set it to nil, but this is sufficient for mapping `Binding` to `Binding` for Alerts, Popovers, etc. + var isNotNil: Bool { + get { self != nil } + set { self = newValue ? self : nil } + } +} diff --git a/Xcodes/Frontend/MainWindow.swift b/Xcodes/Frontend/MainWindow.swift new file mode 100644 index 0000000..28eb395 --- /dev/null +++ b/Xcodes/Frontend/MainWindow.swift @@ -0,0 +1,74 @@ +import SwiftUI + +struct MainWindow: View { + @EnvironmentObject var appState: AppState + @State private var selection: Xcode.ID? + @State private var searchText: String = "" + @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(searchText: searchText, category: category) + .frame(minWidth: 300) + .layoutPriority(1) + + InspectorPane() + .frame(minWidth: 300, maxWidth: .infinity) + .frame(width: isShowingInfoPane ? nil : 0) + .isHidden(!isShowingInfoPane) + } + .mainToolbar( + category: $category, + isShowingInfoPane: $isShowingInfoPane, + searchText: $searchText + ) + .navigationSubtitle(subtitleText) + .frame(minWidth: 600, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity) + .alert(item: $appState.error) { error in + Alert(title: Text(error.title), + 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) + } + } + + private var subtitleText: Text { + if let lastUpdated = lastUpdated.map(Date.init(timeIntervalSince1970:)) { + return Text("Updated at \(lastUpdated, style: .date) \(lastUpdated, style: .time)") + } else { + return Text("") + } + } + + @ViewBuilder + private func secondFactorView(_ secondFactorData: AppState.SecondFactorData) -> some View { + switch secondFactorData.option { + case .codeSent: + SignIn2FAView(isPresented: $appState.secondFactorData.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData) + case .smsSent(let trustedPhoneNumber): + SignInSMSView(isPresented: $appState.secondFactorData.isNotNil, trustedPhoneNumber: trustedPhoneNumber, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData) + case .smsPendingChoice: + SignInPhoneListView(isPresented: $appState.secondFactorData.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData) + } + } +} + +struct MainWindow_Previews: PreviewProvider { + static var previews: some View { + MainWindow() + } +} diff --git a/Xcodes/Frontend/XcodeList/MainToolbar.swift b/Xcodes/Frontend/XcodeList/MainToolbar.swift index 42594a4..b34c88e 100644 --- a/Xcodes/Frontend/XcodeList/MainToolbar.swift +++ b/Xcodes/Frontend/XcodeList/MainToolbar.swift @@ -2,7 +2,7 @@ import SwiftUI struct MainToolbarModifier: ViewModifier { @EnvironmentObject var appState: AppState - @Binding var category: XcodeListView.Category + @Binding var category: XcodeListCategory @Binding var isShowingInfoPane: Bool @Binding var searchText: String @@ -55,7 +55,7 @@ struct MainToolbarModifier: ViewModifier { extension View { func mainToolbar( - category: Binding, + category: Binding, isShowingInfoPane: Binding, searchText: Binding ) -> some View { diff --git a/Xcodes/Frontend/XcodeList/XcodeListCategory.swift b/Xcodes/Frontend/XcodeList/XcodeListCategory.swift new file mode 100644 index 0000000..520414e --- /dev/null +++ b/Xcodes/Frontend/XcodeList/XcodeListCategory.swift @@ -0,0 +1,15 @@ +import Foundation + +enum XcodeListCategory: String, CaseIterable, Identifiable, CustomStringConvertible { + case all + case installed + + var id: Self { self } + + var description: String { + switch self { + case .all: return "All" + case .installed: return "Installed" + } + } +} diff --git a/Xcodes/Frontend/XcodeList/XcodeListView.swift b/Xcodes/Frontend/XcodeList/XcodeListView.swift index 74d1897..a2fa65a 100644 --- a/Xcodes/Frontend/XcodeList/XcodeListView.swift +++ b/Xcodes/Frontend/XcodeList/XcodeListView.swift @@ -4,11 +4,13 @@ import PromiseKit struct XcodeListView: View { @EnvironmentObject var appState: AppState - @State private var selection: Xcode.ID? - @State private var searchText: String = "" - @AppStorage("lastUpdated") private var lastUpdated: Double? - @SceneStorage("isShowingInfoPane") private var isShowingInfoPane = false - @SceneStorage("xcodeListCategory") private var category: Category = .all + private let searchText: String + private let category: XcodeListCategory + + init(searchText: String, category: XcodeListCategory) { + self.searchText = searchText + self.category = category + } var visibleXcodes: [Xcode] { var xcodes: [Xcode] @@ -26,114 +28,46 @@ struct XcodeListView: View { return xcodes } - enum Category: String, CaseIterable, Identifiable, CustomStringConvertible { - case all - case installed - - var id: Self { self } - - var description: String { - switch self { - case .all: return "All" - case .installed: return "Installed" - } - } - } - var body: some View { - HSplitView { - List(visibleXcodes, selection: $appState.selectedXcodeID) { xcode in - HStack { - appIconView(for: xcode) + List(visibleXcodes, selection: $appState.selectedXcodeID) { xcode in + HStack { + appIconView(for: xcode) + + VStack(alignment: .leading) { + Text(xcode.description) + .font(.body) - VStack(alignment: .leading) { - Text(xcode.description) - .font(.body) - - Text(verbatim: xcode.path ?? "") - .font(.caption) - .foregroundColor(appState.selectedXcodeID == xcode.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) + Text(verbatim: xcode.path ?? "") + .font(.caption) + .foregroundColor(appState.selectedXcodeID == xcode.id ? Color(NSColor.selectedMenuItemTextColor) : Color(NSColor.secondaryLabelColor)) } - .contextMenu { - InstallButton(xcode: xcode) - - Divider() - - if xcode.installed { - SelectButton(xcode: xcode) - OpenButton(xcode: xcode) - RevealButton(xcode: xcode) - CopyPathButton(xcode: xcode) - } + + 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 { + InstallButton(xcode: xcode) + + Divider() + + if xcode.installed { + SelectButton(xcode: xcode) + OpenButton(xcode: xcode) + RevealButton(xcode: xcode) + CopyPathButton(xcode: xcode) } } - .frame(minWidth: 300) - .layoutPriority(1) - - InspectorPane() - .frame(minWidth: 300, maxWidth: .infinity) - .frame(width: isShowingInfoPane ? nil : 0) - .isHidden(!isShowingInfoPane) - } - .mainToolbar( - category: $category, - isShowingInfoPane: $isShowingInfoPane, - searchText: $searchText - ) - .navigationSubtitle(subtitleText) - .frame(minWidth: 200, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity) - .alert(item: $appState.error) { error in - Alert(title: Text(error.title), - 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) - } - } - - @ViewBuilder - func secondFactorView(_ secondFactorData: AppState.SecondFactorData) -> some View { - switch secondFactorData.option { - case .codeSent: - SignIn2FAView(isPresented: $appState.secondFactorData.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData) - case .smsSent(let trustedPhoneNumber): - SignInSMSView(isPresented: $appState.secondFactorData.isNotNil, trustedPhoneNumber: trustedPhoneNumber, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData) - case .smsPendingChoice: - SignInPhoneListView(isPresented: $appState.secondFactorData.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData) - } - } - - private var subtitleText: Text { - if let lastUpdated = lastUpdated.map(Date.init(timeIntervalSince1970:)) { - return Text("Updated at \(lastUpdated, style: .date) \(lastUpdated, style: .time)") - } else { - return Text("") } } @@ -152,7 +86,7 @@ struct XcodeListView: View { struct XcodeListView_Previews: PreviewProvider { static var previews: some View { Group { - XcodeListView() + XcodeListView(searchText: "", category: .all) .environmentObject({ () -> AppState in let a = AppState() a.allXcodes = [ @@ -167,11 +101,3 @@ struct XcodeListView_Previews: PreviewProvider { .previewLayout(.sizeThatFits) } } - -extension Optional { - /// Note that this is lossy when setting, so you can really only set it to nil, but this is sufficient for mapping `Binding` to `Binding` for Alerts, Popovers, etc. - var isNotNil: Bool { - get { self != nil } - set { self = newValue ? self : nil } - } -} diff --git a/Xcodes/XcodesApp.swift b/Xcodes/XcodesApp.swift index b7e1b66..fae3bb0 100644 --- a/Xcodes/XcodesApp.swift +++ b/Xcodes/XcodesApp.swift @@ -9,8 +9,7 @@ struct XcodesApp: App { var body: some Scene { WindowGroup("Xcodes") { - XcodeListView() - .frame(minWidth: 600) + MainWindow() .environmentObject(appState) // This is intentionally used on a View, and not on a WindowGroup, // so that it's triggered when an individual window's phase changes instead of all window phases.