I'm experiencing a visual bug when using a List and TextFields in SwiftUI. After focusing on a TextField in the List and then removing the focus (I've tried various methods of doing this, like Buttons in the List rows/keyboard toolbar etc.), there is a visual bug where the black view behind the keyboard dismisses, hangs for a second over the safe area, then suddenly disappears, causing the animation not to appear smooth. Also, if you scroll to the bottom of the List and perform the same steps, after the black view lingers for a second, it then suddenly disappears again but causes the List to 'snap' down a bit and again ruins the animation.
Here is a minimal demonstration of the issue:
struct ContentView: View {
#State var text: String = ""
#FocusState private var focused: Int?
var body: some View {
List {
ForEach(0..<50) { item in
HStack {
TextField("", text: $text)
.id(item)
.focused($focused, equals: item)
.background(.white)
Button {
focused = nil
} label: {
Text("Hide")
}
}
}
}
.scrollContentBackground(.hidden)
.background(.red)
}
}
One solution is using the List in a VStack with something below the List, but this is something I'd like to avoid with my UI. Also, I could use a ScrollView, but there is then a separate issue, where after dismissing the keyboard, the extra padding under the List produced by the keyboard avoidance stays there until you try and scroll the List again. I would also like to use the native swipe actions in my actual project. Finally, ignoring the safe area of the List/ScrollView works but then this disables keyboard avoidance, which is something that I'd like to keep.
Note: experienced on iOS 16.0 and 16.1
I am not 100% sure why this is happening, since I have not worked a lot with lists. But I guess it has something to do with that you are not explicitly telling your view to ignore the safe area. Adding .edgesIgnoringSafeArea(.bottom) to your list when leaving focus, and then changing it to trailing on entering focus seems to solve it.
struct ContentView: View {
#State var text: String = ""
#State var ignoreSafeArea = false
#FocusState private var focused: Int?
var body: some View {
List {
ForEach(0..<50) { item in
HStack {
TextField("", text: $text)
.id(item)
.focused($focused, equals: item)
.background(.white)
Button {
focused = nil
} label: {
Text("Hide")
}
}
}
.onChange(of: focused) { focused in
if focused == nil {
ignoreSafeArea = true
} else {
ignoreSafeArea = false
}
}
}.edgesIgnoringSafeArea(ignoreSafeArea ? .bottom : .trailing)
}
}
Related
Is it possible for iPadOS to control the position of the popover, to appear on the right side of the view, or under, basically the same behaviour that arrowEdge is offering for macOS.
Right now it is always placed above of the view, like in the picture below. I would like to place it on the right side of the view, where the "red" popover is placed
The code:
struct PopoverExample: View {
#State private var isShowingPopover = false
var body: some View {
Button("Show Popover") {
self.isShowingPopover = true
}
.popover(isPresented: $isShowingPopover, arrowEdge: .trailing) {
Text("Popover Content")
.padding()
}
}
}
Description
I have this following code:
NavigationView {
List {
ForEach(someList) { someElement in
NavigationLink(destination: someView()) {
someRowDisplayView()
} //: NavigationLink
} //: ForEach
} //: List
} //: NavigationView
Basically, it displays a dynamic list of class objects previously filled by user input. I have an "addToList()" function that allows users to add some elements (let's say name, email, whatever) and once the user confirms it adds the element to this list. Then I have a view that displays the list through a ForEach and then makes a NavigationLink of each element as indicated in the code example above.
Once clicked on an element of this list, the user should be properly redirected to a destination view as the NavigationLink implies. All this is working fine.
What I want
Here's where I face an issue: I want the user to be able to edit the content of a row of this list without having to delete the row and re-add another one.
I decided to use the EditMode() feature of SwiftUI. So this is what I came up with:
#State private var editMode = EditMode.inactive
[...]
NavigationView {
List {
ForEach(someList) { someElement in
NavigationLink(destination: someView()) {
someRowDisplayView()
} //: NavigationLink
} //: ForEach
} //: List
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
EditButton()
}
}
.environment(\.editMode, $editMode)
} //: NavigationView
The EditMode is properly triggered when I click on the Edit button. But I noticed that the list row is not clickable in edit mode, which is fine because I do not want it to follow the NavigationLink while in edit mode.
Though what I want is that the user is either redirected to a view that allows editing of the tapped row, or better, that an edition sheet is presented to the user.
What I have tried
Since I couldn't tap the row in edition mode, I have tried several tricks but none of them concluded as I wanted. Here are my tries.
.simultaneousGesture
I tried to add a .simultaneousGesture modified to my NavigationLink and toggle a #State variable in order to display the edition sheet.
#State private var isShowingEdit: Bool = false
[...]
.simultaneousGesture(TapGesture().onEnded{
isShowingEdit = true
})
.sheet(isPresented: $isShowingEdit) {
EditionView()
}
This simply does not work. It seems that this .simultaneousGesture somehow breaks the NavigationLink, the tap succeeds like once out of five times.
It doesn't work even by adding a condition on the edit mode, like:
if (editMode == .active) {
isShowingEdit = true
}
In non-edition mode the NavigationLink is still bugged. But I have noticed that once in edition mode, it kind of does what I wanted.
Modifier condition extension on View
After the previous failure my first thought was that I needed the tap gesture to be triggered only in edit mode, so that in non-edit mode the view doesn't even know that it has a tap gesture.
I decided to add an extension to the View structure in order to define conditions to modifiers (I found this code sample somewhere on Stackoverflow):
extension View {
#ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
Then in my main code, instead of the previously tried .simultaneousGesture modifier, I added this to NavigationLink:
.if(editMode == .active) { view in
view.onTapGesture {
isShowingEdit = true
}
}
And it worked, I was able to display this edition view once a user taps on a row while in edition mode and the normal non-edition mode still worked as it properly triggered the NavigationLink.
Remaining issues
But something bothered me, it didn't feel like a natural or native behavior. Once tapped, the row didn't show any feedback like it shows in non-edition mode when following the NavigationLink: it doesn't highlight even for a very short time.
The tap gesture modifier simply executes the code I have asked without animation, feedback or anything.
I would like the user to know and see which row was tapped and I would like it to highlight just as it does when when tapping on the NavigationLink: simply make it look like the row was actually "tapped". I hope it makes sense.
I would also like the code to be triggered when the user taps on the whole row and not only the parts where the text is visible. Currently, tapping on an empty field of the row does nothing. It has to have text on it.
An even better solution would be something that prevents me from applying such conditional modifiers and extensions to the View structure as I surely prefer a more natural and better method if this is possible.
I'm new to Swift so maybe there is a lot easier or better solution and I'm willing to follow the best practices.
How could I manage to accomplish what I want in this situation?
Thank you for your help.
Additional information
I am currently using the .onDelete and .onMove implementations on the List alongside with the SwiftUI edition mode and I want to keep using them.
I am developing the app for minimum iOS 14, using Swift language and SwiftUI framework.
If you need more code samples or better explanations please feel free to ask and I will edit my question.
Minimal working example
Asked by Yrb.
import SwiftUI
import PlaygroundSupport
extension View {
#ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
struct SomeList: Identifiable {
let id: UUID = UUID()
var name: String
}
let someList: [SomeList] = [
SomeList(name: "row1"),
SomeList(name: "row2"),
SomeList(name: "row3")
]
struct ContentView: View {
#State private var editMode = EditMode.inactive
#State private var isShowingEditionSheet: Bool = false
var body: some View {
NavigationView {
List {
ForEach(someList) { someElement in
NavigationLink(destination: EmptyView()) {
Text(someElement.name)
} //: NavigationLink
.if(editMode == .active) { view in
view.onTapGesture {
isShowingEditionSheet = true
}
}
} //: ForEach
.onDelete { (indexSet) in }
.onMove { (source, destination) in }
} //: List
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
EditButton()
}
}
.environment(\.editMode, $editMode)
.sheet(isPresented: $isShowingEditionSheet) {
Text("Edition sheet")
}
} //: NavigationView
}
}
PlaygroundPage.current.setLiveView(ContentView())
As we discussed in the comments, your code does everything in your list except highlight the row when the disclosure chevron is tapped. To change the background color of a row, you use .listRowBackground(). To make just one row highlight, you need to make the row identifiable off of the id in your array in the ForEach. You then have an optional #State variable to hold the value of the row id, and set the row id in your .onTapGesture. Lastly, you use a DispatchQueue.main.asyncAfter() to reset the variable to nil after a certain time. This gives you a momentary highlight.
However, once you go this route, you have to manage the background highlighting for your NavigationLink as well. This brings its own complexity. You need to use the NavigationLink(destination:,isActive:,label:) initializer, create a binding with a setter and getter, and in the getter, run your highlight code as well.
struct ContentView: View {
#State private var editMode = EditMode.inactive
#State private var isShowingEditionSheet: Bool = false
#State private var isTapped = false
#State private var highlight: UUID?
var body: some View {
NavigationView {
List {
ForEach(someList) { someElement in
// You use the NavigationLink(destination:,isActive:,label:) initializer
// Then create your own binding for it
NavigationLink(destination: EmptyView(), isActive: Binding<Bool>(
get: { isTapped },
// in the set, you can run other code
set: {
isTapped = $0
// Set highlight to the row you just tapped
highlight = someElement.id
// Reset the row id to nil
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
highlight = nil
}
}
)) {
Text(someElement.name)
} //: NavigationLink
// identify your row
.id(someElement.id)
.if(editMode == .active) { view in
view.onTapGesture {
// Set highlight to the row you just tapped
highlight = someElement.id
// Reset the row id to nil
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
highlight = nil
}
isShowingEditionSheet = true
}
}
.listRowBackground(highlight == someElement.id ? Color(UIColor.systemGray5) : Color(UIColor.systemBackground))
} //: ForEach
.onDelete { (indexSet) in }
.onMove { (source, destination) in }
} //: List
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
EditButton()
}
}
.environment(\.editMode, $editMode)
.sheet(isPresented: $isShowingEditionSheet) {
Text("Edition sheet")
}
} //: NavigationView
}
}
I've implemented a search bar in my app inside a custom header. Beneath the search bar I have added a false List with 100 rows that is intended to show search results.
The problem that I'm facing is:
when the list appears, the search bar moves out of bounds. When I add a top padding of 400px, the search bar comes back to bounds. Link to video 1
The next two are a bit out of topic.
When the keyboard is on screen, the last few rows of the list are not visible. How to fix it? Link to video 2
How to set a background color for a List? I haven't been able to figure that out. (listRowBackground modifier isn't working as suggested by an article I read.)
I'm using Xcode 12.0 beta 6.
let screen = UIScreen.main.bounds
struct SearchBarView: View {
#Binding var search: String
#Binding var searchSelected: Bool
var body: some View {
VStack {
CustomTextField(text: $search, isFirstResponder: true)
.modifier(SearchBarTextFieldStyle(search: $search))
if !search.isEmpty {
List(1..<100) { i in
Text("Hello \(i)")
}.frame(width: screen.width)
}
}
.frame(width: screen.width, height: !search.isEmpty ? screen.height : 40)
.background(Color("ThemeColor"))
}
}
when the list appears, the search bar moves out of bounds. When I add
a top padding of 400px, the search bar comes back to bounds.
The issue is that everything is placed in one VStack. So when search is not empty anymore the TextField shares the space provided to it with the List.
Place the TextField in a separate Stack like this:
struct ContentView: View {
#State var search: String = ""
var body: some View {
// Everything wrapped in one Stack
VStack() {
// Separate Stack for the TextField
HStack() {
TextField("Title", text: self.$search)
}.padding()
// One Stack for the content
VStack {
if !search.isEmpty {
List(1..<100) { i in
Text("Hello \(i)")
}.frame(width: UIScreen.main.bounds.width)
}
}.frame(width: UIScreen.main.bounds.width)
.background(Color.red)
Spacer() // So that the TextField is also on top when no content is displayed
}
}
}
When the keyboard is on screen, the last few rows of the list are not
visible. How to fix it?
Add a padding to the bottom of the list but I'd recommend implementing the solution of this: Move TextField up when the keyboard has appeared in SwiftUI
E.g. with padding:
import SwiftUI
struct ContentView: View {
#State var search: String = ""
var body: some View {
VStack() {
HStack() {
TextField("Title", text: self.$search)
}.padding()
VStack {
if !search.isEmpty {
List(1..<100) { i in
Text("Hello \(i)")
}.frame(width: UIScreen.main.bounds.width)
.padding(.bottom, 300) // here padding
}
}.frame(width: UIScreen.main.bounds.width)
Spacer()
}
}
}
How to set a background color for a List? I haven't been able to
figure that out. (listRowBackground modifier isn't working as
suggested by an article I read.)
This question has also already been answered here:
SwiftUI List color background
I have a crash happening unpredictably when I'm presenting a sheet over a view that is already in a sheet. I have been able to slowly strip out parts of my view and custom types until I've got the very simple and generic view structure below that still exhibits the crash.
The crash happens when I interact with the TextField then interact with one of the buttons that shows the sub-sheet. It sometimes takes a lot of tapping around between the buttons and the text field to trigger the crash, and sometimes it happens right away (as in the GIF below). Sometimes I can't get the crash to happen at all, but my users keep reporting it.
In the gif below the crash occurs the minute the bottom button is pressed. You can see the button never comes out of its "pressed" state and the sheet never appears.
Xcode doesn't give any helpful info about the crash (screenshots included below).
I've only gotten it to happen on an iPhone XR running 13.4.1 and Xcode 11.4.1. I have tried on an iPhone 6s and several simulators and can't trigger the crash, but users have reported it on several devices.
struct ContentView: View {
#State var showingSheetOne: Bool = false
var body: some View {
Button(action: { self.showingSheetOne = true }) {
Text("Show")
}
.sheet(isPresented: $showingSheetOne) {
SheetOne(showingSheetOne: self.$showingSheetOne)
}
}
}
struct SheetOne: View {
#Binding var showingSheetOne: Bool
#State var text = ""
var body: some View {
VStack {
SheetTwoButton()
SheetTwoButton()
SheetTwoButton()
TextField("Text", text: self.$text)
}
}
}
struct SheetTwo: View {
#Binding var showing: Bool
var body: some View {
Button(action: {
self.showing = false
}) {
Text("Hide")
.frame(width: 300, height: 100)
.foregroundColor(.white)
.background(Color.blue)
}
}
}
struct SheetTwoButton: View {
#State private var showSheetTwo: Bool = false
var body: some View {
Button(action: { self.showSheetTwo = true } ) {
Image(systemName: "plus.circle.fill")
.font(.headline)
}.sheet(isPresented: self.$showSheetTwo) {
SheetTwo(showing: self.$showSheetTwo)
}
}
}
I ran into a similar problem a few weeks ago. Turns out that when I presented the new sheet with the keyboard open it would lead to a crash.
I found using UIApplication.shared.endEditing() before showing the second sheet would solve the problem
UPDATE
For iOS 14 I’ve created an extension because the above function is no longer available
extension UIApplication {
static func endEditing() {
let resign = #selector(UIResponder.resignFirstResponder)
UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
}
}
The usage is similar UIApplication.endEditing()
I'm using this in Swift 5.7, iOS 16.0:
#if !os(watchOS)
import UIKit
func dismissKeyboard() {
// Dismiss text editing context menu
UIMenuController.shared.hideMenu()
// End any text editing - dismisses keyboard.
UIApplication.shared.endEditing()
}
#endif
However, since iOS 16.0 there's now a warning:
'UIMenuController' was deprecated in iOS 16.0:
UIMenuController is deprecated. Use UIEditMenuInteraction instead.
I haven't yet figured out how to use UIEditMenuInteraction to do the same.
I'm trying to make a tabbed application on macOS with SwiftUI, and I have an odd issue with TabView.
When I have two tabs with TextFields each and save their text states to their respective private variables, something odd happens: When I switch from tab A to tab B after entering text into tab A's TextField, the tab indicator shows that I am still on tab A, but the content shows tab B's content. When I click on the button for tab B once again, it will still show tab B's content. Furthermore, when I press the button for tab A afterward, it will show the content of tab A, but the indicator for the tab still shows that I am on tab B.
What might I possibly be doing wrong?
Here is an example that illustrates my issue:
import SwiftUI
struct ContentView: View {
var body: some View {
TabView
{
TabAView()
.tabItem({Text("Tab A")})
TabBView()
.tabItem({Text("Tab B")})
}
}
}
struct TabAView: View {
#State private var text = ""
var body: some View {
VStack{
Text("Tab A")
TextField("Enter", text: $text)
}
}
}
struct TabBView : View {
#State private var text = ""
var body: some View {
VStack {
Text("Tab B")
TextField("Enter", text: $text)
}
}
}
Here's a screen capture of the issue occurring:
This is definitely a bug in SwiftUI's implementation of TabView. But you can easily work around the problem by binding to TabView selection and setting the current tab manually like so:
struct ContentView: View {
#State private var currentTab = 0
var body: some View {
TabView(selection: $currentTab)
{
TabAView()
.tabItem({Text("Tab A")})
.tag(0)
.onAppear() {
self.currentTab = 0
}
TabBView()
.tabItem({Text("Tab B")})
.tag(1)
.onAppear() {
self.currentTab = 1
}
}
}
}
This bug only seems to manifest itself when the user changes tabs while a TextField has focus.
If you make the above changes to your code, it will work as expected.