How can I tap the Toggle under a View in SwiftUI? - ios

There are two views (Toggle and Button) under a view (Rectangle):
struct ContentView: View {
#State var val = false
var body: some View {
ZStack {
VStack {
Text("Control penetration")
.font(.title)
Toggle(isOn: $val){EmptyView()}
Button(action: {
print("Yes! Clicked!")
}){
Text("Can you click me???")
}
}
.padding()
Rectangle()
.fill(Color.green.opacity(0.2))
.allowsHitTesting(false)
}
}
}
I use allowsHitTesting() to make the click penetrate to the bottom.
But only the Button can respond to click, the Toggle can not!
What's wrong with it? How can I make the Toggle respond click too? thanks a lot!

Here is possible workaround - give it explicit tap gesture handler. Tested with Xcode 12 / iOS 14
Toggle(isOn: $val){EmptyView()}
.onTapGesture {
withAnimation { val.toggle() }
}

Related

iOS button and navigationLink next to each other in List

I have a List, each element has its own HStack that contains Button and NavigationLink to the next view but both (checkbox button and navigation link) is activated wherever I click on single HStack element.
That means icon on the button changes when I click on the element but application also loads the next view. The same happens when I want to go to the next view by simply clicking on the NavigationLink. Can you help me separate this two functionalities (checkbox Button and NavigationLink)?
struct ContentView: View {
#ObservedObject var spendingList: SpendingList
var body: some View {
NavigationView {
List {
ForEach($spendingList.spendings) { $spending in
HStack{
Button(action: {
spending.Bought = !spending.Bought
}, label: {
if spending.Bought == false {
Image(systemName: "square")
.foregroundColor(.accentColor)
} else {
Image(systemName: "checkmark.square")
.foregroundColor(.accentColor)
}
})
NavigationLink(destination: DetailView(spending: $spending)
.navigationTitle(Text(spending.Name)),
label: {
Text(spending.Name).frame(maxWidth: .infinity, alignment: .leading)
if spending.Price != 0 {
Text(String(spending.Price)).frame(maxWidth: .infinity, alignment: .trailing)
} else {
Text("empty").foregroundColor(.gray)
}
})
}
}
.navigationTitle(Text("Spending Priority"))
}
}
}
}
The default Style of Button & NavigationLink makes the whole row click as one. However, using PlainButtonStyle() fixes the issue by making the button clickable & not the cell:
.buttonStyle(.plain)//to your button & NavigationLink
Unfortunately, the parent-view list item becomes the Navigation link. So the button press will never be recorded.
In iOS 16 you can solve this by replacing the NavigationLink with a Button and pushing an item onto the navigation stack with the Button.
Documentation: https://developer.apple.com/documentation/swiftui/migrating-to-new-navigation-types
Something like this, for example:
struct ContentView: View {
#State var path: [View] = []
var body: some View {
NavigationStack(path: $path) {
List {
ForEach($spendingList.spendings) { $spending in
HStack {
Button(action: {
spending.Bought = !spending.Bought
}, label: {
Image(...)
})
Button(action: {
path.append(DetailView(spending: $spending))
}, label: {
Text(...)
})
Text(spending.Name)
})
}
}
}
.navigationTitle(Text("Spending Priority"))
.navigationDestination(for: View.self) { view in
view
}
}
}
}

SwiftUI disappear back button with navigationLink

