How to disable NavigationView push and pop animations - swiftui

Given this simple NavigationView:
struct ContentView : View {
var body: some View {
NavigationView {
VStack {
NavigationLink("Push Me", destination: Text("PUSHED VIEW"))
}
}
}
}
Did anyone find a way of disabling the NavigationView animation when a destination view is pushed/popped into/from the stack?
This has been possible in UIKit since iOS2.0! I think it is not too much to ask from the framework. I tried all sorts of modifiers on all views (i.e., the NavigationView container, the destination view, the NavigationLink, etc)
These are some of the modifiers I tried:
.animation(nil)
.transition(.identity)
.transaction { t in t.disablesAnimations = true }
.transaction { t in t.animation = nil }
None made a difference. I did not find anything useful in the EnvironmentValues either :-(
Am I missing something very obvious, or is the functionality just not there yet?

Xcode 11.3:
Right now there is no modifier to disable NavigationView animations.
You can use your struct init() to disable animations, as below:
struct ContentView : View {
init(){
UINavigationBar.setAnimationsEnabled(false)
}
var body: some View {
NavigationView {
VStack {
NavigationLink("Push Me", destination: Text("PUSHED VIEW"))
}
}
}
}

First you need state for the NavigationLink to respond to, then set that state inside a transaction with animations disabled, as follows:
struct ContentView : View {
#State var isActive = false
var body: some View {
NavigationView {
VStack {
NavigationLink(isActive: $isActive, destination: {
Text("PUSHED VIEW")}) {
Text("Push Me")
}
Button("Navigate Without Animation") {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
isActive = true
}
}
}
}
}
}

