From d41a6b17d250c965a90c99fad095ebf42339da81 Mon Sep 17 00:00:00 2001 From: mahee96 <47920326+mahee96@users.noreply.github.com> Date: Wed, 15 Oct 2025 20:22:35 +0530 Subject: [PATCH] iOS26: added support for iOS 26 deployment target + CI (fixed layout issues, added splash screen, fixed nav title insets). --- AltStore.xcodeproj/project.pbxproj | 8 +- AltStore/Base.lproj/Main.storyboard | 3 + AltStore/LaunchViewController.swift | 539 +++++++++--------- AltStore/Settings/Settings.storyboard | 3 + .../Settings/SettingsViewController.swift | 10 + AltStore/Sources/Sources.storyboard | 2 + AltStore/Sources/SourcesViewController.swift | 8 + Makefile | 18 +- 8 files changed, 304 insertions(+), 287 deletions(-) diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 3460091f..7d3c5fd5 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -3472,7 +3472,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; @@ -3500,7 +3500,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; @@ -3527,7 +3527,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; @@ -3554,7 +3554,7 @@ ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; diff --git a/AltStore/Base.lproj/Main.storyboard b/AltStore/Base.lproj/Main.storyboard index d33cf363..a5e12d09 100644 --- a/AltStore/Base.lproj/Main.storyboard +++ b/AltStore/Base.lproj/Main.storyboard @@ -532,6 +532,7 @@ + @@ -561,6 +562,7 @@ + @@ -913,6 +915,7 @@ + diff --git a/AltStore/LaunchViewController.swift b/AltStore/LaunchViewController.swift index d10c291f..195dd2bf 100644 --- a/AltStore/LaunchViewController.swift +++ b/AltStore/LaunchViewController.swift @@ -10,301 +10,152 @@ import UIKit import Roxas import minimuxer import WidgetKit - import AltStoreCore import UniformTypeIdentifiers let pairingFileName = "ALTPairingFile.mobiledevicepairing" -final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDelegate -{ +final class LaunchViewController: UIViewController, UIDocumentPickerDelegate { private var didFinishLaunching = false - - private var destinationViewController: TabBarController! - - override var launchConditions: [RSTLaunchCondition] { - let isDatabaseStarted = RSTLaunchCondition(condition: { DatabaseManager.shared.isStarted }) { (completionHandler) in - DatabaseManager.shared.start(completionHandler: completionHandler) - } + private var retries = 0 + private var maxRetries = 3 + private var splashView: SplashView! + private var destinationViewController: TabBarController? + private var startTime: Date! - return [isDatabaseStarted] - } - - override var childForStatusBarStyle: UIViewController? { - return self.children.first - } - - override var childForStatusBarHidden: UIViewController? { - return self.children.first - } - - override func viewDidLoad() - { - defer { - // Create destinationViewController now so view controllers can register for receiving Notifications. - self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as! TabBarController - } + override func viewDidLoad() { super.viewDidLoad() + splashView = SplashView(frame: view.bounds, appName: "SideStore") + destinationViewController = storyboard!.instantiateViewController(withIdentifier: "tabBarController") as? TabBarController + view.addSubview(splashView) } - + override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(true) - if #available(iOS 17, *), !UserDefaults.standard.sidejitenable { - DispatchQueue.global().async { - self.isSideJITServerDetected() { result in - DispatchQueue.main.async { - switch result { - case .success(): - let dialogMessage = UIAlertController(title: "SideJITServer Detected", message: "Would you like to enable SideJITServer", preferredStyle: .alert) - - // Create OK button with action handler - let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in - UserDefaults.standard.sidejitenable = true - }) - - let cancel = UIAlertAction(title: "Cancel", style: .cancel) - //Add OK button to a dialog message - dialogMessage.addAction(ok) - dialogMessage.addAction(cancel) - - // Present Alert to - self.present(dialogMessage, animated: true, completion: nil) - case .failure(_): - print("Cannot find sideJITServer") + super.viewDidAppear(animated) + guard !didFinishLaunching else { return } + Task { + startTime = Date() + await runLaunchSequence() + doPostLaunch() + } + } + + private func runLaunchSequence() async { + guard retries < maxRetries else { return } + retries += 1 + + await Task.detached { + if !DatabaseManager.shared.isStarted { + await withCheckedContinuation { continuation in + DatabaseManager.shared.start { error in + if let error { + Task { await self.handleLaunchError(error, retryCallback: self.runLaunchSequence) } + } else { + Task { await self.finishLaunching() } } + continuation.resume(returning: ()) } } + } else { + await self.finishLaunching() } - } - + }.value + } + + private func doPostLaunch() { + SideJITManager.shared.checkAndPromptIfNeeded(presentingVC: self) if #available(iOS 17, *), UserDefaults.standard.sidejitenable { - DispatchQueue.global().async { - self.askfornetwork() - } + DispatchQueue.global().async { SideJITManager.shared.askForNetwork() } print("SideJITServer Enabled") } - - - + #if !targetEnvironment(simulator) - - guard let pf = fetchPairingFile() else { + guard let pf = PairingFileManager.shared.fetchPairingFile(presentingVC: self) else { displayError("Device pairing file not found.") return } start_minimuxer_threads(pf) #endif } - - func askfornetwork() { - let address = UserDefaults.standard.textInputSideJITServerurl ?? "" - - var SJSURL = address - - if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty { - SJSURL = "http://sidejitserver._http._tcp.local:8080" + + func start_minimuxer_threads(_ pairing_file: String) { + target_minimuxer_address() + let documentsDirectory = FileManager.default.documentsDirectory.absoluteString + do { + let loggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled + try minimuxer.startWithLogger(pairing_file, documentsDirectory, loggingEnabled) + } catch { + try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent(pairingFileName)) + displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR")") } - - // Create a network operation at launch to Refresh SideJITServer - let url = URL(string: "\(SJSURL)/re/")! - let task = URLSession.shared.dataTask(with: url) { (data, response, error) in - print(data) - } - task.resume() + start_auto_mounter(documentsDirectory) } - - func isSideJITServerDetected(completion: @escaping (Result) -> Void) { - let address = UserDefaults.standard.textInputSideJITServerurl ?? "" - - var SJSURL = address - - if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty { - SJSURL = "http://sidejitserver._http._tcp.local:8080" - } - - // Create a network operation at launch to Refresh SideJITServer - let url = URL(string: SJSURL)! - let task = URLSession.shared.dataTask(with: url) { (data, response, error) in - if let error = error { - print("No SideJITServer on Network") - completion(.failure(error)) - return - } - completion(.success(())) - } - task.resume() - return - } - - func fetchPairingFile() -> String? { - let filename = "ALTPairingFile.mobiledevicepairing" - let fm = FileManager.default - let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)") - if fm.fileExists(atPath: documentsPath.path), let contents = try? String(contentsOf: documentsPath), !contents.isEmpty { - print("Loaded ALTPairingFile from \(documentsPath.path)") - return contents - } else if - let appResourcePath = Bundle.main.url(forResource: "ALTPairingFile", withExtension: "mobiledevicepairing"), - fm.fileExists(atPath: appResourcePath.path), - let data = fm.contents(atPath: appResourcePath.path), - let contents = String(data: data, encoding: .utf8), - !contents.isEmpty, - !UserDefaults.standard.isPairingReset { - print("Loaded ALTPairingFile from \(appResourcePath.path)") - return contents - } else if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, !plistString.isEmpty, !plistString.contains("insert pairing file here"), !UserDefaults.standard.isPairingReset{ - print("Loaded ALTPairingFile from Info.plist") - return plistString - } else { - // Show an alert explaining the pairing file - // Create new Alert - let dialogMessage = UIAlertController(title: "Pairing File", message: "Select the pairing file or select \"Help\" for help.", preferredStyle: .alert) - - // Create OK button with action handler - let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in - // Try to load it from a file picker - var types = UTType.types(tag: "plist", tagClass: UTTagClass.filenameExtension, conformingTo: nil) - types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: UTTagClass.filenameExtension, conformingTo: UTType.data)) - types.append(.xml) - let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: types) - documentPickerController.shouldShowFileExtensions = true - documentPickerController.delegate = self - self.present(documentPickerController, animated: true, completion: nil) - UserDefaults.standard.isPairingReset = false - }) - - //Add "help" button to take user to wiki - let wikiOption = UIAlertAction(title: "Help", style: .default) { (action) in - let wikiURL: String = "https://docs.sidestore.io/docs/installation/pairing-file" - if let url = URL(string: wikiURL) { - UIApplication.shared.open(url) - } - sleep(2) - exit(0) - } - - //Add buttons to dialog message - dialogMessage.addAction(wikiOption) - dialogMessage.addAction(ok) - // Present Alert to - self.present(dialogMessage, animated: true, completion: nil) - - let dialogMessage2 = UIAlertController(title: "Analytics", message: "This app contains anonymous analytics for research and project development. By continuing to use this app, you are consenting to this data collection", preferredStyle: .alert) - - let ok2 = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in}) - - dialogMessage2.addAction(ok2) - self.present(dialogMessage2, animated: true, completion: nil) - - return nil - } - } + func fetchPairingFile() -> String? { PairingFileManager.shared.fetchPairingFile(presentingVC: self) } func displayError(_ msg: String) { print(msg) - // Create a new alert - let dialogMessage = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert) - - // Present alert to user - self.present(dialogMessage, animated: true, completion: nil) + let alert = UIAlertController(title: "Error launching SideStore", message: msg, preferredStyle: .alert) + self.present(alert, animated: true) } - + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { let url = urls[0] let isSecuredURL = url.startAccessingSecurityScopedResource() == true do { - // Read to a string - let data1 = try Data(contentsOf: urls[0]) - let pairing_string = String(bytes: data1, encoding: .utf8) - if pairing_string == nil { + let data = try Data(contentsOf: url) + guard let pairingString = String(data: data, encoding: .utf8) else { displayError("Unable to read pairing file") + return } - - // Save to a file for next launch - let pairingFile = FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)") - try pairing_string?.write(to: pairingFile, atomically: true, encoding: String.Encoding.utf8) - - // Start minimuxer now that we have a file - start_minimuxer_threads(pairing_string!) + try pairingString.write(to: FileManager.default.documentsDirectory.appendingPathComponent(pairingFileName), atomically: true, encoding: .utf8) + start_minimuxer_threads(pairingString) } catch { displayError("Unable to read pairing file") } - - if (isSecuredURL) { - url.stopAccessingSecurityScopedResource() - } - controller.dismiss(animated: true, completion: nil) + + if isSecuredURL { url.stopAccessingSecurityScopedResource() } + controller.dismiss(animated: true) } - + func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { displayError("Choosing a pairing file was cancelled. Please re-open the app and try again.") } - - func start_minimuxer_threads(_ pairing_file: String) { - target_minimuxer_address() - let documentsDirectory = FileManager.default.documentsDirectory.absoluteString - do { - // enable minimuxer console logging only if enabled in settings - let isMinimuxerConsoleLoggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled - try minimuxer.startWithLogger(pairing_file, documentsDirectory, isMinimuxerConsoleLoggingEnabled) - } catch { - try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")) - displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")") - } - start_auto_mounter(documentsDirectory) - // Create destinationViewController now so view controllers can register for receiving Notifications. - self.destinationViewController = self.storyboard!.instantiateViewController(withIdentifier: "tabBarController") as? TabBarController - } } -extension LaunchViewController -{ - override func handleLaunchError(_ error: Error) - { - do - { - throw error - } - catch let error as NSError - { +extension LaunchViewController { + @MainActor + func handleLaunchError(_ error: Error, retryCallback: (() async -> Void)? = nil) { + do { throw error } catch let error as NSError { let title = error.userInfo[NSLocalizedFailureErrorKey] as? String ?? NSLocalizedString("Unable to Launch SideStore", comment: "") - - let errorDescription: String - - if #available(iOS 14.5, *) - { - let errorMessages = [error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription } - errorDescription = errorMessages.joined(separator: "\n\n") + let desc: String + if #available(iOS 14.5, *) { + desc = ([error.debugDescription] + error.underlyingErrors.map { ($0 as NSError).debugDescription }).joined(separator: "\n\n") + } else { + desc = error.debugDescription } - else - { - errorDescription = error.debugDescription - } - - let alertController = UIAlertController(title: title, message: errorDescription, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { (action) in - self.handleLaunchConditions() - })) - self.present(alertController, animated: true, completion: nil) + let alert = UIAlertController(title: title, message: desc, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default) { _ in + Task { await retryCallback?() } + }) + present(alert, animated: true) } } - - override func finishLaunching() - { - super.finishLaunching() - - guard !self.didFinishLaunching else { return } + + @MainActor + func finishLaunching() async { + guard !didFinishLaunching else { return } + didFinishLaunching = true AppManager.shared.update() AppManager.shared.updatePatronsIfNeeded() PatreonAPI.shared.refreshPatreonAccount() - AppManager.shared.updateAllSources { result in guard case .failure(let error) = result else { return } Logger.main.error("Failed to update sources on launch. \(error.localizedDescription, privacy: .public)") + let errorDesc = ErrorProcessing(.fullError).getDescription(error: error as NSError) print("Failed to update sources on launch. \(errorDesc)") @@ -312,63 +163,64 @@ extension LaunchViewController if String(describing: error).contains("The Internet connection appears to be offline"){ mode = .localizedDescription // dont make noise! } - let toastView = ToastView(error: error, mode: mode) toastView.addTarget(self.destinationViewController, action: #selector(TabBarController.presentSources), for: .touchUpInside) - toastView.show(in: self.destinationViewController.selectedViewController ?? self.destinationViewController) + toastView.show(in: self.destinationViewController!.selectedViewController ?? self.destinationViewController!) } - - self.updateKnownSources() - - // Ask widgets to be refreshed + updateKnownSources() WidgetCenter.shared.reloadAllTimelines() + didFinishLaunching = true - // Add view controller as child (rather than presenting modally) - // so tint adjustment + card presentations works correctly. - self.destinationViewController.view.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height) - self.destinationViewController.view.alpha = 0.0 - self.addChild(self.destinationViewController) - self.view.addSubview(self.destinationViewController.view, pinningEdgesWith: .zero) - self.destinationViewController.didMove(toParent: self) + let destinationVC = destinationViewController! - UIView.animate(withDuration: 0.2) { - self.destinationViewController.view.alpha = 1.0 - } + let elapsed = abs(startTime.timeIntervalSinceNow) + let remaining = elapsed >= 1 ? 0 : 1 - elapsed + try? await Task.sleep(nanoseconds: UInt64(remaining * 1_000_000_000)) - self.didFinishLaunching = true - } -} + destinationVC.loadViewIfNeeded() + addChild(destinationVC) + destinationVC.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(destinationVC.view) + destinationVC.didMove(toParent: self) + + // Pin edges BEFORE animation + NSLayoutConstraint.activate([ + destinationVC.view.topAnchor.constraint(equalTo: view.topAnchor), + destinationVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + destinationVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + destinationVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) -private extension LaunchViewController -{ - func updateKnownSources() - { + // Set initial alpha for fade-in + destinationVC.view.alpha = 0 + + UIView.transition(with: view, duration: 0.3, options: .transitionCrossDissolve) { [self] in + self.splashView.alpha = 0 + destinationVC.view.alpha = 1 + } completion: { _ in + self.splashView.removeFromSuperview() + self.destinationViewController = destinationVC + } + } + + func updateKnownSources() { AppManager.shared.updateKnownSources { result in - switch result - { + switch result { case .failure(let error): print("[ALTLog] Failed to update known sources:", error) case .success((_, let blockedSources)): DatabaseManager.shared.persistentContainer.performBackgroundTask { context in let blockedSourceIDs = Set(blockedSources.lazy.map { $0.identifier }) let blockedSourceURLs = Set(blockedSources.lazy.compactMap { $0.sourceURL }) - - let predicate = NSPredicate(format: "%K IN %@ OR %K IN %@", - #keyPath(Source.identifier), blockedSourceIDs, - #keyPath(Source.sourceURL), blockedSourceURLs) - - let sourceErrors = Source.all(satisfying: predicate, in: context).map { (source) in - let blockedSource = blockedSources.first { $0.identifier == source.identifier } - return SourceError.blocked(source, bundleIDs: blockedSource?.bundleIDs, existingSource: source) + let predicate = NSPredicate(format: "%K IN %@ OR %K IN %@", #keyPath(Source.identifier), blockedSourceIDs, #keyPath(Source.sourceURL), blockedSourceURLs) + let sourceErrors = Source.all(satisfying: predicate, in: context).map { source in + let blocked = blockedSources.first { $0.identifier == source.identifier } + return SourceError.blocked(source, bundleIDs: blocked?.bundleIDs, existingSource: source) } - guard !sourceErrors.isEmpty else { return } - Task { - for error in sourceErrors - { + for error in sourceErrors { let title = String(format: NSLocalizedString("“%@” Blocked", comment: ""), error.$source.name) let message = [error.localizedDescription, error.recoverySuggestion].compactMap { $0 }.joined(separator: "\n\n") - await self.presentAlert(title: title, message: message) } } @@ -377,3 +229,142 @@ private extension LaunchViewController } } } + +// MARK: - SplashView +final class SplashView: UIView { + let iconView = UIImageView() + let titleLabel = UILabel() + + init(frame: CGRect, appName: String) { + super.init(frame: frame) + backgroundColor = .systemBackground + setupIcon() + setupTitle(appName: appName) + } + + required init?(coder: NSCoder) { fatalError() } + + private func setupIcon() { + let container = UIView() + container.translatesAutoresizingMaskIntoConstraints = false + container.layer.shadowColor = UIColor.black.cgColor + container.layer.shadowOpacity = 0.25 + container.layer.shadowOffset = CGSize(width: 0, height: 4) + container.layer.shadowRadius = 8 + addSubview(container) + + iconView.image = UIImage(named: "AppIcon") ?? UIImage(named: "AppIcon60x60") ?? UIImage(systemName: "app.fill") + iconView.contentMode = .scaleAspectFit + iconView.translatesAutoresizingMaskIntoConstraints = false + iconView.layer.cornerRadius = 24 + iconView.clipsToBounds = true + container.addSubview(iconView) + + NSLayoutConstraint.activate([ + container.centerXAnchor.constraint(equalTo: centerXAnchor), + container.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -20), + container.widthAnchor.constraint(equalToConstant: 120), + container.heightAnchor.constraint(equalToConstant: 120), + iconView.topAnchor.constraint(equalTo: container.topAnchor), + iconView.bottomAnchor.constraint(equalTo: container.bottomAnchor), + iconView.leadingAnchor.constraint(equalTo: container.leadingAnchor), + iconView.trailingAnchor.constraint(equalTo: container.trailingAnchor) + ]) + } + + private func setupTitle(appName: String) { + titleLabel.text = appName + titleLabel.font = .systemFont(ofSize: 24, weight: .bold) + titleLabel.textColor = .label + titleLabel.textAlignment = .center + titleLabel.translatesAutoresizingMaskIntoConstraints = false + addSubview(titleLabel) + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 12), + titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor) + ]) + } +} + +// MARK: - PairingFileManager +final class PairingFileManager { + static let shared = PairingFileManager() + func fetchPairingFile(presentingVC: UIViewController) -> String? { + let fm = FileManager.default + let filename = pairingFileName + let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)") + if fm.fileExists(atPath: documentsPath.path), + let contents = try? String(contentsOf: documentsPath), !contents.isEmpty { + return contents + } + if let url = Bundle.main.url(forResource: "ALTPairingFile", withExtension: "mobiledevicepairing"), + fm.fileExists(atPath: url.path), + let data = fm.contents(atPath: url.path), + let contents = String(data: data, encoding: .utf8), + !contents.isEmpty, !UserDefaults.standard.isPairingReset { return contents } + if let plistString = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String, + !plistString.isEmpty, !plistString.contains("insert pairing file here"), !UserDefaults.standard.isPairingReset { return plistString } + + presentPairingFileAlert(on: presentingVC) + return nil + } + + private func presentPairingFileAlert(on vc: UIViewController) { + let alert = UIAlertController(title: "Pairing File", message: "Select the pairing file or select \"Help\" for help.", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "Help", style: .default) { _ in + if let url = URL(string: "https://docs.sidestore.io/docs/installation/pairing-file") { UIApplication.shared.open(url) } + sleep(2); exit(0) + }) + alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in + var types = UTType.types(tag: "plist", tagClass: .filenameExtension, conformingTo: nil) + types.append(contentsOf: UTType.types(tag: "mobiledevicepairing", tagClass: .filenameExtension, conformingTo: .data)) + types.append(.xml) + let picker = UIDocumentPickerViewController(forOpeningContentTypes: types) + picker.delegate = vc as? UIDocumentPickerDelegate + picker.shouldShowFileExtensions = true + vc.present(picker, animated: true) + UserDefaults.standard.isPairingReset = false + }) + vc.present(alert, animated: true) + } +} + +// MARK: - SideJITManager +final class SideJITManager { + static let shared = SideJITManager() + func checkAndPromptIfNeeded(presentingVC: UIViewController) { + guard #available(iOS 17, *), !UserDefaults.standard.sidejitenable else { return } + DispatchQueue.global().async { + self.isSideJITServerDetected { result in + DispatchQueue.main.async { + switch result { + case .success(): + let alert = UIAlertController(title: "SideJITServer Detected", message: "Would you like to enable SideJITServer", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in UserDefaults.standard.sidejitenable = true }) + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + presentingVC.present(alert, animated: true) + case .failure(_): print("Cannot find sideJITServer") + } + } + } + } + } + + func askForNetwork() { + let address = UserDefaults.standard.textInputSideJITServerurl ?? "" + let SJSURL = address.isEmpty ? "http://sidejitserver._http._tcp.local:8080" : address + URLSession.shared.dataTask(with: URL(string: "\(SJSURL)/re/")!) { data, resp, err in + print("data: \(String(describing: data)), response: \(String(describing: resp)), error: \(String(describing: err))") + }.resume() + } + + func isSideJITServerDetected(completion: @escaping (Result) -> Void) { + let address = UserDefaults.standard.textInputSideJITServerurl ?? "" + let SJSURL = address.isEmpty ? "http://sidejitserver._http._tcp.local:8080" : address + guard let url = URL(string: SJSURL) else { return } + URLSession.shared.dataTask(with: url) { _, _, error in + if let error = error { completion(.failure(error)); return } + completion(.success(())) + }.resume() + } +} diff --git a/AltStore/Settings/Settings.storyboard b/AltStore/Settings/Settings.storyboard index 9b1ea321..a60c7141 100644 --- a/AltStore/Settings/Settings.storyboard +++ b/AltStore/Settings/Settings.storyboard @@ -1450,6 +1450,8 @@ + + @@ -1923,6 +1925,7 @@ Settings by i cons from the Noun Project + diff --git a/AltStore/Settings/SettingsViewController.swift b/AltStore/Settings/SettingsViewController.swift index f6c4cd8f..f4f16c05 100644 --- a/AltStore/Settings/SettingsViewController.swift +++ b/AltStore/Settings/SettingsViewController.swift @@ -202,6 +202,16 @@ final class SettingsViewController: UITableViewController { super.viewDidLoad() + // --- iOS 26 fix --- + if #available(iOS 26.0, *) { + let appearance = UINavigationBarAppearance() +// appearance.configureWithOpaqueBackground() // or .defaultBackground if you want blur +// appearance.backgroundColor = UIColor(named: "SettingsBackground") + appearance.titleTextAttributes = [.foregroundColor: UIColor.white] + appearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white] + navigationController?.navigationBar.standardAppearance = appearance + navigationController?.navigationBar.scrollEdgeAppearance = appearance // required for iOS 26, maybe enforce it in storyboard? + } let nib = UINib(nibName: "SettingsHeaderFooterView", bundle: nil) self.prototypeHeaderFooterView = nib.instantiate(withOwner: nil, options: nil)[0] as? SettingsHeaderFooterView diff --git a/AltStore/Sources/Sources.storyboard b/AltStore/Sources/Sources.storyboard index d521df37..c8e5cf4e 100644 --- a/AltStore/Sources/Sources.storyboard +++ b/AltStore/Sources/Sources.storyboard @@ -20,6 +20,7 @@ + @@ -248,6 +249,7 @@ + diff --git a/AltStore/Sources/SourcesViewController.swift b/AltStore/Sources/SourcesViewController.swift index 5e9b80fa..8f74fd96 100644 --- a/AltStore/Sources/SourcesViewController.swift +++ b/AltStore/Sources/SourcesViewController.swift @@ -46,6 +46,14 @@ final class SourcesViewController: UICollectionViewController { super.viewDidLoad() + // Ensure large titles + navigationController?.navigationBar.prefersLargeTitles = true + navigationItem.largeTitleDisplayMode = .automatic + + // Set title + navigationItem.title = "Sources" + navigationController?.navigationBar.layoutMargins.left = 20 + let layout = self.makeLayout() self.collectionView.collectionViewLayout = layout diff --git a/Makefile b/Makefile index a5d17569..937e80d7 100755 --- a/Makefile +++ b/Makefile @@ -201,7 +201,7 @@ build-and-test: @echo "" @echo "Performing a build and running tests..." @xcodebuild test \ - -destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' \ + -destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.0' \ -resultBundlePath build/tests/test-results.xcresult \ -enableCodeCoverage YES \ $(COMMON_BUILD_SETTINGS) @@ -213,7 +213,7 @@ build-tests: @echo "Performing a build-for-testing..." @xcodebuild build-for-testing \ -enableCodeCoverage YES \ - -destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' \ + -destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.0' \ $(COMMON_BUILD_SETTINGS) run-tests: @@ -224,22 +224,22 @@ run-tests: @xcodebuild test-without-building \ -enableCodeCoverage YES \ -resultBundlePath build/tests/test-results.xcresult \ - -destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' \ + -destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.0' \ $(COMMON_BUILD_SETTINGS) boot-sim-async: - @if xcrun simctl list devices "iPhone 16 Pro" | grep -q "Booted"; then \ - echo "Simulator 'iPhone 16 Pro' is already booted."; \ + @if xcrun simctl list devices "iPhone 17 Pro" | grep -q "Booted"; then \ + echo "Simulator 'iPhone 17 Pro' is already booted."; \ else \ - echo "Booting simulator 'iPhone 16 Pro' asynchronously..."; \ - xcrun simctl boot "iPhone 16 Pro" & \ + echo "Booting simulator 'iPhone 17 Pro' asynchronously..."; \ + xcrun simctl boot "iPhone 17 Pro" & \ echo "Simulator boot command dispatched."; \ fi sim-boot-check: @echo "Checking simulator boot status..." - @if xcrun simctl list devices "iPhone 16 Pro" | grep -q "Booted"; then \ - echo "Simulator 'iPhone 16 Pro' is booted."; \ + @if xcrun simctl list devices "iPhone 17 Pro" | grep -q "Booted"; then \ + echo "Simulator 'iPhone 17 Pro' is booted."; \ else \ echo "Simulator bootup failed or is not booted yet."; \ exit 1; \