How to make SwiftUI button label uppercase - ios

Is it possible to transform that text of a label in a SwiftUI button to uppercase using a style?
struct UppercaseButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.makeUppercase() // ?
}
}

struct UppercaseButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.textCase(.uppercase) // <- here
}
}
usage:
struct ContentView: View {
var body: some View {
Button("test", action: {})
.buttonStyle(UppercaseButtonStyle()) // <= here
}
}

The textCase modifier will work directly on your button, e.g.:
Button("test", action: {})
.textCase(.uppercase)
However, if you want to wrap this up in a style, it's better to use a PrimitiveButtonStyle, as this comes with a Configuration object that can be passed into the Button initializer.
struct UppercaseButtonStyle: PrimitiveButtonStyle {
func makeBody(configuration: Configuration) -> some View {
Button(configuration)
.textCase(.uppercase)
}
}
// bonus points - add a shorthand description to match built-in styles
extension PrimitiveButtonStyle where Self == UppercaseButtonStyle {
static var uppercase = UppercaseButtonStyle()
}
// usage
Button("Test") { }
.buttonStyle(.uppercase)
This means that you don't need to worry about any other type of configuration on the button, and your style should be able to play nicely with others, e.g.:
Button("Test", role: .destructive) { }
.buttonStyle(.borderedProminent)
.buttonStyle(.uppercase)

Related

How do I change the background colour of a list in swiftui? [duplicate]

