Swift - Autofill does not populate UITextField - ios

When a user uses Autofill (not password generation - rather, when they tap a login and use iCloud Keychain to log in - see User taps on Autofill item, if FaceID isn't completed immediately, the UITextField does not populate with the user's username and password. It only fills some of the time.
Since my app is written in SwiftUI, I use a custom TextField.
struct AutoFocusTextField<V: Hashable>: UIViewRepresentable {
#Binding var text: String
let placeholder: String
var id: V
#Binding var firstResponder: V?
var onCommit: () -> Void
var formatText: () -> Void
var inputAccessoryView: UIToolbar? = nil
var secureEntry: Binding<Bool>? = nil
var returnKeyType: UIReturnKeyType
var autoCorrection: UITextAutocorrectionType
var capitalize: UITextAutocapitalizationType
var keyboardType: UIKeyboardType
var contentType: UITextContentType?
var datePicker: UIDatePicker?
init(text: Binding<String>, placeholder: String, id: V, firstResponder: Binding<V?>, onCommit: #escaping (() -> Void) = {}, formatText: #escaping (() -> Void) = {},
secureEntry: Binding<Bool>? = nil, returnKeyType: UIReturnKeyType = .default, autoCorrection: UITextAutocorrectionType = .no, capitalize: UITextAutocapitalizationType = .none, keyboardType: UIKeyboardType = .default, contentType: UITextContentType? = .none, datePicker: UIDatePicker? = nil) {
self.id = id
_text = text
_firstResponder = firstResponder
self.placeholder = placeholder
self.onCommit = onCommit
self.formatText = formatText
self.secureEntry = secureEntry
self.returnKeyType = returnKeyType
self.autoCorrection = autoCorrection
self.capitalize = capitalize
self.keyboardType = keyboardType
self.contentType = contentType
self.datePicker = datePicker
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text,
format: format,
onStartEditing: startedEditing,
onEndEditing: finishedEditing,
onReturnTap: returnTap)
}
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
textField.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [NSAttributedString.Key.foregroundColor: UIColor(named: "primary-gray"),
NSAttributedString.Key.font: UIFont(name: "MDPrimer-Regular", size: 16)
])
textField.textColor = UIColor(named: "primary-black")
textField.font = UIFont(name: "MDPrimer-Regular", size: 16)
textField.tintColor = UIColor(named: "primary-black")
if let datePicker = datePicker {
textField.inputView = datePicker
datePicker.datePickerMode = .date
datePicker.preferredDatePickerStyle = .wheels
datePicker.addTarget(context.coordinator, action: #selector(Coordinator.dateChanged(_:)), for: .valueChanged)
}
else {
textField.keyboardType = keyboardType
}
textField.tintColor = UIColor(named: "primary-lilac")!
textField.isSecureTextEntry = secureEntry?.wrappedValue ?? false
textField.autocorrectionType = autoCorrection
textField.returnKeyType = returnKeyType
textField.autocapitalizationType = capitalize
textField.textContentType = contentType ?? .none
textField.contentVerticalAlignment = .center
textField.contentHorizontalAlignment = .center
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textField.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
textField.addTarget(context.coordinator, action: #selector(Coordinator.textFieldDidChange(_:)), for: .editingChanged)
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
if id == firstResponder, uiView.isFirstResponder == false {
DispatchQueue.main.async {
uiView.becomeFirstResponder()
}
}
}
func startedEditing() {
if id != firstResponder {
firstResponder = id
}
}
func finishedEditing() {
guard id == firstResponder else { return }
firstResponder = nil
}
func format() {
self.formatText()
}
func returnTap() {
self.onCommit()
}
}
protocol TextFieldReturnKeyProtocol {
func returnTapped()
}
class Coordinator: NSObject, UITextFieldDelegate, TextFieldReturnKeyProtocol {
#Binding private var text: String
private let format: (() -> Void)
private let onStartEditing: (() -> Void)
private let onEndEditing: (() -> Void)
private let onReturnTap: (() -> Void)
var previousText: String?
var nextText: String?
init(text: Binding<String>, format: #escaping (() -> Void), onStartEditing: #escaping (() -> Void), onEndEditing: #escaping (() -> Void), onReturnTap: #escaping (() -> Void)) {
_text = text
self.onStartEditing = onStartEditing
self.onEndEditing = onEndEditing
self.onReturnTap = onReturnTap
self.format = format
super.init()
}
#objc func textFieldDidChange(_ textField: UITextField) {
DispatchQueue.main.async { [weak self] in
self?.text = textField.text ?? ""
self?.format()
}
}
#objc func dateChanged(_ sender: UIDatePicker) {
let df = DateFormatter()
df.dateFormat = "MM/dd/yyyy"
self.text = df.string(from: sender.date)
}
func textFieldDidBeginEditing(_ textField: UITextField) {
onStartEditing()
}
func textFieldDidEndEditing(_ textField: UITextField) {
onEndEditing()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
returnTapped()
return true
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard var safeText = textField.text else { return true }
DispatchQueue.main.async {
var str = string
if string.count > 1 && textField.textContentType == .telephoneNumber && textField.keyboardType == .numberPad {
if string.count > 11 && string.starts(with: "+1") {
str.removeFirst(2)
textField.text = str
} else if string.count > 10 && string.starts(with: "1") {
str.removeFirst()
textField.text = str
}
}
}
return true
}
#objc func returnTapped() {
onReturnTap()
}
}
This is wrapped in a larger SwiftUI view.
struct AutoFocusTextFieldWrapper: View {
#Binding var text: String
let placeholder: String
let id: ResponderFields
#Binding var firstResponder: ResponderFields?
let onCommit: () -> Void
var hasError: Bool = false
var secureEntry: Binding<Bool>? = nil
var returnKeyType: UIReturnKeyType = .default
var autoCorrection: UITextAutocorrectionType = .no
var capitalize: UITextAutocapitalizationType = .none
var keyboardType: UIKeyboardType = .default
var contentType: UITextContentType? = .none
var datePicker: UIDatePicker? = nil
var formatText: () -> Void
var enableBorder: Bool = true
var highlight: Bool = false
var body: some View {
AutoFocusTextField(text: $text, placeholder: placeholder, id: id, firstResponder: $firstResponder, onCommit: onCommit, formatText: formatText, secureEntry: secureEntry, returnKeyType: returnKeyType, autoCorrection: autoCorrection, capitalize: capitalize, keyboardType: keyboardType, contentType: contentType, datePicker: datePicker)
.padding(EdgeInsets(top: 0, leading: 24, bottom: 0, trailing: 24))
.frame(height: 48)
.frame(maxWidth: .infinity)
}
}
And then, here is the Login View.
struct LoginView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#EnvironmentObject var loginRegistrationViewModel: LoginRegistrationViewModel
#EnvironmentObject var viewModel: LoginViewModel
var dismiss: () -> Void
var body: some View {
NavigationView {
VStack(alignment: .center, spacing: 0) {
NavigationTitle(title: viewModel.navigationTitle, description: viewModel.navigationDescription, backButtonAction: dismiss)
AutoFocusTextFieldWrapper(
text: $viewModel.email,
placeholder: "Email",
id: .loginEmail,
firstResponder: $viewModel.firstResponder,
onCommit: {
viewModel.firstResponder = .loginPassword
},
hasError: viewModel.hasError,
returnKeyType: .next,
keyboardType: .emailAddress,
contentType: .username,
formatText: {},
highlight: viewModel.successLoggingIn
)
.padding(EdgeInsets(top: 48, leading: 0, bottom: 0, trailing: 0))
AutoFocusTextFieldWrapper(
text: $viewModel.password,
placeholder: "Password",
id: .loginPassword,
firstResponder: $viewModel.firstResponder,
onCommit: {
UIApplication.shared.endEditing()
},
hasError: viewModel.hasError,
secureEntry: .constant(true),
returnKeyType: .next,
contentType: .password,
formatText: {},
highlight: viewModel.successLoggingIn
)
.padding(EdgeInsets(top: 24, leading: 0, bottom: 0, trailing: 0))
Spacer()
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
For some reason, the autofill does not always work. I've noticed that if I comment out the code in startedEditing and finishedEditing, it seems to work more consistently. Any idea on why it's not working all the time?

Related

OTC View autolayout in SwiftUI

I'm new to swiftui and swift basically, i made One-time-code screen, and here i have a problem. When i run project on my old phone (iphone 6) when keyboard appears, my textfield size changes (it gets very thin vertically). So i was wondering is there any way to add autolayout for older devices?
Here is my code
struct OneTimeCodeBoxes: View {
#Binding var codeDict: [Int: String]
#State var firstResponderIndex = 0
var onCommit: (()->Void)?
var body: some View {
HStack {
ForEach(0..<codeDict.count) { i in
let isEmpty = codeDict[i]?.isEmpty == true
OneTimeCodeInput(
index: i,
codeDict: $codeDict,
firstResponderIndex: $firstResponderIndex,
onCommit: onCommit
)
.aspectRatio(1, contentMode: .fit)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(lineWidth: isEmpty ? 1 : 2)
.foregroundColor(isEmpty ? .secondary : .green))
}
}
}
}
struct OneTimeCodeBoxes_Previews: PreviewProvider {
static var previews: some View {
OneTimeCodeBoxes(codeDict: .constant([0: "", 1: "", 2: "", 3: ""]))
.padding()
.previewLayout(.sizeThatFits)
}
}
Here is my OneTimeCodeInput part of code
struct OneTimeCodeInput: UIViewRepresentable {
typealias UIViewType = UITextField
let index: Int
#Binding var codeDict: [Int: String]
#Binding var firstResponderIndex: Int
var onCommit: (()->Void)?
class Coordinator: NSObject, UITextFieldDelegate {
let index: Int
#Binding var codeDict: [Int: String]
#Binding var firstResponderIndex: Int
private lazy var codeDigits: Int = codeDict.count
init(index: Int, codeDict: Binding<[Int: String]>, firstResponderIndex: Binding<Int>) {
self.index = index
self._codeDict = codeDict
self._firstResponderIndex = firstResponderIndex
}
#objc func textFieldEditingChanged(_ textField: UITextField) {
print("textField.text!", textField.text!)
guard textField.text!.count == codeDigits else { return }
codeDict = textField.text!.enumerated().reduce(into: [Int: String](), { dict, tuple in
let (index, char) = tuple
dict.updateValue(String(char), forKey: index)
})
firstResponderIndex = codeDigits - 1
}
func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool
{
print("replacementString", string)
if string.isBackspace {
codeDict.updateValue("", forKey: index)
firstResponderIndex = max(0, textField.text == "" ? index - 1 : index)
return false
}
for i in index..<min(codeDict.count, index + string.count) {
codeDict.updateValue(string.stringAt(index: i - index), forKey: i)
// print(codeDict)
firstResponderIndex = min(codeDict.count - 1, index + string.count)
}
return true
}
}
func makeCoordinator() -> Coordinator {
.init(index: index, codeDict: $codeDict, firstResponderIndex: $firstResponderIndex)
}
func makeUIView(context: Context) -> UITextField {
let tf = BackspaceTextField(onDelete: {
firstResponderIndex = max(0, index - 1)
})
tf.addTarget(context.coordinator, action: #selector(Coordinator.textFieldEditingChanged), for: .editingChanged)
tf.delegate = context.coordinator
tf.keyboardType = .numberPad
tf.textContentType = .oneTimeCode
tf.font = .preferredFont(forTextStyle: .largeTitle)
tf.textAlignment = .center
return tf
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = codeDict[index]
if index == firstResponderIndex {
uiView.becomeFirstResponder()
}
if index == firstResponderIndex,
codeDict.values.filter({ !$0.isEmpty }).count == codeDict.count
{
onCommit?()
}
}
}
extension OneTimeCodeInput {
class BackspaceTextField: UITextField {
var onDelete: (()->Void)?
init(onDelete: (()->Void)?) {
self.onDelete = onDelete
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func deleteBackward() {
super.deleteBackward()
onDelete?()
}
}
}
This is how it shows on Ipnone13 simulator, and it's correct. I'm trying to do same on older devices

How do I get the position of the cursor of TextEditor in SwiftUI?

So in my text editor, I'd like to know the position of the cursor geometrically. I'm also planning to append some text after that position.
So how do I do this?
Okay... So I figured out a way to do this.
First, I created a struct to store the cursor positions
import foundation
struct CursorPosition {
start: Int
end: Int
}
Then I initialize it to be static
class Global {
public static var cursorPosition = CursorPosition(start: 0, end: 0)
}
Then finally, I created a custom view to match the SwiftUI TextEditor and listen for selection change and update the CursorPosition
import UIKit
import SwiftUI
fileprivate struct UITextViewWrapper: UIViewRepresentable {
typealias UIViewType = UITextView
#Binding var text: String
var onDone: (() -> Void)?
func makeUIView(context: UIViewRepresentableContext<UITextViewWrapper>) -> UITextView {
let textField = UITextView()
textField.delegate = context.coordinator
textField.isEditable = true
textField.font = UIFont.preferredFont(forTextStyle: .body)
textField.isSelectable = true
textField.isUserInteractionEnabled = true
textField.isScrollEnabled = true
textField.backgroundColor = UIColor.clear
if nil != onDone {
textField.returnKeyType = .done
}
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return textField
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
if uiView.text != self.text {
uiView.text = self.text
}
if uiView.window != nil, !uiView.isFirstResponder {
uiView.becomeFirstResponder()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text, onDone: onDone)
}
final class Coordinator: NSObject, UITextViewDelegate {
var text: Binding<String>
var onDone: (() -> Void)?
init(text: Binding<String>, onDone: (() -> Void)? = nil) {
self.text = text
self.onDone = onDone
}
func textViewDidChange(_ uiView: UITextView) {
text.wrappedValue = uiView.text
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if let onDone = self.onDone, text == "\n" {
textView.resignFirstResponder()
onDone()
return false
}
return true
}
func textViewDidChangeSelection(_ textView: UITextView) {
if let range = textView.selectedTextRange {
Global.cursorPosition.start = textView.offset(from: textView.beginningOfDocument, to: range.start)
Global.cursorPosition.end = textView.offset(from: textView.beginningOfDocument, to: range.end)
}
}
}
}
struct EditText: View {
private var placeholder: String
private var onCommit: (() -> Void)?
#Binding private var text: String
private var internalText: Binding<String> {
Binding<String>(get: { self.text } ) {
self.text = $0
self.showingPlaceholder = $0.isEmpty
}
}
#State private var showingPlaceholder = false
init (_ placeholder: String = "", text: Binding<String>, onCommit: (() -> Void)? = nil) {
self.placeholder = placeholder
self.onCommit = onCommit
self._text = text
self._showingPlaceholder = State<Bool>(initialValue: self.text.isEmpty)
}
var body: some View {
UITextViewWrapper(text: self.internalText, onDone: onCommit)
.background(placeholderView, alignment: .topLeading)
}
var placeholderView: some View {
Group {
if showingPlaceholder {
Text(placeholder).foregroundColor(.gray)
.padding(.leading, 4)
.padding(.top, 8)
}
}
}
}
And using it:
EditText("", text: $text)
.onChange(of: text){ _ in
let cursorStart = Global.cursorPosition.start
}

UITextView avoid text selection, but keep tappable links

I have a pop-up window, that shows terms of service & privacy policy for users, how to disable text selection but keep links tappable. screenshot
If I set:
SOLVED: By adding textViewDidChangeSelection method in UIViewDelegate and setting
textView.isSelectable = false
textView.isUserInteractionEnabled = true
textView.isSelectable = true
Links become uninteractable.
How do I keep the link tappable and text not selectable?
My whole class:
// HyperLinkTextView.swift
import SwiftUI
struct Hyperlink {
var word: String
var url: NSURL
}
struct HyperLinkTextView: UIViewRepresentable {
private var text: String
private var links: [Hyperlink]
let textView = UITextView()
init(text: String, links: [Hyperlink]) {
self.text = text
self.links = links
}
func makeUIView(context: Self.Context) -> UITextView {
let attributedString = NSMutableAttributedString(string: text)
links.forEach { hyperlink in
let linkAttributes = [NSAttributedString.Key.link: hyperlink.url]
var nsRange = NSMakeRange(0, 0)
if let range = text.range(of: hyperlink.word) {
nsRange = NSRange(range, in: text)
}
attributedString.setAttributes(linkAttributes, range: nsRange)
attributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSNumber(value: 1), range: nsRange)
}
textView.isEditable = false
textView.delegate = context.coordinator
textView.attributedText = attributedString
textView.linkTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor(Theme.colorClickables)]
textView.isUserInteractionEnabled = true
textView.isSelectable = true
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.font = UIFont(name: "ArialMT", size: 18)
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
public class Coordinator: NSObject, UITextViewDelegate, NSLayoutManagerDelegate {
weak var textView: UITextView?
public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction, replacementText text: String) -> Bool {
return true
}
func textViewDidChangeSelection(_ textView: UITextView) {
if textView.selectedTextRange != nil {
textView.delegate = nil
textView.selectedTextRange = nil
textView.delegate = self
}
}
}
}
Add this UITextViewDelegate textViewDidChangeSelection and comment out isEditable and isSelectable:
func textViewDidChangeSelection(_ textView: UITextView) {
if textView.selectedTextRange != nil {
textView.delegate = nil
textView.selectedTextRange = nil
textView.delegate = self
}
}

UITextView with NSMutableAttributedString with Link SwiftUI

I have this Custom UITextView that uses "ShouldInteractWith" method:
class StudyText: UITextView, UITextViewDelegate {
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
print(URL)
return false
}
}
struct ClickableText: UIViewRepresentable {
#Binding var text: NSMutableAttributedString
func makeUIView(context: Context) -> StudyText {
let view = StudyText()
view.dataDetectorTypes = .all
view.isEditable = false
view.isSelectable = true
view.delegate = view
view.isUserInteractionEnabled = true
return view
}
func updateUIView(_ uiView: StudyText, context: Context) {
uiView.attributedText = text
}
}
I have this extension to set a link to the attributed text:
extension NSMutableAttributedString {
func apply(link: String, subString: String) {
if let range = self.string.range(of: subString) {
self.apply(link: link, onRange: NSRange(range, in: self.string))
}
}
private func apply(link: String, onRange: NSRange) {
self.addAttributes([NSAttributedString.Key.link: link], range: onRange)
}
}
And I created this layout:
struct contentView: View {
#State text: NSMutableAttributedString = NSMutableAttributedString(string: "")
var body: some View {
VStack {
ClickableText(text: self.$text)
}
.onAppear{
let myText = "Click Me!"
let attributedString = NSMutableAttributedString(string: myText)
attributedString.apply(link: "Soeme random link", subString: myText)
self.text = attributedString
}
}
}
When I click on the text view it doesn't print anything to the console and sometimes it crashes.
How can I fix this?
It must be provided valid URL, like
let attributedString = NSMutableAttributedString(string: myText)
attributedString.apply(link: "https://www.google.com", subString: myText)

