Workaround for weird behaviour of SwiftUI-Menu in iOS 16 - ios

I have a simple SwiftUI-view that looks like this:
var body: some View {
VStack {
TextEditor(text: $textInput)
.padding()
.border(.blue, width: 1)
HStack {
Button("Bottom button") { print("Button pressed") }
.padding()
.border(.green, width: 1)
Spacer()
Menu("Bottom Menu") {
Button("Menu option 1") { print("Option 1 pressed") }
Button("Menu option 2") { print("Option 2 pressed") }
}
.padding()
.border(.red, width: 1)
}
.background(.gray)
}
}
The problem is that when the keyboard appears on this view, on a device running iOS 16, the frame of "Bottom menu" behaves really strange. It becomes even stranger if I also tap on the menu, as you can see in the screencast below.
I have tried to use .fixedSize() on the menu, and various other solution found here on Stack Overflow and other places, but nothing seems to work. As soon as the keyboard opens, the menu is all over the place.
When testing the same things on iOS 15, everything works as expected, so it seems to be a bug in iOS 16? Is there a workaround?
The simulator to the left is using iOS 16, and the one to the right is iOS 15.

Related

SwiftUI Button touch area is abnormal

I created SwiftUI Button and its touch area is slightly strange. The touch area extends the label of the button.
This is my code.
struct TestView: View {
var body: some View {
HStack(spacing: 0) {
Spacer()
Button {
print("aaaaaa")
} label: {
HStack(spacing: 0) {
Spacer()
}
.frame(width: 50.0, height: 50.0)
.background(Color.yellow)
}
Spacer()
}
.frame(height: 50.0)
.background(Color.red)
}
}
And I'll attach the result in simulator.
The button area is filled with yellow color. But I could click the button outside of yellow color.
The same thing happens in real device too.
How is this possible?
This is a completely normal behavior of Button/onTapGesture in SwiftUI. Its touch area is slightly bigger than its border size.
This is not a bug or glitch.

SwiftUI iOS 14/15 Can't tap Button underneath Spacer in ScrollView

In SwiftUI, I have a Button that's underneath the Spacer within a ScrollView. The ScrollView steals the tap gesture, so the Button never sees the tap.
So, for example, the button in this example does not work:
struct DoesNotWork: View {
var body: some View {
ZStack {
VStack {
// Button doesn't work
Button("Tap This") {
print("Tapped")
}
Spacer()
}
VStack {
ScrollView {
Spacer()
.frame(height: 150)
Rectangle()
.foregroundColor(.blue)
.frame(height: 150)
Spacer()
}
}
}
}
}
This version -- with everything else the same except no ScrollView -- works fine:
struct ThisWorks: View {
var body: some View {
ZStack {
VStack {
// Button works normally
Button("Tap This") {
print("Tapped")
}
Spacer()
}
VStack {
Spacer()
.frame(height: 150)
Rectangle()
.foregroundColor(.blue)
.frame(height: 150)
Spacer()
}
}
}
}
So that rules out the VStack, ZStack, and the Spacer.
I've tried using .allowsHitTesting(false) on the Spacer that's within the ScrollView, and also tried .disabled(true) (and in combination). Adding .allowsHitTesting(false) to the ScrollView makes the button works but of course breaks the ScrollView.
I also tried setting .foregroundColor(.clear) on the Spacer.
For what it's worth, I'm having the same behavior with both Xcode 13.0 Beta 5 testing with iOS 15.0 beta 7, as well as with Xcode 12.5.1 with iOS 14.7.1.
I'm out of ideas. It seems like it should be the simplest thing in the world, but I can't figure out a way around this.
Any help is greatly appreciated!

Issues with a custom tab view combined with ScrollView

The look of my tab view is great. Everything in fact looks good. But with the current code. The ScrollView ends behind the tab bar instead of above. I'm guessing it has to do with it being a ZStack (it - meaning the content).
I tried adding padding on each view inside the switch (padding bottom same height as tab bar but not working).
The two results I've gotten is either it won't scroll enough to see all content but tab bar looks good.
OR
Scrolling works great but the tab bar has some overlaying issues visually with showing whitespace.
I just don't know how to move around the code any more to get a better result. :(
//Landing-page..
VStack {
ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) {
switch tabController.selectedIndex {
case 0:
HomeView()
.padding(.bottom, 50)
case 1:
Text("Test..")
case 2:
Text("Test...")
case 3:
Text("Test....")
default:
VStack {
Text("Default View")
}
}
CustomTabView()
}
}
.background(Color.gray.ignoresSafeArea()).edgesIgnoringSafeArea(.bottom)
// TabView..
HStack {
// tabControllers.icons.count = 4 tabs at the moment
ForEach(0..<tabController.icons.count, id: \.self) { tab in
Button(action: {
// animation when changing tab comes here eventually
tabController.selectedIndex = tab
}, label: {
Spacer()
Image(systemName: tabController.icons[tab])
.frame(width: 50, height: 50)
.font(.system(
size: 22,
weight: tabController.selectedIndex == tab ? .semibold : .regular,
design: .default))
.foregroundColor(tabController.selectedIndex == tab ? Color.themeAccent : Color.themeAccent.opacity(0.3))
.background(tabController.selectedIndex == tab ? Color.primaryPurple : nil)
.cornerRadius(tabController.selectedIndex == tab ? 30 : 0)
Spacer()
})
}
}
.padding()
.background(Color.blue)
.clipShape(CShape())
.shadow(color: Color.black.opacity(0.3), radius: 20, x: 0.0, y: 0.0)
The ScrollView starts at the top in the HomeView.
Tab view showing correctly, but not scrollview
ScrollView showing correctly, but not tab view
I think you can get rid of the ZStack, put the switch/case in the VStack and the CustomTabView() below that. The ScrollView will push the CustomTabView to the bottom and not overlap it. If not all of the views in the switch are ScrollViews, then you can add a Spacer() above the CustomTabView, which will push the views apart to the top and bottom.

