13.
Outlining a Photo Collage App
Written by Caroline Begbie
Congratulations, you’ve written your first app! HIITFit uses standard iOS user interaction with lists and swipeable page views. Now you’ll get your teeth into something a bit more complex with custom gestures and custom views.
Photo collage apps are very popular, and in this section, you’ll build your own collaging app in which you’ll create cards to share. You’ll be able to add images — from your photos or from the internet — and add text and stickers too. This app will be real-world with real-world problems to match.
In this chapter, you’ll take a look at a sketch outline of the app idea and create a view hierarchy that will be the skeleton of your app.
At the end of Section 2, your finished app will look like this:
Initial App Idea
The first step to creating a new app is having the idea. Before writing any code, you should do research as to whether your app is going to be a hit or a miss. Work out who your target audience is and talk to some people who might use your app. Find out what your competition is in the App Store and explore how your app can offer something new and different.
Once you’ve decided that you have a hit on your hands, sketch your app out and work out feasibility and where technical difficulties may lie.
Your photo collaging app will have a primary view — where you list all the cards — and a detail view for the selected card — where you can add photos and text. This might be the back-of-the-napkin sketch:
In future chapters, you’ll set up the data model and data storage, but for now, examine the design and think about possible implementation difficulties that you’ll need to overcome. Always take a modular approach and test each aspect of the app as separately from the main app as possible.
SwiftUI is great for this, because you can construct views and controls independently using SwiftUI’s Live Preview. When you’re happy with how a view works, add it to your app.
Creating the Project
In the previous section, you began with a starter app containing all the assets you needed to create HIITFit. In this section, you’ll start with a new app, and you’ll find out how to add assets as you move through the next few chapters.
➤ Open Xcode and choose File ▸ New ▸ Project… and create a new project called Cards using the iOS App template. If you need a refresher on how to create a new SwiftUI project, you’ll find all the information in Chapter 1, “Checking Your Tools”.
➤ Click the run destination button and select iPhone 16 Pro. Build and run your app using Command-R to make sure that everything works OK. Simulator will run using the iPhone 16 Pro configuration, and show ContentView’s globe image and “Hello, world!” text.
You should take these steps every time you create a new app just in case something in your environment has changed.
Creating the First View for Your Project
Skills you’ll learn in this section:
ScrollView
➤ Create a new SwiftUI View file called CardsListView.swift.
When complete, this view will show a scrolling thumbnail list of all the cards you create in your app.
Creating a List of Cards
Instead of cards, for the moment, you’ll show a placeholder list of rounded rectangles.
➤ In CardsListView.swift, replace body with:
var body: some View {
ScrollView {
VStack {
ForEach(0..<10) { _ in
RoundedRectangle(cornerRadius: 15)
.foregroundStyle(.gray)
.frame(width: 150, height: 250)
}
}
}
}
This places ten shapes in a scrollable VStack.
A ScrollView can be vertical or horizontal. The default, which you use here, is vertical, but you can specify a horizontal axis with ScrollView(.horizontal).
➤ In Live Preview, scroll the list. When you scroll, you can see an ugly scroll bar by the side of the cards.
In case you can’t see the canvas, you can enable it using the icon at the top right of Xcode:
➤ Change ScrollView { to:
ScrollView(showsIndicators: false) {
This turns the scroll bar off.
Refactoring the View
Skills you’ll learn in this section: refactoring views
As you add views, you’ll recognize that later on, some views will become more complex. The RoundedRectangle is such a view. You’ve given it basic styling, but you’ll probably want to style it a bit further down the line.
It’s much easier to refactor views early on, so you’ll create a new view for the placeholder card now. You extracted a view in Chapter 3, “Prototyping the Main View”, so this should be a refresher for you.
➤ Create a new SwiftUI View file called CardThumbnail.swift.
➤ Back in CardsListView.swift, Control-click RoundedRectangle and choose Extract Subview.
Xcode will copy RoundedRectangle() to a new View and rename the reference to ExtractedView().
➤ Control-click ExtractedView(), choose Refactor > Rename and rename ExtractedView to CardThumbnail.
The extracted view is now at the end of the current file and looks like this:
struct CardThumbnail: View {
var body: some View {
RoundedRectangle(cornerRadius: 15)
.foregroundStyle(.gray)
.frame(width: 150, height: 250)
}
}
➤ Cut this code and open CardThumbnail.swift.
➤ Select the entire CardThumbnail structure and paste in the cut code.
➤ Open CardsListView.swift. Your list still looks the same, but it will be easier for you to add colors and shadows to the thumbnails later.
Setting Up the Single Card View
A card will have a colored background to which you’ll add photos, stickers and text.
➤ Create a SwiftUI View file called SingleCardView.swift.
➤ Replace body with:
var body: some View {
Color.yellow
}
This color will eventually come from the card data, but for the moment you’ll just make the card yellow.
Transitioning From List to Card
Skills you’ll learn in this section: full screen modal
When you tap a card in the scrolling list in CardsListView, you want to show SingleCardView. You can achieve this in several ways:
- Replace
CardsListViewin the view hierarchy. With this option, when you return to the thumbnails after editing the card, you’d lose your current scrolling position in the cards list. - Use a
NavigationStackwith aNavigationLinkdestination that pushesSingleCardViewto the front. You’ll learn aboutNavigationStackin your third app in Section 3. - Present a full screen modal view. The view slides up from the bottom and covers the whole screen. This is the option you’ll use here.
Creating a Full Screen Modal View
➤ In CardsListView, create a new property to track the modal presentation:
@State private var isPresented = false
➤ Add a new modifier to ScrollView:
.fullScreenCover(isPresented: $isPresented) {
SingleCardView()
}
When isPresented is true, you’ll show SingleCardView as a full screen modal.
➤ Add a modifier to CardThumbnail():
.onTapGesture {
isPresented = true
}
➤ In Live Preview, tap one of the cards.
isPresented becomes true and SingleCardView slides up from the bottom of the device’s screen. Currently there is no way to dismiss the view and return to the list of cards, so you’ll need to create a Done button.
Before tackling the button, set up your app to run in Simulator, so that if Live Preview fails, you can still see your app.
➤ Open CardsApp.swift and change ContentView() to:
CardsListView()
You call the view that will show the list of cards instead of ContentView.
➤ Build and run and make sure that your app works in Simulator, just as it does in the preview.
You aren’t using ContentView.swift any more, but you can leave it in the project to experiment with other SwiftUI layouts.
The Navigation Toolbar
Skills you’ll learn in this section: toolbars;
NavigationStack; tuples
A Done button in SingleCardView will dismiss the full screen modal view. You can set up buttons at the top and bottom of the screen using toolbars.
➤ Open SingleCardView.swift and add the environment object that dismisses a modal to SingleCardView:
@Environment(\.dismiss) var dismiss
➤ Add a new toolbar modifier to Color.yellow:
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Done") {
dismiss()
}
}
}
You place a Done button at the top right of the screen. When the user taps this button, SwiftUI dismisses the SingleCardView modal.
toolbar(content:) allows multiple ToolbarItems. Some placement options are:
-
topBarLeading: The leading edge of the top bar. -
topBarTrailing: The trailing edge of the top bar. -
principal: On iOS, the principal placement is in the center of the bar. -
bottomBar: The bottom toolbar.
You’ll use the bottom toolbar placement shortly.
➤ Preview SingleCardView.
NavigationStack
Notice that the button doesn’t show up. This is because ToolbarItem(placement:) is using topBarTrailing, so any item will only show up if the view is inside a NavigationStack.
➤ In SingleCardView, Control-click Color and choose Embed….
➤ Change the placeholder Container to NavigationStack.
Your button will now show up.
When you use Lists, you often use NavigationStack and NavigationLink together, which have built-in push and pop transitions and titles. You’ll explore this more in Section 3. Currently, you’re using a NavigationStack not for transitions but simply to make the Done button show up in the SingleCardView.
➤ Open CardsListView.swift and test your app so far in Live Preview. Tap a thumbnail to show the yellow card and dismiss the card by tapping the Done button.
The Bottom Toolbar
The single card view will have four buttons at the bottom, allowing you to add elements to your card:
- Photos: Pick photos from your Photo Library.
- Frames: Change the shape of the photo element.
- Stickers: Add some fun to your card with app stickers.
- Text: Add words.
Each of these buttons will show a separate modal view. When you have discrete values, such as these four destinations, you can use an enumeration to create your set of values. Enumerations make your code easier to read and ensure that values are restricted to those defined in the enumeration.
➤ Create a new empty file called ToolbarSelection.swift and add this code:
import Foundation
enum ToolbarSelection {
case photoModal, frameModal, stickerModal, textModal
}
These cases correspond to each of the buttons.
➤ Create a new SwiftUI View file called BottomToolbar.swift.
In this view, you’ll set up the four buttons at the bottom of the screen.
➤ Above BottomToolbar, add a new View for a single toolbar button:
struct ToolbarButton: View {
var body: some View {
VStack {
Image(systemName: "heart.circle")
.font(.largeTitle)
Text("Stickers")
}
.padding(.top)
}
}
Each modal button will use this view, and you’ll style this to be more generic shortly.
➤ In BottomToolbar, add a binding for the current modal:
@Binding var modal: ToolbarSelection?
➤ Replace body with:
var body: some View {
HStack {
Button {
modal = .stickerModal
} label: {
ToolbarButton()
}
}
}
Here you create an HStack containing a button that will change the modal state. Because the text label for the button is a custom view, rather than a string, you use the Button(action:label:) initializer. You’ll add more toolbar items in a moment to this HStack.
➤ Fix the preview to send a modal binding:
#Preview {
BottomToolbar(modal: .constant(.stickerModal))
}
In Live Preview, you’ll see your Stickers button and icon:
Adding the Bottom Toolbar
➤ Open SingleCardView.swift and add a new property to SingleCardView:
@State private var currentModal: ToolbarSelection?
When you tap a button on the bottom bar, the button will update this property. Later on, you’ll show the corresponding modal view.
➤ In body, locate .toolbar {. This is where you currently have the Done button.
➤ Add a new toolbar item inside toolbar(content:), under the previous toolbar item:
ToolbarItem(placement: .bottomBar) {
BottomToolbar(modal: $currentModal)
}
Here you add your new toolbar at the bottom of the screen.
Adding the Other Buttons
➤ Open BottomToolbar.swift and add a new property to ToolbarButton:
let modal: ToolbarSelection
ToolbarButton is the view that shows each toolbar button. You’ll send in the modal value for each button and show the correct image for that button. You’ll get a compile error until you fix BottomToolbar.
You already set up body to show an image and text for the Stickers button. You could do a switch in body and show the appropriate image for all the modal options. However, it’s more succinct to set a dictionary of all the possible options with the text and image name. In case you need a refresher on dictionaries, you first used them in Chapter 7, “Saving Settings”.
➤ Add this property to ToolbarButton:
private let modalButton: [
ToolbarSelection: (text: String, imageName: String)
] = [
.photoModal: ("Photos", "photo"),
.frameModal: ("Frames", "square.on.circle"),
.stickerModal: ("Stickers", "heart.circle"),
.textModal: ("Text", "textformat")
]
Here you set up a dictionary of type [ToolbarSelection: (String, String)] containing values for all the possible button states. You could have set up a structure that contains text and imageName, but if you’re only using a type once in an object with not much code, you can set up an “ad hoc” data type called a tuple.
Tuples
A tuple is a group of values. For example, you could initialize a tuple with three elements like this:
let button = ("Stickers", "heart.circle", 1)
And access the data:
let text = button.0
let number = button.2
It’s obviously good practice to name your types rather than using numbers to access the data, which is why you defined your modalButton tuple with (text:imageName:)
➤ In ToolbarButton, replace body with:
var body: some View {
if let text = modalButton[modal]?.text,
let imageName = modalButton[modal]?.imageName {
VStack {
Image(systemName: imageName)
.font(.largeTitle)
Text(text)
}
.padding(.top)
}
}
Using your dictionary, you access the text and image name and use those for the button instead of the hard coded Stickers values.
To show all the buttons in BottomToolbar, you can iterate through all the values of ToolbarSelection. Swift enumerations have a built-in array called allCases, but to use it, the enumeration must conform to the CaseIterable protocol.
➤ Open ToolbarSelection.swift and conform ToolbarSelection:
enum ToolbarSelection: CaseIterable {
You can now iterate through the values using ToolbarSelection.allCases.
➤ Back in BottomToolbar.swift, in BottomToolbar replace the contents of body with:
HStack {
ForEach(ToolbarSelection.allCases, id: \.self) { selection in
Button {
modal = selection
} label: {
ToolbarButton(modal: selection)
}
}
}
You iterate through all the available modals. Each button shows the correct image and text for the modal, and the action sets the new modal state.
➤ Open SingleCardView.swift and, in Live Preview, admire your layout so far.
Adding Modal Views
Skills you’ll learn in this section: multiple modal sheets;
Identifiableenumerations;Hashable
As of now, the buttons don’t do anything, so you’ll attach text views to each button. As you progress through the book, you’ll replace the text view with the appropriate content.
In Section 1, you used View.sheet(isPresented:onDismiss:content:), where you passed in a Boolean state property. When you have multiple sheets to show conditionally, you can choose a different method of presentation, by passing the sheet an optional data source binding.
This data source can be of any type that conforms to Identifiable.
➤ In SingleCardView.swift, add this modifier to Color.yellow.
.sheet(item: $currentModal) { item in
switch item {
default:
Text(String(describing: item))
}
}
Here, for every modal selection, you show the description text of the current modal. Later, you’ll add cases to this switch statement whenever you configure each of the four modal views.
currentModal is of type ToolbarSelection, which doesn’t conform to Identifiable. Because of this, you’ll get a compile error: Instance method sheet(item:onDismiss:content:) requires that ToolbarSelection conform to Identifiable.
Making an Enumeration Identifiable
➤ Open ToolbarSelection.swift and conform ToolbarSelection to Identifiable:
enum ToolbarSelection: CaseIterable, Identifiable {
As you already know, to conform to Identifiable, you have to provide an id.
Add the id to ToolbarSelection:
var id = UUID()
You’ll immediately get a compiler error, saying “Enums must not contain stored properties”. Remember that you can’t make a copy of an enumeration by instantiating it, so you can’t add stored vars to an enumeration. Yet, you need to include var id in order to conform to Identifiable.
Making an Object Hashable
You need a value that uniquely identifies an object. That describes a hash value. Hashing algorithms calculate values from any data to provide a digital fingerprint that identifies an object.
Fortunately, enumerations automatically conform to Hashable, which provides a hash value.
➤ Replace var id = UUID() with:
var id: Int {
hashValue
}
Instead of a stored property, this var is a computed property. Now, when you create a ToolbarSelection object, each object will have a different ID calculated from the enumeration’s hash value.
➤ Open CardsListView.swift and, in Live Preview, tap a card. Then tap the bottom buttons to preview each of your modal views. The modal’s description displays on each modal view. Swipe down on the modal to dismiss it.
Cleaning Up
➤ Open BottomToolbar.swift, and in BottomToolbar, remove , id: \.self from the ForEach loop.
Because ToolbarSelection is now Identifiable, the extra id: parameter is superfluous.
The ForEach loop is now:
ForEach(ToolbarSelection.allCases) { selection in
➤ Build and run your app in Simulator and check out your app’s outline so far.
You now have a prototype of the main views of your app and can visualize how they will fit together. A prototype is useful, even at this early stage, so that you can show it to other people to find out what they think of it and whether the interface is intuitive enough for them to navigate without help. It’s better to find out that your app is not useful as early as possible in its development so that you can either incorporate feedback or pivot entirely.
Challenge
Make it a habit to regularly tidy up the code and files in your app.
Challenge 1: Tidy up Files
Look down the list of files and see which ones you can group together. Command-click each file that you want to group together, then Control-click the selected files and choose New Folder from Selection. Name the folder. If you miss any files, just drag them into the folder later.
As an example, you can group all the files with View in their name into a folder called Views. You can then have a sub folder for the views used for a single card. You’ll find suggested folders in the challenge project for this chapter.
Challenge 2: Refactor Code
You don’t always have to create new structures for views. Sometimes, if it’s a simple view and you’re only using it once, it’s easier to keep track of views as properties or methods.
In CardsListView.swift, refactor ScrollView to be a property called list. Leave the full screen cover modifier in body as a modifier on list. Try and keep body as short as possible so that it’s easier to read.
Note: Xcode’s Refactor doesn’t work well for this in Xcode 16.2, so create a new property, and cut and paste the relevant code into the new property.
Key Points
-
Prototypes are always worth doing. With a prototype it’s easier to see what’s missing and what the next steps should be. They don’t have to be complicated. So far, you aren’t creating or saving any data, but you can tell how the app will flow. Writing an app is more than just writing code. It’s finding your target audience, creating a good design and overcoming technical problems.
-
Refactor your views early and often. Whenever you have a stack enclosing a number of views with multiple modifiers, it’s worth extracting those views to either a new
Viewstructure or to a newViewproperty within the existingViewstructure. -
There are several ways you can navigate apps. You can use built-in
NavigationStacks, completely customize with buttons or tapped views and modal views or layered views. -
SwiftUI toolbars are great for standard placement. You must enclose them in a
NavigationStackto be able to see them. -
Dictionaries are useful for holding disparate values. For example, you can use them to hold options to format views. Using tuples, you can create ad hoc types.
-
You can show modal views conditionally using a type that conforms to
Identifiable. -
Enumerations have a unique identifier called a hash value.