Eureka - Row to present multiple controllers

I'd like to create Eureka row to look and behave as Postal Address Row in create/edit Contact Screen in iOS Contacts App. I need to present Label or Country Picker when corresponding button in cell is pressed. Based on Eureka documentation:
every row that displays a new view controller must conform to PresenterRowType protocol
However this protocol is generic. So my understanding is that I can't show more than one child screen per row. Do I get this right? Is it possible to present more than one child screen?
What I have so far follows.
Row Protocols:
protocol PostalAddressFormatterConformance: class {
var streetUseFormatterDuringInput: Bool { get set }
var streetFormatter: Formatter? { get set }
var stateUseFormatterDuringInput: Bool { get set }
var stateFormatter: Formatter? { get set }
var postalCodeUseFormatterDuringInput: Bool { get set }
var postalCodeFormatter: Formatter? { get set }
var cityUseFormatterDuringInput: Bool { get set }
var cityFormatter: Formatter? { get set }
}
protocol LabeledRowConformance {
func onLabelButtonDidPress()
}
protocol CountryRowConformance {
func onCountryButtonDidPress()
}
protocol PostalAddressRowConformance: PostalAddressFormatterConformance, LabeledRowConformance, CountryRowConformance {
var placeholderColor : UIColor? { get set }
var streetPlaceholder : String? { get set }
var statePlaceholder : String? { get set }
var postalCodePlaceholder : String? { get set }
var cityPlaceholder : String? { get set }
}
Postal Address Row Base class:
class _PostalAddressRow<Cell: CellType>: Row<Cell>, PostalAddressRowConformance, CountryRowConformance, LabeledRowConformance, KeyboardReturnHandler where Cell: BaseCell, Cell: PostalAddressCellConformance {
//MARK: - LabeledRowConformance
func onLabelButtonDidPress() {
// TODO: Present Label Picker Screen
}
//MARK: - CountryRowConformance
func onCountryButtonDidPress() {
// TODO: Present Country Picker Screen
}
//MARK: - KeyboardReturnHandler
/// Configuration for the keyboardReturnType of this row
var keyboardReturnType : KeyboardReturnTypeConfiguration?
//MARK: - PostalAddressRowConformance
/// The textColor for the textField's placeholder
var placeholderColor : UIColor?
/// The placeholder for the street textField
var streetPlaceholder : String?
/// The placeholder for the state textField
var statePlaceholder : String?
/// The placeholder for the zip textField
var postalCodePlaceholder : String?
/// The placeholder for the city textField
var cityPlaceholder : String?
/// A formatter to be used to format the user's input for street
var streetFormatter: Formatter?
/// A formatter to be used to format the user's input for state
var stateFormatter: Formatter?
/// A formatter to be used to format the user's input for zip
var postalCodeFormatter: Formatter?
/// A formatter to be used to format the user's input for city
var cityFormatter: Formatter?
/// If the formatter should be used while the user is editing the street.
var streetUseFormatterDuringInput: Bool
/// If the formatter should be used while the user is editing the state.
var stateUseFormatterDuringInput: Bool
/// If the formatter should be used while the user is editing the zip.
var postalCodeUseFormatterDuringInput: Bool
/// If the formatter should be used while the user is editing the city.
var cityUseFormatterDuringInput: Bool
public required init(tag: String?) {
streetUseFormatterDuringInput = false
stateUseFormatterDuringInput = false
postalCodeUseFormatterDuringInput = false
cityUseFormatterDuringInput = false
super.init(tag: tag)
}
}
Postal Address Row Final:
final class PostalAddressRow: _PostalAddressRow<PostalAddressCell>, RowType {
public required init(tag: String? = nil) {
super.init(tag: tag)
// TODO
cellProvider = CellProvider<PostalAddressCell>(nibName: "PostalAddressCell")
}
}
Cell:
public protocol CountryCellConformance {
var countryButton: UIButton? { get }
}
public protocol PostalAddressCellConformance: CountryCellConformance {
var streetTextField: UITextField? { get }
var stateTextField: UITextField? { get }
var postalCodeTextField: UITextField? { get }
var cityTextField: UITextField? { get }
}
class _PostalAddressCell<T: PostalAddressType>: Cell<T>, PostalAddressCellConformance, UITextFieldDelegate, CellType {
#IBOutlet weak var changeLabelButton: UIButton!
//MARK: - CountryCellConformance
#IBOutlet weak var countryButton: UIButton?
//MARK: - PostalAddressCellConformance
#IBOutlet weak var streetTextField: UITextField?
#IBOutlet weak var stateTextField: UITextField?
#IBOutlet weak var postalCodeTextField: UITextField?
#IBOutlet weak var cityTextField: UITextField?
// ??? Style Color
#IBOutlet var separatorViews: [UIView]!
// Helper
var textFieldOrdering: [UITextField?] = []
//MARK: - Lifecycle
public required init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
open override func awakeFromNib() {
super.awakeFromNib()
textFieldOrdering = [streetTextField, stateTextField, postalCodeTextField, cityTextField]
}
deinit {
streetTextField?.delegate = nil
streetTextField?.removeTarget(self, action: nil, for: .allEvents)
stateTextField?.delegate = nil
stateTextField?.removeTarget(self, action: nil, for: .allEvents)
postalCodeTextField?.delegate = nil
postalCodeTextField?.removeTarget(self, action: nil, for: .allEvents)
cityTextField?.delegate = nil
cityTextField?.removeTarget(self, action: nil, for: .allEvents)
}
//MARK: - Actions
#IBAction func changeLabelButtonPressed(_ sender: Any) {
if let rowConformance = row as? LabeledRowConformance {
rowConformance.onLabelButtonDidPress()
}
}
#IBAction func countryButtonPressed(_ sender: Any) {
if let rowConformance = row as? CountryRowConformance {
rowConformance.onCountryButtonDidPress()
}
}
func internalNavigationAction(_ sender: UIBarButtonItem) {
guard let inputAccesoryView = inputAccessoryView as? NavigationAccessoryView else { return }
var index = 0
for field in textFieldOrdering {
if field?.isFirstResponder == true {
let _ = sender == inputAccesoryView.previousButton ? textFieldOrdering[index-1]?.becomeFirstResponder() : textFieldOrdering[index+1]?.becomeFirstResponder()
break
}
index += 1
}
}
func textFieldDidChange(_ textField : UITextField){
if row.baseValue == nil{
row.baseValue = PostalAddress()
}
guard let textValue = textField.text else {
switch(textField) {
case let field where field == streetTextField:
row.value?.street = nil
case let field where field == stateTextField:
row.value?.state = nil
case let field where field == postalCodeTextField:
row.value?.postalCode = nil
case let field where field == cityTextField:
row.value?.city = nil
default:
break
}
return
}
if let rowConformance = row as? PostalAddressRowConformance {
var useFormatterDuringInput = false
var valueFormatter: Formatter?
switch(textField) {
case let field where field == streetTextField:
useFormatterDuringInput = rowConformance.streetUseFormatterDuringInput
valueFormatter = rowConformance.streetFormatter
case let field where field == stateTextField:
useFormatterDuringInput = rowConformance.stateUseFormatterDuringInput
valueFormatter = rowConformance.stateFormatter
case let field where field == postalCodeTextField:
useFormatterDuringInput = rowConformance.postalCodeUseFormatterDuringInput
valueFormatter = rowConformance.postalCodeFormatter
case let field where field == cityTextField:
useFormatterDuringInput = rowConformance.cityUseFormatterDuringInput
valueFormatter = rowConformance.cityFormatter
default:
break
}
if let formatter = valueFormatter, useFormatterDuringInput{
let value: AutoreleasingUnsafeMutablePointer<AnyObject?> = AutoreleasingUnsafeMutablePointer<AnyObject?>.init(UnsafeMutablePointer<T>.allocate(capacity: 1))
let errorDesc: AutoreleasingUnsafeMutablePointer<NSString?>? = nil
if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) {
switch(textField){
case let field where field == streetTextField:
row.value?.street = value.pointee as? String
case let field where field == stateTextField:
row.value?.state = value.pointee as? String
case let field where field == postalCodeTextField:
row.value?.postalCode = value.pointee as? String
case let field where field == cityTextField:
row.value?.city = value.pointee as? String
default:
break
}
if var selStartPos = textField.selectedTextRange?.start {
let oldVal = textField.text
textField.text = row.displayValueFor?(row.value)
if let f = formatter as? FormatterProtocol {
selStartPos = f.getNewPosition(forPosition: selStartPos, inTextInput: textField, oldValue: oldVal, newValue: textField.text)
}
textField.selectedTextRange = textField.textRange(from: selStartPos, to: selStartPos)
}
return
}
}
}
guard !textValue.isEmpty else {
switch(textField){
case let field where field == streetTextField:
row.value?.street = nil
case let field where field == stateTextField:
row.value?.state = nil
case let field where field == postalCodeTextField:
row.value?.postalCode = nil
case let field where field == cityTextField:
row.value?.city = nil
default:
break
}
return
}
switch(textField){
case let field where field == streetTextField:
row.value?.street = textValue
case let field where field == stateTextField:
row.value?.state = textValue
case let field where field == postalCodeTextField:
row.value?.postalCode = textValue
case let field where field == cityTextField:
row.value?.city = textValue
default:
break
}
}
//MARK: - Setup
override func setup() {
super.setup()
height = { 149 }
selectionStyle = .none
for textField in textFieldOrdering {
textField?.addTarget(self,
action: #selector(_PostalAddressCell.textFieldDidChange(_:)), // TODO: Move in extension
for: .editingChanged)
textField?.textAlignment = .left
textField?.clearButtonMode = .whileEditing
textField?.delegate = self
textField?.font = .preferredFont(forTextStyle: .body)
}
for separator in separatorViews {
separator.backgroundColor = .gray
}
}
//MARK: - Update
override func update() {
super.update()
textLabel?.text = nil
detailTextLabel?.text = nil
imageView?.image = nil
for textField in textFieldOrdering {
textField?.isEnabled = !row.isDisabled
textField?.textColor = row.isDisabled ? .gray : .black
textField?.autocorrectionType = .no
textField?.autocapitalizationType = .words
}
streetTextField?.text = row.value?.street
streetTextField?.keyboardType = .asciiCapable
stateTextField?.text = row.value?.state
stateTextField?.keyboardType = .asciiCapable
postalCodeTextField?.text = row.value?.postalCode
postalCodeTextField?.keyboardType = .numbersAndPunctuation
cityTextField?.text = row.value?.city
cityTextField?.keyboardType = .asciiCapable
if let rowConformance = row as? PostalAddressRowConformance {
setPlaceholderToTextField(textField: streetTextField, placeholder: rowConformance.streetPlaceholder)
setPlaceholderToTextField(textField: stateTextField, placeholder: rowConformance.statePlaceholder)
setPlaceholderToTextField(textField: postalCodeTextField, placeholder: rowConformance.postalCodePlaceholder)
setPlaceholderToTextField(textField: cityTextField, placeholder: rowConformance.cityPlaceholder)
}
countryButton?.setTitle(String(describing: row.value?.country), for: .normal)
}
//MARK: - BaseCell Responder
override func cellCanBecomeFirstResponder() -> Bool {
return !row.isDisabled && (
streetTextField?.canBecomeFirstResponder == true ||
stateTextField?.canBecomeFirstResponder == true ||
postalCodeTextField?.canBecomeFirstResponder == true ||
cityTextField?.canBecomeFirstResponder == true
)
}
override func cellBecomeFirstResponder(withDirection direction: Direction) -> Bool {
return direction == .down ? textFieldOrdering.first??.becomeFirstResponder() ?? false : textFieldOrdering.last??.becomeFirstResponder() ?? false
}
override func cellResignFirstResponder() -> Bool {
return streetTextField?.resignFirstResponder() ?? true
&& stateTextField?.resignFirstResponder() ?? true
&& postalCodeTextField?.resignFirstResponder() ?? true
&& stateTextField?.resignFirstResponder() ?? true
&& cityTextField?.resignFirstResponder() ?? true
}
override var inputAccessoryView: UIView? {
if let v = formViewController()?.inputAccessoryView(for: row) as? NavigationAccessoryView {
guard let first = textFieldOrdering.first, let last = textFieldOrdering.last, first != last else { return v }
if first?.isFirstResponder == true {
v.nextButton.isEnabled = true
v.nextButton.target = self
v.nextButton.action = #selector(_PostalAddressCell.internalNavigationAction(_:)) // TODO: Move in extension
} else if last?.isFirstResponder == true {
v.previousButton.target = self
v.previousButton.action = #selector(_PostalAddressCell.internalNavigationAction(_:))
v.previousButton.isEnabled = true
} else {
v.previousButton.target = self
v.previousButton.action = #selector(_PostalAddressCell.internalNavigationAction(_:))
v.nextButton.target = self
v.nextButton.action = #selector(_PostalAddressCell.internalNavigationAction(_:))
v.previousButton.isEnabled = true
v.nextButton.isEnabled = true
}
return v
}
return super.inputAccessoryView
}
//MARK: - UITextFieldDelegate
func textFieldDidBeginEditing(_ textField: UITextField) {
formViewController()?.beginEditing(of: self)
formViewController()?.textInputDidBeginEditing(textField, cell: self)
}
func textFieldDidEndEditing(_ textField: UITextField) {
formViewController()?.endEditing(of: self)
formViewController()?.textInputDidEndEditing(textField, cell: self)
textFieldDidChange(textField)
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
return formViewController()?.textInputShouldReturn(textField, cell: self) ?? true
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return formViewController()?.textInputShouldEndEditing(textField, cell: self) ?? true
}
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
return formViewController()?.textInputShouldBeginEditing(textField, cell: self) ?? true
}
func textFieldShouldClear(_ textField: UITextField) -> Bool {
return formViewController()?.textInputShouldClear(textField, cell: self) ?? true
}
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
return formViewController()?.textInputShouldEndEditing(textField, cell: self) ?? true
}
//MARK: - Private
private func setPlaceholderToTextField(textField: UITextField?, placeholder: String?) {
if let placeholder = placeholder, let textField = textField {
if let color = (row as? PostalAddressRowConformance)?.placeholderColor {
textField.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [NSForegroundColorAttributeName: color])
} else {
textField.placeholder = placeholder
}
}
}
}
final class PostalAddressCell: _PostalAddressCell<PostalAddress> {
public required init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Models:
protocol CountryType: Equatable {
var country: Country? { get set }
}
func == <T: CountryType>(lhs: T, rhs: T) -> Bool {
return lhs.country == rhs.country
}
//
protocol PostalAddressType: CountryType {
var street: String? { get set }
var state: String? { get set }
var postalCode: String? { get set }
var city: String? { get set }
}
func == <T: PostalAddressType>(lhs: T, rhs: T) -> Bool {
return lhs.street == rhs.street && lhs.state == rhs.state && lhs.postalCode == rhs.postalCode && lhs.city == rhs.city && lhs.country == rhs.country
}
//
class PostalAddress: PostalAddressType {
var street: String?
var state: String?
var postalCode: String?
var city: String?
var country: Country?
public init() {}
public init(street: String?, state: String?, postalCode: String?, city: String?, country: Country?) {
self.street = street
self.state = state
self.postalCode = postalCode
self.city = city
self.country = country
}
}

Resources