I’ve a big iOS venture that was migrated from XIBs to SwiftUI views. The migration appears to have been OK however for a wierd conduct of the keyboard associated with the kinds.
To point out you my drawback I’ve develop a really quick pattern of this drawback the place it may be very simply reproduced (I am utilizing iOS 16). That is the code of my pattern (for design functions I can not use instantly SwiftUI textfields, I’ve tu use UITextFields and wrap them for use by SwiftUI):
MyApp:
import SwiftUI
@major
struct MyApp: App {
var physique: some Scene {
WindowGroup {
FirstView()
}
}
}
FirstView
import SwiftUI
struct FirstView: View {
@State var hasToGoToSecondView = false
var physique: some View {
NavigationView {
VStack {
NavigationLink(vacation spot:SecondView().navigationBarHidden(true),
isActive: $hasToGoToSecondView) {
}
.navigationBarHidden(true)
.hidden()
Button(motion: {
print("Button clicked")
hasToGoToSecondView = true
}) {
VStack {
Picture(systemName: "globe")
.imageScale(.giant)
.foregroundColor(.accentColor)
Textual content("Hey, world!")
}
}
}
}
}
}
SecondView
import SwiftUI
struct SecondView: View {
@State var userUsernameOrEmail: String = ""
@State var userPassword: String = ""
@State var fieldFocus = [false, false]
@Setting(.presentationMode) var presentationMode: Binding<PresentationMode>
var physique: some View {
VStack {
CustomNavigationBar(backAction: {
presentationMode.wrappedValue.dismiss()
})
Spacer()
Textual content("Type")
.padding(.backside, 32)
CustomTextField(inputText: $userUsernameOrEmail,
focusable: $fieldFocus,
tag: 0,
returnKeyType: .subsequent,
keyboardType: .emailAddress)
.body(top: 30)
.padding(32)
CustomTextField(inputText: $userPassword,
focusable: $fieldFocus,
tag: 1,
returnKeyType: .performed)
.body(top: 30)
.padding(32)
Spacer()
}
}
}
CustomNavigationBar
struct CustomNavigationBar: View {
var backAction: (() -> Void)?
init(backAction: (() -> Void)? = nil) {
self.backAction = backAction
}
var physique: some View {
ZStack {
HStack {
Button {
backAction?()
} label: {
Textual content("< Again")
}
.padding([.leading, .trailing], 10)
Spacer()
}
}
.body(top: 54)
.body(maxWidth: .infinity)
.padding([.leading, .trailing], 10)
}
}
CustomTextField
struct CustomTextField: View {
@Binding var inputText: String
var focusable: Binding<[Bool]>? = nil
var tag: Int? = nil
var returnKeyType: UIReturnKeyType = .default
var keyboardType: UIKeyboardType = .default
init(inputText: Binding<String>,
focusable: Binding<[Bool]>? = nil,
tag: Int? = nil,
returnKeyType: UIReturnKeyType = .default,
keyboardType: UIKeyboardType = .default) {
self._inputText = inputText
self.focusable = focusable
self.tag = tag
self.returnKeyType = returnKeyType
self.keyboardType = keyboardType
}
var physique: some View {
HStack {
CustomUITextField(textual content: $inputText,
focusable: focusable,
returnKeyType: returnKeyType,
keyboardType: keyboardType,
tag: tag)
.foregroundColor(Shade.black)
.padding([.leading, .trailing], 24)
.padding([.top, .bottom], 18)
}
.background(Shade.yellow)
.cornerRadius(10)
}
}
CustomUITextField
struct CustomUITextField: UIViewRepresentable {
@Binding var textual content: String
var focusable: Binding<[Bool]>? = nil
var returnKeyType: UIReturnKeyType = .default
var keyboardType: UIKeyboardType = .default
var tag: Int? = nil
func makeUIView(context: Context) -> UITextField {
let textField = UITextField(body: .zero)
textField.delegate = context.coordinator
textField.returnKeyType = returnKeyType
textField.autocapitalizationType = .none
textField.spellCheckingType = .no
textField.autocorrectionType = .no
textField.keyboardType = keyboardType
textField.isSecureTextEntry = false
textField.textAlignment = .left
if let tag = tag {
textField.tag = tag
}
textField.addTarget(context.coordinator, motion: #selector(Coordinator.textFieldDidChange(_:)), for: .editingChanged)
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.textual content = textual content
uiView.isSecureTextEntry = false
if let focusable = focusable?.wrappedValue {
for (index, centered) in focusable.enumerated() {
if uiView.tag == index && centered {
DispatchQueue.major.asyncAfter(deadline: .now() + 0.05) {
uiView.becomeFirstResponder()
}
break
}
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self, maxChars: 20, allowedChars: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890@._%+-")
}
ultimate class Coordinator: NSObject, UITextFieldDelegate {
let management: CustomUITextField
var maxChars: Int32 = 0
var allowedChars: String = ""
init(_ management: CustomUITextField,
maxChars: Int32,
allowedChars: String) {
self.management = management
self.maxChars = maxChars
self.allowedChars = allowedChars
}
func textFieldDidBeginEditing(_ textField: UITextField) {
guard var focusable = management.focusable?.wrappedValue else { return }
for i in 0...(focusable.rely - 1) {
focusable[i] = (textField.tag == i)
}
management.focusable?.wrappedValue = focusable
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
guard var focusable = management.focusable?.wrappedValue else {
DispatchQueue.major.asyncAfter(deadline: .now() + 0.05) {
textField.resignFirstResponder()
}
return true
}
for i in 0...(focusable.rely - 1) {
focusable[i] = (textField.tag + 1 == i)
}
management.focusable?.wrappedValue = focusable
if textField.tag == focusable.rely - 1 {
DispatchQueue.major.asyncAfter(deadline: .now() + 0.05) {
textField.resignFirstResponder()
}
}
return true
}
func textField(_ textField: UITextField, shouldChangeCharactersIn vary: NSRange, replacementString string: String) -> Bool {
let textLength = textField.textual content?.rely ?? 0
let newLength = textLength + string.rely - vary.size
if (maxChars > 0) && (newLength > maxChars) {
return false
} else if allowedChars != "" {
let set = NSCharacterSet(charactersIn: allowedChars)
let forbiddenChars = set.inverted
let matchRange = string.rangeOfCharacter(from: forbiddenChars)
return matchRange == nil
}
return true
}
func textFieldDidEndEditing(_ textField: UITextField) {
print("editfieldend")
}
@objc func textFieldDidChange(_ textField: UITextField) {
management.textual content = textField.textual content ?? ""
print("editfieldchanged")
}
}
}
After I go to SecondView, set values within the textual content fields and return to FirstView (think about the person regrets and desires to be again) the keyboard/views make a really unusual flickering impact.
I’ve tried to cover the keyboard simply earlier than leaving the view:
CustomNavigationBar(backAction: {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
to: nil,
from: nil,
for: nil)
presentationMode.wrappedValue.dismiss()
})
And even with a protracted delay (only for testing functions):
CustomNavigationBar(backAction: {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
to: nil,
from: nil,
for: nil)
DispatchQueue.major.asyncAfter(deadline: .now() + 1.0) {
presentationMode.wrappedValue.dismiss()
}
})
That is the end result:
After seeing this final gif I’ve thought that it appears the keyboard is hidden accurately however there’s “one other” keyboard “floating” within the view. So I’ve tried to cover the keyboard with each textual content subject change, to make sure “there is just one keyboard at a time”. I’ve tried this:
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.textual content = textual content
uiView.isSecureTextEntry = false
if let focusable = focusable?.wrappedValue {
var resignResponder = true
for (index, centered) in focusable.enumerated() {
if uiView.tag == index && centered {
DispatchQueue.major.asyncAfter(deadline: .now() + 0.05) {
uiView.becomeFirstResponder()
}
resignResponder = false
break
}
}
if resignResponder {
DispatchQueue.major.asyncAfter(deadline: .now() + 0.05) {
uiView.resignFirstResponder()
}
}
}
}
However that is even worse becuase it provides a flickering within the keyboard altering textual content fields.
I’ve confirmed that if I shut manually the keyboard earlier than clicking again, it really works okay.
Lastly, I’ve although about make one thing like this:
CustomNavigationBar(backAction: {
resignFocusedField()
presentationMode.wrappedValue.dismiss()
})
non-public func resignFocusedField() {
for i in 0..<fieldFocus.rely {
let isFocused = fieldFocus[i]
if isFocused {
// resign i textfiels
}
}
}
However I do not understand how might I do that.
Any assist could be very appreciated.