I am trying to display a list in a view after a condition. If data is recieved from API than screen will load new view having list showing multiple fields. Here I am showing only one field in the list. The navigation code is working fine and data is also decoded however blank screen appears when clicking list button screen moves to next view but blank screen.
Here is the view in which I am showing list :
import SwiftUI
struct MyPriceList: View {
#StateObject var road = ListAPI()
List
{
ForEach(road.priceRoad)
{
road in
HStack{
Text(road.packageName)
.font(.system(size: 15))
.foregroundColor(.black)
}
}
}
}
struct MyPriceList_Previews: PreviewProvider {
static var previews: some View {
MyPriceList()
}
}
}
The following is the viewmodel in which I have decoded the JSON data and applied the navigation
import Foundation
class ListAPI : ObservableObject
{
#Published var priceRoad = [ResponseList]()
func getList()
{
// url building code //
let list = URLSession.shared.dataTask(with: urlRequest)
{
(data, response, error) in
if let error = error {
print("Error \(error)")
}
if let data = data
{
do
{
let jsonList = try JSONDecoder().decode(PriceList.self, from: data)
let panama = jsonList.response
for pan in panama
{
print(pan.packageName) //successfully printing
}
if jsonList.success==true
{
DispatchQueue.main.async
{
self.navigate = true
self.priceRoad = jsonList.response
}
}
else
{
DispatchQueue.main.async
{
self.errorMessage = jsonList.message
}
}
}
catch
{
print("error \(error)")
}
}
}
list.resume()
} }
This is the data model for Json
struct PriceList : Codable
{
let success: Bool
let message: String
let response: [ResponseList]
enum CodingKeys:String, CodingKey{
case response = "ResponseData"
case success = "IsSuccess"
case message = "Message"
}
}
struct ResponseList:Codable
{
let packageId: Int
let packageName: String
let price: Double
let discountedPrice: Double
let testType: String
let testPackageGroupId: Int?
let SampleType: [SampleTypeList]?
enum CodingKeys:String, CodingKey{
case packageId = "PackageId"
case packageName = "PackageName"
case price = "Price"
case discountedPrice = "DiscountedPrice"
case testType = "Type"
case testPackageGroupId = "TestPackageGroupId"
case SampleType = "SampleTypeList"
}}
struct SampleTypeList:Codable
{
let testSampleTypeId: String
let sampleName: String
let colourCode: String
enum CodingKeys:String, CodingKey{
case testSampleTypeId = "TestSampleTypeId"
case sampleName = "SampleName"
case colourCode = "ColourCode"
}
}
I need to display TestName as packageName, MRP as price, B2B as discountedPrice, and TestType as testType.
try this approach, calling road.getList() in .onAppear{}:
struct MyPriceList: View {
#StateObject var road = ListAPI()
var body: some View { // <-- here need a body
List
{
ForEach(road.priceRoad)
{
road in
HStack{
Text(road.packageName)
.font(.system(size: 15))
.foregroundColor(.black)
}
}
}
.onAppear {
road.getList() // <-- here load your data
}
}
}
and make PriceList and ResponseList Identifiable, like this:
struct PriceList : Identifiable, Codable {
let id = UUID()
// ...
}
struct ResponseList: Identifiable, Codable {
let id = UUID()
// ...
}
struct SampleTypeList: Identifiable, Codable {
let id = UUID()
// ...
}
Alternatively, in ListAPI, you could have init() { getList() }, instead of using .onAppear {road.getList()}
Related
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 followed the example from here: SwiftUI 3 MacOs Table single selection and double click open sheet
But it's not working well for me.
I have a structure like this:
struct Response: Codable {
var items: [Repository]
}
struct Repository: Codable, Identifiable {
var number = UUID()
var id: Int = 0
let name: String
let updated_at: String
let owner: Owner
let stargazers_count: Int
let forks_count: Int
let language: String?
let description: String?
struct Owner: Codable {
let login: String
}
enum CodingKeys: String, CodingKey {
case name, updated_at, owner, stargazers_count, forks_count, language, description
}
}
class Api : ObservableObject{
func loadData(query: String = "javascript", completion:#escaping ([Repository]) -> ()) {
guard let url = URL(string: "https://api.github.com/search/repositories?q=\(query)") else {
print("Invalid url...")
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data!) {
var id = 0
let results = decodedResponse.items.map { (repo: Repository) -> Repository in
var copyRepo = repo
copyRepo.id = id
id += 1
return copyRepo
}
print(results)
DispatchQueue.main.async {
completion(results)
}
}
}.resume()
}
}
...
#State var repositories = [Repository]()
#State private var sortOrder = [KeyPathComparator(\Repository.name)]
#State private var selectedRepository: Repository.ID?
public var body: some View {
if(selectedRepository != nil)//null??
Text(repositories[selectedRepository!].name)
Table(repositories, selection: $selectedRepository, sortOrder: $sortOrder) {
TableColumn("Name") {
Text($0.name)
.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .leading
)
.contentShape(Rectangle())
.contextMenu {
Button(action: {}) { Text("Action") }
}
/*.gesture(TapGesture(count: 2).onEnded {
print("pr", selectedRepository )
})*/
}
TableColumn("Last updated"){
Text($0.updated_at)
}
TableColumn("Owner", value: \.owner.login)
}
.onAppear() {
Api().loadData { (repositories) in
self.repositories = repositories
self.isLoading = false
}
}
.onChange(of: sortOrder) {
print($0)
repositories.sort(using: $0)
}
}
I would like to make sure that when the user clicks on an element of the table, then activates the selection to show the data of the selected element in a Text field, I have tried to have the name printed but it is not working.
It gives me the following error after starting the app: Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
Can you give me a hand?
May be some missing character :
if(selectedRepository != nil)//null?? <- missing '{'
Text(repositories[selectedRepository!].name)
Also be writing like :
if let selectedRepository = selectedRepository {
Text(repositories[selectedRepository!].name)
...
}
Even if not necessary I always prefer to initialise properties (It is more an habit that something useful) :
#State private var selectedRepository: Repository.ID? = nil
This question builds on my previous question. Basically Im making an async call to the Google Books Api when a certain button is pressed. While I got the call working when its a method of the View however I want to overlay an activity indicator while it's loading. Hence I tried making an ObservableObject to make the call instead but Im not sure how to do it.
Here's what I have so far:
class GoogleBooksApi: ObservableObject {
enum LoadingState<Value> {
case loading(Double)
case loaded(Value)
}
#Published var state: LoadingState<GoogleBook> = .loading(0.0)
enum URLError : Error {
case badURL
}
func fetchBook(id identifier: String) async throws {
var components = URLComponents(string: "https://www.googleapis.com/books/v1/volumes")
components?.queryItems = [URLQueryItem(name: "q", value: "isbn=\(identifier)")]
guard let url = components?.url else { throw URLError.badURL }
self.state = .loading(0.25)
let (data, _) = try await URLSession.shared.data(from: url)
self.state = .loading(0.75)
self.state = .loaded(try JSONDecoder().decode(GoogleBook.self, from: data))
}
}
struct ContentView: View {
#State var name: String = ""
#State var author: String = ""
#State var total: String = ""
#State var code = "ISBN"
#ObservedObject var api: GoogleBooksApi
var body: some View {
VStack {
Text("Name: \(name)")
Text("Author: \(author)")
Text("total: \(total)")
Button(action: {
code = "978-0441013593"
Task {
do {
try await api.fetchBook(id: code)
let fetchedBooks = api.state
let book = fetchedBooks.items[0].volumeInfo
name = book.title
author = book.authors?[0] ?? ""
total = String(book.pageCount!)
} catch {
print(error)
}
}
}, label: {
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.blue)
})
}
}
}
// MARK: - GoogleBook
struct GoogleBook: Codable {
let kind: String
let totalItems: Int
let items: [Item]
}
// MARK: - Item
struct Item: Codable {
let id, etag: String
let selfLink: String
let volumeInfo: VolumeInfo
}
// MARK: - VolumeInfo
struct VolumeInfo: Codable {
let title: String
let authors: [String]?
let pageCount: Int?
let categories: [String]?
enum CodingKeys: String, CodingKey {
case title, authors
case pageCount, categories
}
}
and this is what works without the loading states:
struct ContentView: View {
#State var name: String = ""
#State var author: String = ""
#State var total: String = ""
#State var code = "ISBN"
enum URLError : Error {
case badURL
}
private func fetchBook(id identifier: String) async throws -> GoogleBook {
guard let encodedString = "https://www.googleapis.com/books/v1/volumes?q={\(identifier)}"
.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let url = URL(string: encodedString) else { throw URLError.badURL}
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(GoogleBook.self, from: data)
}
var body: some View {
VStack {
Text("Name: \(name)")
Text("Author: \(author)")
Text("total: \(total)")
Button(action: {
code = "978-0441013593"
Task {
do {
let fetchedBooks = try await fetchBook(id: code)
let book = fetchedBooks.items[0].volumeInfo
name = book.title
author = book.authors?[0] ?? ""
total = String(book.pageCount!)
} catch {
print(error)
}
}
}, label: {
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.blue)
})
}
}
}
// MARK: - GoogleBook
struct GoogleBook: Codable {
let kind: String
let totalItems: Int
let items: [Item]
}
// MARK: - Item
struct Item: Codable {
let id, etag: String
let selfLink: String
let volumeInfo: VolumeInfo
}
// MARK: - VolumeInfo
struct VolumeInfo: Codable {
let title: String
let authors: [String]?
let pageCount: Int?
let categories: [String]?
enum CodingKeys: String, CodingKey {
case title, authors
case pageCount, categories
}
}
I would go a step further and add idle and failed states.
Then instead of throwing an error change the state to failed and pass the error description. I removed the Double value from the loading state to just show a spinning ProgressView
#MainActor
class GoogleBooksApi: ObservableObject {
enum LoadingState {
case idle
case loading
case loaded(GoogleBook)
case failed(Error)
}
#Published var state: LoadingState = .idle
func fetchBook(id identifier: String) async {
var components = URLComponents(string: "https://www.googleapis.com/books/v1/volumes")
components?.queryItems = [URLQueryItem(name: "q", value: "isbn=\(identifier)")]
guard let url = components?.url else { state = .failed(URLError(.badURL)); return }
self.state = .loading
do {
let (data, _) = try await URLSession.shared.data(from: url)
let response = try JSONDecoder().decode(GoogleBook.self, from: data)
self.state = .loaded(response)
} catch {
state = .failed(error)
}
}
}
In the view you have to switch on the state and show different views.
And – very important – you have to declare the observable object as #StateObject. This is a very simple implementation
struct ContentView: View {
#State var code = "ISBN"
#StateObject var api = GoogleBooksApi()
var body: some View {
VStack {
switch api.state {
case .idle: EmptyView()
case .loading: ProgressView()
case .loaded(let books):
if let info = books.items.first?.volumeInfo {
Text("Name: \(info.title)")
Text("Author: \(info.authors?.joined(separator: ", ") ?? "")")
Text("total: \(books.totalItems)")
}
case .failed(let error):
if error is DecodingError {
Text(error.description)
} else {
Text(error.localizedDescription)
}
}
Button(action: {
code = "978-0441013593"
Task {
await api.fetchBook(id: code)
}
}, label: {
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.blue)
})
}
}
}
It seems like you're not initializing the GoogleBooksApi.
#ObservedObject var api: GoogleBooksApi
neither any init where it can be modified.
Other than that - I'd suggest using #StateObject (provided you deployment target is minimum iOS 14.0). Using ObservableObject might lead to multiple initializations of the GoogleBooksApi (whereas you need only once)
You should use #StateObject for any observable properties that you
initialize in the view that uses it. If the ObservableObject instance
is created externally and passed to the view that uses it mark your
property with #ObservedObject.
I've done my best to implement other peoples similar questions from around the internet but haven't been successful. I am working on a simple app that displays the top 50 cryptocurrencies. The information I will show will be the symbol(BTC, ETH...) and price. For now I am just trying to show the symbol.
I am able to get the symbol for each coin individually by using Text(self.fetcher.publishedCoins?.data.coins[0].symbol ?? "") and changing the array index. Obviously I don't want to do that 50 times so I tried implementing ForEach but couldn't figure it out. Here's where I'm at...
ContentView.swift
import SwiftUI
import Foundation
import Combine
struct ContentView: View {
#ObservedObject var fetcher = CoinFetcher()
var body: some View {
NavigationView {
List {
//Text(self.fetcher.publishedCoins?.data.coins[0].symbol ?? "Error Updating")
//Attempting to iterate through Coin.symbol
ForEach(self.fetcher.publishedCoins?.data.coins[Coin] ?? "") { select in
Text(select.symbol)
}
}
}
}}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
LoadJSON.swift
import Foundation
public class CoinFetcher: ObservableObject {
#Published var publishedCoins: Top?
init() {
loadJSON()
}
func loadJSON() {
let url = URL(string: "https://api.coinranking.com/v1/public/coins")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let retrievedData = data {
let webData = try JSONDecoder().decode(Top.self, from: retrievedData)
print(Top.self)
DispatchQueue.main.async {
self.publishedCoins = webData
}
} else {
print("No data loaded")
}
} catch {
print ("Error here")
}
}.resume()
}
}
Coins.swift
import Foundation
// MARK: - Top
struct Top: Codable {
let status: String
let data: Data
}
// MARK: - Data
struct Data: Codable {
let coins: [Coin]
}
// MARK: - Coin
struct Coin: Codable {
let id: Int
let uuid: String
let slug: String
let symbol: String
let name: String
let confirmedSupply: Bool
let volume: Int
let marketCap: Int
let price: String
let circulatingSupply: Double
let totalSupply: Double
let approvedSupply: Bool
let change: Double
let rank: Int
let history: [String?]
enum CodingKeys: String, CodingKey {
case id, uuid, slug, symbol, name, confirmedSupply, volume, marketCap, price, circulatingSupply, totalSupply, approvedSupply, change, rank, history
}
}
Thank you for your help!
If you can conform Coin to Hashable:
struct Coin: Codable, Hashable { ... }
you can try the following:
NavigationView {
List {
ForEach(self.fetcher.publishedCoins?.data.coins ?? [], id:\.self) { coin in
Text(coin.symbol)
}
}
}
Note that as your data can change you need to use a dynamic ForEach loop (with an explicit id parameter)
I have an API that returns a JSON object with keys:
http://acnhapi.com/v1/bugs
Since it's not an array, I'd like to understand how to traverse it. I am trying to get a list of the bug names "common butterfly" and "yellow butterfly" etc. by using their keys common_butterfly and yellow_butterfly etc.
I want to display the value of common_butterfly.name.name-USen, but for each bug. So my list view should ultimately be displayed as:
common butterfly
yellow butterfly
tiger butterfly
etc.
(Alphabetical would be a bonus)
data
import SwiftUI
struct Bugs: Codable, Identifiable {
let id = UUID()
var name: String
}
class FetchBugs: ObservableObject {
#Published var bugs = [Bugs]()
init() {
let url = URL(string: "http://acnhapi.com/v1/bugs")!
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let bugsData = data {
let decodedData = try JSONDecoder().decode([Bugs].self, from: bugsData)
DispatchQueue.main.async {
self.bugs = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
list
import SwiftUI
struct BugList: View {
#ObservedObject var fetch = FetchBugs()
var body: some View {
VStack {
List(fetch.bugs) { bug in
VStack(alignment: .leading) {
Text(bug.name)
}
}
}
}
}
struct BugList_Previews: PreviewProvider {
static var previews: some View {
BugList()
}
}
With this solution you can decode all your localised names:
struct Bug: Decodable, Identifiable {
enum CodingKeys: String, CodingKey { case name }
let id = UUID()
var localizedNames: [String: String] = [:]
var nameUSen: String {
localizedNames["name-USen"] ?? "error"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let names = try container.decode([String: String].self, forKey: .name)
for (key, value) in names {
localizedNames[key] = value
}
}
}
Use .sorted { $0.nameUSen < $1.nameUSen } to sort your data:
class FetchBugs: ObservableObject {
#Published var bugs = [Bug]()
init() {
let url = URL(string: "http://acnhapi.com/v1/bugs")!
URLSession.shared.dataTask(with: url) { data, response, error in
do {
if let bugsData = data {
let decodedData = try JSONDecoder().decode([String: Bug].self, from: bugsData)
DispatchQueue.main.async {
self.bugs = Array(decodedData.values).sorted { $0.nameUSen < $1.nameUSen }
}
} else {
print("No data")
}
} catch {
print(error)
}
}.resume()
}
}
And display the USen name:
struct BugList: View {
#ObservedObject var fetch = FetchBugs()
var body: some View {
VStack {
List(fetch.bugs) { bug in
VStack(alignment: .leading) {
Text(bug.nameUSen)
}
}
}
}
}
If you'd ever want to access any other name you can use:
bug.localizedNames["name-EUde"]!
Here's a playground that illustrates getting all bug's names in alphabetical order:
struct Bug: Decodable, Identifiable {
let id: Int
let name: Name
struct Name: Decodable {
let nameUSen: String
enum CodingKeys: String, CodingKey {
case nameUSen = "name-USen"
}
}
}
do {
let butterflies = try Data(contentsOf: URL(string: "http://acnhapi.com/v1/bugs")!)
let allBugs = try JSONDecoder().decode([String: Bug].self, from: butterflies)
let bugs = Array(allBugs.values.sorted { $0.name.nameUSen < $1.name.nameUSen })
bugs.forEach { print($0.name.nameUSen) }
} catch {
print(error)
}