In this hackpack we will cover the basics of SwiftUI and create a simple iOS app. SwiftUI is a framework that allows you to build user interfaces across all Apple platforms with the power of Swift. It is a declarative framework, meaning that you can describe your user interface in a declarative manner, and SwiftUI will handle the rest. SwiftUI is also cross-platform, meaning that you can use the same code to build apps for iOS, macOS, watchOS, and tvOS (assuming the user interface is updated to fit the platform). Without further ado, let's get started!
-
Install Xcode from the App Store (https://apps.apple.com/us/app/xcode/id497799835?mt=12)
-
Open Xcode and click "Create New Project"
- Select "App" under "iOS"
- Enter a product name. For the organization identifier, enter your name in reverse domain name notation (e.g. com.apple). Leave the rest of the fields as default and click "Next"
- Choose a location to save your project and click "Create". You should see a screen as shown below. On the right side of the screen, you should see a preview of your app. This is the default view that Xcode provides for you. You can click the play button on the top left of the screen to run your app in the simulator (or on your device if you have one connected).
- On the left side of the screen, you should see a file navigator. This is where you can view all of the files in your project and also create new files/folders. Above the folder tree, you should see a row of buttons that show you different properties of the project. For example, you can see all the breakpoints or monitor the memory usage of your app.
- At the very top, you should see the current device Xcode would run your app on. You can click this to change the device.
- Github is integrated into Xcode. You can click the source control button on the top right of the screen to see all the changes you've made to your project. You can also commit and push your changes to Github from here.
-
By default, you should see a file called
ContentView.swift
. This is the file that contains the code for the default view that Xcode provides for you. You can see the code for the view on the right side of the screen. -
ContentView.swift
is called inMy_AppApp.swift
. This is the file that contains the code for the app itself. You can see the code for the app on the right side of the screen. For now, you can ignore this file. -
Let's go back to
ContentView.swift
. You'll notice that it's a struct that conforms to theView
protocol. This means that it's a view. You can see the code for the view on the right side of the screen. Thebody
property is a computed property that returns asome View
. This means that the type of the view is unknown, but it conforms to theView
protocol. Thesome
keyword is used to hide the type of the view. You can read more about opaque return types here.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
}
}
#Preview {
ContentView()
}
-
Within the
body
property, you'll see aVStack
. This is a view that stacks its children vertically (docs). Within theVStack
, you'll see anImage
(docs) and aText
(docs). You'll also see a modifier calledpadding
(docs). -
A modifier is a method that returns a modified version of the view. They should be indented under the view they modify. You can chain multiple modifiers together to modify the view. For example, you can add a background color to the view by adding a modifier called
background
to the end of the view (docs). The order of the modifiers matters. If you add thebackground
modifier before thepadding
modifier, the background color will only be applied to the view itself, not the padding. If you add thebackground
modifier after thepadding
modifier, the background color will be applied to the view and the padding.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
.background(Color.red)
}
}
- At the very bottom of the file, there's a
#Preview
struct (albeit with special syntax) that conforms to thePreviewProvider
protocol. This struct is what creates the preview on the right side of the screen.
-
Let's add a new view to the project that creates a button. Right click on the folder tree and click "New File...". Select "Swift File" and click "Next". Enter "ButtonView.swift" as the file name and click "Create". You should see a new file called
ButtonView.swift
in the folder tree. -
In
ButtonView.swift
, add the following shown below. By creating avar label: String
property, we can pass in a label to the view. You can read more about properties here.
import SwiftUI
struct ButtonView: View {
var label: String
var action: () -> Void = {}
var body: some View {
Button(action: action) {
Text(label)
}
}
}
- Go back to
ContentView.swift
. Add the following code to theVStack
, underText
:
ButtonView(
label: "Tap me!",
action: {
print("Button tapped")
}
)
- Click the play button on the top left of the screen to run your app in the simulator. You should see a button that says "Tap me!". When you tap the button, you should see "Button tapped" printed in the console (which is at the bottom of the screen).
- Navigation is a way to navigate between different views. Let's add a navigation view to the app. Go back to
ContentView.swift
. Wrap theVStack
in aNavigationStack
(docs).
NavigationStack {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
ButtonView(
label: "Tap me!",
action: {
print("Button tapped")
}
)
}
.padding()
}
-
Let's create a new screen that displays a list of items. Right click on the folder tree and click "New File...". Select "Swift File" and click "Next". Enter "ListView.swift" as the file name and click "Create". You should see a new file called
ListView.swift
in the folder tree. -
In
ListView.swift
, add the following shown below. By creating avar items: [String]
property, we can pass in a list of items to the view. You can read more about arrays here.
import SwiftUI
struct ListView: View {
var items: [String]
var body: some View {
List(items, id: \.self) { item in
Text(item)
}
}
}
- Go back to
ContentView.swift
. Add a navigation link to theVStack
, underButtonView
. You can read more about navigation links here.
NavigationStack {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
ButtonView(
label: "Tap me!",
action: {
print("Button tapped")
}
)
NavigationLink(destination: ListView(items: ["Item 1", "Item 2", "Item 3"])) {
Text("Go to list")
}
}
.padding()
}
- Here's what the app should look like now:
-
Let's create a new screen that displays a list of items from an API. Right click on the folder tree and click "New File...". Select "Swift File" and click "Next". Enter "NetworkingView.swift" as the file name and click "Create". You should see a new file called
NetworkingView.swift
in the folder tree. -
In
NetworkingView.swift
, let's call the JSON Placeholder API to get a list of posts. Add the following shown below. By creating a@State var posts: [Post]
property, we can store the list of posts in the view. You can read more about states here.
import SwiftUI
struct NetworkingView: View {
@State var posts: [Post] = []
var body: some View {
List(posts, id: \.id) { post in
VStack(alignment: .leading) {
Text(post.title)
.font(.headline)
Text(post.body)
.font(.subheadline)
}
}
.task {
do {
posts = try await fetchPosts()
} catch {
print(error)
}
}
}
func fetchPosts() async throws -> [Post] {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Post].self, from: data)
}
}
struct Post: Codable, Identifiable {
let id: Int
let title: String
let body: String
}
-
A task is a piece of code that runs asynchronously. In the
task
modifier, we call thefetchPosts
function to get a list of posts. We then set theposts
property to the list of posts. You can read more about thetask
modifier here. -
A
Codable
type is a type that can be encoded and decoded from JSON (docs). We create aPost
struct that conforms to theCodable
protocol. We also make it conform to theIdentifiable
protocol so that we can use it in aList
. -
An
Identifiable
type is a type that has a stable identity (docs). We make theid
property of thePost
struct the stable identity. -
Go back to
ContentView.swift
. Add a navigation link to theVStack
, underNavigationLink
.
NavigationStack {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
ButtonView(
label: "Tap me!",
action: {
print("Button tapped")
}
)
NavigationLink(destination: ListView(items: ["Item 1", "Item 2", "Item 3"])) {
Text("Go to list")
}
NavigationLink(destination: NetworkingView()) {
Text("Go to networking")
}
}
.padding()
}
- Here's what the app should look like now:
-
There are 3 main layout views in SwiftUI:
VStack
,HStack
, andZStack
. We've already seenVStack
, which stacks its children vertically.HStack
stacks its children horizontally (docs).ZStack
stacks its children on top of each other (docs). -
You can also use the
Spacer
view to add space between views (docs). -
Let's add the following code to
ContentView.swift
(inside VStack, under the NavigationLink):
Spacer()
HStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
ZStack {
Circle()
.fill(Color.blue)
.padding(50)
Text("Hello, world!")
}
- Here's what the app should look like now:
-
Now that we've gotten some basic SwiftUI down, let's create a more structured layout with some styling.
-
First, let's make the buttons look more like clickable buttons. Go to
ButtonView.swift
and replace the contents of the file with the following:
import SwiftUI
struct ButtonView<Content: View>: View {
var label: Content
var action: () -> Void = {}
var body: some View {
Button(action: action) {
label
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.blue)
.cornerRadius(10)
}
}
}
-
By making the
label
property a generic type, we can pass in any view as the label. However, we also have to change theButtonView
header tostruct ButtonView<Content: View>: View {
, which means that the structButtonView
has a generic type paramterContent
that conforms to theView
protocol. You can read more about generics here. -
Let's make navigation links look more like buttons. Make a new file called
NavigationButtonView.swift
and add the following code:
import SwiftUI
struct NavigationButtonView<Content: View>: View {
var label: Content
var destination: AnyView
var body: some View {
NavigationLink(destination: destination) {
label
.font(.headline)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.blue)
.cornerRadius(10)
}
}
}
- Replace the contents of
ContentView.swift
with the following:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationStack {
VStack {
HStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 10)
.padding()
ZStack {
Circle()
.fill(Color.blue)
.padding(50)
Text("Hello, world!")
}
.padding()
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 10)
.padding()
Spacer()
HStack {
ButtonView(
label: Text("Tap me!")
action: {
print("Button tapped")
}
)
NavigationButtonView(
label: Text("Go to List View"),
destination: AnyView(ListView(items: ["Item 1", "Item 2", "Item 3"]))
)
NavigationButtonView(
label: Text("Go to Network"),
destination: AnyView(NetworkingView())
)
}
.padding(.horizontal)
}
}
}
}
-
Notice how for the
destination
argument inNavigationButtonView
, we have to wrap the view inAnyView
, because that's what thedestination
argument expects. This is because thedestination
argument is of typeAnyView
(docs). -
Here's what the app should look like now:
You've finished the SwiftUI hackpack! Now, you can build your own SwiftUI apps!
MIT
HackPacks are built by the TreeHacks team and contributors to help hackers build great projects at our hackathon that happens every February at Stanford. We believe that everyone of every skill level can learn to make awesome things, and this is one way we help facilitate hacker culture. We open source our hackpacks (along with our internal tech) so everyone can learn from and use them! Feel free to use these at your own hackathons, workshops, and anything else that promotes building :)
If you're interested in attending TreeHacks, you can apply on our website during the application period.
You can follow us here on GitHub to see all the open source work we do (we love issues, contributions, and feedback of any kind!), and on Facebook, Twitter, and Instagram to see general updates from TreeHacks.