Last active
November 12, 2023 15:52
-
-
Save Alhomaidhi/91d3bc9fda789b19b8e7c3b1c71dd6e5 to your computer and use it in GitHub Desktop.
Material Design textField to SwiftUI (UIViewRepresentable)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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 | |
} | |
} |
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.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Can you show me how you're using it? It should still work as a class.