Los property wrappers es una funcionalidad disponible desde Swift 5.1 que permiten asociar lógica cuando las propiedades cambian. Esencialmente envuelven el valor original añadiendo funcionalidades. Se pueden implementar como struct o class al añadir el atributo @propertywrapper
. Para conformarse deben incluir una propiedad calculada llamada wrappedValue. Es en esta propiedad en la que se implementa la lógica al asignar un valor a la propiedad o cuando es invocada.
Os traigo el ejemplo de un property wrapper que se encargará de manejar la lectura y escritura de un dato tipo Codable en un archivo JSON en el directorio de documentos de la app.
Para poder codificar y decodificar la información se requiere que el tipo de dato cumpla con el protocolo Codable
.
DocStorage
@propertyWrapper
struct DocStorage<Value: Codable> { //..
Solicitaremos un nombre para el archivo donde se almacenará el valor, así que incluimos esa propiedad y una ruta, que hemos predefinido que sera URL.documentsDirectory
//..
var filename: String
private var url: URL
//..
Debemos añadir la propiedad wrappedValue de tipo Codable
, pero del mismo tipo que el asignado, es por ello que el tipo es Value
. Esa propiedad empaquetará una lógica al ser leída y asignada, por ello el get y el set
//..
var wrappedValue: Value? {
get {
loadJsonFromDocuments()
}
set {
saveJsonToDocuments(newValue)
}
}
//..
newValue es un valor opcional proporcionado por el set de manera automática, es el “nuevo valor” asignado a nuestra propiedad.
Implementamos las funciones que se encargarán de la lógica cuando se lea o escriba.
//..
private func loadJsonFromDocuments() -> Value? {
guard let data = try? Data(contentsOf: url),
let value = try? JSONDecoder().decode(Value.self, from: data) else { return nil }
return value
}
private func saveJsonToDocuments(_ value: Value?) {
if let value {
let data = try? JSONEncoder().encode(value)
try? data?.write(to: url, options: .atomic)
}
}
//..
El init
deberá solicitar cómo el nombre de ese archivo y añadirlo a la url. Si existe un archivo en esa ruta con ese nombre se cargará y decodificará para asignarlo a la “propiedad empaquetada”.
init(wrappedValue: Value?, fileName: String) {
self.fileName = fileName
url = URL.documentsDirectory.appendingPathComponent(fileName, conformingTo: .json)
if FileManager.default.fileExists(atPath: url.absoluteString) {
self.wrappedValue = loadJsonFromDocuments()
}
}
}
Uso
En nuestro View Model definimos nuestras propiedades empaquetadas.
Aquí es donde debemos declarar el tipo de dato, en fileName el nombre del archivo y el nombre de la propiedad. En este caso se asigna un Array vacío en caso de que al ejecutar el get haya devuelto nil.
final class DocViewModel: ObservableObject {
@DocStorage<[Mobile]>(fileName: "mobiles") var mobilesFile = []
@DocStorage<[SmartTv]>(fileName: "smartTvs") var smartTvsFile = []
//..
Definir las propiedades @Published
que serán utilizas por la view. En este caso llevan el observador de propiedad didSet
, en la que cada vez que haya un cambio, éste lo asignará a nuestro propertywrapper @DocStorage
//..
@Published var mobiles: [Mobile] = [] {
didSet { mobilesFile = mobiles }
}
@Published var tvs: [SmartTv] = [] {
didSet { smartTvsFile = tvs }
}
//..
Finalmente cuando el DocViewModel
se inicialice deberá asignar los valores de @DocStorage
a nuestro @Published
//..
init() {
loadFrom(docStorage: mobilesFile, to: &mobiles)
loadFrom(docStorage: smartTvsFile, to: &tvs)
}
//..
La función puede inferir de la propiedad a la que vamos a asignar el tipo de dato. Hay que tener en cuenta que el valor de property es inout
, que indica que el valor que pasamos a la función puede ser modificado y que dichos cambios se verán reflejados en esa misma variable. Es decir pasamos la referencia de la propiedad y no una copia por valor.
Para hacer explicito, en la invocación de la función añadimos el modificador &
//..
private func loadFrom<J: Codable>(docStorage: J?, to property: inout J) {
if let docStorage {
property = docStorage
}
}
//..
Con ello tenemos una propiedad que carga y persiste los datos en disco, que va a ser invocada al inicializarse el DocViewModel
y cuando haya una modificación en los datos de la propiedad @Published
.
Hemos visto como crear un propertywrapper personalizado, que tiene una funcionalidad equivalente a @AppStorage
(UserDefaults).
Podemos hacer nuestros property wrappers por ejemplo para validar los datos de entrada, añadir un formato específico, cifrar y descifrar valores o guardar y recuperar del KeyChain.
Espero te haya sido útil y comiences a incluir property wrappers o empaquetadores de propiedades en tus proyectos.
Aquí tienes el código completo
@propertyWrapper
struct DocStorage<Value:Codable> {
var fileName: String
private var url: URL
init(wrappedValue: Value?, fileName: String) {
self.fileName = fileName
url = URL.documentsDirectory.appendingPathComponent(fileName, conformingTo: .json)
if FileManager.default.fileExists(atPath: url.absoluteString) {
self.wrappedValue = loadJsonFromDocuments()
}
}
var wrappedValue: Value? {
get { loadJsonFromDocuments() }
set { saveJsonInDocuments(newValue) }
}
private func loadJsonFromDocuments() -> Value? {
guard let data = try? Data(contentsOf: url),
let value = try? JSONDecoder().decode(Value.self, from: data) else { return nil }
return value
}
private func saveJsonInDocuments(_ value: Value?) {
if let value {
let data = try? JSONEncoder().encode(value)
try? data?.write(to: url, options: .atomic)
}
}
}
final class DocViewModel: ObservableObject {
@DocStorage<[Mobile]>(fileName: "mobiles") var mobilesFile = []
@DocStorage<[SmartTv]>(fileName: "smartTvs") var smartTvsFile = []
@DocStorage<String>(fileName:"cadena") var cadena = ""
@Published var mobiles: [Mobile] = [] {
didSet { mobilesFile = mobiles }
}
@Published var tvs: [SmartTv] = [] {
didSet { smartTvsFile = tvs }
}
init() {
loadFrom(docStorage: mobilesFile, to: &mobiles)
loadFrom(docStorage: smartTvsFile, to: &tvs)
}
}
¿Quieres recibir posts, cheatCodes, enlaces y katas en Swift para practicar?
Quincenalmente recibirás en tu correo electrónico la newsletter, solo hace falta tu correo electrónico.