NavigationLinks are being grouped - core-data

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.

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 do I do drag and drop on a LazyVGrid using core data entities?

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.

Saving Item in CoreData Initiates Navigation

In my ContentView I have a FetchRequest<Project>. I navigate to ProjectView using a NavigationLink. From ProjectView I navigate to AddItemView using another NavigationLink. In AddItemView when I add an Item to the Project and call container.viewContext.save() the AddItemView automatically dismisses back to the ContentView.
My guess is that saving to CoreData updates the FetchRequest<Project> list which in turn updates the views, but I am not sure.
How can I save a new Item to the Project in CoreData and only navigate back to ProjectView and not ContentView?
To reproduce:
Create new Single View App and check Core Data and Host in CloudKit
In the .xcdatamodel delete the default Entity and replace it with an Entity called Project which has attributes date: Date and title: String and an Entity called Item which has an attribute name: String. Give the Project a relationship called items (to type Item) and choose β€œto many” on the right. Give the Item a relationship called project that is the inverse of items.
Replace the code in Persistence.swift with this:
// Persistence.swift
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "CoreDataBug")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
Copy ContentView
// ContentView.swift
import SwiftUI
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
let projects: FetchRequest<Project>
init() {
projects = FetchRequest<Project>(entity: Project.entity(), sortDescriptors: [
NSSortDescriptor(keyPath: \Project.date, ascending: false)
])
}
var body: some View {
NavigationView {
List {
ForEach(projects.wrappedValue) { project in
NavigationLink(destination: ProjectView(project: project)) {
Text(project.title ?? "Title")
}
}
}
.navigationTitle("Projects")
.toolbar {
ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) {
Button {
withAnimation {
let project = Project(context: moc)
let now = Date()
project.date = now
project.title = now.description
try? moc.save()
}
} label: {
Label("Add Project", systemImage: "plus")
}
}
}
}
}
}
Create ProjectView.swift and copy this:
// ProjectView.swift
import SwiftUI
struct ProjectView: View {
#ObservedObject var project: Project
var items: [Item] {
project.items?.allObjects as? [Item] ?? []
}
var body: some View {
List {
ForEach(items) { item in
Text(item.name ?? "")
}
}
.toolbar {
ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) {
NavigationLink(destination: AddItemView(project: project)) {
Label("Add Item", systemImage: "plus")
}
}
}
}
}
Create AddItemView.swift and copy this:
import SwiftUI
// AddItemView.swift
import SwiftUI
struct AddItemView: View {
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var moc
let project: Project
#State private var selectedName: String = ""
var body: some View {
TextField("Type name here", text: $selectedName)
.navigationTitle("Add Item")
.navigationBarItems(trailing: Button("Add") {
let ingestion = Item(context: moc)
ingestion.project = project
ingestion.name = selectedName
try? moc.save()
presentationMode.wrappedValue.dismiss()
})
}
}
Run the app. Click the plus on the top right. Click the project that just slid in. In the ProjectView click the plus on the top right again. Type a name in the TextField and click add on the top right. When the AddItemView is dismissed it probably went back to ContentView. If not add another item to the project.

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

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