SwiftUI: NavigationView/List with programmatic selection under macOS - swift

I have a NavigationView/List combination that allows programmatic selection. The basic concept is similar to the one described here: https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-programmatic-navigation-in-swiftui
With so many things, this works fine on iOS, but on macOS there's an issue: the EmptyView from the NavigationView becomes visible as soon as an item is selected:
Does anybody know how to remove this unwanted EmptyView()?
Here's a demo project:
struct ContentView: View {
#ObservedObject private var state = State()
var body: some View {
NavigationView {
VStack {
List(state.items, id: \.self, selection: $state.selected) { item in
Text(item)
.buttonStyle(.borderless)
}
// navigation to the detail view if an item is selected
if let selected = state.selected {
NavigationLink(
destination: Text(selected),
isActive: state.hasSelectionBinding
) {
EmptyView()
}
}
}
}
}
}
class State: ObservableObject {
#Published var items: [String] = ["Item 1", "Item 2", "Item 3"]
#Published var selected: String?
var hasSelectionBinding: Binding<Bool> {
Binding(
get: { self.selected != nil },
set: {
if $0 == false {
self.selected = nil
}
}
)
}
}
EDIT 1: I've tried putting the NavigationLink into a background modifier on the stack, but now the "Empty View" appears next to "Item 3":
var body: some View {
NavigationView {
VStack {
List(state.items, id: \.self, selection: $state.selected) { item in
Text(item)
.buttonStyle(.borderless)
}
}
.background(Group {
// navigation to the detail view if an item is selected
if let selected = state.selected {
NavigationLink(
destination: Text(selected),
isActive: state.hasSelectionBinding
) {
EmptyView()
}
}
})
}
}

you could try this workaround in your first ContentView code:
NavigationLink("", // <--- here
destination: Text(selected),
isActive: state.hasSelectionBinding
).position(x: 9999, y: 9999) // <--- here
No need for the EmptyView().
Note, I suggest you rename your State model, since SwiftUI already has a State struct.

Related

How to manually set selection on SwiftUI List?

Same question here: How to stop showing Detail view when item in Master view deleted?.
Now I am developing a macOS app, which there's a List and a Detail view, also there's a selection binding the list row, which use to delete the row. But when I delete the row, the detail view didn't disappears.
Also there's an ADD button, when user click it, a new row will append to the last positon. But the selection always stay on the last postion, so I want to change to the new created one.
Here's the code:
struct DetailView: View {
var item: String
var body: some View {
Text("Detail of \(item)")
}
}
struct ContentView: View {
#State private var items = ["Item 1", "Item 2", "Item 3"]
#State private var selection: String?
var body: some View {
NavigationView {
VStack {
List {
ForEach(items, id: \.self, selection: $selection) { item in
NavigationLink(destination: DetailView(item: item)) {
Text(item)
}
}
.onDelete(perform: delete)
}
Button {
let newCreatedItem = add()
selection = newCreatedItem // not work!
} label: {
Image(systemName: "add")
}
Button {
if let item = selection {
delete(item: item) // some other function
}
} label: {
Image(systemName: "minus.rectangle.fill")
}
.disabled(selection == nil)
}
Text("Please select an item.")
}
}
func delete(at offsets: IndexSet) {
items.remove(atOffsets: offsets)
}
}
I tried to set selection = nil on deletion, but it doesn't work.
Also I want to set selection = newCreatedItem, it doesn't work.

Edit details of a dynamic list of core data object

