What You're Building
The About Me app is a real, running iOS app — not a prototype or a mock-up. It lives on your iPad, opens like any other app, and has five tabs you can swipe between. Each tab teaches a new SwiftUI concept while you personalise it to be all about you.
Why Swift? Swift is Apple's programming language — the same one used to build every app on the App Store. Learning SwiftUI here means you're using the same tools as professional iOS developers at companies worldwide.
1. The App Entry Point
Every SwiftUI app starts with a single file marked @main.
This tells the system: "start here."
In AboutMeApp.swift you'll see the simplest possible app shell:
import SwiftUI
@main
struct AboutMeApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
} @main Marks this struct as the program entry point. There can only be one in a project.
App protocol Every SwiftUI app conforms to the App protocol, which requires a body of type Scene.
WindowGroup The standard scene for apps with a main window. It launches ContentView as the first visible screen.
You'll rarely change this file. Its job is just to hand control to
ContentView, which is where the real app lives.
2. TabView — The App's Navigation Skeleton
ContentView.swift uses a TabView to create the tab bar at
the bottom of the screen. Each child view inside TabView becomes one tab.
TabView {
HomeView()
.tabItem {
Label("Home", systemImage: "person")
}
StoryView()
.tabItem {
Label("Story", systemImage: "book")
}
FavoritesView()
.tabItem {
Label("Favorites", systemImage: "star")
}
FunFactsView()
.tabItem {
Label("Fun Facts", systemImage: "hand.thumbsup")
}
YourTab()
.tabItem {
Label("My Tab", systemImage: "heart.fill")
}
} How a Tab Works
Each tab has two parts:
1. The View
The full-screen content shown when the tab is active — e.g. HomeView(), StoryView().
2. The .tabItem modifier
Sets the icon and label shown in the tab bar. Uses a Label with a title string and an SF Symbol name.
TabItem rule
The .tabItem modifier only accepts Label, Text, and Image views.
Using a Button or any other view type will produce an empty tab — it won't error, it just silently disappears.
3. Views: The Building Blocks
In SwiftUI, everything you see is a View. Text is a view. An image is a view. A button, a colour block, a divider — all views. You compose complex screens by nesting simple views inside container views.
Every custom view is a struct that conforms to the View protocol.
That means it must have a body property that returns some View:
struct HomeView: View {
var body: some View {
// Everything inside here is what gets displayed
Text("All About")
}
}
Core Views You Used
| View | What it displays | Used in |
|---|---|---|
| Text("...") | A string of text | Every tab |
| Image("name") | A photo or image from Assets | Home, Story |
| Image(systemName: "...") | An SF Symbol icon | Home, tab bar labels |
| Button("...") { } | A tappable button with action | Fun Facts |
| Color.blue | A solid colour block | Favorites |
| Divider() | A thin horizontal separator line | Story, Favorites |
| Spacer() | Flexible empty space that pushes views apart | Favorites |
4. The Three Stacks
Stacks are container views that arrange their children in a line. There are exactly three, and you need to know all of them.
Vertical — top to bottom
Used in: Home, Story, Fun Facts
Horizontal — left to right
Used in: Home name row, Favorites
Depth — back to front
Used in: Fun Facts background
You can — and should — nest stacks inside each other.
The Home tab uses a VStack as the outer container, with a
VStack for the name card, inside which sits an HStack
for the icon row. This nesting pattern is the core of all SwiftUI layout.
5. Modifiers
A modifier is a method chained onto a view that changes how it looks or behaves. You can chain as many as you like — each one wraps the previous result.
Text("Roan Venter")
.font(.custom(FontNames.courier, size: 40))
.foregroundColor(Color(red: 0.5, green: 0.25, blue: 0.1))
Order matters. Modifiers are applied from top to bottom. Adding
.padding() before .background() means the background fills
the padded area. Swapping them means only the text itself gets the background colour,
not the space around it. Try both and see the difference.
Modifier Reference
Text / Appearance
.font(.largeTitle)— preset size.font(.custom("Courier", size: 40))— custom font.fontWeight(.bold)— weight.foregroundColor(.brown)— text colour.italic()— italic style.foregroundColor(.secondary)— grey
Layout / Shape
.padding(30)— space around view.padding([.top, .bottom])— selective padding.background(Color(...))— fill colour.cornerRadius(15)— round corners.shadow(color: .black, radius: 10).frame(maxWidth: .infinity)
6. Images: Resizing, Clipping, and Overlays
A plain Image view renders at its natural pixel size — often enormous.
You almost always need to add at least .resizable() and a sizing modifier
before anything else will work correctly.
The Standard Image Stack
Image("profilePhoto")
.resizable()
.scaledToFit()
.clipShape(Circle())
.overlay(
Circle()
.stroke(.yellow, style: StrokeStyle(lineWidth: 15))
.shadow(color: .black, radius: 10)
) clipShape Options
Try each of these in place of Circle() — swap them one at a time to see the effect:
// Clip to a circle
.clipShape(Circle())
// Rounded rectangle (adjust cornerRadius to taste)
.clipShape(RoundedRectangle(cornerRadius: 20))
// Ellipse — like a circle but can be wider than tall
.clipShape(Ellipse())
// Capsule — rectangle with fully-rounded short ends
.clipShape(Capsule()) The overlay Modifier
.overlay() layers another view in front of the current one,
perfectly aligned. This is how the yellow border ring works — a
Circle drawn with .stroke() sits on top of the
clipped photo.
StrokeStyle lets you control line width, dash pattern, and line cap style.
For a simple solid border, StrokeStyle(lineWidth: 10) is all you need.
Increase the number for a thicker ring.
7. Custom Fonts
Instead of hard-coding font names as raw strings scattered through your code,
you can collect them in a helper struct called FontNames
that lives at the bottom of HomeView.swift:
struct FontNames {
static var americanTypewriter = "American Typewriter"
static var arial = "Arial"
static var baskerville = "Baskerville"
static var chalkduster = "Chalkduster"
static var courier = "Courier"
static var georgia = "Georgia"
static var helvetica = "Helvetica"
static var palatino = "Palatino"
static var zapfino = "Zapfino"
}
// Using it:
Text("Your Name")
.font(.custom(FontNames.courier, size: 40))
Now if you want to change a font you only edit it in one place —
and the autocomplete menu shows you all your options when you type FontNames..
Finding more fonts
Apple maintains a full list at developer.apple.com/fonts/system-fonts.
Add any font name you find there as a new static var in FontNames,
then use it anywhere in the app.
Font Size Presets
When you don't need a custom font, use SwiftUI's semantic size names — they respect the user's accessibility text-size setting:
| Modifier | Approx. size | Typical use |
|---|---|---|
| .largeTitle | 34pt | Screen title (e.g. "Fun Facts", "Favorites") |
| .title | 28pt | Section headings |
| .title2 | 22pt | Sub-headings (e.g. "Hobbies", "Foods") |
| .title3 | 20pt | Minor headings |
| .headline | 17pt bold | Emphasis text, chip labels |
| .body | 17pt | Default body text |
| .subheadline | 15pt | Author bylines, supporting text |
| .footnote | 13pt | Small annotations |
| .caption | 12pt | Image captions, timestamps |
8. SF Symbols
Apple ships over 5,000 icons free with every device — called SF Symbols.
You access them by name using Image(systemName:):
Finding symbols in Playgrounds: Tap the symbols library button (✪) in the toolbar.
You can search by keyword, browse categories, and tap to insert the code directly. Every symbol
also has .fill, .circle, .square, and other variants.
9. Colour Models
SwiftUI gives you four ways to specify a colour. All four were used in this project:
// Named SwiftUI colours
Color.blue
Color.red
Color.green
Color.orange
Color.purple
Color.pink
Color.yellow
Color.mint
Color.brown
Color.cyan
// RGB — values between 0.0 and 1.0
Color(red: 0.5, green: 0.25, blue: 0.1) // warm brown
Color(red: 0.13, green: 0.29, blue: 0.54) // TKC navy
// Hue / Saturation / Brightness (HSB)
Color(hue: 0.9, saturation: 0.5, brightness: 0.9)
// Greyscale — 0.0 is black, 1.0 is white
Color(white: 0.75)
// Transparency via opacity
Color.blue.opacity(0.3) RGB — Red, Green, Blue
Each channel is a decimal between 0.0 and 1.0 (not 0–255).
Used in the Home tab name card background Color(red: 0.75, green: 0.75, blue: 0.75) — a neutral grey.
HSB — Hue, Saturation, Brightness
Often easier for picking vivid colours. Hue is the colour wheel angle (0–1), saturation is intensity, brightness is lightness. Used in the Favorites colour swatches.
10. ScrollView
When content might overflow the screen, wrap it in a ScrollView.
By default it scrolls vertically — used in the Story tab.
Pass .horizontal to make it scroll sideways — used in the Favorites foods row.
Vertical (Story Tab)
ScrollView {
VStack(alignment: .leading) {
Text("My Story")
.font(.largeTitle)
Text("Author")
.font(.subheadline)
.foregroundColor(.secondary)
Divider()
Text("Everyone knows that I just love …")
.padding([.top, .bottom])
Text(" Rugby. My favourite team is the ACT Brumbies.")
.padding(.bottom)
}
.padding()
.frame(maxWidth: .infinity)
.background(in: RoundedRectangle(cornerRadius: 15))
.padding()
}
.background(Image("Blue").opacity(0.5)) Horizontal (Favorites Tab)
ScrollView(.horizontal) {
HStack(spacing: 30) {
Text("🥐").font(.system(size: 48))
Text("🌮").font(.system(size: 48))
Text("🍣").font(.system(size: 48))
Text("🍉").font(.system(size: 48))
Text("🥖").font(.system(size: 48))
Text("🍫").font(.system(size: 48))
}
}
.padding() Tip: Horizontal ScrollView is great for any list that could grow
without bound — foods, hobby emojis, photo cards. It keeps the layout clean no matter how many
items you add.
11. Group — the 10-View Limit
SwiftUI has a hard limit: no more than 10 direct children inside a single stack. If you add an 11th view, you'll get an error that says something like "Extra arguments at positions #11 in call".
The fix is Group. It doesn't change the visual output at all —
it's purely an organisational wrapper that counts as one child, regardless of how
many views are inside it.
// SwiftUI only allows 10 direct children in a stack.
// Wrap related views in Group{} to get around the limit.
VStack {
Group {
Text("My Story")
.font(.largeTitle)
Text("Roan Venter")
.font(.subheadline)
.foregroundColor(.secondary)
Divider()
}
Group {
Text("Everyone knows that I just love …")
.padding([.top, .bottom])
Text(" Rugby.")
.padding(.bottom)
Text("My super power is …")
Text(" Teaching")
}
Group {
// more content...
}
}
Beyond fixing the limit, Group is also good practice for organising
your code into logical sections — title block, body paragraphs, images — even when
you haven't hit 10 views yet.
12. DisclosureGroup — Collapsible Sections
A DisclosureGroup shows a header that the user can tap to reveal or hide content.
It was used in the Favorites tab to let users guess your favourite colours before revealing them.
DisclosureGroup {
// Content shown when expanded
HStack(spacing: 30) {
Color.purple
.frame(width: 70, height: 70)
.cornerRadius(10)
Color(hue: 0.9, saturation: 0.5, brightness: 0.9)
.frame(width: 70, height: 70)
.cornerRadius(10)
Color.orange
.frame(width: 70, height: 70)
.cornerRadius(10)
Spacer()
}
.padding(.vertical)
} label: {
// The header — always visible
Text("Guess my favourite colors")
.font(.title2)
}
.padding()
.accentColor(.purple) Collapsed (default)
Expanded (after tap)
The label: trailing closure is what's always visible — the header.
The first trailing closure (before label:) is the content, revealed on tap.
13. Arrays
An array is an ordered list of values, all of the same type. In the Fun Facts tab, an array holds all the facts waiting to be shown:
// Declare an array of Strings
var allFunFacts = [
"I have visited 8 different countries.",
"I play guitar in my spare time.",
"I coach a junior rugby team on weekends.",
"I have read over 200 books."
]
// Pick a random element — returns Optional, so use ?? for a default
funFact = allFunFacts.randomElement() ?? "No facts available." Key concepts from this usage
var allFunFacts = [...] Declares a variable array of String values. The square brackets define the array; each item is a String in quotes, separated by commas.
.randomElement() A built-in method on any Array that returns a random item. It returns an Optional — the value might be nil if the array is empty.
Optional — String? randomElement() returns String? (Optional String) because the array could theoretically be empty. You must unwrap it before using it.
?? "No facts available." The nil-coalescing operator. If the left side is nil, use the right side as a default. This is how you safely unwrap an Optional in one line.
14. @State and Button — Reactive UI
The Fun Facts tab is where the app comes alive. A @State variable
stores what text is currently showing. A Button changes it.
When the value changes, SwiftUI rebuilds the view automatically.
struct FunFactsView: View {
var allFunFacts = [
"I have visited 8 different countries.",
"I play guitar in my spare time.",
"I coach a junior rugby team on weekends."
]
@State private var funFact = ""
var body: some View {
ZStack {
Image("Green") // background layer
VStack {
Text("Fun Facts")
.font(.largeTitle)
Text(funFact)
.font(.title)
.frame(maxWidth: 400, minHeight: 300)
Button("Show Random Fact") {
funFact = allFunFacts.randomElement() ?? "No fun."
}
.padding()
.foregroundColor(.white)
.background(.blue)
.clipShape(RoundedRectangle(cornerRadius: 15))
.font(.title2)
}
.padding()
}
}
} How the data flows:
funFact = allFunFacts.randomElement() ?? "..."Text(funFact) view
This pattern — @State variable + Button + reactive Text — is
the foundation of almost all interactive SwiftUI development.
Master it here and you've understood something that scales all the way to professional apps.
private means
@State private var funFact = "" — the private keyword means
this variable can only be accessed inside FunFactsView.
Other views can't reach in and change it. This is good practice: keep state local
to the view that owns it.
15. The Home Tab — Putting It All Together
The Home tab uses the most techniques at once. Here's the complete code your teacher wrote as a model — read through it and identify every concept it uses:
struct HomeView: View {
var body: some View {
VStack {
Text("All About")
.font(.largeTitle)
.fontWeight(.bold)
.padding()
Image("Family")
.resizable()
.scaledToFit()
.clipShape(RoundedRectangle(cornerRadius: 26.0))
.overlay(
RoundedRectangle(cornerRadius: 26.0)
.stroke(.yellow, style: StrokeStyle(lineWidth: 10))
.shadow(color: .black, radius: 10)
)
VStack {
Text("Roan Venter")
.font(.custom(FontNames.courier, size: 40))
.foregroundColor(Color(red: 0.5, green: 0.25, blue: 0.1))
HStack {
Image(systemName: "flag.pattern.checkered")
.foregroundColor(.brown)
Text("Grateful")
.font(.custom(FontNames.courier, size: 20))
Image(systemName: "flag.pattern.checkered")
.foregroundColor(.brown)
}
}
.padding(30)
.background(Color(red: 0.75, green: 0.75, blue: 0.75))
.cornerRadius(15)
.shadow(color: .black, radius: 10)
}
.background(Image("Blue"))
.padding()
}
} Concepts in use in HomeView:
16. Your Own Tab
The final challenge is YourTab.swift — a blank view that starts with
just a "Hello, World!" placeholder. You design it from scratch using everything
you've learned:
// Start with the placeholder:
struct YourTab: View {
var body: some View {
Text("Hello, World!")
}
}
// Replace it with your own design:
struct YourTab: View {
var body: some View {
VStack(spacing: 20) {
Image(systemName: "trophy.fill")
.font(.system(size: 60))
.foregroundColor(.yellow)
Text("Achievements")
.font(.largeTitle)
.fontWeight(.bold)
Text("Year 10, 2025")
.foregroundColor(.secondary)
}
.padding()
}
}
Once your tab is built, wire it into ContentView.swift by adding
it to the TabView:
// In ContentView.swift, add your tab inside TabView:
YourTab()
.tabItem {
Label("My Tab", systemImage: "heart.fill")
} 17. App Settings
The final polish step is configuring the app's identity in App Settings:
Open the left sidebar
Tap the sidebar button in the top-left of Swift Playgrounds.
Tap App Settings
Near the top of the sidebar — this opens the app configuration panel.
Change the app name
Replace "About Me" with your own title — this appears under the icon when installed.
Set the accent colour
This tints interactive elements (buttons, toggles, DisclosureGroup arrows) across the whole app.
Change the app icon
Tap "Placeholder" to pick from system options, or tap "Custom" to upload your own design from Photos.