SwiftUI Essentials
⭐️
SwiftUI ⟩ intro ⟩ SwiftUI Essentials
時間╱主題 | 程式碼 |
---|---|
2:30 - Declarative views | 👉 2:30 tab |
2:43 - Declarative views: layout | 👉 2:43 tab |
2:56 - Declarative views: list | 👉 Pet, ContentView tabs |
3:07 - Declarative views: list | 👉 Pet, ContentView tabs |
👉 Pet, ContentView tabs | |
5:33 - Layout container | 👉 5:33 tab |
5:41 - Container views | 👉 Pet, ContentView tabs |
6:23 - View modifiers | 👉 Pet, ContentView tabs |
7:14 - Custom views | 👉 PetRowView, Pet tabs |
7:20 - Custom views: iteration | 👉 PetRowView, Pet tabs |
👉 PetRowView, Pet tabs | |
👉 PetRowView tab | |
7:53 - Custom views: reuse |
SwiftUI Essentials (WWDC2024)
2:30 - Declarative views
// declarative views
Text("Whiskers")
Image(systemName: "cat.fill")
Button("Give Treat") { /* action */ }
2:43 - Declarative views: layout
// layout (container view)
HStack {
Label("Whiskers", systemImage: "cat.fill")
Spacer()
Text("Tightrope walking")
}
5:33 - Layout container
// container view (5:33)
HStack {
Label("Whiskers", systemImage: "cat.fill")
Spacer()
Text("Tightrope walking")
}
7:53 - Custom views: reuse
PetRowView(pet: model.pet(named: "Whiskers"))
PetRowView(pet: model.pet(named: "Roofus"))
PetRowView(pet: model.pet(named: "Bubbles"))
Views
ContentView
PetRowView
RatingView
RatingContainerView
PetListView
struct ContentView: View {
// view state
@State private var pets = Pet.samplePets
var body: some View {
List(pets) { pet in // scrollable list
HStack {
// 固定的內容
Label("Whiskers", systemImage: "cat.fill")
Spacer()
Text("Tightrope walking")
}
}
}
}
struct ContentView: View {
// view state
@State private var pets = Pet.samplePets
var body: some View {
List(pets) { pet in
HStack {
// 內容由 pet 決定
Label(pet.name, systemImage: pet.kind.systemImage)
Spacer()
Text(pet.trick)
}
}
}
}sw
struct ContentView: View {
// view state
@State private var pets = Pet.samplePets
var body: some View {
// action button
Button("Add Pet") {
// imperative code (4:24)
pets.append(Pet("Toby", kind: .dog, trick: "WWDC Presenter"))
}
// declarative views (2:30)
List(pets) { pet in
HStack {
Label(pet.name, systemImage: pet.kind.systemImage)
Spacer()
Text(pet.trick)
}
}
}// body
}
struct ContentView: View {
var body: some View {
// container view (5:41)
HStack {
Image(whiskers.profileImage)
// container view (5:41)
VStack(alignment: .leading) {
Label("Whiskers", systemImage: "cat.fill")
Text("Tightrope walking")
}
Spacer()
}
}
}
struct ContentView: View {
var body: some View {
Image(whiskers.profileImage)
.clipShape(.circle) // view modifier
.shadow(radius: 3) // view modifier
.overlay { // view modifier
Circle().stroke(.green, lineWidth: 2)
}
}
}
struct ContentView: View {
var model: PetStore
var body: some View {
List(model.allPets) { pet in // List
PetRowView(pet: pet)
}
}
}
struct ContentView: View {
var model: PetStore
var body: some View {
List {
ForEach(model.allPets) { pet in // ForEach
PetRowView(pet: pet)
}
}
}
}
struct ContentView: View {
var model: PetStore
var body: some View {
List {
// ⭐️ List composition: sections
Section("My Pets") {
ForEach(model.myPets) { pet in
PetRowView(pet: pet)
}
}
Section("Other Pets") {
ForEach(model.otherPets) { pet in
PetRowView(pet: pet)
}
}
}
}
}
struct ContentView: View {
var model: PetStore
var body: some View {
List {
Section("My Pets") {
ForEach(model.myPets) { pet in
row(pet: pet)
}
}
Section("Other Pets") {
ForEach(model.otherPets) { pet in
row(pet: pet)
}
}
}
}
// ⭐️ private view prperty
private func row(pet: Pet) -> some View {
PetRowView(pet: pet)
// ⭐️ row action
.swipeActions(edge: .leading) {
Button("Award", systemImage: "trophy") {
pet.giveAward()
}
.tint(.orange)
ShareLink(item: pet, preview: SharePreview("Pet", image: Image(pet.name)))
}
}
}
// PetRowView
struct PetRowView: View {
var body: some View {
HStack {
Image(whiskers.profileImage)
.clipShape(.circle)
.shadow(radius: 3)
.overlay {
Circle()
.stroke(.green, lineWidth: 2)
}
Text("Whiskers")
Spacer()
}
}
}
// PetRowView (custom view)(7:24, 7:34)
struct PetRowView: View {
var body: some View {
HStack { // container view (5:33)
// ⭐️ pet image
profileImage // private view property (7:24)
// pet name + trick
VStack(alignment: .leading) { // container view (5:41)
Text("Whiskers")
Text("Tightrope walking")
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
}
}
// ⭐️ private view property (7:24)
private var profileImage: some View {
Image(whiskers.profileImage)
.clipShape(.circle) // view modifier (6:23)
.shadow(radius: 3) // view modifier
.overlay { // view modifier
Circle().stroke(.green, lineWidth: 2)
}
}
}
// PetRowView(pet: pet)
struct PetRowView: View {
// ⭐️ input property
var pet: Pet
var body: some View {
HStack {
profileImage
VStack(alignment: .leading) {
Text(pet.name)
Text(pet.trick)
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
}
}
// private view property
private var profileImage: some View {
Image(pet.profileImage)
.clipShape(.circle)
.shadow(radius: 3)
.overlay {
Circle().stroke(pet.favoriteColor, lineWidth: 2)
}
}
}
struct PetRowView: View {
var pet: Pet
var body: some View {
HStack {
profileImage
VStack(alignment: .leading) {
HStack(alignment: .firstTextBaseline) {
Text(pet.name)
// ⭐️ conditional view
if pet.hasAward {
Image(systemName: "trophy.fill")
.foregroundStyle(.orange)
}
}
Text(pet.trick)
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
}
}
private var profileImage: some View {
Image(pet.profileImage)
.clipShape(.circle)
.shadow(radius: 3)
.overlay {
Circle().stroke(pet.favoriteColor, lineWidth: 2)
}
}
}
10:57 - State changes
struct RatingView: View {
// ⭐️ internal state
@State var rating: Int = 5
var body: some View {
HStack {
// ⭐️ button (-)
Button("Decrease", systemImage: "minus.circle") {
rating -= 1
}
.disabled(rating == 0)
.labelStyle(.iconOnly)
// ⭐️
Text(rating, format: .number.precision(.integerLength(2)))
.font(.title.bold())
// ⭐️ button (+)
Button("Increase", systemImage: "plus.circle") {
rating += 1
}
.disabled(rating == 10)
.labelStyle(.iconOnly)
}
}
}
11:51 - State changes: animation
struct RatingView: View {
@State var rating: Int = 5
var body: some View {
HStack {
Button("Decrease", systemImage: "minus.circle") {
withAnimation { // ⭐️ animation
rating -= 1
}
}
.disabled(rating == 0)
.labelStyle(.iconOnly)
Text(rating, format: .number.precision(.integerLength(2)))
.font(.title.bold())
Button("Increase", systemImage: "plus.circle") {
withAnimation { // ⭐️ animation
rating += 1
}
}
.disabled(rating == 10)
.labelStyle(.iconOnly)
}
}
}
12:05 - State changes: text content transition
struct RatingView: View {
@State var rating: Int = 5
var body: some View {
HStack {
Button("Decrease", systemImage: "minus.circle") {
withAnimation {
rating -= 1
}
}
.disabled(rating == 0)
.labelStyle(.iconOnly)
Text(rating, format: .number.precision(.integerLength(2)))
// ⭐️ text content transition
.contentTransition(.numericText(value: Double(rating)))
.font(.title.bold())
Button("Increase", systemImage: "plus.circle") {
withAnimation {
rating += 1
}
}
.disabled(rating == 10)
.labelStyle(.iconOnly)
}
}
}
12:45 - State changes: state and binding
struct RatingView: View {
// ⭐️ external binding
@Binding var rating: Int
var body: some View {
HStack {
Button("Decrease", systemImage: "minus.circle") {
withAnimation {
rating -= 1
}
}
.disabled(rating == 0)
.labelStyle(.iconOnly)
Text(rating, format: .number.precision(.integerLength(2)))
.contentTransition(.numericText(value: Double(rating)))
.font(.title.bold())
Button("Increase", systemImage: "plus.circle") {
withAnimation {
rating += 1
}
}
.disabled(rating == 10)
.labelStyle(.iconOnly)
}
}
}
12:22 - State changes: multiple state 12:45 - State changes: state and binding
struct RatingContainerView: View {
// ⭐️ internal state
@State private var rating: Int = 5
var body: some View {
Gauge(value: Double(rating), in: 0...10) {
Text("Rating")
}
RatingView()
}
}
15:19 - Searchable
struct PetListView: View {
// ⭐️
@Bindable var viewModel: PetStoreViewModel
var body: some View {
List {
Section("My Pets") {
ForEach(viewModel.myPets) { pet in
row(pet: pet)
}
}
Section("Other Pets") {
ForEach(viewModel.otherPets) { pet in
row(pet: pet)
}
}
}
// ⭐️ searchable
.searchable(text: $viewModel.searchText)
}
private func row(pet: Pet) -> some View {
PetRowView(pet: pet)
.swipeActions(edge: .leading) {
Button("Reward", systemImage: "trophy") {
pet.giveAward()
}
.tint(.orange)
ShareLink(item: pet, preview: SharePreview("Pet", image: Image(pet.name)))
}
}
}
15:20 - Searchable: customization
struct PetListView: View {
@Bindable var viewModel: PetStoreViewModel
var body: some View {
List {
Section("My Pets") {
ForEach(viewModel.myPets) { pet in
row(pet: pet)
}
}
Section("Other Pets") {
ForEach(viewModel.otherPets) { pet in
row(pet: pet)
}
}
}
// ⭐️ searchable: customization
.searchable(text: $viewModel.searchText, editableTokens: $viewModel.searchTokens) { $token in
Label(token.kind.name, systemImage: token.kind.systemImage)
}
// ⭐️ searchable: customization
.searchScopes($viewModel.searchScope) {
Text("All Pets").tag(PetStoreViewModel.SearchScope.allPets)
Text("My Pets").tag(PetStoreViewModel.SearchScope.myPets)
Text("Other Pets").tag(PetStoreViewModel.SearchScope.otherPets)
}
// ⭐️ searchable: customization
.searchSuggestions {
PetSearchSuggestions(viewModel: viewModel)
}
}
private func row(pet: Pet) -> some View {
PetRowView(pet: pet)
.swipeActions(edge: .leading) {
Button("Reward", systemImage: "trophy") {
pet.giveAward()
}
.tint(.orange)
ShareLink(item: pet, preview: SharePreview("Pet", image: Image(pet.name)))
}
}
}
Data Types
Pet
PetStore
PetStoreViewModel
// ⭐️ whiskers (global constant)
let whiskers = Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers")
// ⭐️ Pet
struct Pet: Identifiable {
// -------- ⭐️ Pet.Kind --------
enum Kind {
// enum cases
case cat
case dog
case fish
case bird
case lizard
case turtle
case rabbit
case bug
// ⭐️ pet.kind.systemImage
var systemImage: String {
switch self {
case .cat: return "cat.fill"
case .dog: return "dog.fill"
case .fish: return "fish.fill"
case .bird: return "bird.fill"
case .lizard: return "lizard.fill"
case .turtle: return "tortoise.fill"
case .rabbit: return "rabbit.fill"
case .bug: return "ant.fill"
}
}
}// enum Kind
// -------- pet properties --------
let id = UUID() // pet.id ⭐️
var name: String // pet.name
var kind: Kind // pet.kind
var trick: String // pet.trick
var profileImage: String // pet.profileImage
// -------- init --------
// ⭐️ Pet("name", .cat, trick: "trick", profileImage: "imageName")
init(_ name: String, kind: Kind, trick: String, profileImage: String) {
self.name = name
self.kind = kind
self.trick = trick
self.profileImage = profileImage
}
}
import SwiftUI // for Color ⭐️
// Pet
struct Pet: Identifiable {
enum Kind {
case cat
case dog
case fish
case bird
case lizard
case turtle
case rabbit
case bug
var systemImage: String {
switch self {
case .cat: return "cat.fill"
case .dog: return "dog.fill"
case .fish: return "fish.fill"
case .bird: return "bird.fill"
case .lizard: return "lizard.fill"
case .turtle: return "tortoise.fill"
case .rabbit: return "rabbit.fill"
case .bug: return "ant.fill"
}
}
}
let id = UUID()
var name: String
var kind: Kind
var trick: String
var profileImage: String
var favoriteColor: Color // ⭐️ new prperty (7:41)
// ⭐️ new initializer (7:41)
init(
_ name: String,
kind: Kind,
trick: String,
profileImage: String,
favoriteColor: Color // ⭐️ new prperty (7:41)
) {
self.name = name
self.kind = kind
self.trick = trick
self.profileImage = profileImage
self.favoriteColor = favoriteColor
}
}
@Observable
class Pet: Identifiable {
// Pet.Kind
enum Kind {
case cat
case dog
case fish
case bird
case lizard
case turtle
case rabbit
case bug
var systemImage: String {
switch self {
case .cat: return "cat.fill"
case .dog: return "dog.fill"
case .fish: return "fish.fill"
case .bird: return "bird.fill"
case .lizard: return "lizard.fill"
case .turtle: return "tortoise.fill"
case .rabbit: return "rabbit.fill"
case .bug: return "ant.fill"
}
}
}
var name: String
var kind: Kind
var trick: String
var profileImage: String
var favoriteColor: Color
var hasAward: Bool = false // ⭐️ new prperty (9:31)
init(_ name: String, kind: Kind, trick: String, profileImage: String, favoriteColor: Color) {
self.name = name
self.kind = kind
self.trick = trick
self.profileImage = profileImage
self.favoriteColor = favoriteColor
}
// ⭐️ new method (9:31)
func giveAward() {
hasAward = true
}
}
// ⭐️
extension Pet: Transferable {
static var transferRepresentation: some TransferRepresentation {
ProxyRepresentation { $0.name }
}
}
// 7:59, 8:14
@Observable
class PetStore {
var allPets: [Pet] = [
Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers", favoriteColor: .green),
Pet("Roofus", kind: .dog, trick: "Home runs", profileImage: "Roofus", favoriteColor: .blue),
Pet("Bubbles", kind: .fish, trick: "100m freestyle", profileImage: "Bubbles", favoriteColor: .orange),
Pet("Mango", kind: .bird, trick: "Basketball dunk", profileImage: "Mango", favoriteColor: .green),
Pet("Ziggy", kind: .lizard, trick: "Parkour", profileImage: "Ziggy", favoriteColor: .purple),
Pet("Sheldon", kind: .turtle, trick: "Kickflip", profileImage: "Sheldon", favoriteColor: .brown),
Pet("Chirpy", kind: .bug, trick: "Canon in D", profileImage: "Chirpy", favoriteColor: .orange)
]
}
// 8:27, 9:31
@Observable
class PetStore {
var myPets: [Pet] = [
Pet("Roofus", kind: .dog, trick: "Home runs", profileImage: "Roofus", favoriteColor: .blue),
Pet("Sheldon", kind: .turtle, trick: "Kickflip", profileImage: "Sheldon", favoriteColor: .brown),
]
var otherPets: [Pet] = [
Pet("Whiskers", kind: .cat, trick: "Tightrope walking", profileImage: "Whiskers", favoriteColor: .green),
Pet("Bubbles", kind: .fish, trick: "100m freestyle", profileImage: "Bubbles", favoriteColor: .orange),
Pet("Mango", kind: .bird, trick: "Basketball dunk", profileImage: "Mango", favoriteColor: .green),
Pet("Ziggy", kind: .lizard, trick: "Parkour", profileImage: "Ziggy", favoriteColor: .purple),
Pet("Chirpy", kind: .bug, trick: "Canon in D", profileImage: "Chirpy", favoriteColor: .orange)
]
}
15:19 - Searchable
@Observable
class PetStoreViewModel {
var petStore: PetStore
var searchText: String = ""
init(petStore: PetStore) {
self.petStore = petStore
}
var myPets: [Pet] {
// For illustration purposes only. The filtered pets should be cached.
petStore.myPets.filter { searchText.isEmpty || $0.name.contains(searchText) }
}
var otherPets: [Pet] {
// For illustration purposes only. The filtered pets should be cached.
petStore.otherPets.filter { searchText.isEmpty || $0.name.contains(searchText) }
}
}
App Definition
16:58 - App definition
@main
struct SwiftUIEssentialsApp: App {
var body: some Scene {
// ⭐️ app definition
WindowGroup {
ContentView()
}
}
}
17:15 - App definition: multiple scenes
@main
struct SwiftUIEssentialsApp: App {
var body: some Scene {
// ⭐️ multiple scenes
WindowGroup {
ContentView()
}
// ⭐️ multiple scenes
WindowGroup("Training History", id: "history", for: TrainingHistory.ID.self) { $id in
TrainingHistoryView(historyID: id)
}
// ⭐️ multiple scenes
WindowGroup("Pet Detail", id: "detail", for: Pet.ID.self) { $id in
PetDetailView(petID: id)
}
}
}
17:23 - Widgets
struct ScoreboardWidget: Widget {
var body: some WidgetConfiguration {
// ...
}
}
struct ScoreboardWidgetView: View {
var petTrick: PetTrick
var body: some View {
ScoreCard(rating: petTrick.rating)
.overlay(alignment: .bottom) {
Text(petTrick.pet.name)
.padding()
}
.widgetURL(petTrick.pet.url)
}
}
19:37 - Digital Crown rotation
ScoreCardStack(rating: $rating)
.focusable()
#if os(watchOS)
.digitalCrownRotation($rating, from: 0, through: 10)
#endif
Last updated