How to manually set selection on SwiftUI List? - swift

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.

Related

SwiftUI Swipe Actions Note Working Correctly

So I’m working on this shopping list and basically I have a list of tile views that have swipe actions.
The bought and delete actions work perfectly, but I am unable to get the edit button to work. The edit button pulls up a form already filled with the item’s information so you can edit it.
The code for the button works, when I make the tile itself a button and tap the tile the edit form comes up.
Is there a limit to swipe actions that limit you pulling up a form in a swipe action? Here is the code for the list. Thanks! (Program is coded using swift ui and swift)
ForEach(Item) { madeItem in
FoodListTileView(foodItem: madeItem)
.swipeActions(edge: .leading) {
Button {
madeItem.justBought()
FoodDataController.shared.saveItem(madeItem: madeItem)
} label: {
Label("Just Bought", systemImage: "checkmark.square")
}
.tint(.green)
}
.swipeActions(edge: .trailing) {
Button { FoodDataController.shared.deleteItem(madeItem: madeItem)} label : {
Label("Delete", systemImage:"trash")
}
.tint(.red)
Button(action: {showingCreateView = true}) {
Label("Edit", systemImage: "pencil.circle")
}
.sheet(isPresented: $showingCreateView) {
AddItemView(Item: madeItem)
}
.tint(.orange)
}
}
}
.listStyle(.plain)
#Raja is absolutely right. The .sheet inside swipeAction doesn't work, it has to be outside. Better even outside of the ForEach, as otherwise it will always only show the last value of the forEach.
If it's outside the ForEach, you have to use a different way to let sheet know which data to display: .sheet(item:) which is the better alternative in most cases anyway. For that go from #State var showingCreateView: Bool to something like #State var editingItem: Item? where Item should be your items Class. And in the button action, give it the current item value.
Here is the amended code:
// test item struct
struct Item: Identifiable {
let id = UUID()
var name: String
}
struct ContentView: View {
// test data
#State private var items = [
Item(name: "Butter"),
Item(name: "Honey"),
Item(name: "Bread"),
Item(name: "Milk"),
Item(name: "Ham")
]
#State private var editingItem: Item? // replace Item with your item type
var body: some View {
List {
ForEach(items) { madeItem in
Text(madeItem.name)
.swipeActions(edge: .leading){
Button {
} label: {
Label("Just Bought", systemImage: "checkmark.square")
}
.tint(.green)
}
.swipeActions(edge: .trailing){
Button {
} label : {
Label("Delete", systemImage:"trash")
}
.tint(.red)
Button {
editingItem = madeItem // setting editingItem (anything else but nil) brings up the sheet
} label: {
Label("Edit", systemImage: "pencil.circle")
}
.tint(.orange)
}
}
}
.sheet(item: $editingItem) { item in // change to item: init here
Text("Edit: \(item.name)")
}
.listStyle(.plain)
}
}

SwiftUI: NavigationView/List with programmatic selection under macOS

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.

TextField in a list not working well in SwiftUI

