Upgrade to Pro — share decks privately, control downloads, hide ads and more …

利用_SwiftUI_製作電子書_App.pdf

 利用_SwiftUI_製作電子書_App.pdf

使用 SwiftUI 開發電子書 App
認識 SwiftUI 的 UI 元件 & modifier
使用 TabView 製作分頁
使用 NavigationView & NavigationLink 切換頁面 & 傳資料到下一頁
使用 List 製作表格
使用 HStack,VStack,ZStack,Spacer,padding,offset,overlay & position 排版
利用 ScrollView 實現水平捲動
設計照片牆頁面(grid layout)。
支援 dark mode
使用 iOS 13 的 SF Symbol
利用 extract subview 將 view 模組化
支援 iPhone,iPad,Mac
SwiftUI 的 Preview

More Decks by 愛瘋一切為蘋果的彼得潘

Other Decks in Programming

Transcript

  1. SwiftUI 的好處 • Better apps. Less code: 程式愈少,bug 愈少 •

    Declarative(陳述) Syntax : 更容易理解,⽤程式描述畫⾯長 什麼樣⼦ • Preview: 可以預覽程式產⽣的 App 畫⾯,甚⾄將預覽畫⾯ 變成可以操作互動的 App。 • iOS,macOS,watchOS, tvOS App 可以採⽤類似的寫法。 • 利⽤ binding(綁定)機制,資料跟畫⾯更容易同步
  2. SwiftUI 的限制 • iOS 13 以上才能使⽤ • 只能⽤ Swift 撰寫

    SwiftUI 程式,不能⽤ Objective-C • 預覽 SwiftUI 設計的畫⾯要搭配 macOS 10.15 以 上版本
  3. 練習 建立 Single View SwiftUI 專案和 觀看預覽畫⾯ • macOS 10.14

    Mojave 開發 SwiftUI App 的缺點 http://bit.ly/2m1weQ1 • 從 Apple Beta Software Program 網站安裝 10.15 beta https://beta.apple.com/sp/betaprogram/ • 不是 macOS 10.15,請⼿動輸入程式,將 App 安裝到 模擬器看畫⾯
  4. 定義畫⾯的型別 struct ContentView: View { var body: some View {

    Text("Hello World") } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } 預覽呈現的畫⾯,在此我們⽣成型別 ContentView 的元件當 預覽畫⾯ SwiftUI 的 UI 元件以 struct 定義, 遵從 protocol View
  5. SwiftUI 的 UI 元件以 struct 定義, 遵從 protocol View struct

    Button<Label> : View where Label : View struct Text : Equatable extension Text : View struct Image : Equatable extension Image : View • SwiftUI 的 UI 元件都是 View • 類似類別繼承的概念,比⽅ Dog & Cat 繼承 Animal, 所以產⽣的⼩狗和⼩貓物件都是 Animal
  6. protocol View public protocol View { /// The type of

    view representing the body of this view. /// /// When you create a custom view, Swift infers this type from your /// implementation of the required `body` property. associatedtype Body : View /// Declares the content and behavior of this view. var body: Self.Body { get } } 遵從 protocol View 須定義 computed property body 精於算計的 computed property http://bit.ly/2lVJeqh 幫助 protocol 實現型別代號的 Associated Type http://bit.ly/2lDJIS6
  7. 變數 body 的內容決定畫⾯顯⽰ 的東⻄ var body: some View { return

    Text("Hello World") } var body: some View { Text("Hello World") } 只有⼀⾏程式時可以省略 return 沒有省略 return • 變數 body 是 computed property,類似 function,所以有 { } • 讀取變數 body 時,將執⾏ { } 的程式,得到它回傳的東⻄ • 回傳 Text,所以畫⾯顯⽰⽂字 省略 return http://bit.ly/2FuvcTA
  8. var body: some View struct ContentView: View { var body:

    some View { Text("Hello World") } } • 回傳的元件的型別須遵從 protocol View,必須是 某⼀種 view,畫⾯才能顯⽰,比⽅⽂字,圖片等 • some 跟 Swift 的 Opaque Types 有關, https://docs.swift.org/swift-book/LanguageGuide/OpaqueTypes.html
  9. 加入元件,ViewBuilder,Group 畫⾯加入元件的⽅法 http://bit.ly/2lWiy8Y 最後⼀名的 trailing closure http://bit.ly/2lFJOID ViewBuilder & Ambiguous

    reference to member buildBlock() http://bit.ly/2lZTTR1 在結尾輸入 { } ⽣成 SwiftUI 元件 http://bit.ly/2lWVaIn
  10. iOS 13 的 SF Symbols • 超過 1500 個 •

    https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/ • https://developer.apple.com/design/ • 沒有美術天份也能開發美美的 App 了 ! • 可以另外⾃⼰設計 • 圖片可調整樣式和搭配⽂字字型
  11. 參數 systemName 傳入 SF Symbol 的名字 利⽤ Font 設定粗細和⼤⼩,利⽤ foregroundColor

    設定顏⾊ VStack { Image(systemName: "magnifyingglass") .resizable() .scaledToFit() .frame(width: 200, height: 300) Image(systemName: "magnifyingglass") .font(Font.system(size: 100, weight: .heavy)) .foregroundColor(.blue) }
  12. 調整 VStack 的間距 & 對⿑ VStack(alignment: .leading, spacing: 50) {

    Image("peter") Text("我要⼀步⼀步學App,等待朋 友崇拜看著我的臉,⼩⼩的天有⼤⼤的夢想, 重重的課做著輕輕的App。我要⼀步⼀步學 App,在最餓點吃著蘋果寫著Code,⼩⼩的 天流過的淚和汗,總有⼀天我有屬於我的 App。") } VStack(alignment: .trailing, spacing: 50) { Image("peter") Text("我要⼀步⼀步學App,等待朋 友崇拜看著我的臉,⼩⼩的天有⼤⼤的夢想, 重重的課做著輕輕的App。我要⼀步⼀步學 App,在最餓點吃著蘋果寫著Code,⼩⼩的 天流過的淚和汗,總有⼀天我有屬於我的 App。") }
  13. 調整 HStack 的間距 & 對⿑ HStack(alignment: .top, spacing: 50) {

    Image("peter") Text("我要⼀步⼀步學App,等待朋 友崇拜看著我的臉,⼩⼩的天有⼤⼤的夢想, 重重的課做著輕輕的App。我要⼀步⼀步學 App,在最餓點吃著蘋果寫著Code,⼩⼩的 天流過的淚和汗,總有⼀天我有屬於我的 App。") } HStack(alignment: .bottom, spacing: 50) { Image("peter") Text("我要⼀步⼀步學App,等待朋 友崇拜看著我的臉,⼩⼩的天有⼤⼤的夢想, 重重的課做著輕輕的App。我要⼀步⼀步學 App,在最餓點吃著蘋果寫著Code,⼩⼩的 天流過的淚和汗,總有⼀天我有屬於我的 App。") }
  14. 利⽤ Spacer & Padding 調整元件位置 VStack { Image("peter") Text("我要⼀步⼀步學App,等待朋友崇拜 看著我的臉,⼩⼩的天有⼤⼤的夢想,重重的課

    做著輕輕的App。我要⼀步⼀步學App,在最餓 點吃著蘋果寫著Code,⼩⼩的天流過的淚和汗, 總有⼀天我有屬於我的App。") .padding() Spacer() }
  15. IntroView struct IntroView: View { var body: some View {

    Text("2⽉7⽇⽔中瓶,愛瘋⼀切為蘋果。桌球快狠不忘準,⾳樂聲裡 讀推理。 非思不可jobs, 為愛尋夢往前⾶。iPad不是放⼤Phone,兩者皆通彼 得潘。") } } IntroView.swift
  16. 加入 NavigationView navigation bar NavigationView { VStack { Image("peter") Text("我要⼀步⼀步學App,等待朋友

    崇拜看著我的臉,⼩⼩的天有⼤⼤的夢想,重重的 課做著輕輕的App。我要⼀步⼀步學App,在最餓 點吃著蘋果寫著Code,⼩⼩的天流過的淚和汗,總 有⼀天我有屬於我的App。") .padding() Spacer() } } ContentView.swift
  17. 利⽤ navigationBarTitle 顯⽰標題 NavigationView { VStack { Image("peter") Text("我要⼀步⼀步學App,等待朋友崇 拜看著我的臉,⼩⼩的天有⼤⼤的夢想,重重的課

    做著輕輕的App。我要⼀步⼀步學App,在最餓點吃 著蘋果寫著Code,⼩⼩的天流過的淚和汗,總有⼀ 天我有屬於我的App。") .padding() Spacer() } .navigationBarTitle("情歌王") } ContentView.swift 透過在 NavigationView { } 裡的元件呼叫 navigationBarTitle,比⽅從以上程式的 VStack 呼叫
  18. IntroView 顯⽰標題 struct IntroView: View { var body: some View

    { Text("2⽉7⽇⽔中瓶,愛瘋⼀切為蘋果。桌球快狠不忘準,⾳樂聲裡 讀推理。 非思不可jobs, 為愛尋夢往前⾶。iPad不是放⼤Phone,兩者皆通彼 得潘。") .navigationBarTitle("⾃我介紹") } } NavigationView 在之前畫⾯,preview 不知道, 因此 preview 看不到 navigation bar & 標題 IntroView.swift
  19. 加入 NavigationLink 點選圖片切換⾴⾯ NavigationView { VStack { NavigationLink(destination: IntroView()) {

    Image("peter") .renderingMode(.original) } Text("我要⼀步⼀步學App,等待朋友崇拜看著我的臉,⼩⼩的 天有⼤⼤的夢想,重重的課做著輕輕的App。我要⼀步⼀步學App,在最餓 點吃著蘋果寫著Code,⼩⼩的天流過的淚和汗,總有⼀天我有屬於我的 App。") .padding() Spacer() } .navigationBarTitle("情歌王") } ContentView.swift
  20. Live Preview & Preview on Device http://bit.ly/2m1KhoV • 點選圖片切換⾴⾯可從 preview

    測試 • 修改程式 Live Preview 會⾃動更新,不⽤重新啟動 App。
  21. 新增顯⽰情歌列表的 SongList struct SongList: View { var body: some View

    { Text("Hello World!") } } SwiftUI 的表格稱為 List SongList.swift
  22. 控制 iOS App 的第⼀個畫⾯ http://bit.ly/2YWmU2J func scene(_ scene: UIScene, willConnectTo

    session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { let contentView = SongList() if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) window.rootViewController = UIHostingController(rootView: contentView) self.window = window window.makeKeyAndVisible() } } SceneDelegate.swift 改成 SongList
  23. 新增顯⽰每⼀⾸歌的 SongRow 新增 SongRow.swift struct SongRow: View { var body:

    some View { HStack { Image("對的時間點") .resizable() .scaledToFill() .frame(width: 80, height: 80) .clipped() VStack(alignment: .leading) { Text("對的時間點") Text("林俊傑") } } } } SongRow.swift
  24. 利⽤ Spacer 讓 HStack 的寬度變 成螢幕寬度 struct SongRow: View {

    var body: some View { HStack { Image("對的時間點") .resizable() .scaledToFill() .frame(width: 80, height: 80) .clipped() VStack(alignment: .leading) { Text("對的時間點") Text("林俊傑") } Spacer() } } } SongRow.swift
  25. 利⽤ previewLayout 調整 preview 的尺⼨ struct SongRow_Previews: PreviewProvider { static

    var previews: some View { SongRow() .previewLayout(.fixed(width: 300, height: 70)) } } SongRow.swift
  26. 利⽤ List 顯⽰表格 struct SongList: View { var body: some

    View { List { SongRow() SongRow() SongRow() SongRow() SongRow() SongRow() SongRow() SongRow() SongRow() SongRow() } } } SongList.swift
  27. SongRow 顯⽰ Song 的內容 struct SongRow: View { var song:

    Song var body: some View { HStack { Image(song.name) .resizable() .scaledToFill() .frame(width: 80, height: 80) .clipped() VStack(alignment: .leading) { Text(song.name) Text(song.singer) } Spacer() } } } SongRow.swift 儲存顯⽰的歌曲
  28. 預覽畫⾯建立 SongRow 時傳入 Song struct SongRow_Previews: PreviewProvider { static var

    previews: some View { SongRow(song: Song(name: "對的時間點", singer: "林俊傑")) .previewLayout(.fixed(width: 300, height: 70)) } } SongRow.swift
  29. 更新 SongList 的歌單 struct SongList: View { var body: some

    View { List { SongRow(song: Song(name: "對的時間 點", singer: "林俊傑")) SongRow(song: Song(name: "說好不 哭", singer: "周杰倫")) } } } 如果有 100 ⾸歌 ? SongList.swift
  30. 定義儲存照片資料的 array songs 新增 Data.swift let songs = [Song(name: "對的時間點",

    singer: "林俊傑"), Song(name: "說好不哭", singer: "周杰倫")] ⽅便測試的全域變數 Data.swift
  31. ⽤ List 搭配 range 顯⽰表格 • data: 數字範圍,控制 row 的數量

    • rowContent: 傳入 closure(以 { } 包含的程式), 回傳表格每⼀⾏顯⽰的 view
  32. ⽤ List 搭配 range 顯⽰表格 按 enter 表格將有 3 個

    row,呼叫參數 rowContent 的 { } 程式 得到每個 row 的 view,型別 Int 的參數將告知要 回傳第幾個 row,因為 0..<3,所以參數將為 0, 1, 2
  33. ⽤ List 搭配 range 顯⽰表格 struct SongList: View { var

    body: some View { List(0..<3) { (index) in Text("第\(index)個") } } } SongList.swift index = 0 時,產⽣ Text("第0個") Index = 1 時,產⽣ Text("第1個")
  34. ⽤ List 搭配 array 顯⽰表格 struct SongList: View { var

    body: some View { List(0..<songs.count) { (index) in SongRow(song: songs[index]) } } } SongList.swift index = 0 時,產⽣ SongRow(song: songs[0]) Index = 1 時,產⽣ SongRow(song: songs[1])
  35. 增加 property lyrics struct Song { var name: String var

    singer: String var lyrics: String } Song.swift
  36. 更新 Songs let songs = [Song(name: "對的時間點", singer: "林俊傑", lyrics:

    "如果 愛情是場 遠程的渦旋 僅管 繞著圈⼦ 也要走向前 不離⼼太遠 我要⾯朝最 藍的晴天 不脫離軌道有你在⾝邊"), Song(name: "說好不哭", singer: "周 杰倫", lyrics: "沒有了聯絡 後來的⽣活 我都是聽別⼈說 說妳怎麼了 說妳怎 麼過 放不下的⼈是我 ⼈多的時候 就待在⾓落 就怕別⼈問起我 你們怎麼了 妳低 著頭 護著我連抱怨都沒有")] Data.swift
  37. 新增歌曲明細⾴ 新增 SongDetail.swift struct SongDetail: View { var song: Song

    var body: some View { VStack { Image(song.name) .resizable() .scaledToFill() .frame(minWidth: 0, maxWidth: .infinity, maxHeight: 300) .clipped() Text(song.lyrics) .padding() } } } struct SongDetail_Previews: PreviewProvider { static var previews: some View { SongDetail(song: songs[0]) } } Image 的寬度等於螢幕寬度 SongDetail.swift 儲存歌曲資訊
  38. 將 SongList 包在 NavigationView 裡 & 顯⽰標題 struct SongList: View

    { var body: some View { NavigationView { List(0..<songs.count) { (index) in SongRow(song: songs[index]) } .navigationBarTitle("情歌王") } } } SongList.swift
  39. 加入 NavigationLink 切換⾴⾯ & 傳資料到下⼀⾴ NavigationView { List(0..<songs.count) { (index)

    in NavigationLink(destination: SongDetail(song: songs[index])) { SongRow(song: songs[index]) } } .navigationBarTitle("情歌王") } SongList.swift 將點選的歌曲傳到下⼀⾴的 SongDetail
  40. 歌曲明細⾴顯⽰標題 struct SongDetail: View { var song: Song var body:

    some View { VStack { Image(song.name) .resizable() .scaledToFill() .frame(height: 300) .clipped() Text(song.lyrics) .padding() } .navigationBarTitle(song.name) } } SongDetail.swift
  41. ⽤ TabView 實現兩個 tab ⾴⾯ 新增 AppView.swift 利⽤ tabItem(_:) 設定

    tab 的圖⽂ tabItem(_:) 只能顯⽰傳統 tab 的樣式,比⽅圖片在上,⽂字在下 struct AppView: View { var body: some View { TabView { SongList() .tabItem { Image(systemName: "music.house.fill") Text("情歌") } ContentView() .tabItem { Image(systemName: "info.circle.fill") Text("About") } } } } AppView.swift
  42. 將第⼀個畫⾯變成 AppView SceneDelegate.swift func scene(_ scene: UIScene, willConnectTo session: UISceneSession,

    options connectionOptions: UIScene.ConnectionOptions) { let contentView = AppView()
  43. ⽔平捲動的 Scroll View ScrollView(.horizontal) { HStack(spacing: 20) { Image("peter0") .resizable()

    .scaledToFill() .frame(width: 200) .clipped() Image("peter1") .resizable() .scaledToFill() .frame(width: 200) .clipped() Image("peter2") .resizable() .scaledToFill() .frame(width: 200) .clipped() } .frame(height: 200) } 圖片寬⾼ 200 * 200 ScrollView 裡包 HStack
  44. ForEach: 利⽤集合裡的東⻄⽣成⼀ 個個 view,然後再合併成⼀個 view ScrollView(.horizontal) { HStack(spacing: 20) {

    ForEach(0..<3) { (index) in Image("peter\(index)") .resizable() .scaledToFill() .frame(width: 200) .clipped() } } .frame(height: 200) } 先介紹 ForEach 搭配 range 的寫法 http://bit.ly/2kH7lsK
  45. 天⽣⽀援 dark mode 的 SwiftUI Color http://bit.ly/2meuJhy 利⽤ Environment Overrides

    快速切換 App 的 light mode & dark mode http://bit.ly/2lYt1kA
  46. extract subview 將某個 subview 變成獨立的型別 按住 cmd 鍵點選 Image,然後點選 Extract

    Subview ps: 如果沒有出現 Extract Subview 的選項,請⾃⼰⼿動建立 好處: 增加程式可讀性,可重覆使⽤ SongDetail.swift SongDetail
  47. 在 SongImage 加上 property song struct SongImage: View { var

    song: Song var body: some View { Image(song.name) .resizable() .scaledToFill() .frame(height: 300) .clipped() } } SongDetail.swift
  48. 建立 SongImage 時傳入 song struct SongDetail: View { var song:

    Song var body: some View { VStack { SongImage(song: song) Text(song.lyrics) .padding() Spacer() } .navigationBarTitle(song.name) } } SongDetail.swift
  49. 結合 List & ScrollView NavigationView { List { ScrollView(.horizontal) {

    HStack(spacing: 20) { ForEach(0..<3) { (index) in Image("peter\(index)") .resizable() .scaledToFill() .frame(width: 200) .clipped() } } .frame(height: 200) } ForEach(0..<songs.count) { (index) in NavigationLink(destination: SongDetail(song: songs[index])) { SongRow(song: songs[index]) } } } .navigationBarTitle("情歌王") } • List { } 裡包 ScrollView & ForEach 呈現的 SongRow • 點選 ScrollView 裡的元件到下⼀⾴ ? ScrollView 裡的 Image 加上 NavigationLink
  50. struct ContentView: View { let names = [["Federer", "Nadal"], ["Djokovic",

    "Murray"]] var columnCount = 2 let photoWidth = (UIScreen.main.bounds.size.width - 10) / 2 var body: some View { List { ForEach(0..<names.count) { (row) in HStack(spacing:10) { ForEach(0..<self.names[row].count) { (column) in Image(self.names[row][column]) .resizable() .scaledToFill() .frame(width: self.photoWidth, height: self.photoWidth) .clipped() } } } .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 10, trailing: 0)) } .onAppear { UITableView.appearance().separatorColor = .clear } } } 照片牆⾴⾯(grid layout) array 裡包 array,外層 array 決定 row 的數量,內層 array 決定每個 row 的照片數量 以 HStack 裝 row 的照片
  51. 圓形邊框陰影照片 struct SongRow: View { var song: Song var body:

    some View { HStack { Image(song.name) .resizable() .scaledToFill() .frame(width: 80, height: 80) .clipShape(Circle()) .overlay(Circle().stroke(Color.white, lineWidth: 4)) .shadow(radius: 10) VStack(alignment: .leading) { Text(song.name) Text(song.singer) } Spacer() } } }
  52. 客製樣式的 list row List(0..<songs.count) { (index) in VStack { Image(songs[index].name)

    .resizable() .scaledToFill() .frame(minWidth: 0, maxWidth: .infinity, maxHeight: 200) .clipped() .padding() Text(songs[index].name) .padding(.bottom, 10) } .background(Color.yellow) .cornerRadius(20) .shadow(radius: 5) } .onAppear { UITableView.appearance().separatorColor = .clear } 黃⾊圓⾓陰影的⽂青卡片
  53. ⽤ List 搭配 array & Identifiable 顯⽰表格 struct Song: Identifiable

    { var id = UUID() var name: String var singer: String var lyrics: String } List(songs) { (song) in SongRow(song: song) }
  54. 分類表格的 Section 比⽅晴天情歌跟雨天情歌 let sunnyDaySongs = [Song(name: "晴天", singer: "何維健",

    lyrics: "要記得晴天勝得過下雨天 你的笑要被我看⾒ 就這樣笑著笑著 煩惱 不⾒ 你就是我的太陽 別在乎別⼈怎麼想")] let rainyDaySongs = [Song(name: "下雨天", singer: "南拳媽媽", lyrics: "下雨天了怎麼辦 我好想你 我不敢打給你 我找不到原因"), Song(name: "⼜下雨了", singer: "李⼼潔", lyrics: "⼜下雨了你最喜 歡的天氣 收⾳機正播放著傷⼼的歌曲 你在這座城的那裏看雨 是⼀個⼈還是已 經有⼈陪著你")] Data.swift
  55. 分類表格的 Section 在 Section 的 { } 裡以 ForEach 搭配

    array 設定內容 NavigationView { List { Section(header: Text("Sunny")) { ForEach(0..<sunnyDaySongs.count) { (index) in NavigationLink(destination: SongDetail(song: sunnyDaySongs[index])) { SongRow(song: sunnyDaySongs[index]) } } } Section(header: Text("Rainy")) { ForEach(0..<rainyDaySongs.count) { (index) in NavigationLink(destination: SongDetail(song: rainyDaySongs[index])) { SongRow(song: rainyDaySongs[index]) } } } }.navigationBarTitle("情歌王") }
  56. List ⾃動為 row 加入靠左的 HStack 中英⽂時 Leading 為左邊 NavigationView {

    List(0..<songs.count) { index in NavigationLink(destination: SongDetail(song: songs[index])) { Image(songs[index].name) .resizable() .scaledToFill() .frame(width: 80, height: 80) .clipped() VStack(alignment: .leading) { Text(songs[index].name) Text(songs[index].singer) } } } } 可以省略 HStack & Spacer
  57. struct SongList: View { var body: some View { NavigationView

    { List(0..<songs.count) { index in SongRow(song: songs[index]) } } } } struct SongRow: View { var song: Song var body: some View { NavigationLink(destination: SongDetail(song: song)) { Image(song.name) .resizable() .scaledToFill() .frame(width: 80, height: 80) .clipped() VStack(alignment: .leading) { Text(song.name) Text(song.singer) } } } }
  58. 更多有趣的 SwiftUI 應⽤ Maybe 之後的新⼿練功坊 使⽤ SwiftUI 的 UI 元件

    & data binding 創作有趣的 App http://bit.ly/2oI1apV 利⽤ SwiftUI 的 Path 繪圖 http://bit.ly/2khdk7g