Using CoreData Objects in a List as Environment Object - core-data

I'm currently creating a News-Feed-Reader App with SwiftUI.
I'm fetching the feed-items and storing them in CoreData
I'd like to display the objects in a List containing NavigationLinks to a Detail View and automatically mark them as read when clicking on them.
I'm currently fetching the objects and putting them in a ObservableObject.
This is the ObservableObject class:
final class FeedItem: ObservableObject, Hashable {
static func == (lhs: FeedItem, rhs: FeedItem) -> Bool {
return lhs.item.pubDate == rhs.item.pubDate && lhs.item.articleUrl == rhs.item.articleUrl
}
func hash(into hasher: inout Hasher) {
hasher.combine(item.articleUrl)
}
let objectWillChange = PassthroughSubject<Void, Never>()
init(item: NewsItem) {
self.item = item
}
// NewsItem is the Managed Object
var item: NewsItem
var bookmarked: Bool {
set {
//This function fetches the Object and marks it as bookmarked
DatabaseManager().markAs(item: item, .read, newValue)
self.item.bookmarked = newValue
}
get {
self.item.bookmarked
}
}
var read: Bool {
set {
//This function fetches the Object and marks it as read
DatabaseManager().markAs(item: item, .read, newValue)
self.item.read = newValue
}
get {
self.item.read
}
}
}
In the moment Im creating an environment Object (EO) containing an array of all ObservableObjects
This EO is passed down to the list and whenever Im clicking on an item Im setting its read value to true thereby changing the Core Data Object.
This is the list:
#EnvironmentObject var feed: // THe array of ObservableObjects
List() {
ForEach(feed.items.indices, id:\.self) { i in
Button(action: {
self.feed.items[i].read = true
self.selectedItem = i
self.showDetail = true
}) {
ListFeedItem(item: self.$feed.items[i])
}
}
}
This method is quite slow. Whenever I'm opening the Detail View and going back a few seconds later the List-Item takes multiple seconds to refresh.
Any ideas on how I could improve this?

Related

SwiftUI ForEach force UI update when updating the contents of a core data relationship

