diff --git a/AltStore/Authentication/AuthenticationViewController.swift b/AltStore/Authentication/AuthenticationViewController.swift index 11367b58..d1db2e18 100644 --- a/AltStore/Authentication/AuthenticationViewController.swift +++ b/AltStore/Authentication/AuthenticationViewController.swift @@ -34,9 +34,13 @@ final class AuthenticationViewController: UIViewController // fetch anisette servers asap when loading Auth Screen (if list is empty if(UserDefaults.standard.menuAnisetteServersList.isEmpty){ Task{ - await AnisetteViewModel.getListOfServers( - serverSource: UserDefaults.standard.menuAnisetteList - ) + let sourceURL = UserDefaults.standard.menuAnisetteList + do{ + _ = try await AnisetteViewModel.getListOfServers(serverSource: sourceURL) + print("AuthenticationViewController: Server list refresh request completed for sourceURL: \(sourceURL)") + }catch{ + print("AuthenticationViewController: Server list refresh request Failed for sourceURL: \(sourceURL) Error: \(error)") + } } } diff --git a/AltStore/Settings/AnisetteServerList.swift b/AltStore/Settings/AnisetteServerList.swift index 2b62d81e..f6c9405d 100644 --- a/AltStore/Settings/AnisetteServerList.swift +++ b/AltStore/Settings/AnisetteServerList.swift @@ -36,65 +36,79 @@ class AnisetteViewModel: ObservableObject { } } - func getDefaultListOfServers() { - // initiate fetch but do not wait in blocking manner - Task{ - let anisetteServers = await AnisetteViewModel.getListOfServers(serverSource: self.source) - - // always update on main thread for Observables - DispatchQueue.main.async { + @MainActor + func getCurrentListOfServers(_ completionHandler: @escaping (Result) -> Void = {_ in }) { + // dispatch fetch operation but don't do a blocking wait for results + Task { + do { + let anisetteServers = try await AnisetteViewModel.getListOfServers(serverSource: self.source) + // Update UI-related state on the main thread self.servers = anisetteServers + print("AnisetteViewModel: Server list refresh request completed for sourceURL: \(self.source)") + completionHandler(.success(())) + } catch { + print("AnisetteViewModel: Server list refresh request Failed for sourceURL: \(self.source) Error: \(error)") + completionHandler(.failure(error)) } } } - static func getListOfServers(serverSource: String) async -> [Server] { + static func getListOfServers(serverSource: String) async throws -> [Server] { var aniServers: [Server] = [] guard let url = URL(string: serverSource) else { return aniServers } - + // DO NOT use local cache when fetching anisette servers var request = URLRequest(url: url) request.cachePolicy = .reloadIgnoringLocalCacheData - - return await withCheckedContinuation{(continuation: CheckedContinuation<[Server], Never>) in - // perform async operation with callback - URLSession.shared.dataTask(with: request) { data, response, error in - if error == nil, let data = data { - do { - let decoder = Foundation.JSONDecoder() - let servers = try decoder.decode(AnisetteServerData.self, from: data) - aniServers.append(contentsOf: servers.servers) - // store server addresses as list - UserDefaults.standard.menuAnisetteServersList = aniServers.map(\.self.address) - } catch { - // Handle decoding error - print("Failed to decode JSON: \(error)") - } - } - //unblock the continuation - continuation.resume(returning: aniServers) - }.resume() + do { + // Use async/await pattern here, avoiding CheckedContinuation directly + let (data, response) = try await URLSession.shared.data(for: request) + + // Check if the response is valid and has a 2xx HTTP status code + guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { + // Handle non-2xx status codes + let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1 + throw NSError(domain: "AnisetteViewModel: ServerError", code: statusCode, userInfo: [NSLocalizedDescriptionKey: "Request failed with status code: \(statusCode)"]) + } + + let decoder = Foundation.JSONDecoder() + let servers = try decoder.decode(AnisetteServerData.self, from: data) + print("AnisetteViewModel: JSON Decode successful for sourceURL: \(serverSource) servers: \(servers)") + aniServers.append(contentsOf: servers.servers) + // Store server addresses as list + UserDefaults.standard.menuAnisetteServersList = aniServers.map(\.address) + return aniServers + } catch { + if let urlError = error as? URLError { + print("AnisetteViewModel: URL Error: \(urlError.localizedDescription)") + } else if let decodingError = error as? DecodingError { + print("AnisetteViewModel: Failed to decode JSON: \(decodingError.localizedDescription)") + } else { + print("AnisetteViewModel: An unexpected error occurred: \(error.localizedDescription)") + } + throw error // Propagate the error } } } -struct AnisetteServers: View { +struct AnisetteServersView: View { @Environment(\.presentationMode) var presentationMode @StateObject var viewModel: AnisetteViewModel = AnisetteViewModel() @State var selected: String? = nil @State private var showingConfirmation = false var errorCallback: () -> () + var refreshCallback: (Result) -> Void var body: some View { ZStack { Color(UIColor.systemBackground) .ignoresSafeArea() .onAppear { - viewModel.getDefaultListOfServers() + viewModel.getCurrentListOfServers(refreshCallback) } VStack { if #available(iOS 16.0, *) { @@ -161,7 +175,8 @@ struct AnisetteServers: View { .shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5) .onChange(of: viewModel.source) { newValue in UserDefaults.standard.menuAnisetteList = newValue - viewModel.getDefaultListOfServers() +// viewModel.getCurrentListOfServers(refreshCallback) // don't spam + viewModel.getCurrentListOfServers() } HStack(spacing: 16) { @@ -183,7 +198,7 @@ struct AnisetteServers: View { .shadow(color: Color.accentColor.opacity(0.4), radius: 10, x: 0, y: 5) SUIButton(action: { - viewModel.getDefaultListOfServers() + viewModel.getCurrentListOfServers(refreshCallback) }) { HStack{ Spacer() diff --git a/AltStore/Settings/SettingsViewController.swift b/AltStore/Settings/SettingsViewController.swift index aa7e3e8a..ae78243a 100644 --- a/AltStore/Settings/SettingsViewController.swift +++ b/AltStore/Settings/SettingsViewController.swift @@ -209,14 +209,19 @@ final class SettingsViewController: UITableViewController { super.viewWillAppear(animated) + // show nav bar if not shown already + self.navigationController?.setNavigationBarHidden(false, animated: animated) + self.update() } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "anisetteServers" { - let controller = UIHostingController(rootView: AnisetteServers(selected: UserDefaults.standard.menuAnisetteURL, errorCallback: { - ToastView(text: "Cleared adi.pb!", detailText: "You will need to log back into Apple ID in SideStore.").show(in: self) - })) + let controller = segue.destination + + // disable bottom tab bar since 'back' button is already available +// controller.hidesBottomBarWhenPushed = true + self.show(controller, sender: nil) } else { super.prepare(for: segue, sender: sender) @@ -944,10 +949,39 @@ extension SettingsViewController self.tableView.deselectRow(at: indexPath, animated: true) case .anisetteServers: - self.prepare(for: UIStoryboardSegue(identifier: "anisetteServers", source: self, destination: UIHostingController(rootView: AnisetteServers(selected: "", errorCallback: { - ToastView(text: "Reset adi.pb", detailText: "Buh").show(in: self) - }))), sender: nil) -// self.performSegue(withIdentifier: "anisetteServers", sender: nil) + + func handleRefreshResult(_ result: Result) { + var message = "Servers list refreshed" + var details: String? = nil + var duration: TimeInterval = 1.0 + + switch result { + case .success: + // No additional action needed, default message is sufficient + break + case .failure(let error): + message = "Failed to refresh servers list" + details = String(describing: error) + duration = 4.0 + } + + let toast = ToastView(text: message, detailText: details) + toast.preferredDuration = duration + toast.show(in: self) + } + + // Instantiate SwiftUI View inside UIHostingController + let anisetteServersView = AnisetteServersView(selected: UserDefaults.standard.menuAnisetteURL, errorCallback: { + ToastView(text: "Cleared adi.pb!", detailText: "You will need to log back into Apple ID in SideStore.") + .show(in: self) + }, refreshCallback: {result in + handleRefreshResult(result) + }) + + let anisetteServersController = UIHostingController(rootView: anisetteServersView) + + self.prepare(for: UIStoryboardSegue(identifier: "anisetteServers", source: self, destination: anisetteServersController), sender: nil) + case .advancedSettings: // Create the URL that deep links to your app's custom settings. if let url = URL(string: UIApplication.openSettingsURLString) {