SwiftUI NavigationLink loads destination view immediately, without clicking - ios

With following code:
struct HomeView: View {
var body: some View {
NavigationView {
List(dataTypes) { dataType in
NavigationLink(destination: AnotherView()) {
HomeViewRow(dataType: dataType)
}
}
}
}
}
What's weird, when HomeView appears, NavigationLink immediately loads the AnotherView. As a result, all AnotherView dependencies are loaded as well, even though it's not visible on the screen yet. The user has to click on the row to make it appear.
My AnotherView contains a DataSource, where various things happen. The issue is that whole DataSource is loaded at this point, including some timers etc.
Am I doing something wrong..? How to handle it in such way, that AnotherView gets loaded once the user presses on that HomeViewRow?

The best way I have found to combat this issue is by using a Lazy View.
struct NavigationLazyView<Content: View>: View {
let build: () -> Content
init(_ build: #autoclosure #escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
Then the NavigationLink would look like this. You would place the View you want to be displayed inside ()
NavigationLink(destination: NavigationLazyView(DetailView(data: DataModel))) { Text("Item") }

EDIT: See #MwcsMac's answer for a cleaner solution which wraps View creation inside a closure and only initializes it once the view is rendered.
It takes a custom ForEach to do what you are asking for since the function builder does have to evaluate the expression
NavigationLink(destination: AnotherView()) {
HomeViewRow(dataType: dataType)
}
for each visible row to be able to show HomeViewRow(dataType:), in which case AnotherView() must be initialized too.
So to avoid this a custom ForEach is necessary.
import SwiftUI
struct LoadLaterView: View {
var body: some View {
HomeView()
}
}
struct DataType: Identifiable {
let id = UUID()
var i: Int
}
struct ForEachLazyNavigationLink<Data: RandomAccessCollection, Content: View, Destination: View>: View where Data.Element: Identifiable {
var data: Data
var destination: (Data.Element) -> (Destination)
var content: (Data.Element) -> (Content)
#State var selected: Data.Element? = nil
#State var active: Bool = false
var body: some View {
VStack{
NavigationLink(destination: {
VStack{
if self.selected != nil {
self.destination(self.selected!)
} else {
EmptyView()
}
}
}(), isActive: $active){
Text("Hidden navigation link")
.background(Color.orange)
.hidden()
}
List{
ForEach(data) { (element: Data.Element) in
Button(action: {
self.selected = element
self.active = true
}) { self.content(element) }
}
}
}
}
}
struct HomeView: View {
#State var dataTypes: [DataType] = {
return (0...99).map{
return DataType(i: $0)
}
}()
var body: some View {
NavigationView{
ForEachLazyNavigationLink(data: dataTypes, destination: {
return AnotherView(i: $0.i)
}, content: {
return HomeViewRow(dataType: $0)
})
}
}
}
struct HomeViewRow: View {
var dataType: DataType
var body: some View {
Text("Home View \(dataType.i)")
}
}
struct AnotherView: View {
init(i: Int) {
print("Init AnotherView \(i.description)")
self.i = i
}
var i: Int
var body: some View {
print("Loading AnotherView \(i.description)")
return Text("hello \(i.description)").onAppear {
print("onAppear AnotherView \(self.i.description)")
}
}
}

I had the same issue where I might have had a list of 50 items, that then loaded 50 views for the detail view that called an API (which resulted in 50 additional images being downloaded).
The answer for me was to use .onAppear to trigger all logic that needs to be executed when the view appears on screen (like setting off your timers).
struct AnotherView: View {
var body: some View {
VStack{
Text("Hello World!")
}.onAppear {
print("I only printed when the view appeared")
// trigger whatever you need to here instead of on init
}
}
}

For iOS 14 SwiftUI.
Non-elegant solution for lazy navigation destination loading, using view modifier, based on this post.
extension View {
func navigate<Value, Destination: View>(
item: Binding<Value?>,
#ViewBuilder content: #escaping (Value) -> Destination
) -> some View {
return self.modifier(Navigator(item: item, content: content))
}
}
private struct Navigator<Value, Destination: View>: ViewModifier {
let item: Binding<Value?>
let content: (Value) -> Destination
public func body(content: Content) -> some View {
content
.background(
NavigationLink(
destination: { () -> AnyView in
if let value = self.item.wrappedValue {
return AnyView(self.content(value))
} else {
return AnyView(EmptyView())
}
}(),
isActive: Binding<Bool>(
get: { self.item.wrappedValue != nil },
set: { newValue in
if newValue == false {
self.item.wrappedValue = nil
}
}
),
label: EmptyView.init
)
)
}
}
Call it like this:
struct ExampleView: View {
#State
private var date: Date? = nil
var body: some View {
VStack {
Text("Source view")
Button("Send", action: {
self.date = Date()
})
}
.navigate(
item: self.$date,
content: {
VStack {
Text("Destination view")
Text($0.debugDescription)
}
}
)
}
}

I was recently struggling with this issue (for a navigation row component for forms), and this did the trick for me:
#State private var shouldShowDestination = false
NavigationLink(destination: DestinationView(), isActive: $shouldShowDestination) {
Button("More info") {
self.shouldShowDestination = true
}
}
Simply wrap a Button with the NavigationLink, which activation is to be controlled with the button.
Now, if you're to have multiple button+links within the same view, and not an activation State property for each, you should rely on this initializer
/// Creates an instance that presents `destination` when `selection` is set
/// to `tag`.
public init<V>(destination: Destination, tag: V, selection: Binding<V?>, #ViewBuilder label: () -> Label) where V : Hashable
https://developer.apple.com/documentation/swiftui/navigationlink/3364637-init
Along the lines of this example:
struct ContentView: View {
#State private var selection: String? = nil
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Text("Second View"), tag: "Second", selection: $selection) {
Button("Tap to show second") {
self.selection = "Second"
}
}
NavigationLink(destination: Text("Third View"), tag: "Third", selection: $selection) {
Button("Tap to show third") {
self.selection = "Third"
}
}
}
.navigationBarTitle("Navigation")
}
}
}
More info (and the slightly modified example above) taken from https://www.hackingwithswift.com/articles/216/complete-guide-to-navigationview-in-swiftui (under "Programmatic navigation").
Alternatively, create a custom view component (with embedded NavigationLink), such as this one
struct FormNavigationRow<Destination: View>: View {
let title: String
let destination: Destination
var body: some View {
NavigationLink(destination: destination, isActive: $shouldShowDestination) {
Button(title) {
self.shouldShowDestination = true
}
}
}
// MARK: Private
#State private var shouldShowDestination = false
}
and use it repeatedly as part of a Form (or List):
Form {
FormNavigationRow(title: "One", destination: Text("1"))
FormNavigationRow(title: "Two", destination: Text("2"))
FormNavigationRow(title: "Three", destination: Text("3"))
}