I'm trying to recreate an UI I built with UIKit in SwiftUI but I'm running into some minor issues.
I want the change the color of the List here, but no property seems to work as I expects. Sample code below:
struct ListView: View {
#EnvironmentObject var listData: ListData
var body: some View {
NavigationView {
List(listData.items) { item in
ListItemCell(item: item)
}
.content.background(Color.yellow) // not sure what content is defined as here
.background(Image("paper-3")) // this is the entire screen
}
}
}
struct ListItemCell: View {
let item: ListItem
var body: some View {
NavigationButton(destination: Text(item.name)) {
Text("\(item.name) ........................................................................................................................................................................................................")
.background(Color.red) // not the area I'm looking for
}.background(Color.blue) // also not the area I'm looking for
}
}
Ok, I found the solution for coloring the list rows:
struct TestRow: View {
var body: some View {
Text("This is a row!")
.listRowBackground(Color.green)
}
}
and then in body:
List {
TestRow()
TestRow()
TestRow()
}
This works as I expect, but I have yet to find out how to then remove the dividing lines between the rows...
This will set the background of the whole list to green:
init() {
UITableView.appearance().separatorStyle = .none
UITableViewCell.appearance().backgroundColor = .green
UITableView.appearance().backgroundColor = .green
}
struct ContentView: View {
var strings = ["a", "b"]
var body: some View {
List {
ForEach(strings, id: \.self) { string in
Text(string)
}.listRowBackground(Color.green)
}
}
}
You can do it by changing UITableView's appearance.
UITableView.appearance().backgroundColor = UIColor.clear
just put this line in Appdelegate's didFinishLaunchingWithOptions method.
In replace of UIColor.clear set whatever color you want to add in background color of list.
Changing Background Color
As other have mentioned, changing the UITableView background will affect all other lists in your app.
However if you want different background colors you can set the default to clear, and set the background color in swiftui views like so:
List {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
// Ignore safe area to take up whole screen
.background(Color.purple.ignoresSafeArea())
.onAppear {
// Set the default to clear
UITableView.appearance().backgroundColor = .clear
}
You probably want to set the tableview appearance earlier, such as in the SceneDelegate or root view like so:
// SceneDelegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else {
print("Returning because screne does not exist")
return
}
// Set here
UITableView.appearance().backgroundColor = .clear
let contentView = ContentView()
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
// Root App View
#main
struct ListBackgroundApp: App {
init() {
UITableView.appearance().backgroundColor = .clear
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
2022
MacOS Solution
The following code makes ALL OF Lists background color transparent:
// Removes background from List in SwiftUI
extension NSTableView {
open override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
backgroundColor = NSColor.clear
if let esv = enclosingScrollView {
esv.drawsBackground = false
}
}
}
..........
..........
..........
the following code makes ALL OF TextEditors background color transparent:
extension NSTextView {
open override var frame: CGRect {
didSet {
backgroundColor = .clear
drawsBackground = true
}
}
}
There is an argument: listRowBackground() in SwiftUI, but if you use List directly to iterate the data collection, it doesn't work.
Here is my workaround:
List {
// To make the background transparent, we have we use a ForEach as a wrapper
ForEach(files) {file in
Label(
title: { Text(file.name ?? fileOptionalFiller).lineLimit(listRowTextLineLimit) },
icon: { AppIcon.doc.foregroundColor(.primary) }
)
}
.listRowBackground(Color.primary.colorInvert())
}
Basically, listRowBackground() works if you use a ForEach inside List.
I was able to get the whole list to change color by using colorMultiply(Color:). Just add this modifier to the end of the list view, and then the padding will push the table to the device edges. For example:
List {...}.colorMultiply(Color.green).padding(.top)
https://www.hackingwithswift.com/quick-start/swiftui/how-to-adjust-views-by-tinting-and-desaturating-and-more
I do not know what is the connection but if you wrap the list with Form it is working.
Form {
List(viewModel.currencyList, id: \.self) { currency in
ItemView(item: currency)
}
.listRowBackground(Color("Primary"))
.background(Color("Primary"))
}
iOS 16 provides a modifier to control the background visibility of List (and other scrollable views): scrollContentBackground(_:)
You can hide the standard system background via .hidden. If you provide a background as well, that will become visible.
List {
Text("One")
Text("Two")
}
.background(Image("MyImage"))
.scrollContentBackground(.hidden)
You may also want to customize the background of list rows - the individual cells - and separators. This can be done like so:
List {
Section("Header") {
Text("One")
Text("Two")
.listRowBackground(Color.red)
}
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
}
.scrollContentBackground(.hidden)
struct Details: View {
var body: some View {
Spacer().overlay(
List {
Text("Hello World!").font(.title2)
.listRowBackground(Color.clear)
Text("Hello World again").font(.title2)
.listRowBackground(Color.clear)
}.onAppear() {
UITableView.appearance().backgroundColor = UIColor.green
UITableViewCell.appearance().backgroundColor = UIColor.green
}
)
}
}
The answer by Islom Alimov https://stackoverflow.com/a/59970379/9439097 seems to be the best implementation so far in my opinion.
Only drawback: this also changes the background color of all other list views in your app, so you need to manually change them back unless you want the same color everywhere.
Here is an example view:
import SwiftUI
struct TestView1: View {
init(){
UITableView.appearance().backgroundColor = UIColor(Color.clear)
}
#State var data = ["abc", "def"]
var body: some View {
VStack {
List {
ForEach(data, id: \.self) {element in
Text("\(String(describing: element))")
}
.background(Color.green)
.listRowBackground(Color.blue)
}
.background(Color.yellow)
Spacer()
Color.red
}
}
}
struct TestView1_Previews: PreviewProvider {
static var previews: some View {
TestView1()
}
}
produces:
Someone may find this useful if attempting to create a floating type cell with SwiftUI using .listRowBackground and applying .padding
var body: some View {
NavigationView {
List {
ForEach (site) { item in
HStack {
Text(String(item.id))
VStack(alignment: .leading) {
Text(item.name)
Text(item.crop[0])
}
}.listRowBackground(Color.yellow)
.padding(.trailing, 5)
.padding(.leading, 5)
.padding(.top, 2)
.padding(.bottom, 2))
}
}
.navigationBarTitle(Text("Locations"))
}
}
I assume the listRowPlatterColor modifier should do this, but isn't as of Xcode 11 Beta 11M336w
var body: some View {
List(pokemon) { pokemon in
PokemonCell(pokemon: pokemon)
.listRowPlatterColor(.green)
}
}
.colorMultiply(...)
As an option you can .colorMultiply(Color.yourColor) modifier.
Warning: this does not change the color! This only applies the Multiply modifier to the current color. Please read the question before any action, because you are probably looking for: "How to CHANGE the background color of a List in SwiftUI" and this will not work for you. ❄️
Example:
List (elements, id:\.self ) { element in
Text(element)
}
.colorMultiply(Color.red) <--------- replace with your color
For me, a perfect solution to change the background of List in SwiftUI is:
struct SomeView: View {
init(){
UITableView.appearance().backgroundColor = UIColor(named: "backgroundLight")
}
...
}
List is not perfect yet.
An option would be to use it like this -> List { ForEach(elements) { }} instead of List($elements)
On my end this is what worked best up to now.
Like #FontFamily said, it shouldn't break any List default behaviors like swiping.
Simply Add UITableView appearance background color in init() method and add list style (.listStyle(SidebarListStyle()). Don't forget to import UIKit module
struct HomeScreen: View {
init() {
UITableView.appearance().backgroundColor = .clear
}
let tempData:[TempData] = [TempData( name: "abc"),
TempData( name: "abc"),
TempData( name: "abc"),
TempData( name: "abc")]
var body: some View {
ZStack {
Image("loginBackgound")
.resizable()
.scaledToFill()
List{
ForEach(tempData){ data in
Text(data.name)
}
}
.listStyle(SidebarListStyle())
}
.ignoresSafeArea(edges: .all)
}
}
Using UITableView.appearance().backgroundColor is not a good idea as it changes the backgroundColor of all tables. I found a working solution for color changing at the exact table you selected in iOS 14, 15.
We will change the color using a modifier that needs to be applied inside the List
extension View {
func backgroundTableModifier(_ color: UIColor? = nil) -> some View {
self.modifier(BackgroundTableModifier(color: color))
}
}
Our task is to find the UITableView and after that change the color.
private struct BackgroundTableModifier: ViewModifier {
private let color: UIColor?
#State private var tableView: UITableView?
init(color: UIColor?) {
self.color = color
}
public func body(content: Content) -> some View {
if tableView?.backgroundColor != color {
content
.overlay(BackgroundTableViewRepresentable(tableBlock: { tableView in
tableView.backgroundColor = color
self.tableView = tableView
}))
} else {
content
}
}
}
private struct BackgroundTableViewRepresentable: UIViewRepresentable {
var tableBlock: (UITableView) -> ()
func makeUIView(context: Context) -> BackgroundTableView {
let view = BackgroundTableView(tableBlock: tableBlock)
return view
}
func updateUIView(_ uiView: BackgroundTableView, context: Context) {}
}
class BackgroundTableView: UIView {
var tableBlock: (UITableView) -> ()
init(tableBlock: #escaping (UITableView) -> ()) {
self.tableBlock = tableBlock
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
if let tableView = findTableView(in: self) {
tableBlock(tableView)
}
}
private func findTableView(in view: UIView) -> UITableView? {
if let tableView = view as? UITableView {
return tableView
}
if let superView = view.superview {
return findTableView(in: superView)
}
return nil
}
}
In order to find UITableView, the modifier must be inside the List. Naturally, you need to ensure that the modifier is called only once, you do not need to apply it to each row. Here is an example of usage
List {
rows()
.backgroundTableModifier(.clear)
}
func rows() -> some View {
ForEach(0..<10, id: \.self) { index in
Row()
}
}
In iOS 16, we got a native way to do this via scrollcontentbackground modifier.
You can either change the color by setting a color (ShapeStyle) to scrollcontentbackground.
List {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
.scrollContentBackground(Color.pink)
Or you can hide the background .scrollContentBackground(.hidden) and set a custom one with .backgroud modifier.
List {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
.background {
Image("ventura")
}
.scrollContentBackground(.hidden)
I've inspired some of the configurator used to config per page NavigationView nav bar style and write some simple UITableView per page configurator not use UITableView.appearance() global approach
import SwiftUI
struct TableViewConfigurator: UIViewControllerRepresentable {
var configure: (UITableView) -> Void = { _ in }
func makeUIViewController(context: UIViewControllerRepresentableContext<TableViewConfigurator>) -> UIViewController {
UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<TableViewConfigurator>) {
let tableViews = uiViewController.navigationController?.topViewController?.view.subviews(ofType: UITableView.self) ?? [UITableView]()
for tableView in tableViews {
self.configure(tableView)
}
}
}
Then there is UIView extension needed to find all UITableViews
extension UIView {
func subviews<T:UIView>(ofType WhatType:T.Type) -> [T] {
var result = self.subviews.compactMap {$0 as? T}
for sub in self.subviews {
result.append(contentsOf: sub.subviews(ofType:WhatType))
}
return result
}
}
And usage at the end is:
List {
}.background(TableViewConfigurator {
$0.backgroundColor = .red
})
Maybe one thing should be improved that is usage of navigationController?.topViewController to make it work even without navigationController in view controllers hierarchy
If anyone came here looking for solutions for background in landscape not full width on iPhone X/11 try:
.listRowBackground(Color("backgroundColour").edgesIgnoringSafeArea(.all))
If you want to avoid setting the appearance for all table views globally, you can combine UITableView.appearance(whenContainedInInstancesOf:) with UIHostingController. Thanks DanSkeel for the comment you left above pointing this out. This is how I used it:
public class ClearTableViewHostingController<Content>: UIHostingController<Content> where Content: View {
public override func viewDidLoad() {
UITableView.appearance(whenContainedInInstancesOf: [ClearTableViewHostingController<Content>.self]).backgroundColor = .clear
}
}
You can use ClearTableViewHostingController like this:
let view = MyListView()
let viewController = ClearTableViewHostingController(coder: coder, rootView: view)
Then in your view you can set the list background color like so:
List {
Text("Hello World")
}
.background(Color.gray)
Make extension List like:
extension List{
#available(iOS 14, *)
func backgroundList(_ color: Color = .clear) -> some View{
UITableView.appearance().backgroundColor = UIColor(color)
return self
}
}
you can use introspect library from Github to set the background color for the underlying table view like this:
List { ... } .introspectTableView { tableView in
tableView.backgroundColor = .yellow
}
For some reason color change is not working, you can try the .listStyle to .plain
Code:
struct ContentView: View {
var body: some View {
VStack {
Text("Test")
List {
ForEach(1 ..< 4) { items in
Text(String(items))
}
}
.listStyle(.plain)
}
}
Changing background did not work for me, because of the system background. I needed to hide it.
List(examples) { example in
ExampleRow(example: example)
}.background(Color.white.edgesIgnoringSafeArea(.all))
.scrollContentBackground(.hidden)
Xcode Version 12.4
The Background property worked for me, but with the mandatory use of Opacity.
Without opacity it is not work.
List {
ForEach(data, id: \.id) { (item) in
ListRow(item)
.environmentObject(self.data)
}
}
.background(Color.black)
.opacity(0.5)

Add modifier to all instances of view

I want to use my theme color on all instances of Toggle(), is there a way to do this with an extension?
extension Toggle {
func content() -> some View {
self.tint(.red)
}
}
The above is not working, is there something else I should call on the extension to modify all instances of Toggle?
This is exactly what .toggleStyle is designed for. Create your own custom ToggleStyle:
struct MyToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
// This just extends the default Toggle appearance, but you can return
// any View you like here. It doesn't have to call `Toggle` first.
Toggle(configuration)
.tint(.red) // Along with whatever other styles you like
}
}
extension ToggleStyle where Self == MyToggleStyle {
static var myToggleStyle: MyToggleStyle { .init() }
}
Then in your top-level ContentView, add the modifier:
.toggleStyle(.myToggleStyle)
Your style will be applied to all Toggles inside of your ContentView.
The best way to do this is to make a custom view with #ViewBuilder.
struct CustomToggle<Content: View>: View {
var isOn: Binding<Bool>
var label: Content
var body: some View {
Toggle(isOn: isOn) { label }
.tint(.red)
}
init(isOn: Binding<Bool>, #ViewBuilder label: #escaping () -> Content) {
self.isOn = isOn
self.label = label()
}
}
If you want to create a modifier to apply to an instance of Toggle(), can do that with the help of ViewModifiers.
i.e: First create a ViewModifier:
struct TintColorModifier: ViewModifier {
func body(content: Content) -> some View {
content
.tint(.red)
}
}
extension Toggle {
func tintStyle() -> some View {
modifier(TintColorModifier())
}
}
Now you can use the extension this way:
struct ContentView: View {
var body: some View {
Toggle()
.tintStyle() // <-- Here
}
}

SwiftUI: Button without Label/String in the initializer but with ButtonStyle

SwiftUI has a few Button initializers, but all of them require either a String or some View as the parameter alongside with the action.
However, the button's appearance can also be customized with the help of ButtonStyles which can add custom views to it.
Let's consider a Copy button with the following icon:
The style I've made for the button looks as follows:
struct CopyButtonStyle: ButtonStyle {
init() {}
func makeBody(configuration: Configuration) -> some View {
let copyIconSize: CGFloat = 24
return Image(systemName: "doc.on.doc")
.renderingMode(.template)
.resizable()
.frame(width: copyIconSize, height: copyIconSize)
.accessibilityIdentifier("copy_button")
.opacity(configuration.isPressed ? 0.5 : 1)
}
}
It works perfectly, however, I have to initialize the Button with an empty string at call site:
Button("") {
print("copy")
}
.buttonStyle(CopyButtonStyle())
So, the question is how can I get rid of the empty string in the button's initialization parameter?
Potential Solution
I was able to create a simple extension that accomplishes the job I need:
import SwiftUI
extension Button where Label == Text {
init(_ action: #escaping () -> Void) {
self.init("", action: action)
}
}
Call site:
Button() { // Note: no initializer parameter
print("copy")
}
.buttonStyle(CopyButtonStyle())
But curious, whether I'm using the Button struct incorrectly and there is already a use-case for that, so that I can get rid of this extension.
An easier way than making a ButtonStyle configuration is to pass in the label directly:
Button {
print("copy")
} label: {
Label("Copy", systemImage: "doc.on.doc")
.labelStyle(.iconOnly)
}
This also comes with some benefits:
By default, the button is blue to indicate it can be tapped
No weird stretching of the image that you currently have
No need to implement how the opacity changes when pressed
You could also refactor this into its own view:
struct CopyButton: View {
let action: () -> Void
var body: some View {
Button(action: action) {
Label("Copy", systemImage: "doc.on.doc")
.labelStyle(.iconOnly)
}
}
}
Called like so:
CopyButton {
print("copy")
}
Which looks much cleaner overall.
Here is a right way for what you are trying to do, you do not need make a new ButtonStyle for each kind of Button, you can create just one and reuse it for any other Buttons you want. Also I solved your Image stretching issue with .scaledToFit().
struct CustomButtonView: View {
let imageString: String
let size: CGFloat
let identifier: String
let action: (() -> Void)?
init(imageString: String, size: CGFloat = 24.0, identifier: String = String(), action: (() -> Void)? = nil) {
self.imageString = imageString
self.size = size
self.identifier = identifier
self.action = action
}
var body: some View {
return Button(action: { action?() } , label: {
Image(systemName: imageString)
.renderingMode(.template)
.resizable()
.scaledToFit()
.frame(width: size, height: size)
.accessibilityIdentifier(identifier)
})
.buttonStyle(CustomButtonStyle())
}
}
struct CustomButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
return configuration.label
.opacity(configuration.isPressed ? 0.5 : 1.0)
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
}
}
use case:
struct ContentView: View {
var body: some View {
CustomButtonView(imageString: "doc.on.doc", identifier: "copy_button", action: { print("copy") })
}
}
You can use EmptyView for label, like
Button(action: { // Note: no initializer parameter
print("copy")
}, label: { EmptyView() })
.buttonStyle(CopyButtonStyle())
but wrapping it in custom button type (like shown in other answer) is more preferable from re-use and code readability point of view.