Textfield tap is dismissing navigation link in SwiftUI

I'm facing a weird issue with textfields and navigation view/link using SwiftUI
All I do is navigate through views using navigation links and inside the destination view there are some textfields. When I tap on any of them the navigation automatically dismisses.
How can I fix navigation link from dismissing when textfield is tapped on and the keyboard shows up?
var emailLoginButton: some View {
NavigationLink(destination: LoginView(viewModel: .init(mode: .login, isPushed: $viewModel.authViewPushed)), isActive: $viewModel.authViewPushed) {
Button(action: { viewModel.authViewPushed = true }) {
HStack {
Image(systemName: "envelope")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
.foregroundColor(.white)
Text("continue_with_email".localized())
.padding(.horizontal, 20)
}
}
.padding()
.frame(maxWidth: .infinity)
.foregroundColor(.white)
.background(Capsule().fill(Color.primaryPurple))
.shadow(color: Color.black.opacity(0.15), radius: 5, x: 5, y: 5)
.padding(.horizontal)
.padding(.bottom, 20)
}
.isDetailLink(false)
}
// Destination's view textfield which "dismisses" navigationLink
var emailTextField: some View {
HStack {
Image(systemName: "envelope")
.font(.title2)
.foregroundColor(.primary)
.frame(width: 35)
TextField(viewModel.emailPlaceholderText.uppercased(), text: $viewModel.email)
.autocapitalization(.none)
}
.padding()
.background(Color.white.opacity(viewModel.email == stringEmpty ? 0 : 0.12))
.cornerRadius(12)
.padding(.horizontal)
}
There seems to be an issue with iOS 14.5 and up where the tapping of any TextField inside a NavigationLink destination makes the view to pop out to the previous one.
There is a workaround thanks to #SeitenWerk and documented on the Apple Developer Forums
https://developer.apple.com/forums/thread/677333
The solution is simple. Just add an empty NavigationLink next to the one that fails. Interestingly, it logs "Unable to present. Please file a bug." on the debug console but makes things right with the usability of the app
NavigationLink(destination: LoginView()){
Text("LOGIN")
}
NavigationLink(destination: EmptyView()) {
EmptyView()
}
Remember to thank SeitenWerk on the Developer Forums.
After some time of research I've found that NavigationLink closes because authViewPushed parameter in the viewModel becomes false. That happens because viewModel is being recreated because of firstView update. I've faced the same issue and here is the solution:
struct MyView: View {
#StateObject var viewModel = MyViewModel()
var body : some View {
}
}
In this case MyView is being updated but MyViewModel remains the same.
on iOS 15
NavigationView {
//your staff
}
.navigationViewStyle(StackNavigationViewStyle())
This might be one of the most frustrating and egregious examples of "this works everywhere else on every other version of whatever" I've seen of late in SwiftUI. (sigh) I'm reworking the whole workflow on two Views just to support previous versions of iOS which I didn't initially set out to do. Grrrr... Are there any known workarounds that are consistently proven to work?
For me the solution is to force out focus of the text field by focus state:
#FocusState private var nameIsFocused: Bool
TextField("text", text: $text)
.textInputAutocapitalization(.never)
.textContentType(.username).keyboardType(.asciiCapable)
.focused($nameIsFocused)
And then in the button callback
var timer:Timer?
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { _ in
moveToScreen.toggle(); // here change the state}

SwiftUI full screen transparent button

I try to add full screen transparent button:
Button(action: {
// my action
}) {
Rectangle()
.opacity(0)
}
But in case .opacity() is less than 0.1 button action stop working. How to implement full screen transparent button?
Tested on iOS 14.3(Sim), iOS 14.2(iPhone X), Xcode 12.3
Here is possible solution. Tested with Xcode 12.1 / iOS 14.1
struct DemoClearButton: View {
var body: some View {
Color.clear
.contentShape(Rectangle())
.onTapGesture {
print(">> transparent tapped")
}
}
}
Note: probably in place of usage you'd wanted to add .edgesIgnoringSafeArea(.all)
For me the .contentShape didn't work. At least, it didn't prevent the view behind my button to also receive the touch.
But, and I feel dirty for suggesting this:
Button(action: {
print("Yeahhh")
}) {
Text("Yeah?").padding(64)
}
.background(LinearGradient(colors: [Color.clear], startPoint: .top, endPoint: .bottom))
Adding a transparent LinearGradient did the trick for me.

Resources