I get difficulties to deal with below code, the value in dict sectionMoviesBundle = [HomeSection: [T]] could be MovieViewModel or ActorViewModel which two are struct type.
So generally how could I deal with this dict [String: [typeA or typeB...]], using generic or AnyObject like nowPlaying.results.map(MovieViewModel.init) as AnyObject? And how to implement it in code?
import SwiftUI
import Combine
class MovieListViewModel: ObservableObject {
private var webService = WebService()
private var cancellableSet: Set<AnyCancellable> = []
#Published var sectionMoviesBundle = [HomeSection: [T]]() // Don't know how to deal with it now=.=!
func getSectionMoviesBundle() {
webService.getSectionsPublisher()
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { status in
switch status {
case .finished:
break
case .failure(let error):
print("ERROR: \(error)")
break
}
}) { (nowPlaying, popular, upComing, topActor) in
self.sectionMoviesBundle[.NowPlaying] = nowPlaying.results.map(MovieViewModel.init)
self.sectionMoviesBundle[.Popular] = popular.results.map(MovieViewModel.init)
self.sectionMoviesBundle[.Upcoming] = upComing.results.map(MovieViewModel.init)
self.sectionMoviesBundle[.TopActor] = topActor.results.map(ActorViewModel.init)
}.store(in: &self.cancellableSet)
}
}
Some alternative that I think you can try
[String: [Any]]
Create a Protocol and implement it to all Struct that you want to use in the dictionary and use [String: [someProtocol]]
to insert it to the Dictionary, you can use your current code
One of the many approach that you can use is:
enum MovieSectionType {
case actor, movie
}
protocol MovieSectionViewModelProtocol {
var sectionType: MovieSectionType { get }
var name: String { get set }
}
struct MovieViewModel: MovieSectionViewModelProtocol{
var sectionType: MovieSectionType = .movie
var name: String
var title: String
}
struct ActorViewModel: MovieSectionViewModelProtocol {
var sectionType: MovieSectionType = .actor
var name: String
var birthday: Date
}
class MovieListViewModel: ObservableObject {
private var webService = WebService()
private var cancellableSet: Set<AnyCancellable> = []
#Published var sectionMoviesBundle = [HomeSection: [MovieSectionViewModelProtocol]]() // Don't know how to deal with it now=.=!
func getSectionMoviesBundle() {
self.sectionMoviesBundle[.Upcoming] = [MovieViewModel(name: "Movie", title: "Movie Title")]
self.sectionMoviesBundle[.TopActor] = [ActorViewModel(name: "Actor", birthday: Date())]
let item = self.sectionMoviesBundle[.Upcoming]!.first!
switch item.sectionType {
case .actor:
guard let actor = item as? ActorViewModel else {
return
}
print(actor.birthday)
break
case .movie:
guard let movie = item as? MovieViewModel else {
return
}
print(movie.title)
break
}
}
}
You could use a “sum type”, which in Swift is an enum:
enum SectionContent {
case actors([ActorViewModel])
case movies([MovieViewModel])
}
#Published var sectionMoviesBundle = [HomeSection: SectionContent]()
func getSectionMoviesBundle() {
webService.getSectionsPublisher()
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { status in
switch status {
case .finished:
break
case .failure(let error):
print("ERROR: \(error)")
break
}
}) { (nowPlaying, popular, upComing, topActor) in
self.sectionMoviesBundle[.NowPlaying] = .movies(nowPlaying.results.map(MovieViewModel.init))
self.sectionMoviesBundle[.Popular] = .movies(popular.results.map(MovieViewModel.init))
self.sectionMoviesBundle[.Upcoming] = .movies(upComing.results.map(MovieViewModel.init))
self.sectionMoviesBundle[.TopActor] = .actors(topActor.results.map(ActorViewModel.init))
}.store(in: &self.cancellableSet)
}
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 am creating mobile GitHub repository search app and I've just figured out how to handle no responses with my friend, but this solution doesn't allow to change screens between the the noResults and List views (I commented them for you in var body).
The code:
import SwiftUI
import Combine
private final class ContentViewState: ObservableObject {
#Published var isLoading = false
#Published var query = ""
#Published var stuff = [String]()
#Published var noResults = false
private var subscription: AnyCancellable?
func fetchRepos(query: String) {
isLoading = true
subscription = Just("test")
.delay(for: 2, scheduler: RunLoop.main)
.sink(receiveValue: {[weak self] (title: String) in
self?.isLoading = false
self?.stuff.append(title)
})
}
}
struct ContentView: View {
#StateObject private var state = ContentViewState()
#State private var items = [Item]()
var body: some View {
VStack {
if state.isLoading {
ProgressView()
}
//noResults view here
else if state.noResults {
HStack {
TextField("Enter search", text: $state.query)
Button("Search") {
state.fetchRepos(query: state.query)
}
}
Text("No results... Try again!")
}
//List view here
else {
HStack {
TextField("Enter search", text: $state.query)
Button("Search") {
state.fetchRepos(query: state.query)
}
}
List(items, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.fullName).font(.headline)
Text(item.urlCode)
}
}.task {
await loadData()
}
}
}
}
func loadData() async {
guard let url = URL(string: "https://api.github.com/search/repositories?q=" + state.query + "&per_page=20") else
{
DispatchQueue.main.async {
state.noResults = true }
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let decodedResponse = try? JSONDecoder().decode(Root.self, from: data) {
items = decodedResponse.items
if items.isEmpty {
DispatchQueue.main.async {
state.noResults = true }
}
}
} catch {
DispatchQueue.main.async {
state.noResults = true}
}
}
}
The problem is, if you pass the valid input, i.e.: Data, Core, then you get the search results loaded into the app.
If you pass the invalid input, i.e.: 'Brigmhnst', you get noResults view, but if you pass the valid input from that state, you won't get the List view.
Both views look almost the same. The only difference is one of them is else if and the other is else and I can't have two 'elses', can I?
I have already tried adding the start-point view (similar to list View) bound to else condition, another list View with else if and created an else condition inside do in func loadData that would run if the items were not empty. The app seemed to work, but the first start-point view wouldn't swap to listView after passing the data in it (it is not in the code, since this solution worked worse than that I already have, but I can add it if you want to see it).
There are a few problems with the code you have.
First, I believe the models (which are not included) are incorrect. I pasted the JSON into quicktype.io and this is the result:
import Foundation
// MARK: - Repos
struct Repos: Codable {
let totalCount: Int
let incompleteResults: Bool
let items: [Item]
enum CodingKeys: String, CodingKey {
case totalCount = "total_count"
case incompleteResults = "incomplete_results"
case items
}
}
// MARK: - Item
struct Item: Codable {
let id: Int
let nodeID, name, fullName: String
let itemPrivate: Bool
let owner: Owner
let htmlURL: String
let itemDescription: String?
let fork: Bool
let url, forksURL: String
let keysURL, collaboratorsURL: String
let teamsURL, hooksURL: String
let issueEventsURL: String
let eventsURL: String
let assigneesURL, branchesURL: String
let tagsURL: String
let blobsURL, gitTagsURL, gitRefsURL, treesURL: String
let statusesURL: String
let languagesURL, stargazersURL, contributorsURL, subscribersURL: String
let subscriptionURL: String
let commitsURL, gitCommitsURL, commentsURL, issueCommentURL: String
let contentsURL, compareURL: String
let mergesURL: String
let archiveURL: String
let downloadsURL: String
let issuesURL, pullsURL, milestonesURL, notificationsURL: String
let labelsURL, releasesURL: String
let deploymentsURL: String
let createdAt, updatedAt, pushedAt: String // Quicktype said these were dates, that didn't work
let gitURL, sshURL: String
let cloneURL: String
let svnURL: String
let homepage: String?
let size, stargazersCount, watchersCount: Int
let language: JSONNull?
let hasIssues, hasProjects, hasDownloads, hasWiki: Bool
let hasPages: Bool
let forksCount: Int
let mirrorURL: JSONNull?
let archived, disabled: Bool
let openIssuesCount: Int
let license: JSONNull?
let allowForking, isTemplate: Bool
let topics: [String]
let visibility: String
let forks, openIssues, watchers: Int
let defaultBranch: String
let score: Int
enum CodingKeys: String, CodingKey {
case id
case nodeID = "node_id"
case name
case fullName = "full_name"
case itemPrivate = "private"
case owner
case htmlURL = "html_url"
case itemDescription = "description"
case fork, url
case forksURL = "forks_url"
case keysURL = "keys_url"
case collaboratorsURL = "collaborators_url"
case teamsURL = "teams_url"
case hooksURL = "hooks_url"
case issueEventsURL = "issue_events_url"
case eventsURL = "events_url"
case assigneesURL = "assignees_url"
case branchesURL = "branches_url"
case tagsURL = "tags_url"
case blobsURL = "blobs_url"
case gitTagsURL = "git_tags_url"
case gitRefsURL = "git_refs_url"
case treesURL = "trees_url"
case statusesURL = "statuses_url"
case languagesURL = "languages_url"
case stargazersURL = "stargazers_url"
case contributorsURL = "contributors_url"
case subscribersURL = "subscribers_url"
case subscriptionURL = "subscription_url"
case commitsURL = "commits_url"
case gitCommitsURL = "git_commits_url"
case commentsURL = "comments_url"
case issueCommentURL = "issue_comment_url"
case contentsURL = "contents_url"
case compareURL = "compare_url"
case mergesURL = "merges_url"
case archiveURL = "archive_url"
case downloadsURL = "downloads_url"
case issuesURL = "issues_url"
case pullsURL = "pulls_url"
case milestonesURL = "milestones_url"
case notificationsURL = "notifications_url"
case labelsURL = "labels_url"
case releasesURL = "releases_url"
case deploymentsURL = "deployments_url"
case createdAt = "created_at"
case updatedAt = "updated_at"
case pushedAt = "pushed_at"
case gitURL = "git_url"
case sshURL = "ssh_url"
case cloneURL = "clone_url"
case svnURL = "svn_url"
case homepage, size
case stargazersCount = "stargazers_count"
case watchersCount = "watchers_count"
case language
case hasIssues = "has_issues"
case hasProjects = "has_projects"
case hasDownloads = "has_downloads"
case hasWiki = "has_wiki"
case hasPages = "has_pages"
case forksCount = "forks_count"
case mirrorURL = "mirror_url"
case archived, disabled
case openIssuesCount = "open_issues_count"
case license
case allowForking = "allow_forking"
case isTemplate = "is_template"
case topics, visibility, forks
case openIssues = "open_issues"
case watchers
case defaultBranch = "default_branch"
case score
}
}
// MARK: - Owner
struct Owner: Codable {
let login: String
let id: Int
let nodeID: String
let avatarURL: String
let gravatarID: String
let url, htmlURL, followersURL: String
let followingURL, gistsURL, starredURL: String
let subscriptionsURL, organizationsURL, reposURL: String
let eventsURL: String
let receivedEventsURL: String
let type: String
let siteAdmin: Bool
enum CodingKeys: String, CodingKey {
case login, id
case nodeID = "node_id"
case avatarURL = "avatar_url"
case gravatarID = "gravatar_id"
case url
case htmlURL = "html_url"
case followersURL = "followers_url"
case followingURL = "following_url"
case gistsURL = "gists_url"
case starredURL = "starred_url"
case subscriptionsURL = "subscriptions_url"
case organizationsURL = "organizations_url"
case reposURL = "repos_url"
case eventsURL = "events_url"
case receivedEventsURL = "received_events_url"
case type
case siteAdmin = "site_admin"
}
}
// MARK: - Encode/decode helpers
class JSONNull: Codable, Hashable {
public static func == (lhs: JSONNull, rhs: JSONNull) -> Bool {
return true
}
public var hashValue: Int {
return 0
}
public init() {}
public required init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if !container.decodeNil() {
throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull"))
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encodeNil()
}
}
Next, your fetchRepos function isn't doing anything. It's really only presenting a loader and stopping that later. The loadData function is actually what you want to use.
Finally, if you keep the task of loadData near the end of the code, you'll be in an infinite loop.
So these changes will clean it up a bit.
#MainActor private final class ContentViewState: ObservableObject {
#Published var isLoading = false
#Published var query = ""
#Published var items = [Item]()
func loadData(query: String = "") async {
guard let url = URL(string: "https://api.github.com/search/repositories?q=" + query + "&per_page=20") else {
return
}
isLoading = true
defer { isLoading = false }
do {
let (data, _) = try await URLSession.shared.data(from: url)
let decodedResponse = try JSONDecoder().decode(Repos.self, from: data)
items = decodedResponse.items
} catch {
print("error \(error)")
}
}
}
struct ContentView: View {
#StateObject private var state = ContentViewState()
var searchView: some View {
HStack {
TextField("Enter search", text: $state.query)
Button("Search") {
Task {
await state.loadData(query: state.query)
}
}
}
}
var body: some View {
if state.isLoading {
ProgressView()
} else {
VStack {
searchView
if state.items.isEmpty {
Text("No results...\nTry again!")
} else {
List(state.items) { item in
VStack(alignment: .leading) {
Text(item.fullName).font(.headline)
Text(item.htmlURL)
}.padding(.vertical)
}
}
}.padding()
}
}
}
The last thing you'll need to do is conform Item to Identifiable
extension Item: Identifiable {} // You could also just add Identifiable to the actual declaration
I decoded json data from an API and now I want to transfer the decoded data to my view where it can be displayed.
the problem is that I cannot transfer the decoded data to my view with #Observable Object.
My decoding class looks like this:
class apirefresh: ObservableObject {
#Published var datareturn : DataClass
func refreshData() {
let url = URL(string: "http://localhost:3000/Data")!
url.getResult { (result: Result<DataClass, Error>) in
switch result {
case let .success(dataout):
self.datareturn = dataout
print(dataout)
//print(dataout.klasse[0].aktive[0].name) //dataout.klasse[0].aktive[0].name
case let .failure(error):
print(error)
}
}
}
}
and my error like this:
Class Error
I also got an error in my View but this error seams to be related to the previous error.
View Error
And my json model looks like this:
// MARK: - APICall
struct APICall: Codable {
let data: DataClass
enum CodingKeys: String, CodingKey {
case data = "Data"
}
}
// MARK: - DataClass
struct DataClass: Codable {
let klasse: Klasse
let update: Update
}
// MARK: - Klasse
struct Klasse: Codable {
let aktive, bjugend, cjugend, djugend: [Verein]
let ejugend, fjugend, bambini: [Verein]
}
// MARK: - Aktive
struct Verein: Codable {
let id, place: String
let name: String
let tore, punkte: String
let gruppe: String
}
// MARK: - Update
struct Update: Codable {
let last: String
}
Thanks for your help in advance.
The problem is that datareturn in this class doesn't have an initial value, which is why the compiler is asking for a class initializer wherein you would give datareturn an initial value. So you must either do that:
class apirefresh: ObservableObject {
#Published var datareturn : DataClass
init(datareturn: DataClass) {
self.datareturn = datareturn
}
func refreshData() {
let url = URL(string: "http://localhost:3000/Data")!
url.getResult { (result: Result<DataClass, Error>) in
switch result {
case let .success(dataout):
self.datareturn = dataout
print(dataout)
//print(dataout.klasse[0].aktive[0].name) //dataout.klasse[0].aktive[0].name
case let .failure(error):
print(error)
}
}
}
}
Or make datareturn an optional (which makes its initial value nil).
class apirefresh: ObservableObject {
#Published var datareturn : DataClass?
func refreshData() {
let url = URL(string: "http://localhost:3000/Data")!
url.getResult { (result: Result<DataClass, Error>) in
switch result {
case let .success(dataout):
self.datareturn = dataout
print(dataout)
//print(dataout.klasse[0].aktive[0].name) //dataout.klasse[0].aktive[0].name
case let .failure(error):
print(error)
}
}
}
}
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)
}
first, I'm very (very) new to Swift programming. Challenging but so interesting!
Right now, in a Playground, I'm trying to fetch the data from a JSON that I can access using a URL.
I need to store the data somewhere (in this case I need to store an array of BixiStationViewModel so I can later on play with the data I fetch from the URL.
I think the issue is coming from the asynchronous process that is fetching the data and then having my code processing it.
You can see at the end of the code the print(allBixi.allStations) statement: it returns an empty array.
import Foundation
// JSON structure
struct BixiStationDataModel: Codable {
let lastUpdated, ttl: Int?
let data: StationsData?
enum CodingKeys: String, CodingKey {
case lastUpdated = "last_updated"
case ttl, data
}
}
struct StationsData: Codable {
let stations: [StationData]?
}
struct StationData: Codable {
let stationID: String?
let numBikesAvailable, numEbikesAvailable, numBikesDisabled, numDocksAvailable: Int?
let numDocksDisabled, isInstalled, isRenting, isReturning: Int?
let lastReported: Int?
let eightdHasAvailableKeys: Bool?
let eightdActiveStationServices: [EightdActiveStationService]?
enum CodingKeys: String, CodingKey {
case stationID = "station_id"
case numBikesAvailable = "num_bikes_available"
case numEbikesAvailable = "num_ebikes_available"
case numBikesDisabled = "num_bikes_disabled"
case numDocksAvailable = "num_docks_available"
case numDocksDisabled = "num_docks_disabled"
case isInstalled = "is_installed"
case isRenting = "is_renting"
case isReturning = "is_returning"
case lastReported = "last_reported"
case eightdHasAvailableKeys = "eightd_has_available_keys"
case eightdActiveStationServices = "eightd_active_station_services"
}
}
struct EightdActiveStationService: Codable {
let id: String?
}
// Calling the API
class WebserviceBixiStationData {
func loadBixiStationDataModel(url: URL, completion: #escaping ([StationData]?) -> ()) {
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {
completion(nil)
return
}
let response = try? JSONDecoder().decode(BixiStationDataModel.self, from: data)
if let response = response {
DispatchQueue.main.async {
completion(response.data?.stations)
}
}
}.resume()
}
}
// Data Model
class BixiStationViewModel {
let id = UUID()
let station: StationData
init(station: StationData) {
self.station = station
}
var stationID: String {
return self.station.stationID ?? ""
}
var numBikesAvailable: Int {
return self.station.numBikesAvailable ?? 0
}
var numDocksAvailable: Int {
return self.station.numDocksAvailable ?? 0
}
var isInstalled: Int {
return self.station.isInstalled ?? 0
}
var isReturning: Int {
return self.station.isReturning ?? 0
}
}
class BixiStationListModel {
init() { fetchBixiApiDataModel() }
var allStations = [BixiStationViewModel]()
private func fetchBixiApiDataModel() {
guard let url = URL(string: "https://api-core.bixi.com/gbfs/en/station_status.json") else {
fatalError("URL is not correct")
}
WebserviceBixiStationData().loadBixiStationDataModel(url: url) { stations in
if let stations = stations {
self.allStations = stations.map(BixiStationViewModel.init)
}
}
}
}
// Checking if the data has been dowloaded
let allBixi = BixiStationListModel()
print(allBixi.allStations)
How can I fix the code so I could access the values in the var allStations = [BixiStationViewModel]()
Thanks in advance, I've Benn working on it this issue for a while now and this would help me a lot in my app development
In a playground you need continuous execution to work with a url response.
Add PlaygroundPage.current.needsIndefiniteExecution = true to the top of your file (doesn't matter where it's added but I always do the top)
To get your data to print, add a print statement inside your loadBixiStationDataModel callback
WebserviceBixiStationData().loadBixiStationDataModel(url: url) { stations in
if let stations = stations {
self.allStations = stations.map(BixiStationViewModel.init)
print(stations)
}
}