I used to save important App data like login credentials into UserDefaults using the following statement:
UserDefaults.standard.set("sample#email.com", forKey: "emailAddress")
Now, I have come to know SwiftUI has introduced new property wrapper called:
#AppStorage
Could anyone please explain how the new feature works?
AppStorage
#AppStorage is a convenient way to save and read variables from UserDefaults and use them in the same way as #State properties. It can be seen as a #State property which is automatically saved to (and read from) UserDefaults.
You can think of the following:
#AppStorage("emailAddress") var emailAddress: String = "sample#email.com"
as an equivalent of this (which is not allowed in SwiftUI and will not compile):
#State var emailAddress: String = "sample#email.com" {
get {
UserDefaults.standard.string(forKey: "emailAddress")
}
set {
UserDefaults.standard.set(newValue, forKey: "emailAddress")
}
}
Note that #AppStorage behaves like a #State: a change to its value will invalidate and redraw a View.
By default #AppStorage will use UserDefaults.standard. However, you can specify your own UserDefaults store:
#AppStorage("emailAddress", store: UserDefaults(...)) ...
Unsupported types (e.g., Array):
As mentioned in iOSDevil's answer, AppStorage is currently of limited use:
types you can use in #AppStorage are (currently) limited to: Bool, Int, Double, String, URL, Data
If you want to use any other type (like Array), you can add conformance to RawRepresentable:
extension Array: RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Element].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
}
}
Demo:
struct ContentView: View {
#AppStorage("itemsInt") var itemsInt = [1, 2, 3]
#AppStorage("itemsBool") var itemsBool = [true, false, true]
var body: some View {
VStack {
Text("itemsInt: \(String(describing: itemsInt))")
Text("itemsBool: \(String(describing: itemsBool))")
Button("Add item") {
itemsInt.append(Int.random(in: 1...10))
itemsBool.append(Int.random(in: 1...10).isMultiple(of: 2))
}
}
}
}
Useful links:
What is the #AppStorage property wrapper?
AppStorage Property Wrapper SwiftUI
Disclaimer: iOS 14 Beta 2
In addition to the other useful answers, the types you can use in #AppStorage are (currently) limited to: Bool, Int, Double, String, URL, Data
Attempting to use other types (such as Array) results in the error: "No exact matches in call to initializer"
Re-implementation for iOS 13 and without SwiftUI
In additon to pawello2222 answer, here. is the reimplementation of the AppStorage that I named it as UserDefaultStorage:
#propertyWrapper
struct UserDefaultStorage<T: Codable> {
private let key: String
private let defaultValue: T
private let userDefaults: UserDefaults
init(key: String, default: T, store: UserDefaults = .standard) {
self.key = key
self.defaultValue = `default`
self.userDefaults = store
}
var wrappedValue: T {
get {
guard let data = userDefaults.data(forKey: key) else {
return defaultValue
}
let value = try? JSONDecoder().decode(T.self, from: data)
return value ?? defaultValue
}
set {
let data = try? JSONEncoder().encode(newValue)
userDefaults.set(data, forKey: key)
}
}
}
This wrapper can store/restore any kind of codable into/from the user defaults. Also, it works in iOS 13 and it doesn't need to import SwiftUI.
Usage
#UserDefaultStorage(key: "myCustomKey", default: 0)
var myValue: Int
Note that it can't be used directly as a State
This is a persistent storage provided by SwiftUI. This code will persist the email across app launches.
struct AppStorageView: View {
#AppStorage("emailAddress") var emailAddress = "initial#hey.com"
var body: some View {
TextField("Email Address", text: $emailAddress)
}
}
With pure SwiftUI code, we can now persist such data without using UserDefaults at all.
But if you do want to access the underlying data, it is no secret that the wrapper is using UserDefaults. For example, you can still update using UserDefaults.standard.set(...), and the benefit is that AppStorage observes the store, and the SwiftUI view will update automatically.
#AppStorage property wrapper is a source of truth that allows you read data from and write it to UserDefaults. It has a value semantics. You can use #AppStorage to store any basic data type (like preferences' values or any default values) for as long as the app is installed.
For the best performance, when working with UserDefaults, you should keep the size of the stored data between 512 KB and 1 MB.
import SwiftUI
struct ContentView: View {
#AppStorage("t_101") var t_101: String = "Terminator T-101"
#AppStorage("t_1000", store: .standard) var t_1000 = "Liquid Metal"
var body: some View {
VStack {
Text("Hey, \(t_101)!").foregroundColor(.indigo)
Text("Hey, \(t_1000)!").foregroundColor(.teal)
Divider()
Button("Real Names") {
t_101 = "Arnie"
t_1000 = "Robbie"
}
}
}
}
We can test this via simple approach:
struct Content: View {
private enum Keys {
static let numberOne = "myKey"
}
#AppStorage(Keys.numberOne) var keyValue2: String = "no value"
var body: some View {
VStack {
Button {
keyValue2 = "Hello"
print(
UserDefaults.standard.value(forKey: Keys.numberOne) as! String
)
} label: {
Text("Update")
}
Text(keyValue2)
}
.padding()
.frame(width: 100)
}
}
I have experimented a behavior that I want to share with you. I had a View (called MainView) that used an #AppStorage Bool to display or not a message to users. In this View, I also had many views displaying data loaded from a JSON (cities).
I've developed a 'Add to favorite' feature that uses UserDefault to store a list of cities ID added by user in his/her favorites. The strange behavior was this one: As soon as user added or removed a city as favorite, all MainView's body (and all its child views) was updated. That means all content was reloaded for no reason.
After hours of investigations, specially refactoring all my ObservableObjects (models), my #Published variables and so on, I've find out what was creating this mass update: The #AppStorage in my MainView! As soon as you update any key in 'UserDefaults.standard' (in my case by storing my new favorite), all views's body using an #AppStorage are updated.
So #AppStorage is awesome and easy to use. But be aware of that behavior. You don't expect that a view is updated when you set a key that is not even used in that view!
Related
So I have an ParseObject setup as this - This is the main Object in Parse called MGLocation:
struct MGLocation: ParseObject {
var objectId: String?
var createdAt: Date?
var updatedAt: Date?
var originalData: Data?
var ACL: ParseACL?
var title: String?
var category: String?
init() {}
init(objectId: String?) {
self.objectId = objectId
}
}
Then I have my Codable setup using the following code:
struct Place: Codable, Identifiable {
let id: Int
var b4aId = ""
let title: String?
let category: String
init(
id: Int,
title: String?,
category: String,
) {
self.id = id
self.title = title
self.category = category
}
init(with p: MGLocation) {
self.id = atomicId.wrappingIncrementThenLoad(ordering: .relaxed)
self.b4aId = p.objectId ?? ""
self.title = p.title ?? "Undefined"
self.category = p.category ?? "Uncategorized"
}
}
Then I have the following function which pulls in the MGLocation:
func fetchPlaces() {
let query = MGLocation.query().limit(1000)
query.find { [weak self] result in
guard let self = self else {
return
}
switch result {
case .success(let items):
self.places = items.map({
Place(with: $0)
})
case .failure(let error):
}
}
}
Questions:
What is the best way that I can pull in a relational column? Inside MGLocation, I have a related column called images which can access another object.
This calls MGImage which has the following columns:
id, title, column, mime
Does anyone know how I can infuse and pull in the related column also? All help will be appreciated!
I recommend using Parse-Swift from here: https://github.com/netreconlab/Parse-Swift, as oppose to the parse-community one. I'm one of the original developers of Pase-Swift (you can see this on the contributors list) and I don't support the parse-community version anymore. The netreconlab version is way ahead of the other in terms of bug fixes and features and you will get better and faster support from the netreconlab since I've been the one to address the majority of issues in the past.
What is the best way that I can pull in a relational column? Inside MGLocation, I have a related column called images which can access another object.
I always recommend looking at the Playgrounds for similar examples. The Playgrounds has Roles and Relation examples. Assuming you are using version 4.16.2 on netreconlab. The only way you can do this with your current relation on your server is like so:
// You need to add your struct for MGImage on your client
struct MGImage: ParseObject { ... }
struct MGLocation: ParseObject {
var objectId: String?
var createdAt: Date?
var updatedAt: Date?
var originalData: Data?
var ACL: ParseACL?
var title: String?
var category: String?
var images: ParseRelation<Self>? // Add this relation
/* These aren't needed as they are already given for free.
init() {}
init(objectId: String?) {
self.objectId = objectId
}
*/
}
//: It's recommended to place custom initializers in an extension
//: to preserve the memberwise initializer.
extension MGLocation {
// The two inits in your code aren't needed because ParseObject gives them to you
}
//: Now we will see how to use the stored `ParseRelation on` property in MGLocation to create query
//: all of the relations to `scores`.
Task {
do {
//: Fetch the updated location since the previous relations were created on the server.
let location = try await MGLocation(objectId: "myLocationObjectId").fetch()
print("Updated current location with relation: \(location)")
let usableStoredRelation = try location.relation(location.images, key: "images")
let images = try await (usableStoredRelation.query() as Query<MGImage>).find()
print("Found related images from stored ParseRelation: \(images)")
} catch {
print("\(error.localizedDescription)")
}
}
If your images column was of type [MGImage] instead of Relation<MGImage> then you would be able to use var images: [MGImage]? in your MGLocation model and simply use include on your query. You can see more here: https://github.com/netreconlab/Parse-Swift/blob/325196929fed80ca3120956f2545cf2ed980616b/ParseSwift.playground/Pages/8%20-%20Pointers.xcplaygroundpage/Contents.swift#L168-L173
Hi there,
Ive been coding an app for my friend and me recently and currently I'm implementing Google Firebase's Firestore Database. I have set up a Data Model and a View Model to handle data to my view. Bear in mind I'm still new to Swift(UI) so my code might be a little messy.
This is where the database is accessed and the data is put into the data model.
Friends_Model.swift
import Foundation
import Firebase
import FirebaseFirestore
class Friends_Model: ObservableObject {
#Published var friend_list = [Friends_Data]()
#Published var noFriends = false
func getData() {
let db = Firestore.firestore()
db.collection("users").getDocuments { snapshot, error in
//check for errors
if error == nil {
print("no errors")
if let snapshot = snapshot {
//Update the list property in main thread
DispatchQueue.main.async {
//get all docs and create friend list
self.friend_list = snapshot.documents.map { d in
//Create friend item for each document
return Friends_Data(id: d.documentID,
userID: d["userID"] as? String ?? "")
}
}
}
} else {
// handle error
}
}
}
}
This is my data model. To my understanding this just sets the variables.
Friends_Data.swift
import Foundation
struct Friends_Data: Identifiable {
var id: String
var userID: String
}
This is my actual view where I output the data (just the relevant part ofc).
FriendsPanel.swift (Swift View File)
// var body etc. etc.
if let user = user {
let uid = user.uid ?? "error: uid"
let email = user.email ?? "error: email"
let displayName = user.displayName
VStack {
Group{
Text("Your Friends")
.font(.title)
.fontWeight(.bold)
}
List (friends_model.friend_list) { item in
Text(item.userID)
}
.refreshable {
friends_model.getData()
}
}
// further code
Displaying all entries in the database works fine, though I'd wish to only display the entries with the attribute "friendsWith" having the same string as oneself (uid).
Something like
if friends_model.friends_list.userID == uid {
// display List
} else {
Text("You don't have any friends")
}
I couldn't work it out yet, although I've been going on and about for the past 2 hours now trying to solve this. Any help would be greatly appreciated. Also sorry if I forgot to add anything.
Load only the data you need:
Use a query:
let queryRef = db.collection("users").whereField("friendsWith", isEqualTo: uid)
and then:
queryRef.getDocuments { snapshot, error in......
Here you can find more about firestore:
https://firebase.google.com/docs/firestore/query-data/queries
You need to make a View that you init with friends_model.friend_list and store it in a let friendList. In that View you need an onChange(of: friendList) and then filter the list and set it on an #State var filteredFriendList. Then in the same view just do your List(filteredFriendList) { friend in
e.g.
struct FiltererdFriendView: View {
let friendList: [Friend] // body runs when this is different from prev init.
#State var filteredFriendList = [Friend]()
// this body will run whenever a new friendList is supplied to init, e.g. after getData was called by a parent View and the parent body runs.
var body: some View {
List(filteredFriendList) { friend in
...
}
.onChange(of: friendList) { fl in
// in your case this will be called every time the body is run but if you took another param to init that changed then body would run but this won't.
filteredFriendList = fl.filter ...
}
}
}
I am building a SwiftUI app for iOS to help me and my classmates keep up with assignments.
The way I am syncing homework across devices is by pushing some JSON with the assignments into GitHub Pages and then the app parses the JSON code into a sleek and simple Section within a Form.
I would like to have the following JSON...
[
{
"subject": "Maths",
"content": "Page 142, exercises 4, 5, 6.",
"dueDate": "15/01/2022"
},
{
"subject": "English",
"content": "Write an essay about your favorite subject. 2 pages at least.",
"dueDate": "18/01/2022"
},
{
"subject": "Chemistry",
"content": "Learn every element from the Periodic Table.",
"dueDate": "16/01/2022"
}
]
... turn into something that looks like this:
The easiest way would be to create about 5 Sections and, if there aren't 5 assignments, leave them empty. This solution didn't work because not having 5 assignments in the JSON file means the function that handles the file would abort because JSONDecoder would return nil when unwrapping the 4th assignment.
I've been struggling for quite a while to find a solution. I tried this:
struct Assignments: Decodable {
let subject: [String]
let content: [String]
let dueDate: [String]
}
struct AssignmentView: View {
#State private var currentNumber = 0
#State private var numberOfAssignments = 0
#State private var subject = [""]
#State private var dueDate = [""]
#State private var content = [""]
func downloadAssignments() {
let assignemtURL = URL(string: "https://example.com/assignments.json")!
var assignmentRequest = URLRequest(url: assignmentURL)
assignmentRequest.httpMethod = "GET"
assignmentRequest.attribution = .developer
NSURLConnection.sendAsynchronousRequest(assignmentRequest, queue: OperationQueue.main) {(response, data, error) in
if error != nil {
print("Could not get assignments. :/")
} else {
guard let data = data else { return }
let assignmentJSON = "\(data)"
let jsonData = assignmentJSON.data(using: .utf8)!
let decodedAssignments: Assignments = try! JSONDecoder().decode(Assignments.self, from: jsonData)
let numberOfAssignments: Int = decodedAssignments.count
// Here comes the problem. How do I create an array / modify an existing array with each individual subject taken in order?
}
}
}
var body: some View {
let _ = downloadAssignments()
VStack {
Form {
Section {
ForEach(0..<numberOfAssignments) {
Text("\(subject[$0]) - Due \(dueDate[$0])")
.font(.system(size: 20, weight: .semibold))
Text("\(content[$0])")
}
}
}
}
}
}
}
How can I get the value of each variable from the JSON file and combine it into an array (example: var subjects = ["Maths", "English", "Chemistry"])?
I've been looking for an answer for weeks with no solution. Some help on this would be highly appreciated!
There are quite a few things to point out in your code. I'm afraid I can't present you a full solution, but these pointers might help you go into the right direction:
The structure of your Assignments model doesn't match the JSON (and is also not really easy to work with in the list). Instead of having a single model which has an array for each property, there should one model object per assignment
struct Assignment: Decodable {
let subject: String
let content: String
let dueDate: String
}
and then you can decode/store an array of those:
let decodedAssignments: [Assignment] = try! JSONDecoder().decode([Assignment].self, from: jsonData)
Triggering the download as a side-effect of the body being evaluated is not a good idea, because this does not happen only once. In fact, whenever you modify any state of the view it would re-trigger the download, basically leaving you in an infinite download-loop. Instead, a better place to start the download would be when the view appears:
var body: some View {
VStack {
// ...
}
.onAppear(perform: downloadAssignments)
}
NSURLConnection is an extreeeemly old API that you don't want to use nowadays (it has been officially deprecated for 6 years now!). Have a look at URLSession instead.
I’m setting up my Settings class which gets/sets values from UserDefaults. I wish for as much of the code to be generic to minimise effort involved whenever a new setting is introduced (I have many as it is, and I expect many more in the future too), thereby reducing the probability of any human errors/bugs.
I came across this answer to create a wrapper for UserDefaults:
struct UserDefaultsManager {
static var userDefaults: UserDefaults = .standard
static func set<T>(_ value: T, forKey: String) where T: Encodable {
if let encoded = try? JSONEncoder().encode(value) {
userDefaults.set(encoded, forKey: forKey)
}
}
static func get<T>(forKey: String) -> T? where T: Decodable {
guard let data = userDefaults.value(forKey: forKey) as? Data,
let decodedData = try? JSONDecoder().decode(T.self, from: data)
else { return nil }
return decodedData
}
}
I’ve created an enum to store all the setting keys:
enum SettingKeys: String {
case TimeFormat = "TimeFormat"
// and many more
}
And each setting has their own enum:
enum TimeFormat: String, Codable {
case ampm = "12"
case mili = "24"
}
In this simplified Settings class example, you can see that when it’s instantiated I initialise the value of every setting defined. I check if its setting key was found in UserDefaults: if yes, I use the value found, otherwise I set it a default and save it for the first time to UserDefaults.
class Settings {
var timeFormat: TimeFormat!
init() {
self.initTimeFormat()
}
func initTimeFormat() {
guard let format: TimeFormat = UserDefaultsManager.get(forKey: SettingKeys.TimeFormat.rawValue) else {
self.setTimeFormat(to: .ampm)
return
}
self.timeFormat = format
}
func setTimeFormat(to format: TimeFormat) {
UserDefaultsManager.set(format, forKey: SettingKeys.TimeFormat.rawValue)
self.timeFormat = format
}
}
This is working fine, and pretty straightforward. However, thinking ahead, this will be tedious (and therefore error-prone) to replicate for every setting in this app (and every other I look to do in the future). Is there a way for the init<name of setting>() and set<name of setting>() to be generalised, whereby I pass it a key for a setting (and nothing more), and it handles everything else?
I’ve identified every setting to have these shared elements:
settings key (e.g. SettingsKey.TimeFormat in my example)
unique type (could be AnyObject, String, Int, Bool etc. e.g. enum TimeFormat in my example)
unique property (e.g. timeFormat in my example)
default value (e.g. TimeFormat.ampm in my example)
Thanks as always!
UPDATE:
This may or may not make a difference, but considering all settings will have a default value, I realised they don’t need to be a non-optional optional but can have the default set at initialisation.
That is, change:
var timeFormat: TimeFormat!
func initTimeFormat() {
guard let format: TimeFormat = UserDefaultsManager.get(forKey: SettingKeys.TimeFormat.rawValue) else {
self.setTimeFormat(to: .ampm)
return
}
self.timeFormat = format
}
To:
var timeFormat: TimeFormat = .ampm
func initTimeFormat() {
guard let format: TimeFormat = UserDefaultsManager.get(forKey: SettingKeys.TimeFormat.rawValue) else {
UserDefaultsManager.set(self.timeFormat, forKey: SettingKeys.TimeFormat.rawValue)
return
}
self.timeFormat = format
}
The setTimeFormat() function is still needed when its value is changed by the user in-app.
Note that your settings don't have to be stored properties. They can be computed properties:
var timeFormat: TimeFormat {
get {
UserDefaultsManager.get(forKey: SettingKeys.TimeFormat.rawValue) ?? .ampm
}
set {
UserDefaultsManager.set(newValue, forKey: SettingKeys.TimeFormat.rawValue)
}
}
You don't have to write as much code this way, when you want to add a new setting. Since you will just be adding a new setting key enum case, and a computed property.
Further more, this computed property can be wrapped into a property wrapper:
#propertyWrapper
struct FromUserDefaults<T: Codable> {
let key: SettingKeys
let defaultValue: T
init(key: SettingKeys, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
UserDefaultsManager.get(forKey: key.rawValue) ?? defaultValue
}
set {
UserDefaultsManager.set(newValue, forKey: key.rawValue)
}
}
}
Usage:
class Settings {
#FromUserDefaults(key: .TimeFormat, defaultValue: .ampm)
var timeFormat: TimeFormat
}
Now to add a new setting, you just need to write a new enum case for the key, and two more lines of code.
#State var documents: [ScanDocument] = []
func loadDocuments() {
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext =
appDelegate.persistentContainer.viewContext
let fetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "ScanDocument")
do {
documents = try managedContext.fetch(fetchRequest) as! [ScanDocument]
print(documents.compactMap({$0.name}))
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
}
In the first view:
.onAppear(){
self.loadDocuments()
}
Now I'm pushing to detail view one single object:
NavigationLink(destination: RenameDocumentView(document: documents[selectedDocumentIndex!]), isActive: $pushActive) {
Text("")
}.hidden()
In RenameDocumentView:
var document: ScanDocument
Also, one function to update the document name:
func renameDocument() {
guard !fileName.isEmpty else {return}
document.name = fileName
try? self.moc.save()
print(fileName)
self.presentationMode.wrappedValue.dismiss()
}
All this code works. This print statement always prints updated value:
print(documents.compactMap({$0.name}))
Here's the list code in main View:
List(documents, id: \.id) { item in
ZStack {
DocumentCell(document: item)
}
}
But where user comes back to previous screen. The list shows old data. If I restart the app it shows new data.
Any help of nudge in a new direction would help.
There is a similar question here: SwiftUI List View not updating after Core Data entity updated in another View, but it's without answers.
NSManagedObject is a reference type so when you change its properties your documents is not changed, so state does not refresh view.
Here is a possible approach to force-refresh List when you comes back
add new state
#State var documents: [ScanDocument] = []
#State private var refreshID = UUID() // can be actually anything, but unique
make List identified by it
List(documents, id: \.id) { item in
ZStack {
DocumentCell(document: item)
}
}.id(refreshID) // << here
change refreshID when come back so forcing List rebuild
NavigationLink(destination: RenameDocumentView(document: documents[selectedDocumentIndex!])
.onDisappear(perform: {self.refreshID = UUID()}),
isActive: $pushActive) {
Text("")
}.hidden()
Alternate: Possible alternate is to make DocumentCell observe document, but code is not provided so it is not clear what's inside. Anyway you can try
struct DocumentCell: View {
#ObservedObject document: ScanDocument
...
}
Change
var document: ScanDocument
to
#ObservedObject var document: ScanDocument
Core Data batch updates do not update the in-memory objects. You have to manually refresh afterwards.
Batch operations bypass the normal Core Data operations and operate directly on the underlying SQLite database (or whatever is backing your persistent store). They do this for benefits of speed but it means they also don't trigger all the stuff you get using normal fetch requests.
You need to do something like shown in Apple's Core Data Batch Programming Guide: Implementing Batch Updates - Updating Your Application After Execution
Original answer
similar case
similar case
let request = NSBatchUpdateRequest(entity: ScanDocument.entity())
request.resultType = .updatedObjectIDsResultType
let result = try viewContext.execute(request) as? NSBatchUpdateResult
let objectIDArray = result?.result as? [NSManagedObjectID]
let changes = [NSUpdatedObjectsKey: objectIDArray]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [managedContext])
An alternative consideration when attempting to provide a solution to this question is relating to type definition and your force down casting of your fetch request results to an array of ScanDocument object (i.e. [ScanDocument]).
Your line of code...
documents = try managedContext.fetch(fetchRequest) as! [ScanDocument]
...is trying to force downcast your var documents to this type - an array of objects.
In fact an NSFetchRequest natively returns an NSFetchRequestResult, but you have already defined what type you are expecting from the var documents.
In similar examples where in my code I define an array of objects, I leave out the force downcast and the try will then attempt to return the NSFetchRequestResult as the already defined array of ScanDocument object.
So this should work...
documents = try managedContext.fetch(fetchRequest)
Also I note you are using SwiftUI List...
Comment No.1
So you could try this...
List(documents, id: \.id) { item in
ZStack {
DocumentCell(document: item)
}
.onChange(of: item) { _ in
loadDocuments()
}
}
(Note: Untested)
But more to the point...
Comment No.2
Is there a reason you are not using the #FetchRequest or #SectionedFetchRequest view builders? Either of these will greatly simplify your code and make life a lot more fun.
For example...
#FetchRequest(entity: ScanDocument.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \.your1stAttributeAsKeyPath, ascending: true),
NSSortDescriptor(keyPath: \.your2ndAttributeAsKeyPath, ascending: true)
] // these are optional and can be replaced with []
) var documents: FetchedResults<ScanDocument>
List(documents, id: \.id) { item in
ZStack {
DocumentCell(document: item)
}
}
and because all Core Data entities in SwiftUI are by default ObservedObjects and also conform to the Identifiable protocol, you could also leave out the id parameter in your List.
For example...
List(documents) { item in
ZStack {
DocumentCell(document: item)
}
}