This problem is with SwiftUI for a iPhone 12 app, Using xcode 13.1.
I build a List with TextField in each row, but every time i try to edit the contents, it is only allow me tap one time and enter only one character then can not keep enter characters anymore, unless i tap again then enter another one character.Did i write something code wrong with it?
class PieChartViewModel: ObservableObject, Identifiable {
#Published var options = ["How are you", "你好", "Let's go to zoo", "OKKKKK", "什麼情況??", "yesssss", "二百五", "明天見"]
}
struct OptionsView: View {
#ObservedObject var viewModel: PieChartViewModel
var body: some View {
NavigationView {
List {
ForEach($viewModel.options, id: \.self) { $option in
TextField(option, text: $option)
}
}
.navigationTitle("Options")
.toolbar {
ToolbarItem(placement: .bottomBar) {
Button {
addNewOption()
} label: {
HStack {
Image(systemName: "plus")
Text("Create a new option")
}
}
}
}
}
}
func addNewOption() {
viewModel.options.insert("", at: viewModel.options.count)
}
}
struct OptionsView_Previews: PreviewProvider {
static var previews: some View {
let pieChart = PieChartViewModel()
OptionsView(viewModel: pieChart)
}
}
Welcome to StackOverflow! Your issue is that you are directly updating an ObservableObject in the TextField. Every change you make to the model, causes a redraw of your view, which, of course, kicks your focus from the TextField. The easiest answer is to implement your own Binding on the TextField. That will cause the model to update, without constantly redrawing your view:
struct OptionsView: View {
// You should be using #StateObject instead of #ObservedObject, but either should work.
#StateObject var model = PieChartViewModel()
#State var newText = ""
var body: some View {
NavigationView {
VStack {
List {
ForEach(model.options, id: \.self) { option in
Text(option)
}
}
List {
//Using Array(zip()) allows you to sort by the element, but use the index.
//This matters if you are rearranging or deleting the elements in a list.
ForEach(Array(zip(model.options, model.options.indices)), id: \.0) { option, index in
// Binding implemented here.
TextField(option, text: Binding<String>(
get: {
model.options[index]
},
set: { newValue in
//You can't update the model here because you will get the same behavior
//that you were getting in the first place.
newText = newValue
}))
.onSubmit {
//The model is updated here.
model.options[index] = newText
newText = ""
}
}
}
.navigationTitle("Options")
.toolbar {
ToolbarItem(placement: .bottomBar) {
Button {
addNewOption()
} label: {
HStack {
Image(systemName: "plus")
Text("Create a new option")
}
}
}
}
}
}
}
func addNewOption() {
model.options.insert("", at: model.options.count)
}
}

NavigationLink with isActive creates unusual scrolling behavior in macOS SwiftUI app

I have a macOS app with a 3-column view. In the first column, there is a List of items that is lengthy -- perhaps a couple hundred items.
If my NavigationLink for each item contains a isActive parameter, when clicking on the NavigationLinks, I get unpredictable/unwanted scrolling behavior on the list.
For example, if I scroll down to Item 100 and click on it, the List may decide to scroll back up to Item 35 or so (where the active NavigationLink is out of the frame). The behavior seems somewhat non-deterministic -- it doesn't always scroll to the same place. It seems less likely to scroll to an odd location if I scroll through the list to my desired item and then wait for the system scroll bars to disappear before clicking on the NavigationLink, but it doesn't make the problem disappear completely.
If I remove the isActive parameter, the scroll position is maintained when clicking on the NavigationLinks.
struct ContentView : View {
var body: some View {
NavigationView {
SidebarList()
Text("No selection")
Text("No selection")
.frame(minWidth: 300)
}
}
}
struct Item : Identifiable, Hashable {
let id = UUID()
var name : String
}
struct SidebarList : View {
#State private var items = Array(0...300).map { Item(name: "Item \($0)") }
#State private var activeItem : Item?
func navigationBindingForItem(item: Item) -> Binding<Bool> {
.init {
activeItem == item
} set: { newValue in
if newValue { activeItem = item }
}
}
var body: some View {
List(items) { item in
NavigationLink(destination: InnerSidebar(),
isActive: navigationBindingForItem(item: item) //<-- Remove this line and everything works as expected
) {
Text(item.name)
}
}.listStyle(SidebarListStyle())
}
}
struct InnerSidebar : View {
#State private var items = Array(0...100).map { Item(name: "Inner item \($0)") }
var body: some View {
List(items) { item in
NavigationLink(destination: Text("Detail")) {
Text(item.name)
}
}
}
}
I would like to keep isActive, as I have some programatic navigation that I'd like to be able to do that depends on it. For example:
Button("Go to item 10") {
activeItem = items[10]
}
Is there any way to use isActive on the NavigationLink and avoid the unpredictable scrolling behavior?
(Built and tested with macOS 11.3 and Xcode 13.0)
The observed effect is because body of your main view is refreshed and all internals rebuilt. To avoid this we can separate sensitive part of view hierarchy into standalone view, so SwiftUI engine see that dependency not changed and view should not be updated.
Here is a fixed parts:
struct SidebarList : View {
#State private var items = Array(0...300).map { Item(name: "Item \($0)") }
#State private var activeItem : Item?
var body: some View {
List(items) {
SidebarRowView(item: $0, activeItem: $activeItem) // << here !!
}.listStyle(SidebarListStyle())
}
}
struct SidebarRowView: View {
let item: Item
#Binding var activeItem: Item?
func navigationBindingForItem(item: Item) -> Binding<Bool> {
.init {
activeItem == item
} set: { newValue in
if newValue {
activeItem = item
}
}
}
var body: some View {
NavigationLink(destination: InnerSidebar(),
isActive: navigationBindingForItem(item: item)) {
Text(item.name)
}
}
}
Tested with Xcode 13 / macOS 11.6