How to check if a view is displayed on the screen? (Swift 5 and SwiftUI)

I have a view like below. I want to find out if it is the view which is displayed on the screen. Is there a function to achieve this?
struct TestView: View {
var body: some View {
Text("Test View")
}
}
You could use onAppear on any kind of view that conforms to View protocol.
struct TestView: View {
#State var isViewDisplayed = false
var body: some View {
Text("Test View")
.onAppear {
self.isViewDisplayed = true
}
.onDisappear {
self.isViewDisplayed = false
}
}
func someFunction() {
if isViewDisplayed {
print("View is displayed.")
} else {
print("View is not displayed.")
}
}
}
PS: Although this solution covers most cases, it has many edge cases that has not been covered. I'll be updating this answer when Apple releases a better solution for this requirement.
You can check the position of view in global scope using GeometryReader and GeometryProxy.
struct CustomButton: View {
var body: some View {
GeometryReader { geometry in
VStack {
Button(action: {
}) {
Text("Custom Button")
.font(.body)
.fontWeight(.bold)
.foregroundColor(Color.white)
}
.background(Color.blue)
}.navigationBarItems(trailing: self.isButtonHidden(geometry) ?
HStack {
Button(action: {
}) {
Text("Custom Button")
} : nil)
}
}
private func isButtonHidden(_ geometry: GeometryProxy) -> Bool {
// Alternatively, you can also check for geometry.frame(in:.global).origin.y if you know the button height.
if geometry.frame(in: .global).maxY <= 0 {
return true
}
return false
}
As mentioned by Oleg, depending on your use case, a possible issue with onAppear is its action will be performed as soon as the View is in a view hierarchy, regardless of whether the view is potentially visible to the user.
My use case is wanting to lazy load content when a view actually becomes visible. I didn't want to rely on the view being encapsulated in a LazyHStack or similar.
To achieve this I've added an extension onBecomingVisible to View that has the same kind of API as onAppear, but will only call the action when the view intersects the screen's visible bounds.
public extension View {
func onBecomingVisible(perform action: #escaping () -> Void) -> some View {
modifier(BecomingVisible(action: action))
}
}
private struct BecomingVisible: ViewModifier {
#State var action: (() -> Void)?
func body(content: Content) -> some View {
content.overlay {
GeometryReader { proxy in
Color.clear
.preference(
key: VisibleKey.self,
// See discussion!
value: UIScreen.main.bounds.intersects(proxy.frame(in: .global))
)
.onPreferenceChange(VisibleKey.self) { isVisible in
guard isVisible else { return }
action?()
action = nil
}
}
}
}
struct VisibleKey: PreferenceKey {
static var defaultValue: Bool = false
static func reduce(value: inout Bool, nextValue: () -> Bool) { }
}
}
Discussion
I'm not thrilled by using UIScreen.main.bounds in the code! Perhaps a geometry proxy could be used for this instead, or some #Environment value – I've not thought about this yet though.

SwiftUI - Close Keyboard on Scroll

I have a simple search list:
struct ContentView: View {
#State var text:String = ""
var items = 1...100
var body: some View {
VStack {
List {
TextField("Search", text: $text)
Section{
ForEach(items.filter({"\($0)".contains(text)}),id: \.self){(i) in
Text("option \(i)")
}
}
}
}
}
}
How can I make the keyboard close when scrolling for more than 2 cells/few points?
If you are using a ScrollView (probably also with a List but I haven't confirmed it), you could use the UIScrollView appearance, this will affect all ScrollViews though.
UIScrollView.appearance().keyboardDismissMode = .onDrag
A thorough discussion on how to resign the keyboard with various answers can be found for this question.
One solution to resign the keyboard on a drag gesture in the list is using a method on UIApplication window as shown below. For easier handling I created an extension on UIApplication and view modifier for this extension and finally an extension to View:
extension UIApplication {
func endEditing(_ force: Bool) {
self.windows
.filter{$0.isKeyWindow}
.first?
.endEditing(force)
}
}
struct ResignKeyboardOnDragGesture: ViewModifier {
var gesture = DragGesture().onChanged{_ in
UIApplication.shared.endEditing(true)
}
func body(content: Content) -> some View {
content.gesture(gesture)
}
}
extension View {
func resignKeyboardOnDragGesture() -> some View {
return modifier(ResignKeyboardOnDragGesture())
}
}
So the final modifier for resigning the keyboard is just one modifier that has to be placed on the list like this:
List {
ForEach(...) {
//...
}
}
.resignKeyboardOnDragGesture()
I have also implemented a pure swiftUI version of a search bar that might be interesting for you. You can find it in this answer.
As for now, since iOS 16 beta we have a new modifier scrollDismissesKeyboard() that allows to do exactly what you need.
In your example it should look like
struct ContentView: View {
#State var text: String = ""
var items = 1...100
var body: some View {
List {
TextField("Search", text: $text)
Section {
ForEach(items.filter({"\($0)".contains(text)}), id: \.self) { (i) in
Text("option \(i)")
}
}
}
.scrollDismissesKeyboard(.interactively) // <<-- Put this line
}
}
The scrollDismissesKeyboard() modifier has a parameter that determine the dismiss rules. Here are the possible values:
.automatic: Dismissing based on the context of the scroll.
.immediately: The keyboard will be dismissed as soon as any scroll happens.
.interactively: The keyboard will move/disappear inline with the user’s gesture.
.never: The keyboard will never dismissed when user is scrolling.
Form {
...
}.gesture(DragGesture().onChanged { _ in
UIApplication.shared.windows.forEach { $0.endEditing(false) }
})
#FocusState wrapper along with .focused() TextField modifier can be useful.
struct ContentView: View {
#FocusState private var focusedSearchField: Bool
#State var text:String = ""
var items = 1...100
var body: some View {
VStack {
List {
TextField("Search", text: $text)
.focused($focusedSearchField)
Section{
ForEach(items.filter({"\($0)".contains(text)}),id: \.self){(i) in
Text("option \(i)")
}
}
} // to also allow swipes on items (theoretically)
.simultaneousGesture(DragGesture().onChanged({ _ in
focusedSearchField = false
}))
.onTapGesture { // dissmis on tap as well
focusedSearchField = false
}
}
}
}
struct EndEditingKeyboardOnDragGesture: ViewModifier {
func body(content: Content) -> some View {
content.highPriorityGesture (
DragGesture().onChanged { _ in
UIApplication.shared.endEditing()
}
)
}
}
extension View {
func endEditingKeyboardOnDragGesture() -> some View {
return modifier(EndEditingKeyboardOnDragGesture())
}
}

Resources