In a SwiftUI View i have a List based on #FetchRequest showing data of a Primary entity and the via relationship connected Secondary entity.
The View and its List is updated correctly, when I add a new Primary entity with a new related secondary entity.
The problem is, when I update the connected Secondary item in a detail view, the database gets updated, but the changes are not reflected in the Primary List.
Obviously, the #FetchRequest does not get triggered by the changes in another View.
When I add a new item in the primary view thereafter, the previously changed item gets finally updated.
As a workaround, i additionally update an attribute of the Primary entity in the detail view and the changes propagate correctly to the Primary View.
My question is:
How can I force an update on all related #FetchRequests in SwiftUI Core Data?
Especially, when I have no direct access to the related entities/#Fetchrequests?
import SwiftUI
extension Primary: Identifiable {}
// Primary View
struct PrimaryListView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(
entity: Primary.entity(),
sortDescriptors: [NSSortDescriptor(key: "primaryName", ascending: true)]
)
var fetchedResults: FetchedResults<Primary>
var body: some View {
List {
ForEach(fetchedResults) { primary in
NavigationLink(destination: SecondaryView(primary: primary)) {
VStack(alignment: .leading) {
Text("\(primary.primaryName ?? "nil")")
Text("\(primary.secondary?.secondaryName ?? "nil")").font(.footnote).foregroundColor(.secondary)
}
}
}
}
.navigationBarTitle("Primary List")
.navigationBarItems(trailing:
Button(action: {self.addNewPrimary()} ) {
Image(systemName: "plus")
}
)
}
private func addNewPrimary() {
let newPrimary = Primary(context: context)
newPrimary.primaryName = "Primary created at \(Date())"
let newSecondary = Secondary(context: context)
newSecondary.secondaryName = "Secondary built at \(Date())"
newPrimary.secondary = newSecondary
try? context.save()
}
}
struct PrimaryListView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return NavigationView {
PrimaryListView().environment(\.managedObjectContext, context)
}
}
}
// Detail View
struct SecondaryView: View {
#Environment(\.presentationMode) var presentationMode
var primary: Primary
#State private var newSecondaryName = ""
var body: some View {
VStack {
TextField("Secondary name:", text: $newSecondaryName)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
.onAppear {self.newSecondaryName = self.primary.secondary?.secondaryName ?? "no name"}
Button(action: {self.saveChanges()}) {
Text("Save")
}
.padding()
}
}
private func saveChanges() {
primary.secondary?.secondaryName = newSecondaryName
// TODO: ❌ workaround to trigger update on primary #FetchRequest
primary.managedObjectContext.refresh(primary, mergeChanges: true)
// primary.primaryName = primary.primaryName
try? primary.managedObjectContext?.save()
presentationMode.wrappedValue.dismiss()
}
}
I also struggled with this and found a very nice and clean solution:
You have to wrap the row in a separate view and use #ObservedObject in that row view on the entity.
Here's my code:
WineList:
struct WineList: View {
#FetchRequest(entity: Wine.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \Wine.name, ascending: true)
]
) var wines: FetchedResults<Wine>
var body: some View {
List(wines, id: \.id) { wine in
NavigationLink(destination: WineDetail(wine: wine)) {
WineRow(wine: wine)
}
}
.navigationBarTitle("Wines")
}
}
WineRow:
struct WineRow: View {
#ObservedObject var wine: Wine // !! #ObserveObject is the key!!!
var body: some View {
HStack {
Text(wine.name ?? "")
Spacer()
}
}
}
You need a Publisher which would generate event about changes in context and some state variable in primary view to force view rebuild on receive event from that publisher.
Important: state variable must be used in view builder code, otherwise rendering engine would not know that something changed.
Here is simple modification of affected part of your code, that gives behaviour that you need.
#State private var refreshing = false
private var didSave = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)
var body: some View {
List {
ForEach(fetchedResults) { primary in
NavigationLink(destination: SecondaryView(primary: primary)) {
VStack(alignment: .leading) {
// below use of .refreshing is just as demo,
// it can be use for anything
Text("\(primary.primaryName ?? "nil")" + (self.refreshing ? "" : ""))
Text("\(primary.secondary?.secondaryName ?? "nil")").font(.footnote).foregroundColor(.secondary)
}
}
// here is the listener for published context event
.onReceive(self.didSave) { _ in
self.refreshing.toggle()
}
}
}
.navigationBarTitle("Primary List")
.navigationBarItems(trailing:
Button(action: {self.addNewPrimary()} ) {
Image(systemName: "plus")
}
)
}
An alternative method: using a Publisher and List.id():
struct ContentView: View {
/*
#FetchRequest...
*/
private var didSave = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave) //the publisher
#State private var refreshID = UUID()
var body: some View {
List {
...
}
.id(refreshID)
.onReceive(self.didSave) { _ in //the listener
self.refreshID = UUID()
print("generated a new UUID")
}
}
}
Every time you call save() of NSManagedObjects in a context, it genertates a new UUID for the List view, and it forces the List view to refresh.
To fix that you have to add #ObservedObject to var primary: Primary in SecondaryView to work List properly. Primary belong to NSManagedObject class, which already conforms to #ObservableObject protocol. This way the changes in instances of Primary are observed.
import SwiftUI
extension Primary: Identifiable {}
// Primary View
struct PrimaryListView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(
entity: Primary.entity(),
sortDescriptors: [NSSortDescriptor(key: "primaryName", ascending: true)]
)
var fetchedResults: FetchedResults<Primary>
var body: some View {
List {
ForEach(fetchedResults) { primary in
NavigationLink(destination: SecondaryView(primary: primary)) {
VStack(alignment: .leading) {
Text("\(primary.primaryName ?? "nil")")
Text("\(primary.secondary?.secondaryName ?? "nil")").font(.footnote).foregroundColor(.secondary)
}
}
}
}
.navigationBarTitle("Primary List")
.navigationBarItems(trailing:
Button(action: {self.addNewPrimary()} ) {
Image(systemName: "plus")
}
)
}
private func addNewPrimary() {
let newPrimary = Primary(context: context)
newPrimary.primaryName = "Primary created at \(Date())"
let newSecondary = Secondary(context: context)
newSecondary.secondaryName = "Secondary built at \(Date())"
newPrimary.secondary = newSecondary
try? context.save()
}
}
struct PrimaryListView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return NavigationView {
PrimaryListView().environment(\.managedObjectContext, context)
}
}
}
// Detail View
struct SecondaryView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var primary: Primary
#State private var newSecondaryName = ""
var body: some View {
VStack {
TextField("Secondary name:", text: $newSecondaryName)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
.onAppear {self.newSecondaryName = self.primary.secondary?.secondaryName ?? "no name"}
Button(action: {self.saveChanges()}) {
Text("Save")
}
.padding()
}
}
private func saveChanges() {
primary.secondary?.secondaryName = newSecondaryName
try? primary.managedObjectContext?.save()
presentationMode.wrappedValue.dismiss()
}
}
I tried to touch the primary object in the detail view like this:
// TODO: ❌ workaround to trigger update on primary #FetchRequest
if let primary = secondary.primary {
secondary.managedObjectContext?.refresh(primary, mergeChanges: true)
}
Then the primary list will update. But the detail view has to know about the parent object. This will work, but this is probably not the SwiftUI or Combine way...
Edit:
Based on the above workaround, I modified my project with a global save(managedObject:) function. This will touch all related Entities, thus updating all relevant #FetchRequest's.
import SwiftUI
import CoreData
extension Primary: Identifiable {}
// MARK: - Primary View
struct PrimaryListView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(
sortDescriptors: [
NSSortDescriptor(keyPath: \Primary.primaryName, ascending: true)]
)
var fetchedResults: FetchedResults<Primary>
var body: some View {
print("body PrimaryListView"); return
List {
ForEach(fetchedResults) { primary in
NavigationLink(destination: SecondaryView(secondary: primary.secondary!)) {
VStack(alignment: .leading) {
Text("\(primary.primaryName ?? "nil")")
Text("\(primary.secondary?.secondaryName ?? "nil")")
.font(.footnote).foregroundColor(.secondary)
}
}
}
}
.navigationBarTitle("Primary List")
.navigationBarItems(trailing:
Button(action: {self.addNewPrimary()} ) {
Image(systemName: "plus")
}
)
}
private func addNewPrimary() {
let newPrimary = Primary(context: context)
newPrimary.primaryName = "Primary created at \(Date())"
let newSecondary = Secondary(context: context)
newSecondary.secondaryName = "Secondary built at \(Date())"
newPrimary.secondary = newSecondary
try? context.save()
}
}
struct PrimaryListView_Previews: PreviewProvider {
static var previews: some View {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
return NavigationView {
PrimaryListView().environment(\.managedObjectContext, context)
}
}
}
// MARK: - Detail View
struct SecondaryView: View {
#Environment(\.presentationMode) var presentationMode
var secondary: Secondary
#State private var newSecondaryName = ""
var body: some View {
print("SecondaryView: \(secondary.secondaryName ?? "")"); return
VStack {
TextField("Secondary name:", text: $newSecondaryName)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
.onAppear {self.newSecondaryName = self.secondary.secondaryName ?? "no name"}
Button(action: {self.saveChanges()}) {
Text("Save")
}
.padding()
}
}
private func saveChanges() {
secondary.secondaryName = newSecondaryName
// save Secondary and touch Primary
(UIApplication.shared.delegate as! AppDelegate).save(managedObject: secondary)
presentationMode.wrappedValue.dismiss()
}
}
extension AppDelegate {
/// save and touch related objects
func save(managedObject: NSManagedObject) {
let context = persistentContainer.viewContext
// if this object has an impact on related objects, touch these related objects
if let secondary = managedObject as? Secondary,
let primary = secondary.primary {
context.refresh(primary, mergeChanges: true)
print("Primary touched: \(primary.primaryName ?? "no name")")
}
saveContext()
}
}
If you are here, i don't find the reason why your view isn't updating, i think this will help you:
Always use the #ObservedObject when you declare a core data type.
If you are using MVVM, wrap the view model also with #ObservedObject, and in the VM create the core data type with #Published.
This is an example of creating a VM with #ObservedObject, so when core data receives the update, the instance of the view model recreate itself, and the view is updated.
class ProductTitleValueViewModel: BaseViewModel, ObservableObject {
// MARK: - Properties
#Published var product: Product
var colorSet: [Color]
var currency: Currency
// MARK: - Init
init(product: Product, colorSet: [Color], currency: Currency) {
self.product = product
self.colorSet = colorSet
self.currency = currency
}
}
struct ProductTitleValueView: View {
#ObservedObject var viewModel: ProductTitleValueViewModel
var body: some View {
VStack(alignment: .leading, spacing: 5) {
HStack {
Circle()
.fill(
LinearGradient(colors: viewModel.colorSet, startPoint: .leading, endPoint: .trailing)
)
.opacity(0.6)
.frame(width: 20, height: 20)
Text(viewModel.product.wrappedName)
.font(.callout.bold())
.foregroundColor(ThemeColor.lightGray)
}
Text(viewModel.product.balance.toCurrency(with: viewModel.currency))
.font(.callout.bold())
.padding(.leading, 28)
}
}
}
If you follow this 2 simple things, you are not going to have problem with core date updating your views.
Related
I'm trying to create a master detail relationship with CoreData. I have a settings tab that is used to select the master (it's global and not done very often by the user). There is another tab that shows the detail entries for the current master.
The master has one field, name, a string and the details array. The detail has one field, name, a string. I'm using UUID().uuidString to populate the names for the example.
The problem I'm having is that when I select the detail tab, it shows the details for the current master. If I add details (click the + button) they do not appear until I change the master (settings -> select master). If I edit the details and delete some, the list entries go away but when I finish editing, they immediately come back. I can switch masters and then go back to the edited master and the data looks correct (I have to change the activeMaster published property).
I'm thinking that the published property isn't forcing the update to the details view because swift doesn't see the master variable change. I may also not be adding or deleting the details correctly.
How is adding details to a master typically done (here master is one to many details)
How is deleting details from a master typically done?
Is the data no showing up due to the published property not "publishing" Any ideas on how to better do this?
Thanks.
Code is below.
Here's the global application data:
import Foundation
import CoreData
import SwiftUI
class ApplicationData: ObservableObject
{
let container: NSPersistentContainer
#Published var activeMaster: Master?
init(preview: Bool = false)
{
container = NSPersistentContainer(name: "MasterDetail")
if (preview)
{
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)")
}
})
}
}
Just persistence and a single optional active master. The application data is created in the application code and set as an environment object:
import SwiftUI
#main
struct MasterDetailApp: App
{
#StateObject var appData = ApplicationData()
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(appData)
.environment(\.managedObjectContext, appData.container.viewContext)
}
}
}
The tab view:
import Foundation
import SwiftUI
struct MainView: View
{
#AppStorage("selectedTab") var selectedTab: Int = 0
#EnvironmentObject var appData: ApplicationData
var body: some View
{
TabView(selection: $selectedTab)
{
DetailView()
.tabItem({Label("Detail", systemImage: "house")})
.tag(0)
SettingsView()
.tabItem({Label("Settings", systemImage: "gear")})
.tag(1)
}
.environment(\.managedObjectContext, appData.container.viewContext)
}
}
The detail tab allows the user to add details and to edit the list:
import Foundation
import SwiftUI
import CoreData
struct DetailView: View
{
#Environment(\.managedObjectContext) private var viewContext
#Environment(\.dismiss) var dismiss
#EnvironmentObject var appData: ApplicationData
var body: some View
{
NavigationView
{
List
{
ForEach(appData.activeMaster?.wrappedDetail ?? [])
{
detail in Text(detail.name ?? "None")
}
.onDelete(perform: { indexes in Task(priority: .high) { await deleteDetails(indexes: indexes) } } )
}
.toolbar
{
ToolbarItem(placement: .navigationBarTrailing)
{
EditButton()
}
ToolbarItem(placement: .navigationBarTrailing)
{
Button
{
let detail = Detail(context: viewContext)
detail.name = UUID().uuidString
detail.master = appData.activeMaster
do
{
try viewContext.save()
}
catch
{
print("Error adding master")
}
} label: { Image(systemName: "plus") }
.disabled(appData.activeMaster == nil)
}
}
}
}
/*
* Delete indexes - assumes that appData.activeWeapon is set.
*/
private func deleteDetails(indexes: IndexSet) async
{
await viewContext.perform
{
for index in indexes
{
print(index)
viewContext.delete(appData.activeMaster!.wrappedDetail[index])
}
do
{
try viewContext.save()
}
catch
{
print("Error deleting dope entry")
}
}
}
}
The settings view just has a navigation link to a view to select the master and an add button to add masters:
import Foundation
import SwiftUI
struct SettingsView: View
{
#Environment(\.managedObjectContext) private var viewContext
#EnvironmentObject var appData: ApplicationData
var body: some View
{
NavigationView
{
Form
{
Section(header: Text("Masters"))
{
NavigationLink(destination: SelectMastersView(selectedMaster: $appData.activeMaster), label:
{
Text(appData.activeMaster?.name ?? "Select Master")
})
Button
{
let master = Master(context: viewContext)
master.name = UUID().uuidString
do
{
try viewContext.save()
}
catch
{
print("Error adding master")
}
} label: { Image(systemName: "plus") }
}
}
}
}
}
The view for selecting the master just has a fetch request to get all masters and assign the selected one to the global app data published property:
import Foundation
import SwiftUI
struct SelectMastersView: View
{
#Environment(\.dismiss) var dismiss
#FetchRequest(entity: Master.entity(), sortDescriptors: [], animation: .default)
var masters: FetchedResults<Master>
#Binding var selectedMaster: Master?
var body: some View
{
List
{
ForEach(masters)
{ master in
Text(master.name ?? "None")
.onTapGesture
{
selectedMaster = master
dismiss()
}
}
}
.navigationBarTitle("Masters")
}
}
Edited to add extension to Master I forgot to post.
import Foundation
extension Master
{
var wrappedDetail: [Detail]
{
detail?.allObjects as! [Detail]
}
}
I finally figured it out this morning. I think putting the example code together last night helped quite a bit.
I got it work by creating fetch request in the detail view and passing the master into the view in init().
Here's the updated code for the tab view:
import Foundation
import SwiftUI
struct MainView: View
{
#AppStorage("selectedTab") var selectedTab: Int = 0
#EnvironmentObject var appData: ApplicationData
var body: some View
{
TabView(selection: $selectedTab)
{
DetailView(master: appData.activeMaster)
.tabItem({Label("Detail", systemImage: "house")})
.tag(0)
SettingsView()
.tabItem({Label("Settings", systemImage: "gear")})
.tag(1)
}
.environment(\.managedObjectContext, appData.container.viewContext)
}
}
and the updated detail view:
import Foundation
import SwiftUI
import CoreData
struct DetailView: View
{
#Environment(\.managedObjectContext) private var viewContext
#Environment(\.dismiss) var dismiss
#EnvironmentObject var appData: ApplicationData
#FetchRequest(entity: Detail.entity(), sortDescriptors: [])
var details: FetchedResults<Detail>
let master: Master?
init(master: Master?)
{
self.master = master
if master != nil
{
let predicate = NSPredicate(format: "%K == %#", #keyPath(Detail.master), master ?? NSNull())
_details = FetchRequest(sortDescriptors: [], predicate: predicate)
}
}
#ViewBuilder
var body: some View
{
NavigationView
{
List
{
if master != nil
{
ForEach(details)
{
detail in Text(detail.name ?? "None")
}
.onDelete(perform: { indexes in Task(priority: .high) { await deleteDetails(indexes: indexes) } } )
}
}
.toolbar
{
ToolbarItem(placement: .navigationBarTrailing)
{
EditButton().disabled(master == nil || details.isEmpty)
}
ToolbarItem(placement: .navigationBarTrailing)
{
Button
{
let detail = Detail(context: viewContext)
detail.name = UUID().uuidString
detail.master = appData.activeMaster
do
{
try viewContext.save()
}
catch
{
print("Error adding master")
}
} label: { Image(systemName: "plus") }
.disabled(appData.activeMaster == nil)
}
}
}
}
/*
* Delete indexes - assumes that appData.activeWeapon is set.
*/
private func deleteDetails(indexes: IndexSet) async
{
await viewContext.perform
{
for index in indexes
{
print(index)
viewContext.delete(appData.activeMaster!.wrappedDetail[index])
}
do
{
try viewContext.save()
}
catch
{
print("Error deleting dope entry")
}
}
}
}
This is not as clean as I'd like. I had to move to a view build for the list. I'd like to be able to create an empty fetch request so I don't have to use a view builder.
G'day everyone,
I'm trying to work out how CoreData relationships can work with UI elements like pickers.
At the moment I have a 3 view app (based on the Xcode boilerplate code) which displays a list of parent entities, which have children which have children. I want a picker to select which grandchild a child entity should refer to.
At the moment I have two funny side effects:
When I run the app as a preview (so there is pre-populated data... this sample code will break without the data in place),
the selected grandchild in the picker is the grandchild of the first
child, irrespective of which child you're dropped into in the first
view.
When I drop back and pick another child, now the picked grabs the correct initial selection from the child entity
When I select a child and "save" that, the value in the child summary does not change, until I click another child at which point the value changes before the transition to the modal view.
I am clearly missing something in my understanding of the sequence of events when presenting modals in SwiftUI... can any what shed any light on what I've done wrong?
Here's a video to make this more clear:
https://github.com/andrewjdavison/Test31/blob/main/Test31%20-%20first%20click%20issue.mov?raw=true
Git repository of the sample is https://github.com/andrewjdavison/Test31.git, but in summary:
Data Model:
View Source:
import SwiftUI
import CoreData
struct LicenceView : View {
#Environment(\.managedObjectContext) private var viewContext
#Binding var licence: Licence
#Binding var showModal: Bool
#State var selectedElement: Element
#FetchRequest private var elements: FetchedResults<Element>
init(currentLicence: Binding<Licence>, showModal: Binding<Bool>, context: NSManagedObjectContext) {
self._licence = currentLicence
self._showModal = showModal
let fetchRequest: NSFetchRequest<Element> = Element.fetchRequest()
fetchRequest.sortDescriptors = []
self._elements = FetchRequest(fetchRequest: fetchRequest)
_selectedElement = State(initialValue: currentLicence.wrappedValue.licenced!)
}
func save() {
licence.licenced = selectedElement
try! viewContext.save()
showModal = false
}
var body: some View {
VStack {
Button(action: {showModal = false}) {
Text("Close")
}
Picker(selection: $selectedElement, label: Text("Element")) {
ForEach(elements, id: \.self) { element in
Text("\(element.desc!)")
}
}
Text("Selected: \(selectedElement.desc!)")
Button(action: {save()}) {
Text("Save")
}
}
}
}
struct RegisterView : View {
#Environment(\.managedObjectContext) private var viewContext
#State var showModal: Bool = false
var currentRegister: Register
#State var currentLicence: Licence
init(currentRegister: Register) {
currentLicence = Array(currentRegister.licencedUsers! as! Set<Licence>)[0]
self.currentRegister = currentRegister
}
var body: some View {
VStack {
List {
ForEach (Array(currentRegister.licencedUsers! as! Set<Licence>), id: \.self) { licence in
Button(action: {currentLicence = licence; showModal = true}) {
HStack {
Text("\(licence.leasee!) : ")
Text("\(licence.licenced!.desc!)")
}
}
}
}
}
.sheet(isPresented: $showModal) {
LicenceView(currentLicence: $currentLicence, showModal: $showModal, context: viewContext )
}
}
}
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Register.id, ascending: true)],
animation: .default)
private var registers: FetchedResults<Register>
var body: some View {
NavigationView {
List {
ForEach(registers) { register in
NavigationLink(destination: RegisterView(currentRegister: register)) {
Text("Register id \(register.id!)")
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
[1]: https://i.stack.imgur.com/AfaNb.png
I didn't really understand this
• selected grandchild in the picker is the grandchild of the first child, irrespective of which child you're dropped into in the first view.
• When I drop back and pick another child, now the picked grabs the correct initial selection from the child entity
Could you attach a video that represents a problem?
But I can give you a solution to the preview problem and the second one.
Preview
If you use preview with Core Data, you need to use a viewContextcreated with MockData and pass it to your View. Here I provide a generic code, that can be modified for each of your views:
In your Persistance struct (CoreData Manager) declare a variable preview with your preview Items:
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
// Here you create your Mock Data
let newItem = Item(context: viewContext)
newItem.yourProperty = yourValue
do {
try viewContext.save()
} catch {
// error handling
}
return result
}()
Make sure it has inMemory: Bool in its init, as it is responsible for separating real viewContext and previewContext:
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "TestCD")
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)")
}
})
}
Create Mock Item from your viewContext and pass it to preview:
struct YourView_Previews: PreviewProvider {
static var previews: some View {
let context = PersistenceController.preview.container.viewContext
let request: NSFetchRequest<Item> = Item.fetchRequest()
let fetchedItems = try! context.fetch(request)
YourView(item: fetchedItems)
}
}
If you use #FetchRequest and #FetchedResults it makes it easier, as they will do creating and fetching objects for you. Just implement a preview like this:
struct YourView_Previews: PreviewProvider {
static var previews: some View {
YourView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
Here is Persistence struct created by Xcode at the moment of the project initialization:
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
let item = Item(context: viewContext)
item.property = yourProperty
do {
try viewContext.save()
} catch {
}
return result
}()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "TestCD")
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)")
}
})
}
}
Second problem
Core Data objects are built with classes, so their type is a reference. When you change a property is a class it doesn't notifiy the view struct to redraw with a new value. (exception is classes, that are created to notify about changes.)
You need to explicitly tell your RegisterView struct to redraw itself after you dismiss your LicenceView. You can do it by creating one more variable in your RegisterView - #State var id = UUID(). Then attach an .id(id) modifier at the end of your VStack
VStack {
//your code
}.id(id)
Finally, create a function viewDismissed which will change the id property in your struct:
func viewDismissed() {
id = UUID()
}
Now, attach this function to your sheet with an optional parameter onDismiss
.sheet(isPresented: $showModal, onDismiss: viewDismissed) {
LicenceView(currentLicence: $currentLicence, showModal: $showModal, context: viewContext )
}
OK. Huge vote of thanks to Lorem for getting me to the answer. Thanks too for Roma, but it does turn out that his solution, whilst it worked to resolve one of my key problems, does introduce inefficiencies - and didn't resolve the second one.
If others are hitting the same issue I'll leave the Github repo up, but the crux of it all was that #State shouldn't be used when you're sharing CoreData objects around. #ObservedObject is the way to go here.
So the resolution to the problems I encountered were:
Use #ObservedObject instead of #State for passing around the CoreData objects
Make sure that the picker has a tag defined. The documentation I head read implied that this gets generated automatically if you use ".self" as the id for the objects in ForEach, but it seems this is not always reliable. so adding ".tag(element as Element?)" to my picker helped here.
Note: It needed to be an optional type because CoreData makes all the attribute types optional.
Those two alone fixed the problems.
The revised "LicenceView" struct is here, but the whole solution is in the repo.
Cheers!
struct LicenceView : View {
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var licence: Licence
#Binding var showModal: Bool
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Element.desc, ascending: true)],
animation: .default)
private var elements: FetchedResults<Element>
func save() {
try! viewContext.save()
showModal = false
}
var body: some View {
VStack {
Button(action: {showModal = false}) {
Text("Close")
}
Picker(selection: $licence.licenced, label: Text("Element")) {
ForEach(elements, id: \.self) { element in
Text("\(element.desc!)")
.tag(element as Element?)
}
}
Text("Selected: \(licence.licenced!.desc!)")
Button(action: {save()}) {
Text("Save")
}
}
}
}
Using SwiftUI
I have created a CoreData Entity called SystemSettings and would like to use the information stored in it for calculating results in different functions on different views without using ForEach. SystemSettings will always have only one object (Record) stored from preloaded database. Is there a way I can use #FetchRequest and pull out the only object and its attributes out, as a var.... ?
Thank you
import SwiftUI
import CoreData
struct NewComponent: View {
#FetchRequest(entity: SystemSettings.entity(), sortDescriptors: []) var settings: FetchedResults<SystemSettings>
#Environment(\.managedObjectContext) var moc
#Environment(\.presentationMode) var presentationMode
var calculator = Calculator()
// USER DATA TEXT FIELDS
#State var parameterOne = "12.5"
#State var parameterTwo = "2.5"
#State var calculationResult = ""
var body: some View {
VStack {
Form {
Section (header: Text("Parameters").font(.headline).bold().italic()) {
HStack {
Text ("Length")
.font(.headline)
Spacer()
TextField ("m", text: $parameterOne).modifier(ClearButton(text: $parameterOne))
.multilineTextAlignment(.trailing)
.keyboardType(.decimalPad)
.font(.headline)
}
HStack {
Text ("Width")
.font(.headline)
Spacer()
TextField ("m", text: $parameterTwo).modifier(ClearButton(text: $parameterTwo))
.multilineTextAlignment(.trailing)
.keyboardType(.decimalPad)
.font(.headline)
}
}
}
Section(header:
Text("\(self.calculationResult)").bold().italic().padding(.top)
) {
Button(action: {
self.calculationResult = self.result()
}) {
HStack {
Spacer()
Image(systemName: "x.squareroot")
Text("Calculate!")
Spacer()
}.font(.headline).foregroundColor(Color.green)
.padding(.bottom, 40.0).padding(.top, 20.0)
}
}
}.navigationBarTitle(Text("Component Calculator"))
.gesture(DragGesture().onChanged{_ in UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)})
.modifier(KeyboardObserving())
.navigationBarItems(trailing: Button(action: {
let newEstimatedComponent = EstimatedComponents(context: self.moc)
newEstimatedComponent.name = "New Component"
newEstimatedComponent.price = self.priceToDisplay()
try? self.moc.save()
self.presentationMode.wrappedValue.dismiss()
}){Image(systemName: "checkmark.circle").font(.system(size: 30))})
}
func result () -> String {
let result = componentCost()
return (String(format: "%.2f",(result)) + " €")
}
func priceToDisplay() -> Double {
return componentCost()
}
func componentCost () -> Double {
return calculator (componentWidth: Double(parameterOne), componentHight: Double(parameterTwo), componentPrice: settings!.componentM2Price )
}
/*settings!componentM2Price not working of course, so I need to get the first and the only object from SystemSettings Entity in a way that I can use it in func componentCost()...*/
THANKS AGAIN...
Solved!
#EnvironmentObject var settings: ModifySystemSettings
let systemSettings: SystemSettings
You can find more information here:
https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-environmentobject-to-share-data-between-views
I have a two CoreData objects:
RoadTrip
StatePlate.
Each RoadTrip items holds an NSSet of StatePlate.
Screen 1 (TripList) shows a list of all RoadTrip items. Screen 2 (StateList) shows a list of all StatePlate items in associated with the RoadTrip that a user selects. Selecting a StatePlate item in Screen 2 will toggle a bool value associated with that item.
Even though I can show the data and can toggle the bool value of each StatePlate, I am not seeing an immediate change to the UI of the screen. The StatePlate should jump from Section to Section in Screen 2 when it's bool value is toggled.
How can I pass this FetchedObject correctly from Screen 1 to Screen 2 so the UI is binded with the data?
Screen 1 (TripList)
struct TripList: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: RoadTrip.entity(), sortDescriptors: []) var roadTripItems: FetchedResults<RoadTrip>
var body: some View {
List {
ForEach(roadTripItems, id: \.self) { trip in
NavigationLink(destination: StateList(trip: trip)
.environment(\.managedObjectContext, self.managedObjectContext)) {
TripRow(roadTrip: trip)
}
}
}
}
}
Screen 2 (StateList)
struct StateList: View {
#Environment(\.managedObjectContext) var managedObjectContext
var trip: RoadTrip
var plates: [StatePlate] {
trip.plateArray
}
var unseenPlates: [StatePlate] {
trip.plateArray.filter { !$0.hasBeenSeen }
}
var seenPlates: [StatePlate] {
trip.plateArray.filter { $0.hasBeenSeen }
}
var body: some View {
List {
if !unseenPlates.isEmpty {
Section(header: Text("Unseen Plates")) {
ForEach(unseenPlates, id: \.self) { plate in
StateRow(plate: plate)
}
}
}
if !seenPlates.isEmpty {
Section(header: Text("Seen Plates")) {
ForEach(seenPlates, id: \.self) { plate in
StateRow(plate: plate)
}
}
}
}
}
}
StateRow
struct StateRow: View {
#Environment(\.managedObjectContext) var managedObjectContext
#ObservedObject var plate: StatePlate
var body: some View {
Button(action: {
self.plate.hasBeenSeen.toggle()
try? self.managedObjectContext.save()
}) {
HStack {
Text(String(describing: plate.name!))
Spacer()
if plate.hasBeenSeen {
Image(systemName: "eye.fill")
} else {
Image(systemName: "")
}
}
}
}
}
Your trip as object is not changed when plate has changed, so even if it was observed UI was not refreshed.
Here is possible force-refresh approach.
struct StateList: View {
#Environment(\.managedObjectContext) var managedObjectContext
#ObservedObject var trip: RoadTrip // << make observed
// .. other code
and add handling for updated plate/s
StateRow(plate: plate)
.onReceive(plate.objectWillChange) { _ in
self.trip.objectWillChange.send()
}
I am using a TabView/NavigationView & NavigationLink to programmatically navigate from a list view to detailed view and when I update the boolean property 'pinned' to true & save the core data entity in the detailed view, I am getting the following unfortunate side effects:
an involuntary navigation back to the list view and then again back to the detailed view or
an involuntary navigation to another copy of the same detailed view and then back to the list view.
I have prepared a small Xcode project with the complete sample code
In the list view I use #FetchRequest to query the list and sort on the following:
#FetchRequest(entity: Task.entity(),sortDescriptors: [NSSortDescriptor(key: "pinned", ascending: false),
NSSortDescriptor(key: "created", ascending: true),
NSSortDescriptor(key: "name", ascending: true)])
In the list view I use the following:
List() {
ForEach(tasks, id: \.self) { task in
NavigationLink(destination: DetailsView(task: task), tag: task.id!.uuidString, selection: self.$selectionId) {
HStack() {
...
}
}
}
.
(1) If I omit the 'NSSortDescriptor(key: "pinned" ...)' I don't see the behavior.
(2) If I omit the 'tag:' and the 'selection:' parameters in the NavigationLink() I don't see the behavior. But I need to be able to trigger the navigation link programmatically when I create a new Task entity.
(3) It seems never to happen when I have a single entity in the list or changing the value of the 'pinned' boolean property in the first entity in the list.
(4) I get the warning:
[TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window)...
The parent view to the list view (TasksListView) contains a TabView:
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
TasksListView()
}
.tabItem {
Image(systemName: "tray.full")
.font(.title)
Text("Master")
}
NavigationView {
EmptyView()
}
.tabItem {
Image(systemName: "magnifyingglass")
.font(.title)
Text("Search")
}
}
}
}
struct TasksListView: View {
// NSManagedObjectContext
#Environment(\.managedObjectContext) var viewContext
// Results of fetch request for tasks:
#FetchRequest(entity: Task.entity(),sortDescriptors: [NSSortDescriptor(key: "pinned", ascending: false),
NSSortDescriptor(key: "created", ascending: true),
NSSortDescriptor(key: "name", ascending: true)])
var tasks: FetchedResults<Task>
// when we create a new task and navigate to it programitically
#State var selectionId : String?
#State var newTask : Task?
var body: some View {
List() {
ForEach(tasks, id: \.self) { task in
NavigationLink(destination: DetailsView(task: task), tag: task.id!.uuidString, selection: self.$selectionId) {
HStack() {
VStack(alignment: .leading) {
Text("\(task.name ?? "unknown")")
.font(Font.headline.weight(.light))
.padding(.bottom,5)
Text("Created:\t\(task.created ?? Date(), formatter: Self.dateFormatter)")
.font(Font.subheadline.weight(.light))
.padding(.bottom,5)
if task.due != nil {
Text("Due:\t\t\(task.due!, formatter: Self.dateFormatter)")
.font(Font.subheadline.weight(.light))
.padding(.bottom,5)
}
}
}
}
}
}
.navigationBarTitle(Text("Tasks"),displayMode: .inline)
.navigationBarItems(trailing: rightButton)
}
var rightButton: some View {
Image(systemName: "plus.circle")
.foregroundColor(Color(UIColor.systemBlue))
.font(.title)
.contentShape(Rectangle())
.onTapGesture {
// create a new task and navigate to it's detailed view to add values
Task.create(in: self.viewContext) { (task, success, error) in
if success {
self.newTask = task
self.selectionId = task!.id!.uuidString
}
}
}
}
}
struct DetailsView: View {
// NSManagedObjectContext
#Environment(\.managedObjectContext) var viewContext
#ObservedObject var task : Task
#State var name : String = ""
#State var dueDate : Date = Date()
#State var hasDueDate : Bool = false
#State var isPinned : Bool = false
var body: some View {
List() {
Section() {
Toggle(isOn: self.$isPinned) {
Text("Pinned")
}
}
Section() {
TextField("Name", text: self.$name)
.font(Font.headline.weight(.light))
Text("\(task.id?.uuidString ?? "unknown")")
.font(Font.headline.weight(.light))
}
Section() {
HStack() {
Text("Created")
Spacer()
Text("\(task.created ?? Date(), formatter: Self.dateFormatter)")
.font(Font.subheadline.weight(.light))
}
Toggle(isOn: self.$hasDueDate) {
Text("Set Due Date")
}
if self.hasDueDate {
DatePicker("Due Date", selection: self.$dueDate, in: Date()... , displayedComponents: [.hourAndMinute, .date])
}
}
}
.navigationBarTitle(Text("Task Details"),displayMode: .inline)
.navigationBarItems(trailing: rightButton)
.listStyle(GroupedListStyle())
.onAppear() {
if self.task.pinned {
self.isPinned = true
}
if self.task.name != nil {
self.name = self.task.name!
}
if self.task.due != nil {
self.dueDate = self.task.due!
self.hasDueDate = true
}
}
}
// save button
var rightButton: some View {
Button("Save") {
// save values in task & save:
self.task.pinned = self.isPinned
if self.hasDueDate {
self.task.due = self.dueDate
}
if self.name.count > 0 {
self.task.name = self.name
}
Task.save(in: self.viewContext) { (success, error) in
DispatchQueue.main.async {
if success {
print("Task saved")
}
else {
print("****** Error: Task can't be saved, error = \(error!.localizedDescription)")
}
}
}
}
.contentShape(Rectangle())
}
}
extension Task {
static func save(in managedObjectContext: NSManagedObjectContext, completion: #escaping (Bool, NSError?) -> Void ) {
managedObjectContext.performAndWait() {
do {
try managedObjectContext.save()
completion(true, nil)
} catch {
let nserror = error as NSError
print("****** Error: Unresolved error \(nserror), \(nserror.userInfo)")
completion(false, nserror)
}
}
}
}
Any suggestions?
You seem to be creating your tab view in a different way than me. Those extra navigation views made cause an issue.
Not sure if it will help or not but I do it like that:
struct ContentView: View {
var body: some View {
TabView {
TasksListView()
.tabItem {
Image(systemName: "tray.full")
.font(.title)
Text("Master")
}
EmptyView()
.tabItem {
Image(systemName: "magnifyingglass")
.font(.title)
Text("Search")
}
}
}
}
This is a bug in iOS 13.
It has been fixed since iOS 14.0 beta 3.
You will find a similar question here (the accepted answer provides a workaround):
Issue when rearranging List item in detail view using SwiftUI Navigation View and Sorted FetchRequest