Making A Data Model With Compound Types That Works For A Document Based App ( And what is the SpreadSheet Type? )

There seems to be a lack of tutorials and documentation on how to put data models with compound types in document types, and then provide it to document Scenes. ( If “compound types” is the right term to cover both struct’s equivalent of being subclassed, and subclassed classes? )

I’ve been looking for weeks, and nothing really covers beyond how to bind a single class, or struct, with a few properties, to a document. Or if they're out there, I couldn't find them. Unfortunately those tutorials don’t make it clear how you would bind sub-typed data models, even though it seems (to me at least), to be a logical next step, and pretty small one in complexity.

As far as learning coding is concerned, is it expected that if you learn coding properly, you can pull the proper implementation, and syntax for this out of thin air for something like this, given the fundaments and knowing the simple version?

There are a number of multi hour YouTube videos on Swift as a whole I haven’t checked yet, but I didn’t had much luck with them so far when it comes to answering my questions.

Any help would be greatly appreciated!


For context, and in case it's useful to anyone who's trying to do something similar, here's what I think I've learned so far, and the general choices I've made.


( My code for the skeleton of my data model is in the middle of this post, and all my major questions are listed at the end. FEEL FREE TO SKIP THE CONTEXT, AND SUMMARY OF WHAT I'VE BEEN ABLE TO FIND IF YOU THINK YOU DON'T NEED IT. IT'S VERY LONG. )

( If you're just interested in the SpreadSheet type, which appears to have very little information available, it's in section 4 of this post. )


( Disclaimer, there’s a good chance I’m wrong on a number of these things. )

  1. There are two main options for making a document based app in SwiftUI - using SwiftData: FileDocument, and DocumentGroup. At least if you want to use things that do most of the heavy lifting for you, instead of making something completely from scratch. And that’s well above my coding skill level, and that’s probably only necessary in rare cases, if ever. There’s also ReferenceFileDocument.
  • Which one you use, comes down to what kind of data model(s) you want to use to store information in a document type(s): either declared and owned by your app, or a type your app does not own, but still views, creates, and/or edits, like the PDF file type.

  • FileDocument is exclusively (as far as I've read) for document data models made of structs, while DocumentGroup is exclusively for document data models made of classes that are made to be persistent (more on that later). ( Though giving structs a class behavior, with things like UUIDs in combination with other code, may be an exception. I don’t know. Can structs be made persistent in this context, or at all? )

  • You can have an app that supports multiple document types, by having more than one document Scene, in you’re main app struct.

  • In Xcode when creating a new project, you can select document based app, and if you also pick SwiftData for the storage, it does a lot for you. But if you want to start with a clean slate, instead of having to delete, or replace most of the stuff, its perfectly fine to select “none”, as "storage".

  • I heard that it’s highly recommended for complex data models, that you split up you’re classes and/or structs into separate Swift files in the Xcode project, with import Foundation at the beginning of each. Then you can put the swift files in folders within the project, and it makes it a lot easier to keep track of what’s going on.

  • Don’t use Swift Playgrounds to test the document functionality, use Xcode for that instead. For prototyping everything else it should be easy to do in Playgrounds. ( I haven’t used the new built in Playgrounds feature in Xcode. Just know it exists. Might be supper cool. )


  1. The advice of many tutorials, and articles is that "a type (of you're data model) should be made a class, if you want all its instances to be bound to one bucket of data, meaning if you change one, the data shared for all of the instances is being changed, and if you want every instance of a given type to have its own data, separate form the other instances of that given type, than you should make the type a struct." ( Though based on the functionality of some official code along samples, such as the “FlashCards” app from “WWDC23: Build an app with SwiftData | Apple”, it seems in the case of making a document based app, THERE MAY BE A FEW CRUCIAL EXCEPTIONS IN THE WAYS STURCTS AND CLASSES, CAN AND CAN'T BE USED WITHIN DOCUMENT DATA MODEL(S). )
  • Say you're making an app with similar data model requirements to a spreadsheet document based app, and want users to be able to create unlimited new cells (new instances of a type - which is probably named "Cell"). Each instance of "Cell" storing a String as a property of the type. The stored String of each “Cell” can be edited by the user in TextFields created in a View. Every TextField added to the View (displayed in the UI) needs a connection to the (implicitly, or explicitly) specified type instance added to the data model, as a new item in an array.

For example, this is the simple version of the data model I’m hoping to use.

( This how the data model would look being split into separate files in Xcode, or Swift Playgrounds. )

import Foundation

struct ContentCell {
    var isMarkedOff: Bool // < This is an extra property I wanted. ( It's not required. )
    var text: String
    
}

import Fondation

struct TableSection_Row { // < Each instance of this is *one* row of the table section. 
    
    var Row_ContentCells: [ContentCell] = 
        [ContentCell(isMarkedOff: false, text: ""),
        ContentCell(isMarkedOff: false, text: ""),
        ContentCell(isMarkedOff: false, text: ""),
        ContentCell(isMarkedOff: false, text: ""),
        ContentCell(isMarkedOff: false, text: ""),
        ContentCell(isMarkedOff: false, text: ""),
        ContentCell(isMarkedOff: false, text: ""),
        ContentCell(isMarkedOff: false, text: "")] 
    
}

"Row_ContentCells" is a property, which holds the "ContentCell" instances, for a given "TableSection_Row".

If you don't want to have any rows, or cells in each new document to start out with, the array should be empty. Written like this:

var ContentCells: [ContentCell] = []

Continuing on:

import Foundation

struct TableSection {
    
    var Section_Header: String
    
    var TableSection_Rows: [TableSection_Row] =
        [TableSection_Row(),
        TableSection_Row(),
        TableSection_Row()] // < Making the parenthesis empty makes it so you're not setting the property values here.  
// And the values I did set above for every "TableSection_Row" instance are in there technically, right?  
    
}

Because the user should be able to make an ordered series of table sections, one more array is needed.

import Foundation

class FullTable {

    var TableSections: [TableSection] = 
        [TableSection()]
    
}


//
// This can be a class because its instances don't need to be unique (or have
// an isolated place for there respective data), as one instance of this is the
// whole document, and there will only be one instance per document.  
// - (At least the app I'm trying to make.)  
//
// Having it be a class may also be good if multiple people are eventually 
// going to be able to edit the same document as a feature of the app.  
// That's not something I'm worried about doing now, but I'm trying to 
// keep it in mind, so I don't have to redo the whole app to implement it.  
//

Now it's time to make it so the data model can be binded to views, and so that the data can be stored in documents. ( As long as there's no issues with the data model on its own. )

  • Each cell needs its own data to be separate from all the other cells, and the same is true for each row or column (stored in a type (probably called "CellRow", or "CellColumn") as an array of "Cell"). Each table section stored (as a type that is an array of "CellRow" instances, which themselves store the "Cell" instances for given rows in arrays) inside the document.

  1. This top to bottom requirement of isolated storage for all the nested arrays, and the individual cells, appears to leave no other option, but to use a document data model made of structs, because classes can’t be used to store data separately for each instance. (At least with a data model that can’t just be put in the array made by a Query macro, which lets a class behave like a struct in the “FlashCards” sample mentioned above.)
  • Since bindings can be made for structs, it seems they are the best choice for the data model as a whole. Specifically using the Bindable macro to establish the components of the data model in Views. Or writing custom bindings for the nested structs.

  • To set up for eventually letting multiple people edit the same document, and for the data to be shared with other documents in the app, I think it would be best to have the document data model a class, with all of the structs of arrays inside it. But is that allowed, if it's even a valid approach? And in general what would it look like? Would it use FileDocument, DocumentGroup, or something else?

  • ( Definitely can’t put a Model macro in each element to add persistence, because that doesn’t work for structs, only classes. )


BONUS SECTION

  1. There is a spreadSheet base type (whatever “base type” means) that's built into Swift (not to be confused with: Table, Chart, Sheet, Grid, and their numerous variants, such as LazyVGrid, and LazyHGrid (all of which are container Views, meaning they can display data that’s passed into them from a data model, or you can define data solely within them, if the data only needs to be accessed for that Grid, or whatever the thing is, (though that storage in the view container, will not work for anything you want to save in a separate data model, as result of Swift’s: data display / UI, and data storage separation rule, to keep a SINGLE SOURCE OF TRUTH).
  • According to the one (free) potential in depth source I found on the SpreadSheet type, it is very flexible for making a variety of things such as calendar apps, or anything that might use something that's grid layout, even if it doesn't look like it at first glance. No borders, or spacing is required if set to zero, just like with Grid views. This is if it's the same thing from one video from 5 years ago, when it was a add on from Cocoapod, if I'm not misinterpreting what they said. (Whatever the case was then, SpreadSheet is now built directly into Xcode.)

  • I think spreadSheet not a container View, as it’s defined as a type in the documentation, but it may be able to be used as a container View in addition, anyways. Apparently it’s an App Intent. It seems to have everything from commenting on cells, to selecting multiple cells at once, and keeping track of the number of rows, and columns, a behavior which can be initialized, or something. And that's everything I was able to find on it.

  • ( SECTION SOURCES:

    • By clicking on the plus in the Xcode toolbar, and then going to “{ }” section, and searching “spreadsheet”, you can find some information there.

    • There’s a tiny bit on it in the Apple documentation for the two different pages for “spreadsheet”.

    • The video is called “Add SpreadSheet View to App (Swift 5) Xcode 11 - 2020” - by iOS Academy )

      Google’s AI Overview told me it doesn’t exist in Swift when I googled when it was added, but obviously, this is wrong. Don’t know how it got that answer.

Thanks For Reading!


Summary of my questions:

  1. Can structs be made persistent, as far as a document Scene is concerned? ( Or at all? )

  2. Can a mix of structs and classes be used in a document's data model? ( And is that necessary? )

  3. Should DocumentGroup, FileDocument, ReferenceFileDocument, or some other scene be used?