Push View Programmatically in Callback, Swiftui

Push View programmatically in callback, SwiftUI

I've found the answer. If you want to show another view on callback you should

  1. Create state @State var pushActive = false

  2. When ViewModel notifies that login is successful set pushActive to true

    func handleSuccessfullLogin() {
    self.pushActive = true
    print("handleSuccessfullLogin")
    }
  3. Create hidden NavigationLink and bind to that state

    NavigationLink(destination: 
    ProfileView(viewModel: ProfileViewModelImpl()),
    isActive: self.$pushActive) {
    EmptyView()
    }.hidden()

Push View Programmatically After State Variable Defined

You could make your newProject an optional, since you don't really want the value until it's created. Then, you can use an if let optional binding to only return the destination to ProjectSideBar available if there's a value.

Here's a simplified version of your code:

struct Project {
var name: String
}

struct FirstView: View {
@State var newProject: Project?
@State var pushNewProject:Bool = false

@ViewBuilder var navDestination : some View {
if let newProject = newProject {
ProjectSideBar(project: newProject)
} else {
EmptyView()
}
}

var body: some View {
NavigationView {
VStack {
Button(action: {
self.newProject = Project(name: "test")
self.pushNewProject = true
}){
Text("Create Project")
}

NavigationLink(destination: navDestination,
isActive: self.$pushNewProject) {
EmptyView()
}.hidden()
}
}
}
}

struct ProjectSideBar : View {
var project : Project

var body: some View {
Text(project.name)
}
}

Note that I tried using an if let around the NavigationLink first, without having to do the navDestination computed property, but that results in a situation where there's not an animation to the new view. I believe this used to work, but as of iOS 15, it seems to need the NavigationLink in the hierarchy the whole time to get the animation.

Push, Segue, Summon, Navigate to View Programmatically SwiftUI

A couple of things. First, NavigationLink must be imbedded in a NavigationView to work. Second, the link doesn't need a view as you showed it. This should show the second view. I will leave to you to update the other elements.

 var body: some View {
NavigationView{
VStack {
Text("This is the ContentView")

Toggle(isOn: $show) {
Text("Toggle var show")
}
.padding()

Button(action: {
self.show = !self.show
}, label: {
Text(self.show ? "Off" : "On")
})
Text(String(show))

//this does not work - the ContentView is still shown
NavigationLink(destination: SecondView()){
Text("Click to View")}
Spacer()
// {
// EmptyView()
// }

//this does not work - it adds SecondView to ContentView
//I want a new view here, not an addition
//to the ContentView()
// if show {
// //I want a new view here, not an addition to the ContentView()
// SecondView()
// }
}
}
}

Programmatically navigate to new view in SwiftUI

You can replace the next view with your login view after a successful login. For example:

struct LoginView: View {
var body: some View {
...
}
}

struct NextView: View {
var body: some View {
...
}
}

// Your starting view
struct ContentView: View {

@EnvironmentObject var userAuth: UserAuth

var body: some View {
if !userAuth.isLoggedin {
LoginView()
} else {
NextView()
}

}
}

You should handle your login process in your data model and use bindings such as @EnvironmentObject to pass isLoggedin to your view.

Note: In Xcode Version 11.0 beta 4, to conform to protocol 'BindableObject' the willChange property has to be added

import Combine

class UserAuth: ObservableObject {

let didChange = PassthroughSubject<UserAuth,Never>()

// required to conform to protocol 'ObservableObject'
let willChange = PassthroughSubject<UserAuth,Never>()

func login() {
// login request... on success:
self.isLoggedin = true
}

var isLoggedin = false {
didSet {
didChange.send(self)
}

// willSet {
// willChange.send(self)
// }
}
}

Push, pop view controller equivalent in SwiftUI

Root :

window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(Model()))

iOS version 13.1 :

class Model: ObservableObject {
@Published var pushed = false
}

struct ContentView: View {
@EnvironmentObject var model: Model

var body: some View {
NavigationView {
VStack {
Button("Push") {
self.model.pushed = true
}

NavigationLink(destination: DetailView(), isActive: $model.pushed) { EmptyView() }
}
}
}
}

struct DetailView: View {
@EnvironmentObject var model: Model

var body: some View {
Button("Bring me Back") {
self.model.pushed = false
}
}
}

Removing the default back button and adding our own will let us get through, until the bug gets fixed by Apple.

class Model: ObservableObject {
@Published var pushed = false
}

struct ContentView: View {
@EnvironmentObject var model: Model

var body: some View {
NavigationView {
VStack {
Button("Push") {
self.model.pushed = true
}

NavigationLink(destination: DetailView(), isActive: $model.pushed) { EmptyView() }
}
}
}
}

struct DetailView: View {
@EnvironmentObject var model: Model

var body: some View {
Button("Bring me Back") {
self.model.pushed = false
}
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: MyBackButton(label: "Back!") {
self.model.pushed = false
})
}
}

struct MyBackButton: View {
let label: String
let closure: () -> ()

var body: some View {
Button(action: { self.closure() }) {
HStack {
Image(systemName: "chevron.left")
Text(label)
}
}
}
}

more to refer

Push view with data from asynchronous callback

