Passing data to another view using a model - SwiftUI - swift

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)

Related

Having trouble adding values to core data outside of a View

I'm trying to load data into a CoreData entity "Articles" from a function I would like to call in an init() {} call when my app starts which means I'm not doing this from within a view.
I get the message "Accessing Environment's value outside of being installed on a View. This will always read the default value and will not update."
and would like to work around that. I'm using Xcode 14.2
I do have a standard PersistenceController setup and so on
Here is where I run into the issue "let section = SectionsDB(context: managedObjectContext)"
#main
struct ArticlesExampleApp: App {
let persistanceController = PersistanceController.shared
init() {
let x = Articles.loadSections()
}
var body: some Scene {
WindowGroup {
MasterView()
.environment(\.managedObjectContext, persistanceController.container.viewContext)
}
}
class Articles {
class func loadSections() -> Int {
#Environment(\.managedObjectContext) var managedObjectContext
// Initialize some variables
let myPath = Bundle.main.path(forResource: "Articles", ofType: "json")
// Initialize some counters
var sectionsCount = 0
do {
let myData = try Data(contentsOf: URL(fileURLWithPath: myPath!), options: .alwaysMapped)
// Decode the json
let decoded = try JSONDecoder().decode(ArticlesJSON.self, from: myData)
// **here is where I run into the error on this statement**
let section = SectionsDB(context: managedObjectContext)
while sectionsCount < decoded.sections.count {
print("\(decoded.sections[sectionsCount].section_name) : \(decoded.sections[sectionsCount].section_desc)")
section.name = decoded.sections[sectionsCount].section_name
section.desc = decoded.sections[sectionsCount].section_desc
sectionsCount+=1
}
PersistanceController.shared.save()
} catch {
print("Error: \(error)")
}
return sectionsCount
}
}
Since you are already using a singleton, you can just use that singleton in your loadSections function:
let section = SectionsDB(context: PersistanceController.shared.container.viewContext)
And, remove the #Environment(\.managedObjectContext) var managedObjectContext line

How to transfer data between Class/func and View

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

Fetching JSON, appending to array: Escaping closure captures mutating 'self' parameter

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

SwiftUI: Encode a struct to be saved in AppStorage

Currently trying to build my first app in swiftUI. The part I thought would be the easiest as become a nightmare... save a struct in AppStorage to be available upon restart of the app
I got two struct to save. The first is for player and I have implemented the RawRepresentable
struct Player: Codable, Identifiable {
let id: Int
let name: String
let gamePlayed: Int
let bestScore: Int
let nbrGameWon: Int
let nbrGameLost: Int
let totalScore: Int?
}
typealias PlayerList = [Player]
extension PlayerList: RawRepresentable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(PlayerList.self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
}
Calling in my view this way:
struct AddPlayerView: View {
#State var name: String = ""
#State var isDisabled: Bool = false
#State var modified: Bool = false
#AppStorage("players") var players: PlayerList = PlayerList()
...
}
The above works, now I also want to save the current game data, I have the following struct:
struct Game: Codable, Identifiable {
var id: Int
var currentPlayerIndexes: Int
var currentRoundIndex: Int?
var dealerIndex: Int?
var maxRounds: Int?
var dealResults: [Int: Array<PlayerRoundSelection>]?
var currentLeaderIds: Array<Int>?
var isGameInProgress: Bool?
}
extension Game: RawRepresentable {
public init?(rawValue: String) {
if rawValue == "" {
// did to fix issue when calling AppStorage, but it is probably a bad idea
self = Game(id:1, currentPlayerIndexes:1)
}
else {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(Game.self, from: data)
else {
return nil
}
self = result
}
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return ""
}
return result
}
}
As soon as I try to modify the struct, it calls rawValue and the encoding fails with the following:
error: warning: couldn't get required object pointer (substituting NULL): Couldn't load 'self' because its value couldn't be evaluated
error: Execution was interrupted, reason: EXC_BAD_ACCESS (code=2, address=0x7ffee49bbff8).
Here part of the code that access the struct:
struct SelectPlayersView: View {
#AppStorage("currentGame") var currentGame: Game = Game(rawValue: "")!
....
NavigationLink(
destination: SelectModeTypeView(), tag: 2, selection: self.$selection) {
ActionButtonView(text:"Next", disabled: self.$isDisabled, buttonAction: {
var currentPlayers = Array<Int>()
self.players.forEach({ player in
if selectedPlayers.contains(player.id) {
currentPlayers.insert(player.id, at: currentPlayers.count)
}
})
// This used to be a list of indexes, but for testing only using a single index
self.currentGame.currentPlayerIndexes = 6
self.selection = 2
})
...
I found the code to encode here: https://lostmoa.com/blog/SaveCustomCodableTypesInAppStorageOrSceneStorage/
My understanding is that with the self in the encode, it generate an infinite loop hence the bad access.
I have really no knowledge how to properly encode this, any help, links would be appreciated
I had the same problem and I wanted to share my experience here.
I eventually found that apparently you cannot rely on the default Codable protocol implementation when used in combination with RawRepresentable.
So when I did my own Codable implementation, with CodingKeys and all, it worked!
I think your Codable implementation for Game would be something like:
enum CodingKeys: CodingKey {
case currentPlayerIndexes
case currentRoundIndex
// <all the other elements too>
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.currentPlayerIndexes = try container.decode(Int.self, forKey: .currentPlayerIndexes)
self.currentRoundIndex = try container.decode(Int.self, forKey: .currentRoundIndex)
// <and so on>
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(currentPlayerIndexes, forKey: .currentPlayerIndexes)
try container.encode(currentRoundIndex, forKey: .currentRoundIndex)
// <and so on>
}
I then wondered why your Player coding/decoding did work and found that the default coding and decoding of an Array (i.e. the PlayerList, which is [Player]), works fine.

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.