I recently created an open source project called swiftui-navigation-stack (https://github.com/biobeats/swiftui-navigation-stack) that contains the NavigationStackView, a view that mimics the navigation behaviours of the standard NavigationView adding some useful features. For example, you could use the NavigationStackView and disable the transition animations as requested by Kontiki in the question. When you create the NavigationStackView just specify .none as transitionType:
struct ContentView : View {
var body: some View {
NavigationStackView(transitionType: .none) {
ZStack {
Color.yellow.edgesIgnoringSafeArea(.all)
PushView(destination: View2()) {
Text("PUSH")
}
}
}
}
}
struct View2: View {
var body: some View {
ZStack {
Color.green.edgesIgnoringSafeArea(.all)
PopView {
Text("POP")
}
}
}
}
PushView and PopView are two views that allow you push and pop views (similar to the SwiftUI NavigationLink). Here is the complete example:
import SwiftUI
import NavigationStack
struct ContentView : View {
var body: some View {
NavigationStackView(transitionType: .none) {
ZStack {
Color.yellow.edgesIgnoringSafeArea(.all)
PushView(destination: View2()) {
Text("PUSH")
}
}
}
}
}
struct View2: View {
var body: some View {
ZStack {
Color.green.edgesIgnoringSafeArea(.all)
PopView {
Text("POP")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The result is:
It would be great if you guys joined me in improving this open source project.

Related

Updating EnvironmentObject pushed me back

I have two views ListView and DetailView
ListView:
#EnvironmentObject var userData: UserData
var body: some View {
VStack {
ForEach(userData.packs) { pack in
if pack.added {
NavigationLink(destination: DetailView(packIndex: self.userData.packs.firstIndex(where: { $0.id == pack.id })!)) {
MyRowViewDoesntMatter(pack: pack)
}
}
}
}
.padding(.horizontal)
}
DetailView:
#EnvironmentObject var userData: UserData
var packIndex: Int
VStack {
List {
VStack {
.... some Vies ... doesn't matter
.navigationBarItems(trailing:
THE PROBLEM IS HERE (BELOW)
Button(action: {
self.userData.packs[self.packIndex].added.toggle()
}) {
Image(systemName: self.userData.packs[self.packIndex].added ? "plus.circle.fill" : "plus.circle")
}
...
The problem is when I click on button in the navigationBarItems in DetailView. The "added" property of the "#EnvironmentObject var userData: UserData" is updated and the user's screen is going back (to the RowView). I fond out that the problem with EnvironmentObject, because the data is updated and View tries to rerender (?) that is why it pushes me back?
How to fix it? I want to stay at the DetailView screen after clicking the button.
P.S. I need to use EnvironmentObject type because then when I go back I need to see the results.
Thank you very much!
Here is possible approach (by introducing some kind of selection). As NavigationView does not allow to remove link from stack (as identifier of stacked navigation), probably also worth considering separate view model for DetailView to be applied into common container on finish editing.
Tested with Xcode 11.4 / iOS 13.4.
Some replication of your code, used for testing:
struct ListView: View {
#EnvironmentObject var userData: PushBackUserData
#State private var selectedPack: Pack? = nil
var body: some View {
NavigationView {
VStack {
ForEach(Array(userData.packs.enumerated()), id: \.element.id) { i, pack in
NavigationLink("Pack \(pack.id)", destination:
DetailView(pack: self.$selectedPack)
.onAppear {
self.selectedPack = pack
}
.onDisappear {
self.userData.packs[i].added = self.selectedPack?.added ?? false
}
).isHidden(!pack.added)
}
}
.padding(.horizontal)
}
}
}
struct DetailView: View {
#Binding var pack: Pack?
var body: some View {
VStack {
List {
VStack {
Text("Pack \(pack?.id ?? "<none>")")
}
}
.navigationBarItems(trailing:
Button(action: {
self.pack?.added.toggle()
}) {
Image(systemName: pack?.added ?? false ? "plus.circle.fill" : "plus.circle")
}
)
}
}
}
just convenient helper extension
extension View {
func isHidden(_ hidden: Bool) -> some View {
Group {
if hidden { self.hidden() }
else { self }
}
}
}

How can you switch views without having a navigationView or an popover?

I am trying to change a view without having something over it like when you used segue in swift. But the only solution I came up with is to have a navigation bar navigationBar or a popover.
struct view1: View {
var body: some View{
Button(action: {
// go to view2``
}) {
Text("press")
}
}
}
struct view2: View {
var body: some View{
Text("yeay")
}
}
If you just want to hide the navigation bar it's easy:
import SwiftUI
struct View2: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("POP")
}
}
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
}
}
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: View2()) {
Text("PUSH")
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you, instead, want to get rid of the NavigationView and NavigationLink views you have to implement your own custom navigation. It's a little more complicated. The following is just a simple example of a push/pop transition between two views.
import SwiftUI
struct View2: View {
#Binding var push: Bool
var body: some View {
ZStack {
Color.yellow
Button(action: {
withAnimation(.easeOut(duration: 0.3)) {
self.push.toggle()
}
}) {
Text("POP")
}
}
.edgesIgnoringSafeArea(.all)
}
}
struct View1: View {
#Binding var push: Bool
var body: some View {
ZStack {
Color.green
Button(action: {
withAnimation(.easeOut(duration: 0.3)) {
self.push.toggle()
}
}) {
Text("PUSH")
}
}
.edgesIgnoringSafeArea(.all)
}
}
struct ContentView: View {
#State private var push = false
var body: some View {
ZStack {
if !push {
View1(push: $push)
.transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .leading)))
}
if push {
View2(push: $push)
.transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .trailing)))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Anyone coming to this later might find this to be of interest; in short, shove a hunk of data into #environment, tickle that with a button push in whatever view you want, and since it's at the very top of the overall application stack, it forces a redraw, which acts like a full view navigation, without the potential lost memory and orphaned or lost objects of the whole push/pop navigation view silliness.
It's still a little more "single page app"-ey than I'd like, but since SwiftUI is so crippled in its navigation thoroughness, it'll do nicely.
Not my site, not my link, not my tutorial, and it's buried way down in the list of hits when searching, which is a shame; this is the closest to what many are looking for. IMO, this should be baked into SwiftUI as a first class operation, and made less workaround-ey.
https://blckbirds.com/post/how-to-navigate-between-views-in-swiftui-by-using-an-environmentobject/
You can also do this completely without NavigationView. Take a look at the following example:
struct MainView: View
{
#State private var showView = "LoginView"
var body: some View
{
switch showView
{
case "LoginView":
Text("Please login.")
Button("Login")
{
showView = "NormalView"
}
case "NormalView":
Text("This is youre NormalView!")
Button("Next view")
{
showView = "NextView"
}
case "NextView":
Text("This is the NextView")
Button("Back")
{
showView = "NormalView"
}
default:
Text("Default") // you should never reach this
}
}
}
Perhaps not the best code practice, but it solves your problem.
I think this not best way but it's easy
struct ContentView: View {
#State var gotoDetail3:Bool = false
var body: some View {
NavigationView{
ZStack{
VStack {
// Normal NavigationLink
NavigationLink {
Text("Detail......")
} label: {
Text("goto..")
}
//use for change state
Button {
gotoDetail3.toggle()
} label: {
Text("goto33333")
}
}// End VStack
// Hide Navigation Link
NavigationLink(
LocalizedStringKey("123"), destination: Text("Subsequent View"),
isActive: $gotoDetail3)
.hidden()
}
}
}
}

How to go to another view with button click

I have a button in my code and I have a file called LogindView.swift
I cannot get the code to open another view file when clicking on the button.
Can anybody give me an example on how to do it.
In my button action I have tried to write LogindView() but i just gives me a warning.
"Result of 'LogindView' initializer is unused"
Button(action: {
// Do action
LogindView()
}, label: {
//** Label text
Text("Logind")
.font(.headline)
.padding(.all)
.foregroundColor(Color.white)
})
.background(Color.blue)
You essentially have 3 options to transition between views depending on your needs.
First, you can use a NavigationView. This will provide a back button and will allow the user to go back. Note that there are some bugs currently when you don't put the NavigationLink inside of a List as per https://stackoverflow.com/a/57122621/3179416
import SwiftUI
struct MasterView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: LoginView()) {
Text("Login")
}
}
.navigationBarTitle(Text("Master"))
}
}
}
struct LoginView: View {
var body: some View {
Text("Login View")
}
}
Second, you can present a modal using .sheet. This will present a modal that appears on top of the current view but it can be dismissed by the user by dragging it down.
import SwiftUI
struct MasterView: View {
#State var isModal: Bool = false
var body: some View {
Button("Login") {
self.isModal = true
}.sheet(isPresented: $isModal, content: {
LoginView()
})
}
}
struct LoginView: View {
var body: some View {
Text("Login View")
}
}
Third, you can just use an if statement to change the current view to your Login View like so
import SwiftUI
struct MasterView: View {
#State var showLoginView: Bool = false
var body: some View {
VStack {
if showLoginView {
LoginView()
} else {
Button("Login") {
self.showLoginView = true
}
}
}
}
}
struct LoginView: View {
var body: some View {
Text("Login View")
}
}
If you would like to animate this, so that the transition doesn't appear so abruptly, you can also do this:
import SwiftUI
struct MasterView: View {
#State var showLoginView: Bool = false
var body: some View {
VStack {
if showLoginView {
LoginView()
.animation(.spring())
.transition(.slide)
} else {
Button("Login") {
withAnimation {
self.showLoginView = true
}
}.animation(.none)
}
}
}
}
struct LoginView: View {
var body: some View {
Text("Login View")
}
}
You can use navigation link instead button
var body: some View {
VStack {
Text("Title")
.font(.headline)
Image("myimage").clipShape(Circle())
Text("mytext").font(.title)
NavigationLink(destination: AnotherView()) {
Image(systemName: "person.circle").imageScale(.large)
}
}
}

