How to deselect a selected Form (UITableView) cell - SwiftUI - swift

Everything I've found is how we do this in UIKit. Here's a useful extension for no style at all:
extension UITableViewCell {
func noStyle() {
self.selectionStyle = .none
}
}
How can I select a cell in a SwiftUI Form and remove the selected (namely, still highlighted) cell, even after I push to a new view and return to the view with the selected cell?
To be clear, I am using a Form instead of a List.
Here's an example:
Form {
Section(header: Text("Header")) {
NavigationLink(
destination: DetailView(user: user)) {
HStack {
Text("Cell")
Spacer()
}
}
}
}
Ideally I would be able to do .selectedStyle(.none) on the penultimate brace, on the Section.

#State private var selected: User?
var body: some View {
Form {
Section(header: Text("Header")) {
NavigationLink(
destination: DetailView(user: user),
tag: user,
selection: $selected) {
HStack {
Text("Cell")
Spacer()
}
}
.onDisappear {
selected = nil
}
}
}
}

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.

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

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

List scroll freeze on catalyst NavigationView

I've run in to an odd problem with NavigationView on macCatalyst. Here below is a simple app with a sidebar and a detail view. Selecting an item on the sidebar shows a detail view with a scrollable list.
Everything works fine for the first NavigationLink, the detail view displays and is freely scrollable. However, if I select a list item which triggers a link to a second detail view, scrolling starts, then freezes. The app still works, only the detail view scrolling is locked up.
The same code works fine on an iPad without any freeze. If I build for macOS, the NavigationLink in the detail view is non-functional.
Are there any known workarounds ?
This is what it looks like, after clicking on LinkedView, a short scroll then the view freezes. It is still possible to click on the back button or another item on the sidebar, but the list view is blocked.
Here is the code:
ContentView.swift
import SwiftUI
struct ContentView: View {
var names = [NamedItem(name: "One"), NamedItem(name: "Two"), NamedItem(name:"Three")]
var body: some View {
NavigationView {
List() {
ForEach(names.sorted(by: {$0.name < $1.name})) { item in
NavigationLink(destination: DetailListView(item: item)) {
Text(item.name)
}
}
}
.listStyle(SidebarListStyle())
Text("Detail view")
}
}
}
struct NamedItem: Identifiable {
let name: String
let id = UUID()
}
struct DetailListView: View {
var item: NamedItem
let sections = (0...4).map({NamedItem(name: "\($0)")})
var body: some View {
VStack {
List {
Text(item.name)
NavigationLink(destination: DetailListView(item: NamedItem(name: "LinkedView"))) {
listItem(" LinkedView", "Item")
.foregroundColor(Color.blue)
}
ForEach(sections) { section in
sectionDetails(section)
}
}
}
}
let info = (0...12).map({NamedItem(name: "\($0)")})
func sectionDetails(_ section: NamedItem) -> some View {
Section(header: Text("Section \(section.name)")) {
Group {
listItem("ID", "\(section.id)")
}
Text("")
ForEach(info) { ch in
listItem("Item \(ch.name)", "\(ch.id)")
}
}
}
func listItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Text(title)
.frame(width: 200, alignment: .leading)
Text(value)
.padding(.leading, 10)
}
}
}
TestListApp.swift
import SwiftUI
#main
struct TestListApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
I had this very same problem with Mac Catalyst app. On real device (iPhone 7 with iOS 14.4.2) there was no problem but with Mac Catalyst (MacBook Pro with Big Sur 11.2.3) the scrolling in the navigation view stuck very randomly as you explained. I figured out that the issue was with Macbook's trackpad and was related to scroll indicators because with external mouse the issue was absent. So the easiest solution to this problem is to hide vertical scroll indicators in navigation view. At least it worked for me. Below is some code from root view 'ContentView' how I did it. It's unfortunate to lose scroll indicators with big data but at least the scrolling works.
import SwiftUI
struct TestView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: NewView()) {
Text("Navigation Link to new view")
}
}
.onAppear {
UITableView.appearance().showsVerticalScrollIndicator = false
}
}
}
}
OK, so I managed to find a workaround, so thought I'd post this for help, until what seems to be a macCatalyst SwiftUI bug is fixed. I have posted a radar for the list freeze problem: FB8994665
The workaround is to use NavigationLink only to the first level of the series of pages which can be navigated (which gives me the sidebar and a toolbar), and from that point onwards use the NavigationStack package to mange links to other pages.
I ran in to a couple of other gotcha's with this arrangement.
Firstly the NavigationView toolbar loses its background when scrolling linked list views (unless the window is defocussed and refocussed), which seems to be another catalyst SwiftUI bug. I solved that by setting the toolbar background colour.
Second gotcha was that under macCatalyst the onTouch view modifier used in NavigationStack's PushView label did not work for most single clicks. It would only trigger consistently for double clicks. I fixed that by using a button to replace the label.
Here is the code, no more list freezes !
import SwiftUI
import NavigationStack
struct ContentView: View {
var names = [NamedItem(name: "One"), NamedItem(name: "Two"), NamedItem(name:"Three")]
#State private var isSelected: UUID? = nil
init() {
// Ensure toolbar is allways opaque
UINavigationBar.appearance().backgroundColor = UIColor.secondarySystemBackground
}
var body: some View {
NavigationView {
List {
ForEach(names.sorted(by: {$0.name < $1.name})) { item in
NavigationLink(destination: DetailStackView(item: item)) {
Text(item.name)
}
}
}
.listStyle(SidebarListStyle())
Text("Detail view")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.toolbar { Spacer() }
}
}
}
struct NamedItem: Identifiable {
let name: String
let id = UUID()
}
// Embed the list view in a NavigationStackView
struct DetailStackView: View {
var item: NamedItem
var body: some View {
NavigationStackView {
DetailListView(item: item)
}
}
}
struct DetailListView: View {
var item: NamedItem
let sections = (0...10).map({NamedItem(name: "\($0)")})
var linked = NamedItem(name: "LinkedView")
// Use a Navigation Stack instead of a NavigationLink
#State private var isSelected: UUID? = nil
#EnvironmentObject private var navigationStack: NavigationStack
var body: some View {
List {
Text(item.name)
PushView(destination: linkedDetailView,
tag: linked.id, selection: $isSelected) {
listLinkedItem(" LinkedView", "Item")
}
ForEach(sections) { section in
if section.name != "0" {
sectionDetails(section)
}
}
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(item.name)
}
// Ensure that the linked view has a toolbar button to return to this view
var linkedDetailView: some View {
DetailListView(item: linked)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
self.navigationStack.pop()
}, label: {
Image(systemName: "chevron.left")
})
}
}
}
let info = (0...12).map({NamedItem(name: "\($0)")})
func sectionDetails(_ section: NamedItem) -> some View {
Section(header: Text("Section \(section.name)")) {
Group {
listItem("ID", "\(section.id)")
}
Text("")
ForEach(info) { ch in
listItem("Item \(ch.name)", "\(ch.id)")
}
}
}
// Use a button to select the linked view with a single click
func listLinkedItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Button(title, action: {
self.isSelected = linked.id
})
.foregroundColor(Color.blue)
Text(value)
.padding(.leading, 10)
}
}
func listItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Text(title)
.frame(width: 200, alignment: .leading)
Text(value)
.padding(.leading, 10)
}
}
}
I have continued to experiment with NavigationStack and have made some modifications which will allow it to swap in and out List rows directly. This avoids the problems I was seeing with the NavigationBar background. The navigation bar is setup at the level above the NavigationStackView and changes to the title are passed via a PreferenceKey. The back button on the navigation bar hides if the stack is empty.
The following code makes use of PR#44 of swiftui-navigation-stack
import SwiftUI
struct ContentView: View {
var names = [NamedItem(name: "One"), NamedItem(name: "Two"), NamedItem(name:"Three")]
#State private var isSelected: UUID? = nil
var body: some View {
NavigationView {
List {
ForEach(names.sorted(by: {$0.name < $1.name})) { item in
NavigationLink(destination: DetailStackView(item: item)) {
Text(item.name)
}
}
}
.listStyle(SidebarListStyle())
Text("Detail view")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.toolbar { Spacer() }
}
}
}
struct NamedItem: Identifiable {
let name: String
let depth: Int
let id = UUID()
init(name:String, depth: Int = 0) {
self.name = name
self.depth = depth
}
var linked: NamedItem {
return NamedItem(name: "Linked \(depth+1)", depth:depth+1)
}
}
// Preference Key to send title back down to DetailStackView
struct ListTitleKey: PreferenceKey {
static var defaultValue: String = ""
static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}
extension View {
func listTitle(_ title: String) -> some View {
self.preference(key: ListTitleKey.self, value: title)
}
}
// Embed the list view in a NavigationStackView
struct DetailStackView: View {
var item: NamedItem
#ObservedObject var navigationStack = NavigationStack()
#State var toolbarTitle: String = ""
var body: some View {
List {
NavigationStackView(noGroup: true, navigationStack: navigationStack) {
DetailListView(item: item, linked: item.linked)
.listTitle(item.name)
}
}
.listStyle(PlainListStyle())
.animation(nil)
// Updated title
.onPreferenceChange(ListTitleKey.self) { value in
toolbarTitle = value
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("\(toolbarTitle) \(self.navigationStack.depth)")
.toolbar(content: {
ToolbarItem(id: "BackB", placement: .navigationBarLeading, showsByDefault: self.navigationStack.depth > 0) {
Button(action: {
self.navigationStack.pop()
}, label: {
Image(systemName: "chevron.left")
})
.opacity(self.navigationStack.depth > 0 ? 1.0 : 0.0)
}
})
}
}
struct DetailListView: View {
var item: NamedItem
var linked: NamedItem
let sections = (0...10).map({NamedItem(name: "\($0)")})
// Use a Navigation Stack instead of a NavigationLink
#State private var isSelected: UUID? = nil
#EnvironmentObject private var navigationStack: NavigationStack
var body: some View {
Text(item.name)
PushView(destination: linkedDetailView,
tag: linked.id, selection: $isSelected) {
listLinkedItem(" LinkedView", "Item")
}
ForEach(sections) { section in
if section.name != "0" {
sectionDetails(section)
}
}
}
// Ensure that the linked view has a toolbar button to return to this view
var linkedDetailView: some View {
DetailListView(item: linked, linked: linked.linked)
.listTitle(linked.name)
}
let info = (0...12).map({NamedItem(name: "\($0)")})
func sectionDetails(_ section: NamedItem) -> some View {
Section(header: Text("Section \(section.name)")) {
Group {
listItem("ID", "\(section.id)")
}
Text("")
ForEach(info) { ch in
listItem("Item \(ch.name)", "\(ch.id)")
}
}
}
func buttonAction() {
self.isSelected = linked.id
}
// Use a button to select the linked view with a single click
func listLinkedItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Button(title, action: buttonAction)
.foregroundColor(Color.blue)
Text(value)
.padding(.leading, 10)
}
}
func listItem(_ title: String, _ value: String, tooltip: String? = nil) -> some View {
HStack {
Text(title)
.frame(width: 200, alignment: .leading)
Text(value)
.padding(.leading, 10)
}
}
}

