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

Heads up... You’re accessing parts of this content for free, with some sections shown as srlefnvob text.

Heads up... You’re accessing parts of this content for free, with some sections shown as qdxijkluw text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

Heads up... You’re accessing parts of this content for free, with some sections shown as lfpaxvziw text.

Heads up... You’re accessing parts of this content for free, with some sections shown as tdfyklqip text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
Initial screen
Iyagoeb dhqiop

Creating the first view for your project

Skills you’ll learn in this section: ScrollView

Creating a list of cards

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

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

Heads up... You’re accessing parts of this content for free, with some sections shown as hvhotlxul text.

Heads up... You’re accessing parts of this content for free, with some sections shown as tpnupxwoc text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
Show canvas
Tsuv qisqel

ScrollView(showsIndicators: false) {
With and without the scroll bar
Pegk uvs hayyual mwe ycziny hus

Refactoring the view

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

Name the subview
Voca qvu borbeuj

Heads up... You’re accessing parts of this content for free, with some sections shown as fjmugxcat text.

Heads up... You’re accessing parts of this content for free, with some sections shown as lqlaxhlis text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

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

Set up the single card view

➤ Create a SwiftUI View file called SingleCardView.swift.

var body: some View {
  Color.yellow
}
A yellow card
A lufmij fuzj

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:

Heads up... You’re accessing parts of this content for free, with some sections shown as zmcosskoq text.

Heads up... You’re accessing parts of this content for free, with some sections shown as dmdoztnet text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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
}
@EnvironmentObject var viewState: ViewState
CardsListView()
  .environmentObject(ViewState())
.onTapGesture {
  viewState.showAllCards.toggle()
}

Heads up... You’re accessing parts of this content for free, with some sections shown as tbvoppjet text.

Heads up... You’re accessing parts of this content for free, with some sections shown as lvfovctyr text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
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())
  }
}
if !viewState.showAllCards {
    SingleCardView()
}
Transition from thumbnail to card
Hnekfozuup syuh bvursxeil je ruzw

@StateObject var viewState = ViewState()

Heads up... You’re accessing parts of this content for free, with some sections shown as qljozvbug text.

Heads up... You’re accessing parts of this content for free, with some sections shown as lbpiznduk text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
CardsView()
  .environmentObject(viewState)

Navigation toolbar

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

@EnvironmentObject var viewState: ViewState
SingleCardView()
  .environmentObject(ViewState())
.toolbar {
  ToolbarItem(placement: .navigationBarTrailing) {
    Button(action: { viewState.showAllCards.toggle() }) {
      Text("Done")
    }
  }
}

Heads up... You’re accessing parts of this content for free, with some sections shown as gdkokltic text.

Heads up... You’re accessing parts of this content for free, with some sections shown as kgfoltqih text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
No Done button
Ni Raki vufwuq

NavigationView

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

Navigation bar Done button
Pawoxajous jec Xozo xeffus

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.

Heads up... You’re accessing parts of this content for free, with some sections shown as gsnowsbum text.

Heads up... You’re accessing parts of this content for free, with some sections shown as xtrejnnam text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
@EnvironmentObject var viewState: ViewState
CardDetailView()
  .environmentObject(ViewState())
var body: some View {
  NavigationView {
    CardDetailView()
  }
}
.navigationBarTitleDisplayMode(.inline)

Heads up... You’re accessing parts of this content for free, with some sections shown as gjvyvqhek text.

Heads up... You’re accessing parts of this content for free, with some sections shown as wszybgreh text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
Styling the navigation bar
Gkqnunp pte reqajoteek six

iPad navigation view
eTim selemicauj moiq

.navigationViewStyle(StackNavigationViewStyle())

Heads up... You’re accessing parts of this content for free, with some sections shown as bwgabjcot text.

Heads up... You’re accessing parts of this content for free, with some sections shown as ghbinhgod text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
iPad single navigation view
uGov fohwza behesopaoj vaon

The bottom toolbar

The single card view is going to have four buttons at the bottom to add elements to your card:

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

Heads up... You’re accessing parts of this content for free, with some sections shown as jhqisptax text.

Heads up... You’re accessing parts of this content for free, with some sections shown as gdkapstat text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
struct ToolbarButtonView: View {
  var body: some View {
    VStack {
      Image(systemName: "heart.circle")
        .font(.largeTitle)
      Text("Stickers")
    }
    .padding(.top)
  }
}
@Binding var cardModal: CardModal?
var body: some View {
  HStack {
    Button(action: { cardModal = .stickerPicker }) {
      ToolbarButtonView()
    }
  }
}
struct CardBottomToolbar_Previews: PreviewProvider {
  static var previews: some View {
    CardBottomToolbar(cardModal: .constant(.stickerPicker))
      .previewLayout(.sizeThatFits)
      .padding()
  }
}
Stickers button
Ckogfokc licyoh

Adding the bottom toolbar

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

@State private var currentModal: CardModal?

Heads up... You’re accessing parts of this content for free, with some sections shown as fbzudzrip text.

Heads up... You’re accessing parts of this content for free, with some sections shown as wtbihmmoz text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
ToolbarItem(placement: .bottomBar) {
  CardBottomToolbar(cardModal: $currentModal)
}
Bottom toolbar
Kassiz xoegnam

Adding the other buttons

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

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

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)
let text = button.0
let number = button.2

Heads up... You’re accessing parts of this content for free, with some sections shown as gstixsqec text.

Heads up... You’re accessing parts of this content for free, with some sections shown as mjgyzdwoj text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
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)
  }
}
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)
  }
}
Button Preview
Famnid Nqidoop

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.

Heads up... You’re accessing parts of this content for free, with some sections shown as lmtoprsyx text.

Heads up... You’re accessing parts of this content for free, with some sections shown as hrlafmneq text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Key points

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.
© 2025 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as jcfibpnyl text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now