Show a new View from Button press Swift UI

I would like to be able to show a new view when a button is pressed on one of my views.
From the tutorials I have looked at and other answered questions here it seems like everyone is using navigation button within a navigation view, unless im mistaken navigation view is the one that gives me a menu bar right arrows the top of my app so I don't want that. when I put the navigation button in my view that wasn't a child of NavigationView it was just disabled on the UI and I couldn't click it, so I guess I cant use that.
The other examples I have seen seem to use presentation links / buttons which seem to show a sort of pop over view.
Im just looking for how to click a regular button and show another a view full screen just like performing a segue used to in the old way of doing things.
Possible solutions
1.if you want to present on top of current view(ex: presentation style in UIKit)
struct ContentView: View {
#State var showingDetail = false
var body: some View {
Button(action: {
self.showingDetail.toggle()
}) {
Text("Show Detail")
}.sheet(isPresented: $showingDetail) {
DetailView()
}
}
}
2.if you want to reset current window scene stack(ex:after login show home screen)
Button(action: goHome) {
HStack(alignment: .center) {
Spacer()
Text("Login").foregroundColor(Color.white).bold()
Spacer()
}
}
func goHome() {
if let window = UIApplication.shared.windows.first {
window.rootViewController = UIHostingController(rootView: HomeScreen())
window.makeKeyAndVisible()
}
}
3.push new view (ex: list->detail, navigation controller of UIKit)
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("Show Detail View")
}.navigationBarTitle("Navigation")
}
}
}
}
4.update the current view based on #state property, (ex:show error message on login failure)
struct ContentView: View {
#State var error = true
var body: some View {
...
... //login email
.. //login password
if error {
Text("Failed to login")
}
}
}
For simple example you can use something like below
import SwiftUI
struct ExampleFlag : View {
#State var flag = true
var body: some View {
ZStack {
if flag {
ExampleView().tapAction {
self.flag.toggle()
}
} else {
OtherExampleView().tapAction {
self.flag.toggle()
}
}
}
}
}
struct ExampleView: View {
var body: some View {
Text("some text")
}
}
struct OtherExampleView: View {
var body: some View {
Text("other text")
}
}
but if you want to present more view this way looks nasty
You can use stack to control view state without NavigationView
For Example:
class NavigationStack: BindableObject {
let didChange = PassthroughSubject<Void, Never>()
var list: [AuthState] = []
public func push(state: AuthState) {
list.append(state)
didChange.send()
}
public func pop() {
list.removeLast()
didChange.send()
}
}
enum AuthState {
case mainScreenState
case userNameScreen
case logginScreen
case emailScreen
case passwordScreen
}
struct NavigationRoot : View {
#EnvironmentObject var state: NavigationStack
#State private var aligment = Alignment.leading
fileprivate func CurrentView() -> some View {
switch state.list.last {
case .mainScreenState:
return AnyView(GalleryState())
case .none:
return AnyView(LoginScreen().environmentObject(state))
default:
return AnyView(AuthenticationView().environmentObject(state))
}
}
var body: some View {
GeometryReader { geometry in
self.CurrentView()
.background(Image("background")
.animation(.fluidSpring())
.edgesIgnoringSafeArea(.all)
.frame(width: geometry.size.width, height: geometry.size.height,
alignment: self.aligment))
.edgesIgnoringSafeArea(.all)
.onAppear {
withAnimation() {
switch self.state.list.last {
case .none:
self.aligment = Alignment.leading
case .passwordScreen:
self.aligment = Alignment.trailing
default:
self.aligment = Alignment.center
}
}
}
}
.background(Color.black)
}
}
struct ExampleOfAddingNewView: View {
#EnvironmentObject var state: NavigationStack
var body: some View {
VStack {
Button(action:{ self.state.push(state: .emailScreen) }){
Text("Tap me")
}
}
}
}
struct ExampleOfRemovingView: View {
#EnvironmentObject var state: NavigationStack
var body: some View {
VStack {
Button(action:{ self.state.pop() }){
Text("Tap me")
}
}
}
}
In my opinion this bad way, but navigation in SwiftUI much worse

