My core data attribute for statuses is
because I want to keep the logs of the status changes. So one value won't be sufficient.
when I write data to core data like below
#State var statusIndexEdits : [Int] = []
#State var selectedStatusIndex: Int = 0
.onChange(of: self.selectedStatusIndex) { newValue in
self.editItem(account: account)
}
private func editItem(account: CDAccount) {
viewContext.performAndWait {
if account.statusIndex != selectedStatusIndex {
account.statusIndex = Int64(selectedStatusIndex)
statusIndexEdits.append(selectedStatusIndex)
account.statuses = statusIndexEdits as NSObject
print(account.statuses!)
}
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
My print(account.statuses!) statement prints
statuses = "(\n 4,\n 1,\n 12,\n 1,\n 0,\n 5\n)";
So while trying to convert my account.statuseslike below I can not get Int values from it.
I have tried to reach those status index values with
ForEach(Array(arrayLiteral: account.statuses), id: \.self) { statusIndex in
trial 1
//Text(self.status[statusIndex as! Int])
trial 2
//Text("\(statusIndex ?? 0 as NSObject)")
trial 3
//Text(status[Int("\(statusIndex ?? 0 as NSObject)")!])
}
Btw if you want to know what the heck is status it is
#State var status : [String] = Constants.Status.status
struct Constants {
struct Status {
static let status = ["Introduction", "Demonstration", "Offer submitted", "Waiting for reply", "Accepted", "Declined", "Revise offer", "Contract sent", "Contract signed", "Server setup", "Training", "Integration", "Live", "Invoice"]
}
So according to the status index, I am reaching its status with status[statusIndex]
So long story short how can I convert those core data NSObject to Int. Thus I can use status[statusIndex] easily.
if statuses is [Int]. Just define it as the "Custom Class" in the "Data Model Inspector". It will work like a regular array
Related
I have several apps that use CoreData / iCloud syncing and they all receive a slew of update/change/insert/delete/etc notifications when they start and sometimes as they are running without any changes to the underlying data. When a new item is added or deleted, it appears that I get notifications for everything again. Even the number of notifications are not consistent.
My question is, how do I avoid this? Is there a cut-off that can be applied once I'm sure I have everything up to date on a device by device basis.
Persistence
import Foundation
import UIKit
import CoreData
struct PersistenceController {
let ns = NotificationStuff()
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.stuff = "Stuff"
newItem.timestamp = Date()
}
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "TestCoreDataSync")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
class NotificationStuff
{
var changeCtr = 0
init()
{
NotificationCenter.default.addObserver(self, selector: #selector(self.processUpdate), name: Notification.Name.NSPersistentStoreRemoteChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(contextDidSave(_:)), name: Notification.Name.NSManagedObjectContextDidSave, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(contextObjectsDidChange(_:)), name: Notification.Name.NSManagedObjectContextObjectsDidChange, object: nil)
}
#objc func processUpdate(_ notification: Notification)
{
//print(notification)
DispatchQueue.main.async
{ [self] in
observerSelector(notification)
}
}
#objc func contextObjectsDidChange(_ notification: Notification)
{
DispatchQueue.main.async
{ [self] in
observerSelector(notification)
}
}
#objc func contextDidSave(_ notification: Notification)
{
DispatchQueue.main.async
{
self.observerSelector(notification)
}
}
func observerSelector(_ notification: Notification) {
DispatchQueue.main.async
{ [self] in
if let insertedObjects = notification.userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject>, !insertedObjects.isEmpty
{
print("Insert")
}
if let updatedObjects = notification.userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObject>, !updatedObjects.isEmpty
{
changeCtr = changeCtr + 1
print("Change \(changeCtr)")
}
if let deletedObjects = notification.userInfo?[NSDeletedObjectsKey] as? Set<NSManagedObject>, !deletedObjects.isEmpty
{
print("Delete")
}
if let refreshedObjects = notification.userInfo?[NSRefreshedObjectsKey] as? Set<NSManagedObject>, !refreshedObjects.isEmpty
{
print("Refresh")
}
if let invalidatedObjects = notification.userInfo?[NSInvalidatedObjectsKey] as? Set<NSManagedObject>, !invalidatedObjects.isEmpty
{
print("Invalidate")
}
let mainManagedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
guard let context = notification.object as? NSManagedObjectContext else { return }
// Checks if the parent context is the main one
if context.parent === mainManagedObjectContext
{
// Saves the main context
mainManagedObjectContext.performAndWait
{
do
{
try mainManagedObjectContext.save()
} catch
{
print(error.localizedDescription)
}
}
}
}
}
}
ContentView
import SwiftUI
import CoreData
struct ContentView: View {
#State var stuff = ""
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
VStack
{
TextField("Type here", text: $stuff,onCommit: { addItem(stuff: stuff)
stuff = ""
})
List {
ForEach(items) { item in
Text(item.stuff ?? "??")
}
.onDelete(perform: deleteItems)
}
}.padding()
}
private func addItem(stuff: String) {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
newItem.stuff = stuff
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
The database has an Item entity with a timestamp field and a string field named stuff.
It depends on if it's for examining Production or Debug builds in the system's Console or Xcode's Console respectively.
For Production builds, my understanding is the aim is to make my messages more findable (rather than de-emphasising/hiding other messages) by consistently using something like:
let log = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "YourCategorisationOfMessagesGoingToThisHandle")
and then in the code I might have things like
log.debug("My debug message")
log.warning("My warning etc")
fwiw: I tend to categorise stuff by the file it's in, as that's deterministic and helps me find the file, so my source files tend to start with
fileprivate let log = Logger(subsystem: Bundle.main.bundleIdentifier!, category: #file.components(separatedBy: "/").last ?? "")
If I do this, then I can easily filter the system's console messages to find stuff that's relevant to my app.
There's more on how to use this and the console to filter for the app's messages in the sytem console over here.
For Debug builds and the Xcode console the same consistent app log messages from my app could be used, e.g. my app's debug messages always start with "Some easily findable string or other". I don't believe there is a way to throttle/cut-off responses selectively. But it definitely possible to turn off debug messages from many of the noisy sub-systems completely (once happy that they are working reliably)
For Core Data and CloudKit cases mentioned, if I run the Debug builds with the -com.apple.CoreData.Logging.stderr 0 and -com.apple.CoreData.CloudKitDebug 0 launch args then that make Xcode's console a lot quieter :-). Nice instructions on how to set this up in the SO answer over here
My problem was that CoreData -> CloudKit integration was re-synching the same items over and over, thus the notifications. I discovered that I needed to add a sorted index for the modifiedTimestamp on all entities. Now things are much faster and few if any re-synched items.
I have created a core data entity with the following attributes (from Item+CoreDataProperties.swift file):
extension Item {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Item> {
return NSFetchRequest<Item>(entityName: "Item")
}
#NSManaged public var id: UUID?
#NSManaged public var likes: Int16
}
I am trying to return within ContentView a variable that shows the total number of Item that have more than 5 likes. What would be the best way to do this?
I've tried adding the following computed property, but it only checks whether the first item in the array fulfills the condition and I I'm not sure how to get it to loop over all of them.
#FetchRequest(entity: Item.entity(), sortDescriptors: [])
var item: FetchedResults<Item>
var likesCount:Int {
get {
if item[0].likes > 5 {
return 1
}
else {
return 0
}
}
}
I was also reading about a method called countForFetchRequest, but it seems to only be for detecting errors and I'm not sure it applies here.
Many thanks! Any help greatly appreciated.
Maybe something like this:
var countOfItems: Int {
getCount()
}
and then:
func getCount() -> Int {
var countOfItems: Int = 0
let context = PersistenceController.shared.container.viewContext
let itemFetchRequest: NSFetchRequest<Item> = Item.fetchRequest()
itemFetchRequest.predicate = NSPredicate(format: "likes > %d", 5)
do {
countOfItems = try context.count(for: itemFetchRequest)
print (countOfItems)
}
catch {
print (error)
}
return countOfItems
}
I seem to be caught in a Catch-22 situation. Perhaps my approach is entirely wrong here. I hope someone can help. I want to create a star-based rating display using feedback from users who visit a particular real world landmark.
In CoreData I have an entity called Rating with attributes called rating (Int32) and landmark (String). I want to get the average for all rating(s) associated with a given landmark in order to display stars in the view for each.
Here is the code for the View:
struct TitleImageView: View {
#Environment(\.managedObjectContext) var viewContext : NSManagedObjectContext
let landmark: Landmark
var body: some View {
Image(landmark.imageName)
.resizable()
.shadow(radius: 10 )
.border(Color.white)
.scaledToFit()
.padding([.leading, .trailing], 40)
.layoutPriority(1)
.overlay(TextOverlay(landmark: landmark))
.overlay(RatingsOverlay(rating: stars))
}
}
Here is the fetch (which works as expected when the argument for the fetch is hard coded):
let fetchRequest = Rating.fetchRequestForLandmark(landmark: landmark.name)
var ratings: FetchedResults<Rating> {
fetchRequest.wrappedValue
}
var sum: Int32 {
ratings.map { $0.rating }.reduce(0, +)
}
var stars : Int32 {
sum / Int32(ratings.count)
}
The problem is this: When I insert the fetch before the body of the view, I get the warning
"Cannot use instance member 'landmark' within property initializer; property initializers run before 'self' is available"
When I place the fetch after the body, I get:
"Closure containing a declaration cannot be used with function builder 'ViewBuilder'" (with reference to var ratings)
Is there an easy way out of this conundrum or must I go back to the proverbial drawingboard? Thanks.
How about wrapping the fetch in a calculated property that returns a sum & stars tuple? Slightly different shape with the same results. Calculated properties can reference other properties, so the swift-init hurdle is cleared!
var metrics : (sum: Int, stars: Int) {
let fetchRequest = Rating.fetchRequestForLandmark(landmark: landmark.name)
var ratings: FetchedResults<Rating> {
fetchRequest.wrappedValue
}
let sum = ratings.map { $0.rating }.reduce(0, +)
let stars = sum / ratings.count
return (sum: sum, stars: stars)
}
Thanks to those who offered suggestions. After a long hiatus, I returned to this problem and arrived at this solution (based on a HackingWithSwift tutorial: https://www.hackingwithswift.com/books/ios-swiftui/dynamically-filtering-fetchrequest-with-swiftui)
Here's the code:
struct RatingsView: View {
var fetchRequest: FetchRequest<Rating>
var body: some View {
HStack(spacing: 0.4){
ForEach(1...5) { number in
if number > stars {
//Image(systemName: "star")
} else {
Image(systemName: "star.fill")
}
}
}
}
init(filter: String) {
fetchRequest = FetchRequest<Rating>(entity: Rating.entity() , sortDescriptors: [], predicate: NSPredicate(format: "%K == %#", "landmark" , filter))
}
var sum: Int16 {
fetchRequest.wrappedValue.reduce(0) { $0 + $1.rating }
}
var stars : Int {
var starCount:Int
if fetchRequest.wrappedValue.count > 0 {
starCount = Int(sum) / fetchRequest.wrappedValue.count
} else {
starCount = 0
}
return starCount
}
}
I'm not sure if this is the absolute best solution, but it's working as intended.
Hope this can help others.
I'm trying to pass a FetchResult to another view in order to have all my tables updated at the same time.
My problem:
view1 {
#FetchRequest
ForEach{
NavigationLink(passing fetchRequest.value to View 2)}
}
View2 {
var value1 :fetchRequest.value from view 1
ForEach{
NavigationLink(passing value1.value to View 3)}
}
View3....
Problem here is, if I do a delete or a add on the view 3, the views 1 and 2 won't update until I go back to view 1, and descend again to view 2 and 3.
Do you have an idea on how to have a quick update of these values ?
Best
Tim
I've never seen anyone else try this before but I just lifted the #FetchRequest up into a superview and passed the fetch results (items in this case) as a param down to the subview:
struct ContentView: View {
#State var count = 0
#FetchRequest<Item>(sortDescriptors: [], predicate: nil, animation: nil) var items
var body: some View {
NavigationView {
MasterView(items: items)
.navigationTitle("Master \(count)")
.navigationBarItems(trailing: Button("Increment"){
count += 1
})
}
}
}
struct MasterView: View {
var items : FetchedResults<Item>
var body: some View {
List {
ForEach(items) { item in
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
}
.onDelete(perform: deleteItems)
}
.toolbar {
// #if os(iOS)
// ToolbarItem(placement: .navigation){
// EditButton()
// }
// #endif
//ToolbarItem(placement: .automatic){
ToolbarItem(placement: .bottomBar){
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
ToolbarItem(placement: .bottomBar){
Button(action: {
ascending.toggle()
}) {
Text(ascending ? "Descending" : "Ascending")
}
}
}
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
newItem.name = "Master"
do {
try newItem.validateForInsert()
try viewContext.obtainPermanentIDs(for: [newItem])
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map {items[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
The reason I did this is I used a the launch argument -com.apple.CoreData.SQLDebug 4 and I noticed it was hitting the database every time the state changed and a View was recreated that contained a #FetchRequest which I didn't want.
I'm simply trying to access a record in core data by a property I've named "id" that is of String type. The following keeps complaining about 'Unable to parse the format string "id == 8DF3F2C6741B47C8864D1052C36E2C4D"'. How can I solve this issue?
private func getEntity(id: String) -> NSManagedObject? {
var myEntity: NSManagedObject?
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "MyEntity")
fetchRequest.predicate = NSPredicate(format: "id = \(id)")
do {
var tempArray = try getCurrentManagedContext().fetch(fetchRequest)
myEntity = tempArray.count > 0 ? tempArray[0] : nil
} catch let error as NSError {
print("get failed ... \(error) ... \(error.userInfo)")
}
return myEntity
}
It’s a format string. Instead of
NSPredicate(format: "id = \(id)")
Write
NSPredicate(format: "id == %#", id)