I need to edit a persisted SwiftData item in an isolated context, because if not is isolated with every change triggers expensive computations.
When the edit is done and I save the temporal context. I expect that the temporal context is merged with the main context and all the views that have a dependency with the edited Item.id be invalidated and updated. But not. When I dismiss the ItemEditView and temporalContext.save() is triggered the ItemNormalView that has a dependency with the Item.id remains not updated so I have to do a non elegant either reliable workaround.
What is the canonical way of do it?
The most relevant code:
struct ItemEditView: View {
@Bindable var temporalItem: Item
let temporalContext: ModelContext
@Binding var updater: Bool
init(itemID: PersistentIdentifier,
container: ModelContainer,
updater: Binding<Bool>){
temporalContext = ModelContext(container)
temporalContext.autosaveEnabled = false
self.temporalItem = temporalContext.model(for: itemID) as! Item
self._updater = updater
}
var body: some View {
TextField("", text: $temporalItem.name)
.onDisappear{
Task{
try! temporalContext.save()
//Workaround:
try? await Task.sleep(nanoseconds:1_000_000_000)
updater.toggle()
}
}
}
}
struct ItemView: View {
let item: Item
@Environment(\.modelContext) private var modelContext
@State private var itemEditorViewModalViewIsPresented = false
@State private var updater = false
var body: some View {
VStack{
ItemNornalView(item: item, updater: $updater)
Button("Show Edit Modal View"){
itemEditorViewModalViewIsPresented.toggle()
}
}
.sheet(isPresented: $itemEditorViewModalViewIsPresented){
ItemEditView(itemID: item.id, container: modelContext.container, updater: $updater)
}
}
}
struct ItemNornalView: View {
let item: Item
@Binding var updater: Bool
var body: some View {
let _ = Self._printChanges()
Text("\(updater)").scaleEffect(0).hidden()
Text("Item name: \(item.name)")
}
}
The rest of the code:
struct ContentView: View {
var body: some View {
TabView{
MetricsView().tabItem { Text("Metrics") }
ItemsList().tabItem { Text("List") }
}
}
}
struct MetricsView: View {
@Query private var items: [Item]
var body: some View {
Chart{
BarMark(x: .value("x", "Average"), y: .value("y", expensiveComputation(items: items)))
}
}
private func expensiveComputation(items: [Item])->Double{
//...
return 1.0
}
}
struct ItemsList: View {
@Query private var items: [Item]
@Environment(\.modelContext) private var modelContext
@State var path = NavigationPath()
var body: some View {
let _ = Self._printChanges()
NavigationStack(path: $path) {
List {
ForEach(items) { item in
Button{
path.append(item)
}label:{
ItemRowView(item: item)
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
.navigationDestination(for: Item.self){ item in
ItemView(item:item)
}
}
}
private func addItem() {
withAnimation {
let newItem = Item()
modelContext.insert(newItem)
try! modelContext.save()
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
for index in offsets {
modelContext.delete(items[index])
}
}
}
}
Post
Replies
Boosts
Views
Activity
Expected behaviour: When I press the button I expect the label change from "Original" to "Edited"
Obtained behaviour: This only happens in scenario A) and B) not C), the desired.
Scenario A: action: update2(item:) and both labels
Scenario B: action: update(itemID:container:) and label: Text(item.name)
Scenario C: action: update(itemID:container:) and label: ItemRow(item:)
import SwiftUI
import SwiftData
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Query private var items: [Item]
var body: some View {
NavigationSplitView {
List {
ForEach(items) { item in
Button(action: {
update(itemID: item.id, container: modelContext.container)
// update2(item: item)
},
label: {
ItemRow(item: item)
// Text(item.name)
})
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
} detail: {
Text("Select an item")
}
}
private func addItem() {
withAnimation {
let newItem = Item(name: "Original")
modelContext.insert(newItem)
//modelContext.save()
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
for index in offsets {
modelContext.delete(items[index])
}
}
}
}
struct ItemRow: View {
let item: Item
var body: some View {
Text(item.name)
}
}
@Model
final class Item {
var name: String
init(name: String) {
self.name = name
}
}
func update(itemID: PersistentIdentifier, container: ModelContainer){
let editModelContext = ModelContext(container)
editModelContext.autosaveEnabled = false
let editModel = editModelContext.model(for: itemID) as! Item
editModel.name = "Edited"
try! editModelContext.save()
}
func update2(item: Item){
item.name = "Edited"
}
I want the line grow without a canvas resize so I need to scale the canvas before the animation.
If I apply both X and Y Scale modifiers to set the domain, animation doesn't work, if I comment either of both, yes... Any idea why and any workaround?
import SwiftUI
import Charts
struct Entry: Identifiable {
var id = UUID()
var time: Double
var value: Double
}
struct ContentView: View {
@State var data: [Entry] = [
.init(time: 0, value: 0),
.init(time: 1, value: 1)]
var body: some View {
VStack{
Button("+"){
data.append(.init(time: 2, value: 3))
}
Chart(data){entry in
LineMark(x: .value("time", entry.time),
y: .value("value", entry.value))
}
.chartXScale(domain: 0...3)
.chartYScale(domain: 0...3)
.padding()
.animation(.easeIn(duration:3), value: data)
}
}
}
This approach works before XCode 15 beta 7:
@Model
final class Item {
var name: String
@Relationship(inverse:\Note.item) var notes: [Note]
init(name: String = "Item name") {
self.name = name
self.notes = []
}
}
@Model final class Note {
var name: String
var item: Item
init(name: String = "Note name", item: Item) {
self.name = name
self.item = item
}
}
@main
struct LifeKPIs_SwiftData_PlaygroundApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Item.self, Note.self])
}
}
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
var body: some View {
//...
let item = Item()
modelContext.insert(item)
//...
let note = Note( item: item) //Error here
}
}
Thread 1: "Illegal attempt to establish a relationship 'item' between objects in different contexts (source = <NSManagedObject: 0x6000021c3020> (entity: Note; id: 0x60000030ef00 x-coredata:///Note/t0C99AC11-423B-4942-9B5D-C9F9E6A4370E7; data: {\n item = nil;\n name = "Note name";\n}) , destination = <NSManagedObject: 0x60000212d400> (entity: Item; id: 0xabdd96b5daee1528 x-coredata://4C4C1274-3F87-425E-837F-F2AEFD751084/Item/p1; data: {\n name = "New Item 0";\n notes = (\n );\n}))"
@Model final class Entry {
var value: Int
var date: Date
var item: Item?
init(value: Int, date: Date) {
self.value = value
self.date = date
}
}
@Model
final class Item {
var name: String
@Relationship(deleteRule:.cascade, inverse: \Entry.item) var entries: [Entry]
init(name: String) {
self.name = name
entries = []
}
}
extension Item {
func checkingDate(in context: ModelContext)->Date{
try! context.fetch(FetchDescriptor<Entry>(predicate:#Predicate<Entry>{$0.item == self})).last!.date
}
}
same if self == self
Why I'm fetching and not directly accessing entries: https://developer.apple.com/forums/thread/735735
If I comment let _ = entries everything goes well, but if not, accessing(not editing) the collection I don't know why forces the view update (dependency change in items) and with the view update the computed property checkingDate is triggered firing the view update... and so on. Cycle
Why accessing entries forces view update ? If I change let _ = entries for let _ = name everything goes well so I conclude that is a problem with collection...
Models:
@Model
final class Item {
var name: String
@Relationship(deleteRule:.cascade) var entries: [Entry]
init(name: String) {
self.name = name
entries = []
}
}
extension Item {
@Transient var checkingDate: Date {
let _ = entries // < -- !!!
//...
return Date()
}
}
@Model final class Entry {
var value: Int
var date: Date
init(value: Int, date: Date) {
self.value = value
self.date = date
}
}
View:
struct ContentView: View {
@Query(sort: \Item.name) private var items: [Item]
@Environment(\.modelContext) private var context
var body: some View {
let _ = Self._printChanges()
List{
ForEach(items){item in
Text("Name:\(item.name), checking date: \(item.checkingDate, format: Date.FormatStyle(date: .numeric, time: .standard))")
}
}
}
}
Is guaranteed that if I run
let item = Item()
container.mainContext.insert(item)
let items = container.mainContext.fetch(FetchDescriptor<Item>())
item is in items ?
I seem to remember that it was not like that in CoreData
With a threadsPerGrid of (10,1,1) and:
[[kernel]] void compute_shader (device atomic_int& incremental [[buffer(0)]],
ushort lid [[thread_position_in_threadgroup]] ){
threadgroup atomic_int local_atomic {0};
atomic_fetch_add_explicit(&local_atomic, 1, memory_order_relaxed);
threadgroup_barrier(mem_flags::mem_threadgroup);
if(lid == 0) {
int local_non_atomic = atomic_load_explicit(&local_atomic, memory_order_relaxed);
atomic_fetch_add_explicit(&incremental, local_non_atomic, memory_order_relaxed);
}
}
I expect:
10
20
30
...
But I get:
1125974026
11259740362000908258
832823256 ...
github.com/quaternionboy/Atomic
Is there any way to cast metal::atomic_intto int in MSLibrary like C++ Standard Library ?
C++:
std::atomicint atom {10};
int num = (int)atom;
MSL:
kernel void compute_shader (device metal::atomic_int& incremental [[buffer(0)]],threadgroup atomic_int& local [[threadgroup(0)]],ushort lid [[thread_position_in_threadgroup]] ){
atomic_fetch_add_explicit(&local, 1, memory_order_relaxed);
threadgroup_barrier(mem_flags::mem_threadgroup);
if(lid == 0) {
atomic_fetch_add_explicit(&incremental, (int)local, memory_order_relaxed);//ERROR
atomic_fetch_add_explicit(&incremental, as_typeint(local), memory_order_relaxed);//ERROR
}
}