Nos encontramos a las puertas de la World Wide Developers Conference de 2020. Lo que significa que ya ha pasado prácticamente un año desde la presentación de SwiftUI. Sin embargo, swiftUI es todavía una tecnología en un estado muy primario y difícil encontrar en proyectos de ámbito empresarial.
Como ya pasó con la transición de Objective-C a Swift, el cambio de una tecnología a otra será un proceso lento y gradual dónde ambas librerías convivirán, quizás mucho mas de lo que podamos pensar a priori.
En este breve artículo voy a tratar de mostrar las herramientas que nos brinda Apple para permitir hacer compatibles ambas librerías y que estas convivan en un mismo proyecto.
SwiftUI en UIKit
Este es el escenario de convivencia que con más probabilidad nos podemos encontrar hoy en dia. Muchos proyectos se encuentran ya desarrollados en UIKit y la llegada de SwiftUI nos brinda una forma declarativa de desarrollar las vistas mucho mas ágil, ligera y cómoda de lo que resultaba hasta ahora UIKit. Es por tanto muy probable que queramos integrar elementos SwiftUI en nuestro proyecto sin alterar o tener que modificar gran parte de su código o todo lo que hayamos desarrollado hasta ahora.
UIHostingController
UIHostingController es una clase que hereda de UIViewController. Dicha clase heredadada, espera en su constructor una vista SwiftUI, es decir una View.
Usar vistas SwiftUI en UIKit es, por lo tanto, muy sencillo: tan solo tenemos que implementar nuestra vista en SwiftUI y pasarla como parámetro en la construcción del UIHostingController. Automáticamente esa vista pasará a ser tratada como un UIViewController pudiendo trabajar con ella tanto desde código como mediante el Storyboard.
Veamos un ejemplo:
import Foundation
import SwiftUI
struct Card: View {
var body: some View {
VStack{
Image("EduImage")
.resizable()
.scaledToFill()
.frame(width: 200, height: 200, alignment: .center)
.cornerRadius(209, antialiased: false)
.shadow(radius: 15)
Text("Eduard Calero").font(.headline)
Text("iOS Developer").font(.subheadline)
Text("iflet.tech").font(.caption)
}
}
}
struct Card_Previews: PreviewProvider {
static var previews: some View {
Card()
.previewLayout(.fixed(width: 300, height: 300))
}
}
En el código anterior, definimos una vista llamada Card, que podemos ver sobre esta linea de texto. A continuación vamos a usar nuestra vista Card dentro de una vista UIKit como por ejemplo un UIViewController. Para ello, crearemnos una clase ViewController que heredará de UIViewController. Dentro de esta clase usaremos la vista Card anteriormente creada:
import UIKit
import SwiftUI
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = #colorLiteral(red: 0.6666666865, green: 0.6666666865, blue: 0.6666666865, alpha: 1)
let cardView = UIHostingController(rootView: Card())
cardView.view.translatesAutoresizingMaskIntoConstraints = false
self.addChild(cardView)
cardView.view.frame = CGRect(x: 50, y: 50, width: 300, height: 300)
view.addSubview(cardView.view)
cardView.didMove(toParent: self)
}
}
Si analizamos en código, veremos que hmos tenido en cuenta que UIHostingController hereda de UIViewControler, y debe ser tratado en consecuancia a la hora de añadirlo a nuestro ViewController. En este caso no aplicaría la forma común usada en cualquier UIView donde la vista es instanciada y añadida como una subvista. En cualquier caso y como ah quedado demostrado, tenemos formas a pesar de esto de añadir este tipo de vistas en la jerarquía de un viewController.
UIKit en SwiftUI
Pero esto no es todo. También aunque con menos probabilidad seguramente nos podemos encontrar con el escenario inverso, es decir, que queramos usar una vista UIKit que ya teníamos previamente creada un entorno SwiftUI.
Puede que la primera impresion sea que esto no os puede resultar muy útil, ya que ¿que sentido tiene seguir usando una librería mas antigua que esta muy probablemente mas cerca de la obscolescencia? En realidad este es un enfoque que personalmente no comparto pero es que además este escenario nos aporta muchas propiedades interesantes y una de las mas destacables es el Canvas. ¿Que os parecería poder usar el Canvas en una pantalla UIKit? Seguramente todos alguna vez u otra os habéis encontrado con el tedioso proceso de tener que refinar una vista que no acaba de dar el resultado deseado y teniendo que lanzar una y otra vez el simulador para poder visualizar los que estáis implementado. Sobretodo si teneis la buena práctica de trabajar directamente sobre código. Pues esta es una nueva posibilidad que aunque con algunos defectos y dificultades nos permite visualizar el resultado de forma instantánea mediante el protocolo PreviewProvider.
Vamos a ello.
UIViewControllerRepresentable
En este ejemplo nos serviremos del protocolo UIViewControllerRepresentable. Este protocolo es el que usamos para embeber las vistas UIKit en SwiftUI y consta de 2 métodos principales:
func makeUIView(context: Context) -> UIViewController
func updateUIView(_ uiView: UIViewController, context: Context) {}
Estos métodos son los encargados de crear el objeto UIKit y de implementar las posibles modificaciones de estado que este pueda necesitar para adecuarse al estado correcto en cada momento. En realidad el protocolo esta definido mediante genéricos y la vista que acepta es cualquiera que herede de UIViewController por lo que en nuestro ejemplo usaremos un UIViewController que contiene un UITableView. Más adelante veremos porqué.
La primera vez que se construye el objeto, ambos métodos son llamados y a partir de ahí updateUIView es llamado sucesivamente en caso de tener que actualizar su estado.
func makeUIViewController(context: Context) -> UIViewController {
let tableView = UITableView()
let viewController = UIViewController()
viewController.view.addSubview(tableView)
tableView.frame = viewController.view.frame
return viewController
}
func updateUIViewController(_ tableView: UIViewController, context: Context){
}
Como podemos ver si ejecutamos el preview. El contenido es mostrado:
Pero de poco nos va a servir ver una vista, si este no puede mostrar nada. Es aqui donde necesitamos al Coordinator.
El Coordinator es un objeto anidado dentro de la propia clase que conforma UIViewControllerRepresentable, que nos va a permitir implementar los patrones asíncronos basados en eventos propios de UIKit. Esto significa que es el objeto a partir del cual vamos a implementar las modificaciones de estado. En este caso implementará el UITableViewDelegate y el UITableViewDataSource de la tabla.
La forma de crear esta clase es implementando el método opcional: func makeCoordinator() -> Coordinator . Una vez creado, el Coordinator es accedido en el método de construcción y refresco a través del contexto.
Por lo tanto ahora tenemos algo como lo siguiente:
struct SwiftUITableViewController: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIViewController {
let tableView = UITableView()
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
let viewController = UIViewController()
viewController.view.addSubview(tableView)
tableView.frame = viewController.view.frame
return viewController
}
func updateUIViewController(_ tableView: UIViewController, context: Context){
}
class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
var parent: SwiftUITableViewController
init(_ pageViewController: SwiftUITableViewController) {
self.parent = pageViewController
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
<#code#>
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
<#code#>
}
}
}
Ahora vamos a rellenar nuestra tabla de datos.
Para ello vamos a extraer datos de https://openweathermap.org/current y los vamos a almacenar como un archivo JSON en nuestro proyecto.
A partir de los datos vamos a extraer un modelo de datos donde poder almacenar adecuadamente dichos datos. Para ello usaremos https://app.quicktype.io/ que es una herramienta muy útil para mapear modelos a partir de su JSON.
// This file was generated from JSON Schema using quicktype, do not modify it directly.
// To parse the JSON, add this file to your project and do:
//
// let countriesWeatherModel = try? newJSONDecoder().decode(CountriesWeatherModel.self, from: jsonData)
// MARK: - CountriesWeatherModel
struct CountriesWeatherModel: Codable {
let cod: String?
let calctime: Double?
let cnt: Int?
let list: [List]
}
// MARK: - List
struct List: Codable {
let id: Int?
let name: String?
let coord: Coord?
let main: MainClass?
let dt: Int?
let wind: Wind?
let rain: Rain?
let clouds: Clouds?
let weather: [Weather]?
}
// MARK: - Clouds
struct Clouds: Codable {
let all: Int?
}
// MARK: - Coord
struct Coord: Codable {
let lon, lat: Double?
}
// MARK: - MainClass
struct MainClass: Codable {
let temp, tempMin, tempMax, pressure: Double?
let seaLevel, grndLevel: Double?
let humidity: Int?
enum CodingKeys: String, CodingKey {
case temp
case tempMin
case tempMax
case pressure
case seaLevel
case grndLevel
case humidity
}
}
// MARK: - Rain
struct Rain: Codable {
let the3H: Double?
enum CodingKeys: String, CodingKey {
case the3H
}
}
// MARK: - Weather
struct Weather: Codable {
let id: Int?
let main: MainEnum?
let weatherDescription, icon: String?
enum CodingKeys: String, CodingKey {
case id, main
case weatherDescription
case icon
}
}
enum MainEnum: String, Codable {
case clouds = "Clouds"
case rain = "Rain"
}
// MARK: - Wind
struct Wind: Codable {
let speed, deg: Double?
let varBeg, varEnd: Int?
enum CodingKeys: String, CodingKey {
case speed, deg
case varBeg
case varEnd
}
}
Estos datos van a ser recuperados y almacenados en nuestra clase:
var data: CountriesWeatherModel = {
if let url = Bundle.main.url(forResource: "Weather", withExtension: "json"),
let json = try? Data(contentsOf: url, options: .mappedIfSafe),
let response = try? JSONDecoder().decode(CountriesWeatherModel.self, from: json) {
return response
}
return CountriesWeatherModel(cod: "", calctime: 0, cnt: 0, list: [])
}()
Finalmente y una vez tenemos datos con los que rellenar de contenido la tabla podemos ver que todo funciona correctamente:
struct SwiftUITableViewController: UIViewControllerRepresentable {
var data: CountriesWeatherModel = {
if let url = Bundle.main.url(forResource: "Weather", withExtension: "json"),
let json = try? Data(contentsOf: url, options: .mappedIfSafe),
let response = try? JSONDecoder().decode(CountriesWeatherModel.self, from: json) {
return response
}
return CountriesWeatherModel(cod: "", calctime: 0, cnt: 0, list: [])
}()
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIViewController {
let tableView = UITableView()
tableView.delegate = context.coordinator
tableView.dataSource = context.coordinator
let viewController = UIViewController()
viewController.view.addSubview(tableView)
tableView.frame = viewController.view.frame
return viewController
}
func updateUIViewController(_ tableView: UIViewController, context: Context){
}
class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
var parent: SwiftUITableViewController
init(_ pageViewController: SwiftUITableViewController) {
self.parent = pageViewController
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return parent.data.list.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let data = parent.data.list[indexPath.row]
let cell = UITableViewCell( style: .value1, reuseIdentifier: "CovCell" )
cell.textLabel?.text = data.name
cell.detailTextLabel?.text = "\(data.main?.temp ?? 0) ºC"
return cell
}
}
}
struct SwiftUITableViewController_Previews: PreviewProvider {
static var previews: some View {
SwiftUITableViewController()
}
}
¡Voilà! Este es el resultado:
Si probamos de hacer modificaciones en el código veremos que el resultado es mostrado en vivo, lo que nos abre un amplio abanico de posibilidades. A partir de aqui es cuestion de cada uno explorar que beneficios y fortalezas puede encontrar a esta técnia que se adaptena sus necesidades.
Espero que os sea de utilidad.
Saludos,
Edu