Chapters

Hide chapters

SwiftUI Apprentice

First Edition · iOS 14 · Swift 5.4 · Xcode 12.5

Section I: Your first app: HIITFit

Section 1: 12 chapters
Show chapters Hide chapters

Section II: Your second app: Cards

Section 2: 9 chapters
Show chapters Hide chapters

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 you’re going build your own collaging app to 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:

Final app
Final app

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:

Back of the napkin sketch
Back of the napkin sketch

In the next 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 out 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 12 Pro. Build and run your app using Command-R to make sure that everything works OK. Your iPhone 12 Pro simulator should start and show ContentView’s “Hello, world!” text.

Initial screen
Initial screen

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 SwiftUI View file called CardsListView.swift.

This view will show a scrolling thumbnail list of all the cards you create in your app.

Creating a list of cards

➤ Open CardsListView.swift. Instead of cards, for the moment, you’ll show a placeholder list of rounded rectangles.

➤ Replace body with:

var body: some View {
  ScrollView {
    VStack {
      ForEach(0..<10) { _ in
        RoundedRectangle(cornerRadius: 15)
          .foregroundColor(.gray)
          .frame(width: 150, height: 250)
      }
    }
  }
}

This places ten shapes in a scrollable VStack.

Placeholder thumbnails
Placeholder thumbnails

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).

➤ Live Preview the view, and you’ll be able to 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:

Show canvas
Show canvas

