Optional #ViewBuilder closure - ios

Is it possible in SwiftUI to have an optional #ViewBuilder closure? For example, let's say I want to develop a custom view that takes two view builder closures like this:
import SwiftUI
struct TopAndBottomView<Content>: View where Content: View {
let topContent: () -> Content
let bottomContent: () -> Content
init(#ViewBuilder topContent: #escaping () -> Content, #ViewBuilder bottomContent: #escaping () -> Content) {
self.topContent = topContent
self.bottomContent = bottomContent
}
var body: some View {
VStack {
topContent()
Spacer()
bottomContent()
}
}
}
struct TopAndBottomView_Previews: PreviewProvider {
static var previews: some View {
TopAndBottomView(topContent: {
Text("TOP")
}, bottomContent: {
Text("BOTTOM")
})
}
}
But I'd like the bottom view to be optional. I tried with:
struct TopAndBottomView<Content>: View where Content: View {
let topContent: () -> Content
let bottomContent: (() -> Content)?
init(#ViewBuilder topContent: #escaping () -> Content, #ViewBuilder bottomContent: (() -> Content)? = nil) {
self.topContent = topContent
self.bottomContent = bottomContent
}
var body: some View {
VStack {
topContent()
Spacer()
if bottomContent != nil {
bottomContent!()
}
}
}
}
but I get this error:
Function builder attribute 'ViewBuilder' can only be applied to a
parameter of function type.
Thanks.

Taking into account buildIf feature of ViewBuilder the following approach is possible that allows to keep ViewBuilder in init (that is preferable)
Tested & works with Xcode 11.2 / iOS 13.2
struct TopAndBottomView<Content>: View where Content: View {
let topContent: () -> Content
let bottomContent: () -> Content?
init(#ViewBuilder topContent: #escaping () -> Content,
#ViewBuilder bottomContent: #escaping () -> Content? = { nil }) {
self.topContent = topContent
self.bottomContent = bottomContent
}
var body: some View {
VStack {
topContent()
Spacer()
bottomContent()
}
}
}
So works as this one
struct TopAndBottomView_Previews: PreviewProvider {
static var previews: some View {
TopAndBottomView(topContent: {
Text("TOP")
}, bottomContent: {
Text("BOTTOM")
})
}
}
and this one
struct TopAndBottomView_Previews: PreviewProvider {
static var previews: some View {
TopAndBottomView(topContent: {
Text("TOP")
})
}
}

#JoeBayLD asked:
How would you do this if the topContent and bottomContent are different view types? I made a new generic property but when using the default 'nil' argument, any callers can't infer the content type
You can make both ViewBuilder parameters non-optional, and then handle the "no bottom content" case by making an extension where BottomContent == EmptyView:
struct TopAndBottomView<TopContent: View, BottomContent: View>: View {
let topContent: TopContent
let bottomContent: BottomContent
init(#ViewBuilder topContent: () -> TopContent,
#ViewBuilder bottomContent: () -> BottomContent) {
self.topContent = topContent()
self.bottomContent = bottomContent()
}
var body: some View {
VStack {
topContent
Spacer()
bottomContent
}
}
}
extension TopAndBottomView where BottomContent == EmptyView {
init(#ViewBuilder topContent: () -> TopContent) {
self.init(topContent: topContent, bottomContent: { EmptyView() })
}
}
// usage
TopAndBottomView(topContent: { Text("hello") })
TopAndBottomView(topContent: { Text("hello") }, bottomContent: { Text("world") })