Multi Level Navigation SwiftUI

I have a master detail application I'm working on in SwiftUI but once on the 1st DetailView, NavigationLink in the NavBar no longer works. I wrote this as a simple demonstration:
struct NavView: View {
var body: some View {
NavigationView {
NavigationLink(destination: Layer()) {Text("Go to Layer 1")}
}
}
}
struct Layer: View {
var body: some View {
Text("Welcome to Layer 1")
.navigationBarItems(trailing: NavigationLink(destination: AnotherLayer()) { Text("Go to Layer 2") })
}
}
struct AnotherLayer: View {
var body: some View {
Text("Welcome to Layer 2")
}
}
Everything renders and you can tap the navigationBarItem in Layer but nothing happens.
What's going on here? How can I access AnotherLayer?
A NavigationLink used as a navigationBarItem will not work no matter if you use it in the first or second level but a NavigationDestinationLink would solve the problem.
import SwiftUI
struct TestSwift: View {
var body: some View {
NavigationView {
NavigationLink(destination: Layer()) {Text("Go to Level")}
}
}
}
struct Layer: View {
let detailView = NavigationDestinationLink(AnotherLayer())
var body: some View {
VStack {
Text("Text")
}
.navigationBarItems(trailing:
Button(action: {
self.detailView.presented?.value = true
}, label: {
Text("Present Now!")
})
)
}
}
struct AnotherLayer: View {
var body: some View {
Text("Hello")
}
}