Files
crossdesk/apps/ios/CrossDeskMobile/AppUpdateChecker.swift
T

79 lines
3.1 KiB
Swift

import Foundation
import Combine
/// Main-thread local-app checks triggered by visits, session endings and manual requests.
final class AppUpdateChecker: ObservableObject {
enum Status { case idle, checking, upToDate, available, failed }
// Fetchers complete asynchronously on the main thread.
typealias Fetch = (@escaping (Data?) -> Void) -> (() -> Void)
@Published private(set) var status: Status = .idle
@Published private(set) var availableVersion = ""
var updateAvailable: Bool { !availableVersion.isEmpty }
let currentVersion: String
private let fetch: Fetch
private let compare: (String, Data) -> String?
private var enabled = false
private var generation = UUID()
private var cancelRequest: (() -> Void)?
init(currentVersion: String = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "",
fetch: @escaping Fetch = AppUpdateChecker.fetchRelease,
compare: @escaping (String, Data) -> String? = CrossDeskRTCBridge.checkMobileUpdate) {
self.currentVersion = currentVersion
self.fetch = fetch
self.compare = compare
}
deinit {
cancelRequest?()
}
func setEnabled(_ value: Bool) {
guard enabled != value else { return }
enabled = value
if enabled { checkNow(); return }
generation = UUID()
cancelRequest?()
cancelRequest = nil
if status == .checking {
status = updateAvailable ? .available : .idle
}
}
func checkNow() {
guard enabled, status != .checking else { return }
status = .checking
let token = UUID()
generation = token
cancelRequest = fetch { [weak self] data in
guard let self, self.enabled, self.generation == token else { return }
self.cancelRequest = nil
if let data, let result = self.compare(self.currentVersion, data) {
self.availableVersion = result
self.status = self.updateAvailable ? .available : .upToDate
} else {
self.status = .failed
}
}
}
// Public metadata only: no identity, installed version or credentials sent.
private static func fetchRelease(completion: @escaping (Data?) -> Void) -> (() -> Void) {
let configuration = URLSessionConfiguration.ephemeral
configuration.timeoutIntervalForRequest = 10
configuration.timeoutIntervalForResource = 15
let session = URLSession(configuration: configuration)
let request = URLRequest(url: URL(string: "https://version.crossdesk.cn/version.json")!,
cachePolicy: .reloadIgnoringLocalCacheData)
let task = session.dataTask(with: request) { data, response, error in
let valid = error == nil && (response as? HTTPURLResponse)?.statusCode == 200 &&
(data?.count ?? 0) <= 256 * 1024
DispatchQueue.main.async { completion(valid ? data : nil) }
session.finishTasksAndInvalidate()
}
task.resume()
return { task.cancel(); session.invalidateAndCancel() }
}
}