diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index 0d467c0..d3642c1 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -8,6 +8,31 @@ import os.log /// Downloads and installs Xcodes extension AppState { + + // check to see if we should auto install for the user + public func autoInstallIfNeeded() { + guard let storageValue = UserDefaults.standard.object(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return } + + if autoInstallType == .none { return } + + // get newest xcode version + guard let newestXcode = allXcodes.first, newestXcode.installState == .notInstalled else { + Logger.appState.info("User has latest Xcode already installed") + return + } + + if autoInstallType == .newestBeta { + Logger.appState.info("Auto installing newest Xcode Beta") + // install it, as user doesn't have it installed and it's either latest beta or latest release + install(id: newestXcode.id) + } else if autoInstallType == .newestVersion && newestXcode.version.isNotPrerelease { + Logger.appState.info("Auto installing newest Xcode") + install(id: newestXcode.id) + } else { + Logger.appState.info("No new Xcodes version found to auto install") + } + } + public func install(_ installationType: InstallationType, downloader: Downloader) -> AnyPublisher { install(installationType, downloader: downloader, attemptNumber: 0) .map { _ in Void() } @@ -517,5 +542,30 @@ public enum InstallationType { case version(AvailableXcode) } +public enum AutoInstallationType: Int, Identifiable { + case none = 0 + case newestVersion + case newestBeta + + public var id: Self { self } + + public var isAutoInstalling: Bool { + get { + return self != .none + } + set { + self = newValue ? .newestVersion : .none + } + } + public var isAutoInstallingBeta: Bool { + get { + return self == .newestBeta + } + set { + self = newValue ? .newestBeta : (isAutoInstalling ? .newestVersion : .none) + } + } +} + let XcodeTeamIdentifier = "59GAB85EFG" let XcodeCertificateAuthority = ["Software Signing", "Apple Code Signing Certification Authority", "Apple Root CA"] diff --git a/Xcodes/Backend/AppState+Update.swift b/Xcodes/Backend/AppState+Update.swift index c8124f2..db8bc0a 100644 --- a/Xcodes/Backend/AppState+Update.swift +++ b/Xcodes/Backend/AppState+Update.swift @@ -6,11 +6,20 @@ import SwiftSoup import struct XCModel.Xcode extension AppState { + + var isReadyForUpdate: Bool { + guard let lastUpdated = Current.defaults.date(forKey: "lastUpdated"), + // This is bad date math but for this use case it doesn't need to be exact + lastUpdated < Current.date().addingTimeInterval(-60 * 60 * 5) + else { + return false + } + return true + } + func updateIfNeeded() { guard - let lastUpdated = Current.defaults.date(forKey: "lastUpdated"), - // This is bad date math but for this use case it doesn't need to be exact - lastUpdated < Current.date().addingTimeInterval(-60 * 60 * 24) + isReadyForUpdate else { updatePublisher = updateSelectedXcodePath() .sink( diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index fec7ac2..6e4d622 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -22,6 +22,9 @@ class AppState: ObservableObject { selectedXcodePath: selectedXcodePath ) } + didSet { + autoInstallIfNeeded() + } } @Published var allXcodes: [Xcode] = [] @Published var selectedXcodePath: String? { @@ -56,7 +59,7 @@ class AppState: ObservableObject { private var installationPublishers: [Version: AnyCancellable] = [:] private var selectPublisher: AnyCancellable? private var uninstallPublisher: AnyCancellable? - + private var autoInstallTimer: Timer? // MARK: - var dataSource: DataSource { @@ -69,8 +72,20 @@ class AppState: ObservableObject { guard !isTesting else { return } try? loadCachedAvailableXcodes() checkIfHelperIsInstalled() + setupAutoInstallTimer() } + // MARK: Timer + /// Runs a timer every 6 hours when app is open to check if it needs to auto install any xcodes + func setupAutoInstallTimer() { + guard let storageValue = UserDefaults.standard.object(forKey: "autoInstallation") as? Int, let autoInstallType = AutoInstallationType(rawValue: storageValue) else { return } + + if autoInstallType == .none { return } + + autoInstallTimer = Timer.scheduledTimer(withTimeInterval: 60*60*6, repeats: true) { [weak self] _ in + self?.updateIfNeeded() + } + } // MARK: - Authentication func validateSession() -> AnyPublisher { diff --git a/Xcodes/Frontend/Preferences/UpdatesPreferencePane.swift b/Xcodes/Frontend/Preferences/UpdatesPreferencePane.swift index 48cfccd..69de839 100644 --- a/Xcodes/Frontend/Preferences/UpdatesPreferencePane.swift +++ b/Xcodes/Frontend/Preferences/UpdatesPreferencePane.swift @@ -5,29 +5,51 @@ import SwiftUI struct UpdatesPreferencePane: View { @StateObject var updater = ObservableUpdater() + @AppStorage("autoInstallation") var autoInstallationType: AutoInstallationType = .none + var body: some View { - GroupBox(label: Text("Updates")) { - VStack(alignment: .leading) { - Toggle( - "Automatically check for app updates", - isOn: $updater.automaticallyChecksForUpdates - ) - - Toggle( - "Include prerelease app versions", - isOn: $updater.includePrereleaseVersions - ) - - Button("Check Now") { - SUUpdater.shared()?.checkForUpdates(nil) + VStack(alignment: .leading, spacing: 20) { + GroupBox(label: Text("Versions")) { + VStack(alignment: .leading) { + Toggle( + "Automatically install new versions of Xcode", + isOn: $autoInstallationType.isAutoInstalling + ) + + Toggle( + "Include prerelease/beta versions", + isOn: $autoInstallationType.isAutoInstallingBeta + ) } - - Text("Last checked: \(lastUpdatedString)") - .font(.footnote) + .fixedSize(horizontal: false, vertical: true) } - .frame(maxWidth: .infinity, alignment: .leading) + .groupBoxStyle(PreferencesGroupBoxStyle()) + + Divider() + + GroupBox(label: Text("Xcodes.app Updates")) { + VStack(alignment: .leading) { + Toggle( + "Automatically check for app updates", + isOn: $updater.automaticallyChecksForUpdates + ) + + Toggle( + "Include prerelease app versions", + isOn: $updater.includePrereleaseVersions + ) + + Button("Check Now") { + SUUpdater.shared()?.checkForUpdates(nil) + } + + Text("Last checked: \(lastUpdatedString)") + .font(.footnote) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .groupBoxStyle(PreferencesGroupBoxStyle()) } - .groupBoxStyle(PreferencesGroupBoxStyle()) .frame(width: 400) }