There is a famous saying “A picture is worth a thousand words” 😎 so we are gonna learn about Image views in SwiftUI.
We can use Image view to display images inside our app. In its simplest form Image View can be initialized with
Image("image name")
or
Image(systemName: "SF symbol name")
Asset Image
Image(“dt”) initializer can load and display an image that is part of app bundle or inside Assets catalog folder. This initializer takes name of image as the parameter. Assets catalog supports both bitmap and PDF based vectors and Imageview is capable of rendering both types without any issue.
struct ImageExample: View {
var body: some View {
Image("dt") // dt is name of image in Assets
}
}
System Image
Image(systemName: “person.circle”) initializer accepts string name for SF symbol.
struct ImageExample: View {
var body: some View {
Image(systemName: "person.circle")
.font(.system(size: 100))
}
}
As you can see in the code above, with SF symbol initializer, you can apply font size.
Working with Images
Resizing Image
Image views are the size of their content meaning Image view renders image in its original size.
If we look at earlier example again:
struct ImageExample: View {
var body: some View {
Image("dt")
}
}
Notice the size of image view, its going out of window(blue rectangle represents size of image view):
We can try to apply .frame modifier to resize:
struct ImageExample: View {
var body: some View {
Image("dt")
.frame(width: 100, height: 100)
}
}
But it only sets view’s frame, Image is still going out of window 😕
Reason for this behavior is because image is still being rendered in its full size but .resizable() modifier can be applied to resize the image to fit in parent view’s frame. This makes image to expand and take all the available space.
struct ImageExample: View {
var body: some View {
Image("dt")
.resizable()
.frame(width: 100, height: 100)
}
}
AspectRatio
We can set aspectRatio for image using .aspectRatio modifier. Aspect ratio is a decimal value representing width/height. Value of aspectRatio is optional and if not provided, image assumes aspect ratio of the view.
struct ImageExample: View {
var body: some View {
Image("dt")
.resizable()
.aspectRatio(1, contentMode: .fit)
}
}
Change value of aspectRatio to see the difference:
struct ImageExample: View {
var body: some View {
Image("dt")
.resizable()
.aspectRatio(3.8, contentMode: .fit)
}
}
You can animate value of aspectRatio to see the effect it has on image:
struct ImageExample: View {
@State private var aspectR: CGFloat = 0.0
var body: some View {
Image("dt")
.resizable()
.aspectRatio(aspectR, contentMode: .fit)
.onAppear {
withAnimation(Animation.easeInOut(duration: 2).repeatForever(autoreverses: true)) {
aspectR = 2.8
}
}
}
}
ScaledToFit
.scaledToFit() modifier is used to make image fit the view while maintaining original aspect ratio:
struct ImageExample: View {
var body: some View {
Image("dt")
.resizable()
.scaledToFit()
}
}
ScaledToFill
.scaledToFill() modifier is used to make image fill the view while maintaining the aspect ratio.
struct ImageExample: View {
var body: some View {
Image("dt")
.resizable()
.scaledToFill()
}
}
Clipped
.clipped modifier can be used to clip image. This modifier is used to clip overflowing image content.
To understand this better, we will start with original image size:
struct ImageExample: View {
var body: some View {
Image("dt")
}
}
Next, we will apply 200x200 frame to the image view.
struct ImageExample: View {
var body: some View {
Image("dt")
.frame(width: 200, height: 200)
}
}
Notice blue rectangle in screenshot above 👆
This rectangle represents frame of our image view but since the image is larger than the size of original image(we are not applying resizable modifier here), image retains its content and is overflowing outside the frame of Image view.
Now we are ready to clip image to see the effect this modifier has on image content.
struct ImageExample: View {
var body: some View {
Image("dt")
.frame(width: 200, height: 200)
.clipped()
}
}
Once clipped modifier is applied, we can only see content of the image that’s present inside the frame.
In order to better see the frame, we will add border around Image view.
struct ImageExample: View {
var body: some View {
Image("dt")
.frame(width: 200, height: 200)
.clipped()
.border(Color.blue)
}
}
ClipShape
We are not limited to clip image content to a frame rectangle but with the help of .clipShape() modifier, we can clip image to a given shape.
In order to visualize this better, we will apply dark appearance modifier to previewProvider
struct ImageExample: View {
var body: some View {
Image("dt")
.frame(width: 200, height: 200)
.clipShape(Circle())
}
}struct ImageExample_Previews: PreviewProvider {
static var previews: some View {
ImageExample()
.preferredColorScheme(.dark)
}
}
CGImage with Image View
SwiftUI Image view comes with initializer that can draw CGImage into the Image view. While creating this Image view, we have opportunity to define scale, orientation and accessibility label for Image view.
For this example, we will store CGImage version of DevTechie image:
struct ImageExample: View {
let dt = UIImage(named: "dt")!.cgImage
var body: some View {
Image(
dt!, // CGImage
scale: 1.0, // Image scale factor(1.0,2.0 or 3.0)
orientation: .up, // Image orientation
label: Text("DevTechie")) // Accessibility label
.resizable() // Make image resizable
.scaledToFit() // Aspect fit image
}
}
Image orientation can be applied while view is being initialized. Try other options available for Image.Orientation as shown below:
struct ImageExample: View {
let dt = UIImage(named: "dt")!.cgImage
var body: some View {
Image(
dt!, // CGImage
scale: 1.0, // Image scale factor(1.0,2.0 or 3.0)
orientation: .upMirrored, // Image orientation
label: Text("DevTechie")) // Accessibility label
.resizable() // Make image resizable
.scaledToFit() // Aspect fit image
}
}
Interpolation
SwiftUI provides way to interpolate image with .interpolation modifier. This modifier takes parameter to indicate level of interpolation to the image, values are high, medium and low.
Small images tend to pixellate when scaled beyond their size. Interpolation can help by filling pixel level color gaps with approximation. Let’s start with interpolation value as .none to see the pixelation:
I have included 300x123 image in Assets catalog with name “DTS” and we will use that for our example:
struct ImageExample: View {
var body: some View {
VStack {
Image("DTS")
.resizable()
.interpolation(.none)
.scaledToFit()
}
}
}
Let’s change interpolation value to .low
struct ImageExample: View {
var body: some View {
VStack {
Image("DTS")
.resizable()
.interpolation(.low)
.scaledToFit()
}
}
}
Let’s apply all of them and see them in action:
struct ImageExample: View {
var body: some View {
VStack {
Image("DTS")
.resizable()
.interpolation(.none)
.scaledToFit()
Image("DTS")
.resizable()
.interpolation(.low)
.scaledToFit()
Image("DTS")
.resizable()
.interpolation(.medium)
.scaledToFit()
Image("DTS")
.resizable()
.interpolation(.high)
.scaledToFit()
}
}
}
RenderingMode
.renderingMode modifier expects templateRenderingMode value and renders image according to the defined renderingMode.
Let’s say we have a PNG image with transparent background, we can render that PNG image inside Image view in original mode(with the colors the image has been imported with) or we can render that image as template(meaning we can define tint color for the image and change image tint to better match our app’s theme).
Let’s take a look at an example. I have following PNG image
If you download this image, you will find that its background is transparent. We will import this into Assets catalog along with another DevTechie image and add them into the view as shown below:
struct ImageExample: View {
var body: some View {
VStack {
Image("DevTechieBackground")
.resizable()
.scaledToFit()
Spacer()
ZStack {
Text("Learn By Doing")
.font(.system(size: 50))
Image("splash")
.renderingMode(.original)
.foregroundColor(.red)
}
}.edgesIgnoringSafeArea(.all)
}
}
Notice renderingMode in this case is set to original so Image view will show blue image. Also note that foregroundColor has no effect on the image as its being rendered as original image.
Let’s change that renderingMode to be template:
struct ImageExample: View {
var body: some View {
VStack {
Image("DevTechieBackground")
.resizable()
.scaledToFit()
Spacer()
ZStack {
Text("Learn By Doing")
.font(.system(size: 50))
Image("splash")
.renderingMode(.template)
.foregroundColor(.red)
}
}.edgesIgnoringSafeArea(.all)
}
}
As soon as we change renderingMode to template, our image is taking foregroundColor into consideration.
We can even animate foregroundColor with the help of Animation and TimelineView
struct ImageExample: View {
var body: some View {
VStack {
Image("DevTechieBackground")
.resizable()
.scaledToFit()
Spacer()
ZStack {
Text("Learn By Doing")
.font(.system(size: 50))
TimelineView(.periodic(from: Date(), by: 2)) { _ in
let color = Color(red: Double.random(in: 0...1), green: Double.random(in: 0...1), blue: Double.random(in: 0...1), opacity: Double.random(in: 0.5...1))
Image("splash")
.renderingMode(.template)
.foregroundColor(color)
.animation(.easeInOut, value: color)
}
}
}.edgesIgnoringSafeArea(.all)
}
}
ScaledMetric PropertyWrapper with Image view
ScaledMetric propertyWrapper can be used to resize Image view’s width and height as dynamic text changes its size.
Let’s create an example with ScaledMetric propertyWrapper:
struct ImageExample: View {
@ScaledMetric var size: CGFloat = 100
var body: some View {
HStack {
Image("dt")
.resizable()
.clipShape(Circle())
.frame(width: size, height: size)
.background(Circle().stroke(Color.orange, lineWidth: 5))
VStack(alignment: .leading) {
Text("DevTechie")
.font(.largeTitle)
Text("Learn by doing!")
.font(.title3)
}
}
}
}
Notice the change in image size along with text size as I increase and decrease the accessibility text.
Working with SFSymbol
SF Symbols are vector based so you can apply font, foregroundColor modifiers to Images backed by SFSymbol.
Per Apple’s definition for SF Symbol 3
With over 3,200 symbols, SF Symbols is a library of iconography designed to integrate seamlessly with San Francisco, the system font for Apple platforms. Symbols come in nine weights and three scales, and automatically align with text labels. They can be exported and edited in vector graphics editing tools to create custom symbols with shared design characteristics and accessibility features. SF Symbols 3 features over 600 new symbols, enhanced color customization, a new inspector, and improved support for custom symbols.
Let’s start with a simple example.
struct ImageExample: View {
var body: some View {
HStack {
Image(systemName: "globe.americas.fill")
Text("DevTechie")
}
.font(.largeTitle)
.foregroundColor(.orange)
}
}
As you can see, SF symbols react to font, foregroundColor and other modifiers just like Text does. This gives us flexibility to customize look and feel of app further without the need of external resources.
Symbol Rendering Mode
SF Symbols support four rendering mode out of the box to further customize to meet your app’s unique requirements. With the help of .symbolRenderingMode modifier, you can provide rendering mode for SF Symbol.
Let’s look at each with an example.
SymbolRenderingMode.monochrome This mode of SF symbol renders image in a single layer. ForegroundColor or ForegroundStyle can be used to define color, as shown below:
struct ImageExample: View {
var body: some View {
VStack {
Image(systemName: "sun.max.circle.fill")
.symbolRenderingMode(.monochrome)
Text("DevTechie")
}
.font(.system(size: 100))
.foregroundColor(.orange)
}
}
SymbolRenderingMode.hierarchical This mode of SF symbol renders the image in multiple layers. Each layer comes with a different opacity of foregroundColor . Variants like secondary or tertiary are used to control the opacity.
struct ImageExample: View {
var body: some View {
VStack {
Image(systemName: "person.3.sequence.fill")
.symbolRenderingMode(.hierarchical)
Text("DevTechie")
}
.font(.system(size: 100))
.foregroundColor(.orange)
}
}
SymbolRenderingMode.multicolor This mode of SF symbol renders the image in multiple layers using colors defined internally at symbol definition level.
struct ImageExample: View {
var body: some View {
VStack {
Image(systemName: "cloud.sun.rain.fill")
.symbolRenderingMode(.multicolor)
Text("DevTechie")
}
.font(.system(size: 100))
.foregroundColor(.orange)
}
}
ForegroundColor application is subjective to the symbol in use. For example, above symbol completely ignores the .foregroundColor(.orange) , we can see this with a different symbol.
struct ImageExample: View {
var body: some View {
VStack {
Image(systemName: "person.crop.circle.badge.checkmark")
.symbolRenderingMode(.multicolor)
Text("DevTechie")
}
.font(.system(size: 100))
.foregroundColor(.orange)
}
}
Last but not the least we have SymbolRenderingMode.palette this mode renders symbol in multiple layers using colors defined by you in foregroundStyle. .foregroundStyle supports primary, secondary and tertiary color variants and symbol adopts these color to render the image onto the screen.
struct ImageExample: View {
var body: some View {
VStack {
Image(systemName: "person.3.sequence.fill")
.symbolRenderingMode(.palette)
Text("DevTechie")
}
.font(.system(size: 100))
.foregroundStyle(.orange, .pink, .white)
}
}
Best way to find out which symbol supports what, is by launching SF Symbols 3 app.
SymbolVariant
SF Symbol has another special modifier to set variant for a symbol, its called .symbolVariant . In order to understand this better, we gotta take a close look at collection of symbols in SF Symbol app.
If we open SF Symbol app and search for “person”, we get three variants as shown below.
Usually, when we want to render an image, we take the name from SF Symbol app and use it like this 👉 Image(systemName: “person.circle”) and this will render an image with person with circle around it. With the help of symbolVariant modifier we can request image’s other variant.
Let’s put both Image views side by side:
struct ImageExample: View {
var body: some View {
VStack {
HStack {
Image(systemName: "person.circle")
Image(systemName: "person")
.symbolVariant(.circle)
}
.font(.system(size: 50))
Text("DevTechie")
}
.font(.system(size: 100))
}
}
Note that both Image views are rendering person with circle around it but for the second Image view, we are using symbolVariant. It’s a convenient way of rendering other variations of symbol.
Available options are:
- none
- circle
- square
- rectangle
- fill
- slash
Benefit of using symbolVariant over string based fully resolved name is that if the symbol is not available, symbolVariant will render generic version of that symbol whereas in case of string based image name, image will not render at all.
Shown below is the example where we are trying to render person.square and notice how string based doesn’t render anything vs symbolVariant based Image view renders person image on screen.
struct ImageExample: View {
var body: some View {
VStack {
HStack {
Image(systemName: "person.square")
Image(systemName: "person")
.symbolVariant(.square)
}
.font(.system(size: 50))
Text("DevTechie")
}
.font(.system(size: 100))
}
}
ForegroundStyle with SF Symbol
Foreground Style modifier was introduced in iOS 15 and is fully supported by SF Symbol as well.
We can use foregroundStyle modifier to create gradient style SF symbol image.
struct ImageExample: View {
var body: some View {
HStack {
Image(systemName: "star")
.foregroundStyle(
LinearGradient(colors: [.blue.opacity(0.8), .orange.opacity(0.8)], startPoint: .top, endPoint: .bottom)
)Text("DevTechie")
.bold()
Image(systemName: "star")
.symbolVariant(.fill)
.foregroundStyle(
LinearGradient(colors: [.blue.opacity(0.8), .orange.opacity(0.8)], startPoint: .top, endPoint: .bottom)
)
}
.font(.largeTitle)
.foregroundColor(.orange)
}
}
Materials with SF Sybmol
foregroundStyle modifier can also be used to apply newly introduced material style to SF Symbols as well.
struct ImageExample: View {
var body: some View {
ZStack(alignment: .bottom) {
Image("background")
.resizable()
.scaledToFill()
.edgesIgnoringSafeArea(.all)
HStack {
Image(systemName: "star")
.foregroundStyle(.regularMaterial)
Image(systemName: "star")
.foregroundStyle(.thickMaterial)
Image(systemName: "star")
.foregroundStyle(.ultraThickMaterial)
}.font(.largeTitle)
.frame(width: 200, height: 50)
.background(.ultraThinMaterial, in: Capsule())
}
}
}
Dynamic Scaling in SF Symbol
When it comes to dynamic fonts, SF Symbols can observe change and resize based on dynamic type. There are two ways of making your symbol observe these changes.
ImageScale
SF Symbol based images can adopt to resize with imageScale modifier. This allows image to scale with dynamic font setting.
ImageScale can be set to small, medium, or large size as shown below:
struct ImageExample: View {
var body: some View {
HStack {
Image(systemName: "star")
.imageScale(.small)
Image(systemName: "star")
.imageScale(.medium)Image(systemName: "star")
.imageScale(.large)Text("DevTechie")
.bold()
Image(systemName: "star")
.imageScale(.large)Image(systemName: "star")
.imageScale(.medium)Image(systemName: "star")
.imageScale(.small)
}
.font(.title)
.foregroundColor(.orange)
}
}
Let’s run the project and apply dynamic font in simulator by going under Settings -> Accessibility -> Display & Text Size -> Large Text -> enable Large Accessibility Sizes and drag sizing slider at the bottom.
Or you can use Environment Overrides menu option from debug menu
This is how our project will look:
ScaledMetric for SF Symbols
Another way to adopt dynamic font resizing is by the use of ScaledMetric property wrapper.
struct ImageExample: View {
@ScaledMetric private var size: CGFloat = 30
var body: some View {
HStack {
Image(systemName: "star")
.font(.system(size: size))
Image(systemName: "star")
.font(.system(size: size + 5))Image(systemName: "star")
.font(.system(size: size + 10))Text("DevTechie")
.bold()
Image(systemName: "star")
.font(.system(size: size + 10))Image(systemName: "star")
.font(.system(size: size + 5))Image(systemName: "star")
.font(.system(size: size))
}
.font(.title)
.foregroundColor(.orange)
}
}
With that, we have reached the end of this article. Thank you once again for reading, if you liked it, don’t forget to subscribe our newsletter.