Cómo crear vistas en UIKit por código y aplicar AutoLayout
Cómo crear vistas en UIKit por código y aplicar AutoLayout

Cómo crear vistas por código y AutoLayout - Curso iOS en Swift

Creamos vista usando UIKit con código Swift, usamos AutoLayout y constraints. Borramos el Storyboard e instanciamos nuestro UIViewController desde el SceneDelegate. Finalmente, movemos la creación de la View en un nuevo fichero y desde el ViewController usamos el método loadView()

SwiftBeta

Tabla de contenido

Aprende a crear vistas por código y AutoLayout en UIKit (y Swift)

Hoy en SwiftBeta vamos a aprender a crear la vista de un ViewController en código Swift (sin usar storyboards). Hacer las vistas por código tiene un gran número de ventajas, y las veremos a lo largo de este post. Vamos a crear una vista muy sencilla, con un UIImageView, UILabel y UIButton. Al añadir estas vistas a la vista de nuestro UIViewController, aplicaremos constraints para que sepan cómo organizarse. Vamos a ir paso a paso, y va a ser un video muy completo.

Imagen para descargar

Image que necesitarás si quieres seguir 100% el tutorial
Image que necesitarás si quieres seguir 100% el tutorial

Crea el proyecto en Xcode

Lo primero de todo que vamos hacer es crear el proyecto de cero en Xcode. Acuerdate de seleccionar como Interface Storyboard (de esta manera se creará nuestra app para usar UIKit en lugar de SwiftUI).

Una vez creado el proyecto, nos vamos a nuestro fichero ViewController, allí vamos a crear 3 propiedades, las 3 propiedades serán 3 subvistas (UIImageView, UILabel y UIButton) que añadiremos a la vista del ViewController.

Creamos propiedad de tipo UIImageView

Lo primero de todo que vamos hacer es crear nuestro UIImageView. Y vamos a configurar la instancia para añadir una imagen:

final class ViewController: UIViewController {
    private let onboardingImageView: UIImageView = {
        let imageView = UIImageView() // 1
        imageView.contentMode = .scaleAspectFit // 2
        imageView.image = UIImage(named: "rickandmorty") // 3
        imageView.translatesAutoresizingMaskIntoConstraints = false // 4
        return imageView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        view.addSubview(onboardingImageView) // 5
    }
}
Creamos nuestro primer UIImageView de UIKit

Lo que acabamos de hacer es crear una propiedad privada llamada onboardingImageView:

  1. Creamos la instancia
  2. Asignamos el valor de scaleAspectFit a la propiedad contentMode
  3. Asignamos una imagen que préviamente hemos añadido a nuestra carpeta de Assets. Esta imagen la puedes encontrar en nuestra comunidad de Discord, en el canal de 🔴 Youtube (únete en el siguiente enlace www.swiftbeta.com/discord). Aunque si quieres puedes usar cualquier otra imagen.
  4. Asignamos false a la propiedad translatesAutoresizingMaskIntoConstraints de esta manera podremos create constraints (AutoLayout) por código
  5. Dentro del viewDidLoad añadimos nuestra instancia de onboardingImageView (UIImageView) a la view del controller
  6. OJO! Aún falta añadir las constraints de esta subvista, lo haremos más adelante.

Lo siguiente que vamos hacer es crear nuestro UILabel

Creamos propiedad de tipo UILabel

A continuación, vamos a crear otra propiedad pero esta vez será de tipo UILabel y por lo tanto tendremos que usar otras propiedades a las vistas en la sección de UIImageView. Vamos a crear esta nueva instancia, justo debajo de nuestra propiedad onboardingImageView

final class ViewController: UIViewController {
    private let onboardingImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFit
        imageView.image = UIImage(named: "rickandmorty")
        imageView.translatesAutoresizingMaskIntoConstraints = false
        return imageView
    }()
    
    private let textLabel: UILabel = {
        let label = UILabel() // 1
        label.numberOfLines = 0 // 2
        label.textAlignment = .center // 3
        label.text = "Bienvenido a la app de Rick and Morty" // 4
        label.font = UIFont(name: "Arial Rounded MT Bold", size: 26) // 5
        label.translatesAutoresizingMaskIntoConstraints = false // 6
        return label
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        [onboardingImageView,
         textLabel]
            .forEach(view.addSubview) // 7
    }
}
Creamos nuestro primer UILabel en UIKit

En este caso nuestra nueva propiedad se llama textLabel

  1. Instanciamos UILabel
  2. Asignamos 0 como numberOfLines, de esta manera si el texto no cabe en el espacio que se le proporciona al UILabel, se usarían tantas líneas como fuera necesario.
  3. Asignamos .center a la propiedad textAlignment para que el texto aparezca centrado.
  4. Damos un texto a nuestro UILabel
  5. Asignamos una fuente a nuestro UILabel
  6. Asignamos false a la propiedad translatesAutoresizingMaskIntoConstraints de esta manera podremos create constraints (AutoLayout) por código
  7. Añadimos nuestro textLabel a la vista del ViewController. En este caso lo he modificado por un forEach. En lugar de usar varias veces addSubview por cada vista que quiero añadir, uso un ForEach para solo escribirlo una vez.

