Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 91 additions & 71 deletions Xcodes.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
2 changes: 1 addition & 1 deletion Xcodes.xcodeproj/xcshareddata/xcschemes/Xcodes.xcscheme
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1220"
LastUpgradeVersion = "2600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "2600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
7 changes: 7 additions & 0 deletions Xcodes/Backend/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ class AppState: ObservableObject {
}
}

@Published var collapsableListEnabled = true {
didSet {
Current.defaults.set(collapsableListEnabled, forKey: "collapsableListEnabled")
}
}

// MARK: - Runtimes

@Published var downloadableRuntimes: [DownloadableRuntime] = []
Expand Down Expand Up @@ -209,6 +215,7 @@ class AppState: ObservableObject {
installPath = Current.defaults.string(forKey: "installPath") ?? Path.defaultInstallDirectory.string
showOpenInRosettaOption = Current.defaults.bool(forKey: "showOpenInRosettaOption") ?? false
terminateAfterLastWindowClosed = Current.defaults.bool(forKey: "terminateAfterLastWindowClosed") ?? false
collapsableListEnabled = Current.defaults.bool(forKey: "collapsibleListEnabled") ?? true
}

// MARK: Timer
Expand Down
94 changes: 93 additions & 1 deletion Xcodes/Backend/Xcode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,97 @@ struct Xcode: Identifiable, CustomStringConvertible {
return nil
}
}


}

struct XcodeMinorVersionGroup: Identifiable {
let majorVersion: Int
let minorVersion: Int
let versions: [Xcode]
var isExpanded: Bool = false

var id: String {
"\(majorVersion).\(minorVersion)"
}

var latestRelease: Xcode? {
versions
.filter { $0.version.isNotPrerelease }
.sorted { $0.version < $1.version }
.last
}

var displayName: String {
"\(majorVersion).\(minorVersion)"
}

var hasInstalled: Bool {
versions.contains { $0.installState.installed }
}

var hasInstalling: Bool {
versions.contains { $0.installState.installing }
}

var selectedVersion: Xcode? {
versions.first { $0.selected }
}
}

struct XcodeMajorVersionGroup: Identifiable {
let majorVersion: Int
let minorVersionGroups: [XcodeMinorVersionGroup]
var isExpanded: Bool = false

var id: Int {
majorVersion
}

var versions: [Xcode] {
minorVersionGroups.flatMap { $0.versions }
}

var latestRelease: Xcode? {
versions
.filter { $0.version.isNotPrerelease }
.sorted { $0.version < $1.version }
.last
}

var displayName: String {
"\(majorVersion)"
}

var hasInstalled: Bool {
minorVersionGroups.contains { $0.hasInstalled }
}

var hasInstalling: Bool {
minorVersionGroups.contains { $0.hasInstalling }
}

var selectedVersion: Xcode? {
minorVersionGroups.compactMap { $0.selectedVersion }.first
}
}