➤ Change ScrollView { to:

ScrollView(showsIndicators: false) {

This turns the scroll bar off.

With and without the scroll bar
With and without the scroll bar

Refactoring the view

Skills you’ll learn in this section: refactoring views; view state in an environment object

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 CardThumbnailView.swift.

➤ Back in CardsListView.swift, Command-click RoundedRectangle and choose Extract Subview.

Name the subview
Name the subview

➤ Rename ExtractedView to CardThumbnailView.

The extracted view is now at the end of the current file and looks like this:

struct CardThumbnailView: View {
  var body: some View {
    RoundedRectangle(cornerRadius: 15)
      .foregroundColor(.gray)
      .frame(width: 150, height: 250)
  }
}

➤ Cut this code and open CardThumbnailView.swift.

➤ Select the entire CardThumbnailView 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.

Set up the single card view

➤ Create a SwiftUI View file called SingleCardView.swift.

➤ A card will have a colored background to which you’ll add photos, stickers and text.

➤ 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.

A yellow card
A yellow card

Transitioning from list to card

When you tap a card in the scrolling list in CardsListView, you want to show SingleCardView. You can achieve this in several ways:

  • A modal view. SingleCardView will have a row of buttons that link to modal views, and it’s not good practice to have modal views inside modal views.
  • A NavigationView with a NavigationLink destination that pushes SingleCardView to the front. This could be a good choice, but currently you can’t customize an animated transition inside a NavigationView, so it reduces your opportunities for styling your app.
  • Replace CardsListView in 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.
  • Place SingleCardView in a new layer in front of CardsListView. This is the option you’ll choose, so that you can experiment with transitions later on.

In HIITFit, you used a state property toggle to show the History view and passed a binding when you showed the modal sheet. Navigation in Cards will be spread over multiple files, so it’s easier to centralize the property with a view state environment object that will be shared throughout the app.

Creating an environment object

➤ Create a new Swift file called ViewState.swift and replace the code with:

import SwiftUI

class ViewState: ObservableObject {
  @Published var showAllCards = true
}

showAllCards will control display of SingleCardView, and you’ll change it when you tap a card. You make it published so that views that use showAllCards will react when the value changes.

You’ll remember from Chapter 7, “Observing Objects”, that an ObservableObject is a publisher, and ViewState must be a class to conform to it.

➤ In CardsListView.swift, add a new property:

@EnvironmentObject var viewState: ViewState

You’ll add the environment object wherever you need to check the view state.

➤ In previews, change CardsListView() to:

CardsListView()
  .environmentObject(ViewState())

This instantiates the environment object for the preview. If you don’t do this, the canvas will crash, without a specific error, when your code tries to access viewState.

➤ Now that you’ve set up the environment, add a modifier to CardThumbnailView():

.onTapGesture {
  viewState.showAllCards.toggle()
}

This will toggle the Boolean when you tap a card thumbnail. When showAllCards is false, you’ll show the selected single card in front of the list of cards.

➤ Create a new SwiftUI View file called CardsView.swift.

This view will be the initial view that controls which full screen views currently show.

➤ Replace the code with:

import SwiftUI

struct CardsView: View {
  @EnvironmentObject var viewState: ViewState

  var body: some View {
    ZStack {
      CardsListView()
    }
  }
}

struct CardsView_Previews: PreviewProvider {
  static var previews: some View {
    CardsView()
      .environmentObject(ViewState())
  }
}

Here you show CardsListView and set up the environment object viewState.

➤ After CardsListView(), add this:

if !viewState.showAllCards {
    SingleCardView()
}

Your ZStack contains CardsListView and, in front of that, SingleCardView, which will only show when you’ve tapped a thumbnail to trigger the Boolean state change.

➤ Live Preview and tap a card. The yellow SingleCardView shows on top of the list of cards.

Transition from thumbnail to card
Transition from thumbnail to card

To return to the thumbnail list from SingleCardView, 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 initialize ViewState as a state object:

@StateObject var viewState = ViewState()

You do this so that viewState persists as long as your app does. If you simply initialize it as an environment object in CardsApp, occasionally the app will reinitialize it, and, if you’re editing a card, you’ll mysteriously land back at the first screen.

➤ Change ContentView() to:

CardsView()
  .environmentObject(viewState)

You call the view that will show the list of cards instead of ContentView, making sure that you put viewState into the app environment.

➤ 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.

Navigation toolbar

Skills you’ll learn in this section: toolbars; NavigationView; navigation bar; tuples

A Done button in SingleCardView will toggle showAllCards in viewState. You can set up buttons at top and bottom of the screen using a navigation toolbar.

➤ Open SingleCardView.swift and add the environment object to SingleCardView:

@EnvironmentObject var viewState: ViewState

➤ Remember to update previews:

SingleCardView()
  .environmentObject(ViewState())

➤ Add a new toolbar modifier to Color.yellow:

.toolbar {
  ToolbarItem(placement: .navigationBarTrailing) {
    Button(action: { viewState.showAllCards.toggle() }) {
      Text("Done")
    }
  }
}

You place a Done button at the top right of the screen. When the user taps this button, SingleCardView toggles showAllCards. Because this is a published property, any view that needs to react to showAllCards will, and CardsView won’t show SingleCardView any more.

toolbar(content:) allows multiple ToolbarItems. placement can be:

  • navigationBarLeading: The leading edge of the top navigation bar.
  • navigationBarTrailing: The trailing edge of the top navigation bar.
  • principal: On iOS, the principal placement is in the center of the navigation bar.
  • bottomBar: The bottom toolbar.

You’ll use the bottom toolbar placement shortly.

➤ Preview SingleCardView.

No Done button
No Done button

Notice that the button doesn’t show up. This is because ToolbarItem(placement:) is using navigationBarTrailing, so any item will only show up if the view is inside a NavigationView.

NavigationView

➤ In SingleCardView, Command-click Color and choose Embed….

➤ Change the placeholder Container to NavigationView.

➤ Resume the preview and your button will show up.

Navigation bar Done button
Navigation bar Done button

Adding a navigation bar

When you use Lists, you often use NavigationView 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 NavigationView, not for transitions, but to make the Done button show up in the navigationBarTrailing placement for SingleCardView’s toolbar. Using a NavigationView means that you can take advantage of the navigation bar style to design the top of the screen.

You’re going to add another modifier to Color.yellow, so now’s the time to take the opportunity to refactor it into a separate view.

Command-click Color and choose Extract Subview.

➤ Name the extracted view CardDetailView.

➤ Create a new SwiftUI View file called CardDetailView.swift and cut and paste the extracted CardDetailView structure into CardDetailView.swift, replacing the boilerplate CardDetailView.

You’ll get a compile error because viewState is missing.

➤ Add the environment object to CardDetailView:

@EnvironmentObject var viewState: ViewState

➤ Update previews, as usual, to instantiate the environment object:

CardDetailView()
  .environmentObject(ViewState())

Your code should now compile.

Back in SingleCardView.swift, your code is looking a lot simpler:

var body: some View {
  NavigationView {
    CardDetailView()
  }
}

➤ Add a new modifier to CardDetailView:

.navigationBarTitleDisplayMode(.inline)

This sets the navigation bar style.

Other styles include automatic and large. If you want to give the view a title as well, you can use .navigationTitle("Title goes here").

➤ Preview SingleCardView to see the styled navigation bar.

Styling the navigation bar
Styling the navigation bar

➤ Build and run on an iPad simulator in portrait orientation. You can press Command-Right Arrow and Command-Left Arrow to rotate the simulator, or choose Device ▸ Orientation and pick the desired orientation.

➤ Tap a card.

You’ll see that you get a white screen with a Back button. When you tap Back, you see a portion of SingleCardView with the Done button. Tapping Done takes you back to the first screen. This is due to the NavigationView which behaves differently with different size configurations.

iPad navigation view
iPad navigation view

In Chapter 16, “Adding Assets to Your App”, you’ll learn that different devices have different size classes. As well as an iPad simulator, you can reproduce this on an iPhone 12 Pro Max simulator in landscape orientation, as this also has a larger size class.

➤ In SingleCardView, add a modifier to NavigationView:

.navigationViewStyle(StackNavigationViewStyle())

This navigation view style ensures that you only see a single top view at a time.

➤ Build and run, and the iPad version of your app will now behave the same way as the iPhone 12 Pro version.

iPad single navigation view
iPad single navigation view

Note: NavigationView can cause issues when you’re doing your own custom transitions and animations, so you have to decide whether using NavigationView is worth it. You could lay out the Done button in ZStack layers without using a NavigationView as you did in HIITFit.

➤ Set your run destination back to iPhone 12 Pro, as it’s easier to preview in the canvas.

The bottom toolbar

The single card view is going to have four buttons at the bottom 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 easy to read and ensure that values are restricted to those defined in the enumeration.

➤ Create a new Swift file called CardModal.swift.

➤ Add this code:

enum CardModal {
  case photoPicker, framePicker, stickerPicker, textPicker
}

These cases correspond to each of the buttons.

➤ Create a new SwiftUI View file called CardBottomToolbar.swift.

In this view you’ll set up the four bottom buttons.

➤ Above CardBottomToolbar, add a new View for a single toolbar button:

struct ToolbarButtonView: 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 CardBottomToolbar, add a binding for the current modal:

@Binding var cardModal: CardModal?

➤ Replace body with:

var body: some View {
  HStack {
    Button(action: { cardModal = .stickerPicker }) {
      ToolbarButtonView()
    }
  }
}

Here you create an HStack containing a button that will change the modal state. You’ll add more toolbar items in a moment to this HStack.

➤ Fix up the preview to send a card modal binding:

struct CardBottomToolbar_Previews: PreviewProvider {
  static var previews: some View {
    CardBottomToolbar(cardModal: .constant(.stickerPicker))
      .previewLayout(.sizeThatFits)
      .padding()
  }
}

➤ Resume the preview, and you’ll see your Stickers button and icon:

Stickers button
Stickers button

Adding the bottom toolbar

➤ Open CardDetailView.swift and add a new property to CardDetailView:

@State private var currentModal: CardModal?

When you tap a button on the bottom bar, the button will update this property. Later on, you’ll show the corresponding modal view.

➤ 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) {
  CardBottomToolbar(cardModal: $currentModal)
}

Here you add your new toolbar at the bottom of the screen.

➤ To see the toolbar, either preview SingleCardView.swift or build and run the app:

Bottom toolbar
Bottom toolbar

Adding the other buttons

➤ Open CardBottomToolbar.swift and add a new property to ToolbarButtonView:

let modal: CardModal

ToolbarButtonView is the view that displays the toolbar button. You’ll send in the modal that the button is tied to and show the correct image for that button. You’ll get a compile error until you fix up CardBottomToolbar.

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 CardModal 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 8, “Saving Settings”.

➤ Add this property to ToolbarButtonView:

private let modalButton: 
  [CardModal: (text: String, imageName: String)] = [
    .photoPicker: ("Photos", "photo"),
    .framePicker: ("Frames", "square.on.circle"),
    .stickerPicker: ("Stickers", "heart.circle"),
    .textPicker: ("Text", "textformat")
  ]

Here you set up a dictionary of type [CardModal: (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 ToolbarButtonView, 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 then use those for the button instead of the hard coded Stickers values.

➤ In CardBottomToolbar, replace HStack and its contents with:

HStack {
  Button(action: { cardModal = .photoPicker }) {
    ToolbarButtonView(modal: .photoPicker)
  }
  Button(action: { cardModal = .framePicker }) {
    ToolbarButtonView(modal: .framePicker)
  }
  Button(action: { cardModal = .stickerPicker }) {
    ToolbarButtonView(modal: .stickerPicker)
  }
  Button(action: { cardModal = .textPicker }) {
    ToolbarButtonView(modal: .textPicker)
  }
}

These are the four buttons that your view needs. Each button shows the correct image and text for the modal, and the action sets the new card modal state.

➤ Resume the preview, or build and run to see your new buttons:

Button Preview
Button Preview

As of now, the buttons don’t do anything, but over the next few chapters, you’ll attach a new modal view to each button.

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

Challenge: Tidy up

Make it a habit to regularly tidy up the files in your app. 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 Group from Selection. Name the group. If you miss any files, just drag them into the group later.

As an example, you can group all the files with View in their name a group called Views. You can then have a sub group for the views used for a single card.

You’ll find suggested groups in the challenge project for this chapter.

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.

  • When you have a button at the top of your screen, whether it’s leading or trailing, you can choose to use a navigation bar. The NavigationView has the advantage of being a standard, built-in control, however, it can reduce your options of custom transitions.

  • 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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.