#02 使用 SwiftUI 製作 BTS 動畫電子書

洪瑋雪
海大 SwiftUI iOS / Flutter App 程式設計
19 min readOct 25, 2023

用 iPad Playgrounds SwiftUI 做出電子書

Light mode
Dark mode

功能展示

使用 TabView 製作下面有 tab bar 的分頁

ZStack{
TabView {
MainView()
.tabItem {
Image(systemName: "house.fill")
Text("主頁")
}
CharacterView()
.tabItem {
Image(systemName: "person.3.fill")
Text("團員介紹")
}
PictureView()
.tabItem {
Image(systemName: "photo.artframe")
Text("照片牆")
}
MusicView()
.tabItem {
Image(systemName: "music.mic")
Text("音樂作品")
}
}
.accentColor(.purple) //選取時呈現紫色
}

使用 NavigationStack & NavigationLink 切換頁面 & 傳資料到下一頁,navigation bar 上要顯示標題

NavigationStack { // 使用NavigationStack來支援導航功能
ScrollView(.vertical, showsIndicators: false) { // 垂直捲動的ScrollView,不顯示捲動指示器
VStack { // 垂直排列的視圖

// 顯示BTS的封面圖片,使用TabView實現輪播效果
TabView {
ForEach(0..<3) { item in
Image(bts[item])
.resizable()
.scaledToFill()
}
}
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
.frame(height: 600)
.overlay(VStack {
Text("關於BTS")
.font(.system(size: 50))
.foregroundColor(.white)
.bold()
Text("About us")
.font(.system(size: 30))
.foregroundColor(.white)
.fontWeight(.light)
})

// 顯示BTS的全體照片
Image("全體照")
.resizable()
.scaledToFill()
.frame(width: 300, height: 250)
.offset(x: 0, y: 10)

NavigationLink {
// 點擊後跳轉到人物介紹畫面
CharacterView()
} label: {
Text("人物介紹")
.font(.system(size: 15))
.bold()
.padding(EdgeInsets(top: 10, leading: 90, bottom: 10, trailing: 90))
.background(LinearGradient(gradient: Gradient(colors: [Color(red: 172/255, green: 236/255, blue: 215/255), Color(red: 251/255, green: 238/255, blue: 152/255)]), startPoint: UnitPoint(x: 0, y: 0), endPoint: UnitPoint(x: 1, y: 1)))
.cornerRadius(10)
.clipShape(RoundedRectangle(cornerRadius: 10))
.offset(x: 0, y: -5)
}

// 顯示BTS的人物照片
Image("人物照")
.resizable()
.scaledToFill()
.frame(width: 300, height: 250)
.offset(x: 0, y: 10)

NavigationLink {
// 點擊後跳轉到照片畫面
PictureView()
} label: {
Text("照片")
.font(.system(size: 15))
.bold()
.padding(EdgeInsets(top: 10, leading: 80, bottom: 10, trailing: 80))
.background(LinearGradient(gradient: Gradient(colors: [Color(red: 172/255, green: 236/255, blue: 215/255), Color(red: 251/255, green: 238/255, blue: 152/255)]), startPoint: UnitPoint(x: 0, y: 0), endPoint: UnitPoint(x: 1, y: 1)))
.cornerRadius(10)
.clipShape(RoundedRectangle(cornerRadius: 10))
.offset(x: 0, y: -10)
}

// 顯示BTS的音樂作品封面
Image("音樂作品")
.resizable()
.scaledToFill()
.frame(width: 300, height: 250)
.offset(x: 0, y: 5)

NavigationLink {
// 點擊後跳轉到音樂作品畫面
MusicView()
} label: {
Text("音樂作品")
.font(.system(size: 15))
.bold()
.padding(EdgeInsets(top: 10, leading: 60, bottom: 10, trailing: 60))
.background(LinearGradient(gradient: Gradient(colors: [Color(red: 172/255, green: 236/255, blue: 215/255), Color(red: 251/255, green: 238/255, blue: 152/255)]), startPoint: UnitPoint(x: 0, y: 0), endPoint: UnitPoint(x: 1, y: 1)))
.cornerRadius(10)
.clipShape(RoundedRectangle(cornerRadius: 10))
.offset(x: 0, y: -10)
}
}
.accentColor(.black) // 設定強調顏色
}
}

