I have custom tab bar with two screen and I created a modifier to display custom sheet from bottom.
I don't want to use .sheet cause I can't change cornerRadius and also I want my sheet to be self-sized (dynamique height).
MainView:
struct MainView: View {
var body: some View {
ZStack(alignment: .bottom) {
TabView(selection: $currentTab) {
Screen1()
.tag(Tab.screen1)
Screen2()
.tag(Tab.screen2)
}
.tabViewStyle(DefaultTabViewStyle())
HStack {
TabItemView(currentTab: $currentTab, tab: .screen1)
TabItemView(currentTab: $currentTab, tab: .screen2)
}
}
}
struct TabItemView: View {
#Binding var currentTab: Tab
var tab: Tab
var body: some View {
Button {
currentTab = tab
} label: {
VStack {
Image(tab.image)
.resizable()
.frame(width: 30, height: 30)
.frame(maxWidth: .infinity)
Text(tab.rawValue)
}.padding(.vertical, 10)
}
}
}
Custom BottomSheet :
struct BottomSheet<Content: View>: View {
#Binding var isPresented: Bool
let onDismiss: (() -> Void)?
let content: Content
init(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, content: #escaping () -> Content) {
self._isPresented = isPresented
self.onDismiss = onDismiss
self.content = content()
}
var isiPad: Bool {
UIDevice.current.userInterfaceIdiom == .pad
}
public var body: some View {
ZStack {
if isPresented {
Color.black.opacity(0.3)
.edgesIgnoringSafeArea(.all)
.transition(.opacity)
.zIndex(1)
.onTapGesture {
dismiss()
}
container
.ignoresSafeArea(.container, edges: .bottom)
.transition(isiPad ? AnyTransition.opacity.combined(with: .offset(x: 0, y: 200)) : .move(edge: .bottom))
.zIndex(2)
}
}.animation(.spring(response: 0.35, dampingFraction: 1), value: isPresented)
}
private var container: some View {
VStack {
Spacer()
if isiPad {
card.aspectRatio(1.0, contentMode: .fit)
Spacer()
} else {
card
}
}
}
private var card: some View {
VStack(alignment: .trailing, spacing: 0) {
content
}
.background(Color.background)
.clipShape(RoundedRectangle(cornerRadius: .radius_xl))
}
func dismiss() {
withAnimation {
isPresented = false
}
onDismiss?()
}
}
extension View {
func bottomSheet<Content: View>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, backgroundColor: Color = Color(.systemGray6), #ViewBuilder content: #escaping () -> Content) -> some View {
ZStack {
self
BottomSheet(isPresented: isPresented, onDismiss: onDismiss) { content() }
}
}
}
Screen 1:
struct Screen1: View {
...
#State var isPresented = false
...
var body: some View {
Button(action: {
isPresented = true
}, label: {
HStack {
Spacer()
Image(systemName: "plus")
Text("Add")
Spacer()
}
})
.bottomSheet(isPresented: $isPresented) {
PopupView(isPresented: $isPresented, message: "") {
//Action when user confirm
}
}
}
}
The problem is bottom sheet is presented behind my tab bar, when I move my .bottomSheet modifier to MainView it's working perfectly but in my case I want to use .bottomSheet in child views cause I have actions to do when user tap confirm or cancel button.
PS: I want to mimic .sheet behavior, always presenting view on top of root
Related
I am using my own ScrollView which has scroll start and end callbacks, so that I can perform some actions based on them, like hiding/showing a banner.
Here's my code for ScrollView
struct TrackableScrollView<Content>: View where Content: View {
private let onScrollingStarted: () -> Void
private let onScrollingFinished: () -> Void
#State var scrollViewHelper = ScrollViewHelper()
let content: Content
public init(#ViewBuilder content: () -> Content,
onScrollingStarted: #escaping () -> Void = {},
onScrollingFinished: #escaping () -> Void = {}) {
self.content = content()
self.onScrollingStarted = onScrollingStarted
self.onScrollingFinished = onScrollingFinished
}
public var body: some View {
GeometryReader { outsideProxy in
ScrollView(.vertical, showsIndicators: true) {
ZStack(alignment: .top) {
GeometryReader { insideProxy in
Color.clear
.preference(key: ScrollOffsetPreferenceKey.self, value: [self.calculateContentOffset(fromOutsideProxy: outsideProxy, insideProxy: insideProxy)])
}
self.content
}
}
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
scrollViewHelper.currentOffset = value[0]
}
.simultaneousGesture(
DragGesture().onChanged { _ in
onScrollingStarted()
}
)
.onReceive(scrollViewHelper.$offsetAtScrollEnd) { _ in
onScrollingFinished()
}
}
}
private func calculateContentOffset(fromOutsideProxy outsideProxy: GeometryProxy, insideProxy: GeometryProxy) -> CGFloat {
return outsideProxy.frame(in: .global).minY - insideProxy.frame(in: .global).minY
}
}
private struct ScrollOffsetPreferenceKey: PreferenceKey {
typealias Value = [CGFloat]
static var defaultValue: [CGFloat] = [0]
static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) {
value.append(contentsOf: nextValue())
}
}
class ScrollViewHelper: ObservableObject {
#Published var currentOffset: CGFloat = 0
#Published var offsetAtScrollEnd: CGFloat = 0
private var cancellable: AnyCancellable?
init() {
cancellable = AnyCancellable($currentOffset
.debounce(for: 0.3, scheduler: DispatchQueue.main)
.dropFirst()
.assign(to: \.offsetAtScrollEnd, on: self))
}
}
And here's my code to show some content
struct ContentView: View {
#State var messageBannerVisisbility: Bool = false
var body: some View {
VStack {
TrackableScrollView {
VStack(alignment: .center, spacing: 0) {
ForEach(0...100, id: \.self) { i in
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.green)
.overlay(Text("\(i)"))
.padding()
}
}
} onScrollingStarted: {
hideMessageBanner()
} onScrollingFinished: {
showMessageBanner()
}
if messageBannerVisisbility {
Rectangle()
.frame(height: 100)
.foregroundColor(.red)
.overlay(Text("Random bottom view"))
.transition(.move(edge: .bottom).combined(with: .opacity))
}
}
.navigationBarHidden(true)
.onAppear {
showMessageBanner()
}
}
}
I am toggling messageBannerVisisbility with animation inside showMessageBanner() function.
If I keep below code, scrolling is not smooth. Could it be because I am showing/hiding the banner with animation on scroll callbacks? I guess I can just update the banner, instead of the whole View, but I am not sure how can I achieve that!
if messageBannerVisisbility {
Rectangle()
.frame(height: 100)
.foregroundColor(.red)
.overlay(Text("Random bottom view"))
.transition(.move(edge: .bottom).combined(with: .opacity))
}
What could I do to improve scrolling experience? My app does support iOS 13, but I am also fine with implementing 2 different solutions, one for iOS 13 and other one for iOS 14 and above, if that makes life a little easier!
I have sub-navigation inside Listview and trying to achieve leftSlide and rightSlide animation but it always shows default slide animation. I even tried to add .transition(.move(edge: . leading)) & .transition(.move(edge: . trailing))
import SwiftUI
import Combine
struct GameTabView: View {
#State var selectedTab: Int = 0
init() {
UITableView.appearance().sectionHeaderTopPadding = 0
}
var body: some View {
listView
.ignoresSafeArea()
}
var listView: some View {
List {
Group {
Color.gray.frame(height: 400)
sectionView
}
.listRowInsets(EdgeInsets())
}
.listStyle(.plain)
}
var sectionView: some View {
Section {
tabContentView
.transition(.move(edge: . leading)) // NOT WORKING
.background(Color.blue)
} header: {
headerView
}
}
private var headerView: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
Button {
withAnimation {
selectedTab = 0
}
} label: {
Text("AAAA")
.padding()
}
Button {
withAnimation {
selectedTab = 1
}
} label: {
Text("BBBB")
.padding()
}
Button {
withAnimation {
selectedTab = 2
}
} label: {
Text("BBBB")
.padding()
}
}
}
}
.background(Color.green)
}
#ViewBuilder private var tabContentView: some View {
switch selectedTab {
case 0:
DummyScreen(title: "FIRST", color: .red)
case 1:
DummyScreen(title: "SECOND", color: .green)
case 2:
DummyScreen(title: "THIRD", color: .blue)
default:
EmptyView()
}
}
}
struct DummyScreen: View {
let title: String
let color: Color
var body: some View {
VStack {
ForEach(0..<15, id: \.self) { index in
HStack {
Text("#\(index): title \(title)")
.foregroundColor(Color.black)
.font(.system(size: 30))
.padding(.vertical, 20)
Spacer()
}
.background(Color.yellow)
}
}
.background(color)
}
}
Just like Asperi said, you can change to another type of View to make the transition works.
I tried your code, changed List to VStack with ScrollView inside to wrap around the tabContentView, and the result of the UI showed the same except with a proper animation now, and you don't have to manually adjust the height of your contents since HStack height is dynamic based on your Text() growth.
Edited: header fixed, animation fixed
import SwiftUI
import SwiftUITrackableScrollView //Added
import Combine
struct GameTabView: View {
#State private var scrollViewContentOffset = CGFloat(0) //Added
#State var selectedTab: Int = 0
init() {
UITableView.appearance().sectionHeaderTopPadding = 0
}
var body: some View {
listView
.ignoresSafeArea()
}
var listView: some View {
ZStack { //Added
TrackableScrollView(.vertical, showIndicators: true, contentOffset: $scrollViewContentOffset) {
VStack {
Color.gray.frame(height: 400)
sectionView
}
}
if(scrollViewContentOffset > 400) {
VStack {
headerView
Spacer()
}
}
}
}
var sectionView: some View {
Section {
tabContentView
.transition(.scale) // FIXED
.background(Color.blue)
} header: {
headerView
}
}
private var headerView: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
Button {
withAnimation {
selectedTab = 0
}
} label: {
Text("AAAA")
.padding()
}
Button {
withAnimation {
selectedTab = 1
}
} label: {
Text("BBBB")
.padding()
}
Button {
withAnimation {
selectedTab = 2
}
} label: {
Text("BBBB")
.padding()
}
}
}
}
.background(Color.green)
}
#ViewBuilder private var tabContentView: some View {
switch selectedTab {
case 0:
DummyScreen(title: "FIRST", color: .red)
case 1:
DummyScreen(title: "SECOND", color: .green)
case 2:
DummyScreen(title: "THIRD", color: .blue)
default:
EmptyView()
}
}
}
struct DummyScreen: View {
let title: String
let color: Color
var body: some View {
VStack {
ForEach(0..<15, id: \.self) { index in
HStack {
Text("#\(index): title \(title)")
.foregroundColor(Color.black)
.font(.system(size: 30))
.padding(.vertical, 20)
Spacer()
}
.background(Color.yellow)
}
}
.background(color)
}
}
Thanks to #tail and #Asperi
I finally got the solution via updating ScrollView with ScrollviewProxy:
import SwiftUI
import Combine
struct GameTabView: View {
#State var selectedTab: Int = 0
#State var proxy: ScrollViewProxy?
init() {
UITableView.appearance().sectionHeaderTopPadding = 0
}
var body: some View {
listView
.ignoresSafeArea()
}
var listView: some View {
List {
Group {
Color.gray.frame(height: 400)
sectionView
}
.listRowInsets(EdgeInsets())
}
.listStyle(.plain)
}
var sectionView: some View {
Section {
tabContentView
} header: {
headerView
}
}
private var headerView: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
Button {
withAnimation {
selectedTab = 0
self.proxy?.scrollTo(0, anchor: .center)
}
} label: {
Text("AAAA")
.padding()
}
Button {
withAnimation {
selectedTab = 1
self.proxy?.scrollTo(1, anchor: .center)
}
} label: {
Text("BBBB")
.padding()
}
Button {
withAnimation {
selectedTab = 2
self.proxy?.scrollTo(2, anchor: .center)
}
} label: {
Text("BBBB")
.padding()
}
}
}
}
.background(Color.green)
}
#ViewBuilder private var tabContentView: some View {
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
ForEach(0..<3, id: \.self) { i in
DummyScreen(title: "Name: \(i)", color: .blue)
.frame(idealWidth: UIScreen.main.bounds.width)
.id(i)
}
}
}
.onAppear {
self.proxy = proxy
}
}
}
}
I have created a bottom sheet as shown in the code below.
Basically, there are two Views, and the animation in one View works,
but the animation in the other is not enabled, and it switches instantly.
(See the comments in the code for where this is happening.)
How can I get the animations to work?
Code:
import SwiftUI
struct ContentView: View {
#State private var isShow = false
var body: some View {
ZStack {
Button(
"Show Sheet",
action: {
self.isShow.toggle()
}
)
.zIndex(0)
BottomSheet(
isShow: self.$isShow,
content: {
VStack {
Text("A")
Text("B")
Text("C")
}
.frame(
maxWidth: .infinity
)
.background(Color(.yellow))
}
)
.zIndex(1)
}
}
}
struct ScrimView: View {
var body: some View {
VStack {}.frame(
maxWidth: .infinity,
maxHeight: .infinity,
alignment: .bottom
)
.background(
Color(.gray)
)
}
}
struct BottomSheet<Content: View>: View {
private let content: () -> Content
#Binding var isShow: Bool
init(
isShow: Binding<Bool>,
content: #escaping () -> Content
) {
self._isShow = isShow
self.content = content
}
var body: some View {
ZStack(alignment: .bottom) {
if self.isShow {
ScrimView().zIndex(
0
)
.transition(.opacity) // <-- not work
.animation(.linear(duration: 5)) // <-- not work
VStack {
Button(
"X",
action: {
self.isShow = false
}
)
self.content()
}
.zIndex(1)
.background(Color(.white))
.cornerRadius(10)
.transition(.move(edge: .bottom)) // <-- work
.animation(.linear(duration: 5)) // <-- work
}
}
}
}
Add the animation out of the condition and set opacity.
var body: some View {
ZStack(alignment: .bottom) {
ScrimView().zIndex( // <-- here work
0
)
.opacity(isShow ? 1 : 0)
.animation(.linear(duration: 5))
Another solution is to set the animation to direct main ZStack
var body: some View {
ZStack(alignment: .bottom) {
if self.isShow {
ScrimView().zIndex(0)
VStack {
Button(
"X",
action: {
self.isShow = false
}
)
self.content()
}
.zIndex(1)
.background(Color(.white))
.cornerRadius(10)
.transition(.move(edge: .bottom))
}
}.animation(.linear(duration: 5))
}
Reason: It does not work because it's direct inside the condition. when you change the toggle it direct reflects with 1 opacity so no effect will shown. your second animation is work because of transition. With your code you can change the toggle inside the animation then it will work or by using the second approach it will also work.
firstly I am really new to iOS development and Swift (2 weeks coming here from PHP :))
I am trying to build a iOS application that has a side menu. And my intention is when I click on a item in the menu the new view will appear on screen like the 'HomeViewController' and for each consequent item like example1, 2 etc (In place of the menu button, Note I will be adding the top nav bar soon to open the menu)
I am wondering how I can accomplish this feature?
Thanks
ContentView.swift
import SwiftUI
struct MenuItem: Identifiable {
var id = UUID()
let text: String
}
func controllView(clickedview:String) {
print(clickedview)
}
struct MenuContent: View{
let items: [MenuItem] = [
MenuItem(text: "Home"),
MenuItem(text: "Example1"),
MenuItem(text: "Example2"),
MenuItem(text: "Example3")
]
var body: some View {
ZStack {
Color(UIColor(red: 33/255.0, green: 33/255.0, blue: 33/255.0, alpha: 1))
VStack(alignment: .leading, spacing: 0) {
ForEach(items) {items in
HStack {
Text(items.text)
.bold()
.font(.system(size: 20))
.multilineTextAlignment(/*#START_MENU_TOKEN#*/.leading/*#END_MENU_TOKEN#*/)
.foregroundColor(Color.white)
Spacer()
}
.onTapGesture {
controllView(clickedview: items.text)
}
.padding()
Divider()
}
Spacer()
}
.padding(.top, 40)
}
}
}
struct SideMenu: View {
let width: CGFloat
let menuOpen: Bool
let toggleMenu: () -> Void
var body: some View {
ZStack {
//Dimmed backgroud
GeometryReader { _ in
EmptyView()
}
.background(Color.gray.opacity(0.15))
.opacity(self.menuOpen ? 1 : 0)
.animation(Animation.easeIn.delay(0.25))
.onTapGesture {
self.toggleMenu()
}
//Menucontent
HStack {
MenuContent()
.frame(width: width)
.offset(x: menuOpen ? 0 : -width)
.animation(.default)
Spacer()
}
}
}
}
struct ContentView: View {
#State var menuOpen = false
var body: some View {
let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
if menuOpen {
withAnimation {
print("Left")
menuOpen.toggle()
}
}
}
if $0.translation.width > -100 {
if !menuOpen {
withAnimation {
print("Right")
menuOpen.toggle()
}
}
}
}
ZStack {
if !menuOpen {
Button(action: {
self.menuOpen.toggle()
}, label: {
Text("Open Menu")
.bold()
.foregroundColor(Color.white)
.frame(width: 200, height: 50, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.background(Color(.systemBlue))
})
}
SideMenu(width: UIScreen.main.bounds.width/1.6, menuOpen: menuOpen, toggleMenu: toggleMenu)
}
.edgesIgnoringSafeArea(.all)
.gesture(drag)
}
func toggleMenu(){
menuOpen.toggle()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The on tap command from the above code:
.onTapGesture {
controllView(clickedview: items.text)
}
HomeViewController.swift
import UIKit
import WebKit
class HomeViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView!
override func loadView() {
webView = WKWebView()
webView.navigationDelegate = self
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "https://developer.apple.com")!
webView.load(URLRequest(url: url))
}
}
You'll need a couple of ingredients:
A way to store the state of the currently active view
A way to communicate the state between your menu and main content view
For the first one, I made an enum that listed the different types of views (ViewType) and added it to your MenuItem model.
For the second, you can pass state via a #Binding from parent to child views and back up the chain.
struct MenuItem: Identifiable {
var id = UUID()
let text: String
let viewType : ViewType
}
enum ViewType {
case home, example1, example2, example3
}
struct MenuContent: View{
#Binding var activeView : ViewType
let items: [MenuItem] = [
MenuItem(text: "Home", viewType: .home),
MenuItem(text: "Example1", viewType: .example1),
MenuItem(text: "Example2", viewType: .example2),
MenuItem(text: "Example3", viewType: .example3)
]
var body: some View {
ZStack {
Color(UIColor(red: 33/255.0, green: 33/255.0, blue: 33/255.0, alpha: 1))
VStack(alignment: .leading, spacing: 0) {
ForEach(items) { item in
HStack {
Text(item.text)
.bold()
.font(.system(size: 20))
.multilineTextAlignment(.leading)
.foregroundColor(Color.white)
Spacer()
}
.onTapGesture {
activeView = item.viewType
}
.padding()
Divider()
}
Spacer()
}
.padding(.top, 40)
}
}
}
struct SideMenu: View {
let width: CGFloat
let menuOpen: Bool
let toggleMenu: () -> Void
#Binding var activeView : ViewType
var body: some View {
ZStack {
//Dimmed backgroud
GeometryReader { _ in
EmptyView()
}
.background(Color.gray.opacity(0.15))
.opacity(self.menuOpen ? 1 : 0)
.animation(Animation.easeIn.delay(0.25))
.onTapGesture {
self.toggleMenu()
}
//Menucontent
HStack {
MenuContent(activeView: $activeView)
.frame(width: width)
.offset(x: menuOpen ? 0 : -width)
.animation(.default)
Spacer()
}
}
}
}
struct ContentView: View {
#State private var menuOpen = false
#State private var activeView : ViewType = .home
var body: some View {
let drag = DragGesture()
.onEnded {
if $0.translation.width < -100 {
if menuOpen {
withAnimation {
print("Left")
menuOpen.toggle()
}
}
}
if $0.translation.width > -100 {
if !menuOpen {
withAnimation {
print("Right")
menuOpen.toggle()
}
}
}
}
ZStack {
VStack {
if !menuOpen {
Button(action: {
self.menuOpen.toggle()
}, label: {
Text("Open Menu")
.bold()
.foregroundColor(Color.white)
.frame(width: 200, height: 50, alignment: .center)
.background(Color(.systemBlue))
})
}
switch activeView {
case .home:
HomeViewControllerRepresented()
case .example1:
Text("Example1")
case .example2:
Text("Example2")
case .example3:
Text("Example3")
}
}
SideMenu(width: UIScreen.main.bounds.width/1.6,
menuOpen: menuOpen,
toggleMenu: toggleMenu,
activeView: $activeView)
.edgesIgnoringSafeArea(.all)
}
.gesture(drag)
}
func toggleMenu(){
menuOpen.toggle()
}
}
struct HomeViewControllerRepresented : UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> HomeViewController {
HomeViewController()
}
func updateUIViewController(_ uiViewController: HomeViewController, context: Context) {
}
}
I am trying to push from login view to detail view but not able to make it.even navigation bar is not showing in login view. How to push on button click in SwiftUI? How to use NavigationLink on button click?
var body: some View {
NavigationView {
VStack(alignment: .leading) {
Text("Let's get you signed in.")
.bold()
.font(.system(size: 40))
.multilineTextAlignment(.leading)
.frame(width: 300, height: 100, alignment: .topLeading)
.padding(Edge.Set.bottom, 50)
Text("Email address:")
.font(.headline)
TextField("Email", text: $email)
.frame(height:44)
.accentColor(Color.white)
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
Text("Password:")
.font(.headline)
SecureField("Password", text: $password)
.frame(height:44)
.accentColor(Color.white)
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
Button(action: {
print("login tapped")
}) {
HStack {
Spacer()
Text("Login").foregroundColor(Color.white).bold()
Spacer()
}
}
.accentColor(Color.black)
.padding()
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
.padding(Edge.Set.vertical, 20)
}
.padding(.horizontal,30)
}
.navigationBarTitle(Text("Login"))
}
To fix your issue you need to bind and manage tag with NavigationLink, So create one state inside you view as follow, just add above body.
#State var selection: Int? = nil
Then update your button code as follow to add NavigationLink
NavigationLink(destination: Text("Test"), tag: 1, selection: $selection) {
Button(action: {
print("login tapped")
self.selection = 1
}) {
HStack {
Spacer()
Text("Login").foregroundColor(Color.white).bold()
Spacer()
}
}
.accentColor(Color.black)
.padding()
.background(Color(UIColor.darkGray))
.cornerRadius(4.0)
.padding(Edge.Set.vertical, 20)
}
Meaning is, when selection and NavigationLink tag value will match then navigation will be occurs.
I hope this will help you.
iOS 16+
Note: Below is a simplified example of how to present a new view. For a more advanced generic example please see this answer.
In iOS 16 we can access the NavigationStack and NavigationPath.
Usage #1
A new view is activated by a simple NavigationLink:
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink(value: "NewView") {
Text("Show NewView")
}
.navigationDestination(for: String.self) { view in
if view == "NewView" {
Text("This is NewView")
}
}
}
}
}
Usage #2
A new view is activated by a standard Button:
struct ContentView: View {
#State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
Button {
path.append("NewView")
} label: {
Text("Show NewView")
}
.navigationDestination(for: String.self) { view in
if view == "NewView" {
Text("This is NewView")
}
}
}
}
}
Usage #3
A new view is activated programmatically:
struct ContentView: View {
#State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
Text("Content View")
.navigationDestination(for: String.self) { view in
if view == "NewView" {
Text("This is NewView")
}
}
}
.onAppear {
path.append("NewView")
}
}
}
iOS 13+
The accepted answer uses NavigationLink(destination:tag:selection:) which is correct.
However, for a simple view with just one NavigationLink you can use a simpler variant: NavigationLink(destination:isActive:)
Usage #1
NavigationLink is activated by a standard Button:
struct ContentView: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
...
NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
Button(action: {
self.isLinkActive = true
}) {
Text("Login")
}
}
}
.navigationBarTitle(Text("Login"))
}
}
}
Usage #2
NavigationLink is hidden and activated by a standard Button:
struct ContentView: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
...
Button(action: {
self.isLinkActive = true
}) {
Text("Login")
}
}
.navigationBarTitle(Text("Login"))
.background(
NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
EmptyView()
}
.hidden()
)
}
}
}
Usage #3
NavigationLink is hidden and activated programmatically:
struct ContentView: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
VStack(alignment: .leading) {
...
}
.navigationBarTitle(Text("Login"))
.background(
NavigationLink(destination: Text("OtherView"), isActive: $isLinkActive) {
EmptyView()
}
.hidden()
)
}
.onAppear {
self.isLinkActive = true
}
}
}
Here is a GitHub repository with different SwiftUI extensions that makes navigation easier.
Another approach:
SceneDelegate
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: BaseView().environmentObject(ViewRouter()))
self.window = window
window.makeKeyAndVisible()
}
BaseView
import SwiftUI
struct BaseView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
if viewRouter.currentPage == "view1" {
FirstView()
} else if viewRouter.currentPage == "view2" {
SecondView()
.transition(.scale)
}
}
}
}
#if DEBUG
struct MotherView_Previews : PreviewProvider {
static var previews: some View {
BaseView().environmentObject(ViewRouter())
}
}
#endif
ViewRouter
import Foundation
import Combine
import SwiftUI
class ViewRouter: ObservableObject {
let objectWillChange = PassthroughSubject<ViewRouter,Never>()
var currentPage: String = "view1" {
didSet {
withAnimation() {
objectWillChange.send(self)
}
}
}
}
FirstView
import SwiftUI
struct FirstView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
Button(action: {self.viewRouter.currentPage = "view2"}) {
NextButtonContent()
}
}
}
}
#if DEBUG
struct FirstView_Previews : PreviewProvider {
static var previews: some View {
FirstView().environmentObject(ViewRouter())
}
}
#endif
struct NextButtonContent : View {
var body: some View {
return Text("Next")
.foregroundColor(.white)
.frame(width: 200, height: 50)
.background(Color.blue)
.cornerRadius(15)
.padding(.top, 50)
}
}
SecondView
import SwiftUI
struct SecondView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
Spacer(minLength: 50.0)
Button(action: {self.viewRouter.currentPage = "view1"}) {
BackButtonContent()
}
}
}
}
#if DEBUG
struct SecondView_Previews : PreviewProvider {
static var previews: some View {
SecondView().environmentObject(ViewRouter())
}
}
#endif
struct BackButtonContent : View {
var body: some View {
return Text("Back")
.foregroundColor(.white)
.frame(width: 200, height: 50)
.background(Color.blue)
.cornerRadius(15)
.padding(.top, 50)
}
}
Hope this helps!
Simplest and most effective solution is :
NavigationLink(destination:ScoresTableView()) {
Text("Scores")
}.navigationBarHidden(true)
.frame(width: 90, height: 45, alignment: .center)
.foregroundColor(.white)
.background(LinearGradient(gradient: Gradient(colors: [Color.red, Color.blue]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(10)
.contentShape(Rectangle())
.padding(EdgeInsets(top: 16, leading: UIScreen.main.bounds.size.width - 110 , bottom: 16, trailing: 20))
ScoresTableView is the destination view.
In my opinion a cleaner way for iOS 16+ is using a state bool to present the view.
struct ButtonNavigationView: View {
#State private var isShowingSecondView : Bool = false
var body: some View {
NavigationStack {
VStack{
Button(action:{isShowingSecondView = true} ){
Text("Show second view")
}
}.navigationDestination(isPresented: $isShowingSecondView) {
Text("SecondView")
}
}
}
}
I think above answers are nice, but simpler way should be:
NavigationLink {
TargetView()
} label: {
Text("Click to go")
}