How can I add multiple gesture handlers to Text without breaking scrolling? - ios

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.

Related

SwiftUI NavigationLink does not reset views positions

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

Navigation bar menu with multiple selection in iOS app with SwiftUI

I want to achieve behavior close to native iOS reminders app on iPad.
There is dropdown Menu in NavigationBar that shows tags in it. I thought that I can do it with Picker but as I see there is now multiple selection support in it.
I did it using
...
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Menu {
MenuView()
} label: {
Image(systemName: "number")
}
Button("Done") {}
}
}
And MenuView's code is
struct MenuView: View {
#State var tags = ["#tag1", "#tag2", "#tag3", "#tag4"]
var body: some View {
ForEach(tags, id:\.self) { tag in
Button {
} label: {
if selectedTags.contains(tag) {
Label(tag, systemImage: "checkmark")
} else {
Text(tag)
}
}
}
Section {
Button {
} label: {
Label("Configure", systemImage: "ellipsis")
}
}
}
}
The main problem right now is when I press any of this buttons, drop down is closed, while I want this button to only toggle selected status.
I tried to use Text instead of buttons but didn't find way to handle tap gestures from them, they look like being disabled by some view modifier.
What is the optimal way to handle it?

How can I inset a SwiftUI list when the keyboard shows and the list is at the bottom like a chat app

I am trying to inset the bottom of a list by the height of the keyboard when the keyboard shows and the list is scrolled to the bottom. I know this question has been asked in different scenarios but I haven't found any proper solution yet. This is specifically for a chat app and the simple code below demonstrates the problem:
#State var text: String = ""
#FocusState private var keyboardVisible: Bool
var body: some View {
NavigationView {
VStack {
List {
ForEach(1...100, id: \.self) { item in
Text("\(item)")
}
}
.navigationTitle("Conversations")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
keyboardVisible = false
} label: {
Text("Hide Keyboard")
}
}
}
ZStack {
Color.red
.frame(height: 44)
TextEditor(text: $text)
.frame(height: 32)
.focused($keyboardVisible)
}
}
}
}
If you scroll to the end and tap in the textEditor, the text editor moves up with the keyboard as expected, but the list doesn't move the content up. I wonder how I can achieve this and have it move up smoothly with the keyboard.
You don't mention trying it, but this is exactly what we have a ScrollViewReader() for. There is a bit of an issue using it in this case, that can be worked around. The issue is that keyboardVisible changes BEFORE the keyboard is fully up. If you scroll at that point, you will cut off the bottom of the List entries. So, we need to delay the reader with DispatchQueue.main.asyncAfter(deadline:). This causes enough delay that when the reader actually reads the position, the keyboard is at full height, and the scroll to the bottom does its job.
#State var text: String = ""
#FocusState private var keyboardVisible: Bool
var body: some View {
NavigationView {
VStack {
ScrollViewReader { scroll in
List {
ForEach(1...100, id: \.self) { item in
Text("\(item)")
}
}
.onChange(of: keyboardVisible, perform: { _ in
if keyboardVisible {
withAnimation(.easeIn(duration: 1)) {
scroll.scrollTo(100) // this would be your array.count - 1,
// but you hard coded your ForEach
}
// The scroll has to wait until the keyboard is fully up
// this causes it to wait just a bit.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// this helps make it look deliberate and finished
withAnimation(.easeInOut(duration: 1)) {
scroll.scrollTo(100) // this would be your array.count - 1,
// but you hard coded your ForEach
}
}
}
})
.navigationTitle("Conversations")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
keyboardVisible = false
} label: {
Text("Hide Keyboard")
}
}
}
ZStack {
Color.red
.frame(height: 44)
TextEditor(text: $text)
.frame(height: 32)
.focused($keyboardVisible)
}
}
}
}
}
Edit:
I changed the code to scroll twice. The first starts the scroll immediately, and the second scrolls it after the keyboard is up to finish the job. It starts quickly and ends smoothly. I also left the comments in the code for the next person; you don't need them.

swiftui bring view to center and blur background after long press like messages app

I can't seem to find how to bring a view to the center like a popup and blur background like when you long press a message in the messages app.
I know there is a simple native way to do this but I can't seem to find out how.
You are looking for contextMenu(menuItems:). It shows those buttons you see, and automatically blurs the background content to focus the selected view.
Example:
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack(spacing: 30) {
ForEach(0 ..< 20) { _ in
Text(String(Int.random(in: 1 ... 100000000)))
.padding()
.contextMenu {
Button {
//
} label: {
Label("Reply", systemImage: "arrowshape.turn.up.left")
}
Button {
//
} label: {
Label("Copy", systemImage: "doc.on.doc")
}
Button {
//
} label: {
Label("Translate", systemImage: "arrow.left.arrow.right")
}
Button {
//
} label: {
Label("Moreā€¦", systemImage: "ellipsis.circle")
}
}
}
}
}
}
}
Result:

SwiftUI NavigationBar not disappearing while scrolling

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...
}
}
}

Resources