[Anisette-Servers]: cleanup and enhanced error handling for anisette-servers-list

This commit is contained in:
Magesh K
2024-12-15 23:00:12 +05:30
parent e39b9fe309
commit 93ca83528b
3 changed files with 95 additions and 42 deletions

View File

@@ -34,9 +34,13 @@ final class AuthenticationViewController: UIViewController
// fetch anisette servers asap when loading Auth Screen (if list is empty // fetch anisette servers asap when loading Auth Screen (if list is empty
if(UserDefaults.standard.menuAnisetteServersList.isEmpty){ if(UserDefaults.standard.menuAnisetteServersList.isEmpty){
Task{ Task{
await AnisetteViewModel.getListOfServers( let sourceURL = UserDefaults.standard.menuAnisetteList
serverSource: 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)")
}
} }
} }

View File

@@ -36,65 +36,79 @@ class AnisetteViewModel: ObservableObject {
} }
} }
func getDefaultListOfServers() { @MainActor
// initiate fetch but do not wait in blocking manner func getCurrentListOfServers(_ completionHandler: @escaping (Result<Void, Error>) -> Void = {_ in }) {
Task{ // dispatch fetch operation but don't do a blocking wait for results
let anisetteServers = await AnisetteViewModel.getListOfServers(serverSource: self.source) Task {
do {
// always update on main thread for Observables let anisetteServers = try await AnisetteViewModel.getListOfServers(serverSource: self.source)
DispatchQueue.main.async { // Update UI-related state on the main thread
self.servers = anisetteServers 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] = [] var aniServers: [Server] = []
guard let url = URL(string: serverSource) else { guard let url = URL(string: serverSource) else {
return aniServers return aniServers
} }
// DO NOT use local cache when fetching anisette servers // DO NOT use local cache when fetching anisette servers
var request = URLRequest(url: url) var request = URLRequest(url: url)
request.cachePolicy = .reloadIgnoringLocalCacheData 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 do {
continuation.resume(returning: aniServers) // Use async/await pattern here, avoiding CheckedContinuation directly
}.resume() 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 @Environment(\.presentationMode) var presentationMode
@StateObject var viewModel: AnisetteViewModel = AnisetteViewModel() @StateObject var viewModel: AnisetteViewModel = AnisetteViewModel()
@State var selected: String? = nil @State var selected: String? = nil
@State private var showingConfirmation = false @State private var showingConfirmation = false
var errorCallback: () -> () var errorCallback: () -> ()
var refreshCallback: (Result<Void, any Error>) -> Void
var body: some View { var body: some View {
ZStack { ZStack {
Color(UIColor.systemBackground) Color(UIColor.systemBackground)
.ignoresSafeArea() .ignoresSafeArea()
.onAppear { .onAppear {
viewModel.getDefaultListOfServers() viewModel.getCurrentListOfServers(refreshCallback)
} }
VStack { VStack {
if #available(iOS 16.0, *) { 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) .shadow(color: Color.black.opacity(0.2), radius: 5, x: 0, y: 5)
.onChange(of: viewModel.source) { newValue in .onChange(of: viewModel.source) { newValue in
UserDefaults.standard.menuAnisetteList = newValue UserDefaults.standard.menuAnisetteList = newValue
viewModel.getDefaultListOfServers() // viewModel.getCurrentListOfServers(refreshCallback) // don't spam
viewModel.getCurrentListOfServers()
} }
HStack(spacing: 16) { HStack(spacing: 16) {
@@ -183,7 +198,7 @@ struct AnisetteServers: View {
.shadow(color: Color.accentColor.opacity(0.4), radius: 10, x: 0, y: 5) .shadow(color: Color.accentColor.opacity(0.4), radius: 10, x: 0, y: 5)
SUIButton(action: { SUIButton(action: {
viewModel.getDefaultListOfServers() viewModel.getCurrentListOfServers(refreshCallback)
}) { }) {
HStack{ HStack{
Spacer() Spacer()

View File

@@ -209,14 +209,19 @@ final class SettingsViewController: UITableViewController
{ {
super.viewWillAppear(animated) super.viewWillAppear(animated)
// show nav bar if not shown already
self.navigationController?.setNavigationBarHidden(false, animated: animated)
self.update() self.update()
} }
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "anisetteServers" { if segue.identifier == "anisetteServers" {
let controller = UIHostingController(rootView: AnisetteServers(selected: UserDefaults.standard.menuAnisetteURL, errorCallback: { let controller = segue.destination
ToastView(text: "Cleared adi.pb!", detailText: "You will need to log back into Apple ID in SideStore.").show(in: self)
})) // disable bottom tab bar since 'back' button is already available
// controller.hidesBottomBarWhenPushed = true
self.show(controller, sender: nil) self.show(controller, sender: nil)
} else { } else {
super.prepare(for: segue, sender: sender) super.prepare(for: segue, sender: sender)
@@ -944,10 +949,39 @@ extension SettingsViewController
self.tableView.deselectRow(at: indexPath, animated: true) self.tableView.deselectRow(at: indexPath, animated: true)
case .anisetteServers: 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) func handleRefreshResult(_ result: Result<Void, any Error>) {
}))), sender: nil) var message = "Servers list refreshed"
// self.performSegue(withIdentifier: "anisetteServers", sender: nil) 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: case .advancedSettings:
// Create the URL that deep links to your app's custom settings. // Create the URL that deep links to your app's custom settings.
if let url = URL(string: UIApplication.openSettingsURLString) { if let url = URL(string: UIApplication.openSettingsURLString) {