How to Hide Keyboard in SwiftUI Form Containing Picker? - swiftui

I have a SwiftUI Form that contains a Picker, a TextField, and a Text:
struct ContentView: View {
var body: some View {
Form {
Section {
Picker(selection: $selection, label: label) {
// Code to populate picker
}.pickerStyle(SegmentedPickerStyle())
HStack {
TextField(title, text: $text)
Text(text)
}
}
}
}
}
The code above results in the following UI:
I am able to easily select the second item in the picker, as shown below:
Below, you can see that I am able to initiate text entry by tapping on the TextField:
In order to dismiss the keyboard when the Picker value is updated, a Binding was added, which can be seen in the following code block:
Picker(selection: Binding(get: {
// Code to get selected segment
}, set: { (index) in
// Code to set selected segment
self.endEditing()
}), label: label) {
// Code to populate picker
}
The call to self.endEditing() is provided in the following method:
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
The following screenshot displays that selecting a different segment of the Picker dismisses the keyboard:
Up to this point, everything works as expected. However, I would like to dismiss the keyboard when tapping anywhere outside of the TextField since I am unable to figure out how to dismiss the keyboard when dragging the Form's containing scroll view.
I attempted to add the following implementation to dismiss the keyboard when tapping on the Form:
Form {
Section {
// Picker
HStack {
// TextField
// Text
}
}
}.onTapGesture {
self.endEditing()
}
Below, the following two screenshot displays that the TextField is able to become the first responder and display the keyboard. The keyboard is then successfully dismissed when tapping outside of the TextField:
However, the keyboard will not dismiss when attempting to select a different segment of the `Picker. In fact, I cannot select a different segment, even after the keyboard has been dismissed. I presume that a different segment cannot be selected because the tap gesture attached to the form is preventing the selection.
The following screenshot shows the result of attempting to select the second value in the Picker while the keyboard is shown and the tap gesture is implemented:
What can I do to allow selections of the Picker's segments while allowing the keyboard to be dismissed when tapping outside of the TextField?

import SwiftUI
struct ContentView: View {
#State private var tipPercentage = 2
let tipPercentages = [10, 15, 20, 25, 0]
#State var text = ""
#State var isEdited = false
var body: some View {
Form {
Section {
Picker("Tip percentage", selection: $tipPercentage) {
ForEach(0 ..< tipPercentages.count) {
Text("\(self.tipPercentages[$0])%")
}
}
.pickerStyle(SegmentedPickerStyle())
HStack {
TextField("Amount", text: $text, onEditingChanged: { isEdited in
self.isEdited = isEdited
}).keyboardType(.numberPad)
}
}
}.gesture(TapGesture().onEnded({
UIApplication.shared.windows.first{$0.isKeyWindow }?.endEditing(true)
}), including: isEdited ? .all : .none)
}
}
Form's tap gesture (to finish editing by tap anywhere) is enabled only if text field isEdited == true
Once isEdited == false, your picker works as before.

You could place all of your code in an VStack{ code }, add a Spacer() to it and add the onTap to this VStack. This will allow you to dismiss the keyboard by clicking anywhere on the screen.
See code below:
import SwiftUI
struct ContentView: View {
#State private var text: String = "Test"
var body: some View {
VStack {
HStack {
TextField("Hello World", text: $text)
Spacer()
}
Spacer()
}
.background(Color.red)
.onTapGesture {
self.endEditing()
}
}
func endEditing() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
Changing the background color of an HStack or VStack to red simplifies figuring out where the user may click to dismiss.
Copy and paste code for a ready to run example.

Related

How to get a horizontal ScrollView in SwiftUI to scroll automatically to end when the enclosed Text is updated

I am writing a SwiftUI iOS app where I need a Text view to automatically scroll to the end of its content whenever the content is updated. The update happens from the model. To not complicate this question with the details of my app, I have created a simple scenario where I have two text fields and a text label. Any text entered in the text fields is concatenated and shown in the text label. The text label is enclosed in a horizontal ScrollView and can be scrolled manually if the text is longer than the screen width. What I want to achieve is for the text to scroll to the end automatically whenever the label is updated.
Here is the simple model code:
class Model: ObservableObject {
var firstString = "" {
didSet { combinedString = "\(firstString). \(secondString)." }
}
var secondString = "" {
didSet { combinedString = "\(firstString). \(secondString)." }
}
#Published var combinedString = ""
}
This is the ContentView:
struct ContentView: View {
#ObservedObject var model: Model
var body: some View {
VStack(alignment: .leading, spacing: 10) {
TextField("First string: ", text: $model.firstString)
TextField("Second string: ", text: $model.secondString)
Spacer().frame(height: 20)
Text("Combined string:")
ScrollView(.horizontal) {
Text(model.combinedString)
}
}
}
}
From the research I have done, the only way I have found to scroll to the end of the text, without having to do it manually, is to add a button to the view, which causes the text in the label to scroll to the end.
Here is the above ScrollView embedded in a ScrollViewReader, with a button to effect the scrolling action.
ScrollViewReader { scrollView in
VStack {
ScrollView(.horizontal) {
Text(model.combinedString)
.id("combinedText")
}
Button("Scroll to end") {
withAnimation {
scrollView.scrollTo("combinedText", anchor: .trailing)
}
}
.padding()
.foregroundColor(.white)
.background(Color.black)
}
}
This works, provided the intention is to use a button to effect the scrolling action.
My question is: Can the scrolling action above be triggered whenever the model is updated, without the need to click a button.
Any help or pointers will be much appreciated.
Thanks.
I assume you wanted this:
ScrollViewReader { scrollView in
VStack {
ScrollView(.horizontal) {
Text(model.combinedString)
.id("combinedText")
}
.onChange(of: model.combinedString) { // << here !!
withAnimation {
scrollView.scrollTo("combinedText", anchor: .trailing)
}
}
}
}
ScrollViewReader is the solution you're looking for. You may need to play around with the value. Also you'll need to add the .id(0) modifier to your textview.
ScrollView {
ScrollViewReader { reader in
Button("Go to first then anchor trailing.") {
value.scrollTo(0, anchor: .trailing)
}
// The rest of your code .......

Two NavigationBar Items appears twice in the second current view

I have a current SwiftUI View with a Navigation Bar item . I created a Back Button on this current view so that the title is not inherited from the previous view (NEWSview) with list of news websites to navigate to the current view and clicking on the back button returns me to the previous view. This is sample code of the previous view screen how i did:
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
NavigationView {
List (networkManager.posts) { post in
NavigationLink (destination: DetailView(url: post.url) ) {
HStack {
Text(String(post.points))
Text(post.title)
}
}
}
.navigationBarTitle("News")
}
.onAppear {
DispatchQueue.main.async {
self.networkManager.fetchData()
}
}
}
}
This is the current screen view that is having the two buttons:
struct DetailView: View {
let url : String?
#Environment(\.presentationMode) private var mode
var body: some View {
WebView(urlString: url)
.navigationBarItems(leading:
Button(action: {
self.mode.wrappedValue.dismiss()
}, label: {
HStack(spacing: 10) {
Image(systemName: "chevron.left")
.resizable()
.frame(width: 10.0, height: 19.0)
.font(.headline)
Text("Back")
}
})
)
}
}
Now the issue is : The current screen NavBar has two buttons like this : one with news title and the back button (Picture attached) . I want only to remain with the Back Button on the current view that on click will navigate back to the previous root view . I thought what i did is right but missing something . If remove the entire code with the button in the current view , i will only see the "NEWS" navigation that on click will return me to the News View but i want a back title that should not be inherited from the news view .

How can I achieve only one possible selection similar to radiogroup in SwiftUI

How can I achieve only one single selection similar to a group of radio buttons using a list of views in SwiftUI?
When a button is pressed, you can store the value for which one was selected.
And you can style the buttons based on which one is selected.
The following code should do what you're looking for. Whichever button is last pressed will be selected, and only the selected button will be blue, because the styling is based on the property. And another button clears the selection.
struct ContentView: View {
let buttons = ["A", "B", "C"]
#State public var buttonSelected: Int?
var body: some View {
VStack(spacing: 20) {
ForEach(0..<buttons.count) { button in
Button(action: {
self.buttonSelected = button
}) {
Text("Button \(self.buttons[button])")
.padding()
.foregroundColor(.white)
.background(self.buttonSelected == button ? Color.blue : Color.green)
.clipShape(Capsule())
}
}
Button(action: {
self.buttonSelected = nil
}) {
Text("Clear Selection")
}
}
}
}

Scroll up to see TextField when the keyboard appears in SwiftUI

In my use case, I have to put a TextField below the available items in a List and by using that TextField, we can add items to the List.
Initially, there're no list items (items array is empty)
Here's a minimal, reproducible example
import SwiftUI
struct ContentView: View {
#State var itemName = ""
#State var items = [String]()
var body: some View {
NavigationView {
List {
ForEach(self.items, id: \.self) {
Text($0)
}
VStack {
TextField("Item Name", text: $itemName)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
self.items.append(self.itemName)
self.itemName = ""
}) {
Text("Add Item")
}
}
}
.navigationBarTitle(Text("Title"))
}
}
}
We can add a new item to the list by typing something in the TextField and clicking "Add Item" Button , Every item that we add using TextField appears above the TextField in the List. So the TextField goes down in the List (Just like Apple’s Reminders app).
If the app has many items (more than 7 items), the keyboard covers the TextField when the keyboard appears and we can’t see the TextField.
Check this screenshot:
What I want to know is how to automatically scroll the List (move the view up) to see the TextField when keyboard appears (like in Apple's Reminders app).
I had a similar problem in my recent project, the easiest way for me to solve it was to wrap UITextField in SwiftUI and from my custom wrapper reach to the parent scroll view and tell it to scroll when the keyboard appears. I tried my approach on your project and it seems to work.
If you take my code for the wrapper and other files from this GitHub folder: https://github.com/LostMoa/SwiftUI-Code-Examples/tree/master/ScrollTextFieldIntoVisibleRange and then replace the SwiftUI TextField with my custom view (TextFieldWithKeyboardObserver) then it should scroll.
import SwiftUI
struct ContentView: View {
#State var itemName = ""
#State var items = [String]()
var body: some View {
NavigationView {
List {
ForEach(self.items, id: \.self) {
Text($0)
}
VStack {
TextFieldWithKeyboardObserver(text: $itemName, placeholder: "Item Name")
Button(action: {
self.items.append(self.itemName)
self.itemName = ""
}) {
Text("Add Item")
}
}
}
.navigationBarTitle(Text("Title"))
}
}
}
I recently wrote an article explaining this solution: https://lostmoa.com/blog/ScrollTextFieldIntoVisibleRange/

Toggle a button on Navigation Bar in SwiftUI & have it change appearance

Using SwiftUI, I would like to have the ability to change the button on my NavigationView based upon some Bool value indicating if it should be On or Off.
This would behave similar to how with UIKit you can replace a bar button item on either side of the screen to show a different button & associated action upon clicking.
I am able to get it working with the following code, but I am not certain if this is the best way to accomplish it, so am open to improvement.
import SwiftUI
struct HomeList: View {
#State var isOn = true
var body: some View {
NavigationView {
List(1 ..< 4) { index in
Text("Row \(index)")
}
.navigationBarTitle(Text(verbatim: "Title"), displayMode: .inline)
.navigationBarItems(trailing:
Button(action: {
self.isOn = !self.isOn
}, label: {
Text(self.isOn ? "On" : "Off")
})
)
}
}
}
The key pieces being:
Using the #State modifier on my isOn variable, which tells my interface to invalidate & re-render upon changes
Having my Button action modify isOn &it can also support other actions if I need
The ternary operator in my Button label that updates the Text (or an Image if I want) to reflect the correct appearance
Finally, how it appears in action: