Refresh a view when the children of an object are changed in SwiftUI - core-data

I am working on a CoreData application with two entities MyList and MyListItem. MyList can have many MyListItem (one to many). When the app is launched, I can see all the lists. I can tap on a list to go to the list items. On that screen, I tap a button to add an item to the selected list. After, adding the item when I go back to the all lists screen I cannot see the number of items reflected in the count. The reason is that MyListsView is not rendered again since the number of lists have not changed.
The complete code is shown below:
import SwiftUI
import CoreData
extension MyList {
static var all: NSFetchRequest<MyList> {
let request = MyList.fetchRequest()
request.sortDescriptors = []
return request
}
}
struct DetailView: View {
#Environment(\.managedObjectContext) var viewContext
let myList: MyList
var body: some View {
VStack {
Text("Detail View")
Button("Add List Item") {
let myListP = viewContext.object(with: myList.objectID) as! MyList
let myListItem = MyListItem(context: viewContext)
myListItem.name = randomString()
myListItem.myList = myListP
try? viewContext.save()
}
}
}
func randomString(length: Int = 8) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
class ViewModel: NSObject, ObservableObject {
#Published var myLists: [MyList] = []
private var fetchedResultsController: NSFetchedResultsController<MyList>
private(set) var context: NSManagedObjectContext
override init() {
self.context = CoreDataManager.shared.context
fetchedResultsController = NSFetchedResultsController(fetchRequest: MyList.all, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
super.init()
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch()
guard let myLists = fetchedResultsController.fetchedObjects else { return }
self.myLists = myLists
} catch {
print(error)
}
}
}
extension ViewModel: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let myLists = controller.fetchedObjects as? [MyList] else { return }
self.myLists = myLists
}
}
struct MyListsView: View {
let myLists: [MyList]
var body: some View {
List(myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
HStack {
Text(myList.name ?? "")
Spacer()
Text("\((myList.items ?? []).count)")
}
}
}
}
}
struct ContentView: View {
#StateObject private var vm = ViewModel()
#Environment(\.managedObjectContext) var viewContext
var body: some View {
NavigationView {
VStack {
// when adding an item to the list the MyListView view is
// not re-rendered
MyListsView(myLists: vm.myLists)
Button("Change List") {
}
}
}
}
func randomString(length: Int = 8) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
Inside ContentView there is a view called "MyListsView". That view is not rendered when the items are added. Since, according to that view nothing changed since the number of lists are still the same.
How do you solve this problem?
UPDATE:
What happens if I add one more level of views like for ListCellView as shown below:
struct MyListCellView: View {
#StateObject var vm: ListCellViewModel
init(vm: ListCellViewModel) {
_vm = StateObject(wrappedValue: vm)
}
var body: some View {
HStack {
Text(vm.name)
Spacer()
Text("\((vm.items).count)")
}
}
}
#MainActor
class ListCellViewModel: ObservableObject {
let myList: MyList
init(myList: MyList) {
self.myList = myList
self.name = myList.name ?? ""
self.items = myList.items!.allObjects as! [MyListItem]
print(self.items.count)
}
#Published var name: String = ""
#Published var items: [MyListItem] = []
}
struct MyListsView: View {
#StateObject var vm: ViewModel
init(vm: ViewModel) {
_vm = StateObject(wrappedValue: vm)
}
var body: some View {
let _ = Self._printChanges()
List(vm.myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
MyListCellView(vm: ListCellViewModel(myList: myList))
}
}
}
}
Now the count is again not being updated.

Your ViewModel is an ObserveableObject, but you are not observing it in MyListsView. When you initialized MyListsView, you set a let constant. Of course that won't update. Do this instead:
struct MyListsView: View {
#ObservedObject var vm: ViewModel
init(viewModel: ViewModel) {
self.vm = viewModel
}
var body: some View {
List(vm.myLists) { myList in
NavigationLink {
DetailView(myList: myList)
} label: {
HStack {
Text(myList.name ?? "")
Spacer()
Text("\((myList.items ?? []).count)")
}
}
}
}
}
Now the #Published in ViewModel will cause MyListView to change when it does, and that includes adding a related entity.

We don't need MVVM in SwiftUI, the View data structs already fill that role and property wrappers make them behave like objects giving best of both worlds. In your case use the #FetchRequest property wrapper for the list and #ObservedObject for the detail and body will be called on any changes to the model data. Examine the code in the app template in Xcode with Core Data checked. It looks like this:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
DetailView(item: item)
} label: {
Text(item.timestamp!, formatter: itemFormatter)
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
...
struct DetailView: View {
#ObservedObject var item: Item
var body: some View {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")

Related

SwiftUI show loading view while core data is being loaded

How I can show loading view while core data is being loaded?.
Currently my app's core data store some many images in Binary Data. So when I switch to another tab showing data stored in core data, app lags 1.5 seconds.
So here are two things I have tried:
first I tried to minimize amount of data being loaded from core data using downsample function:
func downsample(imageAt imageURL: Data, to pointSize: CGSize, scale: CGFloat = UIScreen.main.scale) -> UIImage? {
// Create an CGImageSource that represent an image
//CGImageSourceCreateWithData(_ data: CFData, _ options: CFDictionary?)
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let imageSource = CGImageSourceCreateWithData(imageURL as CFData, imageSourceOptions) else {
return nil
}
// Calculate the desired dimension
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
// Perform downsampling
let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
] as CFDictionary
guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else {
return nil
}
// Return the downsampled image as UIImage
return UIImage(cgImage: downsampledImage)
}
let small = downsample(imageAt: data, to: size)
Image(uiImage: small!)
But there were no difference in lagging time.
So i tried this:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \ToDoItem2.createdAt, ascending: false)])
var toDoItems: FetchedResults<ToDoItem>
var body: some View {
VStack{
if toDoItems.isEmpty {
LoadingView()
} else {
List {
ForEach(toDoItems) { item in
ToDoItemView(item: item)
}
}
}
}
}
}
Tried to detect no loaded state as toDoItems.isEmpty but it doesn't work
Would be there anyway to show loading view while core data is being loaded?
Thanks
you could try using NSAsynchronousFetchRequest, something like this approach (does not have to be exactly like this (untested) code):
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#State private var toDoItems: [ToDoItem] = []
#State var isLoading = true
var body: some View {
VStack {
if isLoading {
LoadingView()
} else {
List {
ForEach(toDoItems) { item in
ToDoItemView(item: item)
}
}
}
}
.onAppear {
isLoading = true
let fetchRequest: NSFetchRequest<ToDoItem> = ToDoItem.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \ToDoItem.createdAt, ascending: false)]
let asyncFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { fetchResult -> Void in
if let resutls = fetchResult.finalResult {
self.toDoItems = resutls
}
self.isLoading = false
}
do {
_ = try viewContext.execute(asyncFetchRequest)
} catch {
print("error: \(error)")
}
}
}
}

How to show a value from CoreData Entity as a state variable after editing

I'm currently developing an application using CoreData in SwiftUI.
I want to show a value from CoreData Entity as a state variable after I edit that data.
When I add a value as a new date I can show that value well, but after I edit the value it doesn't reflect the editing...
If I don't use #State, the value can be shown well, even after I edit it. But I need to use as #State value to bind it to a child view.
How could I solve this problem?
(I want to show the collect value after editing in a place I comment out as B same as like A in ListView.swift )
CDTest.xcdatamodeld
CDTestApp.swift
import SwiftUI
#main
struct CDTestApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
ListView()
}
}
PersistenceController.swift
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "CDTest")
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)")
}
})
}
}
HomeViewModel.swift
import SwiftUI
import CoreData
class HomeViewModel :ObservableObject{
#Published var content = ""
//for newdata sheet
#Published var isNewDate = false
//storing update Item
#Published var updateItem : Task!
func writeData(context : NSManagedObjectContext){
if updateItem != nil {
updateItem.content = content
try! context.save()
updateItem = nil
isNewDate.toggle()
content = ""
return
}
let newTask = Task(context: context)
newTask.content = content
do{
try context.save()
isNewDate.toggle()
content = ""
}catch{
print(error.localizedDescription)
}
}
func EditItem(item:Task){
updateItem = item
content = item.content!
isNewDate.toggle()
}
}
ListView.swift
import SwiftUI
import CoreData
struct ListView: View {
#StateObject var homeData = HomeViewModel()
#FetchRequest(entity: Task.entity(), sortDescriptors: [NSSortDescriptor(key:"date",
ascending:true)],animation:.spring()) var results:FetchedResults<Task>
#Environment(\.managedObjectContext) var context
var body: some View {
NavigationView{
VStack(spacing:0){
//Empty View...
if results.isEmpty{
Text("NO Data")
}else{
LazyVStack{
ForEach(results){task in
VStack(alignment: .leading, spacing: 5, content: {
Text(task.content ?? "") // A
ListRow(content: task.content ?? "") // B
})
.contextMenu{
Button(action: {
homeData.EditItem(item: task)
}, label: {
Text("Edit")
})
Button(action: {
context.delete(task)
try! context.save()
}, label: {
Text("Delete")
})
}
}
}
.padding()
}
//Add Button...
Button(action: {homeData.isNewDate.toggle()}, label: {
Image(systemName: "plus")
.font(.largeTitle)
.padding()
})
.sheet(isPresented: $homeData.isNewDate, content: {
NewDataView(homeData: homeData)
})
}
}
}
}
ListRow.swift
import SwiftUI
struct ListRow: View {
#State var content:String
var body: some View {
Text(content)
NavigationLink(destination:DetailView(content: $content)){
Text("DETAIL")
}
}
}
DetailView.swift
import SwiftUI
struct DetailView: View {
#Binding var content:String
var body: some View {
Text(content)
}
}
NewDataView.swift
import SwiftUI
struct NewDataView: View {
#ObservedObject var homeData : HomeViewModel
#Environment(\.managedObjectContext) var context
var body: some View {
VStack{
Text("\(homeData.updateItem == nil ? "Add New" : "Up date")Task")
.font(.title)
TextEditor(text: $homeData.content)
.padding()
//Add Button...
Button(action: {homeData.writeData(context: context)}, label: {
Text(homeData.updateItem == nil ? "Add Now" : "Update")
.font(.title)
.fontWeight(.bold)
})
.padding()
.disabled(homeData.content == "" ? true : false)
.opacity(homeData.content == "" ? 0.5 : 1)
}
}
}
Xcode: Version 12.0.1
iOS: 14.0
Life Cycle: SwiftUI App
Just pass entire task object into ListRow, like
VStack(alignment: .leading, spacing: 5, content: {
Text(task.content ?? "") // A
ListRow(task: task) // B // << here !!
so now we can use ObservedObject wrapper, which gives binding to published property:
struct ListRow: View {
#ObservedObject var task: Task
var body: some View {
Text(task.content ?? "")
NavigationLink(destination:DetailView(content: $task.content)){
Text("DETAIL")
}
}
}

SwiftUI - How to add "letters sections" and alphabet jumper in a Form?

How can I make a Form in which the elements are automatically divided into sections based on their first letter and add to the right the alphabet jumper to show the elements starting by the selected letter (just like the Contacts app)?
I also noted a strange thing that I have no idea how to recreate: not all letters are shown, some of them appear as "•". However, when you tap on them, they take you to the corresponding letter anyway. I tried using a ScrollView(.vertical) inside a ZStack and adding .scrollTo(selection) into the action of the Buttons, however 1) It didn't scroll to the selection I wanted 2) When I tapped on the "•", it was as if I was tapping on all of them because they all did the tapping animation 3) I wasn't able to divide the List as I wanted to.
I have this:
import SwiftUI
struct ContentView: View {
let alphabet = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W", "X","Y", "Z"]
let values = ["Avalue", "Bvalue", "Cvalue", "Dvalue"]
var body: some View {
ScrollViewReader{ scrollviewr in
ZStack {
ScrollView(.vertical) {
VStack {
ForEach(alphabet, id: \.self) { letters in
Button(letters){
withAnimation {
scrollviewr.scrollTo(letters)
}
}
}
}
}.offset(x: 180, y: 120)
VStack {
ForEach(values, id: \.self){ vals in
Text(vals).id(vals)
}
}
}
}
}
}
But I'd want it like this:
import SwiftUI
struct AlphabetSort2: View {
let alphabet = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W", "X","Y", "Z"]
let values = ["Avalue", "Bvalue", "Cvalue", "Dvalue", "Mvalue", "Zvalue"]
var body: some View {
ScrollView {
ScrollViewReader { value in
ZStack{
List{
ForEach(alphabet, id: \.self) { letter in
Section(header: Text(letter)) {
ForEach(values.filter { $0.hasPrefix(letter) }, id: \.self) { vals in
Text(vals).id(vals)
}
}.id(letter)
}
}
HStack{
Spacer()
VStack {
ForEach(0..<alphabet.count, id: \.self) { idx in
Button(action: {
withAnimation {
value.scrollTo(alphabet[idx])
}
}, label: {
Text(idx % 2 == 0 ? alphabet[idx] : "\u{2022}")
})
}
}
}
}
}
}
}
}
struct AlphabetSort2_Previews: PreviewProvider {
static var previews: some View {
AlphabetSort2()
}
}
Add an swipe-up-and-down action for the alphabet jumper, so we can get an UILocalizedIndexCollation -like swipe effect, it works for view and add mode, but delete, I guess it is due to SwiftUI's UI refresh mechanism.
extension String {
static var alphabeta: [String] {
var chars = [String]()
for char in "abcdefghijklmnopqrstuvwxyz#".uppercased() {
chars.append(String(char))
}
return chars
}
}
struct Document: View {
#State var items = ["Alpha", "Ash", "Aman", "Alisia", "Beta", "Baum", "Bob", "Bike", "Beeber", "Beff", "Calipha", "Cask", "Calf", "Deamon", "Deaf", "Dog", "Silk", "Seal", "Tiger", "Tom", "Tan", "Tint", "Urshinabi", "Verizon", "Viber", "Vein", "Wallet", "Warren", "Webber", "Waiter", "Xeon", "Young", "Yoda", "Yoga", "Yoger", "Yellow", "Zeta"]
var body: some View {
ScrollViewReader { scrollView in
HStack {
List {
ForEach(String.alphabeta, id: \.self){ alpha in
let subItems = items.filter({$0.starts(with: alpha)})
if !subItems.isEmpty {
Section(header: Text(alpha)) {
ForEach(subItems, id: \.self) { item in
Text(item)
}.onDelete(perform: { offsets in
items.remove(at: offsets.first!)
})
}.id(alpha)
}
}
}
VStack{
VStack {
SectionIndexTitles(proxy: scrollView, titles: retrieveSectionTitles()).font(.footnote)
}
.padding(.trailing, 10)
}
}
}
// .navigationBarTitleDisplayMode(.inline)
// .navigationBarHidden(true)
}
func retrieveSectionTitles() ->[String] {
var titles = [String]()
titles.append("#")
for item in self.items {
if !item.starts(with: titles.last!){
titles.append(String(item.first!))
}
}
titles.remove(at: 0)
if titles.count>1 && titles.first! == "#" {
titles.append("#")
titles.removeFirst(1)
}
return titles
}
}
struct Document_Previews: PreviewProvider {
static var previews: some View {
Document()
}
}
struct SectionIndexTitles: View {
class IndexTitleState: ObservableObject {
var currentTitleIndex = 0
var titleSize: CGSize = .zero
}
let proxy: ScrollViewProxy
let titles: [String]
#GestureState private var dragLocation: CGPoint = .zero
#StateObject var indexState = IndexTitleState()
var body: some View {
VStack {
ForEach(titles, id: \.self) { title in
Text(title)
.foregroundColor(.blue)
.modifier(SizeModifier())
.onPreferenceChange(SizePreferenceKey.self) {
self.indexState.titleSize = $0
}
.onTapGesture {
proxy.scrollTo(title, anchor: .top)
}
}
}
.gesture(
DragGesture(minimumDistance: indexState.titleSize.height, coordinateSpace: .named(titles.first))
.updating($dragLocation) { value, state, _ in
state = value.location
scrollTo(location: state)
}
)
}
private func scrollTo(location: CGPoint){
if self.indexState.titleSize.height > 0{
let index = Int(location.y / self.indexState.titleSize.height)
if index >= 0 && index < titles.count {
if indexState.currentTitleIndex != index {
indexState.currentTitleIndex = index
print(titles[index])
DispatchQueue.main.async {
let impactMed = UIImpactFeedbackGenerator(style: .medium)
impactMed.impactOccurred()
// withAnimation {
proxy.scrollTo(titles[indexState.currentTitleIndex], anchor: .top)
// }
}
}
}
}
}
}
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
struct SizeModifier: ViewModifier {
private var sizeView: some View {
GeometryReader { geometry in
Color.clear.preference(key: SizePreferenceKey.self, value: geometry.size)
}
}
func body(content: Content) -> some View {
content.background(sizeView)
}
}

SwiftUI + Core Data - updating an object (Detail -> DetailEdit)

Goal: update a core data object with SwiftUI: DetailView -> EditDetail -> DetailView (updated).
Problem: code bellow works, but creates a new object, instead of updating existing one.
import SwiftUI
struct DetailView: View {
var order = Order()
#State var showOrderEdit = false
var body: some View {
Form{
Text(order.tableNumber)
Text(order.pizzaType)
}
.navigationTitle(order.pizzaType)
.toolbar {
ToolbarItem(placement: .primaryAction) {
//edit button
Button(action: {
showOrderEdit = true
}, label: {
Text("Edit")
})
.sheet(isPresented: $showOrderEdit) {
OrderEdit(order: order)
}
}
}
}
}
import SwiftUI
struct DetailEdit: View {
#State var tableNumber = ""
#Environment(\.managedObjectContext) private var viewContext
#Environment (\.presentationMode) var presentationMode
var order = Order()
var body: some View {
NavigationView {
Form {
TextField("table number", text: $tableNumber)
//update button
Button(action: {
updateOrder(order: order)
}) {
Text("Update")
.foregroundColor(.blue)
}
}
//passing data item detail -> item edit
.onAppear {
self.tableNumber = self.order.tableNumber
}
.navigationTitle("Edit Order")
}
}
func updateOrder(order: Order) {
let newtableNumber = tableNumber
viewContext.performAndWait {
order.tableNumber = newtableNumber
try? viewContext.save()
}
}
You create new Order object in each view, so it is stored as new one into database. Instead you need to inject CoreData object from parent view (which shows DetailView) as observed object,
struct DetailView: View {
#ObservedObject var order: Order // << here !!
// .. other code
and
struct DetailEdit: View {
#State var tableNumber = ""
#Environment(\.managedObjectContext) private var viewContext
#Environment (\.presentationMode) var presentationMode
#ObservedObject var order: Order // << here !!
// ... other code
in such approach you will work with same instance of Order in both views and they will be updated because observe that instance for modifications.

Swift UI + CoreData + Toggle

I am using SwiftUI with CoreData. Each cell has a Toggle which updates CoreData isComplete: Bool. It works great if I have more than one item in core data, if I only have one item, I have to click the Toggle 3 times before it disappears or the view refreshes.
CategoryCompleteToggle(passedCategory: passedCategory, isOn: $isOn)
} // End HStack
Text(catName)
} // End VStack
.padding(10)
}
}
struct CategoryCompleteToggle: View {
var passedCategory: Category
#Environment(\.managedObjectContext) var moc
#ObservedObject var catToggle = CategoryToggle()
#Binding var isOn: Bool
var body: some View {
VStack(alignment: .trailing) {
Toggle(isOn: $isOn) {
EmptyView()
}
.onAppear {
self.isOn = self.passedCategory.catCompleted
print("ONLOAD ISON VALUE: \(self.isOn)")
}
.onTapGesture {
self.catToggle.catToggleOn.toggle()
self.catToggle.catToggleOn = !self.isOn
print("IsOn Value: \(self.isOn)")
print("Passed Category Name: \(self.passedCategory.catName ?? "Unknown")")
print("Current Completed Value: \(self.passedCategory.catCompleted)")
self.passedCategory.catCompleted = !self.isOn
do {
try self.moc.save()
} catch {
print(error.localizedDescription)
}

Resources