PhotosPicker en SwiftUI 4
PhotosPicker nos permite seleccionar archivos de nuestra galería de imágenes del iPhone (del simulador de Xcode o de un dispositivo físico). Hoy aprendemos a usar PhotosPicker, poder seleccionar varias fotos de nuestra galería, y aplicar filtros para mostrar video, livePhotos, panorámicas, etc
Tabla de contenido
Hoy en SwiftBeta vamos a ver una vista que podemos usar desde SwiftUI 4. Esta vista se llama PhotosPicker, y nos permite llamar a nuestra galería de imágenes para poder cargar photos, videos, panorámicas, etc dentro de nuestra app. Antes teníamos que utilizar un wrapper y acceder al framework UIKit, pero con esta nueva mejora nos simplifica muchísimo el código dentro de nuestra app. Y es justo lo que vamos hacer hoy, vamos a explocar PhotosPicker en SwiftUI 4.
Creamos proyecto en Xcode
Lo primero de todo que vamos hacer es crear un proyecto, en este caso debemos crear el proyecto a partir de Xcode 14. Una vez estamos creando el proyecto, hay que seleccionar como interfaz SwiftUI (muy importante).
Una vez creado nos vamos al ContentView y dentro de la propiedad body vamos a empezar a crear nuestra vista.
Lo primero de todo que vamos hacer es crearun Form con una sección, dentro de esta sección vamos a añadir una Image que será la que escojamos de nuestra galería de fotos y por supuesto también vamos a añadir nuestro PhotosPicker.
import SwiftUI
import PhotosUI
struct NewContentView: View {
@State var photoSelection: PhotosPickerItem?
var body: some View {
Form {
Section("Select Photo") {
VStack {
Image(systemName: "photo.on.rectangle.angled")
.resizable()
.scaledToFit()
.frame(width: 120, height: 120)
Divider()
PhotosPicker(selection: $photoSelection, matching: .images, photoLibrary: .shared()) {
Label("Selecciona una Foto", systemImage: "photo.on.rectangle.angled")
}
}
}
}
}
}
Si ahora compilamos nuestra app y la probamos, vemos que aparece correctamente la galería de fotos, de esta manera podemos seleccionar una y al pulsar la vista PhotosPicker desaparece. Pero la vista ContentView queda intacta. Vamos a arreglarlo, y para hacerlo vamos a crear una class que se llame ViewModel. Y dentro del ViewModel añadimos dos propiedades:
final class ViewModel: ObservableObject {
@Published var image: Image = Image(systemName: "photo.on.rectangle.angled")
@Published var photoSelection: PhotosPickerItem?
}
La primera es la imagen que mostramos en el Form y la segunda es la imagen que hemos seleccionado de nuestra galería de fotos. Ahora vamos a nuestra vista ContentView y creamos una propiedad @StateObject de tipo ViewModel:
final class ViewModel: ObservableObject {
@Published var image: Image = Image(systemName: "photo.on.rectangle.angled")
@Published var photoSelection: PhotosPickerItem?
}
struct NewContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
Form {
Section("Select Photo") {
VStack {
viewModel.image
.resizable()
.scaledToFit()
.frame(width: 120, height: 120)
Divider()
PhotosPicker(selection: $viewModel.photoSelection, matching: .images, photoLibrary: .shared()) {
Label("Selecciona una Foto", systemImage: "photo.on.rectangle.angled")
}
}
}
}
}
}
Fíjate que hemos actualizado ContentView para usar las propiedades del ViewModel. Si compilamos y seleccionamos una foto de nuestra galería vemos que sigue sin funcionar. Nos falta un último paso, ahora en SwiftUI 4 hay una nueva manera para transportar datos dentro del sistema, y es lo que vamos a ver ahora.
Vamos a crear un método que transfiera la imagen que nosotros pulsamos al Image de nuestra vista ContentView.
final class ViewModel: ObservableObject {
@Published var image: Image = Image(systemName: "photo.on.rectangle.angled")
@Published var photoSelection: PhotosPickerItem? {
didSet {
if let photoSelection { loadTransferable(from: photoSelection) }
}
}
private func loadTransferable(from photoSelection: PhotosPickerItem) {
photoSelection.loadTransferable(type: Data.self) { result in
DispatchQueue.main.async {
guard photoSelection == self.photoSelection else { return }
switch result {
case .success(let image):
let uiImage = UIImage(data: image!)
self.image = Image(uiImage: uiImage!)
case .failure(let error):
print("Error load transferable\(error)")
self.image = Image(systemName: "multiply.circle.fill")
}
}
}
}
}
Con este pequeño cambio en nuestro ViewModel, si compilamos vemos que al seleccionar una foto de nuestra galería, se dismisea y se muestra la foto seleccionada dentro del Form!
Seleccionar múltiples imagenes en PhotosPicker
Si queremos seleccionar varias imágenes y que estas aparezcan en la vista ContentView, tenemos que modificar muy poco nuestro ViewModel. En lugar de tener Image deberemos cambiarlo a [Image] (Array de images):
import SwiftUI
import PhotosUI
final class ViewModel: ObservableObject {
@Published var image: [Image] = [Image(systemName: "photo.on.rectangle.angled")]
@Published var photoSelection: PhotosPickerItem? {
didSet {
if let photoSelection { loadTransferable(from: photoSelection) }
}
}
private func loadTransferable(from photoSelection: PhotosPickerItem) {
photoSelection.loadTransferable(type: Data.self) { result in
DispatchQueue.main.async {
guard photoSelection == self.photoSelection else { return }
switch result {
case .success(let image):
let uiImage = UIImage(data: image!)
self.image.append(Image(uiImage: uiImage!))
case .failure(let error):
print("Error load transferable\(error)")
self.image.append(Image(systemName: "multiply.circle.fill"))
}
}
}
}
}
Y en nuestro ContentView añadiríamos un ForEach:
struct NewContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
Form {
Section("Select Photo") {
VStack {
ForEach(viewModel.image, content: { image in
image
.resizable()
.scaledToFit()
.frame(width: 120, height: 120)
})
Divider()
PhotosPicker(selection: $viewModel.photoSelection, matching: .images, photoLibrary: .shared()) {
Label("Selecciona una Foto", systemImage: "photo.on.rectangle.angled")
}
}
}
}
}
}
Fíjate que el compilador se queja, nos dice que al usar un ForEach debemos conformar el protocolo Identifiable en los datos que estamos usando. En lugar de crear una struct nueva y dentro meter la image como propiedad, voy a crear una extensión de Image que conforme Identifiable:
extension Image: Identifiable {
public var id: String { UUID().uuidString }
}
Si compilamos y probamos nuestra app, vemos como van apareciendo todas las imágenes que vamos seleccionando de nuestra galería!
Filtros en PhotosPicker
Todas las pruebas que hemos hecho han sido en el simulador, pero en nuestro dispositivo físico acabamos teniendo varios tipos de archivos, con esto quiero decir, que quizás quieres mostrar en tu PhotosPicker la panorámicas, o los retratos, o incluso solo los videos. Cuando inicializamos nuestro PhotosPicker podemos filtrar estos archivos y mostrar los que queramos, tan solo debeís modificar el parámetro matching.
Por ejemplo, si queremos mostrar nuestras live photos, inicializariamos nuestro PhotosPicker de la siguiente manera:
PhotosPicker(selection: $viewModel.photoSelection, matching: .livePhotos, photoLibrary: .shared()) {
Label("Selecciona una Foto", systemImage: "photo.on.rectangle.angled")
}
Puedes acceder para ver todos los tipos que puedes usar
Incluso puedes hacer tus propias queries, es decir, si quieres tus videos y images, podrías hacer lo siguiente:
PhotosPicker(selection: $viewModel.photoSelection, matching: .any(of: [.videos, .images]), photoLibrary: .shared()) {
Label("Selecciona una Foto", systemImage: "photo.on.rectangle.angled")
}
Conclusión
Hoy hemos aprendido a cómo usar PhotosPicker, una vista que hasta ahora no podíamos usar directamente en SwiftUI, pero con SwiftUI 4 es muy simple. Nos muestra nuestra galería de imágenes (con un filtro de los archivos que queremos mostrar, videos, panorámicas, live photos, etc). De esta manera podemos escoger qué imagen queremos añadir dentro de nuestra app.