I am new in SwiftUi and I am trying to show Alert message with TextField and action button, I want to use Alert function for this, but I can't add TextField in Alert function,I shared code at below, how can I add TextField in Alert function? thanks..
.alert(isPresented:$showAlertDialog) {
Alert(
title: Text("Please Enter sended E-mail Code"),
message: Text("There is no undo"),
primaryButton: .destructive(Text("Submit")) {
print("Deleting...")
},
secondaryButton: .cancel()
)
}
As of iOS 15 alerts are not capable of showing TextFields. But you can make a custom SwiftUI alert.
struct TextFieldAlert: ViewModifier {
#Binding var isPresented: Bool
let title: String
#Binding var text: String
let placeholder: String
let action: (String) -> Void
func body(content: Content) -> some View {
ZStack(alignment: .center) {
content
.disabled(isPresented)
if isPresented {
VStack {
Text(title).font(.headline).padding()
TextField(placeholder, text: $text).textFieldStyle(.roundedBorder).padding()
Divider()
HStack{
Spacer()
Button(role: .cancel) {
withAnimation {
isPresented.toggle()
}
} label: {
Text("Cancel")
}
Spacer()
Divider()
Spacer()
Button() {
action(text)
withAnimation {
isPresented.toggle()
}
} label: {
Text("Done")
}
Spacer()
}
}
.background(.background)
.frame(width: 300, height: 200)
.cornerRadius(20)
.overlay {
RoundedRectangle(cornerRadius: 20)
.stroke(.quaternary, lineWidth: 1)
}
}
}
}
}
extension View {
public func textFieldAlert(
isPresented: Binding<Bool>,
title: String,
text: Binding<String>,
placeholder: String = "",
action: #escaping (String) -> Void
) -> some View {
self.modifier(TextFieldAlert(isPresented: isPresented, title: title, text: text, placeholder: placeholder, action: action))
}
}
Use it on the root view. (don't attach it to Buttons to other inner elements for example). This will ensure that presenting view is disabled.
Sample usage:
struct ContentView: View {
#State var isAlertDisplayed = false
#State var text = "Text to change"
var body: some View {
VStack {
Text("Press the button to show the alert").multilineTextAlignment(.center)
Text("Current value is: \(text)")
Button("Change value") {
withAnimation {
isAlertDisplayed = true
}
}
.padding()
}
.textFieldAlert(isPresented: $isAlertDisplayed, title: "Some title", text: $text, placeholder: "Placeholder", action: { text in
print(text)
})
.padding()
}
}
Related
I am using a picker. Onclick textField, it shows the picker menu. But picker options show the top of the textField. Onclick textField toggle shows
Here is the image
Here is my code:
struct PickerView: View {
#State private var text:String = ""
#State private var options = ["Option 1", "Option 2", "Option 3"]
#State private var selectedOption = "Option 1"
#State private var showPicker: Bool = false
var body: some View {
VStack {
HStack {
TextField("", text: $selectedOption)
.disabled(true)
Image(systemName: "chevron.down")
.foregroundColor(.gray)
}
.padding()
.background(Color.gray)
.cornerRadius(5.0)
Picker("Options", selection: $selectedOption) {
ForEach(options, id: \.self) { option in
Text(option).tag(option)
}
}
.pickerStyle(.automatic)
.padding()
.background(Color.white)
.cornerRadius(5.0)
.shadow(radius: 5)
.offset(y: -100)
.opacity(showPicker ? 1 : 0)
.animation(.default)
}
.onTapGesture {
self.showPicker.toggle()
}
}
}
How to show the picker onClick textField directly like drop down?
Please help me..
This might be what you wanted
var body: some View {
VStack(alignment: .leading) {
HStack {
Picker("Options", selection: $selectedOption) {
ForEach(options, id: \.self) { option in
Text(option).tag(option)
}
}
.tint(.black)
.pickerStyle(.menu)
Spacer()
}
.background(.gray)
.cornerRadius(5.0)
}
}
Not sure how to add an optional action to TextFieldButton view and have the TextFieldClearButton view modifier accept the action.
struct TextFieldClearButton: ViewModifier {
#Binding var fieldText: String
var action: (() -> Void)? = nil
func body(content: Content) -> some View {
content
.overlay {
if !fieldText.isEmpty {
HStack {
Spacer()
Button {
fieldText = ""
action
} label: {
Image(systemName: "multiply.circle.fill")
}
.foregroundColor(.secondary)
.padding(.trailing, 4)
}
}
}
}
}
extension View {
func showClearButton(_ text: Binding<String>) -> some View {
self.modifier(TextFieldClearButton(fieldText: text))
}
}
struct TextFieldButton: View {
#State private var text = ""
#FocusState private var isTextFieldFocused: Bool
var body: some View {
VStack {
TextField("", text: $text)
.textFieldStyle(.roundedBorder)
.focused($isTextFieldFocused)
.showClearButton($text)
}
.padding()
.background(Color.purple)
}
}
So far I can only get an "Expression of type '(() -> Void)?' is unused" warning and I am not sure how or if this needs to passed in as a #Binding.
Here is the full working version of this code:
#Binding var fieldText: String
var action: (() -> Void)? = nil
func body(content: Content) -> some View {
content
.overlay {
if !fieldText.isEmpty {
HStack {
Spacer()
Button {
fieldText = ""
action?()
} label: {
Image(systemName: "multiply.circle.fill")
}
.foregroundColor(.secondary)
.padding(.trailing, 4)
}
}
}
}
}
extension View {
func showClearButton(_ text: Binding<String>, action: (() -> Void)? = nil) -> some View {
self.modifier(TextFieldClearButton(fieldText: text, action: action))
}
}
struct TextFieldButton: View {
#State private var text = ""
#FocusState private var isTextFieldFocused: Bool
var body: some View {
VStack {
TextField("", text: $text)
.textFieldStyle(.roundedBorder)
.focused($isTextFieldFocused)
.showClearButton($text, action: testPrint)
}
.padding()
.background(Color.purple)
}
func testPrint() {
print("Test Print Successful.")
}
}
action is a property of type (() -> Void)? You need to call the action closure, you can do this by changing it from action to action?()
Button {
fieldText = ""
action?()
} label: {
Image(systemName: "multiply.circle.fill")
}
My Custom button does not tap and passes to next view called AddCreditCardView.
I have tested the button action with print statement and it won't work too.
I copied my code below in separate.
This is my ContentView
import SwiftUI
struct ContentView: View {
let membershipRows = MembershipData.listData()
let corporateRows = CorporateData.listData()
let otherOperationRows = OtherOperationsData.listData()
#State var selectedCard = CreditCard(id: "", cardOwnerName: "", cardNumber: "", cardExpMonth: "", cardExpYear: "", ccv: "")
#State var shown: Bool = false
var body: some View {
NavigationView {
VStack {
List {
Section(header: Text("Bireysel")) {
ForEach(membershipRows) { row in
NavigationLink(destination: CreditCardView()) {
RowElementView(row: row)
}
}
}
if self.corporateRows.count == 0
{
Rectangle()
.background(Color(.white))
.edgesIgnoringSafeArea(.all)
.foregroundColor(.white)
.padding(.vertical,32)
}
else {
Section(header: Text("Kurumsal")) {
ForEach(corporateRows) { row in
RowElementView(row: row)
}
}
}
Section(header: Text("Diger Islemler")) {
ForEach(otherOperationRows) { row in
RowElementView(row: row)
}
}
Rectangle()
.foregroundColor(.clear)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height )
}
.navigationBarTitle("Odeme Yontemleri", displayMode: .inline)
.font(Font.custom("SFCompactDisplay", size: 16))
Button(action: {
AddCreditCardView(item: self.selectedCard)
}, label: { CustomButton(title: "Odeme Yontemi Ekle", icon: .none, status: .enable)
})
}
}
}
This is my AddCreditCardView
import SwiftUI
struct AddCreditCardView: View {
var item: CreditCard
var body: some View {
NavigationView {
VStack {
TopBar()
Spacer()
CardInfo()
Spacer()
}
.navigationBarTitle("Odeme Yontemi", displayMode: .inline)
}
}
}
struct TopBar : View {
var body: some View {
VStack {
HStack() {
Image("addcreditcard")
Image("line")
Image("locationBar")
Image("line")
Image("check-circle")
}
.padding(.horizontal,62)
VStack {
Text("Kredi Karti Ekle")
.font(Font.custom("SFCompactDisplay-Bold", size: 14))
Text("1. Adim")
.font(Font.custom("SFCompactDisplay", size: 14))
.fontWeight(.regular)
.foregroundColor(.gray)
}
}
.padding()
}
}
struct CardInfo : View {
var body: some View {
VStack {
CustomTextField(tFtext: "Kartin Uzerindeki Isim", tFImage: "user")
.textContentType(.givenName)
CustomTextField(tFtext: "Kredi Kart Numarasi", tFImage: "credit")
.textContentType(.oneTimeCode)
.keyboardType(.numberPad)
HStack {
CreditCardDateTextField(tFtext: "", tFImage: "date")
.textContentType(.creditCardNumber)
Spacer()
Text("|")
.foregroundColor(.black)
.overlay(
Rectangle()
.frame(width: 60, height: 53))
CustomTextField(tFtext: "CCV", tFImage: "")
.textContentType(.creditCardNumber)
}
.foregroundColor(Color(#colorLiteral(red: 0.9647058824, green: 0.9725490196, blue: 0.9882352941, alpha: 1)))
CustomTextField(tFtext: "Kart Ismi", tFImage: "cardEdit")
Spacer()
}
}
}
And Finally, this is my CreditCard Model
import SwiftUI
struct CreditCard: Identifiable {
var id: String = UUID().uuidString
var cardOwnerName : String
var cardNumber: String
var cardExpMonth: String
var cardExpYear: String
var ccv: String
Seems like you are trying to navigate to AddCreditCardView on the button press. The action closure can not present a view automatically like that! You should change that code to something like this:
#State var navigated = false
,,,
NavigationLink("AddCreditCardView", destination: AddCreditCardView(), isActive: $navigated)
Button(action: { self.navigated.toggle() },
label: { CustomButton(title: "Odeme Yontemi Ekle", icon: .none, status: .enable) })
changing the navigated state will show the next page as it seems you wished.
I made a SearchBarView view to use in various other views (for clarity, I removed all the layout modifiers, such as color and padding):
struct SearchBarView: View {
#Binding var text: String
#State private var isEditing = false
var body: some View {
HStack {
TextField("Search…", text: $text, onCommit: didPressReturn)
.overlay(
HStack {
Image(systemName: "magnifyingglass")
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
if isEditing {
Button(action: {
self.text = ""
}) {
Image(systemName: "multiply.circle.fill")
}
}
}
)
}
func didPressReturn() {
print("did press return")
}
}
It looks and works great to filter data in a List.
But now I'd like to use the SearchBarView to search an external database.
struct SearchDatabaseView: View {
#Binding var isPresented: Bool
#State var searchText: String = ""
var body: some View {
NavigationView {
VStack {
SearchBarView(text: $searchText)
// need something here to respond to onCommit and initiate a network call.
}
.navigationBarTitle("Search...")
.navigationBarItems(trailing:
Button(action: { self.isPresented = false }) {
Text("Done")
})
}
}
}
For this, I only want to start the network access when the user hits return. So I added the onCommit part to SearchBarView, and the didPressReturn() function is indeed only called when tapping return. So far, so good.
What I don't understand is how SearchDatabaseView that contains the SearchBarView can respond to onCommit and initiate the database searh - how do I do that?
Here is possible approach
struct SearchBarView: View {
#Binding var text: String
var onCommit: () -> () = {} // inject callback
#State private var isEditing = false
var body: some View {
HStack {
TextField("Search…", text: $text, onCommit: didPressReturn)
.overlay(
HStack {
Image(systemName: "magnifyingglass")
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
if isEditing {
Button(action: {
self.text = ""
}) {
Image(systemName: "multiply.circle.fill")
}
}
}
)
}
func didPressReturn() {
print("did press return")
// do internal things...
self.onCommit() // << external callback
}
}
so now in SearchDatabaseView you can
VStack {
SearchBarView(text: $searchText) {
// do needed things here ...
}
}
SwiftUI layout is very different from what we are used to. Currently I'm fighting against TextFields. Specifically their touchable Area.
TextField(
.constant(""),
placeholder: Text("My text field")
)
.padding([.leading, .trailing])
.font(.body)
This results in a very small TextField (height wise)
Adding the frame modifier fixes the issue (visually)
TextField(
.constant(""),
placeholder: Text("My text field")
).frame(height: 60)
.padding([.leading, .trailing])
.font(.body)
but the touchable area remains the same.
I'm aware of the fact that the frame modifier does nothing else other than wrap the textField in another View with the specified height.
Is there any equivalent to resizable() for Image that will allow a taller TextField with wider touchable Area?
This solution only requires a #FocusState and an onTapGesture, and allows the user to tap anywhere, including the padded area, to focus the field. Tested with iOS 15.
struct MyView: View {
#Binding var text: String
#FocusState private var isFocused: Bool
var body: some View {
TextField("", text: $text)
.padding()
.background(Color.gray)
.focused($isFocused)
.onTapGesture {
isFocused = true
}
}
}
Bonus:
If you find yourself doing this on several text fields, making a custom TextFieldStyle will make things easier:
struct TappableTextFieldStyle: TextFieldStyle {
#FocusState private var textFieldFocused: Bool
func _body(configuration: TextField<Self._Label>) -> some View {
configuration
.padding()
.focused($textFieldFocused)
.onTapGesture {
textFieldFocused = true
}
}
}
Then apply it to your text fields with:
TextField("", text: $text)
.textFieldStyle(TappableTextFieldStyle())
Solution with Button
If you don't mind using Introspect you can do it by saving the UITextField and calling becomeFirstResponder() on button press.
extension View {
public func textFieldFocusableArea() -> some View {
TextFieldButton { self.contentShape(Rectangle()) }
}
}
fileprivate struct TextFieldButton<Label: View>: View {
init(label: #escaping () -> Label) {
self.label = label
}
var label: () -> Label
private var textField = Weak<UITextField>(nil)
var body: some View {
Button(action: {
self.textField.value?.becomeFirstResponder()
}, label: {
label().introspectTextField {
self.textField.value = $0
}
}).buttonStyle(PlainButtonStyle())
}
}
/// Holds a weak reference to a value
public class Weak<T: AnyObject> {
public weak var value: T?
public init(_ value: T?) {
self.value = value
}
}
Example usage:
TextField(...)
.padding(100)
.textFieldFocusableArea()
Since I use this myself as well, I will keep it updated on github: https://gist.github.com/Amzd/d7d0c7de8eae8a771cb0ae3b99eab73d
New solution using ResponderChain
The Button solution will add styling and animation which might not be wanted therefore I now use a new method using my ResponderChain package
import ResponderChain
extension View {
public func textFieldFocusableArea() -> some View {
self.modifier(TextFieldFocusableAreaModifier())
}
}
fileprivate struct TextFieldFocusableAreaModifier: ViewModifier {
#EnvironmentObject private var chain: ResponderChain
#State private var id = UUID()
func body(content: Content) -> some View {
content
.contentShape(Rectangle())
.responderTag(id)
.onTapGesture {
chain.firstResponder = id
}
}
}
You'll have to set the ResponderChain as environment object in the SceneDelegate, check the README of ResponderChain for more info.
Solution Without Any 3rd Parties
Increasing the tappable area can be done without third parties:
Step1: Create a modified TextField. This is done so we can define the padding of our new TextField:
Code used from - https://stackoverflow.com/a/27066764/2217750
class ModifiedTextField: UITextField {
let padding = UIEdgeInsets(top: 20, left: 5, bottom: 0, right: 5)
override open func textRect(forBounds bounds: CGRect) -> CGRect {
bounds.inset(by: padding)
}
override open func placeholderRect(forBounds bounds: CGRect) -> CGRect {
bounds.inset(by: padding)
}
override open func editingRect(forBounds bounds: CGRect) -> CGRect {
bounds.inset(by: padding)
}
}
Step 2: Make the new ModifiedTexField UIViewRepresentable so we can use it SwiftUI:
struct EnhancedTextField: UIViewRepresentable {
#Binding var text: String
init(text: Binding<String>) {
self._text = text
}
func makeUIView(context: Context) -> ModifiedTextField {
let textField = ModifiedTextField(frame: .zero)
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: ModifiedTextField, context: Context) {
uiView.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
let parent: EnhancedTextField
init(_ parent: EnhancedTextField) {
self.parent = parent
}
func textFieldDidChangeSelection(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}
}
Step3: Use the new EnhancedTextField wherever needed:
EnhancedTextField(placeholder: placeholder, text: $binding)
Note: To increase or decrease the tappable area just change the padding in ModifiedTextField
let padding = UIEdgeInsets(top: 20, left: 5, bottom: 0, right: 5)
A little work around but works.
struct CustomTextField: View {
#State var name = ""
#State var isFocused = false
let textFieldsize : CGFloat = 20
var textFieldTouchAbleHeight : CGFloat = 200
var body: some View {
ZStack {
HStack{
Text(name)
.font(.system(size: textFieldsize))
.lineLimit(1)
.foregroundColor(isFocused ? Color.clear : Color.black)
.disabled(true)
Spacer()
}
.frame(alignment: .leading)
TextField(name, text: $name , onEditingChanged: { editingChanged in
isFocused = editingChanged
})
.font(.system(size: isFocused ? textFieldsize : textFieldTouchAbleHeight ))
.foregroundColor(isFocused ? Color.black : Color.clear)
.frame( height: isFocused ? 50 : textFieldTouchAbleHeight , alignment: .leading)
}.frame(width: 300, height: textFieldTouchAbleHeight + 10,alignment: .leading)
.disableAutocorrection(true)
.background(Color.white)
.padding(.horizontal,10)
.padding(.vertical,10)
.border(Color.red, width: 2)
}
}
I don't know which is better for you.
so, I post two solution.
1) If you want to shrink only input area.
var body: some View {
Form {
HStack {
Spacer().frame(width: 30)
TextField("input text", text: $inputText)
Spacer().frame(width: 30)
}
}
}
2) shrink a whole form area
var body: some View {
HStack {
Spacer().frame(width: 30)
Form {
TextField("input text", text: $restrictInput.text)
}
Spacer().frame(width: 30)
}
}
iOS 15 Solution with TextFieldStyle and additional header (it can be removed if need)
extension TextField {
func customStyle(_ title: String) -> some View {
self.textFieldStyle(CustomTextFieldStyle(title))
}
}
extension SecureField {
func customStyle(_ title: String, error) -> some View {
self.textFieldStyle(CustomTextFieldStyle(title))
}
}
struct CustomTextFieldStyle : TextFieldStyle {
#FocusState var focused: Bool
let title: String
init(_ title: String) {
self.title = title
}
public func _body(configuration: TextField<Self._Label>) -> some View {
VStack(alignment: .leading) {
Text(title)
.padding(.horizontal, 12)
configuration
.focused($focused)
.frame(height: 48)
.padding(.horizontal, 12)
.background(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.foregroundColor(.gray)
)
}.onTapGesture {
focused = true
}
}
}
Try using an overlay with a spacer to create a larger tapable/touchable area.
Create a myText variable:
#State private var myText = ""
Then, create your TextField with the following example formatting with an overlay:
TextField("Enter myText...", text: $myText)
.padding()
.frame(maxWidth: .infinity)
.padding(.horizontal)
.shadow(color: Color(.gray), radius: 3, x: 3, y: 3)
.overlay(
HStack {
Spacer()
})
Hope this works for you!
quick workaround would be to just put TextField in a button, and it'll make keyboard open no matter where you tap (in button); I know it's not a solution but it gets the job done (sort of).