SwiftUI: Update NavigationView after deletion (iPad)

I want to show the empty view (here: Text("Please select a person.")) after the deletion of a row has happend on an iPad.
Currently: The detail view on an iPad will not get updated after the deletion of an item.
Expected: Show the empty view after the selected item gets deleted.
struct DetailView: View {
var name: String
var body: some View {
Text("Detail of \(name)")
}
}
struct MainView: View {
#State private var users = ["Paul", "Taylor", "Adele"]
var body: some View {
NavigationView {
List {
ForEach(users, id: \.self) { user in
NavigationLink(destination: DetailView(name: user)) {
Text(user)
}
}
.onDelete(perform: delete)
}
Text("Please select a person.")
}
}
func delete(at offsets: IndexSet) {
users.remove(atOffsets: offsets)
}
}
NavigationView example from Hacking With Swift.
In the example below, the detail view is shown correctly after the first launch: here
But after the deletion of a row, the previously selected detail view (here: Paul) is still shown: here
As of iOS 14, deleting an element from a list in a navigation view does not seem to be supported.
The NavigationLink type takes an isActive binding, but that does not work in the case of deletion. When you receive the .onDelete callback it is too late. That NavigationLink is not in the list anymore and any change to the binding you passed to it is not going to have any effect.
The workaround is to pass a binding to the DetailView with all the elements, so that it can verify if an element is present and display some content accordingly.
struct DetailView: View {
var name: String
#Binding var users: [String]
var body: some View {
if users.contains(name) {
Text("Detail of \(name)")
} else {
EmptyView()
}
}
}
struct MainView: View {
#State private var users = ["Paul", "Taylor", "Adele"]
var body: some View {
NavigationView {
List {
ForEach(users, id: \.self) { user in
NavigationLink(destination: DetailView(name: user, users: $users)) {
Text(user)
}
}
.onDelete(perform: delete)
}
}
}
func delete(at offsets: IndexSet) {
users.remove(atOffsets: offsets)
}
}
You can use a binding from parent view to manually trigger re-rendering (otherwise the child view won't get notified):
import SwiftUI
struct DetailView: View {
var name: String
#Binding var notifier: Int
#State var deleted: Bool = false
var body: some View {
Group {
if !deleted {
Text("Detail of \(name)")
.onChange(of: notifier, perform: { _ in deleted = true })
} else {
Text("Deleted")
}
}
}
}
struct MainView: View {
#State private var users = ["Paul", "Taylor", "Adele"]
#State private var deleteNotifier: Int = 0
var body: some View {
NavigationView {
List {
ForEach(users, id: \.self) { user in
NavigationLink(destination: DetailView(name: user,
notifier: $deleteNotifier)) {
Text(user)
}
}
.onDelete(perform: delete)
}
Text("Please select a person.")
}
}
func delete(at offsets: IndexSet) {
users.remove(atOffsets: offsets)
deleteNotifier += 1
}
}
You can use the second choice for the list updating: index. That way it will reinforce the refreshing:
var body: some View {
NavigationView {
List {
ForEach(users.indices, id: \.self) { index in
NavigationLink(destination: DetailView(name: self.users[index])) {
Text(self.users[index])
}
}
.onDelete(perform: delete)
}
Text("Please select a person.")
}
}