¿Qué es Image?
El componente Image
permite mostrar una imagen. Es un componente equivalente a UIImageView
de UIKit
.
Aquí podéis consultar la documentación oficial
El componente tiene varios 'inicializadores' con diferentes parámetros y propósitos. A nivel más básico, se crea de la siguiente forma:
Image("imageName")
Este método crea una imagen a partir de un String
. Este String
debe coincidir con el nombre de una imagen del .xcassets
. El .xcassets
es un tipo de fichero que podemos añadir a nuestro proyecto de Xcode y en él incluiremos todas las imágenes de nuestro proyecto (entre otros recursos).

Otro 'inicializador' interesante es el siguiente:
Image(uiImage: UIImage(named: "imageName"))
Este 'inicializador' nos permite cargar una imagen a partir de un UIImage
de UIKit
. Es muy útil cuando tenemos que coexistir con el framework de UIKit
y las imágenes ya están 'inicializadas' en formato UIImage
.
Excluir imágenes de la lectura de pantalla con VoiceOver
VoiceOver es la herramienta que Apple proporciona para la lectura de pantalla para aquellas personas invidentes o con visión reducida. Esta herramienta lee todo el contenido de la pantalla, incluidas las imágenes.
Hay ocasiones en las que las imágenes no tienen que ser leídas, ya que son elementos visuales colocados por temas estéticos (como un fondo de pantalla). Para estos casos se debería usar el siguiente 'inicializador':
Image(decorative: "imageName")
Este 'inicializador' excluye a la imagen de ser leídas por VoiceOver.
Modificadores comunes para Image
A parte de los modificadores que se explicarán a continuación, el componente Image
comparte los mismos métodos de personalización que el componente View
y pueden ser consultados en el siguiente enlace.
Para los ejemplos vamos a usar una imagen de un póster de Star Wars:

resizable
Permite redimensionar la imagen para que se ocupe todo el espacio del que disponga el componente.
Image("star_wars")
.resizable()
.frame(height: 400)

Por defecto el componente Image
define su tamaño dependiendo de la vista que cargue, por lo que muchas veces la imagen sobrepasará el propio tamaño de pantalla (prueba el código anterior sin resizable
ni frame
)
Por lo general, este modificador se usará en la mayoría de los casos.
aspectRatio
/ scaledToFit
/ scaledToFill
aspectRatio
permite indicar la relación de aspecto que debe respetar el componente. Si no se indica, la imagen se deformará para ocupar todo el contenedor disponible.
Image("star_wars")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 200)
Image("star_wars")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 100, height: 200)
.clipped()
El parámetro contentMode
puede tener dos valores:
.fit
. Indica que la imagen debe verse completamente respetando el contenedor disponible. El métodoscaledToFit()
consigue el mismo resultado que aplicar.aspectRatio(contentMode: .fit)
.fill
. Indica que la imagen debe ocupar todo el contenedor disponible aunque la imagen no se visualize completamente. El métodoscaledToFill()
consigue el mismo resultado que aplicar.aspectRatio(contentMode: .fill)
. Cuando apliquemos este modificador conviene aplicar.clipped()
para que la imagen no se pinte fuera de sus límites permitidos

interpolation
Permite modificar el nivel de suavizado de pixeles que proporciona el sistema cuando presenta una imagen pequeña en un contenedor más grande.
HStack(spacing: 20) {
VStack {
Image("lion")
.resizable()
.interpolation(.high)
.aspectRatio(contentMode: .fit)
.frame(width: 150, height: 200)
Text(".high")
}
VStack {
Image("lion")
.resizable()
.interpolation(.none)
.aspectRatio(contentMode: .fit)
.frame(width: 150, height: 200)
Text(".none")
}
}

Si no se indica este modificador el sistema aplica una interpolación de nivel .high
.
Para esta prueba puedes usar una imagen pequeña como está:

renderingMode
Permite indicar cómo se debe 'renderizar' la vista.
HStack(spacing: 20) {
VStack {
Button(action: {}, label: {
Image("marker")
.resizable()
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100)
})
Text(".template")
}
VStack {
Button(action: {}, label: {
Image("marker")
.resizable()
.renderingMode(.original)
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100)
})
Text(".original")
}
}

