Load json from #propertyWrapper in SwiftUI ContentView - swift

I've been using a json decoding utility that works great. I'm wondering if that utility can be abstracted into a propertyWrapper that accepts the json file name as a string.
The call site in the ContentView looks like this:
struct ContentView: View {
#DataLoader("tracks.json") var tracks: [Tracks]
...
My rough sketch of the property wrapper looks like this:
#propertyWrapper
struct DataLoader<T: Decodable>: DynamicProperty {
private let fileName: String
var wrappedValue: T {
get {
return Bundle.main.decode(T.self, from: fileName)
}
set {
//not sure i need to set anything since i just want to get the array
}
}
init(_ fileName: String) {
self.fileName = fileName
wrappedValue = Bundle.main.decode(T.self, from: fileName)
}
}
Currently the body of the the ContentView shows this error:
Failed to produce diagnostic for expression; please file a bug report
I like the idea of removing some boilerplate code, but I think I'm missing something fundamental here.

In SwiftUI views are refreshed very often. When a view is refreshed then the #propertyWrapper will be initialised again - this might or might not be desirable. But it's worth noting.
Here is a simple demo showing how to create a property wrapper for loading JSON files. For simplicity I used try? and fatalError but in the real code you'll probably want to add a proper error handling.
#propertyWrapper
struct DataLoader<T> where T: Decodable {
private let fileName: String
var wrappedValue: T {
guard let result = loadJson(fileName: fileName) else {
fatalError("Cannot load json data \(fileName)")
}
return result
}
init(_ fileName: String) {
self.fileName = fileName
}
func loadJson(fileName: String) -> T? {
guard let url = Bundle.main.url(forResource: fileName, withExtension: "json"),
let data = try? Data(contentsOf: url),
let result = try? JSONDecoder().decode(T.self, from: data)
else {
return nil
}
return result
}
}
Then, assuming you have a sample JSON file called items.json:
[
{
"name": "Test1",
"count": 32
},
{
"name": "Test2",
"count": 15
}
]
with a corresponding struct:
struct Item: Codable {
let name: String
let count: Int
}
you can load your JSON file in your view:
struct ContentView: View {
#DataLoader("items") private var items: [Item]
var body: some View {
Text(items[0].name)
}
}

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)

Decode JSON into a target struct which differs from the JSON model

