-
-
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 ?