My app is meant to have a bunch of workouts in core data, each with a relationship to many exercises. A view should display the data in each workout (name, description etc.) and then iterate and display each exercise belonging to that workout.
Adding exercises and displaying them works fine. If an exercise is deleted, however it:
deletes from coredata no worries
the information seems to delete from iterableExercises
however, the Text line does not disappear. it goes from, for example "Squat, Description" to simply " , "
If I close the app entirely and reopen, then the " , " lines do completely disappear.
The problem code:
if let iterableExercises = workout.exercises?.array as? [ExerciseEntity] {
ForEach(iterableExercises) {exercise in
Text("\(exercise.name ?? ""), \(exercise.desc ?? "")")
}
}
I've got the entity relationship set as ordered, but I've also tried unordered with .allObjects instead of .array. This clearly isn't the problem as it's the array iterableExercises that's not correctly being reset?
EDIT: to reproduce, here's all the code you need and some screenshots of the CoreData model.
import SwiftUI
import CoreData
class ViewModel: ObservableObject {
let container: NSPersistentCloudKitContainer
#Published var savedWorkouts: [WorkoutEntity] = []
#Published var savedExercises: [ExerciseEntity] = []
// MARK: INIT
init() {
container = NSPersistentCloudKitContainer(name: "mre")
container.loadPersistentStores { description, error in
if let error = error {
print("Error loading CoreData: \(error)")
}
}
fetchWorkoutEntities()
fetchExerciseEntities()
}
// MARK: FETCHERS
func fetchWorkoutEntities() {
let request = NSFetchRequest<WorkoutEntity>(entityName: "WorkoutEntity")
do {
savedWorkouts = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching WorkoutEntity: \(error)")
}
}
func fetchExerciseEntities() {
let request = NSFetchRequest<ExerciseEntity>(entityName: "ExerciseEntity")
do {
savedExercises = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching ExerciseEntity: \(error)")
}
}
// MARK: SAVE
func saveData() {
do {
try container.viewContext.save()
fetchWorkoutEntities()
fetchExerciseEntities()
} catch let error {
print("Error saving: \(error)")
}
}
// MARK: ADDERS
func addWorkout(name: String) {
let _ = WorkoutEntity(context: container.viewContext)
saveData()
}
func addExerciseToWorkout(workout: WorkoutEntity, name: String) {
let newExercise = ExerciseEntity(context: container.viewContext)
newExercise.name = name
workout.addToExercises(newExercise)
saveData()
}
// MARK: DELETERS
func deleteWorkout(workout: WorkoutEntity) {
container.viewContext.delete(workout)
saveData()
}
func deleteExercise(exercise: ExerciseEntity) {
container.viewContext.delete(exercise)
saveData()
}
// MARK: TODO: UPDATERS
}
struct ContentView: View {
#StateObject var data = ViewModel()
var body: some View {
VStack {
Button {
data.addWorkout(name: "workout")
data.addExerciseToWorkout(workout: data.savedWorkouts[0], name: "[exercisename]")
} label: {
Text("Click ONCE to add workout to work with")
}
Spacer()
if let iterableExercises = data.savedWorkouts[0].exercises?.array as? [ExerciseEntity] {
ForEach(iterableExercises) { exercise in
Button {
data.deleteExercise(exercise: exercise)
} label: {
Text("Click to delete \(exercise.name ?? "") AFTER DELETING IF THIS STILL SHOWS BUT DOESN'T SHOW THE EXERCISE NAME THEN IT'S BROKEN")
}
}
}
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
screenshots of model
I’m not sure if this is the ONLY solution as #malhal gave quite an extensive and seemingly useful response.
But I came across a much easier and immediate fix, within my original solution. The inverse relationships must be specified. Doing this resolved all issues.
We don't use view model objects in SwiftUI. You need to learn the View struct and property wrappers which gives the consistency and efficiency of value types with the benefits of reference types. The property wrapper for core data is #FetchRequest which invalidates the View when the results change. It's also a DynamicProperty (which is how it gets the context from the environment) that you can use it directly without the property wrapper syntax which allows you to use a param in a predicate, in your case to do fetch the one-to-many relation, e.g.
struct WorkoutView: View {
private var fetchRequest: FetchRequest<Exercise>
private var exercices: FetchedResults<Exercise> {
fetchRequest.wrappedValue
}
init(workout: Workout) {
let sortAscending = true
let sortDescriptors = [SortDescriptor(\Exercise.timestamp, order: sortAscending ? .forward : .reverse)]
fetchRequest = FetchRequest(sortDescriptors: sortDescriptors, predicate: NSPredicate(format: "workout = %#", workout), animation: .default)
}
var body: some View {
List(exercises) { exercise in
ExerciseView(exercise: exercise)
}
}
}
For creating the NSPersistentContainer check out the Xcode App template with Core Data checked. Looks like this:
#main
struct TestApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
The reason it is not an #StateObject is we don't want to invalidate this body when it changes and we need it to be init for previewing which is a different singleton.
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
... see template
That other code in your view model class can be moved to NSManagedObject and NSManagedObjectContext extensions. Use the Editor menu to generate the NSManagedObject extension for the model, the files need tidying up though and make sure use extension is selected for the entity.

Count core data attributes that fulfill a certain condition (SwiftUI)

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
}

SwiftUI reorder CoreData Objects in List

I want to change the order of the rows in a list that retrieves objects from the core data. Moving rows works, but the problem is that I can't save the changes. I don't know how to save the changed Index of the CoreData Object.
Here is my Code:
Core Data Class:
public class CoreItem: NSManagedObject, Identifiable{
#NSManaged public var name: String
}
extension CoreItem{
static func getAllCoreItems() -> NSFetchRequest <CoreItem> {
let request: NSFetchRequest<CoreItem> = CoreItem.fetchRequest() as! NSFetchRequest<CoreItem>
let sortDescriptor = NSSortDescriptor(key: "date", ascending: true)
request.sortDescriptors = [sortDescriptor]
return request
}
}
extension Collection where Element == CoreItem, Index == Int {
func move(set: IndexSet, to: Int, from managedObjectContext: NSManagedObjectContext) {
do {
try managedObjectContext.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
List:
struct CoreItemList: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: CoreItem.getAllCoreItems()) var CoreItems: FetchedResults<CoreItem>
var body: some View {
NavigationView{
List {
ForEach(CoreItems, id: \.self){
coreItem in
CoreItemRow(coreItem: coreItem)
}.onDelete {
IndexSet in let deleteItem = self.CoreItems[IndexSet.first!]
self.managedObjectContext.delete(deleteItem)
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
}
.onMove {
self.CoreItems.move(set: $0, to: $1, from: self.managedObjectContext)
}
}
.navigationBarItems(trailing: EditButton())
}.navigationViewStyle(StackNavigationViewStyle())
}
}
Thank you for help.
Caveat: the answer below is untested, although I used parallel logic in a sample project and that project seems to be working.
There's a couple parts to the answer. As Joakim Danielson says, in order to persist the user's preferred order you will need to save the order in your CoreItem class. The revised class would look like:
public class CoreItem: NSManagedObject, Identifiable{
#NSManaged public var name: String
#NSManaged public var userOrder: Int16
}
The second part is to keep the items sorted based on the userOrder attribute. On initialization the userOrder would typically default to zero so it might be useful to also sort by name within userOrder. Assuming you want to do this, then in CoreItemList code:
#FetchRequest( entity: CoreItem.entity(),
sortDescriptors:
[
NSSortDescriptor(
keyPath: \CoreItem.userOrder,
ascending: true),
NSSortDescriptor(
keyPath:\CoreItem.name,
ascending: true )
]
) var coreItems: FetchedResults<CoreItem>
The third part is that you need to tell swiftui to permit the user to revise the order of the list. As you show in your example, this is done with the onMove modifier. In that modifier you perform the actions needed to re-order the list in the user's preferred sequence. For example, you could call a convenience function called move so the modifier would read:
.onMove( perform: move )
Your move function will be passed an IndexSet and an Int. The index set contains all the items in the FetchRequestResult that are to be moved (typically that is just one item). The Int indicates the position to which they should be moved. The logic would be:
private func move( from source: IndexSet, to destination: Int)
{
// Make an array of items from fetched results
var revisedItems: [ CoreItem ] = coreItems.map{ $0 }
// change the order of the items in the array
revisedItems.move(fromOffsets: source, toOffset: destination )
// update the userOrder attribute in revisedItems to
// persist the new order. This is done in reverse order
// to minimize changes to the indices.
for reverseIndex in stride( from: revisedItems.count - 1,
through: 0,
by: -1 )
{
revisedItems[ reverseIndex ].userOrder =
Int16( reverseIndex )
}
}
Technical reminder: the items stored in revisedItems are classes (i.e., by reference), so updating these items will necessarily update the items in the fetched results. The #FetchedResults wrapper will cause your user interface to reflect the new order.
Admittedly, I'm new to SwiftUI. There is likely to be a more elegant solution!
Paul Hudson (Hacking With Swift) has quite a bit more detail. Here is a link for info on moving data in a list. Here is a link for using core data with SwiftUI (it involves deleting items in a list, but is closely analogous to the onMove logic)
Below you can find a more generic approach to this problem. The algorithm minimises the number of CoreData entities that require an update, to the contrary of the accepted answer. My solution is inspired by the following article: https://www.appsdissected.com/order-core-data-entities-maximum-speed/
First I declare a protocol as follows to use with your model struct (or class):
protocol Sortable {
var sortOrder: Int { get set }
}
As an example, assume we have a SortItem model which implements our Sortable protocol, defined as:
struct SortItem: Identifiable, Sortable {
var id = UUID()
var title = ""
var sortOrder = 0
}
We also have a simple SwiftUI View with a related ViewModel defined as (stripped down version):
struct ItemsView: View {
#ObservedObject private(set) var viewModel: ViewModel
var body: some View {
NavigationView {
List {
ForEach(viewModel.items) { item in
Text(item.title)
}
.onMove(perform: viewModel.move(from:to:))
}
}
.navigationBarItems(trailing: EditButton())
}
}
extension ItemsView {
class ViewModel: ObservableObject {
#Published var items = [SortItem]()
func move(from source: IndexSet, to destination: Int) {
items.move(fromOffsets: source, toOffset: destination)
// Note: Code that updates CoreData goes here, see below
}
}
}
Before I continue to the algorithm, I want to note that the destination variable from the move function does not contain the new index when moving items down the list. Assuming that only a single item is moved, retrieving the new index (after the move is complete) can be achieved as follows:
func move(from source: IndexSet, to destination: Int) {
items.move(fromOffsets: source, toOffset: destination)
if let oldIndex = source.first, oldIndex != destination {
let newIndex = oldIndex < destination ? destination - 1 : destination
// Note: Code that updates CoreData goes here, see below
}
}
The algorithm itself is implemented as an extension to Array for the case that the Element is of the Sortable type. It consists of a recursive updateSortOrder function as well as a private helper function enclosingIndices which retrieves the indices that enclose around a certain index of the array, whilst remaining within the array bounds. The complete algorithm is as follows (explained below):
extension Array where Element: Sortable {
func updateSortOrder(around index: Int, for keyPath: WritableKeyPath<Element, Int> = \.sortOrder, spacing: Int = 32, offset: Int = 1, _ operation: #escaping (Int, Int) -> Void) {
if let enclosingIndices = enclosingIndices(around: index, offset: offset) {
if let leftIndex = enclosingIndices.first(where: { $0 != index }),
let rightIndex = enclosingIndices.last(where: { $0 != index }) {
let left = self[leftIndex][keyPath: keyPath]
let right = self[rightIndex][keyPath: keyPath]
if left != right && (right - left) % (offset * 2) == 0 {
let spacing = (right - left) / (offset * 2)
var sortOrder = left
for index in enclosingIndices.indices {
if self[index][keyPath: keyPath] != sortOrder {
operation(index, sortOrder)
}
sortOrder += spacing
}
} else {
updateSortOrder(around: index, for: keyPath, spacing: spacing, offset: offset + 1, operation)
}
}
} else {
for index in self.indices {
let sortOrder = index * spacing
if self[index][keyPath: keyPath] != sortOrder {
operation(index, sortOrder)
}
}
}
}
private func enclosingIndices(around index: Int, offset: Int) -> Range<Int>? {
guard self.count - 1 >= offset * 2 else { return nil }
var leftIndex = index - offset
var rightIndex = index + offset
while leftIndex < startIndex {
leftIndex += 1
rightIndex += 1
}
while rightIndex > endIndex - 1 {
leftIndex -= 1
rightIndex -= 1
}
return Range(leftIndex...rightIndex)
}
}
First, the enclosingIndices function. It returns an optional Range<Int>. The offset argument defines the distance for the enclosing indices left and right of the index argument. The guard ensures that the complete enclosing indices are contained within the array. Further, in case the offset goes beyond the startIndex or endIndex of the array, the enclosing indices will be shifted to the right or left, respectively. Hence, at the boundaries of the array, the index is not necessarily located in the middle of the enclosing indices.
Second, the updateSortOrder function. It requires at least the index around which the update of the sorting order should be started. This is the new index from the move function in the ViewModel. Further, the updateSortOrder expects an #escaping closure providing two integers, which will be explained below. All other arguments are optional. The keyPath is defaulted to \.sortOrder in conformance with the expectations from the protocol. However, it can be specified if the model parameter for sorting differs. The spacing argument defines the sort order spacing that is typically used. The larger this value, the more sort operations can be performed without requiring any other CoreData update except for the moved item. The offset argument should not really be touched and is used in the recursion of the function.
The function first requests the enclosingIndices. In case these are not found, which happens immediately when the array is smaller than three items or either inside one of the recursions of the updateSortOrder function when the offset is such that it would go beyond the boundaries of the array; then the sort order of all items in the array are reset in the else case. In that case, if the sortOrder differs from the items existing value, the #escaping closure is called. It's implementation will be discussed further below.
When the enclosingIndices are found, both the left and right index of the enclosing indices not being the index of the moved item are determined. With these indices known, the existing 'sort order' values for these indices are obtained through the keyPath. It is then verified if these values are not equal (which could occur if the items were added with equal sort orders in the array) as well as if a division of the difference between the sort orders and the number of enclosing indices minus the moved item would result in a non-integer value. This basically checks whether there is a place left for the moved item's potentially new sort order value within the minimum spacing of 1. If this is not the case, the enclosing indices should be expanded to the next higher offset and the algorithm run again, hence the recursive call to updateSortOrder in that case.
When all was successful, the new spacing should be determined for the items between the enclosing indices. Then all enclosing indices are looped through and each item's sorting order is compared to the potentially new sorting order. In case it changed, the #escaping closure is called. For the next item in the loop the sort order value is updated again.
This algorithm results in the minimum amount of callbacks to the #escaping closure. Since this only happens when an item's sort order really needs to be updated.
Finally, as you perhaps guessed, the actual callbacks to CoreData will be handled in the closure. With the algorithm defined, the ViewModel move function is then updated as follows:
func move(from source: IndexSet, to destination: Int) {
items.move(fromOffsets: source, toOffset: destination)
if let oldIndex = source.first, oldIndex != destination {
let newIndex = oldIndex < destination ? destination - 1 : destination
items.updateSortOrder(around: newIndex) { [weak self] (index, sortOrder) in
guard let self = self else { return }
var item = self.items[index]
item.sortOrder = sortOrder
// Note: Callback to interactor / service that updates CoreData goes here
}
}
}
Please let me know if you have any questions regarding this approach. I hope you like it.
Had a problem with Int16 and solved it by changing it to #NSManaged public var userOrder: NSNumber? and in the func: NSNumber(value: Int16( reverseIndex ))
As well I needed to add try? managedObjectContext.save() in the func to actually save the new order.
Now its working fine - thanks!
I'm not sure using a CoreData NSManagedObject for a view model object is the best approach, but if you do below is a sample for moving items in a SwiftUI List and persisting an object value based sort order.
An UndoManager is used in the event an error occurs during the move to rollback any changes.
class Note: NSManagedObject {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Note> {
return NSFetchRequest<Note>(entityName: "Note")
}
#NSManaged public var id: UUID?
#NSManaged public var orderIndex: Int64
#NSManaged public var text: String?
}
struct ContentView: View {
#Environment(\.editMode) var editMode
#Environment(\.managedObjectContext) var viewContext
#FetchRequest(sortDescriptors:
[NSSortDescriptor(key: "orderIndex", ascending: true)],
animation: .default)
private var notes: FetchedResults<Note>
var body: some View {
NavigationView {
List {
ForEach (notes) { note in
Text(note.text ?? "")
}
}
.onMove(perform: moveNotes)
}
.navigationTitle("Notes")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
}
}
func moveNotes(_ indexes: IndexSet, _ i: Int) {
guard
1 == indexes.count,
let from = indexes.first,
from != i
else { return }
var undo = viewContext.undoManager
var resetUndo = false
if undo == nil {
viewContext.undoManager = .init()
undo = viewContext.undoManager
resetUndo = true
}
defer {
if resetUndo {
viewContext.undoManager = nil
}
}
do {
try viewContext.performAndWait {
undo?.beginUndoGrouping()
let moving = notes[from]
if from > i { // moving up
notes[i..<from].forEach {
$0.orderIndex = $0.orderIndex + 1
}
moving.orderIndex = Int64(i)
}
if from < i { // moving down
notes[(from+1)..<i].forEach {
$0.orderIndex = $0.orderIndex - 1
}
moving.orderIndex = Int64(i)
}
undo?.endUndoGrouping()
try viewContext.save()
}
} catch {
undo?.endUndoGrouping()
viewContext.undo()
// TODO: something with the error
// set a state variable to display the error condition
fatalError(error.localizedDescription)
}
}
}
if do like this
.onMove {
self.CoreItems.move(set: $0, to: $1, from: self.managedObjectContext)
try? managedObjectContext.save()

Action when user click on the delete button on the keyboard in SwiftUI

I try to run a function when the user click on the delete button on the keyboard when he try to modify a Textfield.
How can I do that ?
Yes it is possible, however it requires subclassing UITextField and creating your own UIViewRepresentable
This answer is based on the fantastic work done by Costantino Pistagna in his medium article but we need to do a little more work.
Firstly we need to create our subclass of UITextField, this should also conform to the UITextFieldDelegate protocol.
class WrappableTextField: UITextField, UITextFieldDelegate {
var textFieldChangedHandler: ((String)->Void)?
var onCommitHandler: (()->Void)?
var deleteHandler: (() -> Void)?
override func deleteBackward() {
super.deleteBackward()
deleteHandler?()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if let nextField = textField.superview?.superview?.viewWithTag(textField.tag + 1) as? UITextField {
nextField.becomeFirstResponder()
} else {
textField.resignFirstResponder()
}
return false
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let currentValue = textField.text as NSString? {
let proposedValue = currentValue.replacingCharacters(in: range, with: string)
textFieldChangedHandler?(proposedValue as String)
}
return true
}
func textFieldDidEndEditing(_ textField: UITextField) {
onCommitHandler?()
}
}
Because we are creating our own implementation of a TextField we need three functions that we can use for callbacks.
textFieldChangeHandler this will be called when the text property updates and allows us to change the state value associated with our Textfield.
onCommitHandler this will be called when we have finished editing our TextField
deleteHandler this will be called when we perform he delete action.
The code above shows how these are used. The part that you are particularly interested in is the override func deleteBackward(), by overriding this we are able to hook into when the delete button is pressed and perform an action on it. Depending on your use case, you may want the deleteHandler to be called before you call the super.
Next we need to create our UIViewRepresentable.
struct MyTextField: UIViewRepresentable {
private let tmpView = WrappableTextField()
//var exposed to SwiftUI object init
var tag:Int = 0
var placeholder:String?
var changeHandler:((String)->Void)?
var onCommitHandler:(()->Void)?
var deleteHandler: (()->Void)?
func makeUIView(context: UIViewRepresentableContext<MyTextField>) -> WrappableTextField {
tmpView.tag = tag
tmpView.delegate = tmpView
tmpView.placeholder = placeholder
tmpView.onCommitHandler = onCommitHandler
tmpView.textFieldChangedHandler = changeHandler
tmpView.deleteHandler = deleteHandler
return tmpView
}
func updateUIView(_ uiView: WrappableTextField, context: UIViewRepresentableContext<MyTextField>) {
uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
uiView.setContentHuggingPriority(.defaultLow, for: .horizontal)
}
}
This is where we create our SwiftUI version of our WrappableTextField. We create our WrappableTextField and its properties. In the makeUIView function we assign these properties. Finally in the updateUIView we set the content hugging properties, but you may choose not to do that, it really depends on your use case.
Finally we can create a small working example.
struct ContentView: View {
#State var text = ""
var body: some View {
MyTextField(tag: 0, placeholder: "Enter your name here", changeHandler: { text in
// update the state's value of text
self.text = text
}, onCommitHandler: {
// do something when the editing finishes
print("Editing ended")
}, deleteHandler: {
// do something here when you press delete
print("Delete pressed")
})
}
}

Is it possible to implement.onDelete and .onMove functionality to Core Data-backed .listStyle(GroupedListStyle()) in SwiftUI?

I'm able to get a Core Data backed flat list working (with no .listStyle modifier) with delete and move functionality.
But when I tried to make list grouped
}.listStyle(GroupedListStyle())
the wheels fall off conceptually. The onDelete modifier parameter has a function signature of IndexSet? -> Void. So I can't pass in the object to be deleted.
onMove is essentially the same problem, except worse. Both modifiers rely on a data source assumed to be a flat array of sequential values which can be accessed by IndexSet subscription. But I can't think how to build a grouped list using a flat datasource.
My view body looks like this:
//I'm building the list using two independent arrays. This makes onDelete impossible to implement as recommended
ForEach(folders, id: \.self) { folder in
Section(header: Text(folder.title) ) {
ForEach(self.allProjects.filter{$0.folder == folder}, id: \.self){ project in
Text(project.title)
//this modifier is where the confusion starts:
}.onDelete(perform: self.delete)
}
}
}.listStyle(GroupedListStyle())
func delete (at offsets: IndexSet) {
// ??.remove(atOffsets: offsets)
//Since I use two arrays to construct group list, I can't use generic remove at Offsets call. And I can't figure out a way to pass in the managed object.
}
func move (from source: IndexSet, to destination: Int) {
////same problem here. a grouped list has Dynamic Views produced by multiple arrays, instead of the single array the move function is looking for.
}
Can't you store the result of the filter and pass that on inside .onDelete to your custom delete method? Then delete would mean deleting the items inside the IndexSet. Is moving between sections possible? Or do you just mean inside each folder? If only inside each folder you can use the same trick, use the stored projects and implement move manually however you determine position in CoreData.
The general idea is the following:
import SwiftUI
class FoldersStore: ObservableObject {
#Published var folders: [MyFolder] = [
]
#Published var allProjects: [Project] = [
]
func delete(projects: [Project]) {
}
func move(projects: [Project], set: IndexSet, to: Int) {
}
}
struct MyFolder: Identifiable {
let id = UUID()
var title: String
}
struct Project: Identifiable {
let id = UUID()
var title: String
var folder: UUID
}
struct FoldersAndFilesView: View {
var body: some View {
FoldersAndFilesView_NeedsEnv().environmentObject(FoldersStore())
}
}
struct FoldersAndFilesView_NeedsEnv: View {
#EnvironmentObject var store: FoldersStore
var body: some View {
return ForEach(store.folders) { (folder: MyFolder) in
Section(header: Text(folder.title) ) {
FolderView(folder: folder)
}
}.listStyle(GroupedListStyle())
}
}
struct FolderView: View {
var folder: MyFolder
#EnvironmentObject var store: FoldersStore
func projects(for folder: MyFolder) -> [Project] {
return self.store.allProjects.filter{ project in project.folder == folder.id}
}
var body: some View {
let projects: [Project] = self.projects(for: folder)
return ForEach(projects) { (project: Project) in
Text(project.title)
}.onDelete {
self.store.delete(projects: $0.map{
return projects[$0]
})
}.onMove {
self.store.move(projects: projects, set: $0, to: $1)
}
}
}
You are correct that the key to doing what you want is getting one array of objects and grouping it appropriately. In your case, it's your Projects. You do not show your CoreData schema, but I would expect that you have a "Projects" entity and a "Folders" entity and a one-to-many relationship between them. Your goal is to create a CoreData query that creates that array of Projects and groups them by Folder. Then the real key is to use CoreData's NSFetchedResultsController to create the groups using the sectionNameKeyPath.
It's not practical for me to send you my entire project, so I will try to give you enough pieces of my working code to point you in the right direction. When I have a chance, I will add this concept into the sample program that I just published on GitHub. https://github.com/Whiffer/SwiftUI-Core-Data-Test
This is the essence of your List:
#ObservedObject var dataSource =
CoreDataDataSource<Project>(sortKey1: "folder.order",
sortKey2: "order",
sectionNameKeyPath: "folderName")
var body: some View {
List() {
ForEach(self.dataSource.sections, id: \.name) { section in
Section(header: Text(section.name.uppercased()))
{
ForEach(self.dataSource.objects(forSection: section)) { project in
ListCell(project: project)
}
}
}
}
.listStyle(GroupedListStyle())
}
Portions of CoreDataDataSource:
let frc = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: McDataModel.stack.context,
sectionNameKeyPath: sectionNameKeyPath,
cacheName: nil)
frc.delegate = self
public func performFetch() {
do {
try self.frc.performFetch()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
private var fetchedObjects: [T] {
return frc.fetchedObjects ?? []
}
public var sections: [NSFetchedResultsSectionInfo] {
self.performFetch()
return self.frc.sections!
}
public func objects(forSection: NSFetchedResultsSectionInfo) -> [T] {
return forSection.objects as! [T]
}
public func move(from source: IndexSet, to destination: Int) {
self.reorder(from: source, to: destination, within: self.fetchedObjects)
}
If you want to easily delete things from a sectioned (doesn't have to be grouped!) List, you need to take advantage of your nesting. Consider you have the following:
List {
ForEach(self.folders) { folder in
Section(header: folder.title) {
ForEach(folder.items) { project in
ProjectCell(project)
}
}
}
}
Now you want to set up .onDelete. So let's zoom in on the Section declaration:
Section(header: Text(...)) {
...
}
.onDelete { deletions in
// you have access to the current `Folder` at this level of nesting
// this is confirmed to work with singular deletion, not multi-select deletion
// I would hope that this actually gets called once per section that contains a deletion
// but that is _not_ confirmed
guard !deletions.isEmpty else { return }
self.delete(deletions, in: folder)
}
func delete(_ indexes: IndexSet, in folder: Folder) {
// you can now delete this bc you have your managed object type and indexes into the project structure
}

Resources