WWDC24 SALE: Save 50% on all my Swift books and bundles! >>

Building our basic UI

Paul Hudson    @twostraws   

The first step in our project is to build the basic user interface, which for this app will be:

  1. A NavigationStack so we can show our app’s name at the top.
  2. A box prompting users to select a photo, over which we’ll place their imported picture.
  3. An “Intensity” slider that will affect how strongly we apply our Core Image filters, stored as a value from 0.0 to 1.0.
  4. A sharing button to export the processed image from the app.

We won't put all those bits in place to begin with; just enough so you can see how things fit together.

Initially the user won’t have selected an image, so we’ll represent that using an @State optional image property.

First add these two properties to ContentView:

@State private var processedImage: Image?
@State private var filterIntensity = 0.5

Now modify the contents of its body property to this:

NavigationStack {
    VStack {

        // image area


        HStack {
            Slider(value: $filterIntensity)

        HStack {
            Button("Change Filter") {
                // change filter


            // share the picture
    .padding([.horizontal, .bottom])

That uses two spacers so that we always get space above and below the image area, which also ensures the filter controls stay fixed to the bottom of the screen.

In terms of what should go in place of the // image area comment, it will be one of two things: if we have an image already selected then we should show it, otherwise we'll display a simple ContentUnavailableView so users know that space isn't just accidentally blank:

if let processedImage {
} else {
    ContentUnavailableView("No Picture", systemImage: "photo.badge.plus", description: Text("Tap to import a photo"))

I love being able to place optional views right inside a SwiftUI layout, and I think it works particularly well with ContentUnavailableView because only one is visible at a time. Yes, tapping the view won't do anything yet, but we'll tackle that shortly.

Now, as our code was fairly easy here, I want to just briefly explore what it looks like to clean up our body property a little – we have lots of layout code in there, but there's also a button action inside there too.

Yes, the Change Filter button isn't going to have a lot of complex functionality inside it, but this is good practice in splitting off button actions.

Right now that just means adding an empty method to ContentView, like this:

func changeFilter() {

Then calling it from the Change Filter button, like this:

Button("Change Filter", action: changeFilter)

When you’re learning it’s very common to write button actions and similar directly inside your views, but once you get onto real projects it’s a good idea to spend extra time keeping your code cleaned up – it makes your life easier in the long term, trust me!

I’ll be adding more little cleanup tips like this going forward, so as you start to approach the end of the course you feel increasingly confident that your code is in good shape.

Save 50% in my WWDC sale.

SAVE 50% To celebrate WWDC24, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.8/5

Unknown user

You are not logged in

Log in or create account

Link copied to your pasteboard.