利用 page 實現分頁瀏覽

struct MainView: View {
@State private var opacity: Double = 0
let bts = [
"封面照",
"封面照1",
"封面照2"
]
var body: some View {
NavigationStack { // 使用NavigationStack來支援導航功能
ScrollView(.vertical, showsIndicators: false) { // 垂直捲動的ScrollView,不顯示捲動指示器
VStack { // 垂直排列的視圖

// 顯示BTS的封面圖片,使用TabView實現輪播效果
TabView {
ForEach(0..<3) { item in
Image(bts[item])
.resizable()
.scaledToFill()
}
}
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
.frame(height: 600)
.overlay(VStack {
Text("關於BTS")
.font(.system(size: 50))
.foregroundColor(.white)
.bold()
Text("About us")
.font(.system(size: 30))
.foregroundColor(.white)
.fontWeight(.light)
})

使用到酷炫動畫

//這個程式碼顯示了一個圖像、一個按鈕以及相關的文字描述,並包含了透明度和旋轉的動畫效果。
import SwiftUI

struct RM: View {
@State private var opacity: Double = 0 // 控制不透明度的狀態變數
@State private var rotateDegree: Double = 0 // 控制旋轉角度的狀態變數

var body: some View {
VStack { // 垂直排列的主視圖
HStack { // 水平排列的次級視圖
Image("RM2") // 顯示圖片 "RM2"
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.offset(x: 10)
.opacity(opacity)
.animation(.easeInOut(duration: 5), value: opacity) // 動畫效果,控制不透明度

.onAppear { // 當視圖出現時執行
opacity = 1 // 使圖片完全可見
}

VStack {
Button("金南俊") { // 顯示名為 "金南俊" 的按鈕,點擊後會旋轉
rotateDegree = 360 // 旋轉角度設為 360 度
}
.font(.system(size: 30)) // 設定字型大小
.bold() // 使用粗體字型
.offset(x: 10, y: 10) // 偏移按鈕位置
.foregroundColor(Color(red: 244/255, green: 150/255, blue: 170/255)) // 設定文字顏色

Text("金南俊(韓語:김남준 Kim Nam Joon,1994年9月12日-),藝名RM(韓語:알엠[3])。韓國男藝人,為韓國男子團體防彈少年團的隊長,在團內擔任主Rapper、製作人,為第一位入選防彈少年團的成員。2013年6月13日,透過單曲專輯《2 COOL 4 SKOOL》出道。2014年6月4日,透過日語單曲《NO MORE DREAM -Japanese Ver.-》在日本出道。他亦自學英語和日語,故被稱為「腦性男」。2017年11月13日,Big Hit娛樂官方公告藝名由「Rap Monster」更改為「RM」")
.font(.system(size: 16)) // 設定文字字型大小
.offset(y: 30) // 偏移文字位置
.padding(EdgeInsets(top: 0, leading: 15, bottom: 0, trailing: 15)) // 添加內邊距
.rotationEffect(.degrees(rotateDegree)) // 使用旋轉效果
.animation(
.linear(duration: 2) // 動畫持續時間為2秒
.repeatCount(1, autoreverses: true), // 重複一次,並自動反轉
value: rotateDegree // 控制旋轉角度的值
)
}
}
}
}
}

資料存在 array 裡,array 成員的型別是 struct 定義的自訂型別,遵從 protocol Identifiable

//這個視圖用於顯示多個專輯,並提供導航到不同專輯的畫面。每個專輯都包括專輯封面圖片和專輯名稱。用户可以点击專輯以查看更多詳細信息。
import SwiftUI

struct MusicView: View {
let image = [ // 專輯封面圖片的名稱陣列
"MAP OF THE SOUL: 7 ~THE JOURNEY~",
"MAP OF THE SOUL:Persona",
"FACE YOURSELF",
"LOVE YOURSELF轉TEAR"
]

var body: some View {
NavigationStack { // 使用 NavigationStack 支援導航功能
List { // 列表視圖
Section("MAP OF THE SOUL") { // 第一個分段,用於顯示 "MAP OF THE SOUL" 相關專輯
ForEach(0..<2) { item in // 迴圈迭代專輯清單
NavigationLink { // 點擊後跳轉到特定專輯的畫面
//跳到的畫面
if item == 0 {
Map() // 顯示 "Map" 畫面
} else {
Soul() // 顯示 "Soul" 畫面
}
} label: { // 點擊元素
Image(image[item]) // 顯示專輯封面圖片
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
.offset(x: 0)
Text(image[item]) // 顯示專輯名稱
.font(.system(size: 25))
.bold()
.offset(x: 0)
}
}
}

Section("YOURSELF") { // 第二個分段,用於顯示 "YOURSELF" 相關專輯
ForEach(2..<4) { item in // 迴圈迭代專輯清單
NavigationLink { // 點擊後跳轉到特定專輯的畫面
//跳到的畫面
if item == 2 {
Face() // 顯示 "Face" 畫面
} else {
Love() // 顯示 "Love" 畫面
}
} label: { // 點擊元素
Image(image[item]) // 顯示專輯封面圖片
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
.offset(x: 0)
Text(image[item]) // 顯示專輯名稱
.font(.system(size: 25))
.bold()
.offset(x: 0)
}
}
}
}
}
.navigationTitle("專輯") // 設定導航標題
}
}

使用 List 製作表格,至少一個頁面的 List 用到 Section 分類表格

List { // 列表視圖
Section("MAP OF THE SOUL") { // 第一個分段,用於顯示 "MAP OF THE SOUL" 相關專輯
ForEach(0..<2) { item in // 迴圈迭代專輯清單
NavigationLink { // 點擊後跳轉到特定專輯的畫面
//跳到的畫面
if item == 0 {
Map() // 顯示 "Map" 畫面
} else {
Soul() // 顯示 "Soul" 畫面
}
} label: { // 點擊元素
Image(image[item]) // 顯示專輯封面圖片
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
.offset(x: 0)
Text(image[item]) // 顯示專輯名稱
.font(.system(size: 25))
.bold()
.offset(x: 0)
}
}
}

Section("YOURSELF") { // 第二個分段,用於顯示 "YOURSELF" 相關專輯
ForEach(2..<4) { item in // 迴圈迭代專輯清單
NavigationLink { // 點擊後跳轉到特定專輯的畫面
//跳到的畫面
if item == 2 {
Face() // 顯示 "Face" 畫面
} else {
Love() // 顯示 "Love" 畫面
}
} label: { // 點擊元素
Image(image[item]) // 顯示專輯封面圖片
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
.offset(x: 0)
Text(image[item]) // 顯示專輯名稱
.font(.system(size: 25))
.bold()
.offset(x: 0)
}
}
}
}
}

打開連結的 Link 按鈕


// 顯示一個外部連結
Link(destination: URL(string: "https://youtu.be/GZjt_sA2eso?si=ML8WfdpjXYhSfHLY")!, label: {
VStack {
Text("Save ME") // 連結文字
.font(.system(size: 20))
.foregroundColor(.blue)
.offset(x: 0, y: 70)
Image("Save ME") // 連結的圖片
.resizable()
.scaledToFit()
.frame(width: 500, height: 500, alignment: .center)
.offset(y: 70)
}
})

使用格子狀排列的 LazyVGrid 實現照片牆

 VStack { // 垂直排列的次級視圖

let columns = Array(repeating: GridItem(), count: 2) // 定義兩列的網格
LazyVGrid(columns: columns, spacing: 100) { // 使用 LazyVGrid 實現垂直排列的網格,設定列與列之間的間距

// 顯示不同的圖片
Image("1")
.resizable()
.scaledToFit()
.frame(width: 1000, height: 300)
Image("2")
.resizable()
.scaledToFit()
.frame(width: 1000, height: 300)
Image("3")
.resizable()
.scaledToFit()
.frame(width: 450, height: 450)
Image("4")
.resizable()
.scaledToFit()
.frame(width: 500, height: 500)
Image("5")
.resizable()
.scaledToFit()
.frame(width: 500, height: 500)
Image("6")
.resizable()
.scaledToFit()
.frame(width: 500, height: 500)
Image("7")
.resizable()
.scaledToFit()
.frame(width: 500, height: 500)
Image("8")
.resizable()
.scaledToFit()
.frame(width: 500, height: 500)
Image("9")
.resizable()
.scaledToFit()
.frame(width: 500, height: 500)
Image("10")
.resizable()
.scaledToFit()
.frame(width: 500, height: 500)
}
}

上下捲動的 List 裡有水平捲動的 ScrollView

ScrollView(.vertical, showsIndicators: false) { // 垂直捲動的ScrollView,不顯示捲動指示器
ScrollView(.horizontal) { // 水平捲動的ScrollView,用於顯示成員介紹
let rows = [GridItem()] // 使用GridItem來配置列
LazyHGrid(rows: rows) { // 使用LazyHGrid實現水平排列的網格
VStack { // 每個成員的資訊
NavigationLink { // 點擊後跳轉到特定成員的介紹畫面
RM()
} label: { // 點擊元素
Image("RM") // 顯示成員的圖片
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.offset(x: 0)
.clipShape(Circle()) // 裁切成圓形
}
Text("金南俊") // 顯示成員名字
.font(.system(size: 25))
.bold()
.offset(x: 0)
}
.offset(x: 20) // 設定偏移位置

// 類似的結構重複顯示每個成員的介紹
// ...

}
}
.fixedSize(horizontal: false, vertical: true) // 設定不限制水平大小,但垂直大小保持固定
.navigationTitle("團員介紹") // 設定導航標題

Text("防彈少年團") // 顯示團體名稱
.font(.system(size: 30))
.bold()
.offset(y: 20)
.foregroundColor(Color(red: 244/255, green: 150/255, blue: 170/255)) // 設定文字顏色

// 顯示團體的簡介
Text("防彈少年團,常稱為BTS,韓國男子音樂團體,由Jin、SUGA、j-hope、RM、Jimin、V、Jung Kook七名成員組成,...")
.font(.system(size: 20))
.foregroundColor(.gray)
.offset(y: 40)
.padding(EdgeInsets(top: 0, leading: 15, bottom: 0, trailing: 15))

加分功能

加入音樂或音效

//當主視圖出現時,它會載入一個名為"Life goes on.mp3"的音樂並播放。選項卡的選取顏色設定為紫色。
import SwiftUI
import AVFoundation

struct ContentView: View {

@State private var player = AVPlayer() // 使用AVPlayer來播放音樂

var body: some View {

Text("")
.onAppear {
// 當視圖出現時(onAppear),載入音樂並開始播放
let url = Bundle.main.url(forResource: "Life goes on", withExtension: "mp3")! // 當視圖出現時(onAppear),載入音樂並開始播放
let playerItem = AVPlayerItem(url: url) // 創建AVPlayerItem以準備播放
player.replaceCurrentItem(with: playerItem) // 使用AVPlayer來播放選定的AVPlayerItem
player.play() // 開始播放音樂

}

SwiftUI 裁切形狀的 clipShape & mask

Image("RM") // 顯示成員的圖片
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.offset(x: 0)
.clipShape(Circle()) // 裁切成圓形

因為檔案過大,無法上傳至 GitHub

--

--