I have the following JSON:
{
"header":{
"namespace":"Device",
"name":"Response",
"messageID":"60FA815A-DC432316",
"payloadVersion":"1"
},
"payload":{
"device":{
"firmware":"1.23W",
"name":"Device 1",
"uuid":"0ba64a0c-7a88b278-0001",
"security":{
"code":"aXdAPqd2OO9sZ6evLKjo2Q=="
}
},
"system":{
"uptime":5680126
}
}
}
I created the Swift structs using quicktype.io:
// MARK: - Welcome
struct Welcome: Codable {
let header: Header
let payload: Payload
}
// MARK: - Header
struct Header: Codable {
let namespace, name, messageID, payloadVersion: String
}
// MARK: - Payload
struct Payload: Codable {
let device: Device
let system: System
}
// MARK: - Device
struct Device: Codable {
let firmware, name, uuid: String
let security: Security
}
// MARK: - Security
struct Security: Codable {
let code: String
}
// MARK: - System
struct System: Codable {
let uptime: Int
}
However, I already have a Device type, that is a bit more minimal:
struct Device: Identifiable {
let id: UUID
let ip: String
let name: String
let firmware: String
let uptime: Double
// ...
}
How can I nicely decode the raw JSON data into my Device struct? Note that my Device is flat and has fields, that are more deeply nested in the original API response model. Do I a custom Decodable implementation?
You can create intermediate CodingKeys, but this often gets pretty tedious and unnecessary. Instead you can make a general-purpose "string-key" like:
struct AnyStringKey: CodingKey, Hashable, ExpressibleByStringLiteral {
var stringValue: String
init(stringValue: String) { self.stringValue = stringValue }
init<S: StringProtocol>(_ stringValue: S) { self.init(stringValue: String(stringValue)) }
var intValue: Int?
init?(intValue: Int) { return nil }
init(stringLiteral value: String) { self.init(value) }
}
With that, you can navigate your structure pretty easily in a single decoder init by decoding nested containers:
extension Device: Decodable {
init(from decoder: Decoder) throws {
let root = try decoder.container(keyedBy: AnyStringKey.self)
let header = try root.nestedContainer(keyedBy: AnyStringKey.self, forKey: "header")
self.name = try header.decode(String.self, forKey: "name")
let payload = try root.nestedContainer(keyedBy: AnyStringKey.self, forKey: "payload")
let device = try payload.nestedContainer(keyedBy: AnyStringKey.self, forKey: "device")
self.id = try device.decode(UUID.self, forKey: "uuid")
self.firmware = try device.decode(String.self, forKey: "firmware")
let system = try payload.nestedContainer(keyedBy: AnyStringKey.self, forKey: "system")
self.uptime = try system.decode(Double.self, forKey: "uptime")
}
}
(I skipped ip because it's not in your data, and I assumed that your UUID was just a typo since it's not valid.)
With this, you should be able to decode any part you need.
This is very straightforward and standard, but if you have a lot of things to decode it can get a little tedious. You can improve it with a helper function in that case.
extension KeyedDecodingContainer {
func decode<T>(_ type: T.Type, forPath path: String) throws -> T
where T : Decodable, Key == AnyStringKey {
let components = path.split(separator: ".")
guard !components.isEmpty else {
throw DecodingError.keyNotFound(AnyStringKey(path),
.init(codingPath: codingPath,
debugDescription: "Could not find path \(path)",
underlyingError: nil))
}
if components.count == 1 {
return try decode(type, forKey: AnyStringKey(components[0]))
} else {
let container = try nestedContainer(keyedBy: AnyStringKey.self, forKey: AnyStringKey(components[0]))
return try container.decode(type, forPath: components.dropFirst().joined(separator: "."))
}
}
}
With this, you can access values by a dotted-path syntax:
extension Device: Decodable {
init(from decoder: Decoder) throws {
let root = try decoder.container(keyedBy: AnyStringKey.self)
self.name = try root.decode(String.self, forPath: "header.name")
self.id = try root.decode(UUID.self, forPath: "payload.device.uuid")
self.firmware = try root.decode(String.self, forPath: "payload.device.firmware")
self.uptime = try root.decode(Double.self, forPath: "payload.system.uptime")
}
}
I see two quick possible solutions:
Solution 1:
Rename the Codable Device:
struct Device: Codable {
...
}
into
struct DeviceFromAPI: Codable {
...
}
And then replace
struct Payload: Codable {
let device: Device
...
}
into
struct Payload: Codable {
let device: DeviceFromAPI
...
}
Solution2:
Use nested structures.
Put everything inside Welcome (which is the default QuickType.io name by the way, might be interesting to rename it).
struct Welcome: Codable {
let header: Header
let payload: Payload
// MARK: - Header
struct Header: Codable {
let namespace, name, messageID, payloadVersion: String
}
...
}
Go even if needed to put Device in Payload.
Then, you just have to use Welcome.Payload.Device or Welcome.Device (depending on how you nested it) when you want to refer to your Codable Device, and just Device when it's your own.
Then
Then, just have a custom init() for Device with the Codable Device as a parameter.
extension Device {
init(withCodableDevice codableDevice: DeviceFromAPI) {
self.firmware = codableDevice.firmware
...
}
}
or with solution 2:
extension Device {
init(withCodableDevice codableDevice: Welcome.Payload.Device) {
self.firmware = codableDevice.firmware
...
}
}

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.

Decode Nested JSON REST Api Response in to Array of Structs

I am trying to parse the response of https://swapi.dev/api/people/ in to a List but I can't seem to get it work due to the nested response structure instead of a flat response.
This is my ContentView:
struct ContentView: View {
private let charactersURL = URL(string: "https://swapi.dev/api/people/")!
#State private var characters: [Character] = []
var body: some View {
NavigationView {
VStack {
List {
ForEach(characters, id: \.self) { character in
NavigationLink(destination: CharacterView(character: character)) {
Text(character.name)
}
}
}
.onAppear(perform: loadCharacter)
}
.navigationBarTitle("Swapi Api Client")
}
}
private func loadCharacter() {
let request = URLRequest(url: charactersURL)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let charactersResponse = try? JSONDecoder().decode([Character].self, from: data.results) {
withAnimation {
self.characters = charactersResponse.results
}
}
}
}.resume()
}
}
I have a CharactersResponse struct:
struct CharactersResponse {
let count: Int
let next: String
let previous: NSNull
let results: [Character]
}
and a Character struct:
struct Character: Decodable, Hashable {
let name: String
let height: Int
let mass: String
let hairColor: String
let skinColor: String
let eyeColor: String
let birthYear: String
let gender: String
let homeworld: String
let films: [String]
let species: [String]
let vehicles : [String]
let starships: [String]
let created: String
let edited: String
let url: String
}
The error I get is Value of type 'Data' has no member 'results' but I'm not sure how to fix it so that the CharactersResponse results are parsed in to an array of Character structs.
You are mixing up the raw data and the deserialized structs
Replace
if let charactersResponse = try? JSONDecoder().decode([Character].self, from: data.results) { ...
with
if let charactersResponse = try? JSONDecoder().decode(CharactersResponse.self, from: data) { ...
And adopt Decodable in CharactersResponse
struct CharactersResponse : Decodable { ...
You might also replace NSNull with an optional of the expected type
Never try? in a JSONDecoder context. catch a potential error and print it.
do {
let charactersResponse = try JSONDecoder().decode(CharactersResponse.self, from: data) { ...
} catch { print(error) }