In this fantastic post from Sundell, he suggests that we build a custom struct Unwrap to unwrap an optional value and turn it into a View, the following code is what he did in that post:
import SwiftUI
/// # Unwrap
/// unwraps a value (of type `Value`) and turns it
/// into `some View` (== `Optional<Content>`).
struct Unwrap<Value, Content: View>: View {
private let value : Value? // value to be unwrapped
private let content: (Value) -> Content // closure: turn `Value` into `Content`
init(
_ value: Value?,
#ViewBuilder content: #escaping (Value) -> Content // ⭐️ #ViewBuilder
) {
self.value = value
self.content = content
}
var body: some View {
// map: (by the closure `content`)
// nil (Optional<Value>.none) -> nil (Optional<Content>.none)
// Optional<Value>.some(Value) -> Optional<Content>.some(Content)
value.map(content) // Optional<Content>
}
}
And then I wrote some code to demonstrate how we could use Unwrap to construct our views:
import SwiftUI
// MyView
struct MyView: View {
#State private var isValue1Nil = false
#State private var isValue2Nil = false
var value1: Int? { isValue1Nil ? nil : 1}
var value2: Int? { isValue2Nil ? nil : 2}
var body: some View {
VStack {
// stack of `Unwrap`s
VStack {
// ⭐️ `Unwrap` used here.
Unwrap(value1) {
Color.red.overlay(Text("\($0)"))
}
Unwrap(value2) {
Color.orange.overlay(Text("\($0)"))
}
}.border(Color.blue, width: 3)
// toggles
HStack {
Toggle(isOn: $isValue1Nil) {
Text("value1 is nil")
}
Toggle(isOn: $isValue2Nil) {
Text("value2 is nil")
}
Spacer()
}
.padding()
.overlay(Rectangle().stroke(Color.gray, style: .init(dash: [6])))
} // VStack (container)
.padding()
.border(Color.gray, width: 3)
}
}
And the result is as follows:
----[ edited ]----
Or alternatively, we can make a View extension to do the job:
// view.ifLet(_:then:)
extension View {
#ViewBuilder func ifLet<Value, Content: View>(
_ value: Value?,
#ViewBuilder then modifySelfWithValue: (Self, Value) -> Content
) -> some View {
if value != nil {
modifySelfWithValue(self, value!)
} else { self }
}
}
The following is another demo on how to use this extension:
struct ContentView: View {
#State private var isNil = false
var value: Int? { isNil ? nil : 2 }
var body: some View {
VStack {
Color.red.overlay(Text("1"))
// ⭐️ view.ifLet(_:then:)
.ifLet(value) { (thisView, value) in
// construct new view with `thisView` and `value`
VStack {
thisView
Color.orange.overlay(Text("\(value)"))
}
} // view modified by `ifLet`
.border(Color.blue, width: 3)
// toggles
Toggle(isOn: $isNil) { Text("value is nil") }
.padding()
.overlay(Rectangle().stroke(Color.gray, style: .init(dash: [6])))
} // VStack (container)
.padding()
.border(Color.gray, width: 3).frame(height: 300)
}
}
and the result is:

Set the default value for #ViewBuilder View, in order to achieve what you're looking for:
struct AlertView<InputFields: View, Actions: View>: View {
private let inputFields: InputFields
private let actions: Actions
init(
#ViewBuilder inputFields: () -> InputFields = { EmptyView() }, <=== HERE
#ViewBuilder actions: () -> Actions = { EmptyView() } <=== HERE
) {
self.inputFields = inputFields()
self.actions = actions()
}
var body: some View {
VStack{
inputFields
actions
}
}
}

Instead of an optional #ViewBuilder parameter, a workaround is to set the default value of the parameter to EmptyView(). While this is not possible directly in the SwiftUI view struct, we can add an extension with an init() as follows:
/// View with mandatory icon view builder.
struct Hint<IconView: View>: View {
var message: String
#ViewBuilder var icon: IconView
var body: some View {
HStack {
icon.frame(width: 40, height: 40)
Text(message)
}
}
}
/// View Extensions that sets the icon view builder default to EmptyView().
extension Hint<EmptyView> {
init(message: String) {
self.message = message
self.icon = EmptyView()
}
}
Like this you can use the Hint-View either by including the icon view builder or by leaving it out (in which case the default EmptyView is used):
Hint(message: "This is a warning with icon!", icon: { Image(systemName: .exclamationmarkTriangle) })
Hint(message: "This is a warning with icon!")