I have 3 views. One of these have NavigationView second have NavigationLink and last just a child with toolbar.
So my problem when I added toolbar in last view backButton elegant disappear. How can I solve this?
Screen recording of my problem
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Text("Hello, world!")
.padding()
NavigationLink(destination: ListView()) {
Image(systemName: "trash")
.font(.largeTitle)
.foregroundColor(.red)
}
}.navigationBarHidden(true)
.navigationTitle("Image")
}
}
}
import SwiftUI
struct ListView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
List {
NavigationLink(destination: DetailView()) {
Text("Detail")
}
}
}.navigationBarTitle(Text("Data"), displayMode: .large)
.toolbar {
Button("Save") {
presentationMode.wrappedValue.dismiss()
}
}
}
}
import SwiftUI
struct DetailView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
Text("DetailView")
.padding()
}.navigationBarTitle(Text("Data"), displayMode: .large)
.toolbar {
Button("Save") {
presentationMode.wrappedValue.dismiss()
}
}
}
}
In the console, you'll notice this message:
2021-04-27 12:37:36.862733-0700 MyApp[12739:255441] [Assert] displayModeButtonItem is internally managed and not exposed for DoubleColumn style. Returning an empty, disconnected UIBarButtonItem to fulfill the non-null contract.
The default style for NavigationView is usually DefaultNavigationViewStyle, which is really just DoubleColumnNavigationViewStyle. Use StackNavigationViewStyle instead, and it works as expected.
Edit: You are right that StackNavigationViewStyle will break iPad split view. But thankfully, DoubleColumnNavigationViewStyle works fine in iPad and doesn't hide the back button. We can then just use a different NavigationStyle depending on the device, as shown in this answer.
struct ResponsiveNavigationStyle: ViewModifier {
#Environment(\.horizontalSizeClass) var horizontalSizeClass
#ViewBuilder
func body(content: Content) -> some View {
if horizontalSizeClass == .compact { /// iPhone
content.navigationViewStyle(StackNavigationViewStyle())
} else { /// iPad or larger iPhone in landscape
content.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Text("Hello, world!")
.padding()
NavigationLink(destination: ListView()) {
Image(systemName: "trash")
.font(.largeTitle)
.foregroundColor(.red)
}
}
.navigationBarHidden(true)
.navigationTitle("Image")
}
.modifier(ResponsiveNavigationStyle()) /// here!
}
}
Result:
iPad
iPhone
I don't know why, but it's what worked for me:
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button { } label: { } // button to the right
}
ToolbarItem(placement: .navigationBarLeading) {
Text("") // empty text in left to prevent back button to disappear
}
}
I already tried to replace the empty text with EmptyView() but the button keeps disappearing.
FYI: I have this problem only on my device with iOS 14, but in another device with iOS 15 the back button never disappears.

SwiftUI: horizontal ScrollView inside NavigationLink breaks navigation

I want to use a simple horizontal ScrollView as NavigationLink inside a List. However, tapping on the ScrollView is not registered by a NavigationLink and therefore does not navigate to the destination.
NavigationView {
List {
NavigationLink(destination: Text("Detail")) {
ScrollView(.horizontal) {
Text("Tapping here does not navigate.")
}
}
}
}
Any ideas on how can we prevent ScrollView from capturing the navigation tap gesture?
You can move NavigationLink to the background and activate it in onTapGesture:
struct ContentView: View {
#State var isLinkActive = false
var body: some View {
NavigationView {
List {
ScrollView(.horizontal) {
Text("Tapping here does not navigate.")
}
.onTapGesture {
isLinkActive = true
}
}
.background(
NavigationLink(destination: Text("Detail"), isActive: $isLinkActive) {}
)
}
}
}
The final goal is not clear, but the following alternate does also work (tested with Xcode 12 / iOS 14)
NavigationView {
List {
ScrollView(.horizontal) {
NavigationLink(destination: Text("Detail")) {
Text("Tapping here does not navigate.")
}
}
}
}

How to replace the current view in SwiftUI?

