ScrollViews Part 1

Our March code-alongs we will dedicate to different options to create a ScrollView which showcase the images of several animals and their names. To organise the app, we will use TabView to have all the different views in a tab-based navigation. The today’s result will look the following:

Let’s get started!

Step 1: Set up your project

  1. Open Xcode: Launch Xcode and select Create a new Xcode project.
  2. Choose Template: Select App under the iOS tab and click Next.
  3. Name Your Project: Enter a name for your project, like DifferentScrollViews. Choose SwiftUI for the interface and Swiftfor the language. Click Next, and then save your project.

When you open your project, you’ll see the already familiar standard code presenting a globe and the Text “Hello, world!” in the ContentView.swift. This file will be our “Home”-View which will present all the other views by using a TabView. This means that the content will be in other files which will be presented here.

Step 2: Preparing the animal views

Before creating the first ScrollView, we need to define our AnimalPhoto-Views that will be shown in our scrollviews for each and every animal.

Before continuing, please download the animal images here:

Unzip it and from your file explorer, move all the animal images over to your Xcode project into Assets.xcassets. It should look like this:

Assets.jpg

Create an AnimalPhoto

We will create a new SwiftUI file. Go to File - New - File from Template... or simply press cmd + N, choose iOS Template and here SwiftUI View, click Next, choose AnimalPhoto as the name of the new view, ensure that DifferentScrollViewsis selected under Group, check the tick box for Targets: DifferentScrollViews, click Create.

You will see the familiar structure. This time just showing the text “Hello, World!” in a Textview. Replace Textby:

Swift
Image("elephant")
	.resizable()
	.scaledToFit()
	.frame(width: 300)
	.cornerRadius(12)

You got to know Image in our first code-along “Button Animation”. .scaledToFit() resizes the image to fit to the available space. We provide here a frame of width 300 i.e. our image will have a width of 300. The modifier .cornerRadius(12)rounds the edges of the image with a radius of 12.

So, we see already our cute litte elephant. However, here we have defined the animal shown in this image in a static way. We want to re-use this view for all other animals as well.

Create an Animal model

Create a new swift file. Go to File - New - File from Template... or simply press cmd + N, choose iOS Template and here Swift File, click Next, choose Animal as the name of the new view, ensure that DifferentScrollViewsis selected under Group, check the tick box for Targets: DifferentScrollViews, click Create.

Swift
import SwiftUI

struct Animal: Identifiable {

    let id = UUID()
    let imageName: String

    static let all: [Animal] = [
        "lion", "zebra","cheetah", "elephant",
        "giraffe", "hippo", "meerkat", "ostrich",
        "buffalo", "rhino", "wild-dog""gorilla"
    ].map { Animal(imageName: $0) }

    static let example = Animal(imageName: "elephant")
}

We define a structnamed Animaland it confirms to the Identifiable protocol: This allows SwiftUI to uniquely identify each Animal when using e.g. ForEach in lists, grids, etc.

