Newb q: what is an adequate model to store multiple json decoded models

Related to :

Overview trying to do:
On a JSON URL downloaded and Decoded, set a flag and an array to store the decoded data, then update a view using ObservedObject

Question: what is a proper global object pattern to store these?
Im trying to devise a basic App wide global object that stores multiple various named JSON Decoded model arrays as well as flags for hasLoaded

Aside from an answer to the the error, what im asking for is guidance on proper Global management

Error in view file trying to print the data members path

print(SUBPLOTDATA.AllJSONEvents.self[2].featuredImage?.url as Any) 

Instance member 'subscript' cannot be used on type '[DataInEvents]' 

Singleton class and a Struct with Statics

// Singleton for holding global datas
// not sure yet how/where to instance the class for the app
// so also using a struct with statics to save data arrays

class SUPERAPP: ObservableObject {
    static let shared = SUPERAPP()
    private init(){}
    
    @Published var hasLoadedJSON : Bool = false
    var ALLJSONNESS_Events = [DataInEvents].self
}

struct SUBPLOTDATA {
    // this should get namespaced in a struct??
    static var AllJSONEvents = [DataInEvents].self
    static let yy = [DataInEvents].self
}

The view:


struct JsonKindaLoadedView: View {
    
    @ObservedObject private var appSettings = SUPERAPP.shared
    
    var body: some View {
        VStack{
            
            Text(superAPP.hasLoadedJSON ? "YaY" : "nurp")
            Button("json get!!", action: {
                
              Butttton()

              // error: Instance member 'subscript' cannot be used on type '[DataInEvents]'
               print(SUBPLOTDATA.AllJSONEvents.self[2].featuredImage?.url as Any)
                //print(appSettings.ALLJSONNESS_Events[0])
              
            })
            .padding()
            .background(Color.green)
            .foregroundColor(.white)
        }
    }
    
    
    func Butttton() -> Void {
      JSONURLSwift5("mockurl", dataArrayEvents: appSettings.ALLJSONNESS_Events)
    }
    
}

struct JsonKindaLoadedView_Previews: PreviewProvider {
    static var previews: some View {
        JsonKindaLoadedView()
    }
}


You have a poor grasp on the basic syntax of Swift. The type of AllJSONEvents should be [DataInEvents], not [DataInEvents].Type, and the same applies to the variables yy and ALLJSONNESS_Events. Therefore, the above should be changed to the following:

struct SubPlotData {
    static var allJSONEvents: [DataInEvents] = []
    static var yy: [DataInEvents] = []
}

Furthermore, JSONURLSwift5 should return a value of type [DataInEvents] that you can then assign to appSettings.ALLJSONNESS_Events:

appSettings.ALLJSONNESS_Events = JSONURLSwift5("mockurl", dataArrayEvents: [DataInEvents].self)

You need to read the Swift Lanuage Guide. If you don't do this, then you will continue to struggle.

1 Like

I have no arguments there, I read through most of the guide but of course don't yet grasp all of it. In all of the examples for getting json via URL and decoding, I have not seen a return variant of the function, they just set a var either right at the decode or nest the function within a class and then do a print to show that stuff worked. But then finding another example of them using the data later on in a view I have not found. So im gluing things together

I had been various of nuance syntax cause the other example functions of loading a JSON but only from a local file did return an array. But I figured that's not gonna work cause I need to focus on the async nature of getting the data for a real app

Thank you for the help, will keep rereading the guide

Saving data as a result of an asynchronous operation is a notoriously complex concept; please don't feel bad if it takes a while to understand -- it certainly took me a long time to understand.

In this case, because you have a View that is waiting for this data, your intuition to store it as a member variable on your @ObservedObject instance is correct. It seems, however, that you're relying on storing global values and trying to read them synchronously.

Since you're using a singleton instance of SUPERAPP, you can store the properties you're loading directly on instance variables of that class, rather than needing to store them separately as static variables in another struct.

class SuperApp: ObservableObject {
    static let shared = SuperApp()
    private init() {}
    
    @Published var allLoadedEvents: [DataInEvents]?