I am developing an app with SwiftUI.
I have a NavigationView and I have buttons on the navigation bar. I want to replace the current view (which is a result of a TabView selection) with another one.
Basically, when the user clicks "Edit" button, I want to replace the view with another view to make the edition and when the user is done, the previous view is restored by clicking on a "Done" button.
I could just use a variable to dynamically choose which view is displayed on the current tab view, but I feel like this isn't the "right way to do" in SwiftUI. And this way I could not apply any transition visual effect.
Some code samples to explain what I am looking for.
private extension ContentView {
#ViewBuilder
var navigationBarLeadingItems: some View {
if tabSelection == 3 {
Button(action: {
print("Edit pressed")
// Here I want to replace the tabSelection 3 view by another view temporarly and update the navigation bar items
}) {
Text("Edit")
}
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
TabView(selection: $tabSelection) {
ContactPage()
.tabItem {
Text("1")
}
.tag(1)
Text("Chats")
.tabItem() {
Text("2")
}
.tag(2)
SettingsView()
.tabItem {
Text("3")
}
.tag(3)
}.navigationBarItems(leading: navigationBarLeadingItems)
}
}
}
Thank you
EDIT
I have a working version where I simply update a toggle variable in my button action that makes my view display one or another thing, it is working but I cannot apply any animation effect on it, and it doesn't look "right" in SwiftUI, I guess there is something better that I do not know.
If you just want to add animations you can try:
struct ContentView: View {
...
#State var showEditView = false
var body: some View {
NavigationView {
TabView(selection: $tabSelection) {
...
view3
.tabItem {
Text("3")
}
.tag(3)
}
.navigationBarItems(leading: navigationBarLeadingItems)
}
}
}
private extension ContentView {
var view3: some View {
VStack {
if showEditView {
FormView()
.background(Color.red)
.transition(.slide)
} else {
Text("View 3")
.background(Color.blue)
.transition(.slide)
}
}
}
}
struct FormView: View {
var body: some View {
Form {
Text("test")
}
}
}
A possible alternative is to use a ViewRouter: How To Navigate Between Views In SwiftUI By Using An #EnvironmentObject.

Default text for back button in NavigationView in SwiftUI

I use a NavigationLink to navigate from "View1" to "View2", on the second view, the back button gets the title of the previous view
But, if the title of the previous view is very long, then the back button gets the text "Back"
How could I change that "Back" text?
I wanna make my app available in multiple languages, but it seems that "Back" does not change when phone's language changes
struct ContentView: View {
var body: some View {
return NavigationView {
VStack {
Text("View1")
NavigationLink(destination: Text("View2").navigationBarTitle("Title View2", displayMode: .inline)) {
Text("NavigationLink")
}
}.navigationBarTitle("Title View1")
}
}
}
PS: I'd like to keep this functionality as it it, I just want to change the language used for back button
here is a workaround ....
struct ContentView: View {
#State private var isActive: Bool = false
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView(), isActive: $isActive) {
Text("Title View2")
}
}.navigationBarTitle(! isActive ? "Title View2" : "Your desired back Title", displayMode: .inline)
}
}
}
struct DetailView: View {
var body: some View {
Text("View2")
}
}
I found a solution which can work also very good.
View1 set a toolbar item with .principal and add your Text or what you want.
for example :
ToolbarItem(placement: .principal) {
HStack{
Text("View1")
}.font(.subheadline)
}
and set also your title in View1:
.navigationTitle("Back")
And do nothing in your view2. it will automatically add your view1 title to your view2 default back button text
I've managed to localize back buttons by providing translations for the Back key in the Localizable.strings file.
I am using SwiftUI though.
You can create a custom back button in your navigation link by hiding native navigationBackButton. In the custom back button, you can add your translated custom back button title.
struct ContentView: View {
var body: some View {
return NavigationView {
VStack {
Text("View1")
NavigationLink("NavigationLink", destination: NextView())
}.navigationBarTitle("Title View1")
}
}
}
struct NextView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var backButton : some View { Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image("backImage") // BackButton Image
.aspectRatio(contentMode: .fit)
.foregroundColor(.white)
Text("Go Back") //translated Back button title
}
}
}
var body: some View {
VStack {
Text("View2")
}
.navigationBarTitle("Title View2",displayMode: .inline)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: backButton)
}
}
Output:-
Create your own button, then assign it using .navigationBarItems(). I found the following format most nearly approximated the default back button.
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var backButton : some View {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
HStack(spacing: 0) {
Image(systemName: "chevron.left")
.font(.title2)
Text("Cancel")
}
}
}
Make sure you use .navigationBarBackButtonHidden(true) to hide the default button and replace it with your own!
List(series, id:\.self, selection: $selection) { series in
Text(series.SeriesLabel)
}
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: backButton)
Add localization to your project. If language was set with user device settings(or simulator), after you add localization to your project it will work. Project's supported language must match with selected one on device.

Resources