How do I refactor the code for making the API call using async await in swift - swift

I want to refactor the API call that is made using async and await but I am getting the error as the publishing needs to be done on the main thread.
The below is the code that I wrote in the file named LogIn View:-
#State private var quotes = [Quote]()
var body: some View {
NavigationView {
List(quotes, id:\.quote_id) { quote in
VStack(alignment: .leading) {
Text(quote.author)
.font(.headline)
Text(quote.quote)
.font(.body)
}
}
.padding()
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Log out") {
authentication.updateValidation(success: false)
}
}
}
.navigationTitle("Dashboard Screen ")
}
.task {
await fetchData()
}
}
func fetchData() async {
//create url
guard let url = URL(string: "https://breakingbadapi.com/api/quotes") else {
print("URL does not work")
return
}
//fetch data from url
do {
let (data, _) = try await URLSession.shared.data(from: url)
//decode that data
if let decodeResponse = try? JSONDecoder().decode([Quote].self, from: data) {
quotes = decodeResponse
}
} catch {
print("Data not valid")
}
}
I want to write the function fetchData() in a separate file and use it here in LogIn View
but upon trying to do so I am getting the error mentioned above. Can anyone Please help me with this.
PS:- all the variables are defined inside another file named variables. The code for that is as follows:-
import Foundation
struct Quote: Codable {
var quote_id: Int
var quote: String
var author: String
var series: String
}

A nice place is in an extension of NSURLSession, e.g.
extension NSURLSession {
func fetchQuotes() async throws -> [Quote] {
//create url
guard let url = URL(string: "https://breakingbadapi.com/api/quotes") else {
print("URL does not work")
return
}
//fetch data from url
let (data, _) = try await data(from: url)
//decode that data
return try JSONDecoder().decode([Quote].self, from: data)
}
}
Then you can simply do:
.task {
do {
quotes = try await URLSession.shared.fetchQuotes()
} catch {
errorMessage = error.description
}
}
This has the advantage you can use it with a different kind of URLSession, e.g. for API requests we usually use an ephemeral session. Another good place would be a static async func in the Quote struct.

to put your func fetchData() in a model and avoid the error, try this approach:
class QuotesModel: ObservableObject {
#Published var quotes = [Quote]()
#MainActor // <-- here
func fetchData() async {
guard let url = URL(string: "https://breakingbadapi.com/api/quotes") else {
print("Invalid URL")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
quotes = try JSONDecoder().decode([Quote].self, from: data)
} catch {
print(error)
}
}
}
struct ContentView: View {
#StateObject var model = QuotesModel()
var body: some View {
NavigationView {
List(model.quotes, id: \.quote_id) { quote in
VStack(alignment: .leading) {
Text(quote.author)
.font(.headline)
Text(quote.quote)
.font(.body)
}
}
.padding()
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Log out") {
// authentication.updateValidation(success: false)
}
}
}
.navigationTitle("Dashboard Screen ")
}
.task {
await model.fetchData()
}
}
}
struct Quote: Codable {
var quote_id: Int
var quote: String
var author: String
var series: String
}

Make #MainActor method with #Published in ObservableObject class.
and for model use codable.
struct QuoteView: View {
#State var quotes: [Quote] = []
#ObservedObject var quoteStore = QuoteStore()
var body: some View {
NavigationView {
List(quotes, id:\.quote_id) { quote in
VStack(alignment: .leading) {
Text(quote.author)
.font(.headline)
Text(quote.quote)
.font(.body)
}
}
.navigationTitle("Quotes")
}
.task {
quotes = try! await quoteStore.fetchData()
}
}
}
struct Quote: Codable {
let quote_id = UUID()
let quote: String
let author: String
}
class QuoteStore: ObservableObject {
#Published var quotes: [Quote] = []
#MainActor
func fetchData() async throws -> [Quote] {
guard var url = URL(string: "https://breakingbadapi.com/api/quotes") else { throw AppError.invalidURL }
let (data, _) = try await URLSession.shared.data(from: url)
let repos = try JSONDecoder().decode([Quote].self, from: data)
return repos
}
}
enum AppError: Error {
case invalidURL
}

