How to use downloaded URL from getData class in AsyncImage?
struct RecentItemsView: View {
var item: dataType // var's from getData class
var body: some View {
HStack(spacing: 15) {
AsyncImage(url: URL(string: item.pic), content: { image in // item.pic here
image.resizable()
}, placeholder: {
ProgressView()
})
I have full URL from downloadURL but when Im using item.pic parameter in AsyncImage I get error: (See Image)
I understand that the error contains the path to the image, which is not suitable for AsyncImage, that's why I downloaded the full URL, the question is how to use the received URL in AsyncImage?
class getData : ObservableObject {
#Published var datas = [dataType]()
init() {
let db = Firestore.firestore()
db.collection("items").getDocuments { (snap, err) in
if err != nil {
print((err?.localizedDescription)!)
return
}
for i in snap!.documents {
let id = i.documentID
let title = i.get("title") as! String
let description = i.get("description") as! String
let pic = i.get("pic") as! String
self.datas.append(dataType(id: id, title: title, description: description, pic: pic))
let storage = Storage.storage()
let storageRef = storage.reference().child("\(pic)")
storageRef.downloadURL { url, error in
if let error = error {
print("Failed to download url:", error)
return
} else {
print(url!) // Full Url- https://firebasestorage.googleapis.com:...
}
}
}
}
}
}
struct dataType : Identifiable {
var id = UUID().uuidString
var title : String
var description : String
var pic : String
}
Error:
Storage:
Firestore:
This is going to look quite a bit different from your current approach but give it a try, it will simplify your code overall.
Main differences are the use of async await and FirebaseFirestoreSwift.
I choose using async await/Concurrency because it provides a more linear approach to the code and I think resolves your issue about sharing the variable with all the objects.
This is what your ObservableObject will look like
//Keeps UI Updates on the main thread
#MainActor
//Classes and structs should always be uppercased
class GetData : ObservableObject {
#Published var datas = [DataType]()
private var task: Task<Void, Never>? = nil
init() {
task = Task{
do{
try await getData()
}catch{
//Ideally you should present this error
//to the users so they know that something has gone wrong
print(error)
}
}
}
deinit{
task?.cancel()
}
func getData() async throws {
let documentPath = "items"
let svc = FirebaseService()
//async await allows a more linear approach. You can get the images individually
var items : [DataType] = try await svc.retrieve(path: documentPath)
for (idx, item) in items.enumerated() {
//Check if your url is a full url
if !item.pic.localizedCaseInsensitiveContains("https"){
//If it isnt a full url get it from storage and replace the url
items[idx].pic = try await svc.getImageURL(imagePath: item.pic).absoluteString
//Optional update the object so you dont have to retrieve the
//The url each time.
try svc.update(path: documentPath, object: items[idx])
}
}
datas = items
}
}
and your struct should change to use #DocumentID.
//This is a much simpler solution to decoding
struct DataType : Identifiable, FirestoreProtocol {
#DocumentID var id : String?
//If you get decoding errors make these variables optional by adding a ?
var title : String
var description : String
var pic : String
}
Your Views can now be modified to use the updated variables.
#available(iOS 15.0, *)
public struct DataTypeListView: View{
#StateObject var vm: GetData = .init()
public init(){}
public var body: some View{
List(vm.datas){ data in
DataTypeView(data: data)
}
}
}
#available(iOS 15.0, *)
struct DataTypeView: View{
let data: DataType
var body: some View{
HStack{
Text(data.title)
AsyncImage(url: URL(string: data.pic), content: { phase in
switch phase{
case .success(let image):
image
.resizable()
.scaledToFit()
.frame(width: 50, height: 50)
case .failure(let error):
Image(systemName: "rectangle.fill")
.onAppear(){
print(error)
}
case .empty:
Image(systemName: "rectangle.fill")
#unknown default:
Image(systemName: "rectangle.fill")
}
})
}
}
}
The class GetData is pretty bare bones an uses the code below to actually make the calls, I like using generics to simplify code and so it can be reused by various places.
You don't have to completely understand what is going on with this now but you should, I've put a ton of comments so it should be easy.
import FirebaseStorage
import FirebaseFirestore
import FirebaseFirestoreSwift
import SwiftUI
import FirebaseAuth
struct FirebaseService{
private let storage: Storage = .storage()
private let db: Firestore = .firestore()
///Retrieves the storage URL for an image path
func getImageURL(imagePath: String?) async throws -> URL{
guard let imagePath = imagePath else {
throw AppError.unknown("Invalid Image Path")
}
typealias PostContinuation = CheckedContinuation<URL, Error>
//Converts an completion handler approach to async await/concurrency
return try await withCheckedThrowingContinuation { (continuation: PostContinuation) in
storage.reference().child(imagePath).downloadURL { url, error in
if let error = error {
continuation.resume(throwing: error)
} else if let url = url {
continuation.resume(returning: url)
} else {
continuation.resume(throwing: AppError.unknown("Error getting image url"))
}
}
}
}
///Retireves the documetns from the Firestore and returns an array of objects
func retrieve<FC>(path: String) async throws -> [FC] where FC : FirestoreProtocol{
let snapshot = try await db.collection(path).getDocuments()
return snapshot.documents.compactMap { doc in
do{
return try doc.data(as: FC.self)
}catch{
//If you get any decoding errors adjust your struct, you will
//likely need optionals
print(error)
return nil
}
}
}
///Updates the provided document into the provided path
public func update<FC : FirestoreProtocol>(path: String, object: FC) throws{
guard let id = object.id else{
throw AppError.needValidId
}
try db.collection(path).document(id).setData(from: object)
}
}
enum AppError: LocalizedError{
case unknown(String)
case needValidId
}
protocol FirestoreProtocol: Identifiable, Codable{
///Use #DocumentID from FirestoreSwift
var id: String? {get set}
}
All of this code works, if you put all this code in a .swift file it will compile and it should work with your database.
Related
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.
I'm learning swift and I wanted to pull some data from a django model to work with, so I was following a tutorial on YouTube. I copied the code on YouTube and I got his Breaking Bad API (https://breakingbadapi.com/api/quotes) to display on my simulator, however when I subbed in the URL to my API, I returned an empty list and my simulator displays only a blank screen.
I've tried using both http://127.0.0.1:8000/api/main_course/
and http://127.0.0.1:8000/api/main_course/?format=json
From my terminal I get 200 OK:
[14/Sep/2022 21:28:48] "GET /api/main_course/ HTTP/1.1" 200 1185
Here's my code:
import SwiftUI
struct Entree: Codable {
var id: Int
var menu: String
var name: String
var descripton: String
}
struct ContentView: View {
#State private var entrees = [Entree]()
var body: some View {
List(entrees, id: \.id) {entree in
Text(entree.name)
Text("Run")
}
.task {
await loadData()
print(entrees)
}
}
func loadData() async {
// create URL
guard let url = URL(string: "http://127.0.0.1:8000/api/main_course/") else {
print("URL Invalid")
return
}
// fetch data from that URL
do {
let (data, _) = try await URLSession.shared.data(from: url)
// decode that data
if let decodedResponse = try? JSONDecoder().decode([Entree].self, from: data) {
entrees = decodedResponse
}
}
catch {
print("Data invalid")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
It seems that it is on the conversion.
Try using the same keys.
struct Entree: Codable {
var quoteId: Int
var quote: String
var author: String
var series: String
enum CodingKeys: String, CodingKey {
case quoteId = "quote_id"
}
}
I have a small project which is an extension of a Swift UI exercise making a web call to Github from Greg Lim's book Beginning Swift UI:
https://github.com/ethamoos/GitProbe
I’ve been using this to practise basic skills and to try and add other features that could be useful in a realworld app.
My main change from the initial exercise was to add the option to choose which user to lookup (this was previously hardcoded) and allow the user to enter this. Because this can return a lot of data I would now like to make the resulting List .searchable so that the user can filter the results.
I’ve been following this tutorial here:
https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-a-search-bar-to-filter-your-data
but I’ve realised that this is based upon the data being returned being Strings, and therefore the search is a string.
I am returning JSON decoded into a list of User data objects so a straight search does not work. I am assuming that I can adjust this to match a string search against my custom objects but I'm not sure how to do this.
To give you an idea of what I mean here is the code:
import SwiftUI
import URLImage
struct Result: Codable {
let totalCount: Int
let incompleteResults: Bool
let items: [User]
enum CodingKeys: String, CodingKey {
case totalCount = "total_count"
case incompleteResults = "incomplete_results"
case items
}
}
struct User: Codable, Hashable {
let login: String
let id: Int
let nodeID: String
let avatarURL: String
let gravatarID: String
enum CodingKeys: String, CodingKey {
case login, id
case nodeID = "node_id"
case avatarURL = "avatar_url"
case gravatarID = "gravatar_id"
}
}
class FetchUsers: ObservableObject {
#Published var users = [User]()
func search(for user:String) {
var urlComponents = URLComponents(string: "https://api.github.com/search/users")!
urlComponents.queryItems = [URLQueryItem(name: "q", value: user)]
guard let url = urlComponents.url else {
return
}
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let data = data {
let decodedData = try JSONDecoder().decode(Result.self, from: data)
DispatchQueue.main.async {
self.users = decodedData.items
}
} else {
print("No data")
}
} catch {
print("Error: \(error)")
}
}.resume()
}
}
struct ContentView: View {
#State var username: String = ""
var body: some View {
NavigationView {
Form {
Section {
Text("Enter user to search for")
TextField("Enter your username", text: $username).disableAutocorrection(true)
.autocapitalization(.none)
}
NavigationLink(destination: UserView(username: username)) {
Text("Show detail for \(username)")
}
}
}
}
}
struct UserView: View {
#State var username: String
#ObservedObject var fetchUsers = FetchUsers()
#State var searchText = ""
var body: some View {
List {
ForEach(fetchUsers.users, id:\.self) { user in
NavigationLink(user.login, destination: UserDetailView(user:user))
}
}.onAppear {
self.fetchUsers.search(for: username)
}
.searchable(text: $searchText)
.navigationTitle("Users")
}
/// With suggestion added
/// The search results
private var searchResults: [User] {
if searchText.isEmpty {
return fetchUsers.users // your entire list of users if no search input
} else {
return fetchUsers.search(for: searchText) // calls your search method passing your search text
}
}
}
struct UserDetailView: View {
var user: User
var body: some View {
Form {
Text(user.login).font(.headline)
Text("Git iD = \(user.id)")
URLImage(URL(string:user.avatarURL)!){ image in
image.resizable().frame(width: 50, height: 50)
}
}
}
}
Any help with this would be much appreciated.
Your UserListView is not properly constructed. I don't see why you would need a ScrollView with an empty text inside? I removed that.
So I removed searchText from the View to the FetchUsers class so we can delay the server requests thus avoiding unnecessary multiple calls. Please adjust it to your needs (check Apple's Debounce documentation. Everything should work as expected now.
import Combine
class FetchUsers: ObservableObject {
#Published var users = [User]()
#Published var searchText = ""
var subscription: Set<AnyCancellable> = []
init() {
$searchText
.debounce(for: .milliseconds(500), scheduler: RunLoop.main) // debounces the string publisher, delaying requests and avoiding unnecessary calls.
.removeDuplicates()
.map({ (string) -> String? in
if string.count < 1 {
self.users = [] // cleans the list results when empty search
return nil
}
return string
}) // prevents sending numerous requests and sends nil if the count of the characters is less than 1.
.compactMap{ $0 } // removes the nil values
.sink { (_) in
//
} receiveValue: { [self] text in
search(for: text)
}.store(in: &subscription)
}
func search(for user:String) {
var urlComponents = URLComponents(string: "https://api.github.com/search/users")!
urlComponents.queryItems = [URLQueryItem(name: "q", value: user.lowercased())]
guard let url = urlComponents.url else {
return
}
URLSession.shared.dataTask(with: url) {(data, response, error) in
guard error == nil else {
print("Error: \(error!.localizedDescription)")
return
}
guard let data = data else {
print("No data received")
return
}
do {
let decodedData = try JSONDecoder().decode(Result.self, from: data)
DispatchQueue.main.async {
self.users = decodedData.items
}
} catch {
print("Error: \(error)")
}
}.resume()
}
}
struct UserListView: View {
#State var username: String
#ObservedObject var fetchUsers = FetchUsers()
var body: some View {
NavigationView {
List {
ForEach(fetchUsers.users, id:\.self) { user in
NavigationLink(user.login, destination: UserDetailView(user:user))
}
}
.searchable(text: $fetchUsers.searchText) // we move the searchText to fetchUsers
.navigationTitle("Users")
}
}
}
I hope this helps! :)
In the end, I think I've figured this out - thanks to the suggestions from Andre.
I need to correctly filter my data and then return the remainder.
Here's the corrected (abridged) version:
import SwiftUI
import URLImage
struct Result: Codable {
let totalCount: Int
let incompleteResults: Bool
let items: [User]
enum CodingKeys: String, CodingKey {
case totalCount = "total_count"
case incompleteResults = "incomplete_results"
case items
}
}
struct User: Codable, Hashable {
let login: String
let id: Int
let nodeID: String
let avatarURL: String
let gravatarID: String
enum CodingKeys: String, CodingKey {
case login, id
case nodeID = "node_id"
case avatarURL = "avatar_url"
case gravatarID = "gravatar_id"
}
}
class FetchUsers: ObservableObject {
#Published var users = [User]()
func search(for user:String) {
var urlComponents = URLComponents(string: "https://api.github.com/search/users")!
urlComponents.queryItems = [URLQueryItem(name: "q", value: user)]
guard let url = urlComponents.url else {
return
// print("error")
}
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let data = data {
let decodedData = try JSONDecoder().decode(Result.self, from: data)
DispatchQueue.main.async {
self.users = decodedData.items
}
} else {
print("No data")
}
} catch {
print("Error: \(error)")
}
}.resume()
}
}
struct ContentView: View {
#State var username: String = ""
var body: some View {
NavigationView {
Form {
Section {
Text("Enter user to search for")
TextField("Enter your username", text: $username).disableAutocorrection(true)
.autocapitalization(.none)
}
NavigationLink(destination: UserView(username: username)) {
Text("Show detail for \(username)")
}
}
}
}
}
struct UserView: View {
#State var username: String
#ObservedObject var fetchUsers = FetchUsers()
#State var searchText = ""
var body: some View {
List {
ForEach(searchResults, id:\.self) { user in
NavigationLink(user.login, destination: UserDetailView(user:user))
}
}.onAppear {
self.fetchUsers.search(for: username)
}
.searchable(text: $searchText)
.navigationTitle("Users")
}
var searchResults: [User] {
if searchText.isEmpty {
print("Search is empty")
return fetchUsers.users
} else {
print("Search has a value - is filtering")
return fetchUsers.users.filter { $0.login.contains(searchText) }
}
}
}
struct UserDetailView: View {
var user: User
var body: some View {
Form {
Text(user.login).font(.headline)
Text("Git iD = \(user.id)")
URLImage(URL(string:user.avatarURL)!){ image in
image.resizable().frame(width: 50, height: 50)
}
}
}
}
I did more trial and error and a bit of online research and this is what I came back with:
func presentWelcomeMessage() {
//Get specific document from current user
let docRef = Firestore.firestore()
.collection("users")
.whereField("uid", isEqualTo: Auth.auth().currentUser?.uid ?? "")
// Get data
docRef.getDocuments { (querySnapshot, err) in
if let err = err {
print(err.localizedDescription)
} else if querySnapshot!.documents.count != 1 {
print("More than one document or none")
} else {
let document = querySnapshot!.documents.first
let dataDescription = document?.data()
guard let firstname = dataDescription?["firstname"] else { return }
self.welcomeLabel.text = "Hey, \(firstname) welcome!"
}
}
It works, but am not sure if it is the most optimal solution.
First I should say firstname is not really the best way to store a var. I would recommend using firstName instead for readability. I also recommend getting single documents like I am, rather than using a whereField.
An important thing to note is you should create a data model like I have that can hold all of the information you get.
Here is a full structure of how I would get the data, display it, and hold it.
struct UserModel: Identifiable, Codable {
var id: String
var firstName: String
private enum CodingKeys: String, CodingKey {
case id
case firstName
}
}
import SwiftUI
import FirebaseAuth
import FirebaseFirestore
import FirebaseFirestoreSwift
class UserDataManager: ObservableObject {
private lazy var authRef = Auth.auth()
private lazy var userInfoCollection = Firestore.firestore().collection("users")
public func getCurrentUIDData(completion: #escaping (_ currentUserData: UserModel) -> Void) {
if let currentUID = self.authRef.currentUser?.uid {
self.userInfoCollection.document(currentUID).getDocument { (document, error) in
if let document = document {
if let userData = try? document.data(as: UserModel.self) {
completion(userData)
}
} else if let error = error {
print("Error getting current UID data: \(error)")
}
}
} else {
print("No current UID")
}
}
}
struct ContentView: View {
#State private var userData: UserModel? = nil
private let
var body: some View {
ZStack {
if let userData = self.userData { <-- safely unwrap data
Text("Hey, \(userData.firstName) welcome!")
}
}
.onAppear {
if self.userData == nil { <-- onAppear can call more than once
self.udm.getCurrentUIDData { userData in
self.userData = userData <-- pass data from func to view
}
}
}
}
}
Hopefully this can point you in a better direction of how you should be getting and displaying data. Let me know if you have any further questions or issues.
I have prepared a simple test project at Github to demo my problem:
I have a SwiftUI List and I try to display the var items:[String] in it.
When I only have a hardcoded array like below - it works fine and displays in iPhone:
items = (1...200).map { number in "Item \(number)" }
But when I try to fetch JSON web page and append results to items then I get the error:
Escaping closure captures mutating 'self' parameter
I understand that the line items.append(str) modifies the parent ContentView object out of dataTask closure and that is not good for some reason... but how to fix my code then?
import SwiftUI
struct TopResponse: Codable {
let data: [Top]
}
struct Top: Codable {
let uid: Int
let elo: Int
let given: String
let photo: String?
let motto: String?
let avg_score: Double?
let avg_time: String?
}
struct ContentView: View {
var items:[String];
init() {
items = (1...200).map { number in "Item \(number)" }
let url = URL(string: "https://slova.de/ws/top")!
let task = URLSession.shared.dataTask(with: url) {
data, response, error in
let decoder = JSONDecoder()
guard let data = data else { return }
do {
let tops = try decoder.decode(TopResponse.self, from: data)
for (index, top) in tops.data.enumerated() {
let str = "\(index + 1): \(top.given)"
items.append(str) // this results in compile error!
}
} catch {
print("Error while parsing: \(error)")
}
}
task.resume()
}
var body: some View {
List(items, id: \.self) { item in
Text(item)
}
}
}
Should I move the items out of the View maybe?
My final target is to have the JSON data in Core Data and then update/notify the List from it.
I have such an app in Android (structured as MVVM) and now I am trying to port it to SwiftUI, being a Swift newbie.
UPDATE:
I have added a view model file as suggested by achu (thanks!) and it kind of works, but the List is only updated with new items when I drag at it. And there is a warning
[SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
I will move the items to ViewModel and eventually move the service call to an APIManager class
EDIT: The UI update should be in the main thread. Added service call on ViewModel init().
struct TestView: View {
#ObservedObject var viewModel = TestViewModel()
var body: some View {
List(viewModel.items, id: \.self) { item in
Text(item)
}
}
}
class TestViewModel: ObservableObject {
#Published var items: [String] = []
init() {
self.fetchData()
}
func fetchData() {
let url = URL(string: "https://slova.de/ws/top")!
let task = URLSession.shared.dataTask(with: url) {
data, response, error in
let decoder = JSONDecoder()
guard let data = data else { return }
do {
let tops = try decoder.decode(TopResponse.self, from: data)
for (index, top) in tops.data.enumerated() {
let str = "\(index + 1): \(top.given)"
self.updateItems(str)
}
} catch {
print("Error while parsing: \(error)")
}
}
task.resume()
}
func updateItems(_ str: String) {
DispatchQueue.main.async {
self.items.append(str)
}
}
}