It seems that you dont need the #ViewBuilder in your initializer so this would work:
struct TopAndBottomView<Content>: View where Content: View {
let topContent: () -> Content
let bottomContent: (() -> Content)?
init(#ViewBuilder topContent: #escaping () -> Content, bottomContent: (() -> Content)? = nil) {
self.topContent = topContent
self.bottomContent = bottomContent
}
var body: some View {
VStack {
topContent()
Spacer()
if bottomContent != nil {
bottomContent!()
}
}
}
}
And how to use:
TopAndBottomView(topContent: {
Text("top")
})
TopAndBottomView(topContent: {
Text("top")
}, bottomContent: {
Text("optional bottom")
})

Related

SwiftUI: Add refreshable to LazyVStack?

When I use a List view I can easily add the refreshable modifier to trigger refresh logic. My question is how to achieve the same when using a LazyVStack instead.
I have the following code:
struct TestListView: View {
var body: some View {
Text("the list view")
// WORKS:
// VStack {
// List {
// ForEach(0..<10) { n in
// Text("N = \(n)")
// }
// }
// .refreshable {
//
// }
// }
// DOES NOT SHOW REFRESH CONTROL:
ScrollView {
LazyVStack {
ForEach(0..<10) { n in
Text("N = \(n)")
}
}
}
.refreshable {
}
}
}
How can I get the pull to refresh behavior in the LazyVStack case?
Actually it is possible, and I'd say, I understand Apple's idea - they give default built-in behavior for heavy List, but leave lightweight ScrollView just prepared so we could customise it in whatever way we need.
So here is a demo of solution (tested with Xcode 13.4 / iOS 15.5)
Main part:
struct ContentView: View {
var body: some View {
ScrollView {
RefreshableView()
}
.refreshable { // << injects environment value !!
await fetchSomething()
}
}
}
struct RefreshableView: View {
#Environment(\.refresh) private var refresh // << refreshable injected !!
#State private var isRefreshing = false
var body: some View {
VStack {
if isRefreshing {
MyProgress()
.transition(.scale)
}
// ...
.onPreferenceChange(ViewOffsetKey.self) {
if $0 < -80 && !isRefreshing { // << any creteria we want !!
isRefreshing = true
Task {
await refresh?() // << call refreshable !!
await MainActor.run {
isRefreshing = false
}
}
}
}
Complete test module is here
Based on Asperi answer:
import SwiftUI
struct ContentView: View {
var body: some View {
ScrollView {
RefreshableView {
RoundedRectangle(cornerRadius: 20)
.fill(.red).frame(height: 100).padding()
.overlay(Text("Button"))
.foregroundColor(.white)
}
}
.refreshable { // << injects environment value !!
await fetchSomething()
}
}
func fetchSomething() async {
// demo, assume we update something long here
try? await Task.sleep(nanoseconds: 2 * 1_000_000_000)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct RefreshableView<Content: View>: View {
var content: () -> Content
#Environment(\.refresh) private var refresh // << refreshable injected !!
#State private var isRefreshing = false
var body: some View {
VStack {
if isRefreshing {
MyProgress() // ProgressView() ?? - no, it's boring :)
.transition(.scale)
}
content()
}
.animation(.default, value: isRefreshing)
.background(GeometryReader {
// detect Pull-to-refresh
Color.clear.preference(key: ViewOffsetKey.self, value: -$0.frame(in: .global).origin.y)
})
.onPreferenceChange(ViewOffsetKey.self) {
if $0 < -80 && !isRefreshing { // << any creteria we want !!
isRefreshing = true
Task {
await refresh?() // << call refreshable !!
await MainActor.run {
isRefreshing = false
}
}
}
}
}
}
struct MyProgress: View {
#State private var isProgress = false
var body: some View {
HStack{
ForEach(0...4, id: \.self){index in
Circle()
.frame(width:10,height:10)
.foregroundColor(.red)
.scaleEffect(self.isProgress ? 1:0.01)
.animation(self.isProgress ? Animation.linear(duration:0.6).repeatForever().delay(0.2*Double(index)) :
.default
, value: isProgress)
}
}
.onAppear { isProgress = true }
.padding()
}
}
public struct ViewOffsetKey: PreferenceKey {
public typealias Value = CGFloat
public static var defaultValue = CGFloat.zero
public static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
Now SwiftUI Added the .refreshable modifier to ScrollView.
Just use it the way you do with List
ScrollView {
LazyVStack {
// Loop and add View
}
}
.refreshable {
refreshLogic()
}
Its supported starting iOS 15 though.
Here is the documentation reference
#available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
extension View {
/// Marks this view as refreshable.
public func refreshable(action: #escaping #Sendable () async -> Void) -> some View
}
Simply create file RefreshableScrollView in your project
public struct RefreshableScrollView<Content: View>: View {
var content: Content
var onRefresh: () -> Void
public init(content: #escaping () -> Content, onRefresh: #escaping () -> Void) {
self.content = content()
self.onRefresh = onRefresh
}
public var body: some View {
List {
content
.listRowSeparatorTint(.clear)
.listRowBackground(Color.clear)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
.listStyle(.plain)
.refreshable {
onRefresh()
}
}
}
then use RefreshableScrollView anywhere in your project
example:
RefreshableScrollView{
// Your content LaztVStack{}
} onRefresh: {
// do something you want
}

SwiftUI Slide Over Animation Like Builtin Navigation

I'm experimenting with replicating SwiftUI's navigation without all the black box magic. However, I'm having trouble with the animation. No animation happens until maybe the second or third push/pop. When it does finally animate, it's hard to describe what it does. But it definitely isn't what I would expect.
I've tried various different animations but it's generally the same behavior.
struct RouterDemo: View {
#State private var items: [Int] = Array(0..<50)
#State private var selectedItem: Int?
var body: some View {
RouterStore(
route: $selectedItem,
state: { route in items.first(where: { $0 == route }) },
content: { ItemsList(items: items, selectedItem: $0) },
destination: { route, item in
ItemDetail(item: item, selectedItem: route)
}
)
}
}
public struct RouterStore<Destination, Content, Route, DestinationState>: View
where Destination: View,
Content: View,
Route: Hashable,
DestinationState: Equatable {
#Binding private var route: Route?
private let toDestinationState: (Route) -> DestinationState?
private let destination: (Binding<Route?>, DestinationState) -> Destination
private let content: (Binding<Route?>) -> Content
public init(
route: Binding<Route?>,
state toDestinationState: #escaping (Route) -> DestinationState?,
#ViewBuilder content: #escaping (Binding<Route?>) -> Content,
#ViewBuilder destination: #escaping (Binding<Route?>, DestinationState) -> Destination
) {
self._route = route
self.toDestinationState = toDestinationState
self.destination = destination
self.content = content
}
public var body: some View {
GeometryReader { geometry in
ZStack {
content($route)
wrappedDestination()
.frame(width: geometry.size.width)
.offset(
x: route == nil ? geometry.size.width : 0,
y: 0
)
.animation(self.animation)
}
}
}
private var animation: Animation = .easeIn(duration: 2)
#ViewBuilder
private func wrappedDestination() -> some View {
if let _route = Binding($route),
let _destinationState = toDestinationState(_route.wrappedValue) {
ZStack {
Group {
if #available(iOS 15.0, *) {
Color(uiColor: UIColor.systemBackground)
} else {
Color(UIColor.systemBackground)
}
}
.preferredColorScheme(.light)
.ignoresSafeArea()
self.destination($route, _destinationState)
}
} else {
EmptyView()
}
}
}
struct ItemsList: View {
let items: [Int]
#Binding var selectedItem: Int?
var body: some View {
List {
ForEach(items, id: \.self) { item in
Button(
action: { selectedItem = item },
label: { Text(String(item)) }
)
.contentShape(Rectangle())
}
}
}
}
struct ItemDetail: View {
let item: Int
#Binding var selectedItem: Int?
var body: some View {
VStack {
Text(String(item))
Button(
action: { selectedItem = nil },
label: { Text("Back") }
)
}
}
}
Thanks to the links Asperi provided, I figured it out.
Applying the animation to the container and providing the value to monitor to the animation fixed it.

SwiftUI, Passing a View as params in #Viewbuilder

My curiosity takes me to pass a View type as parameter to #ViewBuilder. Passing a Model/Primitive type as param in #ViewBuilder is perfectly valid.
As shown below code.
struct TestView<Content: View>: View {
let content: (String) -> Content
init(#ViewBuilder content: #escaping (String) -> Content) {
self.content = content
}
var body: some View {
content("Some text")
}
}
struct ContentTestView: View {
var body: some View {
TestView {
Text("\($0)")
}
}
}
In place of String in
let content: (String) -> Content
If I try to pass a SwiftUI View type, then Compiler is not happy with it.
let content: (View) -> Content
Even though params for #ViewBuilder accepts custom Protocol type like Searchable but not View protocol.
compiler tell me this Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements
My whole idea is that content can be allowed to hold Section/List/Text in it.
Edit: I expected code like below.
struct TestView<Content: View>: View {
let content: (View) -> Content
init(#ViewBuilder content: #escaping (View) -> Content) {
self.content = content
}
var body: some View {
content(
List {
ForEach(0..<10) { i in
Text(\(i))
}
}
)
}
}
struct ContentTestView: View {
var body: some View {
TestView { viewbody -> _ in
Section(header: Text("Header goes here")) {
viewbody
}
}
}
}
Any way can I achieve this ?
The possible solution is to use AnyView, like
struct TestView<Content: View>: View {
let content: (AnyView) -> Content
init(#ViewBuilder content: #escaping (AnyView) -> Content) {
self.content = content
}
var body: some View {
content(AnyView(
Text("Demo") // << put here any view hierarchy
))
}
}

Building a custom TabView-like view in SwiftUI

I've been trying to build a toolbar-tabview component for macOS that can be composed with child views just like the TabView like bellow:
struct ContentView: View {
var body: some View {
TabView {
Text("First View")
.tabItem {
Image(name: "NSUserAccounts")
Text("First")
}.tag(0)
Text("Second View")
.tabItem {
Image(name: "NSUserAccounts")
Text("Second")
}.tag(1)
}
}
}
At the moment I've something like this:
struct ToolbarTabView<Content>: NSViewControllerRepresentable where Content: View {
let content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeNSViewController(context: NSViewControllerRepresentableContext<ToolbarTabView>) -> NSTabViewController {
let vc = NSTabViewController()
vc.tabStyle = .toolbar
for item in self.content() as! something? {
let t = NSTabViewItem(viewController: NSHostingController(rootView: item))
t.image = item.image
t.label = item.label
t.identifier = item.identifier
vc.addTabViewItem(t)
}
return vc
}
func updateNSViewController(_ nsViewController: NSTabViewController, context: NSViewControllerRepresentableContext<ToolbarTabView>) {
}
typealias NSViewControllerType = NSTabViewController
}
Is something like this possible with SwiftUI? How is TabView content being casted and used to get Image and Label information?
Instead of just passing content to your ToolbarTabView, you can have it take an array that also contains other values. Try something like this:
struct ToolbarTabView<Content>: NSViewControllerRepresentable where Content: View {
let tabs: [(imageName: String, label: String, identifier: Int, content: () -> Content)]
init(tabs: [(imageName: String, label: String, identifier: Int, content: () -> Content)]) {
self.tabs = tabs
}
func makeNSViewController(context: NSViewControllerRepresentableContext<ToolbarTabView>) -> NSTabViewController {
let vc = NSTabViewController()
vc.tabStyle = .toolbar
for item in tabs {
let t = NSTabViewItem(viewController: NSHostingController(rootView: item.content()))
//t.image = item.image
t.label = item.label
t.identifier = item.identifier // this causes an "unrecognized selector" error, but maybe I'm passing a bad value for this, I'm not sure what it expects
vc.addTabViewItem(t)
}
return vc
}
func updateNSViewController(_ nsViewController: NSTabViewController, context: NSViewControllerRepresentableContext<ToolbarTabView>) {
}
typealias NSViewControllerType = NSTabViewController
}
struct ToolbarTabView_Previews: PreviewProvider {
static var previews: some View {
ToolbarTabView(tabs: [
(
imageName: "My Image",
label: "tab1",
identifier: 0,
content: {
Text("testing")
}
)
])
}
}

SwiftUI NavigationLink loads destination view immediately, without clicking

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

Resources