extension Array where Element == Xcode {
func groupedByMajorVersion() -> [XcodeMajorVersionGroup] {
let majorGroups = Dictionary(grouping: self) { $0.version.major }
return majorGroups.map { majorVersion, xcodes in
let minorGroups = Dictionary(grouping: xcodes) { $0.version.minor }
let minorVersionGroups = minorGroups.map { minorVersion, minorXcodes in
XcodeMinorVersionGroup(
majorVersion: majorVersion,
minorVersion: minorVersion,
versions: minorXcodes.sorted { $0.version > $1.version }
)
}.sorted { $0.minorVersion > $1.minorVersion }

return XcodeMajorVersionGroup(
majorVersion: majorVersion,
minorVersionGroups: minorVersionGroups
)
}.sorted { $0.majorVersion > $1.majorVersion }
}
}
1 change: 1 addition & 0 deletions Xcodes/Frontend/Preferences/GeneralPreferencePane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ struct GeneralPreferencePane: View {

GroupBox(label: Text("Misc")) {
Toggle("TerminateAfterLastWindowClosed", isOn: $appState.terminateAfterLastWindowClosed)
Toggle("EnableCollapsibleList", isOn: $appState.collapsableListEnabled)
}
.groupBoxStyle(PreferencesGroupBoxStyle())
}
Expand Down
104 changes: 96 additions & 8 deletions Xcodes/Frontend/XcodeList/XcodeListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ struct XcodeListView: View {
private let architecture: XcodeListArchitecture
private let isInstalledOnly: Bool
@AppStorage(PreferenceKey.allowedMajorVersions.rawValue) private var allowedMajorVersions = Int.max
@State private var expandedMajorVersions = Set<Int>()
@State private var expandedMinorVersions = Set<String>()

init(selectedXcodeID: Binding<Xcode.ID?>, searchText: String, category: XcodeListCategory, isInstalledOnly: Bool, architecture: XcodeListArchitecture) {
self._selectedXcodeID = selectedXcodeID
Expand All @@ -29,12 +31,12 @@ struct XcodeListView: View {
case .beta:
xcodes = appState.allXcodes.filter { $0.version.isPrerelease }
}

if architecture == .appleSilicon {
xcodes = xcodes.filter { $0.architectures == [.arm64] }
}


let latestMajor = xcodes.sorted(\.version)
.filter { $0.version.isNotPrerelease }
.last?
Expand All @@ -54,17 +56,32 @@ struct XcodeListView: View {
if !searchText.isEmpty {
xcodes = xcodes.filter { $0.description.contains(searchText) }
}

if isInstalledOnly {
xcodes = xcodes.filter { $0.installState.installed }
}

return xcodes
}

var majorVersionGroups: [XcodeMajorVersionGroup] {
visibleXcodes.groupedByMajorVersion()
}

var body: some View {
List(visibleXcodes, selection: $selectedXcodeID) { xcode in
XcodeListViewRow(xcode: xcode, selected: selectedXcodeID == xcode.id, appState: appState)
List(selection: $selectedXcodeID) {
if appState.collapsableListEnabled {
CollapsableListView(
visibleXcodes: visibleXcodes,
selectedXcodeID: $selectedXcodeID,
appState: appState
)
} else {
ForEach(visibleXcodes) { xcode in
XcodeListViewRow(xcode: xcode, selected: selectedXcodeID == xcode.id, appState: appState)
.tag(xcode.id)
}
}
}
.listStyle(.sidebar)
.safeAreaInset(edge: .bottom, spacing: 0) {
Expand All @@ -76,6 +93,72 @@ struct XcodeListView: View {
}
}

struct CollapsableListView: View {
let visibleXcodes: [Xcode]
@Binding var selectedXcodeID: Xcode.ID?
let appState: AppState

@State private var expandedMajorVersions = Set<Int>()
@State private var expandedMinorVersions = Set<String>()

var majorVersionGroups: [XcodeMajorVersionGroup] {
visibleXcodes.groupedByMajorVersion()
}

var body: some View {
ForEach(majorVersionGroups) { majorVersionGroup in
let isMajorExpanded = expandedMajorVersions.contains(majorVersionGroup.majorVersion)

XcodeMajorVersionRow(
majorVersionGroup: majorVersionGroup,
isExpanded: isMajorExpanded,
onToggleExpanded: {
if isMajorExpanded {
expandedMajorVersions.remove(majorVersionGroup.majorVersion)
// Collapse all minor versions when major version is collapsed
for minorGroup in majorVersionGroup.minorVersionGroups {
expandedMinorVersions.remove(minorGroup.id)
}
} else {
expandedMajorVersions.insert(majorVersionGroup.majorVersion)
}
},
appState: appState
)
.tag(majorVersionGroup.selectedVersion?.id)

if isMajorExpanded {
ForEach(majorVersionGroup.minorVersionGroups) { minorVersionGroup in
let isMinorExpanded = expandedMinorVersions.contains(minorVersionGroup.id)

XcodeMinorVersionRow(
minorVersionGroup: minorVersionGroup,
isExpanded: isMinorExpanded,
onToggleExpanded: {
if isMinorExpanded {
expandedMinorVersions.remove(minorVersionGroup.id)
} else {
expandedMinorVersions.insert(minorVersionGroup.id)
}
},
appState: appState
)
.tag(minorVersionGroup.selectedVersion?.id)

if isMinorExpanded {
ForEach(minorVersionGroup.versions) { xcode in
XcodeListViewRow(xcode: xcode, selected: selectedXcodeID == xcode.id, appState: appState)
.padding(.leading, 40)
.tag(xcode.id)
}
}
}
}
}
}
}


struct PlatformsPocket: View {
@SwiftUI.Environment(\.openWindow) private var openWindow

Expand All @@ -92,7 +175,11 @@ struct PlatformsPocket: View {
.background(.quaternary.opacity(0.75))
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
}

.font(.body.weight(.medium))
.padding(.horizontal)
.padding(.vertical, 12)
.background(.quaternary.opacity(0.9))
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
}
.buttonStyle(.plain)
}
Expand Down Expand Up @@ -132,3 +219,4 @@ struct XcodeListView_Previews: PreviewProvider {
.previewLayout(.sizeThatFits)
}
}

Loading