diff --git a/Scripts/check_fork_identity.sh b/Scripts/check_fork_identity.sh index 3e7c9203..d3f631d0 100755 --- a/Scripts/check_fork_identity.sh +++ b/Scripts/check_fork_identity.sh @@ -43,8 +43,8 @@ readonly app_id="dev.jacobcx.Xcodes" readonly tests_id="dev.jacobcx.Xcodes.Tests" readonly helper_id="dev.jacobcx.Xcodes.Helper" readonly team_id="K2648T24P4" -readonly marketing_version="4.1.1" -readonly build_number="49" +readonly marketing_version="4.1.2" +readonly build_number="50" readonly app_copyright="Fork contributions © 2026 JacobCXDev. Upstream contributors retain their copyrights." # shellcheck disable=SC2016 # Xcode expands this build-setting literal, not the shell. readonly app_requirement='identifier "dev.jacobcx.Xcodes" and info [CFBundleShortVersionString] >= "1.0.0" and anchor apple generic and certificate leaf[subject.OU] = "$(CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT)"' diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index fb1c4357..3a41f83a 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -36,9 +36,10 @@ CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */; }; CA2518EC25A7FF2B00F08414 /* AppStateUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2518EB25A7FF2B00F08414 /* AppStateUpdateTests.swift */; }; D10000010000000000000001 /* PinCodeInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D10000020000000000000001 /* PinCodeInputTests.swift */; }; + D20000010000000000000001 /* SDKsViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20000020000000000000001 /* SDKsViewTests.swift */; }; + D30000010000000000000001 /* ProgressButtonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D30000020000000000000001 /* ProgressButtonTests.swift */; }; CA2518EE25A7FF2B00F08414 /* SignInCredentialsViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2518ED25A7FF2B00F08414 /* SignInCredentialsViewTests.swift */; }; CA378F992466567600A58CE0 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA378F982466567600A58CE0 /* AppState.swift */; }; - CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */; }; CA42DD7325AEB04300BC0B0C /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA42DD7225AEB04300BC0B0C /* Logger.swift */; }; CA44901F2463AD34003D8213 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA44901E2463AD34003D8213 /* Tag.swift */; }; CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */; }; @@ -219,9 +220,10 @@ CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeCommands.swift; sourceTree = ""; }; CA2518EB25A7FF2B00F08414 /* AppStateUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateUpdateTests.swift; sourceTree = ""; }; D10000020000000000000001 /* PinCodeInputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinCodeInputTests.swift; sourceTree = ""; }; + D20000020000000000000001 /* SDKsViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKsViewTests.swift; sourceTree = ""; }; + D30000020000000000000001 /* ProgressButtonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressButtonTests.swift; sourceTree = ""; }; CA2518ED25A7FF2B00F08414 /* SignInCredentialsViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInCredentialsViewTests.swift; sourceTree = ""; }; CA378F982466567600A58CE0 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; - CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreButtonStyle.swift; sourceTree = ""; }; CA42DD7225AEB04300BC0B0C /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; CA44901E2463AD34003D8213 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = ""; }; CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressIndicator.swift; sourceTree = ""; }; @@ -469,7 +471,6 @@ CABFAA142592F73000380FEE /* XcodeList */ = { isa = PBXGroup; children = ( - CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */, CAFBC3FF259AC17F00E2A3D8 /* InstallationStepRowView.swift */, CAFBDC4D2599B33D003DCC5A /* MainToolbar.swift */, CA44901E2463AD34003D8213 /* Tag.swift */, @@ -610,6 +611,8 @@ CAD2E7B72449575100113D76 /* AppStateTests.swift */, CA2518EB25A7FF2B00F08414 /* AppStateUpdateTests.swift */, D10000020000000000000001 /* PinCodeInputTests.swift */, + D20000020000000000000001 /* SDKsViewTests.swift */, + D30000020000000000000001 /* ProgressButtonTests.swift */, E10000020000000000000001 /* FileOperationsTests.swift */, CA2518ED25A7FF2B00F08414 /* SignInCredentialsViewTests.swift */, 884f01aed2f43048ab4d3323 /* NewVersionNotificationTests.swift */, @@ -981,7 +984,6 @@ 536CFDD4263C9A8000026CE0 /* XcodesSheet.swift in Sources */, E89342FA25EDCC17007CF557 /* NotificationManager.swift in Sources */, CA5D781E257365D6008EDE9D /* PinCodeTextView.swift in Sources */, - CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -993,6 +995,8 @@ CAC281E2259FA44600B8AB0B /* Bundle+XcodesTests.swift in Sources */, CA2518EC25A7FF2B00F08414 /* AppStateUpdateTests.swift in Sources */, D10000010000000000000001 /* PinCodeInputTests.swift in Sources */, + D20000010000000000000001 /* SDKsViewTests.swift in Sources */, + D30000010000000000000001 /* ProgressButtonTests.swift in Sources */, E10000010000000000000001 /* FileOperationsTests.swift in Sources */, 14d2f5a1273f6c350cad4406 /* NewVersionNotificationTests.swift in Sources */, CAB3AB0E25BCA6C200BF1B04 /* AppStateTests.swift in Sources */, @@ -1090,7 +1094,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 49; + CURRENT_PROJECT_VERSION = 50; DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\""; DEVELOPMENT_TEAM = K2648T24P4; ENABLE_HARDENED_RUNTIME = NO; @@ -1102,7 +1106,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 4.1.1; + MARKETING_VERSION = 4.1.2; PRODUCT_BUNDLE_IDENTIFIER = dev.jacobcx.Xcodes; PRODUCT_NAME = Xcodes; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1343,7 +1347,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 49; + CURRENT_PROJECT_VERSION = 50; DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\""; DEVELOPMENT_TEAM = K2648T24P4; ENABLE_HARDENED_RUNTIME = YES; @@ -1355,7 +1359,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 4.1.1; + MARKETING_VERSION = 4.1.2; PRODUCT_BUNDLE_IDENTIFIER = dev.jacobcx.Xcodes; PRODUCT_NAME = Xcodes; SWIFT_VERSION = 6.0; @@ -1372,7 +1376,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 49; + CURRENT_PROJECT_VERSION = 50; DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\""; DEVELOPMENT_TEAM = K2648T24P4; ENABLE_HARDENED_RUNTIME = YES; @@ -1384,7 +1388,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 4.1.1; + MARKETING_VERSION = 4.1.2; PRODUCT_BUNDLE_IDENTIFIER = dev.jacobcx.Xcodes; PRODUCT_NAME = Xcodes; SWIFT_VERSION = 6.0; diff --git a/Xcodes/Backend/AppState+Runtimes.swift b/Xcodes/Backend/AppState+Runtimes.swift index 0305c422..d6b2f354 100644 --- a/Xcodes/Backend/AppState+Runtimes.swift +++ b/Xcodes/Backend/AppState+Runtimes.swift @@ -60,18 +60,31 @@ extension AppState { } func refreshInstalledRuntimes() async throws { + try Task.checkCancellation() let refreshID = UUID() installedRuntimesRefreshID = refreshID + isRefreshingInstalledRuntimes = true + installedRuntimesError = nil defer { if installedRuntimesRefreshID == refreshID { installedRuntimesRefreshID = nil + isRefreshingInstalledRuntimes = false } } - let runtimes = try await runtimeService.installedRuntimes() - try Task.checkCancellation() - guard installedRuntimesRefreshID == refreshID else { return } - installedRuntimes = runtimes.map(CoreSimulatorImage.init) + do { + let runtimes = try await runtimeService.installedRuntimes() + try Task.checkCancellation() + guard installedRuntimesRefreshID == refreshID else { return } + installedRuntimes = runtimes.map(CoreSimulatorImage.init) + } catch { + if installedRuntimesRefreshID == refreshID, + !Task.isCancelled, + !(error is CancellationError) { + installedRuntimesError = error + } + throw error + } } func downloadRuntime(runtime: DownloadableRuntime) { @@ -98,14 +111,14 @@ extension AppState { if let error = error as? RuntimeInstallPolicyError { switch error { case .noSelectedXcode: - presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: "No selected Xcode. Please make an Xcode active") + presentedAlert = .generic(title: localizeString("RuntimeInstallError"), message: localizeString("RuntimeNeedsActiveXcode")) case .xcode16_1OrGreaterRequired: - presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: localizeString("Alert.Install.Error.Need.Xcode16.1")) + presentedAlert = .generic(title: localizeString("RuntimeInstallError"), message: localizeString("Alert.Install.Error.Need.Xcode16.1")) case .xcode26OrGreaterRequired: - presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: localizeString("Alert.Install.Error.Need.Xcode26")) + presentedAlert = .generic(title: localizeString("RuntimeInstallError"), message: localizeString("Alert.Install.Error.Need.Xcode26")) } } else { - presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription) + presentedAlert = .generic(title: localizeString("RuntimeInstallError"), message: error.legibleLocalizedDescription) } } @@ -151,9 +164,9 @@ extension AppState { Logger.appState.error("Error downloading runtime: \(error.localizedDescription)") self.error = error if let error = error as? XcodesKitError { - self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.message) + self.presentedAlert = .generic(title: localizeString("RuntimeInstallError"), message: error.message) } else { - self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription) + self.presentedAlert = .generic(title: localizeString("RuntimeInstallError"), message: error.legibleLocalizedDescription) } } } @@ -191,9 +204,9 @@ extension AppState { Logger.appState.error("Error downloading runtime: \(error.localizedDescription)") self.error = error if let error = error as? XcodesKitError { - self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.message) + self.presentedAlert = .generic(title: localizeString("RuntimeInstallError"), message: error.message) } else { - self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription) + self.presentedAlert = .generic(title: localizeString("RuntimeInstallError"), message: error.legibleLocalizedDescription) } } } diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 63e56408..44ffbc37 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -362,6 +362,8 @@ class AppState: ObservableObject { } } + var showOpenInRosettaOptionDisabled: Bool { PreferenceKey.showOpenInRosettaOption.isManaged() } + @Published var terminateAfterLastWindowClosed = false { didSet { Current.defaults.set(terminateAfterLastWindowClosed, forKey: "terminateAfterLastWindowClosed") @@ -378,6 +380,8 @@ class AppState: ObservableObject { @Published var downloadableRuntimes: [DownloadableRuntime] = [] @Published var installedRuntimes: [CoreSimulatorImage] = [] + @Published var isRefreshingInstalledRuntimes = false + @Published var installedRuntimesError: Error? // MARK: - Operation State @@ -483,6 +487,9 @@ class AppState: ObservableObject { // MARK: Timer /// Runs a timer every 6 hours when app is open to check if it needs to auto install any xcodes func setupAutoInstallTimer() { + autoInstallTimer?.invalidate() + autoInstallTimer = nil + guard let storageValue = Current.defaults.get(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return } if autoInstallType == .none { return } diff --git a/Xcodes/Backend/XcodeCommands.swift b/Xcodes/Backend/XcodeCommands.swift index fc40395a..9198782c 100644 --- a/Xcodes/Backend/XcodeCommands.swift +++ b/Xcodes/Backend/XcodeCommands.swift @@ -77,8 +77,10 @@ struct CancelRuntimeInstallButton: View { var body: some View { Button(action: cancelInstall) { - Image(systemName: "xmark.circle.fill") - }.help(localizeString("StopInstallation")) + Label("StopInstallation", systemImage: "xmark.circle.fill") + } + .labelStyle(.iconOnly) + .help(localizeString("StopInstallation")) .buttonStyle(.plain) } diff --git a/Xcodes/Frontend/About/ScrollingTextView.swift b/Xcodes/Frontend/About/ScrollingTextView.swift index 97d053cf..f5356b0f 100644 --- a/Xcodes/Frontend/About/ScrollingTextView.swift +++ b/Xcodes/Frontend/About/ScrollingTextView.swift @@ -9,6 +9,7 @@ struct ScrollingTextView: NSViewRepresentable { let view = NSTextView.scrollableTextView() let textView = view.documentView as? NSTextView textView?.isEditable = false + textView?.textContainerInset = NSSize(width: 16, height: 16) return view } diff --git a/Xcodes/Frontend/Common/ProgressButton.swift b/Xcodes/Frontend/Common/ProgressButton.swift index 34b6f6e8..7a1ce7b4 100644 --- a/Xcodes/Frontend/Common/ProgressButton.swift +++ b/Xcodes/Frontend/Common/ProgressButton.swift @@ -34,5 +34,9 @@ struct ProgressButton: View { ) } .disabled(isInProgress) + .accessibilityRepresentation { + Button(action: action, label: label) + .disabled(isInProgress) + } } } diff --git a/Xcodes/Frontend/Common/View+ErrorHandling.swift b/Xcodes/Frontend/Common/View+ErrorHandling.swift index bd1a8ad5..6d8121c8 100644 --- a/Xcodes/Frontend/Common/View+ErrorHandling.swift +++ b/Xcodes/Frontend/Common/View+ErrorHandling.swift @@ -45,8 +45,13 @@ protocol ErrorHandler: Sendable { } struct AlertErrorHandler: ErrorHandler { + private let title: String private let id = UUID() + nonisolated init(title: String = "An error occurred") { + self.title = title + } + func handle( _ error: Binding, in view: T, @@ -96,8 +101,8 @@ private extension AlertErrorHandler { switch error.resolveCategory() { case let .recoverable(recoveryOption): return Alert( - title: Text("An error occurred"), - message: Text(error.localizedDescription), + title: Text(LocalizedStringKey(title)), + message: Text(error.legibleLocalizedDescription), primaryButton: .default(Text("Dismiss")), secondaryButton: .default( Text(recoveryOption.description), @@ -106,8 +111,8 @@ private extension AlertErrorHandler { ) case .nonRecoverable: return Alert( - title: Text("An error occurred"), - message: Text(error.localizedDescription), + title: Text(LocalizedStringKey(title)), + message: Text(error.legibleLocalizedDescription), dismissButton: .default(Text("Dismiss")) ) case .requiresSignout: diff --git a/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift b/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift index e4bb64ff..aaf55464 100644 --- a/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift +++ b/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift @@ -19,7 +19,7 @@ struct InstallationStepDetailView: View { case .authenticating, .unarchiving, .moving, .trashingArchive, .checkingSecurity, .finishing: ProgressView() - .scaleEffect(0.5) + .progressViewStyle(.linear) } } } diff --git a/Xcodes/Frontend/InfoPane/InstalledStateButtons.swift b/Xcodes/Frontend/InfoPane/InstalledStateButtons.swift index 0de4924b..541e2fcf 100644 --- a/Xcodes/Frontend/InfoPane/InstalledStateButtons.swift +++ b/Xcodes/Frontend/InfoPane/InstalledStateButtons.swift @@ -21,8 +21,9 @@ struct InstalledStateButtons: View { HStack { Text(xcode.installedPath?.string ?? "") Button(action: { appState.reveal(xcode.installedPath) }) { - Image(systemName: "arrow.right.circle.fill") + Label("RevealInFinder", systemImage: "arrow.right.circle.fill") } + .labelStyle(.iconOnly) .buttonStyle(PlainButtonStyle()) .help("RevealInFinder") } diff --git a/Xcodes/Frontend/InfoPane/PlatformsView.swift b/Xcodes/Frontend/InfoPane/PlatformsView.swift index d169fbcb..40aa124b 100644 --- a/Xcodes/Frontend/InfoPane/PlatformsView.swift +++ b/Xcodes/Frontend/InfoPane/PlatformsView.swift @@ -55,6 +55,16 @@ struct PlatformsView: View { } } + if runtimes.isEmpty { + Text("NoRuntimesToShow") + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + if appState.downloadableRuntimes.isEmpty { + Button("Refresh") { appState.updateDownloadableRuntimes() } + .frame(maxWidth: .infinity, alignment: .leading) + } + } + ForEach(runtimes, id: \.identifier) { runtime in runtimeView(runtime: runtime) .frame(minWidth: 200) @@ -118,8 +128,9 @@ struct PlatformsView: View { func pathIfAvailable(xcode: Xcode, runtime: DownloadableRuntime) -> some View { if let path = appState.runtimeInstallPath(xcode: xcode, runtime: runtime) { Button(action: { appState.reveal(path: path.string) }) { - Image(systemName: "arrow.right.circle.fill") + Label("RevealInFinder", systemImage: "arrow.right.circle.fill") } + .labelStyle(.iconOnly) .buttonStyle(PlainButtonStyle()) .help("RevealInFinder") } else { diff --git a/Xcodes/Frontend/InfoPane/SDKsView.swift b/Xcodes/Frontend/InfoPane/SDKsView.swift index 459801c1..075c93b6 100644 --- a/Xcodes/Frontend/InfoPane/SDKsView.swift +++ b/Xcodes/Frontend/InfoPane/SDKsView.swift @@ -39,7 +39,8 @@ struct SDKsView: View { ("macOS", sdks.macOS), ("iOS", sdks.iOS), ("watchOS", sdks.watchOS), - ("tvOS", sdks.tvOS) + ("tvOS", sdks.tvOS), + ("visionOS", sdks.visionOS) ].compactMap { // remove nil compiler guard $0.1 != nil, // has version array !$0.1!.isEmpty // has at least 1 version diff --git a/Xcodes/Frontend/MainWindow.swift b/Xcodes/Frontend/MainWindow.swift index 5ee6db78..bd8083eb 100644 --- a/Xcodes/Frontend/MainWindow.swift +++ b/Xcodes/Frontend/MainWindow.swift @@ -61,19 +61,6 @@ struct MainWindow: View { }) .help("ManageAppleAccount") .disabled(appState.isRestoringAuthenticationState) - if #available(macOS 14, *) { - SettingsLink(label: { - Label("Preferences", systemImage: "gearshape") - }) - .help("PreferencesDescription") - } else { - Button(action: { - NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) - }, label: { - Label("Preferences", systemImage: "gearshape") - }) - .help("PreferencesDescription") - } } } } @@ -151,10 +138,10 @@ struct MainWindow: View { } else if case let .waitingForFederatedAuthentication(federationResponse) = appState.authenticationState { SignInFederatedView(federationResponse: federationResponse) .environmentObject(appState) - .frame(width: 400) + .frame(width: 452) } else { SignInCredentialsView() - .frame(width: 400) + .frame(width: 452) } } diff --git a/Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift b/Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift index 27891997..abbc91c9 100644 --- a/Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift +++ b/Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift @@ -15,8 +15,9 @@ struct AdvancedPreferencePane: View { .fixedSize(horizontal: false, vertical: true) .lineLimit(2) Button(action: { appState.reveal(path: appState.installPath) }) { - Image(systemName: "arrow.right.circle.fill") + Label("RevealInFinder", systemImage: "arrow.right.circle.fill") } + .labelStyle(.iconOnly) .buttonStyle(PlainButtonStyle()) .help("RevealInFinder") .fixedSize() @@ -52,8 +53,9 @@ struct AdvancedPreferencePane: View { .fixedSize(horizontal: false, vertical: true) .lineLimit(2) Button(action: { appState.reveal(path: appState.localPath) }) { - Image(systemName: "arrow.right.circle.fill") + Label("RevealInFinder", systemImage: "arrow.right.circle.fill") } + .labelStyle(.iconOnly) .buttonStyle(PlainButtonStyle()) .help("RevealInFinder") .fixedSize() @@ -91,7 +93,7 @@ struct AdvancedPreferencePane: View { Text(SelectedActionType.rename.description) .tag(SelectedActionType.rename) } label: { - Text(verbatim: "OnSelect") + Text("Active/Select") } .labelsHidden() .pickerStyle(.inline) @@ -124,7 +126,7 @@ struct AdvancedPreferencePane: View { if HostHardware.isAppleSilicon() { GroupBox(label: Text("Apple Silicon")) { Toggle("ShowOpenInRosetta", isOn: $appState.showOpenInRosettaOption) - .disabled(appState.createSymLinkOnSelectDisabled) + .disabled(appState.showOpenInRosettaOptionDisabled) Text("ShowOpenInRosettaDescription") .font(.footnote) .foregroundStyle(.secondary) diff --git a/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift b/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift index c565a5af..b32bc578 100644 --- a/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift +++ b/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift @@ -53,6 +53,7 @@ struct GeneralPreferencePane: View { GroupBox(label: Text("AppBehaviour")) { Toggle("TerminateAfterLastWindowClosed", isOn: $appState.terminateAfterLastWindowClosed) Toggle("GroupXcodeVersionsInList", isOn: $appState.enableGroupedXcodeList) + .disabled(PreferenceKey.enableGroupedXcodeList.isManaged()) } .groupBoxStyle(PreferencesGroupBoxStyle()) } diff --git a/Xcodes/Frontend/Preferences/PlatformsListView.swift b/Xcodes/Frontend/Preferences/PlatformsListView.swift index 093d80b3..773ed20c 100644 --- a/Xcodes/Frontend/Preferences/PlatformsListView.swift +++ b/Xcodes/Frontend/Preferences/PlatformsListView.swift @@ -13,13 +13,37 @@ import OrderedCollections struct PlatformsListView: View { @EnvironmentObject var appState: AppState - @State private var runtimes: OrderedDictionary = [:] @State private var selectedRuntime: InstalledPlatformRuntime? + + private var runtimes: OrderedDictionary { + OrderedDictionary(grouping: appState.installedPlatformRuntimes(), by: { $0.runtime.platform }) + } var body: some View { List(selection: $selectedRuntime) { Text("PlatformsList.Title") .font(.body) + if appState.isRefreshingInstalledRuntimes { + ProgressView("RefreshDescription") + .controlSize(.small) + } else if let error = appState.installedRuntimesError { + VStack(alignment: .leading, spacing: 8) { + Label("RuntimeLoadError", systemImage: "exclamationmark.triangle") + Text(error.legibleLocalizedDescription) + .font(.callout) + .textSelection(.enabled) + Button("Refresh") { appState.updateInstalledRuntimes() } + } + } else if runtimes.isEmpty { + ContentUnavailableView { + Label("NoRuntimesToShow", systemImage: "iphone") + } actions: { + Button("Refresh") { + appState.updateDownloadableRuntimes() + appState.updateInstalledRuntimes() + } + } + } ForEach(runtimes.elements.sorted(\.key.order), id: \.key) { platform, runtimeList in Section { ForEach(runtimeList) { installedRuntime in @@ -31,10 +55,13 @@ struct PlatformsListView: View { Button { deleteRuntime(runtime: installedRuntime) } label: { - Image(systemName: "trash") + Label("Alert.DeletePlatform.PrimaryButton", systemImage: "trash") } + .labelStyle(.iconOnly) + .accessibilityValue(runtime.name) .foregroundStyle(.red) .buttonStyle(.plain) + .disabled(appState.isRefreshingInstalledRuntimes) } .frame(height: 30) } @@ -44,6 +71,7 @@ struct PlatformsListView: View { runtimeList.first!.runtime.icon() .aspectRatio(contentMode: .fit) .frame(width: 20) + .accessibilityHidden(true) Text(platform.shortName) .font(.headline) } @@ -53,17 +81,6 @@ struct PlatformsListView: View { } } .listStyle(.inset(alternatesRowBackgrounds: true)) - .task { - loadRuntimes() - } - .onChange(of: appState.installedRuntimes) { _ in - loadRuntimes() - } - } - - func loadRuntimes() { - let filteredRuntimes = appState.installedPlatformRuntimes() - runtimes = OrderedDictionary(grouping: filteredRuntimes, by: { $0.runtime.platform }) } func deleteRuntime(runtime: InstalledPlatformRuntime) { diff --git a/Xcodes/Frontend/Preferences/PreferencesView.swift b/Xcodes/Frontend/Preferences/PreferencesView.swift index 39509cf5..c58ed088 100644 --- a/Xcodes/Frontend/Preferences/PreferencesView.swift +++ b/Xcodes/Frontend/Preferences/PreferencesView.swift @@ -2,7 +2,7 @@ import SwiftUI struct PreferencesView: View { private enum Tabs: Hashable { - case general, updates, advanced, experiment + case general, updates, downloads, advanced, experiment } @EnvironmentObject var appState: AppState @EnvironmentObject var updater: ObservableUpdater @@ -26,8 +26,13 @@ struct PreferencesView: View { .tabItem { Label("Downloads", systemImage: "icloud.and.arrow.down") } - AdvancedPreferencePane() - .environmentObject(appState) + .tag(Tabs.downloads) + ScrollView { + AdvancedPreferencePane() + .environmentObject(appState) + .padding(.trailing, 8) + } + .frame(height: 560) .tabItem { Label("Advanced", systemImage: "gearshape.2") } diff --git a/Xcodes/Frontend/Preferences/UpdatesPreferencePane.swift b/Xcodes/Frontend/Preferences/UpdatesPreferencePane.swift index 7ffe7037..96e6c276 100644 --- a/Xcodes/Frontend/Preferences/UpdatesPreferencePane.swift +++ b/Xcodes/Frontend/Preferences/UpdatesPreferencePane.swift @@ -2,6 +2,7 @@ import Sparkle import SwiftUI struct UpdatesPreferencePane: View { + @EnvironmentObject var appState: AppState @EnvironmentObject var updater: ObservableUpdater @AppStorage("autoInstallation") var autoInstallationType: AutoInstallationType = .none @@ -57,6 +58,9 @@ struct UpdatesPreferencePane: View { } .groupBoxStyle(PreferencesGroupBoxStyle()) } + .onChange(of: autoInstallationType) { + appState.setupAutoInstallTimer() + } } private var lastUpdatedString: String { diff --git a/Xcodes/Frontend/SignIn/SignIn2FAView.swift b/Xcodes/Frontend/SignIn/SignIn2FAView.swift index 34684337..90367968 100644 --- a/Xcodes/Frontend/SignIn/SignIn2FAView.swift +++ b/Xcodes/Frontend/SignIn/SignIn2FAView.swift @@ -10,7 +10,7 @@ struct SignIn2FAView: View { var body: some View { VStack(alignment: .leading) { Text(String(format: localizeString("DigitCodeDescription"), authOptions.securityCode!.length)) - .fixedSize(horizontal: true, vertical: false) + .fixedSize(horizontal: false, vertical: true) HStack { Spacer() @@ -43,7 +43,9 @@ struct SignIn2FAView: View { .frame(height: 25) } .padding() + .frame(width: 452) .emittingError($appState.authError, recoveryHandler: { _ in }) + .handlingErrors(using: AlertErrorHandler(title: "SignInFailure")) } } diff --git a/Xcodes/Frontend/SignIn/SignInFederatedView.swift b/Xcodes/Frontend/SignIn/SignInFederatedView.swift index 36943bfc..f0998c57 100644 --- a/Xcodes/Frontend/SignIn/SignInFederatedView.swift +++ b/Xcodes/Frontend/SignIn/SignInFederatedView.swift @@ -15,6 +15,9 @@ struct SignInFederatedView: View { Text("This Apple Account uses federated authentication via \(organizationName).") .fixedSize(horizontal: false, vertical: true) + Text("FederatedSignInInstructions") + .fixedSize(horizontal: false, vertical: true) + Button("Open Browser") { if let idpURL = federationResponse.idpURL { NSWorkspace.shared.open(idpURL) @@ -53,7 +56,7 @@ struct SignInFederatedView: View { } private var organizationName: String { - let orgName = federationResponse.federatedAuthIntro?.orgName ?? "your organization" + let orgName = federationResponse.federatedAuthIntro?.orgName ?? localizeString("YourOrganisation") if let idpName = federationResponse.federatedAuthIntro?.idpName { return "\(orgName) (\(idpName))" } diff --git a/Xcodes/Frontend/SignIn/SignInPhoneListView.swift b/Xcodes/Frontend/SignIn/SignInPhoneListView.swift index 54f9355a..08468dda 100644 --- a/Xcodes/Frontend/SignIn/SignInPhoneListView.swift +++ b/Xcodes/Frontend/SignIn/SignInPhoneListView.swift @@ -11,10 +11,12 @@ struct SignInPhoneListView: View { VStack(alignment: .leading) { if let phoneNumbers = authOptions.trustedPhoneNumbers, !phoneNumbers.isEmpty { Text(String(format: localizeString("SelectTrustedPhone"), authOptions.securityCode!.length)) + .fixedSize(horizontal: false, vertical: true) List(phoneNumbers, selection: $selectedPhoneNumberID) { Text($0.numberWithDialCode) } + .frame(minHeight: 100) .onAppear { if phoneNumbers.count == 1 { selectedPhoneNumberID = phoneNumbers.first?.id @@ -23,6 +25,7 @@ struct SignInPhoneListView: View { } else { Text("NoTrustedPhones") .font(.callout) + .fixedSize(horizontal: false, vertical: true) Spacer() } @@ -41,8 +44,10 @@ struct SignInPhoneListView: View { .frame(height: 25) } .padding() - .frame(width: 400, height: 200) + .frame(width: 452) + .frame(minHeight: 200) .emittingError($appState.authError, recoveryHandler: { _ in }) + .handlingErrors(using: AlertErrorHandler(title: "SignInFailure")) } } diff --git a/Xcodes/Frontend/SignIn/SignInSMSView.swift b/Xcodes/Frontend/SignIn/SignInSMSView.swift index 73f4960d..597a1fe6 100644 --- a/Xcodes/Frontend/SignIn/SignInSMSView.swift +++ b/Xcodes/Frontend/SignIn/SignInSMSView.swift @@ -11,6 +11,7 @@ struct SignInSMSView: View { var body: some View { VStack(alignment: .leading) { Text(String(format: localizeString("EnterDigitCodeDescription"), authOptions.securityCode!.length, trustedPhoneNumber.numberWithDialCode)) + .fixedSize(horizontal: false, vertical: true) HStack { Spacer() @@ -43,7 +44,9 @@ struct SignInSMSView: View { .frame(height: 25) } .padding() + .frame(width: 452) .emittingError($appState.authError, recoveryHandler: { _ in }) + .handlingErrors(using: AlertErrorHandler(title: "SignInFailure")) } } diff --git a/Xcodes/Frontend/SignIn/SignInSecurityKeyPinView.swift b/Xcodes/Frontend/SignIn/SignInSecurityKeyPinView.swift index 98cebe36..21eafadf 100644 --- a/Xcodes/Frontend/SignIn/SignInSecurityKeyPinView.swift +++ b/Xcodes/Frontend/SignIn/SignInSecurityKeyPinView.swift @@ -18,7 +18,7 @@ struct SignInSecurityKeyPinView: View { var body: some View { VStack(alignment: .leading) { Text(localizeString("SecurityKeyPinDescription")) - .fixedSize(horizontal: true, vertical: false) + .fixedSize(horizontal: false, vertical: true) HStack { Spacer() @@ -46,7 +46,9 @@ struct SignInSecurityKeyPinView: View { .frame(height: 25) } .padding() + .frame(width: 452) .emittingError($appState.authError, recoveryHandler: { _ in }) + .handlingErrors(using: AlertErrorHandler(title: "SignInFailure")) } func submitPinCode() { diff --git a/Xcodes/Frontend/SignIn/SignInSecurityKeyTouchView.swift b/Xcodes/Frontend/SignIn/SignInSecurityKeyTouchView.swift index aafd179a..1d251f22 100644 --- a/Xcodes/Frontend/SignIn/SignInSecurityKeyTouchView.swift +++ b/Xcodes/Frontend/SignIn/SignInSecurityKeyTouchView.swift @@ -16,11 +16,12 @@ struct SignInSecurityKeyTouchView: View { VStack(alignment: .center) { Image(systemName: "key.radiowaves.forward") .font(.system(size: 32)).bold() + .accessibilityHidden(true) .padding(.bottom) HStack { Spacer() Text(localizeString("SecurityKeyTouchDescription")) - .fixedSize(horizontal: true, vertical: false) + .fixedSize(horizontal: false, vertical: true) Spacer() } HStack { @@ -33,12 +34,13 @@ struct SignInSecurityKeyTouchView: View { .scaleEffect(x: 0.5, y: 0.5, anchor: .center) .isHidden(!appState.isProcessingAuthRequest) - .keyboardShortcut(.defaultAction) } .frame(height: 25) } .padding() + .frame(width: 452) .emittingError($appState.authError, recoveryHandler: { _ in }) + .handlingErrors(using: AlertErrorHandler(title: "SignInFailure")) } func cancel() { diff --git a/Xcodes/Frontend/XcodeList/AppStoreButtonStyle.swift b/Xcodes/Frontend/XcodeList/AppStoreButtonStyle.swift deleted file mode 100644 index b500a429..00000000 --- a/Xcodes/Frontend/XcodeList/AppStoreButtonStyle.swift +++ /dev/null @@ -1,149 +0,0 @@ -import SwiftUI - -struct AppStoreButtonStyle: ButtonStyle { - var primary: Bool - var highlighted: Bool - - private struct AppStoreButton: View { - @SwiftUI.Environment(\.isEnabled) var isEnabled - var configuration: ButtonStyle.Configuration - var primary: Bool - var highlighted: Bool - - var textColor: Color { - if isEnabled { - if primary { - if highlighted { - return Color.accentColor - } - else { - return Color.white - } - } - else { - return Color.accentColor - } - } else { - if primary { - if highlighted { - return Color(.disabledControlTextColor) - } - else { - return Color.white - } - } - else { - if highlighted { - return Color.white - } - else { - return Color(.disabledControlTextColor) - } - } - } - } - - func background(isPressed: Bool) -> some View { - Group { - if isEnabled { - if primary { - Capsule() - .fill( - highlighted ? - Color.white : - Color.accentColor - ) - .brightness(isPressed ? -0.25 : 0) - } else { - Capsule() - .fill( - Color(NSColor(red: 0.95, green: 0.95, blue: 0.97, alpha: 1)) - ) - .brightness(isPressed ? -0.25 : 0) - } - } else { - if primary { - Capsule() - .fill( - highlighted ? - Color.white : - Color(.disabledControlTextColor) - ) - .brightness(isPressed ? -0.25 : 0) - } else { - EmptyView() - } - } - } - } - var body: some View { - configuration.label - .font(Font.caption.weight(.bold)) - .foregroundColor(textColor) - .padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8)) - .frame(minWidth: 65) - .background(background(isPressed: configuration.isPressed)) - .padding(1) - } - } - - func makeBody(configuration: ButtonStyle.Configuration) -> some View { - AppStoreButton(configuration: configuration, primary: primary, highlighted: highlighted) - } -} - -struct AppStoreButtonStyle_Previews: PreviewProvider { - static var previews: some View { - Group { - ForEach([ColorScheme.light, .dark], id: \.self) { colorScheme in - Group { - Button{ } label: { - Text(verbatim: "Open") - } - .buttonStyle(AppStoreButtonStyle(primary: true, highlighted: false)) - .padding() - .background(Color(.textBackgroundColor)) - .previewDisplayName("Primary") - Button{ } label: { - Text(verbatim: "Open") - } - .buttonStyle(AppStoreButtonStyle(primary: true, highlighted: true)) - .padding() - .background(Color(.controlAccentColor)) - .previewDisplayName("Primary, Highlighted") - Button{ } label: { - Text(verbatim: "Open") - } - .buttonStyle(AppStoreButtonStyle(primary: true, highlighted: false)) - .padding() - .disabled(true) - .background(Color(.textBackgroundColor)) - .previewDisplayName("Primary, Disabled") - Button{ } label: { - Text(verbatim: "Install") - } - .buttonStyle(AppStoreButtonStyle(primary: false, highlighted: false)) - .padding() - .background(Color(.textBackgroundColor)) - .previewDisplayName("Secondary") - Button{ } label: { - Text(verbatim: "Install") - } - .buttonStyle(AppStoreButtonStyle(primary: false, highlighted: true)) - .padding() - .background(Color(.controlAccentColor)) - .previewDisplayName("Secondary, Highlighted") - Button{ } label: { - Text(verbatim: "Install") - } - .buttonStyle(AppStoreButtonStyle(primary: false, highlighted: false)) - .padding() - .disabled(true) - .background(Color(.textBackgroundColor)) - .previewDisplayName("Secondary, Disabled") - } - .environment(\.colorScheme, colorScheme) - } - } - } -} diff --git a/Xcodes/Frontend/XcodeList/XcodeListView.swift b/Xcodes/Frontend/XcodeList/XcodeListView.swift index 5fec4f9f..0cebb150 100644 --- a/Xcodes/Frontend/XcodeList/XcodeListView.swift +++ b/Xcodes/Frontend/XcodeList/XcodeListView.swift @@ -62,6 +62,30 @@ struct XcodeListView: View { } } .listStyle(.sidebar) + .overlay { + if visibleXcodes.isEmpty { + if appState.isUpdating { + ProgressView() + .accessibilityLabel(Text("RefreshDescription")) + } else if !searchText.isEmpty { + ContentUnavailableView.search(text: searchText) + } else if !appState.allXcodes.isEmpty { + ContentUnavailableView { + Label("NoMatchingXcodes", systemImage: "line.3.horizontal.decrease") + } description: { + Text("NoMatchingXcodesDescription") + } + } else { + ContentUnavailableView { + Label("Xcodes", systemImage: "hammer") + } description: { + Text("RefreshDescription") + } actions: { + Button("Refresh") { appState.update() } + } + } + } + } Divider() PlatformsPocket() @@ -152,7 +176,7 @@ private struct GroupedXcodeListContent: View { self.expandedMinorVersions = updatedExpandedMinorVersions } ) - .tag(majorVersions.first { $0.selected }?.id) + .selectionDisabled() if isMajorExpanded { ForEach(majorVersionGroup.minorVersionGroups) { minorVersionGroup in @@ -182,7 +206,7 @@ private struct GroupedXcodeListContent: View { self.expandedMinorVersions = updatedExpandedMinorVersions } ) - .tag(minorVersions.first { $0.selected }?.id) + .selectionDisabled() if isMinorExpanded { ForEach(minorVersionGroup.versions) { entry in @@ -224,6 +248,7 @@ private struct XcodeVersionGroupRow: View { .frame(width: 12, height: 12) icon + .accessibilityHidden(true) VStack(alignment: .leading, spacing: 2) { Text(verbatim: "Xcode \(displayName)") @@ -231,7 +256,7 @@ private struct XcodeVersionGroupRow: View { .fixedSize(horizontal: false, vertical: true) if let latestRelease { - Text(verbatim: "Latest: \(latestRelease.description)") + Text(String(format: localizeString("LatestReleaseDescription"), latestRelease.description)) .font(.caption) .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) @@ -275,19 +300,22 @@ private struct XcodeVersionGroupRow: View { case .installed: if let latestSelectionTarget, latestSelectionTarget.id != selectedVersion.id { Button(action: { appState.select(xcode: latestSelectionTarget) }) { - Image(systemName: "checkmark.circle.fill") + Image(systemName: "arrow.up.circle.fill") .foregroundColor(.yellow) } + .accessibilityLabel(Text("MakeActiveVersionDescription")) .buttonStyle(PlainButtonStyle()) .help(staleSelectedHelpText(selectedVersion: selectedVersion, latestRelease: latestSelectableRelease, selectionTarget: latestSelectionTarget)) } else { - Image(systemName: "checkmark.circle.fill") + Image(systemName: "arrow.up.circle.fill") .foregroundColor(.yellow) + .accessibilityLabel(Text("ActiveVersionDescription")) .help(staleSelectedHelpText(selectedVersion: selectedVersion, latestRelease: latestSelectableRelease, selectionTarget: latestSelectionTarget)) } case .notInstalled: - Image(systemName: "checkmark.circle.fill") + Image(systemName: "arrow.up.circle.fill") .foregroundColor(.yellow) + .accessibilityLabel(Text("ActiveVersionDescription")) .help(staleSelectedHelpText(selectedVersion: selectedVersion, latestRelease: latestSelectableRelease, selectionTarget: latestSelectionTarget)) case .installing, .uninstalling, .none: EmptyView() @@ -295,12 +323,14 @@ private struct XcodeVersionGroupRow: View { } else if selectedVersion?.selected == true { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) + .accessibilityLabel(Text("ActiveVersionDescription")) .help("ActiveVersionDescription") } else if let latestSelectionTarget { Button(action: { appState.select(xcode: latestSelectionTarget) }) { Image(systemName: "checkmark.circle") .foregroundColor(.secondary) } + .accessibilityLabel(Text("MakeActiveVersionDescription")) .buttonStyle(PlainButtonStyle()) .help("MakeActiveVersionDescription") } @@ -338,15 +368,13 @@ private struct XcodeVersionGroupRow: View { switch latestRelease.installState { case .installed: Button("Open") { appState.open(xcode: latestRelease) } - .textCase(.uppercase) - .buttonStyle(AppStoreButtonStyle(primary: true, highlighted: false)) + .buttonStyle(.bordered) .help("OpenDescription") case .notInstalled: Button("Install") { appState.checkMinVersionAndInstall(id: latestRelease.id) } - .textCase(.uppercase) - .buttonStyle(AppStoreButtonStyle(primary: false, highlighted: false)) + .buttonStyle(.bordered) .help("InstallDescription") case .installing, .uninstalling: EmptyView() diff --git a/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift b/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift index a278d5fc..1798e0b8 100644 --- a/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift +++ b/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift @@ -19,13 +19,15 @@ struct XcodeListViewRow: View { var body: some View { HStack { appIconView(for: xcode) + .accessibilityHidden(true) VStack(alignment: .leading) { HStack { - Text(verbatim: "\(xcode.description) \(xcode.version.buildMetadataIdentifiersDisplay)") + Text(verbatim: xcode.description) .font(.body) .lineLimit(2) .fixedSize(horizontal: false, vertical: true) + .help("\(xcode.description) \(xcode.version.buildMetadataIdentifiersDisplay)") if !xcode.identicalBuildsForCurrentVariant.isEmpty { Image(systemName: "square.fill.on.square.fill") @@ -37,7 +39,7 @@ struct XcodeListViewRow: View { } if xcode.architectures?.isAppleSilicon ?? false { - Image(systemName: "m4.button.horizontal") + Image(systemName: "cpu") .font(.subheadline) .foregroundColor(.secondary) .accessibility(label: Text("Apple Silicon")) @@ -117,14 +119,16 @@ struct XcodeListViewRow: View { switch latestReleaseForSelectedPrerelease.installState { case .installed: Button(action: { appState.select(xcode: latestReleaseForSelectedPrerelease) }) { - Image(systemName: "checkmark.circle.fill") + Label("MakeActive", systemImage: "arrow.up.circle.fill") .foregroundColor(.yellow) } + .labelStyle(.iconOnly) .buttonStyle(PlainButtonStyle()) .help(staleSelectedHelpText) case .notInstalled: - Image(systemName: "checkmark.circle.fill") + Image(systemName: "arrow.up.circle.fill") .foregroundColor(.yellow) + .accessibilityLabel(staleSelectedHelpText) .help(staleSelectedHelpText) case .installing, .uninstalling: EmptyView() @@ -132,12 +136,14 @@ struct XcodeListViewRow: View { } else if xcode.selected { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) + .accessibilityLabel(Text("ActiveVersionDescription")) .help("ActiveVersionDescription") } else { Button(action: { appState.select(xcode: xcode) }) { - Image(systemName: "checkmark.circle") + Label("MakeActive", systemImage: "checkmark.circle") .foregroundColor(.secondary) } + .labelStyle(.iconOnly) .buttonStyle(PlainButtonStyle()) .help("MakeActiveVersionDescription") } @@ -152,8 +158,7 @@ struct XcodeListViewRow: View { xcode.selected, latestReleaseForSelectedPrerelease.installState == .notInstalled { InstallButton(xcode: latestReleaseForSelectedPrerelease) - .textCase(.uppercase) - .buttonStyle(AppStoreButtonStyle(primary: false, highlighted: false)) + .buttonStyle(.bordered) } else { installStateControl(for: xcode) } @@ -164,11 +169,11 @@ struct XcodeListViewRow: View { switch xcode.installState { case .installed: Button("Open") { appState.open(xcode: xcode) } - .buttonStyle(AppStoreButtonStyle(primary: true, highlighted: selected)) + .buttonStyle(.bordered) .help("OpenDescription") case .notInstalled: InstallButton(xcode: xcode) - .buttonStyle(AppStoreButtonStyle(primary: false, highlighted: false)) + .buttonStyle(.bordered) case let .installing(installationStep): InstallationStepRowView( installationStep: installationStep, diff --git a/Xcodes/Resources/Localizable.xcstrings b/Xcodes/Resources/Localizable.xcstrings index 73aa6a15..315286f5 100644 --- a/Xcodes/Resources/Localizable.xcstrings +++ b/Xcodes/Resources/Localizable.xcstrings @@ -1,6 +1,1306 @@ { "sourceLanguage" : "en", "strings" : { + "SignInFailure" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unable to Sign In" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تعذّر تسجيل الدخول" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "No s’ha pogut iniciar la sessió" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Anmeldung nicht möglich" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Δεν είναι δυνατή η σύνδεση" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se ha podido iniciar sesión" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kirjautuminen ei onnistu" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossible de se connecter" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "साइन इन नहीं हो सका" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossibile accedere" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "サインインできません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "로그인할 수 없음" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Inloggen niet mogelijk" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie można się zalogować" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não foi possível iniciar sessão" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось войти" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ไม่สามารถลงชื่อเข้าใช้ได้" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Giriş Yapılamadı" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не вдалося ввійти" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法登录" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法登入" + } + } + } + }, + "FederatedSignInInstructions" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open the browser and sign in with your organisation. Then copy the redirected URL into the field below." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "افتح المتصفح وسجّل الدخول باستخدام حساب مؤسستك. ثم انسخ عنوان URL الذي أُعيد توجيهك إليه إلى الحقل أدناه." + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Obre el navegador i inicia la sessió amb la teva organització. Després copia l’URL de redirecció al camp següent." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Öffne den Browser und melde dich bei deiner Organisation an. Kopiere dann die URL nach der Weiterleitung in das Feld unten." + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ανοίξτε το πρόγραμμα περιήγησης και συνδεθείτε μέσω του οργανισμού σας. Έπειτα αντιγράψτε τη διεύθυνση URL μετά την ανακατεύθυνση στο παρακάτω πεδίο." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abre el navegador e inicia sesión con tu organización. Después copia la URL de redirección en el campo siguiente." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Avaa selain ja kirjaudu organisaatiosi kautta. Kopioi sitten uudelleenohjauksen jälkeinen URL-osoite alla olevaan kenttään." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ouvrez le navigateur et connectez-vous avec votre organisation. Copiez ensuite l’URL de redirection dans le champ ci-dessous." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ब्राउज़र खोलें और अपने संगठन के खाते से साइन इन करें। फिर रीडायरेक्ट होने के बाद का URL नीचे दिए गए फ़ील्ड में कॉपी करें।" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apri il browser e accedi tramite la tua organizzazione. Poi copia l’URL di reindirizzamento nel campo qui sotto." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ブラウザを開き、組織のアカウントでサインインしてください。その後、リダイレクト先のURLを下のフィールドにコピーしてください。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "브라우저를 열고 조직 계정으로 로그인하세요. 그런 다음 리디렉션된 URL을 아래 필드에 복사하세요." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open de browser en log in via je organisatie. Kopieer daarna de URL na de omleiding naar het onderstaande veld." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Otwórz przeglądarkę i zaloguj się przez swoją organizację. Następnie skopiuj adres URL po przekierowaniu do pola poniżej." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abra o navegador e inicie sessão com sua organização. Depois copie a URL de redirecionamento para o campo abaixo." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Откройте браузер и войдите через свою организацию. Затем скопируйте URL после перенаправления в поле ниже." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "เปิดเบราว์เซอร์และลงชื่อเข้าใช้ด้วยบัญชีองค์กรของคุณ จากนั้นคัดลอก URL หลังการเปลี่ยนเส้นทางลงในช่องด้านล่าง" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tarayıcıyı açın ve kuruluşunuz üzerinden giriş yapın. Ardından yönlendirildiğiniz URL’yi aşağıdaki alana kopyalayın." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Відкрийте браузер і ввійдіть через свою організацію. Потім скопіюйте URL після перенаправлення в поле нижче." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开浏览器并使用组织账户登录。然后将重定向后的 URL 复制到下方的输入框中。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "開啟瀏覽器並使用組織帳號登入。然後將重新導向後的 URL 複製到下方的輸入欄位中。" + } + } + } + }, + "YourOrganisation" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "your organisation" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "مؤسستك" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "la teva organització" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "deine Organisation" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "τον οργανισμό σας" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "tu organización" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "organisaatiosi" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "votre organisation" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "आपका संगठन" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "la tua organizzazione" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "所属組織" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "소속 조직" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "je organisatie" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Twoją organizację" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "sua organização" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "вашу организацию" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "องค์กรของคุณ" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "kuruluşunuz" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "вашу організацію" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "你的组织" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "你的組織" + } + } + } + }, + "RuntimeNeedsActiveXcode" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Make an installed Xcode version active before you install this simulator runtime." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "اجعل إصدارًا مثبّتًا من Xcode نشطًا قبل تثبيت بيئة تشغيل المحاكي هذه." + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activa una versió instal·lada de l’Xcode abans d’instal·lar aquest entorn d’execució del simulador." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktiviere eine installierte Xcode-Version, bevor du diese Simulator-Laufzeitumgebung installierst." + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ενεργοποιήστε μια εγκατεστημένη έκδοση του Xcode πριν εγκαταστήσετε αυτό το περιβάλλον εκτέλεσης του προσομοιωτή." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activa una versión instalada de Xcode antes de instalar este entorno de ejecución del simulador." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktivoi asennettu Xcode-versio ennen tämän simulaattorin suoritusympäristön asentamista." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activez une version installée de Xcode avant d’installer cet environnement d’exécution du simulateur." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "इस सिम्युलेटर रनटाइम को इंस्टॉल करने से पहले Xcode के किसी इंस्टॉल किए गए संस्करण को सक्रिय करें।" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Attiva una versione installata di Xcode prima di installare questo ambiente di esecuzione del simulatore." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "このシミュレータランタイムをインストールする前に、インストール済みのXcodeバージョンをアクティブにしてください。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 시뮬레이터 런타임을 설치하기 전에 설치된 Xcode 버전을 활성 버전으로 설정하세요." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Maak een geïnstalleerde Xcode-versie actief voordat je deze simulatorruntime-omgeving installeert." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Przed zainstalowaniem tego środowiska uruchomieniowego symulatora ustaw zainstalowaną wersję Xcode jako aktywną." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ative uma versão instalada do Xcode antes de instalar este ambiente de execução do simulador." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перед установкой этой среды выполнения симулятора сделайте установленную версию Xcode активной." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ตั้งค่า Xcode เวอร์ชันที่ติดตั้งแล้วให้เป็นเวอร์ชันที่ใช้งานก่อนติดตั้งรันไทม์ของตัวจำลองนี้" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bu simülatör çalışma zamanını kurmadan önce kurulu bir Xcode sürümünü etkinleştirin." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Перш ніж установити це середовище виконання симулятора, зробіть установлену версію Xcode активною." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "安装此模拟器运行时之前,请将已安装的 Xcode 版本设为当前使用的版本。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "安裝此模擬器執行階段之前,請將已安裝的 Xcode 版本設為目前使用的版本。" + } + } + } + }, + "LatestReleaseDescription" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Latest: %@" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "الأحدث: %@" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Més recent: %@" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Neueste Version: %@" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Νεότερη έκδοση: %@" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Más reciente: %@" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uusin: %@" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dernière version : %@" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "नवीनतम: %@" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Più recente: %@" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "最新: %@" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "최신: %@" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nieuwste: %@" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Najnowsza: %@" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mais recente: %@" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Последняя версия: %@" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ล่าสุด: %@" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "En Yeni: %@" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Найновіша версія: %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "最新版本:%@" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "最新版本:%@" + } + } + } + }, + "NoMatchingXcodes" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Matching Xcode Versions" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا توجد إصدارات Xcode مطابقة" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "No hi ha versions de l’Xcode coincidents" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keine passenden Xcode-Versionen" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Δεν υπάρχουν αντίστοιχες εκδόσεις Xcode" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No hay versiones de Xcode coincidentes" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ei vastaavia Xcode-versioita" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucune version de Xcode correspondante" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xcode का कोई मेल खाता संस्करण नहीं" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nessuna versione di Xcode corrispondente" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "条件に一致するXcodeバージョンがありません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "일치하는 Xcode 버전 없음" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geen overeenkomende Xcode-versies" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brak pasujących wersji Xcode" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nenhuma versão correspondente do Xcode" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нет подходящих версий Xcode" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ไม่มี Xcode เวอร์ชันที่ตรงกัน" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eşleşen Xcode Sürümü Yok" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Немає відповідних версій Xcode" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "没有匹配的 Xcode 版本" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "沒有符合條件的 Xcode 版本" + } + } + } + }, + "NoMatchingXcodesDescription" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Change your search or filters to show more versions." + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "غيّر البحث أو عوامل التصفية لعرض المزيد من الإصدارات." + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Canvia la cerca o els filtres per mostrar més versions." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ändere die Suche oder die Filter, um weitere Versionen anzuzeigen." + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Αλλάξτε την αναζήτηση ή τα φίλτρα για να εμφανιστούν περισσότερες εκδόσεις." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cambia la búsqueda o los filtros para mostrar más versiones." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Näytä lisää versioita muuttamalla hakua tai suodattimia." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modifiez la recherche ou les filtres pour afficher plus de versions." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "ज़्यादा संस्करण दिखाने के लिए अपनी खोज या फ़िल्टर बदलें।" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Modifica la ricerca o i filtri per mostrare altre versioni." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "検索条件やフィルタを変更すると、ほかのバージョンが表示されます。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "더 많은 버전을 표시하려면 검색어나 필터를 변경하세요." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wijzig je zoekopdracht of filters om meer versies te tonen." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zmień wyszukiwanie lub filtry, aby wyświetlić więcej wersji." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Altere a pesquisa ou os filtros para mostrar mais versões." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Измените поисковый запрос или фильтры, чтобы увидеть больше версий." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "เปลี่ยนคำค้นหาหรือตัวกรองเพื่อแสดงเวอร์ชันเพิ่มเติม" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Daha fazla sürüm göstermek için aramanızı veya filtreleri değiştirin." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Змініть пошуковий запит або фільтри, щоб побачити більше версій." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "更改搜索条件或筛选条件以显示更多版本。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "更改搜尋條件或篩選條件以顯示更多版本。" + } + } + } + }, + "NoRuntimesToShow" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No Simulator Runtimes to Show" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "لا توجد بيئات تشغيل محاكي لعرضها" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "No hi ha entorns d’execució del simulador per mostrar" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keine Simulator-Laufzeitumgebungen zum Anzeigen" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Δεν υπάρχουν περιβάλλοντα εκτέλεσης προσομοιωτή για εμφάνιση" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No hay entornos de ejecución del simulador para mostrar" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ei näytettäviä simulaattorin suoritusympäristöjä" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aucun environnement d’exécution du simulateur à afficher" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "दिखाने के लिए कोई सिम्युलेटर रनटाइम नहीं" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nessun ambiente di esecuzione del simulatore da mostrare" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "表示するシミュレータランタイムがありません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "표시할 시뮬레이터 런타임 없음" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geen simulatorruntime-omgevingen om te tonen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brak środowisk uruchomieniowych symulatora do wyświetlenia" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nenhum ambiente de execução do simulador para mostrar" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Нет сред выполнения симулятора для отображения" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ไม่มีรันไทม์ของตัวจำลองที่จะแสดง" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gösterilecek Simülatör Çalışma Zamanı Yok" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Немає середовищ виконання симулятора для відображення" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "没有可显示的模拟器运行时" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "沒有可顯示的模擬器執行階段" + } + } + } + }, + "RuntimeLoadError" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unable to Load Simulator Runtimes" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تعذّر تحميل بيئات تشغيل المحاكي" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "No s’han pogut carregar els entorns d’execució del simulador" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Simulator-Laufzeitumgebungen konnten nicht geladen werden" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Δεν είναι δυνατή η φόρτωση των περιβαλλόντων εκτέλεσης του προσομοιωτή" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se han podido cargar los entornos de ejecución del simulador" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Simulaattorin suoritusympäristöjä ei voitu ladata" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossible de charger les environnements d’exécution du simulateur" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सिम्युलेटर रनटाइम लोड नहीं हो सके" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossibile caricare gli ambienti di esecuzione del simulatore" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シミュレータランタイムを読み込めません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "시뮬레이터 런타임을 불러올 수 없음" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Simulatorruntime-omgevingen kunnen niet worden geladen" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie można wczytać środowisk uruchomieniowych symulatora" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não foi possível carregar os ambientes de execução do simulador" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось загрузить среды выполнения симулятора" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ไม่สามารถโหลดรันไทม์ของตัวจำลองได้" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Simülatör Çalışma Zamanları Yüklenemedi" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не вдалося завантажити середовища виконання симулятора" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法加载模拟器运行时" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法載入模擬器執行階段" + } + } + } + }, + "RuntimeInstallError" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unable to Install Simulator Runtime" + } + }, + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تعذّر تثبيت بيئة تشغيل المحاكي" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "No s’ha pogut instal·lar l’entorn d’execució del simulador" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Simulator-Laufzeitumgebung konnte nicht installiert werden" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Δεν είναι δυνατή η εγκατάσταση του περιβάλλοντος εκτέλεσης του προσομοιωτή" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "No se ha podido instalar el entorno de ejecución del simulador" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Simulaattorin suoritusympäristöä ei voitu asentaa" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossible d’installer l’environnement d’exécution du simulateur" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सिम्युलेटर रनटाइम इंस्टॉल नहीं हो सका" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Impossibile installare l’ambiente di esecuzione del simulatore" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "シミュレータランタイムをインストールできません" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "시뮬레이터 런타임을 설치할 수 없음" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Simulatorruntime-omgeving kan niet worden geïnstalleerd" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nie można zainstalować środowiska uruchomieniowego symulatora" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Não foi possível instalar o ambiente de execução do simulador" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не удалось установить среду выполнения симулятора" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ไม่สามารถติดตั้งรันไทม์ของตัวจำลองได้" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Simülatör Çalışma Zamanı Kurulamadı" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Не вдалося встановити середовище виконання симулятора" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法安装模拟器运行时" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "無法安裝模擬器執行階段" + } + } + } + }, "" : { "localizations" : { "ar" : { @@ -1545,6 +2845,12 @@ }, "Active/Select" : { "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Active Xcode" + } + }, "ar" : { "stringUnit" : { "state" : "translated", @@ -2214,7 +3520,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Are you sure you want to stop the installation of Xcode %@?" + "value" : "Stop installing %@?" } }, "es" : { @@ -2869,7 +4175,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Apple only supports downloading runtimes iOS 16.0+, watchOS 9.0+, tvOS 16+, visionOS 1.0+ with Xcode 16.1+. Please download, make active and try again." + "value" : "This simulator runtime requires Xcode 16.1 or later. Install a compatible version of Xcode and make it active. Then try again." } }, "es" : { @@ -3000,7 +4306,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + "value" : "This Apple silicon runtime requires Xcode 26 or later. Make a compatible Xcode version active, then try again. Alternatively, install the universal runtime." } }, "es" : { @@ -3394,7 +4700,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Xcode %@ requires macOS %@, but you are running macOS %@, do you still want to install it?" + "value" : "Xcode %@ requires macOS %@. This Mac runs macOS %@. Install it anyway?" } }, "es" : { @@ -4444,7 +5750,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Unable to create symbolic Link" + "value" : "Unable to create symbolic link" } }, "es" : { @@ -4836,7 +6142,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "It will be moved to the Trash, but won't be emptied." + "value" : "Xcodes will move this Xcode installation to the Trash. The Trash will not be emptied." } }, "es" : { @@ -5850,7 +7156,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Xcodes.app Updates" + "value" : "Xcodes Updates" } }, "es" : { @@ -6353,7 +7659,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "When making an Xcode version Active/Selected, try and create a symbolic link named Xcode.app in the installation directory" + "value" : "When you make an Xcode version active, create or update Xcode.app in the installation directory to link to that version." } }, "es" : { @@ -6469,7 +7775,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "When making a prerelease Xcode version Active/Selected, try and create a symbolic link named Xcode-Beta.app in the installation directory" + "value" : "When you make a prerelease Xcode version active, create or update Xcode-Beta.app in the installation directory to link to that version." } } } @@ -8792,7 +10098,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "The Apple data source scrapes the Apple Developer website. It will always show the latest releases that are available, but is more fragile.\n\n[Xcode Releases](https://xcodereleases.com) is an unofficial list of [Xcode Releases](https://xcodereleases.com). It's provided as well-formed data, contains extra information that is not readily available from Apple, and is less likely to break if Apple redesigns their developer website." + "value" : "The Apple data source reads releases from the Apple Developer website. Changes to the website can interrupt updates.\n\n[Xcode Releases](https://xcodereleases.com) provides an unofficial release list with additional version details." } }, "es" : { @@ -8924,7 +10230,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Enter the %d digit code from one of your trusted devices:" + "value" : "Enter the %d-digit code from one of your trusted devices:" } }, "es" : { @@ -9291,7 +10597,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "[aria2](https://github.com/aria2/aria2) uses up to 16 connections to download Xcode 3-5x faster than URLSession. It's bundled as an executable along with its source code within Xcodes to comply with its GPLv2 license.\n\nURLSession is the default Apple API for making URL requests." + "value" : "[aria2](https://github.com/aria2/aria2) uses multiple connections to download Xcode. Download speed depends on your connection and the server. It's bundled as an executable along with its source code within Xcodes to comply with its GPLv2 license.\n\nURLSession is the default Apple API for making URL requests." } }, "es" : { @@ -9685,7 +10991,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Downloading : %d%% complete" + "value" : "Downloading: %d%% complete" } }, "es" : { @@ -10201,7 +11507,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Enter the %1$d digit code sent to %2$@ : " + "value" : "Enter the %1$d-digit code sent to %2$@:" } }, "es" : { @@ -10587,7 +11893,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Faster Unxip" + "value" : "Xcode Extraction" } }, "es" : { @@ -10718,7 +12024,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Thanks to [@_saagarjha](https://twitter.com/_saagarjha), this experiment can increase unxipping speed by up to 70% for some systems.\n\nMore information on how this is accomplished can be seen on the unxip repo - https://github.com/saagarjha/unxip" + "value" : "Use the experimental unxip tool to extract Xcode archives faster. Performance varies by Mac. [Learn about unxip](https://github.com/saagarjha/unxip)." } }, "es" : { @@ -10963,6 +12269,12 @@ "value" : "Φιλτράρισμα εκδόσεων Xcode κατά αρχιτεκτονική." } }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Filter Xcode versions by architecture" + } + }, "es" : { "stringUnit" : { "state" : "translated", @@ -13285,7 +14597,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Xcode %@ failed its security assessment with the following output:\n%@\nIt remains installed at %@ if you wish to use it anyways." + "value" : "Xcode %@ failed its security assessment:\n%@\nThe installation remains at %@." } }, "es" : { @@ -13809,7 +15121,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Missing username or a password. Please try again." + "value" : "Enter your Apple Account and password, then try again." } }, "es" : { @@ -13940,7 +15252,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "No non-prerelease versions available." + "value" : "No release versions are available." } }, "es" : { @@ -15251,7 +16563,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Step %1$d of %2$d : %3$@" + "value" : "Step %1$d of %2$d: %3$@" } }, "es" : { @@ -15883,7 +17195,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Xcodes searches and installs to a single directory. By default (and recommended) is to keep this /Applications. Any changes to where Xcode is stored may result in other apps/services to stop working. " + "value" : "Choose where Xcodes finds and installs Xcode. /Applications is recommended. Changing this directory does not move existing installations; other tools may depend on their current paths." } }, "es" : { @@ -16014,7 +17326,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Last checked : %@" + "value" : "Last checked: %@" } }, "es" : { @@ -16398,7 +17710,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Xcodes caches available Xcode versions and temporary downloads new versions to a directory" + "value" : "Xcodes stores its version list and temporary downloads in this directory." } }, "es" : { @@ -19644,7 +20956,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Keep name as Xcode-X.X.X.app" + "value" : "Keep the version number in the app name" } }, "es" : { @@ -19775,7 +21087,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "On select, will keep the name as the version eg. Xcode-13.4.1.app" + "value" : "Keep the app’s existing name when making it active, for example Xcode-27.0.app." } }, "es" : { @@ -20037,7 +21349,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "On select, will automatically try and rename the active Xcode to Xcode.app, renaming the previous Xcode.app to the version name." + "value" : "When you make an Xcode version active, rename it to Xcode.app. Rename the previous Xcode.app to include its version number." } }, "es" : { @@ -21154,7 +22466,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "PIN not set" + "value" : "Continue Without a PIN" } }, "es" : { @@ -21545,7 +22857,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Below are a list of platforms that are installed on this machine. " + "value" : "Simulator runtimes installed on this Mac." } }, "es" : { @@ -23865,7 +25177,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Select a trusted phone number to receive a %d digit code via SMS:" + "value" : "Select a trusted phone number to receive a %d-digit code by SMS:" } }, "es" : { @@ -24125,7 +25437,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Show Open In Rosetta option" + "value" : "Show Open in Rosetta" } }, "es" : { @@ -24255,7 +25567,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Open in Rosetta option will show where other \"Open\" functions are available. Note : This will only show for Apple Silicon machines." + "value" : "Show an option to open Xcode using Rosetta on Macs with Apple silicon." } }, "es" : { @@ -26697,7 +28009,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "When unxipping, use experiment" + "value" : "Use faster archive extraction" } }, "es" : { diff --git a/XcodesTests/AppStateTests.swift b/XcodesTests/AppStateTests.swift index e5312062..86bcabc9 100644 --- a/XcodesTests/AppStateTests.swift +++ b/XcodesTests/AppStateTests.swift @@ -178,6 +178,60 @@ class AppStateTests: XCTestCase { XCTAssertTrue(subject.installationTasks.isEmpty) } + func test_AutoInstallTimer_DisablingInvalidatesScheduledTimer() throws { + Current.defaults.get = { _ in AutoInstallationType.newestBeta.rawValue } + subject.setupAutoInstallTimer() + let timer = try XCTUnwrap(autoInstallTimer()) + defer { timer.invalidate() } + + Current.defaults.get = { _ in AutoInstallationType.none.rawValue } + subject.setupAutoInstallTimer() + + XCTAssertFalse(timer.isValid) + XCTAssertNil(autoInstallTimer()) + } + + func test_AutoInstallTimer_ReconfiguringReplacesScheduledTimer() throws { + Current.defaults.get = { _ in AutoInstallationType.newestBeta.rawValue } + subject.setupAutoInstallTimer() + let original = try XCTUnwrap(autoInstallTimer()) + defer { original.invalidate() } + + subject.setupAutoInstallTimer() + let replacement = try XCTUnwrap(autoInstallTimer()) + defer { replacement.invalidate() } + + XCTAssertFalse(original.isValid) + XCTAssertTrue(replacement.isValid) + XCTAssertFalse(original === replacement) + XCTAssertGreaterThan(replacement.fireDate.timeIntervalSinceNow, 60 * 60 * 5) + XCTAssertTrue(subject.installationTasks.isEmpty) + } + + func test_AutoInstallTimer_MissingPreferenceInvalidatesScheduledTimer() throws { + Current.defaults.get = { _ in AutoInstallationType.newestBeta.rawValue } + subject.setupAutoInstallTimer() + let timer = try XCTUnwrap(autoInstallTimer()) + defer { timer.invalidate() } + + Current.defaults.get = { _ in nil } + subject.setupAutoInstallTimer() + + XCTAssertFalse(timer.isValid) + XCTAssertNil(autoInstallTimer()) + } + + private func autoInstallTimer() -> Timer? { + Mirror(reflecting: subject!).children.first { $0.label == "autoInstallTimer" }?.value as? Timer + } + + func test_RosettaPreference_RemainsEnabledWhenSelectionRenamesXcode() throws { + try XCTSkipIf(PreferenceKey.showOpenInRosettaOption.isManaged(), "Rosetta preference is managed on this host") + subject.onSelectActionType = .rename + + XCTAssertFalse(subject.showOpenInRosettaOptionDisabled) + } + func test_AuthenticationPolicy_MapsSession401ToNotAuthorized() { let error = AuthenticationRequestPolicy.mapSessionValidationError( NetworkError.non200StatusCode(statusCode: 401, data: Data()) @@ -1510,6 +1564,76 @@ class AppStateTests: XCTestCase { XCTAssertTrue(Current.network.loginClient.urlSession === replacementSession) } + func test_UpdateInstalledRuntimes_PreservesInventoryAndPublishesFailure() async throws { + subject.installedRuntimes = [CoreSimulatorImage( + uuid: "previous-runtime", path: ["relative": "/tmp/previous-runtime.dmg"], + runtimeInfo: CoreSimulatorRuntimeInfo(build: "23A123") + )] + subject.runtimeService = Self.runtimeService(installedRuntimesOutput: { + throw URLError(.cannotLoadFromNetwork) + }) + + subject.updateInstalledRuntimes() + await subject.installedRuntimesTask?.value + + XCTAssertEqual(subject.installedRuntimes.map(\.uuid), ["previous-runtime"]) + XCTAssertEqual((subject.installedRuntimesError as? URLError)?.code, .cannotLoadFromNetwork) + XCTAssertFalse(subject.isRefreshingInstalledRuntimes) + } + + func test_RefreshInstalledRuntimes_RetryClearsErrorAndPublishesLoading() async throws { + subject.installedRuntimesError = URLError(.cannotLoadFromNetwork) + let pending = TestLockedBox?>(nil) + subject.runtimeService = Self.runtimeService(installedRuntimesOutput: { + try await withCheckedThrowingContinuation { continuation in + pending.withValue { $0 = continuation } + } + }) + let task = Task { @MainActor in try await self.subject.refreshInstalledRuntimes() } + for _ in 0..<100 where pending.read({ $0 == nil }) { await Task.yield() } + let continuation = try XCTUnwrap(pending.read { $0 }) + XCTAssertTrue(subject.isRefreshingInstalledRuntimes) + XCTAssertNil(subject.installedRuntimesError) + + continuation.resume(returning: ProcessOutput(status: 0, out: "{}", err: "")) + try await task.value + + XCTAssertFalse(subject.isRefreshingInstalledRuntimes) + XCTAssertNil(subject.installedRuntimesError) + XCTAssertTrue(subject.installedRuntimes.isEmpty) + } + + func test_UpdateInstalledRuntimes_CancelledFailureCannotOverwriteReplacementState() async throws { + let pending = TestLockedBox<[CheckedContinuation]>([]) + subject.runtimeService = Self.runtimeService(installedRuntimesOutput: { + try await withCheckedThrowingContinuation { continuation in + pending.withValue { $0.append(continuation) } + } + }) + subject.updateInstalledRuntimes() + let original = try XCTUnwrap(subject.installedRuntimesTask) + for _ in 0..<100 where pending.read({ $0.count }) < 1 { await Task.yield() } + subject.updateInstalledRuntimes() + let replacement = try XCTUnwrap(subject.installedRuntimesTask) + for _ in 0..<100 where pending.read({ $0.count }) < 2 { await Task.yield() } + let continuations = pending.read { $0 } + guard continuations.count == 2 else { + XCTFail("Both mock runtime requests must begin") + continuations.forEach { $0.resume(throwing: CancellationError()) } + return + } + + continuations[0].resume(throwing: URLError(.cannotLoadFromNetwork)) + await original.value + XCTAssertTrue(subject.isRefreshingInstalledRuntimes) + XCTAssertNil(subject.installedRuntimesError) + + continuations[1].resume(returning: ProcessOutput(status: 0, out: "{}", err: "")) + await replacement.value + XCTAssertFalse(subject.isRefreshingInstalledRuntimes) + XCTAssertNil(subject.installedRuntimesError) + } + func test_RefreshInstalledRuntimes_UsesLiveSimctlOutput() async throws { let identifier = "97772E90-7BD1-4882-9C51-782E62E0AF4F" let json = """ @@ -1599,6 +1723,27 @@ class AppStateTests: XCTestCase { XCTAssertTrue(subject.installedRuntimes.isEmpty) } + func test_InstalledPlatformRuntimes_AppearsWhenCatalogueLoads() throws { + let runtime = try Self.downloadableRuntime(architectures: [.arm64]) + subject.installedRuntimes = [ + CoreSimulatorImage( + uuid: "runtime-uuid", + path: ["relative": "/Library/Developer/CoreSimulator/Images/runtime.dmg"], + runtimeInfo: CoreSimulatorRuntimeInfo( + build: runtime.simulatorVersion.buildUpdate, + supportedArchitectures: [.arm64] + ) + ) + ] + subject.downloadableRuntimes = [] + XCTAssertTrue(subject.installedPlatformRuntimes().isEmpty) + + subject.downloadableRuntimes = [runtime] + + XCTAssertEqual(subject.installedPlatformRuntimes().map(\.installedRuntimeUUID), ["runtime-uuid"]) + XCTAssertEqual(subject.installedRuntimes.count, 1) + } + func test_InstalledPlatformRuntimes_RejectsArchitectureMismatch() throws { let armRuntime = try Self.downloadableRuntime(architectures: [.arm64]) let x86Runtime = try Self.downloadableRuntime(architectures: [.x86_64]) diff --git a/XcodesTests/ProgressButtonTests.swift b/XcodesTests/ProgressButtonTests.swift new file mode 100644 index 00000000..b2180c59 --- /dev/null +++ b/XcodesTests/ProgressButtonTests.swift @@ -0,0 +1,26 @@ +import AppKit +import SwiftUI +import XCTest + +@testable import Xcodes + +@MainActor +final class ProgressButtonTests: XCTestCase { + func testBusyButtonPreservesItsSize() { + let idle = makeHost(isInProgress: false) + let busy = makeHost(isInProgress: true) + + XCTAssertEqual(idle.fittingSize.width, busy.fittingSize.width, accuracy: 0.5) + XCTAssertEqual(idle.fittingSize.height, busy.fittingSize.height, accuracy: 0.5) + } + + private func makeHost(isInProgress: Bool) -> NSHostingView> { + let host = NSHostingView(rootView: ProgressButton(isInProgress: isInProgress, action: {}) { + Text(verbatim: "Continue") + }) + host.frame = NSRect(x: 0, y: 0, width: 180, height: 40) + host.layoutSubtreeIfNeeded() + return host + } + +} diff --git a/XcodesTests/SDKsViewTests.swift b/XcodesTests/SDKsViewTests.swift new file mode 100644 index 00000000..cf11f8b2 --- /dev/null +++ b/XcodesTests/SDKsViewTests.swift @@ -0,0 +1,34 @@ +import XCTest +import XcodesKit + +@testable import Xcodes + +@MainActor +final class SDKsViewTests: XCTestCase { + func testDisplaysVisionOSWhenItIsTheOnlySDK() { + let sdks = SDKs(visionOS: .init(number: "26.0")) + + XCTAssertEqual(SDKsView(sdks: sdks).content, "visionOS: 26.0") + } + + func testDisplaysAllSDKsInPlatformOrder() { + let sdks = SDKs( + macOS: .init(number: "26.0"), + iOS: .init(number: "26.1"), + watchOS: .init(number: "26.2"), + tvOS: .init(number: "26.3"), + visionOS: .init(number: "26.4") + ) + + XCTAssertEqual( + SDKsView(sdks: sdks).content, + "macOS: 26.0\niOS: 26.1\nwatchOS: 26.2\ntvOS: 26.3\nvisionOS: 26.4" + ) + } + + func testOmitsMissingSDKMetadata() { + XCTAssertEqual(SDKsView(sdks: nil).content, "") + XCTAssertEqual(SDKsView(sdks: SDKs()).content, "") + XCTAssertEqual(SDKsView(sdks: SDKs(visionOS: .init("23A123"))).content, "") + } +}