-
-
Save Alhomaidhi/91d3bc9fda789b19b8e7c3b1c71dd6e5 to your computer and use it in GitHub Desktop.
| // | |
| // MDPlayground.swift | |
| // ValidationDemo | |
| // | |
| // Created by Abdullah Alhomaidhi on 15/06/2021. | |
| // | |
| import SwiftUI | |
| import MaterialComponents | |
| final class UIMDTextField: UIViewRepresentable { | |
| //Should not touch these vars | |
| private var timer: Timer? | |
| private let undoButton: UIButton | |
| private let undoButtonColor: UIColor | |
| private let showButton: UIButton | |
| private var didValidate: Bool | |
| private var trailingView: UIStackView | |
| private var textField: MDCOutlinedTextField | |
| // MARK: - All editable vars | |
| var iconSystemName: String? | |
| var labelText: String? | |
| var labelFont: UIFont? | |
| var labelColor: UIColor | |
| var placeholderText: String? | |
| var placeholderAtributedText: NSAttributedString? | |
| @Binding var text: String | |
| var textFont: UIFont? | |
| var textColor: UIColor? | |
| @Binding var errorText: String | |
| @Binding var errorAtributedText: NSAttributedString | |
| var errorTextColor: UIColor? | |
| var outlinedNormalOutlineColor: UIColor? | |
| var outlinedEditingOutlineColor: UIColor? | |
| var autoCapitalizationType: UITextAutocapitalizationType | |
| var autoCorrectionType: UITextAutocorrectionType | |
| var keyboardType: UIKeyboardType | |
| var isSecureTextEntry: Bool | |
| let isUndoable: Bool | |
| let originalText: String? | |
| var onBeginEditing: (()->())? | |
| var onEndEditing: (()->())? | |
| var validateCompletion: (()->())? | |
| var onTextChanged: (String)->() | |
| init (iconSystemName: String?, | |
| labelText: String? = nil, | |
| labelFont: UIFont? = nil, | |
| labelColor: UIColor = .label, | |
| placeholderText: String? = nil, | |
| placeholderAtributedText: NSAttributedString? = nil, | |
| text: Binding<String>, | |
| textFont: UIFont? = nil, | |
| textColor: UIColor? = nil, | |
| errorText: Binding<String> = .constant(""), | |
| errorAtributedText: Binding<NSAttributedString> = .constant(NSAttributedString()), | |
| errorTextColor: UIColor? = .red, | |
| outlinedNormalOutlineColor: UIColor? = .blue, | |
| outlinedEditingOutlineColor: UIColor? = nil, | |
| autoCapitalizationType: UITextAutocapitalizationType = .sentences, | |
| autoCorrectionType: UITextAutocorrectionType = .default, | |
| keyboardType: UIKeyboardType = .default, | |
| isSecureTextEntry: Bool = false, | |
| isUndoable: Bool = false, | |
| onBeginEditing: (()->())? = nil, | |
| onEndEditing: (()->())? = nil, | |
| validateCompletion: (()->())? = nil, | |
| onTextChanged: @escaping (String)->()) { | |
| //private variables | |
| self.undoButton = UIButton(type: .custom) | |
| self.undoButtonColor = undoButton.tintColor | |
| self.showButton = UIButton(type: .custom) | |
| self.didValidate = false | |
| self.timer = nil | |
| self.trailingView = UIStackView() | |
| self.textField = MDCOutlinedTextField() | |
| self.iconSystemName = iconSystemName | |
| self.labelText = labelText | |
| self.labelFont = labelFont | |
| self.labelColor = labelColor | |
| self.placeholderText = placeholderText | |
| self.placeholderAtributedText = placeholderAtributedText | |
| self._text = text | |
| self.textFont = textFont | |
| self.textColor = textColor | |
| self._errorText = errorText | |
| self._errorAtributedText = errorAtributedText | |
| self.errorTextColor = errorTextColor | |
| self.outlinedNormalOutlineColor = outlinedNormalOutlineColor | |
| self.outlinedEditingOutlineColor = outlinedEditingOutlineColor | |
| self.autoCapitalizationType = autoCapitalizationType | |
| self.autoCorrectionType = autoCorrectionType | |
| self.keyboardType = keyboardType | |
| self.isSecureTextEntry = isSecureTextEntry | |
| self.isUndoable = isUndoable | |
| self.originalText = text.wrappedValue | |
| self.onBeginEditing = onBeginEditing | |
| self.onEndEditing = onEndEditing | |
| self.validateCompletion = validateCompletion | |
| self.onTextChanged = onTextChanged | |
| } | |
| convenience init(text: Binding<String>, | |
| iconName: String, | |
| placeholderText: String, | |
| labelText: String, | |
| errorText: Binding<String>, | |
| onTextChanged: @escaping (String)->()) { | |
| self.init(iconSystemName: iconName, | |
| labelText: labelText, | |
| placeholderText: placeholderText, | |
| text: text, | |
| errorText: errorText, | |
| onTextChanged: onTextChanged) | |
| } | |
| convenience init(text: Binding<String>, | |
| iconName: String, | |
| placeholderText: String, | |
| labelText: String, | |
| errorText: Binding<String>, | |
| onBeginEditing: @escaping (()->()), | |
| onEndEditing: @escaping (()->()), | |
| onTextChanged: @escaping (String)->()) { | |
| self.init(iconSystemName: iconName, | |
| labelText: labelText, | |
| placeholderText: placeholderText, | |
| text: text, | |
| errorText: errorText, | |
| onBeginEditing: onBeginEditing, | |
| onEndEditing: onEndEditing, | |
| onTextChanged: onTextChanged) | |
| } | |
| convenience init(text: Binding<String>, | |
| iconName: String, | |
| placeholderText: String, | |
| labelText: String, | |
| errorText: Binding<String>, | |
| isSecureTextEntry: Bool, | |
| onTextChanged: @escaping (String)->()) { | |
| self.init(iconSystemName: iconName, | |
| labelText: labelText, | |
| placeholderText: placeholderText, | |
| text: text, | |
| errorText: errorText, | |
| isSecureTextEntry: isSecureTextEntry, | |
| onTextChanged: onTextChanged) | |
| } | |
| convenience init(text: Binding<String>, | |
| iconName: String, | |
| placeholderText: String, | |
| labelText: String, | |
| errorText: Binding<String>, | |
| isSecureTextEntry: Bool, | |
| onBeginEditing: @escaping (()->()), | |
| onEndEditing: @escaping (()->()), | |
| onTextChanged: @escaping (String)->()) { | |
| self.init(iconSystemName: iconName, | |
| labelText: labelText, placeholderText: placeholderText, | |
| text: text, | |
| errorText: errorText, | |
| isSecureTextEntry: isSecureTextEntry, | |
| onBeginEditing: onBeginEditing, | |
| onEndEditing: onEndEditing, | |
| onTextChanged: onTextChanged) | |
| } | |
| convenience init(text: Binding<String>, | |
| iconName: String, | |
| placeholderText: String, | |
| labelText: String, | |
| errorText: Binding<String>, | |
| isSecureTextEntry: Bool, | |
| onBeginEditing: (()->())?, | |
| onEndEditing: (()->())?, | |
| onTextChanged: @escaping (String)->()) { | |
| self.init(iconSystemName: iconName, | |
| labelText: labelText, | |
| placeholderText: placeholderText, | |
| text: text, | |
| errorText: errorText, | |
| isSecureTextEntry: isSecureTextEntry, | |
| onBeginEditing: onBeginEditing, | |
| onEndEditing: onEndEditing, | |
| onTextChanged: onTextChanged) | |
| } | |
| // MARK: - UIViewRepresentable | |
| func makeUIView(context: Context) -> MDCOutlinedTextField { | |
| setImages(textField: &textField) | |
| setLabel(textField: &textField) | |
| setPlaceholder(textField: &textField) | |
| setText(textField: &textField) | |
| setError(textField: &textField) | |
| setOutline(textField: &textField) | |
| setInputType(textField: &textField) | |
| setIsSecure(textField: &textField) | |
| setDelegate(textField: &textField, context: context) | |
| setActions(textField: &textField, context: context) | |
| return textField | |
| } | |
| func updateUIView(_ uiView: MDCOutlinedTextField, context: Context) { | |
| uiView.text = text | |
| uiView.leadingAssistiveLabel.text = errorText | |
| if errorText.isEmpty { | |
| uiView.trailingView = nil | |
| if let outlinedNormalOutlineColor = outlinedNormalOutlineColor { | |
| uiView.setOutlineColor(outlinedNormalOutlineColor, for: .normal) | |
| } | |
| } else { | |
| uiView.setOutlineColor(.red, for: .normal) | |
| } | |
| return | |
| } | |
| // MARK: - Coordinator to help with delegates | |
| class Coordinator: NSObject, UITextFieldDelegate { | |
| var parent: UIMDTextField | |
| init(_ parent: UIMDTextField) { | |
| self.parent = parent | |
| } | |
| internal func textFieldDidBeginEditing(_ textField: UITextField) { | |
| guard let onBeginEditing = parent.onBeginEditing else { | |
| return | |
| } | |
| onBeginEditing() | |
| } | |
| internal func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) { | |
| guard let onEndEditing = parent.onEndEditing else { | |
| return | |
| } | |
| if !parent.didValidate { | |
| parent.didValidate = true | |
| parent.timer?.invalidate() | |
| parent.validateCompletion?() | |
| } | |
| onEndEditing() | |
| } | |
| } | |
| func makeCoordinator() -> Coordinator { | |
| Coordinator(self) | |
| } | |
| // MARK: - Helper funcs | |
| private func setImages(textField: inout MDCOutlinedTextField) { | |
| if let leadingIconName = iconSystemName { | |
| textField.leadingView = UIImageView(image: UIImage(systemName: leadingIconName)) | |
| textField.leadingViewMode = .always | |
| } | |
| } | |
| private func setLabel(textField: inout MDCOutlinedTextField) { | |
| textField.label.text = labelText | |
| textField.label.font = labelFont | |
| textField.label.textColor = labelColor | |
| } | |
| private func setPlaceholder(textField: inout MDCOutlinedTextField){ | |
| if let placeholderText = placeholderAtributedText { | |
| textField.attributedPlaceholder = placeholderText | |
| } else if let placeholderText = placeholderText { | |
| textField.placeholder = placeholderText | |
| } | |
| } | |
| private func setText(textField: inout MDCOutlinedTextField) { | |
| textField.text = text | |
| textField.font = textFont | |
| textField.textColor = textColor | |
| } | |
| private func setError(textField: inout MDCOutlinedTextField) { | |
| if errorAtributedText.length > 0 { | |
| textField.leadingAssistiveLabel.attributedText = errorAtributedText | |
| } else if !errorText.isEmpty { | |
| textField.leadingAssistiveLabel.text = errorText | |
| } | |
| if let errorTextColor = errorTextColor { | |
| textField.setLeadingAssistiveLabelColor(errorTextColor, for: .normal) | |
| textField.setLeadingAssistiveLabelColor(errorTextColor, for: .editing) | |
| textField.setLeadingAssistiveLabelColor(errorTextColor, for: .disabled) | |
| } | |
| } | |
| private func setOutline(textField: inout MDCOutlinedTextField) { | |
| if let outlinedNormalOutlineColor = outlinedNormalOutlineColor { | |
| textField.setOutlineColor(outlinedNormalOutlineColor, for: .normal) | |
| textField.setOutlineColor(outlinedNormalOutlineColor, for: .editing) | |
| textField.setOutlineColor(outlinedNormalOutlineColor, for: .disabled) | |
| } | |
| if let outlinedEditingOutlineColor = outlinedEditingOutlineColor { | |
| textField.setOutlineColor(outlinedEditingOutlineColor, for: .editing) | |
| } | |
| } | |
| private func setInputType(textField: inout MDCOutlinedTextField) { | |
| textField.autocapitalizationType = autoCapitalizationType | |
| textField.autocorrectionType = autoCorrectionType | |
| textField.keyboardType = keyboardType | |
| } | |
| private func setIsSecure(textField: inout MDCOutlinedTextField) { | |
| textField.isSecureTextEntry = isSecureTextEntry | |
| } | |
| private func setDelegate(textField: inout MDCOutlinedTextField, context: Context) { | |
| textField.delegate = context.coordinator | |
| textField.addTarget(self, action: #selector(textFieldEditingChanged(_:)), for: .editingChanged) | |
| } | |
| private func setActions(textField: inout MDCOutlinedTextField, context: Context) { | |
| trailingView.spacing = 10 | |
| trailingView.translatesAutoresizingMaskIntoConstraints = false | |
| setUndoButton(context) | |
| setPasswordViewButton(context) | |
| textField.addSubview(trailingView) | |
| textField.trailingView = trailingView | |
| textField.trailingViewMode = .always | |
| } | |
| private func setUndoButton(_ context: UIMDTextField.Context) { | |
| if isUndoable { | |
| undoButton.setImage(UIImage(systemName: "arrow.uturn.backward.circle"), for: .normal) | |
| undoButton.addTarget(self, action: #selector(undoButtonPressed(_:)), for: .touchUpInside) | |
| trailingView.addArrangedSubview(undoButton) | |
| disableUndoButton() | |
| } | |
| } | |
| private func setPasswordViewButton(_ context: UIMDTextField.Context) { | |
| if isSecureTextEntry { | |
| showButton.setImage(UIImage(systemName: "eye"), for: .normal) | |
| showButton.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside) | |
| trailingView.addArrangedSubview(showButton) | |
| } | |
| } | |
| // MARK: - Target selectors | |
| @objc | |
| private func textFieldEditingChanged(_ textField: UITextField) { | |
| let text = textField.text ?? "" | |
| onTextChanged(text) | |
| didValidate = false | |
| timer?.invalidate() | |
| timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in | |
| self.didValidate = true | |
| self.validateCompletion?() | |
| } | |
| if originalText == text { | |
| disableUndoButton() | |
| } else { | |
| enableUndoButton() | |
| } | |
| } | |
| @objc | |
| private func buttonTapped(_ button: UIButton) { | |
| textField.isSecureTextEntry.toggle() | |
| isSecureTextEntry = textField.isSecureTextEntry | |
| showButton.setImage(UIImage(systemName: isSecureTextEntry ? "eye" : "eye.slash"), for: .normal) | |
| } | |
| @objc | |
| private func undoButtonPressed(_ button: UIButton) { | |
| text = originalText ?? "" | |
| textField.text = text | |
| disableUndoButton() | |
| } | |
| private func enableUndoButton() { | |
| undoButton.isEnabled = true | |
| undoButton.tintColor = undoButtonColor | |
| } | |
| private func disableUndoButton() { | |
| undoButton.isEnabled = false | |
| undoButton.tintColor = .clear | |
| } | |
| } |
Looks complete ๐ Is it possible to use this with SwiftUI ? If yes can you guide please ?
This is the library from google for material design. All I did was bridge it over to SwiftUI from UIKit. The library at the moment is in maintenance mode.
Once you download the dependency, the import on line 9 should not be giving you issues. Then you can just call UIMDTextField with any of the initializers. I have included convince inits for my different needs in the project.
When I am trying to use this in my swiftui codebase, it complains about the selectors, and I guess since it's a class not not struct I can not load it in a swiftui view directly ? Forgive my ignorance I am not very much experienced with Swift and iOS :)
When I am trying to use this in my swiftui codebase, it complains about the selectors, and I guess since it's a class not not struct I can not load it in a swiftui view directly ? Forgive my ignorance I am not very much experienced with Swift and iOS :)
Can you show me how you're using it? It should still work as a class.
It fails with the following error
Fatal error: UIViewRepresentables must be value types: UIMDTextField
I am just inflating inside by swiftui view like
UIMDTextField(iconSystemName: nil, text: $value) { String in
}
this link here mentions that we need to use a struct instead of a class for SwiftUI views
I see, then I guess this is a new update. I will update my gist as soon as I can.
Looks complete ๐ Is it possible to use this with SwiftUI ? If yes can you guide please ?