In this app, there is a main screen (WorkoutScreen) that displays the contents of a list one at a time as it iterates through the list (current workout in a list of many). In a popOver, a list that contains all of the workouts appears and has the ability to add, delete or move items in that list.
When I delete the bottom most item, there is no error. When I delete any other item in the list I get this NSRangeException error that crashes the app:
/*
2022-04-24 15:41:21.874306-0400 Trellis
beta[9560:3067012] *** Terminating app due to
uncaught exception 'NSRangeException', reason:
'*** __boundsFail: index 3 beyond bounds [0 ..
2]'
*** First throw call stack:
(0x1809150fc 0x19914fd64 0x180a1e564 0x180a2588c
0x1808c0444 0x1852dcce4 0x1852e1400 0x185424670
0x185423df0 0x185428a40 0x18843e4a0 0x188510458
0x188fd83ec 0x10102f3bc 0x1010500a4 0x188494f4c
0x10102c664 0x10103e0d4 0x18841a944 0x10102be18
0x10103122c 0x18837b8ac 0x188363484 0x18834bb64
0x188371d20 0x1883b88e4 0x1b28fe910 0x1b28fe318
0x1b28fd160 0x18831e780 0x18832f3cc 0x1883f5e34
0x18834206c 0x188345f00 0x182eb0798 0x184613138
0x184605958 0x184619f80 0x184622874 0x1846050b0
0x183266cc0 0x1835015fc 0x183b7d5b0 0x183b7cba0
0x1809370d0 0x180947d90 0x180882098 0x1808878a4
0x18089b468 0x19c42638c 0x18323d088 0x182fbb958
0x1885547a4 0x188483928 0x1884650c0 0x10109a630
0x10109a700 0x1015b9aa4)
libc++abi: terminating with uncaught exception
of type NSException
dyld4 config:
DYLD_LIBRARY_PATH=/usr/lib/system/introspection
DYLD_INSERT_LIBRARIES=/Developer/usr/lib/libBacktrac
eRecording.dylib:/Developer/usr/lib/libMainThreadChecker.dylib:/Developer/Library/Private
Frameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib
*** Terminating app due to uncaught exception 'NSRangeException', reason: '***
__boundsFail: index 3 beyond bounds [0 .. 2]'
terminating with uncaught exception of type NSException
(lldb)
*/
struct WorkoutScreen: View {
#EnvironmentObject var workoutList: CoreDataViewModel //calls it from environment
#StateObject var vm = CoreDataViewModel() //access on the page
#Environment(\.scenePhase) var scenePhase
var body: some View{
//displays the current item in the list
}
}
When I add an item to the list I get the error:
'''
CoreData: warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'FruitEntity' so +entity is unable to disambiguate."
'''
Moving Items without adding or deleting any prior gives me this error upon closing the pop over:
'''
Error saving: Error Domain=NSCocoaErrorDomain Code=133020 "Could not merge changes." UserInfo={conflictList=(
"NSMergeConflict (0x2804f1480) for NSManagedObject (0x28327d900) with objectID '0x9ede5774e26501a4...
'''
Here is the core data and related functions:
class CoreDataViewModel: NSObject, ObservableObject {
private let container: NSPersistentContainer
private let context: NSManagedObjectContext
// Whenever you put your Core Data fetch in a view model, you should use an NSFetchedResultsController.
// This allows you to automatically update your #Published var when your Core Data store changes.
// You must inherit from NSObject to use it.
private let fetchResultsController: NSFetchedResultsController<FruitEntity>
#Published var savedEntities: [FruitEntity] = []
override init() {
container = NSPersistentContainer(name: "FruitsContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
print("ERROR LOADING CORE DATA: \(error)")
}
else {
print("Successfully loaded core data")
}
}
context = container.viewContext
let request = NSFetchRequest<FruitEntity>(entityName: "FruitEntity")
let sort = NSSortDescriptor(keyPath: \FruitEntity.order, ascending: true)
request.sortDescriptors = [sort]
// This initializes the fetchResultsController
fetchResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
// Because you inherit from NSObject, you must call super.init() to properly init the parent class. The order of when
// this is to be called has changed.
super.init()
// Because this is a delegate action, you must set the delegate. Since the view model will respond, we set the delegate to self.
fetchResultsController.delegate = self
fetchFruits()
}
func fetchFruits() {
do {
// Instead of calling container.viewContext.fetch(request) which is static, use fetchResultsController.performFetch()
try fetchResultsController.performFetch()
// Make sure the fetch result is not nil
guard let fruitRequest = fetchResultsController.fetchedObjects else { return }
savedEntities = fruitRequest
// You do not need to let error. error is automatically captured in a do catch.
} catch {
print("Error fetching \(error)")
}
}
func addFruit(text: String, nummSets: Int16, nummWeights: Int16, nummReps: Int16, secOrRepz: Bool, orderNumz: Int64, multilimbz: Bool, countDownz: Int16, repTimez: Int16, restTimez: Int16, circuitz: Bool) {
let newFruit = FruitEntity(context: container.viewContext)
newFruit.name = text
newFruit.numOFSets = nummSets
newFruit.numOFWeight = nummWeights
newFruit.numOFReps = nummReps
newFruit.measure = secOrRepz
newFruit.order = orderNumz
newFruit.multiLimb = multilimbz
newFruit.countDownSec = countDownz
newFruit.timePerRep = repTimez
newFruit.restTime = restTimez
newFruit.circuit = circuitz
saveData()
}
func deleteFunction(indexSet: IndexSet) {
guard let index = indexSet.first else { return }
let entity = savedEntities[index]
container.viewContext.delete(entity)
saveData()
}
func saveData() {
do {
try context.save()
fetchFruits()
} catch let error {
print("Error saving: \(error)")
}
}
}
// This is your delegate extension that handles the updating when your Core Data Store changes.
extension CoreDataViewModel: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller:
NSFetchedResultsController<NSFetchRequestResult>) {
// Essentially, you are redoing just the fetch as the NSFetchedResultsController knows how to fetch from above
guard let fruits = controller.fetchedObjects as? [FruitEntity] else { return }
self.savedEntities = fruits
}
}
Here is the list struct:
struct WorkoutListPopUp: View {
#ObservedObject var vm = CoreDataViewModel()
#EnvironmentObject var listViewModel: ListViewModel
#EnvironmentObject var workoutList: CoreDataViewModel
//Too many #State var to list here
var body: some View {
Button (action: {
//this triggers the bug>
vm.addFruit(text: "Workout name", nummSets: Int16(addSets) ?? 3, nummWeights: Int16(addWeights) ?? 0, nummReps: Int16(addReps) ?? 8, secOrRepz: addSecOrReps, orderNumz: Int64((vm.savedEntities.count)), multilimbz: dualLimbs, countDownz: 10, repTimez: 3, restTimez: 60, circuitz: false)
loadNums()
}, label: {
Image(systemName: "plus")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:20, height: 20)
.foregroundColor(Color.pink.opacity(1.0))
.padding(.top, 0)
})
List(){
ForEach(vm.savedEntities) {entity in
VStack{
EditWorkouts(entity: entity, prescribeMeasure: $prescribeMeasure, addReps: $addReps, measurePrescribed: $measurePrescribed, repTimePicker: $repTimePicker, repz: $repz, restPicker: $restPicker, setz: $setz, ready2Press: $ready2Press, workoutz: $workoutz, weightz: $weightz, setsRemaining: $setsRemaining, workoutNum: $workoutNum, workoutInstructions: $workoutInstructions, multiplelimbs: $multiplelimbs, showAllInfo: $showAllInfo)
//are these onChanges needed if "EditWorkouts" file is saving?
.onChange(of: entity.name) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.numOFSets) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.numOFReps) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.numOFWeight) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.measure) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.order) { text in
vm.saveData()
loadNums()
}
.onChange(of: entity.circuit) { text in
vm.saveData()
loadNums()
}
}
}
.onDelete(perform: vm.deleteFunction)
.onMove(perform: moveItem)
}
}
func loadNums(){
if vm.savedEntities.count > 0 {
workoutz = vm.savedEntities[workoutNum].name ?? "NO Name"
setz = String(vm.savedEntities[workoutNum].numOFSets)
weightz = String(vm.savedEntities[workoutNum].numOFWeight)
repz = String(vm.savedEntities[workoutNum].numOFReps)
multiplelimbs = vm.savedEntities[workoutNum].multiLimb
prescribeMeasure = vm.savedEntities[workoutNum].measure
if setsRemaining == 0 && ((workoutNum + 1) - (Int(vm.savedEntities.count)) == 0) {
workoutInstructions = "Goal: \(repz) \(measurePrescribed)"
}
else {
workoutInstructions = "Goal: \(repz) \(measurePrescribed)"
}
}
else {
workoutz = "Add a Workout 👉"
workoutInstructions = " "
}
}
func moveItem(indexSet: IndexSet, destination: Int) {
let source = indexSet.first!
if destination > source {
var startIndex = source + 1
let endIndex = destination - 1
var startOrder = vm.savedEntities[source].order
while startIndex <= endIndex {
vm.savedEntities[startIndex].order = startOrder
startOrder = startOrder + 1
startIndex = startIndex + 1
}
vm.savedEntities[source].order = startOrder
}
else if destination < source {
var startIndex = destination
let endIndex = source - 1
var startOrder = vm.savedEntities[destination].order + 1
let newOrder = vm.savedEntities[destination].order
while startIndex <= endIndex {
vm.savedEntities[startIndex].order = startOrder
startOrder = startOrder + 1
startIndex = startIndex + 1
}
vm.savedEntities[source].order = newOrder
}
vm.savedEntities[source].circuit = false
vm.saveData()
loadNums()
}
}
This is the EditWorkouts file that the WorkoutPopUp file connects to:
struct EditWorkouts: View {
#EnvironmentObject var workoutList: CoreDataViewModel
#StateObject var vm = CoreDataViewModel()
#EnvironmentObject var listViewModel: ListViewModel
let entity: FruitEntity
//too many #State vars to post
var body: some View {
VStack{
HStack{
//many lines of code for options that alter the respective workout on the list. All are followed by their version of:
//.onChange(of:
//vm.savedEntities[Int(entity.order)].multiLimb) { _ in
//vm.saveData()
//loadNums()"
//}
//-or-
//.onChange(of:vm.savedEntities[Int(entity.order)].circuit) { _ in
//entity.circuit = entity.circuit
//vm.saveData()
//}
}
}
}
}
Picture of CoreData FruitEntity:
Image
Thank you again for your time!!
There are a couple of issues with your code. I suspect one is the sole contributor to the crash, but the other may be contributing as well. First, the most likely culprit. If you use .onDelete(), you can't use id: \.self. The reason is pretty simple: the ForEach can get pretty confused as to which entity is which. .self is often not unique, and it really needs to be if you are deleting and rearranging things in the ForEach(), i.e. .onDelete() and .onMove().
The solution is simple. Whatever you are using in the ForEach should conform to Identifiable. Core Data managed objects all conform to Identifiable, so the fix is easy; remove the `id: .self``:
struct ListView: View {
#StateObject var vm = CoreDataViewModel()
var body: some View {
List {
ForEach(vm.savedEntities) {entity in
Text(entity.name ?? "")
}
.onDelete(perform: vm.deleteFunction)
}
// This just adds a button to create entities.
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
vm.addFruit()
} label: {
Image(systemName: "plus")
}
}
}
}
}
That fix alone will most likely stop the crash. However, I also noticed that you were having issues with your updates in your view. That is because you did not implement an NSFetchedResultsController and NSFetchedResultsControllerDelegate which updates your array when your Core Data store changes. Your view model should look like this:
import SwiftUI
import CoreData
class CoreDataViewModel: NSObject, ObservableObject {
private let container: NSPersistentContainer
private let context: NSManagedObjectContext
// Whenever you put your Core Data fetch in a view model, you should use an NSFetchedResultsController.
// This allows you to automatically update your #Published var when your Core Data store changes.
// You must inherit from NSObject to use it.
private let fetchResultsController: NSFetchedResultsController<FruitEntity>
#Published var savedEntities: [FruitEntity] = []
override init() {
container = NSPersistentContainer(name: "FruitsContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
print("ERROR LOADING CORE DATA: \(error)")
}
else {
print("Successfully loaded core data")
}
}
context = container.viewContext
let request = NSFetchRequest<FruitEntity>(entityName: "FruitEntity")
let sort = NSSortDescriptor(keyPath: \FruitEntity.order, ascending: true)
request.sortDescriptors = [sort]
// This initializes the fetchResultsController
fetchResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
// Because you inherit from NSObject, you must call super.init() to properly init the parent class. The order of when
// this is to be called has changed.
super.init()
// Because this is a delegate action, you must set the delegate. Since the view model will respond, we set the delegate to self.
fetchResultsController.delegate = self
// Renamed function to conform to naming conventions. You should use an active verb like fetch to start the name.
fetchFruits()
}
func fetchFruits() {
do {
// Instead of calling container.viewContext.fetch(request) which is static, use fetchResultsController.performFetch()
try fetchResultsController.performFetch()
// Make sure the fetch result is not nil
guard let fruitRequest = fetchResultsController.fetchedObjects else { return }
savedEntities = fruitRequest
// You do not need to let error. error is automatically captured in a do catch.
} catch {
print("Error fetching \(error)")
}
}
// This is just to be able to add some data to test.
func addFruit() {
var dateFormatter: DateFormatter {
let df = DateFormatter()
df.dateStyle = .short
return df
}
let fruit = FruitEntity(context: context)
fruit.name = dateFormatter.string(from: Date())
fruit.measure = false
fruit.numOfReps = 0
fruit.numOfSets = 0
fruit.numOfWeight = 0
fruit.order = 0
saveData()
}
func deleteFunction(indexSet: IndexSet) {
guard let index = indexSet.first else { return }
let entity = savedEntities[index]
container.viewContext.delete(entity)
saveData()
}
func saveData() {
do {
try context.save()
} catch let error {
print("Error saving: \(error)")
}
}
}
// This is your delegate extension that handles the updating when your Core Data Store changes.
extension CoreDataViewModel: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
// Essentially, you are redoing just the fetch as the NSFetchedResultsController knows how to fetch from above
guard let fruits = controller.fetchedObjects as? [FruitEntity] else { return }
self.savedEntities = fruits
}
}
You will notice refreshID no longer exists in the view. It updates without it. Also, please note that by incorporating the data store init into your view model, you can't expand it to have other entities with other views. Each will have a different context and they will crash the app. You are better off having a controller class that creates a singleton for the Core Data store, such as what Apple gives you in the default set up.
In the end, I think you issue was a combination of using id: .self which is known to crash with .onDelete() AND the fact that you were using refreshID not NSFetchedResultsController to update the List.
Related
The following code effectively uses the #StateObject and the #ObservedObject wrappers to capture changes from an ObservableObject view model. Everything is working as expected, except that I would like to be able to display dummy data in Canvas for designing purposes. As you can see from the code below, I'm able to create a Car object inMemory and effectively retrieve it in the Preview struct, my issue is when trying to display multiple cars, as you can see, to display multiple cars I had to duplicate the code for the List in the Preview struct for the CarsView. In the code below it doesn't seem like a big deal because I removed a lot of the code that makes the List but in my production code, I have a lot of code to customize the rows.
Is this really the best way to display data usually managed by a ViewModel?
Is there a way to display data without having to duplicate a lot of the SwiftUI code?
SwiftUI Views
Parent View / Content View
struct ContentView: View {
#StateObject var coreDataViewModel = CoreDataViewModel()
var body: some View {
TabView(selection: 1){
CarsView(coreDataViewModel: coreDataViewModel)
.tabItem {
Text("Cars")
}.tag(1)
// other tabs...
}
}
}
Second View / Cars View
My issue is that here, I'm duplicating the code from the main view in the Preview struct.
struct CarsView: View {
#ObservedObject var coreDataViewModel:CoreDataViewModel
var body: some View {
List {
ForEach(coreDataViewModel.cars) { car in
// custom row to display cars
}
}
}
}
struct CarsView_Previews: PreviewProvider {
#Environment(\.managedObjectContext) private var viewContext
static var previews: some View {
CarsView(coreDataViewModel: CoreDataViewModel())
let context = CoreDataManager.preview.container.viewContext
let requestCar: NSFetchRequest<Car> = Car.fetchRequest()
let fetchedCar = (try! context.fetch(requestCar).first)!
let cars = [fetchedCar]
// repeated code
List{
ForEach(cars) { car in
Text(car.make ?? "")
}
}
}
}
Core Data
Entities and Attributes
Car
make: String
model: String
Core Data Manager
class CoreDataManager{
static let instance = CoreDataManager()
static var preview: CoreDataManager = {
let result = CoreDataManager(inMemory: true)
let viewContext = result.container.viewContext
// new car
let car = Car(context: viewContext)
car.make = "Ford"
car.model = "Mustang"
do {
try viewContext.save()
} catch {
}
return result
}()
let container: NSPersistentContainer
let context: NSManagedObjectContext
init(inMemory: Bool = false){
container = NSPersistentContainer(name: "CoreDataContainer")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { (description, error) in
if let error = error{
print("Error loading Core Data. \(error)")
}
}
context = container.viewContext
}
func save(){
do{
try context.save()
print("Saved successfully!")
}catch let error{
print("Error saving Core Data. \(error.localizedDescription)")
}
}
}
Core Data View Model
class CoreDataViewModel: ObservableObject{
let manager = CoreDataManager.instance
#Published var cars: [Car] = []
init(){
getCars()
}
// addCar, deleteCar, updateCar etc. methods...
func getCars(){
let request = NSFetchRequest<Car>(entityName: "Car")
let sort = NSSortDescriptor(keyPath: \Car.model, ascending: true)
request.sortDescriptors = [sort]
do{
cars = try manager.context.fetch(request)
}catch let error{
print("Error fetching businesses. \(error.localizedDescription)")
}
}
func save(){
self.manager.save()
}
}
The point of a preview is to use the ordinary view with some given data and not to duplicate the code from the actual view. Apart from that you seem to have much in place.
I would inject the Core Data manager class into the view model to start with:
class CoreDataViewModel: ObservableObject{
let manager: CoreDataManager
#Published var cars: [Car] = []
init(coreDataManager: CoreDataManager = .instance){
self.manager = coreDataManager
getCars()
}
Then I would simplify the preview to
struct CarsView_Previews: PreviewProvider {
static var previews: some View {
CarsView(coreDataViewModel: CoreDataViewModel(coreDataManager: .preview))
}
}
You are already creating one Car object in the preview so why not do that in a loop, here I have moved it into a separate function that can be called from inside the preview declaration or separately
#if DEBUG
extension CoreDataManager {
func createMockCars() {
for i in 1...5 {
let car = Car(context: self.context)
car.make = "Make \(i)"
car.model = "Model \(i)"
}
try! self.context.save()
}
}
#endif
As mentioned in one of the comments you should consider renaming your view model to something to do with cars like CarsViewModel or CarListViewModel.
I want to implement a Text field that displays the current user's existing score in the DB (Firestore). Because of the nature of async in Firebase query, I also need to do some adjustment in my codes. However, it seems that completion() handler does not work well:
// ViewModel.swift
import Foundation
import Firebase
import FirebaseFirestore
class UserViewModel: ObservableObject {
let current_user_id = Auth.auth().currentUser!.uid
private var db = Firestore.firestore()
#Published var xp:Int?
func fetchData(completion: #escaping () -> Void) {
let docRef = db.collection("users").document(current_user_id)
docRef.getDocument { snapshot, error in
print(error ?? "No error.")
self.xp = 0
guard let snapshot = snapshot else {
completion()
return
}
self.xp = (snapshot.data()!["xp"] as! Int)
completion()
}
}
}
// View.swift
import SwiftUI
import CoreData
import Firebase
{
#ObservedObject private var users = UserViewModel()
var body: some View {
VStack {
HStack {
// ...
Text("xp: \(users.xp ?? 0)")
// Text("xp: 1500")
.fontWeight(.bold)
.padding(.horizontal)
.foregroundColor(Color.white)
.background(Color("Black"))
.clipShape(CustomCorner(corners: [.bottomLeft, .bottomRight, .topRight, .topLeft], size: 3))
.padding(.trailing)
}
.padding(.top)
.onAppear() {
self.users.fetchData()
}
// ...
}
}
My result kept showing 0 in Text("xp: \(users.xp ?? 0)"), which represents that the step is yet to be async'ed. So what can I do to resolve it?
I would first check to make sure the data is valid in the Firestore console before debugging further. That said, you can do away with the completion handler if you're using observable objects and you should unwrap the data safely. Errors can always happen over network calls so always safely unwrap anything that comes across them. Also, make use of the idiomatic get() method in the Firestore API, it makes code easier to read.
That also said, the problem is your call to fetch data manually in the horizontal stack's onAppear method. This pattern can produce unsavory results in SwiftUI, so simply remove the call to manually fetch data in the view and perform it automatically in the view model's initializer.
class UserViewModel: ObservableObject {
#Published var xp: Int?
init() {
guard let uid = Auth.auth().currentUser?.uid else {
return
}
let docRef = Firestore.firestore().collection("users").document(uid)
docRef.getDocument { (snapshot, error) in
if let doc = snapshot,
let xp = doc.get("xp") as? Int {
self.xp = xp
} else if let error = error {
print(error)
}
}
}
}
struct ContentView: View {
#ObservedObject var users = UserViewModel()
var body: some View {
VStack {
HStack {
Text("xp: \(users.xp ?? 0)")
}
}
}
}
SwiftUI View - viewDidLoad()? is the problem you ultimately want to solve.
I know its a really simple question but I'm just stuck on it atm so any advice would be greatly appreciated as I am new to SwiftUI.
I am trying to download text from firebase and render it to the view but I keep getting an out of range error:
Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
The code is as follows:
var body: some View{
ZStack {
if fetch.loading == false {
LoadingView()
}
else{
Text(names[0])
.bold()
}
}
.onAppear {
self.fetch.longTask()
}
}
Here is the Fetch Content Page:
#Published var loading = false
func longTask() {
DispatchQueue.main.asyncAfter(deadline: .now()) {
let db = Firestore.firestore()
db.collection("Flipside").getDocuments { (snapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
return
} else {
for document in snapshot!.documents {
let name = document.get("Name") as! String
let description = document.get("Description") as! String
//name = items[doc]
print("Names: ", name)
print("Descriptions: ", description)
names.append(name)
descriptions.append(description)
}
}
}
self.loading = true
}
}
So basically when the view appears, get the data from Firebase when the data has downloaded display the menuPage() until then show the Loading Data text.
Any help is welcome!
As Rob Napier mentioned, the issue is that you're accessing the array index before the array is populated.
I'd suggest a couple of improvements to your code. Also, instead of maintaining separate arrays (names, descriptions, ...) you can create a struct to hold all the properties in one place. This will allow you to use just one array for your items.
struct Item {
let name: String
let description: String
}
class Fetch: ObservableObject {
#Published var items: [Item] = [] // a single array to hold your items, empty at the beginning
#Published var loading = false // indicates whether loading is in progress
func longTask() {
loading = true // start fetching, set to true
let db = Firestore.firestore()
db.collection("Flipside").getDocuments { snapshot, err in
if let err = err {
print("Error getting documents: \(err)")
DispatchQueue.main.async {
self.loading = false // loading finished
}
} else {
let items = snapshot!.documents.map { document in // use `map` to replace `snapshot!.documents` with an array of `Item` objects
let name = document.get("Name") as! String
let description = document.get("Description") as! String
print("Names: ", name)
print("Descriptions: ", description)
return Item(name: name, description: description)
}
DispatchQueue.main.async { // perform assignments on the main thread
self.items = items
self.loading = false // loading finished
}
}
}
}
}
struct ContentView: View {
#StateObject private var fetch = Fetch() // use `#StateObject` in iOS 14+
var body: some View {
ZStack {
if fetch.loading { // when items are being loaded, display `LoadingView`
LoadingView()
} else if fetch.items.isEmpty { // if items are loaded empty or there was an error
Text("No items")
} else { // items are loaded and there's at least one item
Text(fetch.items[0].name)
.bold()
}
}
.onAppear {
self.fetch.longTask()
}
}
}
Note that accessing arrays by subscript may not be needed. Your code can still fail if there's only one item and you try to access items[1].
Instead you can probably use first to access the first element:
ZStack {
if fetch.loading {
LoadingView()
} else if let item = fetch.items.first {
Text(item.name)
.bold()
} else {
Text("Items are empty")
}
}
or use a ForEach to display all the items:
ZStack {
if fetch.loading {
LoadingView()
} else if fetch.items.isEmpty {
Text("Items are empty")
} else {
VStack {
ForEach(fetch.items, id: \.name) { item in
Text(item.name)
.bold()
}
}
}
}
Also, if possible, avoid force unwrapping optionals. The code snapshot!.documents will terminate your app if snapshot == nil. Many useful solutions are presented in this answer:
What does “Fatal error: Unexpectedly found nil while unwrapping an Optional value” mean?
The basic issue is that you're evaluating names[0] before the names array has been filled in. If the Array is empty, then you would see this crash. What you likely want is something like:
Item(title: names.first ?? "", ...)
The reason you're evaluating names[0] too soon is that you call completed before the fetch actually completes. You're calling it synchronously with the initial method call.
That said, you always must consider the case where there are connection errors or or the data is empty or the data is corrupt. As a rule, you should avoid subscripting Arrays (preferring things like .first), and when you do subscript Arrays, you must first make sure that you know how many elements there are.
I have a struct for my model and it needs to conform to a protocol that is NSObject only.
I am looking for a viable alternative to converting the model to a class. The requirements are:
Keeping the model a value type
Updating the model when photoLibraryDidChange is called
This would be the ideal implementation if PHPhotoLibraryChangeObserver would not require the implementation to be an NSObject
struct Model: PHPhotoLibraryChangeObserver {
var images:[UIImages] = []
fileprivate var allPhotos:PHFetchResult<PHAsset>?
mutating func photoLibraryDidChange(_ changeInstance: PHChange) {
let changeResults = changeInstance.changeDetails(for: allPhotos)
allPhotos = changeResults?.fetchResultAfterChanges
updateImages()
}
mutating func updateImages() {
// update self.images
...
}
}
I cannot pass the model to an external class implementing the observer protocol as then all the changes happen on the copy (its a value type...)
Any ideas? Best practices?
EDIT: Reformulated the question
EDIT 2: Progress
I have implemented a delegate as a reference type var of my model and pushed the data inside. Now photoLibraryDidChangeis not being called anymore.
This is the stripped down implementation:
class PhotoKitAdapter:NSObject, PHPhotoLibraryChangeObserver {
var allPhotos: PHFetchResult<PHAsset>?
var images:[UIImage] = []
override init(){
super.init()
}
func photoLibraryDidChange(_ changeInstance: PHChange) {
DispatchQueue.main.async {
if let changeResults = changeInstance.changeDetails(for: self.allPhotos!) {
self.allPhotos = changeResults.fetchResultAfterChanges
//this neve gets executed. It used to provide high quality images
self.updateImages()
}
}
}
func startFetching(){
let allPhotosOptions = PHFetchOptions()
allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
allPhotos = PHAsset.fetchAssets(with: allPhotosOptions)
PHPhotoLibrary.shared().register(self)
//this gets executed and fetches the thumbnails
self.updateImages()
}
fileprivate func appendImage(_ p: PHAsset) {
let pm = PHImageManager.default()
if p.mediaType == .image {
pm.requestImage(for: p, targetSize: CGSize(width: 1024, height: 768), contentMode: .default, options: nil){
image, _ in
if let im = image {
self.images.append(im)
}
}
}
}
fileprivate func updateImages() {
self.images = []
if let ap = allPhotos {
for index in 0..<min(ap.count, 10) {
let p = ap[index]
appendImage(p)
}
}
}
}
struct Model {
private var pkAdapter = PhotoKitAdapter()
var images:[UIImage] {
pkAdapter.images
}
func startFetching(){
pkAdapter.startFetching()
}
// example model implementation
mutating func select(_ image:UIImage){
// read directly from pkAdapter.images and change other local variables
}
}
I have put a breakpoint in photoLibraryDidChange and it just does not go there. I also checked that pkAdapter is always the same object and does not get reinitialised on "copy on change".
**EDIT: adding the model view **
This is the relevant part of the modelview responsible for the model management
class ModelView:ObservableObject {
#Published var model = Model()
init() {
self.model.startFetching()
}
var images:[UIImage] {
self.model.images
}
...
}
EDIT: solved the update problem
It was a bug in the simulator ... on a real device it works
I ended up with 2 possible designs:
decouple the PhotoKit interface completely from the model, let the modelview manage both and connect them, as the model view has access to the model instance.
create a PhotoKit interface as var of the model and push the mutable data that is generated by the PhotoKit interface inside it, so it can be changed with an escaping closure. The model is never called from the interface but just exposes the data inside the PhotoKit through a computer property.
I will show two sample implementation below. They are naive in many respect, they ignore performance problems by refreshing all pictures every time the PhotoLibrary is updated. Implementing proper delta updates (and other optimisation) would just clutter up the code and offer no extra insight on the solution to the original problem.
Decouple the PhotoKit interface
ModelView
class ModelView:ObservableObject {
var pkSubscription:AnyCancellable?
private var pkAdapter = PhotoKitAdapter()
#Published var model = Model()
init() {
pkSubscription = self.pkAdapter.objectWillChange.sink{ _ in
DispatchQueue.main.async {
self.model.reset()
for img in self.pkAdapter.images {
self.model.append(uiimage: img)
}
}
}
self.pkAdapter.startFetching()
}
}
Model
struct Model {
private(set) var images:[UIImage] = []
mutating func append(uiimage:UIImage){
images.append(uiimage)
}
mutating func reset(){
images = []
}
}
PhotoKit interface
class PhotoKitAdapter:NSObject, PHPhotoLibraryChangeObserver, ObservableObject {
var allPhotos: PHFetchResult<PHAsset>?
var images:[UIImage] = []
func photoLibraryDidChange(_ changeInstance: PHChange) {
DispatchQueue.main.async {
if let changeResults = changeInstance.changeDetails(for: self.allPhotos!) {
self.allPhotos = changeResults.fetchResultAfterChanges
self.updateImages()
self.objectWillChange.send()
}
}
}
func startFetching(){
PHPhotoLibrary.requestAuthorization{status in
if status == .authorized {
let allPhotosOptions = PHFetchOptions()
allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
self.allPhotos = PHAsset.fetchAssets(with: allPhotosOptions)
PHPhotoLibrary.shared().register(self)
self.updateImages()
}
}
}
fileprivate func appendImage(_ p: PHAsset) {
// This actually appends multiple copies of the image because
// it gets called multiple times for the same asset.
// Proper tracking of the asset needs to be implemented
let pm = PHImageManager.default()
if p.mediaType == .image {
pm.requestImage(for: p, targetSize: CGSize(width: 1024, height: 768), contentMode: .default, options: nil){
image, _ in
if let im = image {
self.images.append(im)
self.objectWillChange.send()
}
}
}
}
fileprivate func updateImages() {
self.images = []
if let ap = allPhotos {
for index in 0..<min(ap.count, 10) {
let p = ap[index]
appendImage(p)
}
}
}
PhotoKit interface as property of the model
ModelView
class ModelView:ObservableObject {
#Published var model = Model()
}
Model
struct Model {
private var pkAdapter = PhotoKitAdapter()
var images:[UIImage] { pkAdapter.images }
}
PhotoKit interface
class PhotoKitAdapter:NSObject, PHPhotoLibraryChangeObserver{
var allPhotos: PHFetchResult<PHAsset>?
var images:[UIImage] = []
override init(){
super.init()
startFetching()
}
// the rest of the implementation is the same as before
...
A Core Data model with entity Node having name, createdAt, to-many relationship children and to-one relationship parent (both optional). Using CodeGen Class Definition.
Using a #FetchRequest with a predicate of parent == nil, it's possible to grab the root nodes and subsequently walk the tree using the relationships.
Root nodes CRUD refreshes the view fine, but any modifications to child nodes don't display until restart although changes are saved in Core Data.
Simplest possible example in the code below illustrates the problem with child node deletion. The deletion works in Core Data but the view does not refresh if the deletion is on a child. The view refresh works fine if on a root node.
I'm new to Swift, so my apologies if this is a rather elementary question, but how can the view be refreshed upon changes to the child nodes?
import SwiftUI
import CoreData
extension Node {
class func count() -> Int {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let fetchRequest: NSFetchRequest<Node> = Node.fetchRequest()
do {
let count = try context.count(for: fetchRequest)
print("found nodes: \(count)")
return count
} catch let error as NSError {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
}
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: Node.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Node.createdAt, ascending: true)], predicate: NSPredicate(format: "parent == nil"))
var nodes: FetchedResults<Node>
var body: some View {
NavigationView {
List {
NodeWalkerView(nodes: Array(nodes.map { $0 as Node }) )
}
.navigationBarItems(trailing: EditButton())
}
.onAppear(perform: { self.loadData() } )
}
func loadData() {
if Node.count() == 0 {
for i in 0...3 {
let node = Node(context: self.managedObjectContext)
node.name = "Node \(i)"
for j in 0...2 {
let child = Node(context: self.managedObjectContext)
child.name = "Child \(i).\(j)"
node.addToChildren(child)
for k in 0...2 {
let subchild = Node(context: self.managedObjectContext)
subchild.name = "Subchild \(i).\(j).\(k)"
child.addToChildren(subchild)
}
}
}
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
}
}
}
struct NodeWalkerView: View {
#Environment(\.managedObjectContext) var managedObjectContext
var nodes: [Node]
var body: some View {
ForEach( self.nodes, id: \.self ) { node in
NodeListWalkerCellView(node: node)
}
.onDelete { (indexSet) in
let nodeToDelete = self.nodes[indexSet.first!]
self.managedObjectContext.delete(nodeToDelete)
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
}
}
}
struct NodeListWalkerCellView: View {
#ObservedObject var node: Node
var body: some View {
Section {
Text("\(node.name ?? "")")
if node.children!.count > 0 {
NodeWalkerView(nodes: node.children?.allObjects as! [Node] )
.padding(.leading, 30)
}
}
}
}
EDIT:
A trivial but unsatisfying solution is to make NodeListWakerCellView retrieve the children using another #FetchRequest but this feels wrong since the object is already available. Why run another query? But perhaps this is currently the only way to attach the publishing features?
I am wondering if there's another way to use a Combine publisher directly to the children, perhaps within the .map?
struct NodeListWalkerCellView: View {
#ObservedObject var node: Node
#FetchRequest var children: FetchedResults<Node>
init( node: Node ) {
self.node = node
self._children = FetchRequest(
entity: Node.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Node.createdAt, ascending: false)],
predicate: NSPredicate(format: "%K == %#", #keyPath(Node.parent), node)
)
}
var body: some View {
Section {
Text("\(node.name ?? "")")
if node.children!.count > 0 {
NodeWalkerView(nodes: children.map({ $0 as Node }) )
.padding(.leading, 30)
}
}
}
}
You can easily observe all the changes by observing the NSManagedObjectContextObjectsDidChange Notification and refreshing the View.
In the code below you can replicate the issue as follows.
Create a Node entity with the following attributes and relationships
2. Paste the code below into the project
Run the NodeContentView on simulator
Select one of the nodes in the first screen
Edit the node's name
Click on the "Back" button
Notice the name of the selected variable didn't change.
How to "solve"
Uncomment //NotificationCenter.default.addObserver(self, selector: #selector(refreshView), name: Notification.Name.NSManagedObjectContextObjectsDidChange, object: nil) that is located in the init of CoreDataPersistence
Follow steps 3-6
Notice that the node name was updated this time.
import SwiftUI
import CoreData
extension Node {
public override func awakeFromInsert() {
super.awakeFromInsert()
self.createdAt = Date()
}
}
///Notice the superclass the code is below
class NodePersistence: CoreDataPersistence{
func loadSampleData() {
if NodeCount() == 0 {
for i in 0...3 {
let node: Node = create()
node.name = "Node \(i)"
for j in 0...2 {
let child: Node = create()
child.name = "Child \(i).\(j)"
node.addToChildren(child)
for k in 0...2 {
let subchild: Node = create()
subchild.name = "Subchild \(i).\(j).\(k)"
child.addToChildren(subchild)
}
}
}
save()
}
}
func NodeCount() -> Int {
let fetchRequest: NSFetchRequest<Node> = Node.fetchRequest()
do {
let count = try context.count(for: fetchRequest)
print("found nodes: \(count)")
return count
} catch let error as NSError {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
}
struct NodeContentView: View {
//Create the class that sets the appropriate context
#StateObject var nodePers: NodePersistence = .init()
var body: some View{
NodeListView()
//Pass the modified context
.environment(\.managedObjectContext, nodePers.context)
.environmentObject(nodePers)
}
}
struct NodeListView: View {
#EnvironmentObject var nodePers: NodePersistence
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Node.createdAt, ascending: true)], predicate: NSPredicate(format: "parent == nil"))
var nodes: FetchedResults<Node>
var body: some View {
NavigationView {
List {
NodeWalkerView(nodes: Array(nodes))
}
.navigationBarItems(trailing: EditButton())
.navigationTitle("select a node")
}
.onAppear(perform: { nodePers.loadSampleData()} )
}
}
struct NodeWalkerView: View {
#EnvironmentObject var nodePers: NodePersistence
//This breaks observation, it has no SwiftUI wrapper
var nodes: [Node]
var body: some View {
Text(nodes.count.description)
ForEach(nodes, id: \.objectID ) { node in
NavigationLink(node.name.bound, destination: {
NodeListWalkerCellView(node: node)
})
}
.onDelete { (indexSet) in
for idx in indexSet{
nodePers.delete(nodes[idx])
}
}
}
}
struct NodeListWalkerCellView: View {
#EnvironmentObject var nodePers: NodePersistence
#ObservedObject var node: Node
var body: some View {
Section {
//added
TextField("name",text: $node.name.bound) //<---Edit HERE
.textFieldStyle(.roundedBorder)
if node.children?.allObjects.count ?? -1 > 0{
NavigationLink(node.name.bound, destination: {
NodeWalkerView(nodes: node.children?.allObjects.typeArray() ?? [])
.padding(.leading, 30)
})
}else{
Text("empty has no children")
}
}.navigationTitle("Edit name on this screen")
}
}
extension Array where Element: Any{
func typeArray<T: Any>() -> [T]{
self as? [T] ?? []
}
}
struct NodeContentView_Previews: PreviewProvider {
static var previews: some View {
NodeContentView()
}
}
extension Optional where Wrapped == String {
var _bound: String? {
get {
return self
}
set {
self = newValue
}
}
var bound: String {
get {
return _bound ?? ""
}
set {
_bound = newValue
}
}
}
///Generic CoreData Helper not needed just to make stuff easy.
class CoreDataPersistence: ObservableObject{
//Use preview context in canvas/preview
//The setup for this is in XCode when you create a new project
let context = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" ? PersistenceController.preview.container.viewContext : PersistenceController.shared.container.viewContext
init(){
//Observe all the changes in the context, then refresh the View that observes this using #StateObject, #ObservedObject or #EnvironmentObject
//There are other options, like NSPersistentStoreCoordinatorStoresDidChange for the coordinator
//https://developer.apple.com/documentation/foundation/nsnotification/name/1506884-nsmanagedobjectcontextobjectsdid
//NotificationCenter.default.addObserver(self, selector: #selector(refreshView), name: Notification.Name.NSManagedObjectContextObjectsDidChange, object: nil)
}
///Creates an NSManagedObject of any type
func create<T: NSManagedObject>() -> T{
T(context: context)
//Can set any defaults in awakeFromInsert() in an extension for the Entity
//or override this method using the specific type
}
///Updates an NSManagedObject of any type
func update<T: NSManagedObject>(_ obj: T){
//Make any changes like a last modified variable
save()
}
///Creates a sample
func addSample<T: NSManagedObject>() -> T{
return create()
}
///Deletes an NSManagedObject of any type
func delete(_ obj: NSManagedObject){
context.delete(obj)
save()
}
func resetStore(){
context.rollback()
save()
}
internal func save(){
do{
try context.save()
}catch{
print(error)
}
}
#objc
func refreshView(){
objectWillChange.send()
}
}
CoreDataPersistence is a generic class that can be used with any entity. Just copy it into you project and you can use it as a superclass for your own CoreData ViewModels or use it as is if you don't have anything to override or add.
The key part of the solution is the line that is uncommented, and the selector that tells the View to reload. Everything else is extra
The code seems like a lot because this is what was provided by the OP but the solution is contained in CoreDataPersistence. Notice the NodeContentView too the context should match the #FetchRequest with theCoreDataPersistence
Option 2
For this specific use case (children are of the same type as the parent) you can use List with children in the init it simplifies a lot of the setup and updating issues are greatly reduced.
extension Node {
public override func awakeFromInsert() {
super.awakeFromInsert()
self.createdAt = Date()
}
#objc
var typedChildren: [Node]?{
self.children?.allObjects.typeArray()
}
}
struct NodeListView: View {
#EnvironmentObject var nodePers: NodePersistence
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Node.createdAt, ascending: true)], predicate: NSPredicate(format: "parent == nil"))
var nodes: FetchedResults<Node>
var body: some View {
NavigationView {
List(Array(nodes) as [Node], children: \.typedChildren){node in
NodeListWalkerCellView(node: node)
}
.navigationBarItems(trailing: EditButton())
.navigationTitle("select a node")
}
.onAppear(perform: { nodePers.loadSampleData()} )
}
}
struct NodeListWalkerCellView: View {
#EnvironmentObject var nodePers: NodePersistence
#ObservedObject var node: Node
var body: some View {
HStack {
//added
TextField("name",text: $node.name.bound)
.textFieldStyle(.roundedBorder)
Button("delete", role: .destructive, action: {
nodePers.delete(node)
})
}.navigationTitle("Edit name on this screen")
}
}