In the destination view you should listen to the event onAppear and put there all code that needs to be executed only when the new screen appears. Like this:
struct DestinationView: View {
var body: some View {
Text("Hello world!")
.onAppear {
// Do something important here, like fetching data from REST API
// This code will only be executed when the view appears
}
}
}

Related

SwiftUI TabView - run code in subview after sequential taps

I am trying to implement the behavior in a TabView when the user taps the same tab multiple times, such as in the iOS AppStore app. First tap: switch to that view, second tap: pop to root, third tap: scroll to the top if needed.
The code below works fine for switching and didTap() is called for every tap.
import SwiftUI
enum Tab: String {
case one
case two
}
struct AppView: View {
#State private var activeTab = Tab.one
var body: some View {
TabView(selection: $activeTab.onChange(didTap)) {
One()
.tabItem {
Label("one", systemImage: "1.lane")
}
.tag(Tab.one)
Two()
.tabItem {
Label("two", systemImage: "2.lane")
}
.tag(Tab.two)
}
}
func didTap(to value: Tab) {
print(value) // this captures every tap
}
}
extension Binding {
func onChange(_ handler: #escaping (Value) -> Void) -> Binding<Value> {
Binding(
get: { self.wrappedValue },
set: { newValue in
self.wrappedValue = newValue
handler(newValue)
}
)
}
}
What I am struggling with, is how to tell either One or Two that it was tapped for a second or third time? (How to pop and scroll is not the issue).
I have seen this: TabView, tabItem: running code on selection or adding an onTapGesture but it doesn't explain how to run code in one of the views.
Any suggestions?
You can record additional taps (of same value) in an array. The array count gives you the number of taps on the same Tab.
EDIT: now with explicit subview struct.
struct ContentView: View {
#State private var activeTab = Tab.one
#State private var tapState: [Tab] = [Tab.one] // because .one is default
var body: some View {
TabView(selection: $activeTab.onChange(didTap)) {
SubView(title: "One", tapCount: tapState.count)
.tabItem {
Label("one", systemImage: "1.lane")
}
.tag(Tab.one)
SubView(title: "Two", tapCount: tapState.count)
.tabItem {
Label("two", systemImage: "2.lane")
}
.tag(Tab.two)
}
}
func didTap(to value: Tab) {
print(value) // this captures every tap
if tapState.last == value {
tapState.append(value) // apped next tap if same value
print("tapped \(tapState.count) times")
} else {
tapState = [value] // reset tap state to new tab selection
}
}
}
struct SubView: View {
let title: String
let tapCount: Int
var body: some View {
VStack {
Text("Subview \(title)").font(.title)
Text("tapped \(tapCount) times")
}
}
}
Although the answer by #ChrisR did answer my question, I couldn't figure out the next step, i.e. the logic when to pop-to-root or scroll-to-the-top based on the number of taps for a SubView. After lots of reading and trial and error, I recently came across this article: https://notificare.com/blog/2022/11/25/a-better-tabview-in-swiftui/
Inspired by this article, but with some modifications, I came up with the following which does exactly what I was looking for.
The two main changes are:
An EmptyView with an id is added as the first (but invisible) row in the List to be used as an anchor by proxy.scrollTo().
Instead of the global #StateObject var appState that stores the navigation paths for the subviews, I added the paths as separate #State properties. This avoids the Update NavigationAuthority bound path tried to update multiple times per frame. warning.
Hopefully this is helpful for someone.
enum Tab: String {
case one
case two
}
struct ContentView: View {
#State var selectedTab = Tab.one
#State var oneNavigationPath = NavigationPath()
#State var twoNavigationPath = NavigationPath()
var body: some View {
ScrollViewReader { proxy in
TabView(selection: tabViewSelectionBinding(proxy: proxy)) {
SubView(title: "One", path: $oneNavigationPath)
.tabItem {
Label("one", systemImage: "1.lane")
}
.tag(Tab.one)
SubView(title: "Two", path: $twoNavigationPath)
.tabItem {
Label("two", systemImage: "2.lane")
}
.tag(Tab.two)
}
}
}
private func tabViewSelectionBinding(proxy: ScrollViewProxy) -> Binding<Tab> {
Binding<Tab>(
get: { selectedTab },
set: { newValue in
if selectedTab == newValue {
switch selectedTab {
case .one:
if oneNavigationPath.isEmpty {
withAnimation {
proxy.scrollTo(Tab.one, anchor: .bottom)
}
} else {
withAnimation {
oneNavigationPath = NavigationPath()
}
}
case .two:
if twoNavigationPath.isEmpty {
withAnimation {
proxy.scrollTo(Tab.two, anchor: .bottom)
}
} else {
withAnimation {
twoNavigationPath = NavigationPath()
}
}
}
}
selectedTab = newValue
}
)
}
}
struct SubView: View {
let title: String
let items = Array(1 ... 100)
#Binding var path: NavigationPath
var body: some View {
NavigationStack(path: $path) {
List {
EmptyView()
.id(Tab(rawValue: title.lowercased()))
ForEach(items, id: \.self) { item in
NavigationLink(value: item) {
Text("Item \(item)")
}
}
}
.navigationTitle(title)
.navigationDestination(for: Int.self) { item in
Text("Item \(item)")
}
}
}
}

