How to transfer data between Class/func and View - swift

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)
}
}
}
}

Related

Passing data to another view using a model - SwiftUI

I'm trying to pass the data retrieved from the API to a View, but I'm getting the following error:
Class 'ApiManagerViewModel' has no initializers
This is how the ViewModel looks:
class ApiManagerViewModel: ObservableObject {
#Published var blockchainData: ApiDataClass
func callAPI() {
guard let url = URL(string: "myapiurl") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url, timeoutInterval: Double.infinity)
let callAPI = URLSession.shared.dataTask(with: request) { data, responce, error in
do {
if let data = data {
let decodedResponse = try JSONDecoder().decode(APIResponce.self, from: data)
DispatchQueue.main.async {
// update our UI
self.blockchainData = (decodedResponse.data)
}
// Everything is good, so we can exit
return
}
} catch {
print("Unexpected error while fetchign API: \(error).")
return
}
}
callAPI.resume()
}
This is the model:
// MARK: - APIResponce
struct APIResponce: Codable {
let data: ApiDataClass
let error: Bool
}
// MARK: - DataClass
struct ApiDataClass: Codable {
let address, quote_currency: String
let chain_id: Int
let items: [ApiItems]
}
// MARK: - Item
struct ApiItems: Codable {
let contract_decimals: Int32
let contract_name, contract_ticker_symbol, contract_address, logo_url, type, balance: String
let supports_erc: [String]?
let quote_rate: Double?
let quote: Double
}
I've tried initializing it but it's no bueno:
init() {
let address = 0, quote_currency = 0
let chain_id = 0
let items: [ApiItems]
}
If I initialize it like that I get the error, and I also don't want to repeat the same thing the model has:
Return from initializer without initializing all stored properties
I also tried with the variable like:
#Published var blockchainData = []
and I get the error on this line: self.blockchainData = (decodedResponse.data):
Cannot assign value of type 'ApiDataClass' to type '[Any]'
How can I make the variable blockchainData have the value coming from decodedResponse.data so I can pass it to another view?
Thanks
You're getting that error because you've declared var blockchainData: ApiDataClass, but haven't given it an initial value (your attempt at providing an initializer for ApiDataClass didn't help because the problem is ApiManagerViewModel).
The easiest solution to this is to turn it into an optional:
#Published var blockchainData: ApiDataClass?
Then, in your View, you'll probably want to check if it's available. Something like:
if let blockchainData = viewModel.blockchainData {
//code that depends on blockchainData
}
(assuming your instance of ApiManagerViewModel is called viewModel)

Cannot map protocol compliant elements to generic elements

