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
8 changes: 8 additions & 0 deletions Xcodes/Backend/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ enum PreferenceKey: String {
case hideSupportXcodes
case xcodeListArchitectures
case enableGroupedXcodeList
case showTags
case expandedMajorXcodeVersions
case expandedMinorXcodeVersions
case usePrivilegeHelperForFileOperations
Expand Down Expand Up @@ -173,6 +174,12 @@ class AppState: ObservableObject {
}
}

@Published var showTags = true {
didSet {
Current.defaults.set(showTags, forKey: PreferenceKey.showTags.rawValue)
}
}

// MARK: - Runtimes

@Published var downloadableRuntimes: [DownloadableRuntime] = []
Expand Down Expand Up @@ -272,6 +279,7 @@ class AppState: ObservableObject {
terminateAfterLastWindowClosed = Current.defaults.bool(forKey: "terminateAfterLastWindowClosed") ?? false
enableGroupedXcodeList = Current.defaults.get(forKey: PreferenceKey.enableGroupedXcodeList.rawValue) as? Bool ?? true
usePrivilegedHelperForFileOperations = Current.defaults.bool(forKey: PreferenceKey.usePrivilegeHelperForFileOperations.rawValue) ?? false
showTags = Current.defaults.get(forKey: PreferenceKey.showTags.rawValue) as? Bool ?? true
}

// MARK: Timer
Expand Down
2 changes: 1 addition & 1 deletion Xcodes/Frontend/Common/NavigationSplitViewWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ struct NavigationSplitViewWrapper<Sidebar, Detail>: View where Sidebar: View, De
NavigationSplitView {
if #available(macOS 14, *) {
sidebar
.navigationSplitViewColumnWidth(min: 290, ideal: 290)
.navigationSplitViewColumnWidth(min: 380, ideal: 440, max: 700)
} else {
sidebar
}
Expand Down
65 changes: 65 additions & 0 deletions Xcodes/Frontend/Common/TagView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@


import SwiftUI
import Version
import XcodesKit

struct TagView: View {
let text: String
Expand All @@ -22,3 +24,66 @@ struct TagView: View {
)
}
}

/// A tinted capsule that classifies a release at a glance, e.g. Beta, RC or Latest.
struct ReleaseTagView: View {
enum Kind {
case beta
case releaseCandidate
case otherPrerelease
case latest
case active

var color: Color {
switch self {
case .beta: return .orange
case .releaseCandidate: return .purple
case .otherPrerelease: return .teal
case .latest: return .green
case .active: return .blue
}
}
}

let label: Text
let kind: Kind

var body: some View {
label
.font(.caption2.weight(.semibold))
.foregroundStyle(kind.color)
.padding(.horizontal, 6)
.padding(.vertical, 1)
.background(kind.color.opacity(0.16), in: Capsule())
.overlay(Capsule().strokeBorder(kind.color.opacity(0.35), lineWidth: 0.5))
.fixedSize()
}
}