I used the Xcode default CoreData template to build my app.
I have tried to use CoreData and create an entity like this:
I then created a AddItemView which allows me to add item to the view.
struct AddItemView: View {
#Environment(\.managedObjectContext) var viewContext
#Environment(\.presentationMode) var presentationMode
#State private var notes = ""
#State private var selectedDate = Date()
var body: some View {
NavigationView {
Form {
Section {
TextField("notes", text: $notes)
}
Section {
DatePicker("", selection: $selectedDate, displayedComponents: .date)
Text("Your selected date: \(selectedDate)")
}
Section {
Button("Save") {
let newItem = Item(context: self.viewContext)
newItem.notes = self.notes
newItem.recordDate = self.selectedDate
newItem.timestamp = Date()
try? self.viewContext.save()
self.presentationMode.wrappedValue.dismiss()
}
}
}
.navigationBarTitle("Add Item")
}
}
}
It works well and can add items.
Then I want to click on each of the item to go to a Detail View. In the DetailView, there should be an edit button to allow me to modify the object.
I therefore created three files for the purpose: ItemHost, DetailView, EditorView
The Navigation Destination of the item will go to the ItemHost.
struct ItemListView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
#State private var showingAddScreen = false
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) { item in
NavigationLink(destination: ItemHost(item: item)) {
VStack {
Text("Item at \(item.timestamp!, formatter: FormatterUtility.dateTimeFormatter)")
Text("notes: \(item.notes ?? "")")
Text("Item Date: \(item.recordDate!, formatter: FormatterUtility.dateFormatter)")
}
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
#if os(iOS)
EditButton()
#endif
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {self.showingAddScreen.toggle()}) {
Label("Add Item", systemImage: "plus")
}
}
}
.sheet(isPresented: $showingAddScreen) {
AddItemView().environment(\.managedObjectContext, self.viewContext)
}
}
}
The ItemHost as follows:
struct ItemHost: View {
#Environment(\.editMode) var editMode
#Environment(\.managedObjectContext) var contextView
#State var item: Item
var body: some View {
NavigationView {
if editMode?.wrappedValue == .active {
Button("Cancel") {
editMode?.animation().wrappedValue = .inactive
}
}
if editMode?.wrappedValue == .inactive {
ItemDetailView(item: item)
} else {
ItemEditor(item: item)
}
}.navigationBarTitle("EditMode Problem")
.navigationBarItems(trailing: EditButton())
}
}
The DetailView is just a view to display the details, without any special.
struct ItemDetailView: View {
#Environment(\.managedObjectContext) var contextView
#Environment(\.presentationMode) var presentationMode
#State private var showingDeleteAlert = false
let item: Item
var body: some View {
VStack {
Text("notes: \(item.notes ?? "")")
Text("Record Date: \(item.recordDate!, formatter: FormatterUtility.dateFormatter)")
}
.navigationBarTitle(Text("Item Detail"), displayMode: .inline)
.alert(isPresented: $showingDeleteAlert) {
Alert(title: Text("Delete Item"), message: Text("Are you sure?"),
primaryButton: .destructive(Text("Delete")) {
self.deleteItem()
}, secondaryButton: .cancel()
)
}
.navigationBarItems(trailing: Button(action: {
self.showingDeleteAlert = true
}) {
Image(systemName: "trash")
})
}
// Problem here
// Can delete the item and go back to list page. But the actual item in the CoreData has not been removed. If I call contextView.save() it will crash.
func deleteItem() {
contextView.delete(item)
presentationMode.wrappedValue.dismiss()
}
}
The EditorView like this:
struct ItemEditor: View {
#Environment(\.presentationMode) var presentation
#State var item: Item
var body: some View {
List {
HStack {
Text("Notes").bold()
TextField("Notes", text: $item.notes) // Error
}
// Error
DatePicker(selection: $item.recordDate, displayedComponents: .date) {
Text("Record Date").bold()
}
}
}
}
A few problem here:
ItemEditor: Cannot convert value of type 'Binding<String?>' to expected argument type 'Binding'. I have no way to pick the original item object values and display it to let the user know what was the old value inside the object.
Nothing to be displayed once I click on the individual navigation item. I expect that it will originally (not edit mode) and then show the detail view. If it is edit mode, then show the editor.
I get confused with the #binding and how to pass the item into the DetailView and also the Editor. How the editor save the data back to the item object in the contextView?
For the deleteItem() in the ItemDetailView. It can remove the item and go back to the ItemListView apparently. However, when I quit the app, and then run again. I found that the item re-appeared again, not really deleted.
Click on the item now, it shows this:
Don't use #State to var Item in Core Data. You should use #ObservedObject instead. It will refresh a parent view after updating data.
Please read this article:
https://purple.telstra.com/blog/swiftui---state-vs--stateobject-vs--observedobject-vs--environme

SwiftUI picker with a button in the navbar

On the track to learn more and more about SwiftUI. I come accross some weird behaviors.
I have a simple view called Modal. I am using a Picker in it and set a title in the navigation bar to go in the detail view.
That works fine. The problem starts when I add a button in the nav bar.
It end up looking like this
Without the + button
With the + button
And the code is the following:
ContentView.swift
import SwiftUI
struct ContentView: View {
#State var isShowing = false
var body: some View {
VStack(content: {
Button("Modal") {
isShowing = true
}
})
.sheet(isPresented: $isShowing, content: content)
}
#ViewBuilder
func content() -> some View {
Modal()
}
}
Modal.swift
import SwiftUI
import Combine
struct Modal: View {
#State var selection: String = ""
#State var list: [String] = ["1", "2", "3", "4", "5", "6"]
var body: some View {
NavigationView(content: {
Form(content: {
self.type()
})
.navigationBarTitle("Modal", displayMode: .inline)
})
}
}
private extension Modal {
func type() -> some View {
Section(content: {
Picker(selection: $selection, label: Text("Type").bold()) {
ForEach(list, id: \.self) { item in
Text(item)
.tag(UUID())
}
.navigationBarTitle("Select")
.navigationBarItems(trailing: button())
}
})
}
func button() -> some View {
HStack(alignment: .center, content: {
Button(action: {
// Action
}) {
Image(systemName: "plus")
}
})
}
}
This is because .navigationBarItems modifier generates flat view from dynamic ForEach views, attach instead it to one view inside ForEach, like
Section(content: {
Picker(selection: $selection, label: Text("Type").bold()) {
ForEach(list, id: \.self) { item in
if item == list.last {
Text(item)
.navigationBarTitle("Select")
.navigationBarItems(trailing: button())
.tag(UUID())
} else {
Text(item)
.tag(UUID())
}
}
}
})

NavigationView pops back to root, omitting intermediate view

In my navigation, I want to be able to go from ContentView -> ModelListView -> ModelEditView OR ModelAddView.
Got this working, my issue now being that when I hit the Back button from ModelAddView, the intermediate view is omitted and it pops back to ContentView; a behaviour that
ModelEditView does not have.
There's a reason for that I guess – how can I get back to ModelListView when dismissing ModelAddView?
Here's the code:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
List{
NavigationLink(
destination: ModelListView(),
label: {
Text("1. Model")
})
Text("2. Model")
Text("3. Model")
}
.padding()
.navigationTitle("Test App")
}
}
}
struct ModelListView: View {
#State var modelViewModel = ModelViewModel()
var body: some View {
List(modelViewModel.modelValues.indices) { index in
NavigationLink(
destination: ModelEditView(model: $modelViewModel.modelValues[index]),
label: {
Text(modelViewModel.modelValues[index].titel)
})
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(
trailing:
NavigationLink(
destination: ModelAddView(modelViewModel: $modelViewModel), label: {
Image(systemName: "plus")
})
)
}
}
struct ModelEditView: View {
#Binding var model: Model
var body: some View {
TextField("Titel", text: $model.titel)
}
}
struct ModelAddView: View {
#Binding var modelViewModel: ModelViewModel
#State var model = Model(id: UUID(), titel: "")
var body: some View {
TextField("Titel", text: $model.titel)
}
}
struct ModelViewModel {
var modelValues: [Model]
init() {
self.modelValues = [ //mock data
Model(id: UUID(), titel: "Foo"),
Model(id: UUID(), titel: "Bar"),
Model(id: UUID(), titel: "Buzz")
]
}
}
struct Model: Identifiable, Equatable {
let id: UUID
var titel: String
}
Currently placing a NavigationLink in the .navigationBarItems may cause some issues.
A possible solution is to move the NavigationLink to the view body and only toggle a variable in the navigation bar button:
struct ModelListView: View {
#State var modelViewModel = ModelViewModel()
#State var isAddLinkActive = false // add a `#State` variable
var body: some View {
List(modelViewModel.modelValues.indices) { index in
NavigationLink(
destination: ModelEditView(model: $modelViewModel.modelValues[index]),
label: {
Text(modelViewModel.modelValues[index].titel)
}
)
}
.background( // move the `NavigationLink` to the `body`
NavigationLink(destination: ModelAddView(modelViewModel: $modelViewModel), isActive: $isAddLinkActive) {
EmptyView()
}
.hidden()
)
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing: trailingButton)
}
// use a Button to activate the `NavigationLink`
var trailingButton: some View {
Button(action: {
self.isAddLinkActive = true
}) {
Image(systemName: "plus")
}
}
}

SwiftUI deep linking with NavigationLink inside List onAppear with tag: and selection: doesn't activate link

Trying to build deep linking into a list of NavigationList items; I will be reading a value on the SwiftUI view's .onAppear and based on that value, navigate to a specific cell. There are three issues that come up with different setups I have tried: (1) with the below code, navigation doesn't happen at all, (2) if it does navigate, it will immediately pop back, (3) if programmatic navigation works and it doesn't pop back, the manual navigation doesn't work.
I have tried this with a Binding dictionary, and I get issue #2 above. Not only this, but in both solutions, user has to scroll to the cell in order to even read the binding/selection.
import SwiftUI
struct ContentViewTwo: View {
var data = ["1", "2", "3"]
#State var shouldPushPage3: Bool = true
var page3: some View {
Text("Page 3")
}
#State var selected: String?
var body: some View {
return
List(data, id: \.self) { data in
NavigationLink(destination: self.page3, tag: data, selection: self.$selected) {
Text("Tap for Page 3 with Data: \(data):")
}.onAppear() {
print("link appeared.")
}
}.onAppear() {
if (self.shouldPushPage3) {
self.selected = "3" // Has no affect. 😢
self.shouldPushPage3 = false
}
}
}
}
struct ContentView: View {
var body: some View {
return NavigationView() {
VStack {
Text("Page 1")
NavigationLink(destination: ContentViewTwo()) {
Text("Tap for Page 2")
}
}
}
}
}
You need to dispatch the selection.
.onAppear {
guard shouldPushPage3 else { return }
shouldPushPage3 = false
DispatchQueue.main.async {
selection = "3"
}
}