why is SwiftUI modal view updating parent variable here (code attached)

In the code below if I change the value of the TextField and then click "Cancel" (i.e. will not then do a coredata save), after this modal view is hidden the value is changed in the parents UI list?
Is this line effectively passing my ref? If yes how to change to be effectively by value?
UPDATE: Actually it appears the code in the Save button is getting call directly after the Code in the cancel button, that is in the case I'm clickon on Cancel. Not sure why this would be occurring?
Code:
import SwiftUI
struct GCListsViewEdit: View {
#Environment (\.presentationMode) var presentationMode
#State var titleStr : String = ""
var gcItem : GCList?
var body: some View {
NavigationView {
Form {
Section(header: Text("Enter Details")) {
TextField("List Title", text: self.$titleStr)
.onAppear {
self.titleStr = self.gcItem?.title ?? "" // ** HERE **
}
}
HStack {
Button("Cancel") {
self.presentationMode.wrappedValue.dismiss()
}
Spacer()
Button("Save") {
guard !self.titleStr.isEmpty else {
return
}
guard let item = self.gcItem else {
return
}
item.title = self.titleStr
GCCoreData.save()
self.presentationMode.wrappedValue.dismiss()
}
}
}
.navigationBarTitle("Edit List")
}
}
}
PARENT - just the body part
var body : some View {
NavigationView {
VStack {
// -- Main List --
List() {
ForEach(gcLists) { gcList in
HStack {
if self.editMode {
Button(action: {}) {
Text("\(gcList.title)")
}
.onTapGesture {
self.selectedListViewItem = gcList
self.newListItemTitle = gcList.title
self.showEditView.toggle()
}
.sheet(isPresented: self.$showEditView, content: {
GCListsViewEdit(gcItem: self.selectedListViewItem!)
})
} else {
NavigationLink(destination: GCTasksView(withGcList: gcList)) {
Text("\(gcList.title)")
}
}
}
}
.onDelete(perform: self.deleteList)
.onMove(perform: self.move)
}
.environment(\.editMode, editMode ? .constant(.active) : .constant(.inactive))
.alert(isPresented: $showingAlert) {
Alert(
title: Text(verbatim: "Important Message"),
message: Text(self.alertString),
dismissButton: Alert.Button.default(Text(verbatim: "Cancel"))
)
}
.navigationBarTitle( Text("Todo Lists") )
.navigationBarItems(
trailing: Button(action: {
print("Edit" as Any)
self.editMode = !self.editMode
} ) {
Text(editMode ? "Done" : "Edit")
}
)
// -- Add List Item ---selectedListViewItem
Button("Add List") {
self.newListItemTitle = ""
self.showAddView.toggle()
}
.sheet(isPresented: $showAddView, content: { GCListsViewAdd() } )
}
}
}
Form is kind of List and List has specific handling of standard buttons in a row - it makes active entire row, so when row tapped in any place a button (or buttons) got activated.
In your example even if you tap in between Cancel and Save, both action are executed.
There are several possible solutions:
1) use tap gestures (keeping buttons or replacing them with other views, Text, Image, etc.), like
HStack {
Button("Cancel") {}
.onTapGesture {
print(">> do cancel")
}
Spacer()
Button("Save") {}
.onTapGesture {
print(">> do save")
}
}
2) use custom button style, because List intercepts only DefaultButtonStyle buttons
struct CustomButton: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.foregroundColor(configuration.isPressed ? Color.gray : Color.blue)
}
}
HStack {
Button("Cancel") {
print(">> do cancel")
}.buttonStyle(CustomButton())
Spacer()
Button("Save") {
print(">> do save")
}.buttonStyle(CustomButton())
}
3) move buttons out of Form for this screen (eg. in NavigationBar)