Por último, vamos a añadir nuestra última vista, y será de tipo UIButton.

Creamos propiedad de tipo UIButton

Para finalizar, vamos a añadir una instancia de un UIButton a la vista de nuestro ViewController.

final class ViewController: UIViewController {
    private let onboardingImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFit
        imageView.image = UIImage(named: "rickandmorty")
        imageView.translatesAutoresizingMaskIntoConstraints = false
        return imageView
    }()
    
    private let textLabel: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0
        label.textAlignment = .center
        label.text = "Bienvenido a la app de Rick and Morty"
        label.font = UIFont(name: "Arial Rounded MT Bold", size: 26)
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    private lazy var skipOnboardingButton: UIButton = {
        var config = UIButton.Configuration.filled() // 1
        config.title = "Pulsa para continuar" // 2
        config.subtitle = "Onbording" // 3
        
        let button = UIButton(type: .system) // 4
        button.addTarget(self, action: #selector(showMessage), for: .touchUpInside) // 5
        button.configuration = config // 6
        button.translatesAutoresizingMaskIntoConstraints = false // 7
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        [onboardingImageView,
         textLabel,
         skipOnboardingButton]
            .forEach(view.addSubview)  // 8       
    }
    
    @objc
    func showMessage() { // 9
        print("Skip onboarding")
    }
}
Creamos nuestro primer UIButton

En este caso, la propiedad de nuestro UIButton se llama skipOnboardingButton

  1. Creamos la instancia de la configuración de nuestro UIButton
  2. Asignamos un título a nuestro UIButton
  3. Asignamos un subtítulo a nuestro UIButton
  4. Creamos instancia de un UIButton
  5. Añadimos una acción a nuestro UIButton, cada vez que se pulse se ejecutará el método showMessage (lo creamos en el paso 9)
  6. Asignamos la configuración de nuestro UIButton a nuestro UIButton.
  7. Asignamos false a la propiedad translatesAutoresizingMaskIntoConstraints de esta manera podremos create constraints (AutoLayout) por código
  8. Añadimos la propiedad skipOnboardingButton en nuestro Array para añadir como subvista de nuestra View del ViewController
  9. Creamos el método que se ejecutará cuando se pulse nuestro UIButton

Una vez hemos añadido todas estas subvistas, ahora necesitamos aplicar constraints a cada una de ellas.

Añadir constraints a nuestras subvistas

Para ello, vamos a crear un método nuevo llamado setupConstraints. Dentro de este método vamos a añadir todas las reglas de nuestras subvistas, así sabrán cómo distribuirse por nuestra View del ViewController. Para hacerlo vamos a utilizar la clase NSLayoutConstraint con el método activate, así aparte de crearlas las vamos a activar.

