diff --git a/Loop/Extensions/Defaults+Extensions.swift b/Loop/Extensions/Defaults+Extensions.swift index a1c38cb7..7c3c06d2 100644 --- a/Loop/Extensions/Defaults+Extensions.swift +++ b/Loop/Extensions/Defaults+Extensions.swift @@ -81,7 +81,12 @@ extension Defaults.Keys { static let excludedApps = Key<[URL]>("excludedApps", default: [], iCloud: true) // About - static let includeDevelopmentVersions = Key("includeDevelopmentVersions", default: false, iCloud: true) + #if RELEASE + static let includeDevelopmentVersions = Key("includeDevelopmentVersions", default: false, iCloud: true) + #else + // Development versions should check for development updates by default. + static let includeDevelopmentVersions = Key("includeDevelopmentVersions", default: true, iCloud: true) + #endif } // MARK: - Hidden Settings diff --git a/Loop/Extensions/OperatingSystemVersion+Extensions.swift b/Loop/Extensions/OperatingSystemVersion+Extensions.swift new file mode 100644 index 00000000..735403e3 --- /dev/null +++ b/Loop/Extensions/OperatingSystemVersion+Extensions.swift @@ -0,0 +1,14 @@ +// +// OperatingSystemVersion+Extensions.swift +// Loop +// +// Created by Kai Azim on 2026-01-05. +// + +import Foundation + +extension OperatingSystemVersion: @retroactive CustomStringConvertible { + public var description: String { + "\(majorVersion).\(minorVersion).\(patchVersion)" + } +} diff --git a/Loop/Updater/UpdateView.swift b/Loop/Updater/UpdateView.swift index 5f008cea..8536592c 100644 --- a/Loop/Updater/UpdateView.swift +++ b/Loop/Updater/UpdateView.swift @@ -184,10 +184,24 @@ struct UpdateView: View { func changelogView() -> some View { ScrollView(showsIndicators: false) { - LazyVStack { + VStack { // Using LazyVStack seems to cause visual glitches ForEach(updater.changelog, id: \.title) { item in if !item.body.isEmpty { - ChangelogSectionView(item: item) + ChangelogSectionView( + isExpanded: Binding( + get: { + updater.expandedChangelogSections.contains(item.title) + }, + set: { newValue in + if newValue { + updater.expandedChangelogSections.insert(item.title) + } else { + updater.expandedChangelogSections.remove(item.title) + } + } + ), + item: item + ) } } } @@ -201,7 +215,7 @@ struct ChangelogSectionView: View { @Environment(\.luminareAnimation) var luminareAnimation @Environment(\.luminareCornerRadii) var luminareCornerRadii - @State var isExpanded = false + @Binding var isExpanded: Bool let item: (title: String, body: [Updater.ChangelogNote]) var body: some View { diff --git a/Loop/Updater/Updater.swift b/Loop/Updater/Updater.swift index bfad8280..a77b70ee 100755 --- a/Loop/Updater/Updater.swift +++ b/Loop/Updater/Updater.swift @@ -16,8 +16,9 @@ final class Updater: ObservableObject { @Published private(set) var targetRelease: Release? @Published private(set) var progressBar: Double = 0 @Published private(set) var updateState: UpdateAvailability = .notChecked - @Published private(set) var changelog: [(title: String, body: [ChangelogNote])] = .init() @Published private(set) var updatesEnabled: Bool = Updater.checkIfUpdatesEnabled() + @Published private(set) var changelog: [(title: String, body: [ChangelogNote])] = .init() + @Published var expandedChangelogSections: Set = [] // By title private var windowController: NSWindowController? private var includeDevelopmentVersions: Bool { Defaults[.includeDevelopmentVersions] } @@ -207,6 +208,24 @@ final class Updater: ObservableObject { isUpdateAvailable = versionBuild > currentBuild } + // If the update's tag and build number passes the checks above, check the minimum macOS version + if isUpdateAvailable { + let lines = release.body + .split(whereSeparator: \.isNewline) + .reversed() + + for line in lines { + if let minimumMacOSVersion = extractMinimumMacOSVersion(from: String(line)) { + if !ProcessInfo.processInfo.isOperatingSystemAtLeast(minimumMacOSVersion) { + Log.warn("Minimum macOS version requirement for next update not met (required: \(minimumMacOSVersion))", category: .updater) + isUpdateAvailable = false + } else { + Log.success("Minimum macOS version requirement for next update is met", category: .updater) + } + } + } + } + updateState = isUpdateAvailable ? .available : .unavailable if isUpdateAvailable { @@ -214,10 +233,31 @@ final class Updater: ObservableObject { targetRelease = release processChangelog(release.body) + } else { + Log.info("No update available.", category: .updater) } } } + func extractMinimumMacOSVersion(from changelog: String) -> OperatingSystemVersion? { + let regex = /Minimum macOS version:\s*(?\d+)(?:\.(?\d+))?(?:\.(?\d+))?/ + + guard let match = changelog.firstMatch(of: regex.ignoresCase()), + let major = Int(match.major) + else { + return nil + } + + let minor = match.minor.flatMap { Int($0) } ?? 0 + let patch = match.patch.flatMap { Int($0) } ?? 0 + + return OperatingSystemVersion( + majorVersion: major, + minorVersion: minor, + patchVersion: patch + ) + } + private func processChangelog(_ body: String) { changelog = .init() @@ -279,6 +319,10 @@ final class Updater: ObservableObject { )) } } + + if let firstSection = changelog.first { + expandedChangelogSections = [firstSection.title] + } } func showUpdateWindow() async {