How do I do drag and drop on a LazyVGrid using core data entities? - core-data

My end goal is to have a maneuverable list (like swiftui's native list) that reorders the position of items while you drag them.
I am trying to allow dragging and dropping of items in a LazyVGrid, but unlike the answer in SwiftUI | Using onDrag and onDrop to reorder Items within one single LazyGrid?, I am using Core Data, and therefore my array of items is not an observable object and therefore cannot be passed as an #Binding for easy reordering.
Here is what I have:
import SwiftUI
struct TopListTest: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(sortDescriptors: [NSSortDescriptor(key: "order", ascending: true)])
var array: FetchedResults<Item> //An array of items pulled from Core Data
#State private var dragging: Item?
var body: some View {
NavigationView {
ZStack(alignment: .top) {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: .greatestFiniteMagnitude))]) {
ForEach(array) { item in
listItemView(item: item)
.onDrag {
self.dragging = item
return NSItemProvider(object: String(item.order) as NSString)
}
.onDrop(of: ["Item"], delegate: DragRelocateDelegate(item: item, current: $dragging))
}
}
}
}
}
}
}
struct DragRelocateDelegate: DropDelegate {
//Where I would like to pass Core Data array, this would only be a copy, however
var item: Item
#Binding var current: Item?
func performDrop(info: DropInfo) -> Bool {
if item != current {
let from = current!.order
let to = item.order
if from != to {
item.order = from
current!.order = to
}
}
return true
}
}
struct listItemView: View {
var item: Item
var body: some View {
HStack {
Text("\(item.order)")
Spacer()
Text(item.name ?? "")
}
}
}
This code makes a simple list of core data entity Item which has an order which is just an id/position number and a name. This allows you to drag and drop items but it only swaps the position of two items and it does not automatically reorder as you drag like swiftui lists.

Related

SwiftUI UISearchController replacement: search field, results and some scrollable content fail to coexist in a meaningful manner

Starting with this
var body: some View {
ScrollView {
VStack(spacing: 0.0) {
Some views here
}
}
.edgesIgnoringSafeArea(.top)
}
How would I add
List(suggestions, rowContent: { text in
NavigationLink(destination: ResultsPullerView(searchText: text)) {
Text(text)
}
})
.searchable(text: $searchText)
on top if that scrollable content?
Cause no matter how I hoax this together when
#State private var suggestions: [String] = []
gets populated (non empty) the search results are not squeezed in (or, better yet, shown on top of
"Some views here"
So what I want to achieve in different terms: search field is on top, scrollable content driven by the search results is underneath, drop down with search suggestions either temporarily squeeses scrollable content down or is overlaid on top like a modal sheet.
Thanks!
If you are looking for UIKit like search behaviour you have to display your results in an overlay:
1. Let's declare a screen to display the results:
struct SearchResultsScreen: View {
#Environment(\.isSearching) private var isSearching
var results: [String]?
var body: some View {
if isSearching, let results {
if results.isEmpty {
Text("nothing to see here")
} else {
List(results, id: \.self) { fruit in
NavigationLink(destination: Text(fruit)) {
Text(fruit)
}
}
}
}
}
}
2. Let's have an ObservableObject to handle the logic:
class Search: ObservableObject {
static private let fruit = [
"Apples 🍏",
"Cherries 🍒",
"Pears 🍐",
"Oranges 🍊",
"Pineapples 🍍",
"Bananas 🍌"
]
#Published var text: String = ""
var results: [String]? {
if text.isEmpty {
return nil
} else {
return Self.fruit.filter({ $0.contains(text)})
}
}
}
3. And lastly lets declare the main screen where the search bar is displayed:
struct ContentView: View {
#StateObject var search = Search()
var body: some View {
NavigationView {
LinearGradient(colors: [.orange, .red], startPoint: .topLeading, endPoint: .bottomTrailing)
.overlay(SearchResultsScreen(results: search.results))
.searchable(text: $search.text)
.navigationTitle("Find that fruit")
}
}
}

How to use a picker on CoreData relationships in SwiftUI

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")
}
}
}
}

Pass FetchedResults through NavigationLink

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()
}

NavigationLinks are being grouped

I have a restaurant menu app that is grouping menu items inside of sections of the menu with NavigationLinks on each menu item which are intended to display a more detailed description of the item. All the menu items in a section are being grouped together as if they were just a single link and triggering the error "Fatal error: UIKitNavigationBridge: multiple active destinations: file SwiftUI". In other words, it is trying to display the detail for all the items within that section when you click on any individual item.
I'm doing this with a section view that displays the various sections and in turn, each section displays the items within that section.
It appears to be a bug in SwiftUI, but since I'm relatively new to SwiftUI, I thought I'd seek more seasoned advice.
import SwiftUI
struct MenuSectionView: View
{
#Environment(\.managedObjectContext) var managedObjectContext
#EnvironmentObject var env: GlobalEnvironment
var group: Group
var items: [MenuItem]
init(group: Group)
{
self.group = group
items = getMenuItems(businessid: group.businessid!, groupid: group.groupid)
}
var body: some View
{
VStack
{
ForEach (items, id: \.itemid)
{
itemx in
if group.groupid == itemx.groupid
{
MenuItemView(item: itemx)
}
}
}
}
}
import SwiftUI
import CoreData
struct MenuItemView: View
{
#Environment(\.managedObjectContext) var context
#EnvironmentObject var env: GlobalEnvironment
var item: MenuItem
init(item: MenuItem)
{
self.item = item
}
var body: some View
{
return VStack
{
NavigationLink(destination: DetailView(item: item))
{
VStack
{
HStack
{
if let image = item.image
{
Image(uiImage: UIImage(data: image)!).resizable().frame(width: 40, height: 40).cornerRadius(5)
} else
{
Image(item.name!).resizable().frame(width: 40, height: 40).cornerRadius(5)
}
Text(item.name!)
}
Text(item.desc!)
}
}
}
}
}
}
Apparently the VStack in the above example was the cause of the error/bug. I eliminated it and now the links work correctly. It is still a bug since in a more complex iteration, the VStack is needed. I found the same thing happens with Buttons within stacks.

SwiftUI TextField CoreData - Changing an attribute's data

I'm trying to use TextField to change the data of an attribute of CoreData, and everything I've come up with hasn't been successful. There is a similar question (listed below), and I'm going to post the code from the correct answer to that to explain it.
struct ItemDetail: View {
#EnvironmentObject var itemStore: ItemStore
let idx: Int
var body: some View {
NavigationView {
Stepper(value: $itemStore.items[idx].inventory) {
Text("Inventory is \(self.itemStore.items[idx].inventory)")
}
// Here I would like to do this
// TextField("PlaceHolder", $itemStore.items[idx].name)
// That doesn't work... also tried
// TextField("PlaceHolder", $name) - where name is a #State String
// How can you then automaticlly assign the new value of #State name
// To $itemStore.items[idx].name?
.padding()
.navigationBarTitle(itemStore.items[idx].name)
}
}
}
Original Question:
SwiftUI #Binding doesn't refresh View
I now have it working.
struct ItemDetail: View {
#EnvironmentObject var itemStore: ItemStore
let idx: Int
// Added new #State variable
#State var name = ""
var body: some View {
NavigationView {
Stepper(value: $itemStore.items[idx].inventory) {
Text("Inventory is \(self.itemStore.items[idx].inventory)")
}
TextField("Placeholder", text: $name) {
// When the enter key is tapped, this runs.
self.itemStore.items[self.idx].name = self.name
}
.padding()
.navigationBarTitle(itemStore.items[idx].name)
}
}
}

Resources