I'm currently trying to load JSON in Swift to use it in my UI. I think I've managed to get the JSON to load properly, but I can't test it due to the multiple errors I'm getting in my code.
JSONReader.swift:
import Foundation
struct DatabaseObject: Decodable {
let name: String
let books: Books
let memoryVerses: MemoryVerses
struct Books: Codable {
let Romans: Book
let James: Book
struct Book: Codable {
let abbreviation: String
let chapters: [Chapter]
struct Chapter: Codable {
let sections: [Section]
let footnotes: Footnotes
struct Section: Codable {
let title: String
let verses: [String]
}
struct Footnotes: Codable {
let a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z: String
}
}
}
}
struct MemoryVerses: Codable {
let singles: [String]
let multiples: [String]
}
}
public class JSONReaderSuperclass {
@Published var contentData: (status: String, result: DatabaseObject?)
init() {
contentData = (status: "loading", result: nil)
}
}
public class JSONReader: JSONReaderSuperclass, ObservableObject {
private func parse(jsonData: Data) -> (status: String, result: DatabaseObject?) {
do {
let decodedData = try JSONDecoder().decode(DatabaseObject.self, from: jsonData)
print(decodedData)
return (status: "success", result: decodedData)
} catch {
return (status: "fail", result: nil)
}
}
private func loadJson(fromURLString urlString: String,
completion: @escaping (Result<Data, Error>) -> Void) {
if let url = URL(string: urlString) {
let urlSession = URLSession(configuration: .default).dataTask(with: url) { (data, response, error) in
if let error = error {
completion(.failure(error))
}
if let data = data {
completion(.success(data))
}
}
urlSession.resume()
}
}
override init() {
super.init()
DispatchQueue.main.async {
self.loadJson(fromURLString: "redacted for anonymity") { result in
switch result {
case .success(let data):
self.contentData = self.parse(jsonData: data)
case .failure:
self.contentData = (status: "fail", result: nil)
}
}
}
}
}
ContentView.swift:
import SwiftUI
struct ContentView: View {
@StateObject var databaseObject = JSONReader()
var body: some View {
switch ($databaseObject.status) {
case "loading":
Text("Loading...")
case "success":
VersePicker(databaseObject: $databaseObject.result)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.navigationTitle("Content Judge")
.navigationBarTitleDisplayMode(.inline)
case "fail":
Text("An unknown error occured. Check your internet connection and try again.")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
VersePicker.swift:
import SwiftUI
enum Book: String, CaseIterable, Identifiable {
case romans
case james
var id: String { self.rawValue }
}
struct VersePicker: View {
var databaseObject: DatabaseObject
@State private var selectedBook = Book.romans
@State private var selectedChapter: Int = 1
@State private var selectedVerse: Int = 1
var body: some View {
VStack {
Picker("Book", selection: $selectedBook) {
ForEach(Book.allCases) { book in
Text(book.rawValue.capitalized)
.tag(book)
}
}
HStack {
Picker("Chapter", selection: $selectedChapter) {
ForEach(1...100, id: \.self) { number in
Text("\(number)")
}
}
Picker("Verse", selection: $selectedVerse) {
ForEach(1...100, id: \.self) { number in
Text("\(number)")
}
}
}
.frame(maxHeight: .infinity)
Spacer()
NavigationLink(destination: VerseDisplay()) {
Label("Go", systemImage: "arrow.right.circle")
}
}
.padding()
}
}
struct VersePicker_Previews: PreviewProvider {
static var previews: some View {
VersePicker(databaseObject: JSONReader().result)
}
}
I'm getting the following errors:
Any idea what I'm doing wrong? I'm completely new to Swift, so I'm at a loss.
First remove the tuple and use two separate properties instead
public class JSONReaderSuperclass {
@Published var status: String = ""
@Published var result: DatabaseObject? = nil
}
here I suggest you change status
to be an enum
instead.
Then we need to change the switch
in the init because we removed the tuple
switch result {
case .success(let data):
let decoded = self.parse(jsonData: data)
self.status = decoded.status
self.result = decoded.result
case .failure:
self.status = "fail"
self.result = nil
}
As a future change I suggest you don't do the download and decoding in the init but instead put it in a public function that you call from .onAppear
or .task
in your view.
In VersePicker
you need to make the databaseObject
property optional (or remove it since you don't seem to use it)
var databaseObject: DatabaseObject?
And finally in ContentView
you need to make some changes as well, to adjust for the new properties but also when you only read a @State, @StateObject or similar property you should not prefix the property with the $
sign, you only do that when you pass the property to a function or view that will change it.
Here is the body
of ContentView
var body: some View {
switch (databaseObject.status) {
case "loading":
Text("Loading...")
case "success":
VersePicker(databaseObject: databaseObject.result)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.navigationTitle("Content Judge")
.navigationBarTitleDisplayMode(.inline)
case "fail":
Text("An unknown error occured. Check your internet connection and try again.")
default:
fatalError() //Avoid this by changing status from string to an enum
}
}