final class ViewController: UIViewController {
    private let onboardingImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFit
        imageView.image = UIImage(named: "rickandmorty")
        imageView.translatesAutoresizingMaskIntoConstraints = false
        return imageView
    }()
    
    private let textLabel: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0
        label.textAlignment = .center
        label.text = "Bienvenido a la app de Rick and Morty"
        label.font = UIFont(name: "Arial Rounded MT Bold", size: 26)
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    private lazy var skipOnboardingButton: UIButton = {
        var config = UIButton.Configuration.filled()
        config.title = "Pulsa para continuar"
        config.subtitle = "Onbording"
        
        let button = UIButton(type: .system)
        button.addTarget(self, action: #selector(showMessage), for: .touchUpInside)
        button.configuration = config
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        [onboardingImageView,
         textLabel,
         skipOnboardingButton]
            .forEach(view.addSubview)
        
        setupConstraints()
    }
    
    private func setupConstraints() {
        NSLayoutConstraint.activate([
            onboardingImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            onboardingImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            onboardingImageView.bottomAnchor.constraint(equalTo: textLabel.topAnchor,
                                                        constant: -32),
            
            textLabel.bottomAnchor.constraint(equalTo: skipOnboardingButton.topAnchor,
                                              constant: -42),
            textLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            textLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            textLabel.centerXAnchor.constraint(equalTo: skipOnboardingButton.centerXAnchor),
            
            skipOnboardingButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            skipOnboardingButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
    
    @objc
    func showMessage() {
        print("Skip onboarding")
    }
}
Usamos AutoLayout para añadir constraints a las subvistas del UIViewController

Si ahora compilamos nuestra app, vemos que todos los elementos aparecen correctamente distribuidos por la vista, y si pulsamos nuestro UIButton, vemos como se printa un mensaje por consola.

Tal y como hemos visto, hemos creado todo por código, y puede que te preguntes ¿qué sentido tiene tener el Storyboard? la respuesta es ninguno, es por eso que vamos a eliminar el Storyboard a continuación.

BONUS: Eliminar Storyboard en Xcode

  1. Lo primero de todo que vamos hacer es borrar el Storyboard de nuestro listado de ficheros.
Eliminamos el fichero Main del listado de ficheros de Xcode
Eliminamos el fichero Main del listado de ficheros de Xcode

Si ahora ejecutamos nuestra app en un simulador, veríamos un crash parecido al siguiente:

Si compilamos tenemos un crash en nuestra app
Si compilamos tenemos un crash en nuestra app

Hay que hacer unos cuantos cambios para dejar nuestro proyecto listo sin Storyboard.

2. Vamos a ir a nuestro Info.plist y vamos a borrar el valor que aparece en la siguiente imagen, vamos a borrar la key Storyboard Name.

Limpiamos referencias de nuestro Storyboard en el Info.plist
Limpiamos referencias de nuestro Storyboard en el Info.plist

3. Vamos a ir a la sección de General y vamos a eliminar la referencia que hay al fichero Main.

Eliminamos también la referencia Main del Main Interface
Eliminamos también la referencia Main del Main Interface

4. Ahora, vamos al fichero SceneDelegate, y vamos a modificar el primer método

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?


    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        window = UIWindow(windowScene: windowScene)
        window?.rootViewController = ViewController()
        window?.makeKeyAndVisible()
    }
...
Instanciamos nuestro UIViewController desde el SceneDelegate

De esta manera, creamos una instancia de nuestro ViewController y se la asignamos al rootViewController de nuestra propiedad Window.
Ahora si compilamos, deberíamos ver nuestra vista del ViewController dentro del simulador

Xcode Simulador con el resultado final
Xcode Simulador con el resultado final

Por último, vamos a separar la vista que hemos creado en nuestro ViewController. Vamos a crear un fichero llamado OnboardingView y vamos a mover el siguiente código:

import Foundation
import UIKit

final class OnboardingView: UIView {
    private let onboardingImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFit
        imageView.image = UIImage(named: "rickandmorty")
        imageView.translatesAutoresizingMaskIntoConstraints = false
        return imageView
    }()
    
    private let textLabel: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0
        label.textAlignment = .center
        label.text = "Bienvenido a la app de Rick and Morty"
        label.font = UIFont(name: "Arial Rounded MT Bold", size: 26)
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    private lazy var skipOnboardingButton: UIButton = {
        var config = UIButton.Configuration.filled()
        config.title = "Pulsa para continuar"
        config.subtitle = "Onbording"
        
        let button = UIButton(type: .system)
        button.addTarget(self, action: #selector(showMessage), for: .touchUpInside)
        button.configuration = config
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()
    
    override init(frame: CGRect) {
        super.init(frame: .zero)
        setup()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setup() {
        addSubviews()
        configureConstraints()
    }
    
    private func addSubviews() {
        backgroundColor = .white
        
        [onboardingImageView,
         textLabel,
         skipOnboardingButton]
            .forEach(addSubview)
    }
    
    private func configureConstraints() {
        NSLayoutConstraint.activate([
            onboardingImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
            onboardingImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
            onboardingImageView.bottomAnchor.constraint(equalTo: textLabel.topAnchor,
                                                        constant: -32),
            
            textLabel.bottomAnchor.constraint(equalTo: skipOnboardingButton.topAnchor,
                                              constant: -42),
            textLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
            textLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
            textLabel.centerXAnchor.constraint(equalTo: skipOnboardingButton.centerXAnchor),
            
            skipOnboardingButton.centerXAnchor.constraint(equalTo: centerXAnchor),
            skipOnboardingButton.centerYAnchor.constraint(equalTo: centerYAnchor)
        ])
    }
    
    @objc
    func showMessage() {
        print("Skip onboarding")
    }
}
Creamos una UIView llamada OnboardingView

Y en nuestro ViewController, actualizamos nuestro código para que instancie la view que acabamos de crear:

import UIKit

final class ViewController: UIViewController {
    override func loadView() {
        self.view = OnboardingView()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
    }
}
Nuestro UIViewController queda muy limpio

Hemos sobreescrito un método nuevo llamado loadView() y dentro de este método hemos creado una instancia de nuestra vista OnboardingView() y se la hemos asignado a la view de nuestro ViewController

Conclusión

Hoy hemos aprendido a crear nuestra primera vista por código. Hemos usado AutoLayout para crear constraints de todas las subvistas de nuestro ViewController. También, hemos borrado nuestro Storyboard y hemos creado la instancia de nuestro ViewController en nuestra clase SceneDelegate. Y finalmente hemos movido la creación de nuestra vista en un fichero llamado OnboardingView, gracias a esto hemos reducido significativamente nuestro ViewController.

Si quieres seguir aprendiendo sobre SwiftUI, Swift, Xcode, o cualquier tema relacionado con el ecosistema Apple