    func loadJSON() {
        // load the data asynchronously using URLSession
        URLSession.dataTask(with: request) { data, response, error in
            let decodedEvents = // ... decode your events however you'd like

            // @Published requires you to always store the results on the main queue.
            DispatchQueue.main.async {
                self.allLoadedEvents = decodedEvents
            }
        }
    }
}

Doing so has a few benefits:

  1. The changes are scoped to a specific instance of SuperApp, which will make it easier to migrate this away from using a singleton
  2. Because JsonKindaLoadedView references the instance of SuperApp as an @ObservedObject, it will get updated when the values are loaded. Inside the view you can use an if let to conditionally show a view for the results:
var body: some View {
    // ... views
    if let events = appSettings.allLoadedEvents {
      SomeKindOfEventsView(events)
    }
    // ... more views
}

And just to clarify, this syntax here:

static var AllJSONEvents = [DataInEvents].self

will create a static property that just stores the metatype of the type Array<DataInEvents>, which is not particularly relevant to this problem (though a powerful language feature!) The .self is the bit of syntax that distinguishes a type itself from the metatype of that type.

3 Likes

Thank you for these notes! I'll try implementing and get back to you

I had not known if a Singleton was the correct way to store data here. I do it a lot in javascript apps. For this iOS app I had thought it might be best to send off a bunch of json requests during the launch of the app so the splash screen can delay the visage of the loading data, hence the reason to not nest the loading logic in the views.

I am very glad you mentioned this. In Swift and on Apple platforms, singletons are a generally discouraged pattern. What you may want is to scope this data to the view that presents it, which you can do with the @StateObject property wrapper.

struct JsonKindaLoadedView: View {
    
    @StateObject private var appSettings = SuperApp()
    
    var body: some View {
        VStack {
            
            Text(appSettings.allJSONEvents != nil ? "YaY" : "nurp")
            Button("json get!!", action: {
                appSettings.loadJSON()
            })
            .padding()
            .background(Color.green)
            .foregroundColor(.white)
        }
    }
}

@StateObject is a bit magic -- it creates one instance of SuperApp and keeps it alive for as long as JsonKindaLoadedView is alive in the view hierarchy.

We're quickly veering into SwiftUI help territory, which is beyond the scope of these forums (and would be more appropriate for the Apple Developer Forums), but I'd recommend watching these two WWDC talks:

2 Likes

Also, in the interest of using The Best Possible Features, I'd recommend looking into Swift concurrency (if you can support iOS 15+).

That'll greatly simplify your loadJSON method:

@MainActor
func loadJSON() async throws {
  let (data, response) = try await URLSession.shared.data(from: url)
  // decode your data
  self.allJSONEvents = decodedData
}

Definitely recommend watching the CS193p course from Stanford University free on YouTube.

Also your naming conventions are un-Swifty. Your all-caps names are jarring.

It's important to remember that the way to learn how to write idiomatic code in a certain language is through experience. A certain pattern that seems jarring to a Swift programmer may be a common pattern in another language. Reading and writing Swift code will expose newcomers to the patterns and guidelines that have been established by experienced users of the language. Furthermore, advice to newcomers is most helpful when it's actionable. Suggesting established resources, or other resources that have helped you is a great way to help beginners learn not only how to write "Swifty" code, but it will also help newcomers learn where to look first for information the next time they have a question.

For naming conventions, I recommend reading the Swift API design guidelines. This covers fundamental principles such as "clarity at the point of use", general naming conventions such as the camel-case convention, etc.

3 Likes

@allthenames I apologize if my comment seemed gruff. It was not intended as such. I recommended the Stanford course because they do almost exactly what you’re asking, and is also a great intro to Swift.

1 Like

@toph42
Sall good! I had no issue with your suggestion. Im watching the other video suggestions right now, the one you suggested will be a much longer watch.

It is an entire college course, so yeah, it’s not quick, but it is worthwhile and my is go-to recommendation for folks looking to learn Swift when they already have a CS foundation. (The Swift Playgrounds “Learn To Code” courses are my recommendation for those totally new to coding.)

thank you all, your guidence links and example codes helped me piece together a working build!
Im watching the Stanford courses now as well, though long they are easier to let play in the background

1 Like