Transition animation gone when presenting a NavigationLink in SwiftUI - ios

I have a List with NavigationLinks.
List {
ForEach(items, id: \.id) { item in
NavigationLink(destination: ItemView(), tag: item.id, selection: self.$viewModel.selectedItemId) {
Text("Some text")
}
}
.onDelete(perform: delete)
}
.id(UUID())
And a corresponding ViewModel which stores the selected item's id.
class ViewModel: ObservableObject {
#Published var selectedItemId: String? {
didSet {
if let itemId = selectedItemId {
...
}
}
}
...
}
The problem is that when I use NavigationLink(destination:tag:selection:) the transition animation is gone - the child view pops up immediately. When I use NavigationLink(destination:) it works normally, but I can't use it because I need to perform some action when a NavigationLink is selected.
Why the transition animation is gone? Is this a problem with NavigationLink(destination:tag:selection:)?

You could put your action inside the NavigationLink. This should solve your animation problem:
List {
ForEach(items, id: \.id) { item in
NavigationLink(destination: ItemView(), isActive: $isActive, tag: item.id, selection: self.$viewModel.selectedItemId) {
EmptyView()
}
Button(action: {
// The action you wand to have done, when the View pops.
print("Test")
self.isActive = true
}, label: {
Text("Some text")
})
}
.onDelete(perform: delete)
}
.id(UUID())

It turned out to be a problem with .id(UUID()) at the end of the list. Removing it restores the transition animation:
List {
ForEach(items, id: \.id) { item in
NavigationLink(destination: ItemView(), tag: item.id, selection: self.$viewModel.selectedItemId) {
Text("Some text")
}
}
.onDelete(perform: delete)
}
//.id(UUID()) <- removed this line
I added this following this link How to fix slow List updates in SwiftUI. Looks like this hack messes up tags/selections when using NavigationLink(destination:tag:selection:).

Related

SwiftUI Picker and Buttons inside same Form section are triggered by the same user click

I have this AddWorkoutView and I am trying to build some forms similar to what Apple did with "Add new contact" sheet form.
Right now I am trying to add a form more complex than a simple TextField (something similar to "add address" from Apple contacts but I am facing the following issues:
in the Exercises section when pressing on a new created entry (exercise), both Picker and delete Button are triggered at the same time and the Picker gets automatically closed as soon as it gets open and the selected entry is also deleted when going back to AddWorkoutView.
Does anyone have any idea on how Apple implemented this kind of complex form like in the screenshow below?
Thanks to RogerTheShrubber response here I managed to somehow implement at least the add button and to dynamically display all the content I previously added, but I don't know to bring together multiple TextFields/Pickers/any other stuff in the same form.
struct AddWorkoutView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#EnvironmentObject var dateModel: DateModel
#Environment(\.presentationMode) var presentationMode
#State var workout: Workout = Workout()
#State var exercises: [Exercise] = [Exercise]()
func getBinding(forIndex index: Int) -> Binding<Exercise> {
return Binding<Exercise>(get: { workout.exercises[index] },
set: { workout.exercises[index] = $0 })
}
var body: some View {
NavigationView {
Form {
Section("Workout") {
TextField("Title", text: $workout.title)
TextField("Description", text: $workout.description)
}
Section("Exercises") {
ForEach(0..<workout.exercises.count, id: \.self) { index in
HStack {
Button(action: { workout.exercises.remove(at: index) }) {
Image(systemName: "minus.circle.fill")
.foregroundColor(.red)
.padding(.horizontal)
}
Divider()
VStack {
TextField("Title", text: $workout.exercises[index].title)
Divider()
Picker(selection: getBinding(forIndex: index).type, label: Text("Type")) {
ForEach(ExerciseType.allCases, id: \.self) { value in
Text(value.rawValue)
.tag(value)
}
}
}
}
}
Button {
workout.exercises.append(Exercise())
} label: {
HStack(spacing: 0) {
Image(systemName: "plus.circle.fill")
.foregroundColor(.green)
.padding(.trailing)
Text("add exercise")
}
}
}
}
.navigationTitle("Create new Workout")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("Cancel")
}
.accessibilityLabel("Cancel adding Workout")
}
ToolbarItem(placement: .confirmationAction) {
Button {
} label: {
Text("Done")
}
.accessibilityLabel("Confirm adding the new Workout")
}
}
}
}
}

SwiftUI List selection doesn’t show If I add a NavigationLink and a .contextMenu to the list. Is this a known bug?