As you can see below, I downloaded an array of structures containing heterogeneous objects that were decoded into enums containing nested objects.
I would now like to put said objects into a generic Model structure, but the compiler won't allow this - the error is described below in the code comment. I am relatively new to programming in Swift, I would appreciate your help.
import Foundation
let jsonString = """
{
"data":[
{
"type":"league",
"info":{
"name":"NBA",
"sport":"Basketball",
"website":"https://nba.com/"
}
},
{
"type":"player",
"info":{
"name":"Kawhi Leonard",
"position":"Small Forward",
"picture":"https://i.ibb.co/b5sGk6L/40a233a203be2a30e6d50501a73d3a0a8ccc131fv2-128.jpg"
}
},
{
"type":"team",
"info":{
"name":"Los Angeles Clippers",
"state":"California",
"logo":"https://logos-download.com/wp-content/uploads/2016/04/LA_Clippers_logo_logotype_emblem.png"
}
}
]
}
"""
struct Response: Decodable {
let data: [Datum]
}
struct League: Codable {
let name: String
let sport: String
let website: URL
}
extension League: Displayable {
var text: String { name }
var image: URL { website }
}
struct Player: Codable {
let name: String
let position: String
let picture: URL
}
extension Player: Displayable {
var text: String { name }
var image: URL { picture }
}
struct Team: Codable {
let name: String
let state: String
let logo: URL
}
extension Team: Displayable {
var text: String { name }
var image: URL { logo }
}
enum Datum: Decodable {
case league(League)
case player(Player)
case team(Team)
enum DatumType: String, Decodable {
case league
case player
case team
}
private enum CodingKeys : String, CodingKey { case type, info }
init(from decoder : Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(DatumType.self, forKey: .type)
switch type {
case .league:
let item = try container.decode(League.self, forKey: .info)
self = .league(item)
case .player:
let item = try container.decode(Player.self, forKey: .info)
self = .player(item)
case .team:
let item = try container.decode(Team.self, forKey: .info)
self = .team(item)
}
}
}
protocol Displayable {
var text: String { get }
var image: URL { get }
}
struct Model<T: Displayable> {
let text: String
let image: URL
init(item: T) {
self.text = item.text
self.image = item.image
}
}
do {
let response = try JSONDecoder().decode(Response.self, from: Data(jsonString.utf8))
let items = response.data
let models = items.map { (item) -> Model<Displayable> in // error: only struct/enum/class types can conform to protocols
switch item {
case .league(let league):
return Model(item: league)
case .player(let player):
return Model(item: player)
case .team(let team):
return Model(item: team)
}
}
} catch {
print(error)
}
You do not need generics here.
Change Model to accept any type that conforms to Displayable in the init
struct Model {
let text: String
let image: URL
init(item: Displayable) {
self.text = item.text
self.image = item.image
}
}
and then change the closure to return Model
let models = items.map { (item) -> Model in
If you want to keep your Model struct generic then you need to change the map call to
let models: [Any] = items.map { item -> Any in
switch item {
case .league(let league):
return Model(item: league)
case .player(let player):
return Model(item: player)
case .team(let team):
return Model(item: team)
}
}
This will give the following output when conforming to CustomStringConvertible
extension Model: CustomStringConvertible {
var description: String {
"\(text) type:\(type(of: self))"
}
}
print(models)
[NBA type:Model<League>, Kawhi Leonard type:Model<Player>, Los Angeles Clippers type:Model<Team>]
If you are only interested in name and the key representing the URL parse the JSON directly into Model by using nestedContainer this way
struct Response: Decodable {
let data: [Model]
}
struct Model: Decodable {
let name: String
let image: URL
enum DatumType: String, Decodable {
case league
case player
case team
}
private enum CodingKeys : String, CodingKey { case type, info }
private enum CodingSubKeys : String, CodingKey { case name, website, picture, logo }
init(from decoder : Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(DatumType.self, forKey: .type)
let subContainer = try container.nestedContainer(keyedBy: CodingSubKeys.self, forKey: .info)
self.name = try subContainer.decode(String.self, forKey: .name)
let urlKey : CodingSubKeys
switch type {
case .league: urlKey = .website
case .player: urlKey = .picture
case .team: urlKey = .logo
}
self.image = try subContainer.decode(URL.self, forKey: urlKey)
}
}
do {
let response = try JSONDecoder().decode(Response.self, from: Data(jsonString.utf8))
let data = response.data
print(data)
} catch {
print(error)
}

How can I iterate through a specific JSON item in Swift

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)

Swift: Dictionary with multi-types value

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)
}

can not convert json to struct object

