I have a simple VStack with views inside. When the keyboard appears, the views are moving up. I can deal with it.
But when I click on a NavigationLink, and I go back, the views are stuck in their position, pretending the keyboard is still here.
I would be happy to have any solution :(
before and after
var body: some View {
ZStack {
Color(.systemGroupedBackground).edgesIgnoringSafeArea(.all)
VStack {
LogoView()
.padding(.vertical, 50)
if isSigningUp {
SignUpView()
} else {
SignInView()
}
Spacer()
HStack {
Text("Don't have an account ?")
Button(action: {
isSigningUp.toggle()
}) {
Text("\(invertedAuthCaseLabel) !").foregroundColor(Color("mainColor"))
}
}
}.padding()
.frame(maxWidth: 500)
}
}
I tried the .ignoringSafeArea(.keyboard) modifier but it didn't work, my views were still moving.
if you think the culprit here is keyboard, then you can force the keyboard to close in onDisappear(action:_). The following code might help you close the keyboard.
#if canImport(UIKit)
extension View {
func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
#endif
Related
I have text in a scroll-view that I would like to add a context menu on. I also want to close the keyboard when the context menu is open.
Here is some sample code:
struct TestView: View {
var body: some View {
let texts = Array(1...100).map { "Test \($0)" }
ScrollView {
VStack(spacing: 10) {
ForEach(texts, id: \.self) { text in
Text(text)
.frame(maxWidth: .infinity)
.contextMenu {
Button {
UIPasteboard.general.string = text
} label: {
Label("Copy", systemImage: "doc.on.clipboard")
}
}
.simultaneousGesture(LongPressGesture(minimumDuration: 0.5).onEnded { _ in
print("Do something else when the context menu is opened. (i.e. close keyboard)")
})
}
}
}
}
}
This code prevents me from being able to scroll on the scrollview starting from the text though.
I've seen in other answers that adding onTapGesture before the other gestures can fix it, but it doesn't seem to work in this case.
I’m trying to add an accessory view embedded in a navigation bar below the title, which can be seen in the default iOS calendar app (the “s m t w t f s” row) or the GitHub mobile app:
And I’d like it to work along with the large title style navigation bar like the GH mobile.
LazyVStack’s pinnedView with a section header almost work, but I can’t get the background color to make it seemless with the navigation bar, even with the ultraThinMaterial. It also leaves the divider line between the pinned view and the bar.
Is there a way to achieve this layout?
Solutions in SwiftUI, SwiftUI+Introspect, and UIKit are all welcome!
Have you tried setting a .safeAreaInset view? This will have the stickiness you're looking for, and items in the "main" part of the view will take its height into account when rendering, so won't get obscured.
Here's a quick example I knocked up:
struct ContentView: View {
var body: some View {
NavigationView {
List {
ForEach(0 ..< 30) { item in
Text("Hello, world!")
}
}
.navigationTitle("Accessory View")
.safeAreaInset(edge: .top) {
AccessoryView()
}
}
}
}
struct AccessoryView: View {
var body: some View {
HStack {
Button("Button") { }
Button("Button") { }
Button("Button") { }
Spacer()
}
.padding()
.background(Color(uiColor: .systemGroupedBackground))
.buttonStyle(.bordered)
.controlSize(.mini)
}
}
You have to give the view a background otherwise it'll be transparent – but that background will (as long as it's a colour or a material) automatically extend into the navigation bar itself. Here's a GIF of the above code in action, where I've set the background to match the grouped list's background:
It's not perfect, especially as it looks distinct from the nav bar on scroll, but it might be useable for you?
Another idea is to replace the navigation bar with a custom one like this:
{
...
}
.safeAreaInset(edge: .top) {
VStack(alignment: .leading, spacing: 8) {
HStack() {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "chevron.backward")
}
Spacer()
Text(navigationTitle).font(.title2).bold()
.multilineTextAlignment(.center)
.foregroundColor(.accentColor)
.frame(maxWidth: .infinity)
Spacer()
}
HStack {
Button("Button") { }
Button("Button") { }
Button("Button") { }
Spacer()
}
}
.padding()
.background(
.bar
)
}
You will also have to set:
.navigationBarBackButtonHidden(true)
and do not set a navigation title:
// .navigationTitle("....")
In SwiftUI, I would like to use a background color for my view while also setting navigationViewStyle to .stack, to force a single-column stack navigation Plus-sized devices.
var body: some View {
TabView {
NavigationView {
ScrollView {
ForEach(0..<100) { _ in
Text("Hello, world!")
.padding()
}
.frame(maxWidth: .infinity)
}
.navigationTitle("Demo")
// .background(Color.yellow) // I can do this …
}
// .navigationViewStyle(.stack) // … or this, but not both!
.tabItem {
Label("Demo", systemImage: "swift")
}
}
}
However, when I do both, the navigation bar won't collapse when I scroll down. Also both the navigation bar and tab bar appear without background.
When I only set the background, but leave out the line that sets the navigationViewStyle, everything looks fine in portrait mode, or smaller devices. But on a Plus-size device in landscape, it looks like this:
So I guess I really can't do this without setting the navigationViewStyle.
What can I do to fix this? Is this a bug that should be fixed by Apple? All help is greatly appreciated.
Use the .navigationViewStyle view modifier on the ScrollView
struct ContentView: View {
var body: some View {
TabView {
NavigationView {
ScrollView {
ForEach(0..<100) { _ in
Text("Hello, world!")
.padding()
}
.frame(maxWidth: .infinity)
}
.navigationTitle("Demo")
.background(Color.yellow)
.navigationViewStyle(.stack)
}
.tabItem {
Label("Demo", systemImage: "swift")
}
}
}
}
Update
I guess, it is a bug. It does not work.
1
If all of your ScrollViews have the same background, use this once on a view.
struct ScrollViewBackground: ViewModifier {
let color: Color
func body(content: Content) -> some View {
content
.ignoresSafeArea(edges: .horizontal)
.onAppear {
UIScrollView.appearance().backgroundColor = UIColor(color)
}
}
}
extension View {
func setBackgroundColor(color: Color) -> some View {
return self.modifier(ScrollViewBackground(color: color))
}
}
2
Use introspect to access the underlaying UIScrollView and change its background. You need to also use .ignoresSafeArea(edges: .horizontal) on the ScrollView.
ScrollView {
Text("Item 2")
}
.introspectScrollView { scrollView in
scrollView.backgroundColor = UIColor(color.yellow)
}
I want to hide my NavigationBar while scrolling, actually It must hide automatically but when I tried with multiple views It doesn't work. Also, It works when I remove custom views and capsulate List with NavigationView. But I need SearchBar and StatusView view. Is there any suggestion?
By the way, I run it on the device, I use canvas here for demonstration purposes.
Thank you.
var body: some View {
NavigationView {
VStack(spacing: 0) {
SearchBar(searchText: $viewModel.searchText)
StatusView(status: $viewModel.status)
Divider()
List(0...viewModel.characters.results.count, id: \.self) { index in
if index == self.viewModel.characters.results.count {
LastCell(vm: self.viewModel)
} else {
ZStack {
NavigationLink(destination: DetailView(detail: self.viewModel.characters.results[index])) {
EmptyView()
}.hidden()
CharacterCell(character: self.viewModel.characters.results[index])
}
}
}
.navigationBarTitle("Characters", displayMode: .large)
}
}
.onAppear {
self.viewModel.getCharacters()
}
}
Just idea, scratchy... try to put your custom views inside List as below (I know it will work, but I'm not sure if autohiding will work)
NavigationView {
List {
SearchBar(searchText: $viewModel.searchText)
StatusView(status: $viewModel.status)
Divider()
ForEach (0...viewModel.characters.results.count, id: \.self) { index in
...
Based on Asperi's solution, I wanted to have the SearchBar and StatusView always visible, i.e. it should stop scrolling after the title has disappeard. You can achieve this with a section header like shown below (just a rough sketch):
NavigationView {
List {
Section(header: {
VStack {
SearchBar...
StatusView....
}
}) {
ForEach...
}
}
}
I've got a List view and each row of the list contains an HStack with some text view('s) and an image, like so:
HStack{
Text(group.name)
Spacer()
if (groupModel.required) { Text("Required").color(Color.gray) }
Image("ic_collapse").renderingMode(.template).rotationEffect(Angle(degrees: 90)).foregroundColor(Color.gray)
}.tapAction { self.groupSelected(self.group) }
This seems to work great, except when I tap in the empty section between my text and the image (where the Spacer() is) the tap action is not registered. The tap action will only occur when I tap on the text or on the image.
Has anyone else faced this issue / knows a workaround?
As I've recently learned there is also:
HStack {
...
}
.contentShape(Rectangle())
.onTapGesture { ... }
Works well for me.
Why not just use a Button?
Button(action: { self.groupSelected(self.group) }) {
HStack {
Text(group.name)
Spacer()
if (groupModel.required) { Text("Required").color(Color.gray) }
Image("ic_collapse").renderingMode(.template).rotationEffect(Angle(degrees: 90)).foregroundColor(Color.gray)
}
}.foregroundColor(.primary)
If you don't want the button to apply the accent color to the Text(group.name), you have to set the foregroundColor as I did in my example.
works like magic on every view:
extension View {
func onTapGestureForced(count: Int = 1, perform action: #escaping () -> Void) -> some View {
self
.contentShape(Rectangle())
.onTapGesture(count:count, perform:action)
}
}
The best approach in my opinion for accessibility reasons is to wrap the HStack inside of a Button label, and in order to solve the issue with Spacer can't be tap, you can add a .contentShape(Rectangle()) to the HStack.
So based on your code will be:
Button {
self.groupSelected(self.group)
} label: {
HStack {
Text(group.name)
Spacer()
if (groupModel.required) { Text("Required").color(Color.gray) }
Image("ic_collapse").renderingMode(.template).rotationEffect(Angle(degrees: 90)).foregroundColor(Color.gray)
}
.contentShape(Rectangle())
}
I've been able to work around this by wrapping the Spacer in a ZStack and adding a solid color with a very low opacity:
ZStack {
Color.black.opacity(0.001)
Spacer()
}
Simple extension based on Jim's answer
extension Spacer {
/// https://stackoverflow.com/a/57416760/3393964
public func onTapGesture(count: Int = 1, perform action: #escaping () -> Void) -> some View {
ZStack {
Color.black.opacity(0.001).onTapGesture(count: count, perform: action)
self
}
}
}
Now this works
Spacer().onTapGesture {
// do something
}
I just add the background color(except clear color) for HStack works.
HStack {
Text("1")
Spacer()
Text("1")
}.background(Color.white)
.onTapGesture(count: 1, perform: {
})
Although the accepted answer allows the mimicking the button functionality, visually it does not satisfy. Do not substitute a Button with a .onTapGesture or UITapGestureRecognizer unless all you need is an area which accepts finger tap events. Such solutions are considered hacky and are not good programming practices.
To solve your problem you need to implement the BorderlessButtonStyle ⚠️
Example
Create a generic cell, e.g. SettingsNavigationCell.
SettingsNavigationCell
struct SettingsNavigationCell: View {
var title: String
var imageName: String
let callback: (() -> Void)?
var body: some View {
Button(action: {
callback?()
}, label: {
HStack {
Image(systemName: imageName)
.font(.headline)
.frame(width: 20)
Text(title)
.font(.body)
.padding(.leading, 10)
.foregroundColor(.black)
Spacer()
Image(systemName: "chevron.right")
.font(.headline)
.foregroundColor(.gray)
}
})
.buttonStyle(BorderlessButtonStyle()) // <<< This is what you need ⚠️
}
}
SettingsView
struct SettingsView: View {
var body: some View {
NavigationView {
List {
Section(header: "Appearance".text) {
SettingsNavigationCell(title: "Themes", imageName: "sparkles") {
openThemesSettings()
}
SettingsNavigationCell(title: "Lorem Ipsum", imageName: "star.fill") {
// Your function
}
}
}
}
}
}
I've filed feedback on this, and suggest you do so as well.
In the meantime an opaque Color should work just as well as Spacer. You will have to match the background color unfortunately, and this assumes you have nothing to display behind the button.
Kinda in the spirit of everything that has been said:
struct NoButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(Color.black.opacity(0.0001))
}
}
extension View {
func wrapInButton(action: #escaping () -> Void) -> some View {
Button(action: action, label: {
self
})
.buttonStyle(NoButtonStyle())
}
}
I created the NoButtonStyle because the BorderlessButtonStyle was still giving an animation that was different than .onTapGesture
Example:
HStack {
Text(title)
Spacer()
Text("Select Value")
Image(systemName: "arrowtriangle.down.square.fill")
}
.wrapInButton {
isShowingSelectionSheet = true
}
Another option:
extension Spacer {
func tappable() -> some View {
Color.blue.opacity(0.0001)
}
}
Updated:
I've noticed that Color doesn't always act the same as a Spacer when put in a stack, so I would suggest not using that Spacer extension unless you're aware of those differences. (A spacer pushes in the single direction of the stack (if in a VStack, it pushes vertically, if in a HStack, it pushes out horizontally, whereas a Color view pushes out in all directions.)
This website helped answer the same question for me:
https://www.hackingwithswift.com/quick-start/swiftui/how-to-control-the-tappable-area-of-a-view-using-contentshape
Try applying .ContentShape(Rectangle()) to the HStack.