En algunas ocasiones el sistema aplica una capa personalizada a los componentes haciendo que estos se tinten de un color dependiendo de la configuración global o la indicada por el modificador .accentColor
.
Con este modificador podemos indicar si queremos que esto se aplique (.template
) o no (.original
).
Para esta prueba se ha usado está imagen:

Cómo definir el área visible de una vista en base a una máscara
Hay ocasiones en las que necesitamos que una vista tenga una forma irregular diferente a un cuadrado o círculo. Para estos casos se puede usar una máscara para definir el área visible de una vista, y de esta forma podemos conseguir formas irregulares de una manera muy sencilla.
Para aplicar esta máscara debemos aplicar el modificador mask
y pasarle por parámetro una vista que será la que defina la zona visible de la vista objetivo.
Rectangle()
.foregroundColor(.green)
.frame(height: 200)
.background(Color.blue)
.mask(Image("message_bubble")
.resizable(capInsets: EdgeInsets(top: 10, leading: 50, bottom: 20, trailing: 10))
)
Image("star_wars")
.resizable()
.frame(width: 300, height: 200)
.mask(Text("Hello, World!")
.font(Font.system(size: 60, weight: .bold)
)
)

Para la burbuja se ha usado la siguiente imagen:

Cómo cargar imágenes desde una URL
El componente Image
no implementa una forma de cargar imágenes desde una url, por lo que es responsabilidad de los desarrolladores implementarlo de la forma que necesiten en cada proyecto.
Esta es una necesidad muy común en todos los proyectos, por lo que vamos a implementar un nuevo componente que tenga esta capacidad de forma que pueda ser escalable para cualquier otra necesidad que pueda surgir.
¿Qué necesitamos para cargar una imagen desde una URL?
Lo primero de todo es saber que es lo que necesitamos exactamente. Para mostrar una imagen desde una URL tenemos que hacer principalmente dos cosas:
- Obtener la imagen desde la URL.
- Mostrar la imagen en pantalla.
Para conseguir el primer paso tenemos que hacer una llamada a una url que contenga una imagen con el framework de peticiones URLSession
(lo explicaremos en profundidad más adelante).
Para el segundo paso tenemos que tener en cuenta que cuando mostremos la pantalla la imagen podrá estar o no, ya que traerla desde internet puede llevar un tiempo indeterminado que dependerá de la conexión y el peso de la imagen. Entonces para mostrar la imagen correctamente tenemos que controlar dos estados de la vista: un estado donde no tenemos la imagen y otro estado donde sí la tenemos. ¿Cómo se representa esto en una pantalla con SwiftUI
? Con variables de tipo @State
:
@State var image: UIImage? = nil
Este tipo de variables son usadas para manejar los estados de las vistas y harán que las vistas se refresquen automáticamente cuando cambie su valor.
Esto quiere decir que lo que lo único que necesitaremos para mostrar una imagen desde una URL es modificar una variable de estado asignando la propia imagen a mostrar (la petición de la imagen la haremos desde la lógica de negocio de la aplicación) y el propio framework de SwiftUI
se encargará de pintar la imagen.
import SwiftUI
struct ContentView: View {
@State var image: UIImage? = nil
var body: some View {
Group {
if let image = image {
Image(uiImage: image)
.resizable()
} else {
Text("No image")
}
}.onAppear {
getImage("https://picsum.photos/id/43/3000")
}
}
func getImage(_ url: String) {
guard let url = URL(string: url) else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
if let data = data, let image = UIImage(data: data) {
self.image = image
}
}.resume()
}
}
Este ejemplo muestra inicialmente el texto "No image", ya que la variable image
es nil
.
Al mostrarse la pantalla se llama al método getImage
que se encarga de obtener la imagen y asignarla a la variable de estado image
,
por lo que ya deja de estar a nil
. Como esta variable ha cambiado su estado la pantalla se refresca mostrando la imagen porque la condición ya se cumple.

