I want to do two things. Firstly, I want to navigate to SpaceView if I tap on 'TileCell'.
NavigationLink(destination: SpaceView(space: space)) {
TileCell(
image: image,
text: space.name!,
detailText: nil,
isFaded: space.isComplete
)
}
.buttonStyle(PlainButtonStyle())
}
This works great.
But I also want to long press on TileCell to trigger a different action.
NavigationLink(destination: SpaceView(space: space)) {
TileCell(
image: image,
text: space.name!,
detailText: nil,
isFaded: space.isComplete
)
.onLongPressGesture {
action()
}
}
.buttonStyle(PlainButtonStyle())
}
The long-press gesture works, but I can no longer navigate to SpaceView by tapping.
Any help getting both to work would be appreciated.
I would suggest manually triggering the NavigationLink with isActive Binding in the onTapGesture. Then you can handle on tap and long press.
struct ContentView: View {
#State var outputText: String = ""
#State var isActive : Bool = false
var body: some View {
NavigationView {
NavigationLink(destination: SpaceView(), isActive: $isActive) { //<< here use isActive
TileCell()
.onTapGesture {
isActive = true //<< activate navigation link manually
}
.onLongPressGesture {
print("Long press") //<< long press action here
}
}
}
}
}
Building off davidev's answer, here's a version that works with multiple NavigationLinks created with ForEach
struct ContentView: View {
#State var tileTextArray = ["Tile 1", "Tile 2", "Tile 3"]
#State var isActiveArray = [false, false, false]
var body: some View {
NavigationView {
ForEach(tileTextArray.indices, id: \.self) { index in
NavigationLink(destination: SpaceView(), isActive: $isActiveArray[index]) { //<< here use isActive
TileCell()
.onTapGesture {
isActiveArray[index] = true //<< activate navigation link manually
}
.onLongPressGesture {
print("Long press") //<< long press action here
}
}
}
}
}
}
Related
In the new version of iOS and Xcode
NavigationLink(isActive: , destination: , label: )
is deprecated.
How do we control the status of NavigationLink then?
Old/Deprecated way to navigate:
#State private var readyToNavigate : Bool = false
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: MyTargetView(), isActive: $readyToNavigate, label: {Text("Navigate Link")})
Button {
//Code here before changing the bool value
readyToNavigate = true
} label: {
Text("Navigate Button")
}
}
.navigationTitle("Navigation")
}
}
New way to navigate
#State private var readyToNavigate : Bool = false
var body: some View {
NavigationStack {
VStack {
Button {
//Code here before changing the bool value
readyToNavigate = true
} label: {
Text("Navigate Button")
}
}
.navigationTitle("Navigation")
.navigationDestination(isPresented: $readyToNavigate) {
MyTargetView()
}
}
}
Use new NavigationLink(value:label:) as specified. By putting corresponding value into NavigationPath you activate a link.
See more for example in this topic
Check out Apple's migration docs:
https://developer.apple.com/documentation/swiftui/migrating-to-new-navigation-types
The isActive implementation of NavigationLink is deprecated in iOS 16. Instead, you should use the new NavigationLink(value:) along with .navigationDestination with NavigationStack.
I want to better understand binding data across view, so I made this demo app
First View - if isShowing is true, navigating to SecondView (binding value)
struct ParentView: View {
#State var isShowing = false
#State var value = 5
var body: some View {
NavigationView {
if value != 5 {
ThirdView(isShowing: $isShowing)
} else {
NavigationLink(isActive: $isShowing) {
SecondView(value: $value)
} label: {
Text("Go to second view")
}
}
}
}
}
Second view - updating ParentView value
struct SecondView: View {
#Binding var value: Int
#Environment(\.presentationMode) private var presentationMode
var body: some View {
VStack {
Button {
value = 5
presentationMode.wrappedValue.dismiss()
} label: {
Text("Return 5")
}
Button {
value = 1
presentationMode.wrappedValue.dismiss()
} label: {
Text("Return 1")
}
}
}
}
ThirdView - showing in FirstView in case value is not 5
struct ThirdView: View {
#Binding var isShowing: Bool
var body: some View {
ZStack {
Button {
isShowing.toggle()
} label: {
Text("Its a problem... Go to second view")
}
}
}
}
I tried to toggle isShowing in ThirdView so it can open SecondView to update value again.
But when button is clicked in ThirdView, it doesnt do anything.
The way you have things set up, it won't change. When value != 5, your `NavigationLink does not exist in the view. Instead, you want to trigger it programmatically like this:
struct ParentView: View {
#State var isShowing = false
#State var value = 5
var body: some View {
NavigationView {
VStack {
Text(value.description)
if value != 5 {
ThirdView(isShowing: $isShowing)
} else {
// Change out the NavigationLink for a button that sets isShowing.
Button {
isShowing = true
} label: {
Text("Go to second view")
}
}
}
// By placing it in the background, it is always available to be triggered.
.background(
NavigationLink(isActive: $isShowing) {
SecondView(value: $value)
} label: {
EmptyView()
}
)
}
}
}
Lastly, you don't need to toggle isShowing in ThirdView. You are better off either dismissing the view or setting the value to false. Otherwise, you can get confused what it is doing when you are in your various views.
I want to present the two destinations view in full screen mode from a single view.
Below is a sample of my code. Seem that the function only works for single presentation, if I have a second fullScreenCover defined, the first fullScreenCover didn't work properly.Is that any workaround at this moment?
import SwiftUI
struct TesFullScreen: View {
init(game : Int){
print(game)
}
var body: some View {
Text("Full Screen")
}
}
ContentView
import SwiftUI
struct ContentView: View {
#State var showFullScreen1 : Bool = false
#State var showFullScreen2 : Bool = false
var body: some View {
NavigationView {
VStack {
Spacer()
Button(action: { self.showFullScreen1 = true }) {
Text("Show Full Screen 1")
}
Button(action: { self.showFullScreen2 = true }) {
Text("Show Full Screen 2")
}
Spacer()
}
.navigationBarTitle("TextBugs", displayMode: .inline)
}
.fullScreenCover(isPresented: self.$showFullScreen1){
TesFullScreen(game: 1)
}
.fullScreenCover(isPresented: self.$showFullScreen2){
TesFullScreen(game: 2)
}
}
}
Not always the accepted answer works (for example if you have a ScrollView with subviews (cells in former days) which holds the buttons, that set the navigational flags).
But I found out, that you also can add the fullScreen-modifier onto an EmptyView. This code worked for me:
// IMPORTANT: Has to be within a container (e.g. VStack, HStack, ZStack, ...)
if myNavigation.flag1 || myNavigation.flag2 {
EmptyView().fullScreenCover(isPresented: $myNavigation.flag1)
{ MailComposer() }
EmptyView().fullScreenCover(isPresented: $myNavigation.flag2)
{ RatingStore() }
}
Usually some same modifier added one after another is ignored. So the simplest fix is to attach them to different views, like
struct FullSContentView: View {
#State var showFullScreen1 : Bool = false
#State var showFullScreen2 : Bool = false
var body: some View {
NavigationView {
VStack {
Spacer()
Button(action: { self.showFullScreen1 = true }) {
Text("Show Full Screen 1")
}
.fullScreenCover(isPresented: self.$showFullScreen1){
Text("TesFullScreen(game: 1)")
}
Button(action: { self.showFullScreen2 = true }) {
Text("Show Full Screen 2")
}
.fullScreenCover(isPresented: self.$showFullScreen2){
Text("TesFullScreen(game: 2)")
}
Spacer()
}
.navigationBarTitle("TextBugs", displayMode: .inline)
}
}
}
Alternate is to have one .fullScreenCover(item:... modifier and show inside different views depending on input item.
The only thing that worked for me was the answer in this link:
https://forums.swift.org/t/multiple-sheet-view-modifiers-on-the-same-view/35267
Using the EmptyView method or other solutions always broke a transition animation on one of the two presentations. Either transitioning to or from that view and depending on what order I chose them.
Using the approach by Lantua in the link which is using the item argument instead of isPresented worked in all cases:
enum SheetChoice: Hashable, Identifiable {
case a, b
var id: SheetChoice { self }
}
struct ContentView: View {
#State var sheetState: SheetChoice?
var body: some View {
VStack {
...
}
.sheet(item: $sheetState) { item in
if item == .a {
Text("A")
} else {
Text("B")
}
}
}
}
The sheetState needs to be optional for it to work.
I'm trying to create a button that not only navigates to another view, but also run a function at the same time. I tried embedding both a NavigationLink and a Button into a Stack, but I'm only able to click on the Button.
ZStack {
NavigationLink(destination: TradeView(trade: trade)) {
TradeButton()
}
Button(action: {
print("Hello world!") //this is the only thing that runs
}) {
TradeButton()
}
}
You can use .simultaneousGesture to do that. The NavigationLink will navigate and at the same time perform an action exactly like you want:
NavigationLink(destination: TradeView(trade: trade)) {
Text("Trade View Link")
}.simultaneousGesture(TapGesture().onEnded{
print("Hello world!")
})
You can use NavigationLink(destination:isActive:label:). Use the setter on the binding to know when the link is tapped. I've noticed that the NavigationLink could be tapped outside of the content area, and this approach captures those taps as well.
struct Sidebar: View {
#State var isTapped = false
var body: some View {
NavigationLink(destination: ViewToPresent(),
isActive: Binding<Bool>(get: { isTapped },
set: { isTapped = $0; print("Tapped") }),
label: { Text("Link") })
}
}
struct ViewToPresent: View {
var body: some View {
print("View Presented")
return Text("View Presented")
}
}
The only thing I notice is that setter fires three times, one of which is after it's presented. Here's the output:
Tapped
Tapped
View Presented
Tapped
NavigationLink + isActive + onChange(of:)
// part 1
#State private var isPushed = false
// part 2
NavigationLink(destination: EmptyView(), isActive: $isPushed, label: {
Text("")
})
// part 3
.onChange(of: isPushed) { (newValue) in
if newValue {
// do what you want
}
}
This works for me atm:
#State private var isActive = false
NavigationLink(destination: MyView(), isActive: $isActive) {
Button {
// run your code
// then set
isActive = true
} label: {
Text("My Link")
}
}
Use NavigationLink(_:destination:tag:selection:) initializer and pass your model's property as a selection parameter. Because it is a two-way binding, you can define didset observer for this property, and call your function there.
struct ContentView: View {
#EnvironmentObject var navigationModel: NavigationModel
var body: some View {
NavigationView {
List(0 ..< 10, id: \.self) { row in
NavigationLink(destination: DetailView(id: row),
tag: row,
selection: self.$navigationModel.linkSelection) {
Text("Link \(row)")
}
}
}
}
}
struct DetailView: View {
var id: Int;
var body: some View {
Text("DetailView\(id)")
}
}
class NavigationModel: ObservableObject {
#Published var linkSelection: Int? = nil {
didSet {
if let linkSelection = linkSelection {
// action
print("selected: \(String(describing: linkSelection))")
}
}
}
}
It this example you need to pass in your model to ContentView as an environment object:
ContentView().environmentObject(NavigationModel())
in the SceneDelegate and SwiftUI Previews.
The model conforms to ObservableObject protocol and the property must have a #Published attribute.
(it works within a List)
I also just used:
NavigationLink(destination: View()....) {
Text("Demo")
}.task { do your stuff here }
iOS 15.3 deployment target.
In short, I want to do this, but with SwiftUI.
(Home should be removed)
So far, I have not found a way to access the NavigationBarButton directly, and have managed to find the following that appears to be the only way I can find to date for modifying the button:
struct MyList: View {
var body: some View {
Text("MyList")
.navigationBarTitle(Text(verbatim: "MyList"), displayMode: .inline)
.navigationBarItems(leading: Text("<"))
}
}
However, I lose the default return image and get an ugly < instead.
You need to set the title of the view that the back button will pop to:
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView()) {
Text("push view")
}
}.navigationBarTitle("", displayMode: .inline)
}
}
}
struct DetailView: View {
var body: some View {
Text("Detail View")
}
}
Alternatively, to conditionally set or unset the title of the source view, depending on the presentation status you can use the code below.
Beware that the isActive parameter has a bug, but that will most likely be solved soon. Here's a reference to the bug mentioned SwiftUI: NavigationDestinationLink deprecated
struct ContentView: View {
#State private var active: Bool = false
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView(), isActive: $active) {
Text("push view")
}
}.navigationBarTitle(!active ? "View Title" : "", displayMode: .inline)
}
}
}
struct DetailView: View {
var body: some View {
Text("Detail View")
}
}
Works on iOS 16
Since you can update NavigationItem inside the init of the View. You can solve this in 2 steps:
Get visible View Controller.
// Get Visible ViewController
extension UIApplication {
static var visibleVC: UIViewController? {
var currentVC = UIApplication.shared.windows.first { $0.isKeyWindow }?.rootViewController
while let presentedVC = currentVC?.presentedViewController {
if let navVC = (presentedVC as? UINavigationController)?.viewControllers.last {
currentVC = navVC
} else if let tabVC = (presentedVC as? UITabBarController)?.selectedViewController {
currentVC = tabVC
} else {
currentVC = presentedVC
}
}
return currentVC
}
}
Update NavigationItem inside init of the View.
struct YourView: View {
init(hideBackLabel: Bool = true) {
if hideBackLabel {
// iOS 14+
UIApplication.visibleVC?.navigationItem.backButtonDisplayMode = .minimal
// iOS 13-
let button = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
UIApplication.visibleVC?.navigationItem.backBarButtonItem = button
}
}
}