How to send extra data using NavigationStack with SwiftUI?

I have three views A,B and C. User can navigate from A to B and from A to C. User can navigate from B to C. Now I want to differentiate if the user have come from A to C or from B to C so I was looking in how to pass extra data in NavigationStack which can help me differentiate
Below is my code
import SwiftUI
#main
struct SampleApp: App {
#State private var path: NavigationPath = .init()
var body: some Scene {
WindowGroup {
NavigationStack(path: $path){
A(path: $path)
.navigationDestination(for: ViewOptions.self) { option in
option.view($path)
}
}
}
}
enum ViewOptions {
case caseB
case caseC
#ViewBuilder func view(_ path: Binding<NavigationPath>) -> some View{
switch self{
case .caseB:
B(path: path)
case .caseC:
C(path: path)
}
}
}
}
struct A: View {
#Binding var path: NavigationPath
var body: some View {
VStack {
Text("A")
Button {
path.append(SampleApp.ViewOptions.caseB)
} label: {
Text("Go to B")
}
Button {
path.append(SampleApp.ViewOptions.caseC)
} label: {
Text("Go to C")
}
}
}
}
struct B: View {
#Binding var path: NavigationPath
var body: some View {
VStack {
Text("B")
Button {
path.append(SampleApp.ViewOptions.caseC)
} label: {
Text("Go to C")
}
}
}
}
struct C: View {
#Binding var path: NavigationPath
var body: some View {
VStack {
Text("C")
}
}
}
Instead of "pass extra data in NavigationStack" you can pass data in a NavigationRouter. It gives you much more control
#available(iOS 16.0, *)
//Simplify the repetitive code
typealias NavSource = SampleApp.ViewOptions
#available(iOS 16.0, *)
struct NavigationRouter{
var path: [NavSource] = .init()
///Adds the provided View to the stack
mutating func goTo(view: NavSource){
path.append(view)
}
///Searches the stack for the `View`, if the view is `nil`, the stack returns to root, if the `View` is not found the `View` is presented from the root
mutating func bactrack(view: NavSource?){
guard let view = view else{
path.removeAll()
return
}
//Look for the desired view
while !path.isEmpty && path.last != view{
path.removeLast()
}
//If the view wasn't found add it to the stack
if path.isEmpty{
goTo(view: view)
}
}
///Identifies the previous view in the stack, returns nil if the previous view is the root
func identifyPreviousView() -> NavSource?{
//1 == current view, 2 == previous view
let idx = path.count - 2
//Make sure idx is valid index
guard idx >= 0 else{
return nil
}
//return the view
return path[idx]
}
}
Once you have access to the router in the Views you can adjust accordingly.
#available(iOS 16.0, *)
struct SampleApp: View {
#State private var router: NavigationRouter = .init()
var body: some View {
NavigationStack(path: $router.path){
A(router: $router)
//Have the root handle the type
.navigationDestination(for: NavSource.self) { option in
option.view($router)
}
}
}
//Create an `enum` so you can define your options
//Conform to all the required protocols
enum ViewOptions: Codable, Equatable, Hashable{
case caseB
case caseC
//If you need other arguments add like this
case unknown(String)
//Assign each case with a `View`
#ViewBuilder func view(_ path: Binding<NavigationRouter>) -> some View{
switch self{
case .caseB:
B(router: path)
case .caseC:
C(router: path)
case .unknown(let string):
Text("View for \(string.description) has not been defined")
}
}
}
}
#available(iOS 16.0, *)
struct A: View {
#Binding var router: NavigationRouter
var body: some View {
VStack{
Button {
router.goTo(view: .caseB)
} label: {
Text("To B")
}
Button {
router.goTo(view: .caseC)
} label: {
Text("To C")
}
}.navigationTitle("A")
}
}
#available(iOS 16.0, *)
struct B: View {
#Binding var router: NavigationRouter
var body: some View {
VStack{
Button {
router.goTo(view: .caseC)
} label: {
Text("Hello")
}
}.navigationTitle("B")
}
}
#available(iOS 16.0, *)
struct C: View {
#Binding var router: NavigationRouter
//Identify changes based on previous View
var fromA: Bool{
//nil is the root
router.identifyPreviousView() == nil
}
var body: some View {
VStack{
Text("Welcome\(fromA ? " Back" : "" )")
Button {
//Append to the path the enum value
router.bactrack(view: router.identifyPreviousView())
} label: {
Text("Back")
}
Button {
//Append to the path the enum value
router.goTo(view: .unknown("\"some other place\""))
} label: {
Text("Next")
}
}.navigationTitle("C")
.navigationBarBackButtonHidden(true)
}
}
You can read the second-to-last item in the path property to learn what the previous screen was.
To do this, it's easier to use an actual array of ViewOptions as the path, instead of a NavigationPath.
For example:
struct SampleApp: App {
// Use your own ViewOptions enum, instead of NavigationPath
#State private var path: [ViewOptions] = []
var body: some Scene {
WindowGroup {
NavigationStack(path: $path){
A(path: $path)
.navigationDestination(for: ViewOptions.self) { option in
option.view($path)
}
}
}
}
}
struct C: View {
#Binding var path: [ViewOptions]
var previousView: ViewOptions? {
path
.suffix(2) // Get the last 2 elements of the path
.first // Get the first of those last 2 elements
}
var body: some View {
VStack {
Text("C")
}
}
}
Remember, a NavigationPath is nothing more than a type-erased array. It can be used to build a NavigationStack quickly without having to worry that all destination values have to match the same type. Since as you're controlling the navigation flow with your own type ViewOptions, it makes no sense to use NavigationPath.