El ejemplo es ilustrativo y no se recomienda que tengamos esa función getImage
en la propia pantalla.
Creando un componente para la carga de imágenes
Una vez entendido el objetivo podemos crearnos nuestro propio componente reutilizable para todas las pantallas donde tengamos que cargar una imagen desde una URL.
Primero vamos a crear una clase que implemente el protocolo ObservableObject
. Esto nos permitirá referenciar una instancia de esta clase con el atributo @ObservableObject
(muy similar a la etiqueta @State
), haciendo que los cambios en esta clase modifiquen el estado de la vista.
import SwiftUI
import Combine
public class ImageLoader: ObservableObject {
@Published public var image: UIImage? = nil
private let url: URL?
private var cancellable: AnyCancellable? = nil
init(_ url: String?) {
if let url = url {
self.url = URL(string: url)
} else {
self.url = nil
}
}
init(_ url: URL?) {
self.url = url
}
deinit {
cancel()
}
func load() {
guard let url = url else { return }
cancel()
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.receive(on: DispatchQueue(label: "ImageQueue", qos: DispatchQoS.background))
.map {
if let image = UIImage(data: $0.data) {
return image
} else {
return nil
}
}
.replaceError(with: UIImage(named: "error"))
.replaceNil(with: UIImage(named: "error"))
.receive(on: DispatchQueue.main)
.assign(to: \.image, on: self)
}
func cancel() {
cancellable?.cancel()
}
}
La clase ImageLoader
se encargará de la lógica para solicitar una imagen desde una URL.
Después vamos a implementar nuestro componente personalizado ImageAsync
que será el que solicitará la imagen a ImageLoader
y la pintará.
import SwiftUI
public struct ImageAsync: View {
@ObservedObject var imageLoader: ImageLoader
private let content: ((Image) -> Image)?
fileprivate var placeholder: AnyView? = nil
init(_ url: String?, content: ((Image) -> Image)? = nil) {
imageLoader = ImageLoader(url)
self.content = content
}
init(_ url: URL?, placeholder: AnyView? = nil, content: ((Image) -> Image)? = nil) {
imageLoader = ImageLoader(url)
self.content = content
}
public var body: some View {
let image: Image?
if let i = imageLoader.image {
image = Image(uiImage: i)
} else {
image = nil
}
let result: AnyView?
if let image = image {
if let r = content?(image) {
result = AnyView(r)
} else {
result = AnyView(image)
}
} else {
result = AnyView(Color.clear)
}
return Group {
if imageLoader.image == nil {
result.overlay(placeholder)
} else {
result
}
}.onAppear {
imageLoader.load()
}.onDisappear {
imageLoader.cancel()
}
}
}
La clase ImageAsync
también tiene implementado una forma de colocar un placeholder
mientras que la imagen no ha sido cargada. Vamos a crear una extensión de ImageAsync
para que sea más fácil 'setear' este placeholder
.
import SwiftUI
public extension ImageAsync {
func placeholder<T: View>(@ViewBuilder content: () -> T) -> ImageAsync {
var result = self
result.placeholder = AnyView(content())
return result
}
func placeholder(_ text: Text) -> ImageAsync {
var result = self
result.placeholder = AnyView(text)
return result
}
func placeholder(_ image: Image) -> ImageAsync {
var result = self
result.placeholder = AnyView(image)
return result
}
}
De esta forma solo tendremos que usar nuestro nuevo componente ImageAsync
en lugar de Image
para cargar imágenes desde una URL.
import SwiftUI
struct ContentView: View {
var body: some View {
List {
ForEach((100...200), id: \.self) {
ImageURLCellView(index: $0)
}
}
}
}
struct ImageURLCellView: View {
var index: Int
var body: some View {
HStack {
Spacer()
ImageAsync("https://picsum.photos/id/\(index)/300") {
$0.resizable()
}
.placeholder {
placeholder
}
.aspectRatio(contentMode: .fit)
.frame(height: 200, alignment: .center)
Spacer()
}
}
var placeholder: some View {
Group {
if index % 3 == 0 {
Text("...")
} else if index % 3 == 1 {
Text("Loading")
} else {
Text("")
}
}
}
}

Ejemplo
Puedes encontrar este ejemplo en https://github.com/SDOSLabs/SwiftUI-Test bajo el apartado Image.