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

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

Related

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

Value of Selected Option From a SwiftUI Picker does not Update the View

I have the following in a SwiftUI app. Basically I have some settings (Settings class) that I would like to use throughout the app. I have a Settings view that shows a picker to select the value of one of the settings. And other views of the app would only use the current set value of the settings. The following setup works in the sense that in ContentView I see the correct value of firstLevel setting. But the problem is that in SettingsView, I think since selectedFirstLevel is not a #State, its correct value is not shown on the picker I navigate to select either even or odd (oddly, the first time it's correct). This selection is carried correctly to ContentView, but it's not shown correctly on SettingsView. How can I fix this issue?
Settings.swift
import Foundation
class Settings: ObservableObject {
static let shared: Settings = Settings()
#Published var firstLevel: FirstLevel = .even
}
enum FirstLevel: String, CaseIterable, Identifiable {
case even
case odd
var id: String { self.rawValue }
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#State private var showSettings: Bool = false
#ObservedObject var settings = Settings.shared
var body: some View {
VStack {
SettingsButton(showSettings: $showSettings, settings: settings)
Text(settings.firstLevel.id)
.padding()
}
}
}
struct SettingsButton: View {
#Binding var showSettings: Bool
var settings: Settings
var firstLevel: Binding<FirstLevel> {
return Binding<FirstLevel>(
get: {
return self.settings.firstLevel
}) { newFirstLevel in
self.settings.firstLevel = newFirstLevel
}
}
var body: some View {
Button(action: { self.showSettings = true }) {
Image(systemName: "gear").imageScale(.large)
}
.sheet(isPresented: $showSettings) {
SettingsView(selectedFirstLevel: self.firstLevel)
}
}
}
SettingsView.swift
import SwiftUI
struct SettingsView: View {
#Binding var selectedFirstLevel: FirstLevel
var body: some View {
NavigationView {
Form {
Picker("First Level", selection: $selectedFirstLevel) {
ForEach(FirstLevel.allCases) { level in
Text(level.rawValue).tag(level)
}
}
}
.navigationBarTitle("Settings", displayMode: .inline)
}
}
}
It looks overcomplicated, moreover Binding is unreliable as communication between different view hierarchies (which is sheet in your case).
Here is simplified and worked variant. Tested with Xcode 12 / iOS 14.
struct ContentView: View {
#ObservedObject var settings = FLevelSettings.shared
var body: some View {
VStack {
SettingsButton(settings: settings)
Text(settings.firstLevel.id)
.padding()
}
}
}
struct SettingsButton: View {
#State private var showSettings: Bool = false
var settings: FLevelSettings
var body: some View {
Button(action: { self.showSettings = true }) {
Image(systemName: "gear").imageScale(.large)
}
.sheet(isPresented: $showSettings) {
FLevelSettingsView(settings: self.settings)
}
}
}
struct FLevelSettingsView: View {
#ObservedObject var settings: FLevelSettings
var body: some View {
NavigationView {
Form {
Picker("First Level", selection: $settings.firstLevel) {
ForEach(FirstLevel.allCases) { level in
Text(level.rawValue).tag(level)
}
}
}
.navigationBarTitle("Settings", displayMode: .inline)
}
}
}
Note: it can be even more simplified, if you want, due to presence of FLevelSettings.shared, so you can use it inside FLevelSettingsView directly. Just in case.

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

Dismiss NavigationView when Hidden SwiftUI [duplicate]

