Learning swiftui by building an app with core data; stuck in an issue of data flow from Detail to Edit of AddEdit; the flows from AddEdit to List and from List to Detail are ok. Searched but didn't find useful info online or I don't understand. Here is a simplified project for the question. It complies ok on 13.2 beta and works on simulator, with the issue of blank Edit view from Detail.
views:
struct FileList: View {
@FetchRequest(sortDescriptors: [ NSSortDescriptor(keyPath: \Item.fileName, ascending: false) ], animation: .default) var items: FetchedResults<Item>
@State private var showAdd = false
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink(destination: FileDetail(item: item)) {
Text(item.fileName ?? "").font(.headline)
}
}
}
.navigationTitle("List")
.navigationBarItems(trailing: Button(action: {
showAdd = true
}, label: { Image(systemName: "plus.circle")
})
.sheet(isPresented: $showAdd) {
FileAddEdit(items: VM())
}
)
}
}
}
struct FileList_Previews: PreviewProvider {
static let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
static var previews: some View {
FileList()
}
}
struct FileDetail: View {
@Environment(\.managedObjectContext) var context
@Environment(\.presentationMode) var presentationMode
@State var showingEdit = false
@ObservedObject var item: Item
var body: some View {
VStack {
Form {
Text(self.item.fileName ?? "File Name")
Button(action: {
showingEdit.toggle()
}, label: {
title: do { Text("Edit")
}
})
.sheet(isPresented: $showingEdit) {
FileAddEdit(items: VM())
}
}
}.navigationTitle("Detail")
}
}
struct FileDetails_Previews: PreviewProvider {
static let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
static var previews: some View {
let item = Item(context: moc)
return NavigationView {
FileDetail(item: item)
}
}
}
struct FileAddEdit: View {
@Environment(\.managedObjectContext) var moc
@ObservedObject var items = VM()
var body: some View {
NavigationView {
VStack {
Form {
TextField("File Name", text: $items.fileName)
Button(action: {
items.writeData(context: moc)
}, label: {
title: do { Text(items.updateFile == nil ? "Add" : "Edit")
}})
}
}
.navigationTitle("\(items.updateFile == nil ? "Add" : "Edit")")
}
}
}
struct FileAddEdit_Previews: PreviewProvider {
static var previews: some View {
FileAddEdit(items: VM())
}
}
VM:
class VM: ObservableObject {
@Published var fileName = ""
@Published var id = UUID()
@Published var isNewData = false
@Published var updateFile : Item!
init() {
}
var temporaryStorage: [String] = []
func writeData(context : NSManagedObjectContext) {
if updateFile != nil {
updateCurrentFile()
} else {
createNewFile(context: context)
}
do {
try context.save()
} catch {
print(error.localizedDescription)
}
}
func DetailItem(fileItem: Item){
fileName = fileItem.fileName ?? ""
id = fileItem.id ?? UUID()
updateFile = fileItem
}
func EditItem(fileItem: Item){
fileName = fileItem.fileName ?? ""
id = fileItem.id ?? UUID()
isNewData.toggle()
updateFile = fileItem
}
private func createNewFile(context : NSManagedObjectContext) {
let newFile = Item(context: context)
newFile.fileName = fileName
newFile.id = id
}
private func updateCurrentFile() {
updateFile.fileName = fileName
updateFile.id = id
}
private func resetData() {
fileName = ""
id = UUID()
isNewData.toggle()
updateFile = nil
}
}
Much appreciated for your time and advices!
Here is a simplified version of your code Just paste this code into your project and call YourAppParent()
in a body
somewhere in your app as high up as possible since it creates the container.
import SwiftUI
import CoreData
//Class to hold all the Persistence methods
class CoreDataPersistence: ObservableObject{
//Use preview context in canvas/preview
let context = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" ? PersistenceController.preview.container.viewContext : PersistenceController.shared.container.viewContext
///Creates an NSManagedObject of **ANY** type
func create<T: NSManagedObject>() -> T{
T(context: context)
//For adding Defaults see the `extension` all the way at the bottom of this post
}
///Updates an NSManagedObject of any type
func update<T: NSManagedObject>(_ obj: T){
//Make any changes like a last modified variable
//Figure out the type if you want type specific changes
if obj is FileEnt{
//Make type specific changes
let name = (obj as! FileEnt).fileName
print("I'm updating FileEnt \(name ?? "no name")")
}else{
print("I'm Something else")
}
save()
}
///Creates a sample FileEnt
//Look at the preview code for the `FileEdit` `View` to see when to use.
func addSample() -> FileEnt{
let sample: FileEnt = create()
sample.fileName = "Sample"
sample.fileDate = Date.distantFuture
return sample
}
///Deletes an NSManagedObject of any type
func delete(_ obj: NSManagedObject){
context.delete(obj)
save()
}
func resetStore(){
context.rollback()
save()
}
func save(){
do{
try context.save()
}catch{
print(error)
}
}
}
//Entry Point
struct YourAppParent: View{
@StateObject var coreDataPersistence: CoreDataPersistence = .init()
var body: some View{
FileListView()
//@FetchRequest needs it
.environment(\.managedObjectContext, coreDataPersistence.context)
.environmentObject(coreDataPersistence)
}
}
struct FileListView: View {
@EnvironmentObject var persistence: CoreDataPersistence
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \FileEnt.fileDate, ascending: true)],
animation: .default)
private var allFiles: FetchedResults<FileEnt>
var body: some View {
NavigationView{
List{
//Has to be lazy or it will create a bunch of objects because the view gets preloaded
LazyVStack{
NavigationLink(destination: FileAdd(), label: {
Text("Add file")
Spacer()
Image(systemName: "plus")
})
}
ForEach(allFiles) { aFile in
NavigationLink(destination: FileDetailView(aFile: aFile)) {
Text(aFile.fileDate?.description ?? "no date")
}.swipeActions(edge: .trailing, allowsFullSwipe: true, content: {
Button("delete", role: .destructive, action: {
persistence.delete(aFile)
})
})
}
}
}
}
}
struct FileListView_Previews: PreviewProvider {
static var previews: some View {
YourAppParent()
// let pers = CoreDataPersistence()
// FileListView()
// @FetchRequest needs it
// .environment(\.managedObjectContext, pers.context)
// .environmentObject(pers)
}
}
struct FileDetailView: View {
@EnvironmentObject var persistence: CoreDataPersistence
@ObservedObject var aFile: FileEnt
@State var showingFileEdit: Bool = false
var body: some View{
Form {
Text(aFile.fileName ?? "")
}
Button(action: {
showingFileEdit.toggle()
}, label: {
Text("Edit")
})
.sheet(isPresented: $showingFileEdit, onDismiss: {
//Discard any changes that were not saved
persistence.resetStore()
}) {
FileEdit(aFile: aFile)
//sheet needs reinject
.environmentObject(persistence)
}
}
}
///A Bridge to FileEdit that creates the object to be edited
struct FileAdd:View{
@EnvironmentObject var persistence: CoreDataPersistence
//This will not show changes to the variables in this View
@State var newFile: FileEnt? = nil
var body: some View{
Group{
if let aFile = newFile{
FileEdit(aFile: aFile)
}else{
//Likely wont ever be visible but there has to be a fallback
ProgressView()
.onAppear(perform: {
newFile = persistence.create()
})
}
}
.navigationBarHidden(true)
}
}
struct FileEdit: View {
@EnvironmentObject var persistence: CoreDataPersistence
@Environment(\.dismiss) var dismiss
//This will observe changes to variables
@ObservedObject var aFile: FileEnt
var viewHasIssues: Bool{
aFile.fileDate == nil || aFile.fileName == nil
}
var body: some View{
Form {
TextField("required", text: $aFile.fileName.bound)
//DatePicker can give the impression that a date != nil
if aFile.fileDate != nil{
DatePicker("filing date", selection: $aFile.fileDate.bound)
}else{
//Likely wont ever be visible but there has to be a fallback
ProgressView()
.onAppear(perform: {
//Set Default
aFile.fileDate = Date()
})
}
}
Button("save", role: .none, action: {
persistence.update(aFile)
dismiss()
}).disabled(viewHasIssues)
Button("cancel", role: .destructive, action: {
persistence.resetStore()
dismiss()
})
}
}
extension Optional where Wrapped == String {
var _bound: String? {
get {
return self
}
set {
self = newValue
}
}
var bound: String {
get {
return _bound ?? ""
}
set {
_bound = newValue
}
}
}
extension Optional where Wrapped == Date {
var _bound: Date? {
get {
return self
}
set {
self = newValue
}
}
public var bound: Date {
get {
return _bound ?? Date.distantPast
}
set {
_bound = newValue
}
}
}
For adding a preview that requires an object you can use this code with the new CoreDataPersistence
/// How to create a preview that requires a CoreData object.
struct FileEdit_Previews: PreviewProvider {
static let pers = CoreDataPersistence()
static var previews: some View {
VStack{
FileEdit(aFile: pers.addSample()).environmentObject(pers)
}
}
}
And since the create()
is now generic you can use the Entity's extension
to add defaults to the variables.
extension FileEnt{
public override func awakeFromInsert() {
//Set defaults here
self.fileName = ""
self.fileDate = Date()
}
}