extension ReleaseTagView {
/// A tag describing a version's prerelease, e.g. "Beta 6" or "RC 2", or nil for a release.
init?(prereleaseOf version: Version) {
guard version.isPrerelease else { return nil }
let base = Version(major: version.major, minor: version.minor, patch: version.patch).appleDescription
let text = version.appleDescription
.dropFirst(base.count)
.trimmingCharacters(in: .whitespaces)
.replacingOccurrences(of: "Release Candidate", with: "RC")
self.init(label: Text(verbatim: text), kind: Self.kind(forPrerelease: version))
}

static func kind(forPrerelease version: Version) -> Kind {
let identifiers = version.prereleaseIdentifiers.joined(separator: " ").lowercased()
if identifiers.contains("beta") { return .beta }
if identifiers.contains("release") || identifiers.contains("rc") || identifiers.contains("gm") { return .releaseCandidate }
return .otherPrerelease
}

static var active: ReleaseTagView {
ReleaseTagView(label: Text("Tag.Active"), kind: .active)
}

static var latest: ReleaseTagView {
ReleaseTagView(label: Text("Tag.Latest"), kind: .latest)
}
}
15 changes: 11 additions & 4 deletions Xcodes/Frontend/InfoPane/InfoPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,17 @@ struct InfoPane: View {
HStack {
IconView(xcode: xcode)

Text(verbatim: "Xcode \(xcode.description) \(xcode.version.buildMetadataIdentifiersDisplay)")
.font(.title)
.frame(maxWidth: .infinity, alignment: .leading)
.textSelection(.enabled)
VStack(alignment: .leading, spacing: 2) {
Text(verbatim: "Xcode \(xcode.description)")
.font(.title)
if !xcode.version.buildMetadataIdentifiers.isEmpty {
Text(verbatim: xcode.version.buildMetadataIdentifiers.joined(separator: " "))
.font(.subheadline.monospaced())
.foregroundStyle(.secondary)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.textSelection(.enabled)
}
InfoPaneControls(xcode: xcode)
}
Expand Down
8 changes: 7 additions & 1 deletion Xcodes/Frontend/Preferences/GeneralPreferencePane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@ struct GeneralPreferencePane: View {
.groupBoxStyle(PreferencesGroupBoxStyle())
Divider()

GroupBox(label: Text("UI")) {
Toggle("CollapseMajorMinorVersions", isOn: $appState.enableGroupedXcodeList)
Toggle("ShowTags", isOn: $appState.showTags)
}
.groupBoxStyle(PreferencesGroupBoxStyle())
Divider()

GroupBox(label: Text("Misc")) {
Toggle("TerminateAfterLastWindowClosed", isOn: $appState.terminateAfterLastWindowClosed)
Toggle("GroupXcodeVersionsInList", isOn: $appState.enableGroupedXcodeList)
}
.groupBoxStyle(PreferencesGroupBoxStyle())
}
Expand Down
100 changes: 70 additions & 30 deletions Xcodes/Frontend/Preferences/PreferencesView.swift
Original file line number Diff line number Diff line change
@@ -1,44 +1,84 @@
import SwiftUI

struct PreferencesView: View {
private enum Tabs: Hashable {
case general, updates, advanced, experiment
private enum Tabs: Hashable, CaseIterable {
case general, updates, downloads, advanced, experiment

var title: LocalizedStringKey {
switch self {
case .general: return "General"
case .updates: return "Updates"
case .downloads: return "Downloads"
case .advanced: return "Advanced"
case .experiment: return "Experiments"
}
}

var symbol: String {
switch self {
case .general: return "gearshape"
case .updates: return "arrow.triangle.2.circlepath.circle"
case .downloads: return "icloud.and.arrow.down"
case .advanced: return "gearshape.2"
case .experiment: return "lightbulb"
}
}
}

@State private var selectedTab = Tabs.general
@EnvironmentObject var appState: AppState
@EnvironmentObject var updater: ObservableUpdater

var body: some View {
TabView {
GeneralPreferencePane()
.environmentObject(appState)
.tabItem {
Label("General", systemImage: "gearshape")
VStack(spacing: 20) {
// Native Settings toolbar images can retain the first tab's accent tint.
HStack(spacing: 4) {
ForEach(Tabs.allCases, id: \.self) { tab in
Button {
selectedTab = tab
} label: {
VStack(spacing: 6) {
Image(systemName: tab.symbol)
.symbolRenderingMode(.monochrome)
.font(.system(size: 24))
.frame(height: 28)
Text(tab.title)
}
.foregroundStyle(selectedTab == tab ? Color.accentColor : Color.secondary)
.padding(.vertical, 10)
.frame(maxWidth: .infinity)
.background(selectedTab == tab ? Color.primary.opacity(0.08) : Color.clear, in: RoundedRectangle(cornerRadius: 8))
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.focusEffectDisabled()
.accessibilityAddTraits(selectedTab == tab ? .isSelected : [])
}
.tag(Tabs.general)
UpdatesPreferencePane()
.environmentObject(updater)
.tabItem {
Label("Updates", systemImage: "arrow.triangle.2.circlepath.circle")
}
.tag(Tabs.updates)
DownloadPreferencePane()
.environmentObject(appState)
.tabItem {
Label("Downloads", systemImage: "icloud.and.arrow.down")
}
AdvancedPreferencePane()
.environmentObject(appState)
.tabItem {
Label("Advanced", systemImage: "gearshape.2")
}
.tag(Tabs.advanced)
ExperimentsPreferencePane()
.tabItem {
Label("Experiments", systemImage: "lightbulb")
}
Divider()

Group {
switch selectedTab {
case .general:
GeneralPreferencePane()
.environmentObject(appState)
case .updates:
UpdatesPreferencePane()
.environmentObject(updater)
case .downloads:
DownloadPreferencePane()
.environmentObject(appState)
case .advanced:
AdvancedPreferencePane()
.environmentObject(appState)
case .experiment:
ExperimentsPreferencePane()
.environmentObject(appState)
}
.tag(Tabs.experiment)
}
}
.padding(20)
.frame(width: 600)
}

}
10 changes: 5 additions & 5 deletions Xcodes/Frontend/Preferences/UpdatesPreferencePane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@ class ObservableUpdater: ObservableObject {
automaticallyChecksForUpdatesObservation = updater.observe(
\.automaticallyChecksForUpdates,
options: [.initial, .new, .old],
changeHandler: { [weak self] updater, change in
guard change.newValue != change.oldValue else { return }
let automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates
changeHandler: { [weak self] _, change in
// Use the observed value: the updater's properties are main actor-isolated and this handler isn't
guard change.newValue != change.oldValue, let automaticallyChecksForUpdates = change.newValue else { return }
Task { @MainActor [weak self] in
self?.automaticallyChecksForUpdates = automaticallyChecksForUpdates
}
Expand All @@ -121,8 +121,8 @@ class ObservableUpdater: ObservableObject {
lastUpdateCheckDateObservation = updater.observe(
\.lastUpdateCheckDate,
options: [.initial, .new, .old],
changeHandler: { [weak self] updater, change in
let lastUpdateCheckDate = updater.lastUpdateCheckDate
changeHandler: { [weak self] _, change in
let lastUpdateCheckDate = change.newValue ?? nil
Task { @MainActor [weak self] in
self?.lastUpdateCheckDate = lastUpdateCheckDate
}
Expand Down
Loading