List selection doesn’t show If I add a NavigationLink and a .contextMenu to the list, when I select a row, the selection disappears.
struct ContentView: View {
#State private var selection: String?
let names = ["Cyril", "Lana", "Mallory", "Sterling"]
var body: some View {
NavigationView {
List(names, id: \.self, selection: $selection) { name in
NavigationLink(destination: Text("Hello, world!")) {
Text(name)
.contextMenu {
Button(action: {}) {
Text("Tap me!")
}
}
}
}
.toolbar {
EditButton()
}
}
}
}
We can disable context menu button(s) for the moment of construction in edit mode (because the button is a reason of issue).
Here is a possible approach - some redesign is required to handle editMode inside context menu (see also comments inline).
Tested with Xcode 13.2 / iOS 15.2
struct ContentViewSelection: View {
#State private var selection: String?
let names = ["Cyril", "Lana", "Mallory", "Sterling"]
var body: some View {
NavigationView {
List(names, id: \.self, selection: $selection) { name in
// separated view is needed to use editMode
// environment value
NameCell(name: name)
}
.toolbar {
EditButton()
}
}
}
}
struct NameCell: View {
#Environment(\.editMode) var editMode // << !!
let name: String
var body: some View {
NavigationLink(destination: Text("Hello, world!")) {
Text(name)
}
.contextMenu {
if editMode?.wrappedValue == .inactive { // << !!
Button(action: {}) {
Text("Tap me!")
}
}
}
}
}

Adding a navigation link inside a context menu

