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; \