I was playing around with SwiftUI and want to be able to come back to the previous view when tapping a button, the same we use popViewController inside a UINavigationController.
Is there a provided way to do it so far ?
I've also tried to use NavigationDestinationLink to do so without success.
struct AView: View {
var body: some View {
NavigationView {
NavigationButton(destination: BView()) {
Text("Go to B")
}
}
}
}
struct BView: View {
var body: some View {
Button(action: {
// Trying to go back to the previous view
// previously: navigationController.popViewController(animated: true)
}) {
Text("Come back to A")
}
}
}
Modify your BView struct as follows. The button will perform just as popViewController did in UIKit.
struct BView: View {
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
var body: some View {
Button(action: { self.mode.wrappedValue.dismiss() })
{ Text("Come back to A") }
}
}
Use #Environment(\.presentationMode) var presentationMode to go back previous view. Check below code for more understanding.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
ZStack {
Color.gray.opacity(0.2)
NavigationLink(destination: NextView(), label: {Text("Go to Next View").font(.largeTitle)})
}.navigationBarTitle(Text("This is Navigation"), displayMode: .large)
.edgesIgnoringSafeArea(.bottom)
}
}
}
struct NextView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
ZStack {
Color.gray.opacity(0.2)
}.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: { Image(systemName: "arrow.left") }))
.navigationBarTitle("", displayMode: .inline)
}
}
struct NameRow: View {
var name: String
var body: some View {
HStack {
Image(systemName: "circle.fill").foregroundColor(Color.green)
Text(name)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
With State Variables. Try that.
struct ContentViewRoot: View {
#State var pushed: Bool = false
var body: some View {
NavigationView{
VStack{
NavigationLink(destination:ContentViewFirst(pushed: self.$pushed), isActive: self.$pushed) { EmptyView() }
.navigationBarTitle("Root")
Button("push"){
self.pushed = true
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct ContentViewFirst: View {
#Binding var pushed: Bool
#State var secondPushed: Bool = false
var body: some View {
VStack{
NavigationLink(destination: ContentViewSecond(pushed: self.$pushed, secondPushed: self.$secondPushed), isActive: self.$secondPushed) { EmptyView() }
.navigationBarTitle("1st")
Button("push"){
self.secondPushed = true;
}
}
}
}
struct ContentViewSecond: View {
#Binding var pushed: Bool
#Binding var secondPushed: Bool
var body: some View {
VStack{
Spacer()
Button("PopToRoot"){
self.pushed = false
} .navigationBarTitle("2st")
Spacer()
Button("Pop"){
self.secondPushed = false
} .navigationBarTitle("1st")
Spacer()
}
}
}
This seems to work for me on watchOS (haven't tried on iOS):
#Environment(\.presentationMode) var presentationMode
And then when you need to pop
self.presentationMode.wrappedValue.dismiss()
There is now a way to programmatically pop in a NavigationView, if you would like. This is in beta 5.
Notice that you don't need the back button. You could programmatically trigger the showSelf property in the DetailView any way you like. And you don't have to display the "Push" text in the master. That could be an EmptyView(), thereby creating an invisible segue.
(The new NavigationLink functionality takes over the deprecated NavigationDestinationLink)
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
MasterView()
}
}
}
struct MasterView: View {
#State var showDetail = false
var body: some View {
VStack {
NavigationLink(destination: DetailView(showSelf: $showDetail), isActive: $showDetail) {
Text("Push")
}
}
}
}
struct DetailView: View {
#Binding var showSelf: Bool
var body: some View {
Button(action: {
self.showSelf = false
}) {
Text("Pop")
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
It seems that a ton of basic navigation functionality is super buggy, which is disappointing and may be worth walking away from for now to save hours of frustration. For me, PresentationButton is the only one that works. TabbedView tabs don't work properly, and NavigationButton doesn't work for me at all. Sounds like YMMV if NavigationButton works for you.
I'm hoping that they fix it at the same time they fix autocomplete, which would give us much better insight as to what is available to us. In the meantime, I'm reluctantly coding around it and keeping notes for when fixes come out. It sucks to have to figure out if we're doing something wrong or if it just doesn't work, but that's beta for you!
Update: the NavigationDestinationLink API in this solution has been deprecated as of iOS 13 Beta 5. It is now recommended to use NavigationLink with an isActive binding.
I figured out a solution for programmatic pushing/popping of views in a NavigationView using NavigationDestinationLink.
Here's a simple example:
import Combine
import SwiftUI
struct DetailView: View {
var onDismiss: () -> Void
var body: some View {
Button(
"Here are details. Tap to go back.",
action: self.onDismiss
)
}
}
struct MainView: View {
var link: NavigationDestinationLink<DetailView>
var publisher: AnyPublisher<Void, Never>
init() {
let publisher = PassthroughSubject<Void, Never>()
self.link = NavigationDestinationLink(
DetailView(onDismiss: { publisher.send() }),
isDetail: false
)
self.publisher = publisher.eraseToAnyPublisher()
}
var body: some View {
VStack {
Button("I am root. Tap for more details.", action: {
self.link.presented?.value = true
})
}
.onReceive(publisher, perform: { _ in
self.link.presented?.value = false
})
}
}
struct RootView: View {
var body: some View {
NavigationView {
MainView()
}
}
}
I wrote about this in a blog post here.
You can also do it with .sheet
.navigationBarItems(trailing: Button(action: {
self.presentingEditView.toggle()
}) {
Image(systemName: "square.and.pencil")
}.sheet(isPresented: $presentingEditView) {
EditItemView()
})
In my case I use it from a right navigation bar item, then you have to create the view (EditItemView() in my case) that you are going to display in that modal view.
https://developer.apple.com/documentation/swiftui/view/sheet(ispresented:ondismiss:content:)
EDIT: This answer over here is better than mine, but both work: SwiftUI dismiss modal
What you really want (or should want) is a modal presentation, which several people have mentioned here. If you go that path, you definitely will need to be able to programmatically dismiss the modal, and Erica Sadun has a great example of how to do that here: https://ericasadun.com/2019/06/16/swiftui-modal-presentation/
Given the difference between declarative coding and imperative coding, the solution there may be non-obvious (toggling a bool to false to dismiss the modal, for example), but it makes sense if your model state is the source of truth, rather than the state of the UI itself.
Here's my quick take on Erica's example, using a binding passed into the TestModal so that it can dismiss itself without having to be a member of the ContentView itself (as Erica's is, for simplicity).
struct TestModal: View {
#State var isPresented: Binding<Bool>
var body: some View {
Button(action: { self.isPresented.value = false }, label: { Text("Done") })
}
}
struct ContentView : View {
#State var modalPresented = false
var body: some View {
NavigationView {
Text("Hello World")
.navigationBarTitle(Text("View"))
.navigationBarItems(trailing:
Button(action: { self.modalPresented = true }) { Text("Show Modal") })
}
.presentation(self.modalPresented ? Modal(TestModal(isPresented: $modalPresented)) {
self.modalPresented.toggle()
} : nil)
}
}
Below works for me in XCode11 GM
self.myPresentationMode.wrappedValue.dismiss()
instead of NavigationButton use Navigation DestinationLink
but You should import Combine
struct AView: View {
var link: NavigationDestinationLink<BView>
var publisher: AnyPublisher<Void, Never>
init() {
let publisher = PassthroughSubject<Void, Never>()
self.link = NavigationDestinationLink(
BView(onDismiss: { publisher.send() }),
isDetail: false
)
self.publisher = publisher.eraseToAnyPublisher()
}
var body: some View {
NavigationView {
Button(action:{
self.link.presented?.value = true
}) {
Text("Go to B")
}.onReceive(publisher, perform: { _ in
self.link.presented?.value = false
})
}
}
}
struct BView: View {
var onDismiss: () -> Void
var body: some View {
Button(action: self.onDismiss) {
Text("Come back to A")
}
}
}
In the destination pass the view you want to redirect, and inside block pass data you to pass in another view.
NavigationLink(destination: "Pass the particuter View") {
Text("Push")
}

SwiftUI View - viewDidLoad()?

Trying to load an image after the view loads, the model object driving the view (see MovieDetail below) has a urlString. Because a SwiftUI View element has no life cycle methods (and there's not a view controller driving things) what is the best way to handle this?
The main issue I'm having is no matter which way I try to solve the problem (Binding an object or using a State variable), my View doesn't have the urlString until after it loads...
// movie object
struct Movie: Decodable, Identifiable {
let id: String
let title: String
let year: String
let type: String
var posterUrl: String
private enum CodingKeys: String, CodingKey {
case id = "imdbID"
case title = "Title"
case year = "Year"
case type = "Type"
case posterUrl = "Poster"
}
}
// root content list view that navigates to the detail view
struct ContentView : View {
var movies: [Movie]
var body: some View {
NavigationView {
List(movies) { movie in
NavigationButton(destination: MovieDetail(movie: movie)) {
MovieRow(movie: movie)
}
}
.navigationBarTitle(Text("Star Wars Movies"))
}
}
}
// detail view that needs to make the asynchronous call
struct MovieDetail : View {
let movie: Movie
#State var imageObject = BoundImageObject()
var body: some View {
HStack(alignment: .top) {
VStack {
Image(uiImage: imageObject.image)
.scaledToFit()
Text(movie.title)
.font(.subheadline)
}
}
}
}
We can achieve this using view modifier.
Create ViewModifier:
struct ViewDidLoadModifier: ViewModifier {
#State private var didLoad = false
private let action: (() -> Void)?
init(perform action: (() -> Void)? = nil) {
self.action = action
}
func body(content: Content) -> some View {
content.onAppear {
if didLoad == false {
didLoad = true
action?()
}
}
}
}
Create View extension:
extension View {
func onLoad(perform action: (() -> Void)? = nil) -> some View {
modifier(ViewDidLoadModifier(perform: action))
}
}
Use like this:
struct SomeView: View {
var body: some View {
VStack {
Text("HELLO!")
}.onLoad {
print("onLoad")
}
}
}
I hope this is helpful. I found a blogpost that talks about doing stuff onAppear for a navigation view.
Idea would be that you bake your service into a BindableObject and subscribe to those updates in your view.
struct SearchView : View {
#State private var query: String = "Swift"
#EnvironmentObject var repoStore: ReposStore
var body: some View {
NavigationView {
List {
TextField($query, placeholder: Text("type something..."), onCommit: fetch)
ForEach(repoStore.repos) { repo in
RepoRow(repo: repo)
}
}.navigationBarTitle(Text("Search"))
}.onAppear(perform: fetch)
}
private func fetch() {
repoStore.fetch(matching: query)
}
}
import SwiftUI
import Combine
class ReposStore: BindableObject {
var repos: [Repo] = [] {
didSet {
didChange.send(self)
}
}
var didChange = PassthroughSubject<ReposStore, Never>()
let service: GithubService
init(service: GithubService) {
self.service = service
}
func fetch(matching query: String) {
service.search(matching: query) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let repos): self?.repos = repos
case .failure: self?.repos = []
}
}
}
}
}
Credit to: Majid Jabrayilov
Fully updated for Xcode 11.2, Swift 5.0
I think the viewDidLoad() just equal to implement in the body closure.
SwiftUI gives us equivalents to UIKit’s viewDidAppear() and viewDidDisappear() in the form of onAppear() and onDisappear(). You can attach any code to these two events that you want, and SwiftUI will execute them when they occur.
As an example, this creates two views that use onAppear() and onDisappear() to print messages, with a navigation link to move between the two:
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("Hello World")
}
}
}.onAppear {
print("ContentView appeared!")
}.onDisappear {
print("ContentView disappeared!")
}
}
}
ref: https://www.hackingwithswift.com/quick-start/swiftui/how-to-respond-to-view-lifecycle-events-onappear-and-ondisappear
I'm using init() instead. I think onApear() is not an alternative to viewDidLoad(). Because onApear is called when your view is being appeared. Since your view can be appear multiple times it conflicts with viewDidLoad which is called once.
Imagine having a TabView. By swiping through pages onApear() is being called multiple times. However viewDidLoad() is called just once.

Resources