This question is based on my previous two questions but I am asking it because when I responded to those answers I didn't get a response that helped. I have gone through multiple answers online and people seem to have a similar issue where a navigation link wouldn't work in a context menu.
#State private var selectedBook: Book? = nil
//...other code
ForEach(bookData){ bookDetail in
BookView(book: bookDetail)
.background(NavigationLink(destination: EditBook(book: bookDetail), tag: bookDetail, selection: $selectedBook){ EmptyView() })
.contextMenu{
Button(action: {
self.selectedBook = bookDetail
}) {
Label("Edit", systemImage: "pencil")
}
Button(action: {
//Delete action
}) {
abel("Delete", systemImage: "trash")
}
}
Basically in my first question I had the navigation link under the first button which wasn't and the solution was using a bool #State variable and the .background modifier but then the link wasn't passing the right view because of the for each (second question) and now I have arrived on this code.
My problem is that the .background modifier opens the view in the navigation link on the tap of the view it's bound to as well as the tap of the context menu. I need the tap action to open a different view so I want the current nav link to open only at a press of the context menu.
Can you try putting the BookView into a ZStack and just putting the Navigation Link inside? Instead of your BookView & .background, something like:
ZStack {
NavigationLink(destination: EditBook(book: bookDetail), isActive: $selectedBook, label: { EmptyView() })
BookView(book: bookDetail)
}
A derivative of my first answer should work. Here's a more complete version:
struct TestView: View {
#State private var selectedBook: String = ""
#State private var showSelectedBookView: Bool = false
let books = [
"BOOK1",
"BOOK2",
"BOOK3",
"BOOK4",
"BOOK5",
"BOOK6",
"BOOK7",
]
var body: some View {
ZStack {
NavigationLink(destination: Text("SELECTED BOOK IS: \(selectedBook)"), isActive: $showSelectedBookView, label: { EmptyView() })
VStack(spacing: 20) {
ForEach(books, id: \.self) { book in
Text(book)
.contextMenu(ContextMenu(menuItems: {
Button(action: {
self.selectedBook = book
self.showSelectedBookView.toggle()
}, label: {
Label("Edit", systemImage: "pencil")
})
Button(action: {
}, label: {
Label("Delete", systemImage: "trash")
})
}))
}
}
}
}
}

Implementing a delete feature on a list in SwiftUI

I followed this document (and this one) to add a delete feature to my list in an app using SwiftUI. Both pages say that once you add the .onDelete(perform: ...) piece of code you will be able to swipe and get a Delete button. Nevertheless this is not what I see. The code compiles but I see nothing on swipe.
My list is backed up by code like this:
#FetchRequest(
entity: ...,
sortDescriptors: []
) var myList: FetchedResults<MyEntity>
and not by #State. Could this be an issue?
Below follows more of the relevant code, in case this may be useful:
private func deleteSpot(at index: IndexSet) {
print(#function)
}
.........
var body: some View {
VStack {
ForEach(self.myList, id: \.self.name) { item in
HStack {
Spacer()
Button(action: {
self.showingDestinList.toggle()
.....
UserDefaults.standard.set(item.name!, forKey: "LocSpot")
}) {
item.name.map(Text.init)
.font(.largeTitle)
.foregroundColor(.secondary)
}
Spacer()
}
}.onDelete(perform: deleteSpot)
}
The delete on swipe for dynamic container works only in List, so make
var body: some View {
List { // << here !!
ForEach(self.myList, id: \.self.name) { item in
HStack {
// ... other code
}
}.onDelete(perform: deleteSpot)
}
}
By searching and trying various options, I ended up by finding the issue.
I find the solution somewhat ridiculous, but to avoid other people to lose time, here it is:
The last part of the code in the post needs to be modified like the following in order to work.
var body: some View {
VStack {
List {
ForEach(self.myList, id: \.self.name) { item in
HStack {
Spacer()
Button(action: {
self.showingDestinList.toggle()
.....
UserDefaults.standard.set(item.name!, forKey: "LocSpot")
}) {
item.name.map(Text.init)
.font(.largeTitle)
.foregroundColor(.secondary)
}
Spacer()
}
}.onDelete(perform: deleteSpot)
}
}

SwiftUI ForEach not correctly updating in scrollview

I have a SwiftUI ScrollView with an HStack and a ForEach inside of it. The ForEach is built off of a Published variable from an ObservableObject so that as items are added/removed/set it will automatically update in the view. However, I'm running into multiple problems:
If the array starts out empty and items are then added it will not show them.
If the array has some items in it I can add one item and it will show that, but adding more will not.
If I just have an HStack with a ForEach neither of the above problems occur. As soon as it's in a ScrollView I run into the problems.
Below is code that can be pasted into the Xcode SwiftUI Playground to demonstrate the problem. At the bottom you can uncomment/comment different lines to see the two different problems.
If you uncomment problem 1 and then click either of the buttons you'll see just the HStack updating, but not the HStack in the ScrollView even though you see init print statements for those items.
If you uncomment problem 2 and then click either of the buttons you should see that after a second click the the ScrollView updates, but if you keep on clicking it will not update - even though just the HStack will keep updating and init print statements are output for the ScrollView items.
import SwiftUI
import PlaygroundSupport
import Combine
final class Testing: ObservableObject {
#Published var items: [String] = []
init() {}
init(items: [String]) {
self.items = items
}
}
struct SVItem: View {
var value: String
init(value: String) {
print("init SVItem: \(value)")
self.value = value
}
var body: some View {
Text(value)
}
}
struct HSItem: View {
var value: String
init(value: String) {
print("init HSItem: \(value)")
self.value = value
}
var body: some View {
Text(value)
}
}
public struct PlaygroundRootView: View {
#EnvironmentObject var testing: Testing
public init() {}
public var body: some View {
VStack{
Text("ScrollView")
ScrollView(.horizontal) {
HStack() {
ForEach(self.testing.items, id: \.self) { value in
SVItem(value: value)
}
}
.background(Color.red)
}
.frame(height: 50)
.background(Color.blue)
Spacer()
Text("HStack")
HStack {
ForEach(self.testing.items, id: \.self) { value in
HSItem(value: value)
}
}
.frame(height: 30)
.background(Color.red)
Spacer()
Button(action: {
print("APPEND button")
self.testing.items.append("A")
}, label: { Text("APPEND ITEM") })
Spacer()
Button(action: {
print("SET button")
self.testing.items = ["A", "B", "C"]
}, label: { Text("SET ITEMS") })
Spacer()
}
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = UIHostingController(
// problem 1
rootView: PlaygroundRootView().environmentObject(Testing())
// problem 2
// rootView: PlaygroundRootView().environmentObject(Testing(items: ["1", "2", "3"]))
)
Is this a bug? Am I missing something? I'm new to iOS development..I did try wrapping the actual items setting/appending in the DispatchQueue.main.async, but that didn't do anything.
Also, maybe unrelated, but if you click the buttons enough the app seems to crash.
Just ran into the same issue. Solved with empty array check & invisible HStack
ScrollView(showsIndicators: false) {
ForEach(self.items, id: \.self) { _ in
RowItem()
}
if (self.items.count == 0) {
HStack{
Spacer()
}
}
}
It is known behaviour of ScrollView with observed empty containers - it needs something (initial content) to calculate initial size, so the following solves your code behaviour
#Published var items: [String] = [""]
In general, in such scenarios I prefer to store in array some "easy-detectable initial value", which is removed when first "real model value" appeared and added again, when last disappears. Hope this would be helpful.
For better readability and also because the answer didn't work for me. I'd suggest #TheLegend27 answer to be slightly modified like this:
if self.items.count != 0 {
ScrollView(showsIndicators: false) {
ForEach(self.items, id: \.self) { _ in
RowItem()
}
}
}

Resources