Based on the if let method from @pawello2222's answer, I've created this ViewModifier that let's you push a View to the Navigation Stack. It follows the same pattern as the .sheet modifier.

import SwiftUI

private struct PushPresenter<Item: Identifiable, DestinationView: View>: ViewModifier {

let item: Binding<Item?>
let destination: (Item) -> DestinationView
let presentationBinding: Binding<Bool>

init(item: Binding<Item?>, onDismiss: (() -> Void)? = nil, content: @escaping (Item) -> DestinationView) {
self.item = item
self.destination = content

presentationBinding = Binding<Bool> {
item.wrappedValue != nil
} set: { isActive in
if !isActive && item.wrappedValue != nil {
onDismiss?()
item.wrappedValue = nil
}
}
}

func body(content: Content) -> some View {
ZStack {
content
NavigationLink(
destination: item.wrappedValue == nil ? nil : destination(item.wrappedValue!),
isActive: presentationBinding,
label: EmptyView.init
)
}
}
}

extension View {

/// Pushed a View onto the navigation stack using the given item as a data source for the View's content. **Notice**: Make sure to use `.navigationViewStyle(StackNavigationViewStyle())` on the parent `NavigationView` otherwise using this modifier will cause a memory leak.
/// - Parameters:
/// - item: A binding to an optional source of truth for the view's presentation. When `item` is non-nil, the system passes the `item`’s content to the modifier’s closure. This uses a `NavigationLink` under the hood, so make sure to have a `NavigationView` as a parent in the view hierarchy.
/// - onDismiss: A closure to execute when poping the view of the navigation stack.
/// - content: A closure returning the content of the view.
func push<Item: Identifiable, Content: View>(
item: Binding<Item?>,
onDismiss: (() -> Void)? = nil,
content: @escaping (Item) -> Content) -> some View
{
self.modifier(PushPresenter.init(item: item, onDismiss: onDismiss, content: content))
}
}

And then use it like so:

struct NavigationExampleView: View {

@ObservedObject var vm: NavigationExampleViewModel

var body: some View {
Button("Get User") {
vm.getUser()
}
.push(item: $vm.pushUser, content: UserView.init)
}
}

Be sure to keep your implementing view in a NavigationView for the push to take effect.

Edit

I found an issue with this solution. If using a reference type as the binding item, the object will not get deinitialised on dismiss. It seems like the content is holding a reference to the item. The Object will only the deinitialised once a new object is set on the binding (next time a screen is pushed).

This is also the case for @pawello2222's answer, but not an issue when using the sheet modifier. Any suggestions on how to solve this would be much welcomed.

Edit 2

Adding .navigationViewStyle(StackNavigationViewStyle()) to the parent NavigationView removes the memory leak for some reason. See answer here for more details.

Using the if-let-method caused the push view transition to stop working. Instead pass nil to the NavigationLinks destination when the data is not pressent. I've updated ViewModifier to handle this.

Check if there is navigation, and present and push from a SwiftUI view

You could do this multiple ways: delegates, closures or heck even with combine publishers. The easiest way to get started is with an action closure I think. It could look something like this:

struct SwiftUIView: View {
let action: () -> Void

var body: some View {
Button("press me", action: action)
}
}

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

let swiftUIView = SwiftUIView(action: handleButtonPress)

let hostingController = UIHostingController(rootView: swiftUIView)
addChild(hostingController)

hostingController.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(hostingController.view)

NSLayoutConstraint.activate([
hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}

func handleButtonPress() {
print("TODO: Insert navigation controller logic here")
}
}

How to push a new View onto NavigationStack after Async call

You need to use NavigationLink(destination: tag: selection:) and add a @State var, i had called this as finished then you need to use NavigationLink(destination: tag: selection:) where tag will be the expected value in this case true, and in selection parameter you need to pass the @State var in this case $finished

import SwiftUI

struct LandmarkList: View {
@State var finished : Bool? = nil
let landmark = landmarkData[0]
var body: some View {

NavigationView {
VStack {
NavigationLink(destination: LandmarkDetails(currentLandmark: self.landmark), tag: true, selection: self.$finished) {
Button(action: {
self.dummyAsynCall() {
self.finished = true
}
}) {
Text("Button")
}
}
}

}.navigationBarTitle("test")

}

private func dummyAsynCall(_ onComplete: @escaping (() -> ())) {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
print("Call completed")
onComplete()
}
}
}

you can find better explanation here ;) https://mecid.github.io/2019/07/17/navigation-in-swiftui/

iOS SwiftUI: pop or dismiss view programmatically

This example uses the new environment var documented in the Beta 5 Release Notes, which was using a value property. It was changed in a later beta to use a wrappedValue property. This example is now current for the GM version. This exact same concept works to dismiss Modal views presented with the .sheet modifier.

import SwiftUI

struct DetailView: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
Button(
"Here is Detail View. Tap to go back.",
action: { self.presentationMode.wrappedValue.dismiss() }
)
}
}

struct RootView: View {
var body: some View {
VStack {
NavigationLink(destination: DetailView())
{ Text("I am Root. Tap for Detail View.") }
}
}
}

struct ContentView: View {
var body: some View {
NavigationView {
RootView()
}
}
}


Related Topics



Leave a reply



Submit