How to run child function from parent?

I want to call childFunction() demo ChildView by pressing the button in the parent view.
import SwiftUI
struct ChildView: View {
func childFunction() {
print("I am the child")
}
var body: some View {
Text("I am the child")
}
}
struct ContentView: View {
var function: (() -> Void)?
var body: some View {
ChildView()
Button(action: {
self.function!()
}, label: {
Text("Button")
})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Update:
Thanks #RajaKishan, it works, but I need it working also recursively
import SwiftUI
struct ContentView: View {
#State var text: String = "Parent"
var isNavigationViewAvailable = true
func function() {
print("This view is \(text)")
}
var body: some View {
VStack {
if isNavigationViewAvailable {
Button(action: {
function()
}, label: {
Text("Button")
})
}
if isNavigationViewAvailable {
NavigationView {
List {
NavigationLink("Child1") {
ContentView(text: "Child1", isNavigationViewAvailable: false)
}
NavigationLink("Child2") {
ContentView(text: "Child2", isNavigationViewAvailable: false)
}
NavigationLink("Child3") {
ContentView(text: "Child3", isNavigationViewAvailable: false)
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Maybe is is not the best looking example, but the question is, how to force the button to run function of it's child after user visited corresponding child.
Like, on start when user presses the button it prints "This view is Parent". After user comes to child1 the button press should print "This view is Child1" as so on. So, the function that button runs should be referenced from the last child.
Update:
In the end I wrote this solution.
Update:
I received feedback, asking me for clarification. No problem. I hope it'll help somebody.:)
Clarification:
I did not enclose the whole code, just used a simple example. But I needed this in my implementation of a tree-like generated menu: when each item in the menu has or does not have its children. Pressing on parent object user comes into child objects. And here I needed to be able to come back from a child object to parent, but call this dismiss function from the parent object. For this I used the following code and referred to this function to each parent object:
#Environment(\.presentationMode) var presentationMode
presentationMode.wrappedValue.dismiss()
You can create an object for a ChildView.
struct ChildView: View {
func childFunction() {
print("I am the child")
}
var body: some View {
Text("I am the child")
}
}
struct ContentView: View {
let childView = ChildView()
var body: some View {
childView
Button(action: {
childView.childFunction()
}, label: {
Text("Button")
})
}
}
EDIT : For the list, you can use the array of the model and call the destination function by index.
Here is the simple child-parent example.
struct ChildView: View {
var text: String
func childFunction() {
print("This view is \(text)")
}
var body: some View {
Text("I am the child")
}
}
struct ContentView55: View {
#State private var arrData = [Model(title: "Child1", destination: ChildView(text: "Child1")),
Model(title: "Child2", destination: ChildView(text: "Child2")),
Model(title: "Child3", destination: ChildView(text: "Child3"))]
var body: some View {
VStack {
Button(action: {
arrData[1].destination.childFunction()
}, label: {
Text("Button")
})
NavigationView {
SwiftUI.List(arrData) {
NavigationLink($0.title, destination: $0.destination)
}
}
}
}
}
struct Model: Identifiable {
var id = UUID()
var title: String
var destination: ChildView
}
Note: You need to index for the row to call child function.

SwiftUI: How can I determine if a view is presented in a NavigationView, Sheet, or is the root?

I'm working on a project that requires a custom navigation bar that will have custom buttons and title styling, while also allowing an accessory view below the main nav portion.
Essentially, I'd like to abstract away the need to choose the custom back button based on the presentation style. If it's presented in a sheet, I plan to show an X icon. If it is pushed onto a navigation view I want to show a back error. If it's a root view I want to hide the button altogether.
I've mapped the presentationMode environment variable however when I access the isPresented value I always get true, even on the root view of my app.
Here's a general idea of what I'm working on:
import SwiftUI
struct CustomNavigationBar<Content>: View where Content: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
private let title: LocalizedStringKey
private let content: (() -> Content)?
private var backButton: AnyView? {
let button = Button(action: { self.presentationMode.wrappedValue.dismiss() }) {
// custom image extension, just resolves to a back icon
Image.Icons.arrowBack
}
if (presentationMode.wrappedValue.isPresented) {
return AnyView(button)
} else {
return nil
}
}
public init(_ title: LocalizedStringKey, content: (() -> Content)? = nil) {
self.title = title
self.content = content
}
var body: some View {
VStack {
content?()
Divider().foregroundColor(.gray)
}.navigationBarTitle(title, displayMode: .large)
.frame(minHeight: 96)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: backButton)
}
}
Does anyone have any experience or tips for accessing a view's place in the presentation hierarchy with SwiftUI? Thanks!
You can use SwiftUI-Introspect, used to "Introspect underlying UIKit components from SwiftUI".
Here is a working example of what you are looking for. It is an interactive example, so you can click through the different modes.
import Introspect
import SwiftUI
/* ... */
struct ContentView: View {
#State private var testing = 1
private let thingsToTest = 3
var body: some View {
VStack {
Picker("Testing", selection: $testing) {
ForEach(1 ... thingsToTest, id: \.self) { index in
Text("\(index)")
.tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
Divider()
Spacer()
switch testing {
case 1:
PresentationReader { kind in
Text("Hello! Kind: \(kind.rawValue)")
}
case 2:
NavigationView {
PresentationReader { kind in
Text("Hello! Kind: \(kind.rawValue)")
}
}
case 3:
Text("Parent")
.sheet(isPresented: .constant(true)) {
PresentationReader { kind in
Text("Hello! Kind: \(kind.rawValue)")
}
}
default:
fatalError("Unavailable")
}
Spacer()
}
}
}
enum Kind: String {
case navigationView
case root
case sheet
}
struct PresentationReader<Content: View>: View {
typealias PresentedContent = (Kind) -> Content
#State private var kind: Kind = .root
private let content: PresentedContent
init(#ViewBuilder content: #escaping PresentedContent) {
self.content = content
}
var body: some View {
content(kind)
.presentationReader(kind: $kind)
}
}
extension View {
func presentationReader(kind: Binding<Kind>) -> some View {
self
.introspectViewController { vc in
let rootVC = UIApplication.shared.windows.first?.rootViewController
let isRoot = vc === rootVC
var isHosted: Bool { Introspect.findHostingView(from: vc.view) != nil }
if isRoot {
kind.wrappedValue = .root
} else if isHosted {
kind.wrappedValue = .navigationView
} else {
kind.wrappedValue = .sheet
}
}
}
}
It works by getting the current view controller the view is in.
If the class reference of the root view controller is the same as the current root view controller, this is the root view (meaning it isn't embedded in a NavigationView or .sheet(...)).
If this is not the root, we then check if this view is embedded in a hosting view. If it is, it is in a NavigationView.
If the view is neither the root or in a NavigationView, it is therefore in a .sheet(...).
This is now what your CustomNavigationBar will look like with these 3 changes:
struct CustomNavigationBar<Content>: View where Content: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State private var kind: Kind = .root // <--- CHANGE #1
private let title: LocalizedStringKey
private let content: (() -> Content)?
private var backButton: AnyView? {
let button = Button(action: { self.presentationMode.wrappedValue.dismiss() }) {
// custom image extension, just resolves to a back icon
Image.Icons.arrowBack
}
if kind == .navigationView { // <--- CHANGE #2
return AnyView(button)
} else {
return nil
}
}
public init(_ title: LocalizedStringKey, content: (() -> Content)? = nil) {
self.title = title
self.content = content
}
var body: some View {
VStack {
content?()
.presentationReader(kind: $kind) // <--- CHANGE #3
Divider().foregroundColor(.gray)
}.navigationBarTitle(title, displayMode: .large)
.frame(minHeight: 96)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: backButton)
}
}

How to make NavigationLink work if it is not visible, SwiftUI?

When using NavigationLink on the bottom of a view after ForEach it won't work if it is not visible.
I have a list of Buttons. If a button is pressed, it sets a Bool to true. This bool value now shows a NavigationLink which immediately activates because the passed binding is set to true.
However, the link won't work if the array is too long because it will be out of sight once one of the first buttons is pressed.
This is my Code:
import SwiftUI
struct TestLinkView: View {
#State private var linkIsActive = false
var body: some View {
NavigationView {
VStack {
Button(action: {
linkIsActive = true
}) {
Text("Press")
}
NavigationLink(destination: ListView(linkIsActive: $linkIsActive), isActive: $linkIsActive) {
Text("Navigation Link")
}
}
}
}
}
struct ListView: View {
var nameArray = ["Name1","Name2","Name3","Name4","Name5","Name6","Name7","Name8","Name9","Name10","Name11","Name12","Name13","Name14","Name15","Name16","Name17","Name18","Name19","Name20" ]
#State private var showLink: Bool = false
#State private var selectedName: String = ""
#Binding var linkIsActive: Bool
var body: some View {
Form {
ForEach(nameArray, id: \.self) { name in
Button(action: {
selectedName = name
showLink = true
}) {
Text(name)
}
}
if showLink {
NavigationLink(destination: NameView(selectedName: selectedName), isActive: $linkIsActive) {
EmptyView()
}
}
}
.navigationBarTitle("ListView")
}
}
struct NameView: View {
var selectedName: String
var body: some View {
Text(selectedName)
.navigationBarTitle("NameView")
}
}
What would work is to pass the NavigationLink with the if-condition inside the button label. However if I do that, the animation won't work anymore.
You don't need it in Form, which is like a List don't create views far outside of visible area. In your case the solution is to just move link into background of Form (because it does not depend on form internals).
The following tested as worked with Xcode 12 / iOS 14.
Form {
ForEach(nameArray, id: \.self) { name in
Button(action: {
selectedName = name
showLink = true
}) {
Text(name)
}
}
}
.background(Group{
if showLink {
NavigationLink(destination: NameView(selectedName: selectedName), isActive: $linkIsActive) {
EmptyView()
}
}
})

Resources