Maestría en Custom Views y Controles en iOS: Creando Componentes Reusables con Swift y UIKit
Este tutorial te guiará a través del fascinante mundo de la creación de vistas y controles personalizados en iOS utilizando Swift y UIKit. Aprenderás a diseñar componentes visuales que se adapten perfectamente a tus necesidades, desde el dibujo personalizado hasta la gestión avanzada de eventos y la creación de APIs flexibles.
🚀 Introducción a las Custom Views y Controles en iOS
En el desarrollo de aplicaciones iOS, a menudo nos encontramos con la necesidad de funcionalidades o interfaces de usuario que no están cubiertas por los componentes estándar de UIKit. Aquí es donde entran en juego las Custom Views y los Custom Controls. Al dominar su creación, no solo podrás implementar diseños únicos, sino también construir una biblioteca de componentes reusables que aceleren tu flujo de trabajo y mejoren la consistencia de tu UI.
Este tutorial te equipará con los conocimientos y herramientas necesarios para crear tus propios componentes, desde los fundamentos del ciclo de vida de la vista hasta técnicas avanzadas de dibujo y gestión de interacción. ¡Prepárate para llevar tus habilidades de UI/UX al siguiente nivel!
🎯 ¿Por qué crear Custom Views y Controles?
La creación de componentes personalizados ofrece múltiples beneficios para cualquier proyecto iOS:
- Flexibilidad de Diseño: Implementa cualquier diseño, por complejo o único que sea, sin las limitaciones de los componentes predefinidos.
- Reusabilidad: Crea componentes que puedes utilizar en múltiples pantallas o incluso en diferentes proyectos, ahorrando tiempo y esfuerzo.
- Consistencia de UI/UX: Asegura que tu aplicación tenga una apariencia y un comportamiento coherentes, mejorando la experiencia del usuario.
- Modularidad: Encapsula la lógica de UI en unidades independientes, haciendo el código más limpio, mantenible y testeable.
- Optimización del Rendimiento: En algunos casos, puedes optimizar el rendimiento dibujando solo lo que necesitas o implementando lógicas de renderizado más eficientes.
Custom View vs. Custom Control: ¿Cuál es la diferencia? 🤔
Si bien los términos a menudo se usan indistintamente, hay una distinción sutil pero importante en UIKit:
| Característica | UIView (Custom View) | UIControl (Custom Control) |
|---|---|---|
| --- | --- | --- |
| Propósito Principal | Mostrar contenido y organizar subviews | Recibir y responder a eventos de usuario (taps, swipes) |
| Clase Base | UIView | UIControl (que hereda de UIView) |
| --- | --- | --- |
| Interactividad | Interactividad básica a través de UIGestureRecognizer | Manejo de eventos target-action integrado |
| Estados | No tiene estados intrínsecos | Puede tener estados (normal, highlighted, selected, disabled) |
| --- | --- | --- |
| Ejemplos | Tarjetas personalizadas, gráficos de datos, loaders | Botones personalizados, sliders, interruptores, selectores |
En resumen, todos los UIControl son UIView, pero no todas las UIView son UIControl. Si tu componente interactúa directamente con el usuario y necesita responder a eventos de forma específica (como un botón que se ilumina al tocarlo), UIControl es la base adecuada. Si solo necesitas mostrar contenido, UIView es suficiente.
🛠️ Fundamentos: Creando tu Primera Custom View (UIView)
Comencemos con algo básico: una UIView personalizada que muestre un texto y un color de fondo específicos.
Paso 1: Crear una subclase de UIView
Crea un nuevo archivo Swift (ej. CustomCardView.swift) y define tu clase:
import UIKit
class CustomCardView: UIView {
// Propiedades personalizadas para nuestra vista
var title: String = "Título por defecto" {
didSet { // Cuando el título cambia, actualizamos la UI
titleLabel.text = title
}
}
var subtitle: String = "Subtítulo por defecto" {
didSet {
subtitleLabel.text = subtitle
}
}
var cardBackgroundColor: UIColor = .systemBlue {
didSet {
backgroundColor = cardBackgroundColor
}
}
// Subviews internas
private let titleLabel: UILabel = {
let label = UILabel()
label.font = UIFont.preferredFont(forTextStyle: .headline)
label.textColor = .white
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false // ¡Importante para Auto Layout!
return label
}()
private let subtitleLabel: UILabel = {
let label = UILabel()
label.font = UIFont.preferredFont(forTextStyle: .subheadline)
label.textColor = .white.withAlphaComponent(0.8)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
// MARK: - Initializers
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
// MARK: - Setup Methods
private func setupView() {
// Configuración inicial de la vista
layer.cornerRadius = 12
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.2
layer.shadowOffset = CGSize(width: 0, height: 4)
layer.shadowRadius = 8
backgroundColor = cardBackgroundColor
// Añadir subviews
addSubview(titleLabel)
addSubview(subtitleLabel)
// Configurar Auto Layout
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 20),
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),
subtitleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
subtitleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
subtitleLabel.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -20)
])
// Asignar valores iniciales a las etiquetas
titleLabel.text = title
subtitleLabel.text = subtitle
}
// MARK: - Layout Cycle Overrides
override func layoutSubviews() {
super.layoutSubviews()
// Aquí podemos hacer ajustes de layout basados en el tamaño final de la vista
// Por ejemplo, ajustar la sombra si la forma de la vista puede cambiar dinámicamente
// La ruta de la sombra debería coincidir con el `cornerRadius`
layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
}
}
Paso 2: Entendiendo los initializers
Cuando creas una UIView personalizada, es crucial entender los inicializadores:
override init(frame: CGRect): Este se usa cuando creas la vista programáticamente (sin Storyboard/XIB).required init?(coder: NSCoder): Este se usa cuando la vista se carga desde un Storyboard o un archivo XIB. El@requiredsignifica que todas las subclases deben implementar este inicializador.
En ambos, llamamos a super.init(...) para asegurarnos de que la superclase (UIView) se inicialice correctamente y luego llamamos a setupView() para configurar nuestra vista.
Paso 3: Configurando subviews y Auto Layout
En setupView(), realizamos varias tareas:
- Estilo de la vista contenedora:
layer.cornerRadius,layer.shadowpara darle un aspecto de tarjeta. - Añadir subviews: Usamos
addSubview()para añadir lasUILabela nuestraCustomCardView. - Auto Layout: Configuramos las restricciones para posicionar
titleLabelysubtitleLabel. ¡RecuerdatranslatesAutoresizingMaskIntoConstraints = falsepara cualquier vista que quieras controlar con Auto Layout!
Paso 4: Usando la CustomCardView
Ahora puedes usar tu CustomCardView en cualquier UIViewController o incluso dentro de otra UIView:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemGray6
let cardView = CustomCardView()
cardView.title = "Bienvenido al Tutorial 👋"
cardView.subtitle = "Aprende a crear vistas personalizadas."
cardView.cardBackgroundColor = .systemTeal
cardView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(cardView)
NSLayoutConstraint.activate([
cardView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
cardView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
cardView.widthAnchor.constraint(equalToConstant: 300),
cardView.heightAnchor.constraint(equalToConstant: 180)
])
// Otra tarjeta con diferentes propiedades
let anotherCardView = CustomCardView()
anotherCardView.title = "¡Componentes Reusables! ✨"
anotherCardView.subtitle = "Acelera tu desarrollo."
anotherCardView.cardBackgroundColor = .systemIndigo
anotherCardView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(anotherCardView)
NSLayoutConstraint.activate([
anotherCardView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
anotherCardView.topAnchor.constraint(equalTo: cardView.bottomAnchor, constant: 20),
anotherCardView.widthAnchor.constraint(equalToConstant: 300),
anotherCardView.heightAnchor.constraint(equalToConstant: 120)
])
}
}
🎨 Dibujo Personalizado con Core Graphics (draw(rect:))
Cuando los componentes estándar y las subviews no son suficientes, puedes tomar el control total del dibujo utilizando Core Graphics dentro del método draw(rect:).
Entendiendo draw(rect:)
- El método
draw(rect:)es llamado por el sistema cuando la vista necesita ser dibujada o redibujada. - Solo debes implementar
draw(rect:)si necesitas hacer un dibujo completamente personalizado, como formas, degradados o gráficos. Para la mayoría de los casos de UI con texto e imágenes, usarUILabel,UIImageViewysubviewses más eficiente. - Para forzar un redibujo, llama a
setNeedsDisplay(). El sistema agrupará las solicitudes y llamará adraw(rect:)en el próximo ciclo de dibujo.
Vamos a crear una CustomProgressView que muestre un progreso circular.
import UIKit
class CustomProgressView: UIView {
var progress: CGFloat = 0.0 {
didSet {
// Aseguramos que el progreso esté entre 0 y 1
progress = max(0, min(1, progress))
// Forzamos un redibujo de la vista
setNeedsDisplay()
}
}
var progressBarColor: UIColor = .systemGreen
var trackColor: UIColor = .systemGray5
var lineWidth: CGFloat = 10.0
var showPercentage: Bool = true
// MARK: - Initializers
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear // Importante para que el fondo sea transparente
}
required init?(coder: NSCoder) {
super.init(coder: coder)
backgroundColor = .clear
}
// MARK: - Drawing
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else { return }
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = (min(bounds.width, bounds.height) / 2) - (lineWidth / 2)
let startAngle: CGFloat = -.pi / 2 // Empieza arriba
let endAngle: CGFloat = (2 * .pi * progress) - (.pi / 2)
// 1. Dibujar el 'track' (fondo de la barra de progreso)
context.addArc(
center: center,
radius: radius,
startAngle: 0,
endAngle: 2 * .pi,
clockwise: false
)
context.setStrokeColor(trackColor.cgColor)
context.setLineWidth(lineWidth)
context.setLineCap(.round) // Opcional: puntas redondeadas
context.strokePath()
// 2. Dibujar la barra de progreso
context.addArc(
center: center,
radius: radius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: false
)
context.setStrokeColor(progressBarColor.cgColor)
context.setLineWidth(lineWidth)
context.setLineCap(.round)
context.strokePath()
// 3. Dibujar el porcentaje (opcional)
if showPercentage {
let percentageText = "\(Int(progress * 100))%"
let attributes: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: 20, weight: .bold),
.foregroundColor: UIColor.label // Color adaptable al modo oscuro
]
let attributedText = NSAttributedString(string: percentageText, attributes: attributes)
let textSize = attributedText.size()
let textRect = CGRect(
x: center.x - textSize.width / 2,
y: center.y - textSize.height / 2,
width: textSize.width,
height: textSize.height
)
attributedText.draw(in: textRect)
}
}
}
Usando CustomProgressView
import UIKit
class DrawingViewController: UIViewController {
private let progressView = CustomProgressView()
private let slider = UISlider()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
title = "Progreso Circular"
setupProgressView()
setupSlider()
}
private func setupProgressView() {
progressView.translatesAutoresizingMaskIntoConstraints = false
progressView.progressBarColor = .systemPurple
progressView.trackColor = .systemGray4
progressView.lineWidth = 15
progressView.progress = 0.25 // Progreso inicial
view.addSubview(progressView)
NSLayoutConstraint.activate([
progressView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
progressView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -50),
progressView.widthAnchor.constraint(equalToConstant: 200),
progressView.heightAnchor.constraint(equalToConstant: 200)
])
}
private func setupSlider() {
slider.translatesAutoresizingMaskIntoConstraints = false
slider.minimumValue = 0.0
slider.maximumValue = 1.0
slider.value = Float(progressView.progress)
slider.addTarget(self, action: #selector(sliderValueChanged(_:)), for: .valueChanged)
view.addSubview(slider)
NSLayoutConstraint.activate([
slider.topAnchor.constraint(equalTo: progressView.bottomAnchor, constant: 40),
slider.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40),
slider.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40)
])
}
@objc private func sliderValueChanged(_ sender: UISlider) {
progressView.progress = CGFloat(sender.value)
}
}
⚡ Manejo de Eventos: De UIView a UIControl
Cuando tu Custom View necesita responder a interacciones de usuario de una manera más estructurada, o si quieres que se comporte como un control estándar de UIKit (como un botón o un slider), subclasar UIControl es la solución.
UIControl introduce el mecanismo target-action, una forma potente de comunicar eventos desde el control a otros objetos.
Vamos a transformar nuestra CustomCardView en una TappableCardControl que notifica cuando es tocada.
Paso 1: Subclase UIControl
import UIKit
class TappableCardControl: UIControl {
var title: String = "" {
didSet { titleLabel.text = title }
}
var subtitle: String = "" {
didSet { subtitleLabel.text = subtitle }
}
var cardBackgroundColor: UIColor = .systemBlue {
didSet { backgroundColor = cardBackgroundColor }
}
private let titleLabel: UILabel = {
let label = UILabel()
label.font = UIFont.preferredFont(forTextStyle: .headline)
label.textColor = .white
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private let subtitleLabel: UILabel = {
let label = UILabel()
label.font = UIFont.preferredFont(forTextStyle: .subheadline)
label.textColor = .white.withAlphaComponent(0.8)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
// MARK: - Initializers
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
// MARK: - Setup Methods
private func setupView() {
layer.cornerRadius = 12
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.2
layer.shadowOffset = CGSize(width: 0, height: 4)
layer.shadowRadius = 8
backgroundColor = cardBackgroundColor
addSubview(titleLabel)
addSubview(subtitleLabel)
NSLayoutConstraint.activate([
titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 20),
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),
subtitleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
subtitleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
subtitleLabel.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -20)
])
titleLabel.text = title
subtitleLabel.text = subtitle
}
// MARK: - Handling Touch Events (Overrides de UIControl)
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
// Animación de resaltado al tocar
UIView.animate(withDuration: 0.1) { [weak self] in
self?.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
self?.alpha = 0.7
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
// Restaurar estado al soltar
UIView.animate(withDuration: 0.1) { [weak self] in
self?.transform = .identity
self?.alpha = 1.0
}
// Enviar la acción principal del control
sendActions(for: .touchUpInside)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
// Restaurar estado si el toque es cancelado
UIView.animate(withDuration: 0.1) { [weak self] in
self?.transform = .identity
self?.alpha = 1.0
}
}
override func layoutSubviews() {
super.layoutSubviews()
layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
}
}
Paso 2: Implementando el target-action
UIControl nos da los métodos sendActions(for: .touchUpInside) (y otros UIControl.Event) para notificar a los 'targets' que se han añadido. Al sobrescribir touchesEnded, disparamos el evento .touchUpInside.
Paso 3: Usando la TappableCardControl
Ahora podemos añadir un 'target' y una 'action' a nuestra tarjeta de la misma manera que lo haríamos con un UIButton.
import UIKit
class ControlViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()+
view.backgroundColor = .systemGray6
title = "Controles Personalizados"
let cardControl = TappableCardControl()
cardControl.title = "¡Soy un Control! 👆"
cardControl.subtitle = "Tócame para ver la acción."
cardControl.cardBackgroundColor = .systemOrange
cardControl.translatesAutoresizingMaskIntoConstraints = false
// Añadir el target y la acción
cardControl.addTarget(self, action: #selector(cardTapped), for: .touchUpInside)
view.addSubview(cardControl)
NSLayoutConstraint.activate([
cardControl.centerXAnchor.constraint(equalTo: view.centerXAnchor),
cardControl.centerYAnchor.constraint(equalTo: view.centerYAnchor),
cardControl.widthAnchor.constraint(equalToConstant: 320),
cardControl.heightAnchor.constraint(equalToConstant: 150)
])
}
@objc private func cardTapped() {
let alert = UIAlertController(title: "Tarjeta Tocada", message: "Has interactuado con el control personalizado.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
}
}
🌐 Haciendo tu Custom View Configurable y Extensible
Una buena Custom View es fácil de usar y personalizar. Aquí te mostramos cómo mejorar la flexibilidad de tus componentes.
1. Propiedades IBDesignable y IBInspectable
Para poder previsualizar tu Custom View en Interface Builder y editar sus propiedades directamente desde el panel de atributos, usa @IBDesignable y @IBInspectable.
@IBDesignable: Hace que tu vista sea renderizable en Interface Builder. Xcode compilará tu vista y la mostrará en el lienzo.@IBInspectable: Expone una propiedad de tu vista al panel de atributos de Interface Builder, permitiéndote modificarla sin código.
Ejemplo en TappableCardControl.swift:
import UIKit
@IBDesignable // Hace que la vista sea renderizable en Interface Builder
class TappableCardControl: UIControl {
@IBInspectable var title: String = "" {
didSet { titleLabel.text = title }
}
@IBInspectable var subtitle: String = "" {
didSet { subtitleLabel.text = subtitle }
}
@IBInspectable var cardBackgroundColor: UIColor = .systemBlue {
didSet { backgroundColor = cardBackgroundColor }
}
@IBInspectable var cornerRadius: CGFloat = 12 {
didSet { layer.cornerRadius = cornerRadius; setNeedsDisplay() }
}
@IBInspectable var shadowOpacity: Float = 0.2 {
didSet { layer.shadowOpacity = shadowOpacity; setNeedsDisplay() }
}
@IBInspectable var shadowRadius: CGFloat = 8 {
didSet { layer.shadowRadius = shadowRadius; setNeedsDisplay() }
}
// ... (resto del código de la clase, incluyendo subviews y setupView)
override func layoutSubviews() {
super.layoutSubviews()
// Actualizamos shadowPath si cornerRadius o bounds cambian
layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
}
}
Con esto, al arrastrar un UIView genérico desde la librería de objetos a tu Storyboard y cambiar su clase a TappableCardControl, verás tu diseño personalizado y podrás ajustar title, subtitle, cornerRadius, etc., directamente en el Inspector de Atributos.
2. Protocolos y Delegados para Comunicación 💪
Para una comunicación más compleja o específica que el target-action (que es más genérico para eventos de control), los protocolos y delegados son la mejor opción. Permiten que tu Custom View comunique eventos o datos a un objeto delegado, manteniendo la vista independiente y reutable.
Ejemplo: Imagina una UserAvatarView que muestra la imagen de un usuario y permite tocarla para ver su perfil. La vista en sí no debería saber cómo mostrar el perfil, solo que se tocó. El delegado se encargaría de eso.
import UIKit
// 1. Definir el protocolo del delegado
protocol UserAvatarViewDelegate: AnyObject {
func userAvatarViewDidTapAvatar(_ avatarView: UserAvatarView, forUserID userID: String)
}
class UserAvatarView: UIView {
// 2. Declarar una propiedad `delegate` débil
weak var delegate: UserAvatarViewDelegate? // Usar `weak` para evitar ciclos de referencia
private let profileImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private let usernameLabel: UILabel = {
let label = UILabel()
label.font = UIFont.preferredFont(forTextStyle: .caption1)
label.textColor = .label
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
var userID: String? // Propiedad para identificar al usuario
// MARK: - Initializers
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
private func setupView() {
addSubview(profileImageView)
addSubview(usernameLabel)
NSLayoutConstraint.activate([
profileImageView.topAnchor.constraint(equalTo: topAnchor),
profileImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
profileImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
profileImageView.heightAnchor.constraint(equalTo: profileImageView.widthAnchor),
usernameLabel.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 4),
usernameLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
usernameLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
usernameLabel.bottomAnchor.constraint(equalTo: bottomAnchor)
])
// Añadir reconocimiento de gestos para la interactividad
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
addGestureRecognizer(tapGesture)
isUserInteractionEnabled = true // Asegúrate de que la vista puede recibir toques
}
// Actualizar la imagen y el nombre
func configure(withImage image: UIImage?, username: String, userID: String) {
profileImageView.image = image
usernameLabel.text = username
self.userID = userID
}
override func layoutSubviews() {
super.layoutSubviews()
// Aseguramos que la imagen sea circular
profileImageView.layer.cornerRadius = profileImageView.bounds.height / 2
}
@objc private func handleTap() {
guard let userID = userID else { return }
// 3. Notificar al delegado
delegate?.userAvatarViewDidTapAvatar(self, forUserID: userID)
}
}
Implementando el Delegado en un UIViewController:
import UIKit
class ProfileViewController: UIViewController, UserAvatarViewDelegate {
private let avatarView = UserAvatarView()
private let statusLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
title = "Perfil de Usuario"
setupAvatarView()
setupStatusLabel()
}
private func setupAvatarView() {
avatarView.translatesAutoresizingMaskIntoConstraints = false
avatarView.delegate = self // ¡Importante: Asignar el delegado!
avatarView.configure(withImage: UIImage(systemName: "person.circle.fill"), username: "Jane Doe", userID: "user123")
view.addSubview(avatarView)
NSLayoutConstraint.activate([
avatarView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
avatarView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 50),
avatarView.widthAnchor.constraint(equalToConstant: 100),
avatarView.heightAnchor.constraint(equalToConstant: 120)
])
}
private func setupStatusLabel() {
statusLabel.translatesAutoresizingMaskIntoConstraints = false
statusLabel.text = "Toca el avatar para ver el perfil completo."
statusLabel.textColor = .secondaryLabel
statusLabel.textAlignment = .center
view.addSubview(statusLabel)
NSLayoutConstraint.activate([
statusLabel.topAnchor.constraint(equalTo: avatarView.bottomAnchor, constant: 20),
statusLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
statusLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
])
}
// MARK: - UserAvatarViewDelegate
func userAvatarViewDidTapAvatar(_ avatarView: UserAvatarView, forUserID userID: String) {
print("Avatar tocado para el usuario con ID: \(userID)")
let alert = UIAlertController(title: "Perfil de Usuario", message: "Cargando perfil de: \(userID)", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
}
}
Este patrón es fundamental para mantener la separación de responsabilidades y la reusabilidad de tus componentes UI. La UserAvatarView es ahora un componente de UI genérico que puede ser usado en cualquier lugar, y el ProfileViewController decide qué hacer cuando el avatar es tocado.
💡 Consideraciones Avanzadas y Mejores Prácticas
Accesibilidad (Accessibility) ✅
No olvides la accesibilidad. UIKit facilita mucho la creación de interfaces accesibles para todos los usuarios, incluyendo aquellos con discapacidades. Para Custom Views:
- Asegúrate de que las propiedades
isAccessibilityElement,accessibilityLabel,accessibilityHint,accessibilityValueyaccessibilityTraitsestén configuradas correctamente. - Para Custom Controls, asegúrate de que VoiceOver pueda interactuar con ellos, simulando los eventos que el usuario de pantalla haría.
// En tu TappableCardControl
override func setupView() {
// ... (configuración existente)
// Configuración de accesibilidad
isAccessibilityElement = true
accessibilityLabel = "Tarjeta pulsable con título: \(title)"
accessibilityHint = "Doble toque para activar esta tarjeta."
accessibilityTraits = .button // O el rasgo más apropiado
}
Internacionalización (Localization) 🌍
Si tu aplicación va a ser usada en diferentes idiomas, asegúrate de que todos los textos en tus Custom Views sean localizables. Usa NSLocalizedString o cadenas de texto configurables a través de propiedades.
// Ejemplo de texto localizable
usernameLabel.text = NSLocalizedString("username_default_text", comment: "Default text for username label")
Rendimiento 🏎️
- Evita redibujos innecesarios: Implementa
draw(rect:)solo cuando sea estrictamente necesario. Si solo estás moviendo subviews, usa Auto Layout o manipulaframedirectamente. - Cachea cálculos costosos: Si tienes cálculos complejos para el dibujo, cacheados y solo recalcularlos cuando los parámetros relevantes cambien.
opaque = true: Si tu vista es completamente opaca (no tiene transparencias y cubre todo su fondo), estableceisOpaque = truepara que UIKit optimice el dibujo, evitando dibujar contenido detrás de ella.shouldRasterize: Para vistas complejas que no cambian a menudo pero son costosas de renderizar, puedes usarlayer.shouldRasterize = trueconlayer.rasterizationScale = UIScreen.main.scale. Esto rasteriza la vista en una imagen y la cachea, lo que puede mejorar el rendimiento en animaciones, pero puede degradar la calidad si la vista se escala.
Pruebas Unitarias (Unit Testing) 🧪
Diseña tus Custom Views de manera que sean fáciles de testear. Separa la lógica de presentación de la lógica de negocio. Puedes probar:
- Que las propiedades configuran correctamente la UI.
- Que los eventos se disparan cuando deben.
- Que el layout se adapta a diferentes tamaños.
🎁 Envolviendo Componentes Existentes
A veces, en lugar de construir una vista desde cero, solo necesitas envolver y mejorar un componente existente de UIKit. Esto es ideal para aplicar un estilo consistente o añadir funcionalidades específicas sin reescribir todo.
Ejemplo: Un CustomTextField con icono y validación.
Podrías crear una UIView que contenga un UITextField, un UIImageView para el icono y un UILabel para mensajes de error. Luego, expones propiedades y métodos para interactuar con el UITextField interno.
import UIKit
class CustomTextField: UIView {
let textField: UITextField = {
let tf = UITextField()
tf.borderStyle = .none
tf.translatesAutoresizingMaskIntoConstraints = false
return tf
}()
let iconImageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFit
iv.translatesAutoresizingMaskIntoConstraints = false
iv.tintColor = .systemGray
return iv
}()
let errorLabel: UILabel = {
let label = UILabel()
label.font = UIFont.preferredFont(forTextStyle: .caption2)
label.textColor = .systemRed
label.isHidden = true
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
@IBInspectable var placeholder: String? {
didSet { textField.placeholder = placeholder }
}
@IBInspectable var icon: UIImage? {
didSet { iconImageView.image = icon }
}
// ... inicializadores y setupView
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
private func setupView() {
backgroundColor = .systemBackground
layer.cornerRadius = 8
layer.borderWidth = 1
layer.borderColor = UIColor.systemGray4.cgColor
addSubview(iconImageView)
addSubview(textField)
addSubview(errorLabel)
NSLayoutConstraint.activate([
iconImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
iconImageView.centerYAnchor.constraint(equalTo: textField.centerYAnchor),
iconImageView.widthAnchor.constraint(equalToConstant: 24),
iconImageView.heightAnchor.constraint(equalToConstant: 24),
textField.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 8),
textField.topAnchor.constraint(equalTo: topAnchor, constant: 10),
textField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
textField.heightAnchor.constraint(equalToConstant: 30),
errorLabel.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: 4),
errorLabel.leadingAnchor.constraint(equalTo: textField.leadingAnchor),
errorLabel.trailingAnchor.constraint(equalTo: textField.trailingAnchor),
errorLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10)
])
}
func showError(_ message: String?) {
errorLabel.text = message
errorLabel.isHidden = (message == nil)
layer.borderColor = (message == nil) ? UIColor.systemGray4.cgColor : UIColor.systemRed.cgColor
}
func hideError() {
showError(nil)
}
}
Este patrón es extremadamente útil para crear un sistema de diseño propio y asegurar que todos los UITextField (o cualquier otro control) en tu aplicación tengan la misma apariencia y comportamiento. Puedes añadir más propiedades para controlar el tipo de teclado, el texto seguro, etc., simplemente delegando al UITextField interno.
🔚 Conclusión
¡Felicidades! Has recorrido un camino completo para dominar la creación de Custom Views y Controles en iOS con Swift y UIKit. Desde los fundamentos de UIView y UIControl hasta técnicas avanzadas de dibujo con Core Graphics, manejo de eventos, y la mejora de la reusabilidad con IBDesignable/IBInspectable y delegados.
La capacidad de construir tus propios componentes te da una libertad de diseño sin precedentes y la habilidad de crear aplicaciones verdaderamente únicas y eficientes. Recuerda siempre priorizar la reusabilidad, la mantenibilidad y la accesibilidad en tus diseños. ¡Ahora estás listo para crear interfaces sorprendentes!
Tutoriales relacionados
- Desarrollo de Widgets Interactivos en iOS 17 con WidgetKit y SwiftUIintermediate20 min
- Gestión Eficaz de Dependencias en iOS: Integrando Swift Package Manager como Profesionalintermediate25 min
- ¡Desbloquea Core ML! Integrando Modelos de Machine Learning en tus Apps iOS con Swiftintermediate18 min
- Maestría en SwiftData: Persistencia de Datos de Próxima Generación en iOS con SwiftUIintermediate20 min
- Desbloquea ARKit: Realidad Aumentada en iOS con SwiftUI y USDZintermediate20 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!