I understand how to "fetch" data from a JSON API (my local server, in fact), but how should I think about the pipeline from merely having the data to displaying it in views? What I intuitively want to do is "return" the data from the fetching function, though I know that's not the paradigm that the Swift URL functions operate with. My thought is that if I can "return" the data (as a struct) it will be easy to pass into a view for visualization.
Sample Code:
This is the structure of the fetched JSON and the kind of variable I want to pass into views.
struct User: Codable {
let userID: String
let userName: String
let firstName: String
let lastName: String
let fullName: String
}
My hope is that the printUser function can return instead of print a successful fetch.
func printUser() {
fetchUser { (res) in
switch res {
case .success(let user):
print(user.userName) // return here?
// I know it won't work, but that's what seems natural
case .failure(let err):
print("Failed to fetch user: ", err)
}
}
}
func fetchUser(completion: @escaping (Result<User, Error>) -> ()) {
let urlString = "http://localhost:4001/user"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, resp, err) in
if let err = err {
completion(.failure(err))
return
}
do {
let user = try JSONDecoder().decode(User.self, from: data!)
completion(.success(user))
} catch let jsonError {
completion(.failure(jsonError))
}
}.resume()
}
Sample view that would take a user struct
struct DisplayUser: View {
var user: User
var body: some View {
Text(user.userID)
Text(user.userName)
Text(user.lastName)
Text(user.firstName)
Text(user.fullName)
}
}
CodePudding user response:
The reason that you can't just "return" is that your fetchUser
is asynchronous. That means that it might return relatively instantaneously or it may take a long time (or not finish at all). So, your program needs to be prepared to deal with that eventuality. Sure, it would be "be easy to pass into a view for visualization" as you put it, but unfortunately, it just doesn't fit the reality of the situation.
What you can do (in your example) is set the User
as an Optional -- that way, if it hasn't been set, you can display some sort of loading view and if it has been set (ie your async function has returned a value), you can display it. That would look something like this:
class ViewModel : ObservableObject {
@Published var user : User? //could optionally store the entire Result here
func runFetch() {
fetchUser { (res) in
switch res {
case .success(let user):
DispatchQueue.main.async {
self.user = user
}
case .failure(let err):
print("Failed to fetch user: ", err)
//set an error message here? Another @Published variable?
}
}
}
func fetchUser(completion: @escaping (Result<User, Error>) -> ()) {
//...
}
}
struct DisplayUser: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
if let user = viewModel.user {
Text(user.userID)
Text(user.userName)
Text(user.lastName)
Text(user.firstName)
Text(user.fullName)
} else {
Text("Loading...")
}
}.onAppear {
viewModel.fetchUser()
}
}
}
Note: I'd probably refactor the async stuff to use Combine if this were my program, but it's a personal preference issue