Skip to content

Instantly share code, notes, and snippets.

@Alhomaidhi
Last active November 12, 2023 15:52
Show Gist options
  • Save Alhomaidhi/91d3bc9fda789b19b8e7c3b1c71dd6e5 to your computer and use it in GitHub Desktop.
Save Alhomaidhi/91d3bc9fda789b19b8e7c3b1c71dd6e5 to your computer and use it in GitHub Desktop.
Material Design textField to SwiftUI (UIViewRepresentable)
//
// 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
}
}
@oianmol
Copy link

oianmol commented Jun 7, 2023

Looks complete 👍 Is it possible to use this with SwiftUI ? If yes can you guide please ?

@Alhomaidhi
Copy link
Author

Alhomaidhi commented Jun 7, 2023

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.

@oianmol
Copy link

oianmol commented Jun 7, 2023

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 :)

@Alhomaidhi
Copy link
Author

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.

@oianmol
Copy link

oianmol commented Jun 7, 2023

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

@Alhomaidhi
Copy link
Author

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