Create MainWindow to split up XcodeListView

This commit is contained in:
Brandon Evans 2020-12-28 12:28:16 -07:00
parent 9dc3d21f2e
commit 8084f057fd
No known key found for this signature in database
GPG key ID: D58A4B8DB64F8E93
7 changed files with 156 additions and 121 deletions

View file

@ -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 = "<group>"; };
CAD2E7B92449575100113D76 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
CAE4247E259A666100B8B246 /* MainWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainWindow.swift; sourceTree = "<group>"; };
CAE42486259A68A300B8B246 /* XcodeListCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeListCategory.swift; sourceTree = "<group>"; };
CAE4248B259A68B800B8B246 /* Optional+IsNotNil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+IsNotNil.swift"; 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>"; };
CAFBDBA525990C76003DCC5A /* SimpleXPCApp.LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = SimpleXPCApp.LICENSE; sourceTree = "<group>"; };
@ -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 */,

View file

@ -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<Item?>` to `Binding<Bool>` for Alerts, Popovers, etc.
var isNotNil: Bool {
get { self != nil }
set { self = newValue ? self : nil }
}
}

View file

@ -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()
}
}

View file

@ -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<XcodeListView.Category>,
category: Binding<XcodeListCategory>,
isShowingInfoPane: Binding<Bool>,
searchText: Binding<String>
) -> some View {

View file

@ -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"
}
}
}

View file

@ -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,22 +28,7 @@ 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)
@ -82,59 +69,6 @@ struct XcodeListView: View {
}
}
}
.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("")
}
}
@ViewBuilder
@ -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<Item?>` to `Binding<Bool>` for Alerts, Popovers, etc.
var isNotNil: Bool {
get { self != nil }
set { self = newValue ? self : nil }
}
}

View file

@ -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.