16.
Adding Assets to Your App
Written by Caroline Begbie
Initially, in this chapter, you’ll learn about managing assets held in an asset catalog and you’ll create that all-important app icon. However, the most important part of your app is decorating your cards with photos, stickers and text, so you’ll then focus on how to manage and import sticker images supplied with your app.
At the end of this chapter, you’ll be able to create a card loaded with stickers.
The Asset Catalog
Skills you’ll learn in this section: app icons; vector vs bitmap; managing images in asset catalogs; screen resolution
Asset catalogs are by far the best place to manage image and color sets.
Within an asset catalog, under one image set, you can define multiple images for different themes, different devices, different scales and even different color gamuts. When you use the name of the image set in your code, the app will automatically load the correct image for the current environment. When you supply differently scaled images for different devices in an asset catalog, the app store will automatically do app thinning and only download the relevant images for that particular device. This is potentially a huge reduction in app download size.
The asset catalog also holds the app icon, the launch screen image and launch screen background color.
Adding the App Icon
➤ Open the starter project for this chapter, which, aside from removing the preview data, is the same as the previous chapter’s final project.
➤ Click the project name Cards at the top of the Project navigator. Choose the target Cards. On the General tab, find App Icons and Launch Screen:
This is where you specify which icon set to use for your app. You can choose to hold the icons in folders instead of asset catalogs, but it’s much easier to keep them in the asset catalog as Apple intended. You can even change the icon for your app dynamically, by checking include all app icon assets, and defining multiple icon sets in Assets.xcassets.
➤ Open Assets.xcassets and select AppIcon.
The iOS app template created an empty icon set called AppIcon when you first created your project.
If you’re lucky enough to have a designer for your app, as we are, they will distribute a design file, not code. This might be Sketch files or, as in our case, a Figma file.
The designer for this app, Lea Marolt, created all the assets for the app in Figma, a “freemium” vector graphics prototyping tool. You can use Figma in the web interface at https://www.figma.com or download the companion app available from that link. In the assets folder for this chapter, you’ll find a .fig file, which you can import into Figma. As you will see, some design suggestions don’t always make it to the shipped product.
This figma file includes the app icon, itself designed in Figma with different vector shapes:
➤ In Finder, open the assets folder for this chapter and locate app-icon.png. This file has been exported from Figma and is 1024 by 1024 pixels.
➤ With the AppIcon set selected in Xcode, drag app-icon.png to the Any Appearance icon area.
➤ Build and run, and swipe up from the bottom to exit your app. You’ll see your new icon instead of the old placeholder icon.
Any asset you import into the asset catalog is highly configurable. For example, app icons are different sizes on different devices. If you wish to have different icons for different devices, you can.
➤ Select the new icon you just dragged in, and show the Attributes inspector. Change iOS from Single Size to All Sizes.
You can now drag different PNG image files to the various icon spots.
➤ That’s a lot of work, so change All Sizes back to Single Size.
Vector vs Bitmap
You imported a bitmap PNG image for the icon. For other assets, you can use vector formats, such as PDF or SVG. When possible, it’s always better to use vector formats. These are made up of lines, curves and fills. For a vector line, you can set a start point and an end point. When you scale the line, the vector resizes without losing any of its resolution. With a bitmap line, you must stretch or compress pixels.
This image shows two 50 pixel wide images scaled up by twelve to 600 pixels. One is bitmap and the other is vector. You can see the vector image loses none of its sharpness.
Adding a Vector Image
Later, your app will need a placeholder image to show whether there are any errors in loading an image.
➤ In Finder, drag in error.svg from the assets folder for this chapter to the asset catalog panel under AppIcon.
The image imports into Xcode in the 1x space, and leaves the 2x and 3x spaces empty. These spaces are for devices with different resolutions.
Device Resolutions and Image Scale
Early iPhone screens had a 1:1 pixel density which means that a 100x100 pixel image on screen took up 100x100 points. iPhone 4 introduced the retina screen. Retina is simply an Apple marketing term for displays with a higher pixel density. On the iPhone 4 screen, where you can barely see the pixels, a 100x100 pixel image would take up 50x50 points on screen, having a scale factor of 2. iPhone 6s Plus came along, introducing a 3:1 pixel density. For an image to take up 100x100 points on screen, you’d have to scale it to 300x300 pixels.
When you provide bitmap assets, you must provide them for every device resolution. However, error.svg is a vector format image with a native size of 512x512. You don’t need to scale it by 2x and 3x as Xcode can do this for you.
➤ With the error image selected, in the Attributes inspector, change Scales from Individual Scales to Single Scale.
Xcode removes the 2x and 3x options in the center panel. When you build for a 2x resolution device, Xcode will automatically add to your app bundle a 512x512 optimized bitmap image scaled to the correct 2x resolution. Bundle images are held in a .car file, the format of which is not publicly available, so you can’t inspect what Xcode has done.
Launch Screen
Skills you’ll learn in this section: launch screen; size classes
Another use for the asset catalog is to hold a launch screen image and background color that displays while your app is launching. You’ve already come across Info.plist in Chapter 7, “Saving Settings”. This .plist file is where you’ll set the launch image and color.
➤ Click Cards at the top of the Project navigator and choose the Cards target. Choose the Info tab, and you’ll see the contents of Info.plist in the Custom iOS Target Properties section.
You can add new items either by right-clicking an entry and choosing Add Row or by moving your cursor over an item and clicking the + sign that appears. You can delete items by clicking the - sign.
➤ Click the disclosure control next to Launch Screen, then add items for Image Name and for Background Color.
➤ Double-click in the Value field for Image Name and enter:
LaunchImage
➤ Double-click in the Value field for Background color and enter:
LaunchColor
You may need to resize the columns to show the Value column.
➤ Open Assets.xcassets, click the + sign at the bottom of the assets panel and choose Image Set. Rename Image to LaunchImage.
➤ Click the + sign at the bottom of the assets panel again and choose Color Set. Rename Color to LaunchColor.
When you run your app now, the app will use these for the launch screen. Unfortunately, the simulator doesn’t clear launch screen caches, so if you change your launch image or color, in Simulator, you’ll have to go to Device > Erase All Contents and Settings… and clear the simulator completely. On a device, deleting the app should be sufficient, but you might have to restart the device as well.
➤ Click LaunchImage in the catalog. You have the option of filling the three images. However, just as with the error image, you’ll use a single scale SVG image.
➤ In Finder, open assets/Launch Screen. Drag in launch-screen-light.svg to the 1x spot.
➤ In the Attributes inspector, change Scales to Single Scale.
This SVG with a transparent background has a native size of 200x500px. At build time, Xcode will create the appropriately scaled bitmap image from this and display it in the center of the screen. When you launch the app in landscape on iPhone, you’ll need an image with a smaller height, so you’ll use size classes to decide which image to load.
Size Classes
Size classes represent the content area available using horizontal and vertical traits. These two traits can be either regular or compact. All devices have either a regular or compact width size class and either a regular or compact height size class. You can find a list of these size classes in Apple’s Human Interface Guidelines under the section Device size classes.
This is an illustration of some iPhones and iPads laid on top of each other:
For height in portrait mode, all devices fit into the regular height section. In landscape, all iPhones use compact height, but some larger iPhones use regular width rather than the smaller compact width.
iPads are always regular width and regular height. However, you still have to take into account size classes on iPad, because of split screen and resizing app windows. When in portrait mode, split screen apps are both compact width. In landscape mode, the user can size between compact width and regular width.
For your app, the current launch image will fit on all devices except for iPhones in landscape. So you’ll specify a sideways image for compact height.
➤ With the image selected, in the Attributes inspector, change Height Class to Any & Compact.
A new space opens up for the compact height image.
➤ In Finder, from assets/Launch Screen, drag in launch-screen-landscape-light.svg to the compact height image space.
➤ Build and run, and your launch screen should show up briefly before your app does. Try rotating the simulator to get the different landscape launch screen. If your launch screen doesn’t show up, remember to erase the simulator contents.
Now that you’ve got the hang of adding new images for checked conditions in the Attributes inspector, you’ll be able to complete the challenge for Dark Mode at the end of the chapter.
Adding Sticker Images to Your App
Skills you’ll learn in this section: groups; reference folders; loading images from files; lazy loading
Sometimes it’s more efficient to load images from the file system, rather than from the asset catalog. For example, when you release your cards app, one way of making it stand out from the crowd is to have some excellent stickers designed by artists.
You might have many artists who contribute their stickers to a central folder outside Xcode. As your app becomes more popular, you’ll probably add more stickers and, maybe, categorize them into themes. Your app shouldn’t need to know the names and quantity of stickers, but simply read in all stickers in the folder.
➤ In Finder, take a look at the assets/Stickers/Camping folder.
You could add these stickers to an asset catalog, but you’d have to keep track of how many there are and ensure that you have a strict naming convention. And there’s one thing that an asset catalog doesn’t allow. You can’t easily enumerate all the images it contains.
Note: These stickers are from Pixabay. There are several sites, such as https://unsplash.com and https://www.pexels.com, where creators share their work and allow reuse of images. Before adding an image to your app, always check that the license allows commercial use and follow the license instructions. The stickers use Pixabay’s license: free for commercial use with no attribution.
A solution to this is to add the folder containing all the stickers as a bundle resource in your project.
Xcode Build Phases
➤ Click on Cards at the top of the Project navigator to select the project, then select the Cards target.
➤ Along the top panel, select Build Phases and open the disclosure Copy Bundle Resources (2 items).
When you build your project, Xcode goes through all the listed phases.
One item on the list is a script called SwiftLint. This script checks a swiftlint.yml file to ensure that the code follows Kodeco’s standards. If this file exists, Xcode will produce compile errors and warnings if the code is not compliant. For example, if the author accidentally adds an extra empty line, they will get a compile warning. If the .yml file does not exist, the script will do nothing.
➤ Click the + under Copy Bundle Resources, then click Add Other….
➤ Select the assets/Stickers folder in this chapter’s folder and click Open.
➤ Uncheck Copy items if needed and select Create folder references. Then click Finish.
Your project will refer to the Stickers folder without copying it into the project. Your artists will be able to add more stickers to the folder, and when you build the project, Xcode will automatically include the new stickers in the build.
In the File inspector, the location of the folder is Relative to Group.
➤ With Stickers selected in the Project navigator, take a look at the File inspector. The location of Stickers is at ../../../assets/Stickers.
Location describes how to find the folder. Relative to Group in this case means that Xcode starts at the project’s base path, traverses up through the folders Cards, final and projects, then locates assets/Stickers in the projects folder.
If you relocate your project, or move or rename the Stickers folder, the project will no longer compile, and you’ll need to update the project’s bundle resources with the correct folder location.
Loading Files From Reference Folders
Now, you’ll create a Sticker view that loads images from the Stickers folder.
➤ In Single Card Views, create a new folder called Card Modal Views, and in this folder, create a new SwiftUI View file called StickerModal.swift.
➤ Open CardToolbar.swift and locate .sheet(item: $currentModal).
➤ Add a new case to the switch statement before the default case:
case .stickerModal:
StickerModal()
➤ Open SingleCardView.swift, Live Preview it, and pin the preview, so you can access it from other views.
➤ Try out your new stickers modal by tapping Stickers in the preview.
Loading a SwiftUI Image from a folder is not as easy as loading from an asset catalog. Asset catalogs do a lot of the heavy lifting. For example, if you add a vector file to an asset catalog, Xcode will convert to a native pixel format automatically, whereas it’s not an easy task to load a vector file from a folder.
When you load an image from a folder, you load it into an instance of UIKit’s UIImage. You also need to provide the full app bundle resource path.
➤ Open StickerModal.swift and replace body with:
var body: some View {
// 1
if let resourcePath = Bundle.main.resourcePath,
// 2
let image = UIImage(named: resourcePath +
"/Stickers/Camping/fire.png") {
Image(uiImage: image)
} else {
// 3
Image(.error)
}
}
Going through the code:
- Get the full resource path of the app bundle.
- Load the
UIImageusing the full name and path of the sticker and use theuiImageparameter for creating theImageview. - If the image doesn’t exist, show the error image in the asset catalog.
➤ In Live Preview, on the pinned Single Card View, tap the Stickers button and you’ll see the sticker image.
However, you don’t only want one sticker, you want to see all of them. Depending on how many stickers you have, you shouldn’t load up all the UIImages at once, as loading images is resource heavy and will block the user interface.
You can load the file names up front and, as the user scrolls, load the image when it’s needed. This is called lazy loading.
➤ In StickerModal, create a new type method to load the sticker names:
static func loadStickers() -> [String] {
var themes: [URL] = []
var stickerNames: [String] = []
}
You’ll first load the folder names in the top level in the Stickers folder. These will be themes. You’ll be able to add new themes to your app in the future simply by adding a new folder inside the Stickers folder in Finder. You won’t have to change any code to do this.
➤ Add this code at the end of loadStickers():
// 1
let fileManager = FileManager.default
if let resourcePath = Bundle.main.resourcePath,
// 2
let enumerator = fileManager.enumerator(
at: URL(fileURLWithPath: resourcePath + "/Stickers"),
includingPropertiesForKeys: nil,
options: [
.skipsSubdirectoryDescendants,
.skipsHiddenFiles
]) {
// 3
for case let url as URL in enumerator
where url.hasDirectoryPath {
themes.append(url)
}
}
Going through the code:
- Load the default file manager and bundle resource path.
- Get a directory enumerator, if it exists, for the Stickers folder. For the
optionsparameter, you skip subdirectory descendants and hidden files. Unless you skip the subdirectories, an enumerator will continue down the hierarchy. You currently just want to collect the top folder names as the themes. - If the URL is a directory, add it to
themes.
Note: You might later decide to add sticker packs as in-app purchases and not include them in your app bundle. If you download to your app’s documents directory, instead of setting the bundle resource path, you could use
URL.documentsDirectory.path()as the path.
Next you’ll iterate through all the theme directories and retrieve the file names inside.
➤ Add this code after the previous code, at the end of the method:
for theme in themes {
if let files = try?
fileManager.contentsOfDirectory(atPath: theme.path) {
for file in files {
stickerNames.append(theme.path + "/" + file)
}
}
}
return stickerNames
For each theme folder, you retrieve all the files in the directory and append the full path to stickerNames. You then return this array from the method.
➤ Create a new method in StickerModal to load a UIImage from a path:
func image(from path: String) -> UIImage {
print(
"loading:",
NSString(string: path).lastPathComponent)
return UIImage(named: path) ?? UIImage.error
}
You temporarily print out the path name so that you can check whether you’re lazily loading the image. You then return the UIImage loaded from the path name. If the image doesn’t load for any reason, you return the error image from the asset catalog. UIImage conveniently creates a type property for every image in the catalog, so if the error image isn’t in the asset catalog, you get a compile error.
➤ Create a new property in StickerModal to hold the file names:
@State private var stickerNames: [String] = []
➤ Change body to:
var body: some View {
ScrollView {
ForEach(stickerNames, id: \.self) { sticker in
Image(uiImage: image(from: sticker))
.resizable()
.aspectRatio(contentMode: .fit)
}
}
.onAppear {
stickerNames = Self.loadStickers()
}
}
Instead of just showing one sticker, you iterate through all the sticker names and create an Image from the UIImage.
➤ In Live Preview, tap the Stickers icon.
➤ Under the debug console output, click Previews to see the print out.
Watch the debug console output, and you’ll see all the images are loading up front, ending with the tree and the guitar. As mentioned before, with a lot of stickers, this will block the user interface.
Note: At the time of writing, there appears to be a SwiftUI bug that duplicates
ForEach, causing the image list to print out twice in the debug console.
➤ To get the stickers to load lazily, in body, Control-click ForEach and embed it in a VStack.
➤ Change VStack { to:
LazyVStack {
➤ Preview the stickers modal screen again. Now, only the images that show on screen, plus the one just after, load. Scroll down, and you’ll see in the debug console that the guitar image loads as you approach it. Your images are now loading lazily.
➤ Unpin SingleCardView as you will now configure StickerModal.
These images are much too big and would look much better in a grid. Fortunately, as well as lazy VStack and HStacks, SwiftUI provides a lazy loading grid view.
Using Lazy Grid Views
Skills you’ll learn in this section: grids; preview variants
LazyVGrid and LazyHGrid provide vertical and horizontal grids. With the LazyVGrid, you define how to layout columns and, with the LazyHGrid, you layout rows.
➤ Add a new property to StickerModal:
let columns = [
GridItem(spacing: 0),
GridItem(spacing: 0),
GridItem(spacing: 0)
]
➤ Change LazyVStack { to:
LazyVGrid(columns: columns) {
You still use the same ForEach and Image views but they now fit into the available space in the grid instead of taking up the whole width of the screen. The grid uses all the horizontal available space and divides it equally among the specified GridItems.
➤ To visualize this, in the design canvas panel, view Sticker Modal and click the Selectable icon. In the code panel, place the cursor on Image in body. The outlines of the Images will show in the preview.
Swift Tip: If this were a
LazyHGrid, you would definerowsin the same way as you havecolumns, and the grid would divide up the available vertical space. To scroll horizontally, add a horizontal axis:ScrollView(.horizontal).
➤ In the design canvas, click the Variants icon, and choose Orientation Variants.
Although the grid looks good in portrait mode, it would look better with more images horizontally when in landscape.
➤ Change the columns property declaration to:
let columns = [
GridItem(.adaptive(minimum: 120), spacing: 10)
]
The columns will size to 120 points, separated by ten points.
The design canvas updates the views:
Swift Tip: As well as
adaptive,GridItem.sizecan befixedwith a fixed size, orflexible, which sizes to the available space.
Selecting the Sticker
Now that you have the stickers showing, you’ll tap one to select it, dismiss the modal and add the sticker to the card as a card element.
➤ In StickerModal, add a property to hold the selected image:
@Binding var stickerImage: UIImage?
The parent of the modal will pass in a state property to hold the selected image. Your project won’t compile until you have completed storing the image.
➤ Add the environment property that holds the dismiss action:
@Environment(\.dismiss) var dismiss
You’ll use this property to dismiss the modal.
➤ In body, add a modifier to Image:
.onTapGesture {
stickerImage = image(from: sticker)
dismiss()
}
When the user taps an image, you’ll update the bound sticker image and dismiss the modal.
➤ Update the preview:
StickerModal(stickerImage: .constant(UIImage()))
You can now select a sticker and at the same time dismiss the modal. CardDetailView will then take over and store and show the selected sticker. Your project still won’t compile yet.
➤ Open CardToolbar.swift and add new properties to CardToolbar:
@Binding var card: Card
@State private var stickerImage: UIImage?
You receive the current card from the parent view and hold the current sticker chosen from StickerModal.
➤ Update the preview:
#Preview {
Color.yellow
.modifier(CardToolbar(
currentModal: .constant(nil),
card: .constant(Card())))
}
➤ Locate the sheet(item:) with the stickerModal case. This will have a compile error as you’re not yet passing the state property to StickerModal.
➤ Change StickerModal() to:
StickerModal(stickerImage: $stickerImage)
.onDisappear {
if let stickerImage = stickerImage {
card.addElement(uiImage: stickerImage)
}
stickerImage = nil
}
On dismissal of the modal, you should store the sticker as a card element and reset the sticker image to nil. You’ll get a compile error until you’ve added the card binding and written addElement(uiImage:).
➤ Open SingleCardView.swift and, in body, change the modal toolbar modifier to:
.modifier(CardToolbar(
currentModal: $currentModal,
card: $card))
Ensure that your CardToolbar parameters are in the same order that you listed the bindings in CardToolbar.swift, otherwise you will get a compile error.
Storing the Data
You’ve set up the user interface side of things, so now you’ll manage the data.
➤ In the Model group, open CardElement.swift.
Now that you’re adding image data, instead of storing an Image, which is a View, you’ll store the data and construct a view from that data.
➤ Add a new property to ImageElement:
var uiImage: UIImage?
This will hold the sticker image data.
Replace the image definition with:
var image: Image {
Image(uiImage: uiImage ?? UIImage.error)
}
You create a computed property that builds an Image from uiImage. If this is nil, present the error image in the asset catalog.
➤ Open Card.swift and add this new method to Card:
mutating func addElement(uiImage: UIImage) {
let element = ImageElement(uiImage: uiImage)
elements.append(element)
}
Here you take in a new UIImage and add a new ImageElement to the card. In the following chapter, you’ll be able to use this method for adding photos too.
➤ Your project finally compiles, so Live Preview SingleCardView and add some stickers to it. Resize and reposition the stickers as you want and create a masterpiece :].
Challenges
Challenge 1: Set Up a Dark Mode Launch Screen
Your app currently has different launch screens for portrait and landscape, when the height size class is compact. Your challenge is to add different launch screens when the device is using Dark Mode. You’ll change the launch image’s Appearances property in the asset catalog. You’ll find the dark launch screen images in the assets folder. Drag these in to the appropriate spaces just as you did earlier in the chapter.
When you test in Simulator, to get the new launch screen to show, you’ll need to erase all device contents and settings.
Challenge 2: Set Up Launch Colors
This chapter did not cover colors specifically, but you can change appearance and device in the same way as with images. You’ve already set up a launch color in Info.plist to use as the launch background color. Change the launch color in the asset catalog. Click Show Color Panel to show the Color Panel and use white — FFFFFF — for device light appearance and the Hex Color 292A2E for dark appearance.
If you get stuck, the asset catalog in the project in the challenge project will show you what to do.
Key Points
- Most of the time, you should manage your images and colors in asset catalogs.
- To load images dynamically, you can include them in bundle resources or load them from your app’s sandboxed folders.
- In asset catalogs, favor vector images over bitmaps. They are smaller in file size and retain sharpness when scaled. Xcode will automatically scale to the appropriate dimensions for the current device.
- Think about how you can make your app special. Good app design together with artwork can really make you stand out from the crowd.