id = UUID()generates a unique identifier for each Animal instance and imageName: String´: Stores the name of the image (e.g., "lion", "zebra"), which can be used with Image(imageName)`.

The static property ‘all’ defines a static array of Animal objects. It starts with an array of our animal names (strings) and .map { Animal(imageName: $0) } converts each string into an Animal object. The .map {} function transforms each element in a collection (such as an array) and returns a new array with modified values. ‘$0’ is Swift’s shorthand for the first parameter inside a closure. And this means that ‘.map { Animal(imageName: $0) }’ transforms each string (e.g. lion) into an Animal object(e.g. Animal(imageName: "lion")).

The example property provides a sample Animal instance that can be used for previews or testing. This avoids having to create test data manually in multiple places.

Updating the AnimalPhoto

We created the AnimalPhoto-View to just present our cute little elephant. To show also all the other animals, we first need to define a new property called animalby

Swift
let animal: Animal

It expects an Animal instance when AnimalPhoto is created. The passed Animal object will provide an image name (e.g., “lion”, “elephant”).

Now, replace "elephant" by animal.imageName (as defined in our Animal struct) so that the full AnimalPhotoView looks the following:

Swift
import SwiftUI

struct AnimalPhoto: View {

    let animal: Animal

    var body: some View {
        Image(animal.imageName)
            .resizable()
            .scaledToFit()
            .frame(width: 300)
            .cornerRadius(12)
    }
}

#Preview {
    AnimalPhoto(animal: Animal.example)
}

We defined the elephant image as the example in our Animalmodel and that’s why you see in the preview of the AnimalPhoto a nice cute elephant image. This is just relevant for the preview. If you don’t provide the example, the Preview will not work and show an error.

Now we are ready to create our first simple scroll view.

Step 3: Create a simple ScrollView

Create a new swift file. Go to File - New - File from Template... or simply press cmd + N, choose iOS Template and here SwiftUI View, click Next, choose SimpleScrollView as the name of the new view, ensure that DifferentScrollViewsis selected under Group, check the tick box for Targets: DifferentScrollViews, click Create.

Directly after the structline, before the var bodyline, insert the following:

Swift
let animals = Animal.all

In the body, we want to show a title and the scroll view underneath each other. Therefore, inside the existing VStackreplace the content and replace by:

Swift
Text("Simple ScrollView")
	.font(.title.bold())

This creates our title. Next, we want to create a scroll view. Below the Text, insert the following:

Swift
ScrollView(.horizontal, showsIndicators: true) {

}
.frame(height: 300)
.padding(.bottom, 30)

This creates a horizontal scroll view which also shows indicators (showsIndicators: true) which is the little bar below the scroll view which indicates where you are in the horizontal scroll. Our scroll view got a height of 300.

In this scroll view we want to show all our animal photos – i.e. showing AnimalPhoto for all animals. You are already familiar with VStack which vertically aligns views. HStack is similar, just horizontally aligned views next to each other. A LazyHStack is very similar to a HStack. The difference is that a HStack loads all elements at once when the view appears. A LazyHStack works like HStack but with “lazy” loading. It only loads views when they appear on-screen instead of all at once. And therefore it is better for performance in large datasets. Ours is not that large but still. Therefore we will be using a LazyHStack.

Inside the LazyHStack we want to show all our animal images and the name below the image. To run through the animals array we are using a ForEachloop and create a repeated UI for each item. ForEach(animals) { animal in ... } iterates through each Animal in the array. Inside the loop, we will use a VStack for each animal to display the image by using the AnimalPhotowe created earlier and a Text View, i.e. our LazyHStack looks the following:

Swift
LazyHStack(spacing: 20) {
	ForEach(animals) { animal in
		VStack {
			AnimalPhoto(animal: animal)
				.padding(.bottom, 12)
				
			Text(animal.imageName.capitalized)
				.font(.title2)
		}
	}
}

The modifier .capitalized displays the name of the animal with the first letter capitalized.

Congrats, you created your first scroll view!

But we can do better. In this scroll view, the images scroll through and the scroll stops depending how much we swiped. There is no “snap” for the images. Such a “snap” provides a better scroll control and more haptic effect. We can create such a “pagination effect”. We add to the LazyHStackafter its closing curly bracket the modifier

Swift
.scrollTargetLayout()

.scrollTargetLayout() marks items in a LazyHStack or LazyVStack as scrollable targets. This is necessary to allow SwiftUI understand the intended scrolling structure.

To the ScrollView‘s closing curly bracket we add

Swift
.scrollTargetBehavior(.viewAligned)

This ensures that when scrolling stops, one AnimalPhoto is always centered. If you swipe left or right, SwiftUI automatically snaps the next AnimalPhoto to the center. This gives a smooth and user-friendly paging effect.

However, you will see, that the first item is not centered but at the very left edge of the screen. We need to move the LazyHStackto the center of the screen with the appropriate width – which is (screenWidth - imageFrameWidth) / 2. We want the imageFrameWidth to be 80%of the screen width. How do we get the screen width?

For this, we introduce GeometryReader. ‘GeometryReader’ measures the size and position of its parent view and provides that information for layout calculations. GeometryReader takes up all available space and measures its parent. Inside its closure, geometry provides

  • geometry.size.width: width of the available space,
  • geometry.size.height: height of the available space.

Please wrap ScrollView in the following code:

Swift
GeometryReader { geometry in
	let screenWidth = geometry.size.width
	let imageFrameWidth = screenWidth * 0.8
}

Now, add the padding that centers the first image of the LazyHStack, i.e. below .scrollTargetLayout() add:

Swift
.padding(.horizontal, (screenWidth - imageFrameWidth) / 2)

The full code should look the following:

Swift
Text("Pagination Effect")
	.font(.title.bold())

GeometryReader { geometry in
	let screenWidth = geometry.size.width
	let imageFrameWidth = screenWidth * 0.8

	ScrollView(.horizontal, showsIndicators: false) {
		LazyHStack(spacing: 0) {
			ForEach(animals) { animal in
				VStack {
					AnimalPhoto(animal: animal)
						.frame(width: imageFrameWidth)
					Text(animal.imageName.capitalized)
						.font(.title2)
				}
			}
		}
		.scrollTargetLayout()
		.padding(.horizontal, (screenWidth - imageFrameWidth) / 2)
	}
	.scrollTargetBehavior(.viewAligned)
}
.frame(height: 300)

Congrats, you have created 2 simple ScrollViews! Let’s create another one.

Step 4: Create a simple TabView

Create a new SwiftUI file. Go to File - New - File from Template... or simply press cmd + N, choose iOS Template and here SwiftUI View, click Next, choose SimpleTabView as the name of the new view, ensure that DifferentScrollViewsis selected under Group, check the tick box for Targets: DifferentScrollViews, click Create. (Please note: don’t use the name “TabView”. This is a standing term and creates hustles if you are creating a view with this name.)

As you have seen already in SimpleScrollView, let’s define the animalsdirectly after the structline, before the var bodyline, by inserting the following:

Swift
let animals = Animal.all

Inside the VStack let’s define a title for the view:

Swift
Text("TabView")
		.font(.title.bold())

Instead of a scroll view, we will be using a TabView using the same code inside the TabView:

Swift
TabView {
	ForEach(animals) { animal in
		VStack {
			AnimalPhoto(animal: animal)
				.padding(.bottom, 12)

			Text(animal.imageName.capitalized)
				.font(.title2)
		}
	}
}
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(.page(backgroundDisplayMode: .always))
.frame(height: 350)
.padding(.bottom, 20)
.padding(.top, -40)

Spacer()

TabView { ... } is a SwiftUI container that allows users to switch between different views. Usually, TabView creates a tab bar at the bottom (default behavior). .tabViewStyle(PageTabViewStyle()) changes the TabView into a swiping page view instead of a standard tab bar. .indexViewStyle(.page(backgroundDisplayMode: .always)) enables page indicators (dots) below the pages. The backgroundDisplayMode: .always makes the dots always visible.

The user can swipe left and right to navigate between views.

Step 5: Creating a TabViewwith tabItems

If you were running your app as it is, you will not see all the views you created up to now – apart from a globe and the Text “Hello, world!”. Why is that? Because this is what your ContentViewshows. And why is this one shown and not the other ones?

Take a look to the DifferentScrollViewApp.swift. This has been created when setting up the project. Here you see:

Swift
import SwiftUI

@main
struct DifferentScrollViewsApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

This file is the “entry point” of your SwiftUI app. It defines the main structure of your app and specifies which view should be displayed first. The @main marks this as the main entry point of the app. DifferentScrollViewsApp is a struct that conforms to App, which is required for SwiftUI apps.

The

Swift
WindowGroup {
    ContentView()
}

creates a WindowGroup, which is the default container for SwiftUI apps. WindowGroup manages multiple windows on macOS, iPadOS, and visionOS, while on iPhone, it behaves like a single window. Inside WindowGroup, the starting view of your app is set to ContentView().

That’s why you would still see just the globe and “Hello, world!”. Let’s change that and present all the views you have created before:

In ContentView in the body replace the VStack(that holds the globe and the text) by

Swift
TabView {

	SimpleScrollView()
		.tabItem {
			Image(systemName: "list.bullet")
			Text("Simple Scroll")
		}

	SimpleTabView()
		.tabItem {
			Image(systemName: "square.and.arrow.up")
			Text("Simple Tab")
		}
}

TabView creates a tab-based navigation where users can switch between different views using a tab bar. Each view inside TabView is assigned a “tab bar item” using .tabItem { }.

Each tab consists of:

  1. a view (here in our case: SimpleScrollView(), SimpleTabView())

  2. a tabItem modifier, which defines an icon using Image(systemName: "icon-name") (The icon-names are the SF symbols you already got to know when building your first app “ButtonAnimation”.) and a label using Text("Tab Name")

In our app, we got 2 tabs and we present the 3 views we created earlier.

Step 6: Run your app

You see already a preview of your app on the right-hand side in the canvas. To run your app in the simulator, please select a device at the very top-middle of the XCode window – you can also add your own physical device – and press the Play button.

Congratulations!

You’ve successfully built 3 different ScrollViews! 🎉

What you have learned

In this code-along, you’ve learned how to

  • structure a SwiftUI project – Organising code across multiple SwiftUI files for better maintainability.
  • work with ScrollView – Creating both horizontal and paginated scrolling views.
  • use a HStack and a LazyHStack and what is the difference, and embedding ForEach loop.
  • work with models in Swift – Creating an Animal struct and leveraging .map {} for data transformation.
  • create reusable views – Defining AnimalPhoto and using it dynamically for different data.
  • use GeometryReader – Measuring screen width dynamically for layout adjustments.
  • use .scrollTargetBehavior(.viewAligned) – Implementing a smooth snap effect for scroll views.
  • create a TabView – Displaying multiple views with swipe-based navigation and tab bar items.
  • set up an app’s entry point – Understanding ‘@main’, ‘WindowGroup’, and why ‘ContentView’ is initially displayed.

That’s a lot to cover in just one code-along! You made it this far, very well done! 🎉 Learning to structure and build interactive UI components takes time, but each step adds up.

Keep learning, keep building, and let your curiosity guide you. Happy coding! ✨

“Stay hungry. Stay foolish.” — Steve Jobs


The project will be available for download on GitHub after the second part of the scroll view code-along.