depending on your code it may just be as simple as putting the code giving you the error inside await MainActor.run { ... }
That said, as a general rule async code is easier to manage when it returns values to use, rather than setting variables from inside functions.
struct SomeFetcher {
func fetchData() async -> [Quotes] {
...
if let decodeResponse = try? JSONDecoder().decode([Quote].self, from: data) {
return decodeResponse
}
...
}
}
struct TheView: View {
var dataGetter = SomeFetcher()
#State private var quotes = [Quote]()
var body: some View {
NavigationView {
}
.task {
quotes = await dataGetter.fetchData()
}
}
}
Editied: Sorry I wrote this code while sleepy. It does not need to be observed in this case when the view is calling the fetch. Other answers have pointed out that the downloaded info is frequently managed by the class that downloads it and published by it.

Related

SwiftUI how come this is happening?

struct LoginView: View {
#ObservedObject var vm : ViewModel
#State private var username = ""
private var searchAllowed : Bool{
if(username.count>2)
{
return false
}
return true
}
var body: some View {
NavigationView{
VStack{
Text("Enter your Username:")
.font(.title.bold())
TextField("Username", text: $username)
.frame(width: 280)
.padding(.bottom)
NavigationLink{
if (vm.apiLoaded)
{
ProfileView(vm: vm)
}
else{
ProgressView()
.task {
await vm.userToUUID(username: username)
await vm.fetchPlayerData()
}
}
} label:
{
ZStack{
RoundedRectangle(cornerRadius: 5)
.fill(.gray)
.frame(width: 200, height: 50)
Text("Search")
.foregroundColor(.white)
}
}
.disabled(searchAllowed)
}
}
}
}
So I have these two Views with two async methods getting called in the else clause in the NavigationLink, but my problem is that I'm not getting the expected result and I'm assuming that's because both async functions start running at the same time.
The fetchPlayerData one uses UUID to get the PlayerData, is there some way I can make it get the UUID first and only then fetchPlayerData?
ViewModel:
import Foundation
#MainActor final class ViewModel : ObservableObject{
#Published var apiLoaded : Bool = false
#Published var player : Player? //player is kinda our model
func userToUUID(username : String) async {
let playersURLString = "https://api.mojang.com/users/profiles/minecraft//(username)"
if let url = URL(string: playersURLString){
do{
let (data, ) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
let uuid = try decoder.decode(UUIDConversion.self, from: data).id
print(uuid)
player?.uuid = uuid
} catch{
print("bad")
}
}
}
func fetchPlayerData() async{
let playersURLString = "https://api.hypixel.net/player?key=key&uuid=81f5de24b017466aaab4551a3fb38e5c"
if let url = URL(string: playersURLString){
do {
let (data, ) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
player = try decoder.decode(ApiResponse.self, from: data).player
print(player)
apiLoaded = true
} catch {
print("Error")
}
}
}
}
Edit: If I add a UUID property to my ViewModel and then set that in the UUID function if I refer to player.uuid then it works for some reason
#Published var uuid : String = ""
#Published var player : Player? //player is kinda our model
func userToUUID(username : String) async {
let playersURLString = "https://api.mojang.com/users/profiles/minecraft//(username)"
if let url = URL(string: playersURLString){
do{
let (data, _) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
let uuid = try decoder.decode(UUIDConversion.self, from: data).id
print(uuid)
player?.uuid = uuid
self.uuid = uuid
} catch{
print("bad")
}
}
}
But there's something really weird here, in my profileview:
struct ProfileView: View {
#ObservedObject var vm : ViewModel
var body: some View {
List{
VStack{
Text(vm.player?.displayname ?? "T")
.padding(.vertical, 0)
AsyncImage(url: URL(string: "https://crafatar.com/renders/body//(vm.uuid)%22)) { image in
image
.center()
} placeholder: {
ProgressView()
}
}
vm.player?.displayname works and gives me the value but for some reason vm.player?.uuid doesn't work and only vm.uuid works..

How to call API again after change was made?

So I want to search books from google books api, but only through url query, how can I call API again when I enter the text in the search bar? How to reload the call?
I tried also with textfield onSumbit method, but nothing work.
I just want to insert value of textSearch to network.searchText and that network.searchText to insert into q=
here is my code of ContentView:
//
// ContentView.swift
// BookApi
//
// Created by Luka Šalipur on 7.6.22..
//
import SwiftUI
struct URLImage: View{
var urlString: String
#State var data: Data?
var body: some View{
if let data = data, let uiimage = UIImage(data:data) {
Image(uiImage: uiimage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:80, height:120)
.background(Color.gray)
} else {
Image(systemName: "book").onAppear {
fetch()
}
}
}
private func fetch(){
guard let url = URL(string: urlString) else {
return
}
let task = URLSession.shared.dataTask(with:url) { data, _, error in
self.data = data
}
task.resume()
}
}
// ContentView
struct ContentView: View {
#ObservedObject var network = Network()
#State var textSearch:String = "knjiga"
#State private var shouldReload: Bool = false
func context(){
network.searchText = self.textSearch
print(network.searchText)
}
var body: some View {
NavigationView{
List{
ForEach(network.book, id:\.self){ item in
NavigationLink{
Webview(url: URL(string: "\(item.volumeInfo.previewLink)")!)
} label: {
HStack{
URLImage(urlString: item.volumeInfo.imageLinks.thumbnail)
Text("\(item.volumeInfo.title)")
}
}
}
}
.onAppear{
context()
}
.onChange(of: textSearch, perform: { value in
self.shouldReload.toggle()
})
.searchable(text: $textSearch)
.navigationTitle("Books")
.task{
await network.loadData()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And here is my API(network) call:
//
// Network.swift
// BookApi
//
// Created by Luka Šalipur on 7.6.22..
//
import Foundation
import SwiftUI
class Network: ObservableObject{
#Published var book = [Items]()
var searchText: String = "watermelon" {
willSet(newValue) {
print(newValue)
}
}
func loadData() async {
guard let url = URL(string: "https://www.googleapis.com/books/v1/volumes?q=\(searchText)&key=API_KEY_PRIVATE") else {
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let decodedResponse = try? JSONDecoder().decode(Books.self, from: data) {
book = decodedResponse.items
}
} catch {
print("There is an error")
}
}
}
This is a perfect candidate for the Combine framework.
In Network create a publisher which removes duplicates, debounces the input for 0.3 seconds, builds the URL, loads the data and decodes it.
I don't have your types, probably there are many errors. But this is a quite efficient way for dynamic searching. By the way your naming with regard to singular and plural form is pretty confusing.
import Combine
import SwiftUI
class Network: ObservableObject {
#Published var book = [Items]()
#Published var query = ""
private var subscriptions = Set<AnyCancellable>()
init() {
searchPublisher
.sink { completion in
print(completion) // show the error to the user
} receiveValue: { [weak.self] books in
self?.book = books.items
}
.store(in: &subscriptions)
}
var searchPublisher : AnyPublisher<Books,Error> {
return $query
.removeDuplicates()
.debounce(for: 0.3, scheduler: RunLoop.main)
.compactMap{ query -> URL? in
guard !query.isEmpty else { return nil }
guard let url = URL(string: "https://www.googleapis.com/books/v1/volumes?q=\(query)&key=API_KEY_PRIVATE") else {
return nil
}
return url
}
.flatMap { url -> AnyPublisher<Data, URLError> in
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.eraseToAnyPublisher()
}
.decode(type: Books.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
In the view create the view model (must be #StateObject!)
#StateObject var network = Network()
and bind searchable to query in network
.searchable(text: $network.query)
The view is updated when the data is available in network.book
The .task modifier ist not needed
There is another version of task that runs again when a value changes task(id:priority:_:). If a task is still running when the param changes it will be cancelled and restarted automatically. In your case use it as follows:
.task(id: textSearch) { newValue in
books = await getBooks(newValue)
}
Now we have async/await and task there is no need for an ObservableObject anymore.

Why is my function returning an empty array?

I am trying to call the results of this function in my SwiftUI view:
class GetMessages: ObservableObject {
let BASE_URL = "apicallurl.com"
#Published var messages = [Timestamp]()
func fetchMessages() {
guard let url = URL(string: BASE_URL) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard error == nil else {print(error!.localizedDescription); return }
let theData = try! JSONDecoder().decode([String: Timestamp].self, from: data!)
DispatchQueue.main.async {
self.messages = Array(theData.values)
}
}
.resume()
}
}
I am testing the output with a print statement in the onAppear:
struct HomeTab: View {
#StateObject var getMsgs = GetMessages()
var body: some View {
NavigationView {
VStack(spacing: 0) {
greeting.edgesIgnoringSafeArea(.top)
messages
Spacer()
}
.onAppear {
print(getMsgs.fetchMessages())
print(getMsgs.messages)
}
}.navigationBarHidden(true)
}
both print statements print () or []
But when i print print(self.messages) in my GetMessages class the data prints fine.
Why is it empty in my Hometab view?
When you use getMsgs.fetchMessages() it may take some times to fetch the results. Once the results are available
the messages of getMsgs in HomeTab will be updated, and this will trigger a view refresh,
because it is a #StateObject and is "monitored" by the view.
However you should not try to print(getMsgs.messages) before the results are available.
So try the following sample code:
struct HomeTab: View {
#StateObject var getMsgs = GetMessages()
var body: some View {
NavigationView {
List {
ForEach(getMsgs.messages, id: \.self) { msg in
Text("\(msg)")
}
}
.onAppear {
getMsgs.fetchMessages()
// no printing of getMsgs.messages here
}
}.navigationBarHidden(true)
}
}

Swift load user data for dashboard after login

I am trying to retrieve user data once the user gets to the dashboard of my app
I have essentially this to get data:
class UserController: ObservableObject {
#Published var firstName: String = ""
func fetchUser(token: String) {
/* Do url settings */
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else { return }
let rData = try! JSONDecoder().decode(User.self, from: data)
let userData = [
"id": rData.id,
"firstName": rData.firstName,
"lastName": rData.lastName,
"department": rData.department,
]
UserDefaults.standard.set(userData, forKey: "user")
DispatchQueue.main.async {
self.firstName = rData.firstName
}
}.resume()
}
}
And then my view looks like this
struct HomeViewCollection: View {
#Binding var isAuthenticated: Bool
#ObservedObject var userController: UserController = UserController()
var body: some View {
VStack {
Text("Hello \(userController.firstName)!")
}
}
}
I'm just not sure how can I activate fetchUser from the View.
I have tried this in the controller
init() {
guard let tokenData = KeyChain.load(key: "token") else { return }
var token = String(data: tokenData, encoding: .utf8)
if(token != nil) {
print("Token: \(token)")
fetchUser(token: token!)
}
}
That didn't work, and then I tried userController.fetchUser(token: KeyChainTokenHere) and that didn't work because it doesn't conform to the struct.
Try passing the token to HomeViewCollection and initiating the call in onAppear completion block.
struct HomeViewCollection: View {
var token: String
#Binding var isAuthenticated: Bool
#ObservedObject var userController = UserController()
var body: some View {
VStack {
Text("Hello \(userController.firstName)!")
}
.onAppear {
self.userController.fetchUser(token: self.token)
}
}
}
Also, make sure the firstName property is getting set.
#Published var firstName: String = "" {
didSet {
print("firstName is set as \(firstName)")
}
}

SwiftUI - Not able to render the jsonData

I am very new to SwiftUi and I am trying to view the json data and I am currently working on retreiving the weather data from the openweathermap.org which is a free api to retrieve current weather. I am getting Error parsing Weather Json message. I am not sure what I am doing wrong!! Any help would be greatly appreciated and I have been stuck on this for a day. I referred many blogs and tutorials on how to use the Published var and ObservableObject I am not able to fix the problem.
This is my swift file
struct WeatherData {
public var Id: Int
public var main: String
public var weather: [Weather]
public var icon: String
}
extension WeatherData: Decodable, Identifiable {
var id: Int {return Id}
}
struct WeatherView: View {
#ObservedObject var fetch = FetchWeather()
var body: some View {
VStack {
List(fetch.weatherData) {
wthr in
VStack(alignment: .leading){
Text("\(wthr.id)")
Text("\(wthr.weather[0].description)")
Text("\(wthr.icon)")
.font(.system(size:11))
.foregroundColor(Color.gray)
}
}
}
}
}
struct Weather: Decodable {
let description: String
}
struct WeatherView_Previews: PreviewProvider {
static var previews: some View {
WeatherView()
}
}
class FetchWeather: ObservableObject {
#Published var weatherData = [WeatherData] ()
init() {
load()
}
func load() {
let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=London&appid=myapikey")!
URLSession.shared.dataTask(with: url) {
(data, response, error) in
do {
if let wthData = data {
let decodedData = try JSONDecoder().decode([WeatherData].self, from: wthData)
DispatchQueue.main.sync {
self.weatherData = decodedData
}
}
else {
print("No json Data available")
}
}catch {
print("Error parsing Weather Json")
}
}.resume()
}
}
Try this code. I have signed up to get the api and corrected the Model, ViewModel and View accordingly. I have not added the image loader for icon strings.
import SwiftUI
struct Weather: Decodable{
var description: String
var icon :String
}
struct MainData: Decodable {
var temp: Double
var pressure: Int
var humidity: Int
var temp_min: Double
var temp_max: Double
}
struct WeatherData: Decodable, Identifiable {
var id: Int
var main: MainData
var weather: [Weather]
var name: String
}
struct WeatherView: View {
#ObservedObject var fetch = FetchWeather()
var body: some View {
VStack(alignment: .leading) {
Text("Current Weather").font(.title).padding()
List(fetch.weatherData) { wthr in
HStack {
VStack(alignment: .leading){
Text("\(wthr.name)")
Text("\(wthr.weather[0].description)")
.font(.system(size:11))
.foregroundColor(Color.gray)
}
Spacer()
VStack(alignment: .trailing){
Text("\(wthr.main.temp-273.15, specifier: "%.1f") ºC")
}
Text("\(wthr.weather[0].icon)") // Image from "https://openweathermap.org/img/w/\(wthr.weather[0].icon).png"
.foregroundColor(Color.gray)
}
}
}
}
}
class FetchWeather: ObservableObject {
#Published var weatherData = [WeatherData]()
private let baseURL = "https://api.openweathermap.org/data/2.5/weather?q="
private let cities = [ "London", "Mumbai", "New+york", "Vatican+City" ]
private let api = "&appid="+"e44ebeb18c332fff46ab956bb38f9e07"
init() {
for city in self.cities {
self.load(self.baseURL+city+self.api)
}
}
func load(_ urlString: String) {
if let url = URL(string: urlString) {
URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
if let wthData = data {
let decodedData = try JSONDecoder().decode(WeatherData.self, from: wthData)
DispatchQueue.main.sync {
self.weatherData.append(decodedData)
}
}
else {
print("No json Data available")
}
} catch let error as NSError{
print(error.localizedDescription)
}
}.resume()
} else {
print("Unable to decode URL")
}
}
}