I have the following code for my iPad app:
struct ContentView: View {
#State var selectionIndex: Int? = nil
var body: some View {
NavigationView {
VStack {
ForEach(0..<5) { tag in
NavigationLink("Link \(tag)", destination: DetailView(name: "View \(tag)"), tag: tag, selection: self.$selectionIndex)
.foregroundColor((self.selectionIndex ?? 0) == tag ? Color.red : Color.black)
}
}
}
}
}
struct DetailView: View {
var name: String
var body: some View {
Text(self.name)
}
}
Pressing the links works perfectly and also it changes the DetailView. I try to highlight the selected button, therefore I save the selectionIndex.
Unfortunately the selectionIndex sometimes resets to 0. What am I doing wrong?
EDIT
Wrapping the NavigationLink into a List shows the problematic better, as the List has it's own selection (this selection stays, but my own var selectionIndex resets).
NavigationView {
List {
ForEach(0..<5) { tag in
NavigationLink("Link \(tag)", destination: DetailView(name: "View \(tag)"), tag: tag, selection: self.$selectionIndex)
.foregroundColor((self.selectionIndex ?? 0) == tag ? Color.red : Color.black)
}
}
}
See this screen:
Well, this of course looks like a bug, but they do what they documented - show destination of selected tag, no more. Anyway, probably worth submitting feedback.
Here is a working workaround. Tested with Xcode 11.4.
#State var selectionIndex: Int? = nil
#State var highlighted: Int? = nil // << explicit !!
var body: some View {
NavigationView {
VStack {
ForEach(0..<5) { tag in
NavigationLink("Link \(tag)", destination:
PadDetailView(name: "View \(tag)").onAppear { self.highlighted = tag },
tag: tag, selection: self.$selectionIndex)
.foregroundColor(self.highlighted == tag ? Color.red : Color.black)
}
}
}
}
Related
I'm having a problem where I have a ForEach loop inside a NavigationView. When I click the Edit button, and then click the pencil image at the right hand side on each row, I want it to display the text variable we are using from the ForEach loop. But when I click the pencil image for the text other than test123, it still displays the text test123 and I have absolutely no idea why.
Here's a video. Why is this happening?
import SwiftUI
struct TestPopOver: View {
private var stringObjects = ["test123", "helloworld", "reddit"]
#State private var editMode: EditMode = .inactive
#State private var showThemeEditor = false
#ViewBuilder
var body: some View {
NavigationView {
List {
ForEach(self.stringObjects, id: \.self) { text in
NavigationLink( destination: HStack{Text("Test!")}) {
HStack {
Text(text)
Spacer()
if self.editMode.isEditing {
Image(systemName: "pencil.circle").imageScale(.large)
.onTapGesture {
if self.editMode.isEditing {
self.showThemeEditor = true
}
}
}
}
}
.popover(isPresented: $showThemeEditor) {
CustomPopOver(isShowing: $showThemeEditor, text: text)
}
}
}
.navigationBarTitle("Reproduce Editing Bug!")
.navigationBarItems(leading: EditButton())
.environment(\.editMode, $editMode)
}
}
}
struct CustomPopOver: View {
#Binding var isShowing: Bool
var text: String
var body: some View {
VStack(spacing: 0) {
HStack() {
Spacer()
Button("Cancel") {
self.isShowing = false
}.padding()
}
Divider()
List {
Section {
Text(text)
}
}.listStyle(GroupedListStyle())
}
}
}
This is a very common issue (especially since iOS 14) that gets run into a lot with sheet but affects popover as well.
You can avoid it by using popover(item:) rather than isPresented. In this scenario, it'll actually use the latest values, not just the one that was present when then view first renders or when it is first set.
struct EditItem : Identifiable { //this will tell it what sheet to present
var id = UUID()
var str : String
}
struct ContentView: View {
private var stringObjects = ["test123", "helloworld", "reddit"]
#State private var editMode: EditMode = .inactive
#State private var editItem : EditItem? //the currently presented sheet -- nil if no sheet is presented
#ViewBuilder
var body: some View {
NavigationView {
List {
ForEach(self.stringObjects, id: \.self) { text in
NavigationLink( destination: HStack{Text("Test!")}) {
HStack {
Text(text)
Spacer()
if self.editMode.isEditing {
Image(systemName: "pencil.circle").imageScale(.large)
.onTapGesture {
if self.editMode.isEditing {
self.editItem = EditItem(str: text) //set the current item
}
}
}
}
}
.popover(item: $editItem) { item in //item is now a reference to the current item being presented
CustomPopOver(text: item.str)
}
}
}
.navigationBarTitle("Reproduce Editing Bug!")
.navigationBarItems(leading: EditButton())
.environment(\.editMode, $editMode)
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct CustomPopOver: View {
#Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
var text: String
var body: some View {
VStack(spacing: 0) {
HStack() {
Spacer()
Button("Cancel") {
self.presentationMode.wrappedValue.dismiss()
}.padding()
}
Divider()
List {
Section {
Text(text)
}
}.listStyle(GroupedListStyle())
}
}
}
I also opted to use the presentationMode environment property to dismiss the popover, but you could pass the editItem binding and set it to nil as well (#Binding var editItem : EditItem? and editItem = nil). The former is just a little more idiomatic.
I have an issue using a sheet inside a ForEach. Basically I have a List that shows many items in my array and an image that trigger the sheet. The problem is that when my sheet is presented it only shows the first item of my array which is "Harry Potter" in this case.
Here's the code
struct ContentView: View {
#State private var showingSheet = false
var movies = ["Harry potter", "Mad Max", "Oblivion", "Memento"]
var body: some View {
NavigationView {
List {
ForEach(0 ..< movies.count) { movie in
HStack {
Text(self.movies[movie])
Image(systemName: "heart")
}
.onTapGesture {
self.showingSheet = true
}
.sheet(isPresented: self.$showingSheet) {
Text(self.movies[movie])
}
}
}
}
}
}
There should be only one sheet, so here is possible approach - use another sheet modifier and activate it by selection
Tested with Xcode 12 / iOS 14 (iOS 13 compatible)
extension Int: Identifiable {
public var id: Int { self }
}
struct ContentView: View {
#State private var selectedMovie: Int? = nil
var movies = ["Harry potter", "Mad Max", "Oblivion", "Memento"]
var body: some View {
NavigationView {
List {
ForEach(0 ..< movies.count) { movie in
HStack {
Text(self.movies[movie])
Image(systemName: "heart")
}
.onTapGesture {
self.selectedMovie = movie
}
}
}
.sheet(item: self.$selectedMovie) {
Text(self.movies[$0])
}
}
}
}
I changed your code to have only one sheet and have the selected movie in one variable.
extension String: Identifiable {
public var id: String { self }
}
struct ContentView: View {
#State private var selectedMovie: String? = nil
var movies = ["Harry potter", "Mad Max", "Oblivion", "Memento"]
var body: some View {
NavigationView {
List {
ForEach(movies) { movie in
HStack {
Text(movie)
Image(systemName: "heart")
}
.onTapGesture {
self.selectedMovie = movie
}
}
}
.sheet(item: self.$selectedMovie, content: { selectedMovie in
Text(selectedMovie)
})
}
}
}
Wanted to give my 2 cents on the matter.
I was encountering the same problem and Asperi's solution worked for me.
BUT - I also wanted to have a button on the sheet that dismisses the modal.
When you call a sheet with isPresented you pass a binding Bool and so you change it to false in order to dismiss.
What I did in the item case is I passed the item as a Binding. And in the sheet, I change that binding item to nil and that dismissed the sheet.
So for example in this case the code would be:
var movies = ["Harry potter", "Mad Max", "Oblivion", "Memento"]
var body: some View {
NavigationView {
List {
ForEach(0 ..< movies.count) { movie in
HStack {
Text(self.movies[movie])
Image(systemName: "heart")
}
.onTapGesture {
self.selectedMovie = movie
}
}
}
.sheet(item: self.$selectedMovie) {
Text(self.movies[$0])
// My addition here: a "Done" button that dismisses the sheet
Button {
selectedMovie = nil
} label: {
Text("Done")
}
}
}
}
Consider the following project with two views. The first view presents the second one:
import SwiftUI
struct ContentView: View {
private let data = 0...1000
#State private var selection: Set<Int> = []
#State private var shouldShowSheet = false
var body: some View {
self.showSheet()
//self.showPush()
}
private func showSheet() -> some View {
Button(action: {
self.shouldShowSheet = true
}, label: {
Text("Selected: \(selection.count) items")
}).sheet(isPresented: self.$shouldShowSheet) {
EditFormView(selection: self.$selection)
}
}
private func showPush() -> some View {
NavigationView {
Button(action: {
self.shouldShowSheet = true
}, label: {
NavigationLink(destination: EditFormView(selection: self.$selection),
isActive: self.$shouldShowSheet,
label: {
Text("Selected: \(selection.count) items")
})
})
}
}
}
struct EditFormView: View {
private let data = 0...1000
#Binding var selection: Set<Int>
#State private var editMode: EditMode = .active
init(selection: Binding<Set<Int>>) {
self._selection = selection
}
var body: some View {
List(selection: self.$selection) {
ForEach(data, id: \.self) { value in
Text("\(value)")
}
}.environment(\.editMode, self.$editMode)
}
}
Steps to reproduce:
Create an app with the above two views
Run the app and present the sheet with the editable list
Select some items at random indexes, for example a handful at index 0-10 and another handful at index 90-100
Close the sheet by swiping down/tapping back button
Open the sheet again
Scroll to indexes 90-100 to view the selection in the reused cells
Expected:
The selected indexes as you had will be in “selected state”
Actual:
The selection you had before is not marked as selected in the UI, even though the binding passed to List contains those indexes.
This occurs both on the “sheet” presentation and the “navigation link” presentation.
If you select an item in the list, the “redraw” causes the original items that were originally not shown as selected to now be shown as selected.
Is there a way around this?
It looks like EditMode bug, worth submitting feedback to Apple. The possible solution is to use custom selection feature.
Here is a demo of approach (modified only part). Tested & worked with Xcode 11.4 / iOS 13.4
struct EditFormView: View {
private let data = 0...1000
#Binding var selection: Set<Int>
init(selection: Binding<Set<Int>>) {
self._selection = selection
}
var body: some View {
List(selection: self.$selection) {
ForEach(data, id: \.self) { value in
self.cell(for: value)
}
}
}
// also below can be separated into standalone view
private func cell(for value: Int) -> some View {
let selected = self.selection.contains(value)
return HStack {
Image(systemName: selected ? "checkmark.circle" : "circle")
.foregroundColor(selected ? Color.blue : nil)
.font(.system(size: 24))
.onTapGesture {
if selected {
self.selection.remove(value)
} else {
self.selection.insert(value)
}
}.padding(.trailing, 8)
Text("\(value)")
}
}
}
I'm pretty sure this is a bug in SwiftUI, but I wondered if anyone has encountered it and figured out a workaround. My normal use case is to have a search field appear, but I've simplified it to the point where a simple text string exhibits the bug.
Create a single-view app, copy this into ContentView, and run it. Tap the search icon twice, then scroll the view; you'll see the text scrolling UNDER the title.
import SwiftUI
struct ContentView: View {
private var items = (0 ... 50).map {String($0)}
#State private var condition = false
var searchButton: some View {
Button(action: {self.condition.toggle()}) {
Image(systemName: "magnifyingglass").imageScale(.large)
}
}
var body: some View {
NavigationView {
VStack {
if condition {
Text("Peekaboo")
}
List {
ForEach(items, id: \.self) {item in
HStack {
Text(item)
}
}
}
}
.navigationBarTitle("List of Items")
.navigationBarItems(leading: searchButton)
}
}
}
Maybe it is a bug, submit feedback to Apple, but currently this is how NavigationView behaves - it collapses navigation bar only if its top content is List/ScrollView/Form. So to solve the issue move your VStack either into a List or out of NavigationView
1)
var body: some View {
NavigationView {
List {
if condition {
Text("Peekaboo")
}
ForEach(items, id: \.self) {item in
2)
var body: some View {
VStack {
if condition {
Text("Peekaboo")
}
NavigationView {
List {
It seems that a View cannot cope with variable number of views.
A workaround this strange behavior is this:
import SwiftUI
struct ContentView: View {
private var items = (0 ... 50).map {String($0)}
#State private var condition = false
var searchButton: some View {
Button(action: {self.condition.toggle()}) {
Image(systemName: "magnifyingglass").imageScale(.large)
}
}
var body: some View {
NavigationView {
VStack {
if condition {
Text("Peekaboo")
} else {
Text("")
}
// or use this Text(condition ? "Peekaboo" : "")
List {
ForEach(items, id: \.self) {item in
HStack {
Text(item)
}
}
}
}
.navigationBarTitle("List of Items")
.navigationBarItems(leading: searchButton)
}
}
}
Let me know if it works, if not let us know what device/system you are using. Tested with Xcode 11.6 beta, Mac 10.15.5, target ios 13.5 and mac catalyst.
I'm trying to create a button that not only navigates to another view, but also run a function at the same time. I tried embedding both a NavigationLink and a Button into a Stack, but I'm only able to click on the Button.
ZStack {
NavigationLink(destination: TradeView(trade: trade)) {
TradeButton()
}
Button(action: {
print("Hello world!") //this is the only thing that runs
}) {
TradeButton()
}
}
You can use .simultaneousGesture to do that. The NavigationLink will navigate and at the same time perform an action exactly like you want:
NavigationLink(destination: TradeView(trade: trade)) {
Text("Trade View Link")
}.simultaneousGesture(TapGesture().onEnded{
print("Hello world!")
})
You can use NavigationLink(destination:isActive:label:). Use the setter on the binding to know when the link is tapped. I've noticed that the NavigationLink could be tapped outside of the content area, and this approach captures those taps as well.
struct Sidebar: View {
#State var isTapped = false
var body: some View {
NavigationLink(destination: ViewToPresent(),
isActive: Binding<Bool>(get: { isTapped },
set: { isTapped = $0; print("Tapped") }),
label: { Text("Link") })
}
}
struct ViewToPresent: View {
var body: some View {
print("View Presented")
return Text("View Presented")
}
}
The only thing I notice is that setter fires three times, one of which is after it's presented. Here's the output:
Tapped
Tapped
View Presented
Tapped
NavigationLink + isActive + onChange(of:)
// part 1
#State private var isPushed = false
// part 2
NavigationLink(destination: EmptyView(), isActive: $isPushed, label: {
Text("")
})
// part 3
.onChange(of: isPushed) { (newValue) in
if newValue {
// do what you want
}
}
This works for me atm:
#State private var isActive = false
NavigationLink(destination: MyView(), isActive: $isActive) {
Button {
// run your code
// then set
isActive = true
} label: {
Text("My Link")
}
}
Use NavigationLink(_:destination:tag:selection:) initializer and pass your model's property as a selection parameter. Because it is a two-way binding, you can define didset observer for this property, and call your function there.
struct ContentView: View {
#EnvironmentObject var navigationModel: NavigationModel
var body: some View {
NavigationView {
List(0 ..< 10, id: \.self) { row in
NavigationLink(destination: DetailView(id: row),
tag: row,
selection: self.$navigationModel.linkSelection) {
Text("Link \(row)")
}
}
}
}
}
struct DetailView: View {
var id: Int;
var body: some View {
Text("DetailView\(id)")
}
}
class NavigationModel: ObservableObject {
#Published var linkSelection: Int? = nil {
didSet {
if let linkSelection = linkSelection {
// action
print("selected: \(String(describing: linkSelection))")
}
}
}
}
It this example you need to pass in your model to ContentView as an environment object:
ContentView().environmentObject(NavigationModel())
in the SceneDelegate and SwiftUI Previews.
The model conforms to ObservableObject protocol and the property must have a #Published attribute.
(it works within a List)
I also just used:
NavigationLink(destination: View()....) {
Text("Demo")
}.task { do your stuff here }
iOS 15.3 deployment target.