I want to parse JSON data into a struct object but i can't do it.
Actually the code is in different files but here i'm posting it as a whole.
Here is my code :
import Foundation
struct dataResponse: Decodable {
var results: [userData]
init(from decoder: Decoder) throws {
var results = [userData] ()
var container = try decoder.unkeyedContainer()
while !container.isAtEnd {
if let route = try? container.decode(userData.self) {
results.append(route)
}
else {
_ = try? container.decode(dummyData.self)
}
}
self.results = results
}
}
private struct dummyData: Decodable { }
enum dataError: Error {
case dataUnavailable
case cannotProcessData
}
struct userData: Codable {
var avatar: String
var city: String
var contribution: Int
var country: String
var friendOfCount: Int
var handle: String
var lastOnlineTimeSeconds: Int
var maxRank: String
var maxRating: Int
var organization: String
var rank: String
var rating: Int
var registrationTimeSeconds: Int
var titlePhoto: String
}
struct dataRequest {
let requestUrl: URL
init(){
self.requestUrl = URL(string: "https://codeforces.com/api/user.info?handles=abhijeet_ar")!
}
func getData(completionHandler: #escaping(Result<[userData], dataError>) -> Void) {
URLSession.shared.dataTask(with: self.requestUrl) { (data,response, error) in
guard let data = data else {
completionHandler(.failure(.dataUnavailable))
print("-------bye-bye--------")
return
}
do {
print("-------entered--------")
// let dataresponse = try JSONDecoder().decode([userData].self, from: data)
// print(type(of: dataresponse))
// completionHandler(.success(dataresponse))
let jsonResult = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as AnyObject
print(jsonResult)
completionHandler(.success(jsonResult as! [userData]))
}
catch {
completionHandler(.failure(.cannotProcessData))
}
}.resume()
}
}
here userData is my struct
the error says : Could not cast value of type '__NSDictionaryM' (0x7fff8fe2dab0) to 'NSArray' (0x7fff8fe2dd30).
I would appreciate if anyone helps, thanks.
You are making a very common mistake.
You are ignoring the root object which is a dictionary and causes the error.
struct Root: Decodable {
let status : String
let result: [UserData]
}
struct UserData: Decodable {
let avatar: String
let city: String
let contribution: Int
let country: String
let friendOfCount: Int
let handle: String
let lastOnlineTimeSeconds: Int
let maxRank: String
let maxRating: Int
let organization: String
let rank: String
let rating: Int
let registrationTimeSeconds: Int
let titlePhoto: String
}
Forget JSONSerialization and use only JSONDecoder
And it's not a good idea to return meaningless enumerated errors. Use Error and return the real error.
You get the array with dataresponse.result
struct DataRequest { // name structs always with starting capital letter
let requestUrl: URL
init(){
self.requestUrl = URL(string: "https://codeforces.com/api/user.info?handles=abhijeet_ar")!
}
func getData(completionHandler: #escaping(Result<Root, Error>) -> Void) {
URLSession.shared.dataTask(with: self.requestUrl) { (data,response, error) in
guard let data = data else {
completionHandler(.failure(error!))
print("-------bye-bye--------")
return
}
do {
print("-------entered--------")
let dataresponse = try JSONDecoder().decode(Root.self, from: data)
completionHandler(.success(dataresponse))
}
catch {
completionHandler(.failure(error))
}
}.resume()
}
}
And consider that if status is not "OK" the JSON response could be different.
Your problem is you are using the wrong struct. Create and use a Response struct for decoding. You need to keep your Types to have the first letter in capital to avoid confusion. You could use the JSONDecoder() maybe you are using something that doesn't have the correct format. Try the below code.
struct Response: Codable {
var result: [UserData]
}
enum DataError: Error {
case dataUnavailable, cannotProcessData
}
struct DataRequest {
let requestUrl: URL
init(){
self.requestUrl = URL(string: "https://codeforces.com/api/user.info?handles=abhijeet_ar")!
}
func getData(completionHandler: #escaping(Result<Response, DataError>) -> Void) {
URLSession.shared.dataTask(with: self.requestUrl) { (data,response, error) in
guard let data = data else {
completionHandler(.failure(.dataUnavailable))
return
}
do {
let dataresponse = try JSONDecoder().decode(Response.self, from: data)
completionHandler(.success(dataresponse))
} catch {
completionHandler(.failure(.cannotProcessData))
}
}.resume()
}
}
Note: Parameters in UserData always needs to right. Try with and Empty struct to check if everything else is working then proceed to adding the variable one-by-one.