I'm new to swiftui and I'm trying to call a method and pass a callback object:
Callback protocol
protocol AsyncJobCallback {
func onLoadingStarted()
func onDone<T: Decodable>(_ response: T)
func onError(_ error: Error?)
}
Main view
struct MainView: View {
var body: some View {
if AppPreferences.shared.firstStart{
var callback = AsyncJobCallback()
// ....
ApiClient.shared.requestAccessToken(callback: callback)
// request api access token
// - onLoadingStarted: show loading view
// - onDone: set firstStart to false, show MainTabsView
// - onError: show error message
} else {
MainTabsView()
}
}
}
Job class
class GuestSignUpAsyncJob {
let callback: AsyncJobCallback
init(callback: AsyncJobCallback) {
self.callback = callback
}
func signUpAsGuest() {
DispatchQueue.main.async {
self.callback.onLoadingStarted()
}
var body = ApiUserGuestSignUp(
name: "Guest"
)
let endpoint = ApiPath.GUEST_REGISTRATION
ApiClient.shared.postRequest(endpoint: endpoint, body: body) { data, response, error in
if let error = error {
DispatchQueue.main.async {
self.callback.onError(error)
}
return
}
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 201 else {
DispatchQueue.main.async {
// TODO create exception
self.callback.onError(nil)
}
return
}
if let data = data {
do {
let token = try JSONDecoder().decode(ApiAccessToken.self, from: data)
DispatchQueue.main.async {
self.callback.onDone(token)
}
} catch {
DispatchQueue.main.async {
self.callback.onError(error)
}
}
} else {
DispatchQueue.main.async {
// TODO create exception
self.callback.onError(nil)
}
}
}
}
}
what is the proper way to pass a callback to a method an change ui depends on callback return?
I'm new to swiftui and I'm trying to call a method and pass a callback object:
Callback protocol
protocol AsyncJobCallback {
func onLoadingStarted()
func onDone<T: Decodable>(_ response: T)
func onError(_ error: Error?)
}
Main view
struct MainView: View {
var body: some View {
if AppPreferences.shared.firstStart{
var callback = AsyncJobCallback()
// ....
ApiClient.shared.requestAccessToken(callback: callback)
// request api access token
// - onLoadingStarted: show loading view
// - onDone: set firstStart to false, show MainTabsView
// - onError: show error message
} else {
MainTabsView()
}
}
}
Job class
class GuestSignUpAsyncJob {
let callback: AsyncJobCallback
init(callback: AsyncJobCallback) {
self.callback = callback
}
func signUpAsGuest() {
DispatchQueue.main.async {
self.callback.onLoadingStarted()
}
var body = ApiUserGuestSignUp(
name: "Guest"
)
let endpoint = ApiPath.GUEST_REGISTRATION
ApiClient.shared.postRequest(endpoint: endpoint, body: body) { data, response, error in
if let error = error {
DispatchQueue.main.async {
self.callback.onError(error)
}
return
}
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 201 else {
DispatchQueue.main.async {
// TODO create exception
self.callback.onError(nil)
}
return
}
if let data = data {
do {
let token = try JSONDecoder().decode(ApiAccessToken.self, from: data)
DispatchQueue.main.async {
self.callback.onDone(token)
}
} catch {
DispatchQueue.main.async {
self.callback.onError(error)
}
}
} else {
DispatchQueue.main.async {
// TODO create exception
self.callback.onError(nil)
}
}
}
}
}
what is the proper way to pass a callback to a method an change ui depends on callback return?
Share Improve this question asked Mar 20 at 16:21 devopsdevops 9,2036 gold badges50 silver badges77 bronze badges1 Answer
Reset to default 1- You should use
@AppStorage
instead ofAppPreferences.shared.firstStart
. SwiftUI cannot observe changes in the latter (unlessAppPreferences
is@Observable
andfirstStart
is a stored property). - You must show something in the beginning. You cannot wait until
onLoadingStarted
to display anything. That effectively makes the protocol:
@MainActor
protocol AsyncJobCallback {
func onDone<T: Decodable>(_ response: T)
func onError(_ error: (any Error)?)
}
It's unusual that onDone
can receive any Decodable
type. You probably intended to do this instead:
@MainActor
protocol AsyncJobCallback<Response> {
associatedtype Response: Decodable
func onDone(_ response: Response)
func onError(_ error: (any Error)?)
}
And GuestSignUpAsyncJob
would store a AsyncJobCallback<ApiAccessToken>
.
In any case, your view can then implement the callback methods like this:
struct ContentView: View, AsyncJobCallback {
@AppStorage("firstStart") var firstStart = true
@State var error: (any Error)?
var body: some View {
if firstStart {
ProgressView()
.onAppear {
ApiClient.shared.requestAccessToken(callback: self)
}
} else if let error {
// display the error in some way...
Text(verbatim: "\(error)")
} else {
MainTabsView()
}
}
// func onDone(_ response: ApiAccessToken)
func onDone<T>(_ response: T) where T : Decodable {
firstStart = false
}
func onError(_ error: (any Error)?) {
self.error = error
}
}
Finally, instead of using callbacks like this, I strongly suggest that you embrace Swift Concurrency, and rewrite requestAccessToken
to have a signature like this:
func requestAccessToken() async throws -> ApiAccessToken
Then you can write:
ProgressView()
.task {
do {
_ = try await requestAccessToken()
firstStart = false
} catch {
self.error = error
}
}