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

第一次_SwiftUI_10YearChallenge_App_親密接觸.pdf

 第一次_SwiftUI_10YearChallenge_App_親密接觸.pdf

如果有十年的程式練習,我就能開發比 IG 還棒的 App。十年的練習有可能嗎? 可以 ! 就從 iPlayground 的 3 個小時,開始第一次的 SwiftUI App 親密接觸。

Apple 最新推出的 SwiftUI 幫助我們以更直覺精簡的程式製作 App (Better apps. Less code),活動將搭配滿滿的實作練習,介紹 SwiftUI 的各種技術。比方畫面的製作,常用 UI 元件的使用,各種有趣的圖片效果,data binding ,結合表格,Form,navigation bar & tab bar 製作多頁面 App,帶著大家一步步創作有趣的 10YearChallenge App。

Transcript

  1. 第⼀次 SwiftUI 10YearChallenge App 親密接觸 https://medium.com/@z1235678 玲嘉 Sharon https://medium.com/@sharon830316 保鈴

    https://medium.com/@duc50609 Allen https://medium.com/@allenchen08040614 奇妙仙⼦ https://medium.com/@le821227 第⼀次 SwiftUI 10YearChallenge App 親密接觸 https://medium.com/@apppeterpan Peter Pan & 可愛的 SwiftUI App 助教
  2. 彼得潘簡介 App程式設計入⾨:iPhone.iPad 彼得潘的 Swift 程式設計入⾨ 正職: 作家 副業: 專欄作家,⼯程師,講師,顧問,家教,App評審, App接案,企業包班,創業家,iOS

    APP ⾦牌擺渡⼈ iOS App⼯程師/外包廠商的⾯試鑑賞師 http://apppeterpan.strikingly.com
  3. 叫我彼得潘,Peter,Peter Parker Deeplove,⿁塚,Swift⼩王⼦,情歌王⼦ http://bit.ly/2XvKMcw

  4. 彼得潘的 SwiftUI 學習⽂章 http://bit.ly/2lHDosw

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

    Declarative Syntax : 更容易理解,⽤程式描述畫⾯長什麼樣⼦ • Preview: 可以預覽程式產⽣的 App 畫⾯,甚⾄將預覽畫⾯變成可以操作互動的 App。 • 以程式⽣成畫⾯,不像 storyboard 容易有多⼈合作的版本衝突問題。 • iOS,macOS,watchOS, tvOS App 可以採⽤類似的寫法。 • 不會像從前 storyboard & 程式分開,常有不⼀致的問題,比⽅ outlet / action 問 題,cell 名字問題 • 利⽤ binding 機制,資料跟畫⾯更容易同步 • 可以跟 UIKit 結合
  6. SwiftUI 的限制 • iOS 13 以上才能使⽤ • 只能⽤ Swift 撰寫

    SwiftUI 程式,不能⽤ Objective-C • 預覽 SwiftUI 設計的畫⾯要搭配 macOS 10.15 以 上版本,但 macOS 10.14 還是可以開發
  7. Xcode 11 專案的 SwiftUI & Storyboard 選擇 http://bit.ly/30HlBBs

  8. 預覽(preview) SwiftUI 設計的畫⾯ http://bit.ly/2PnvRNW

  9. Xcode preview 顯⽰的 iPhone 機型 http://bit.ly/2xdYLEo

  10. 練習 建立 Single View SwiftUI 專案和 觀看預覽畫⾯ ps:不是 macOS 10.15,請⼿動輸入程式,

    將 App 安裝到模擬器看畫⾯ macOS 10.14 Mojave 開發 SwiftUI App 的缺點 http://bit.ly/2m1weQ1
  11. 定義畫⾯的型別 struct ContentView: View { var body: some View {

    Text("Hello World") } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } 定義預覽畫⾯的型別,遵從 protocol PreviewProvider, 在此我們⽣成型別 ContentView 的元件當預覽畫⾯ SwiftUI 的 UI 元件以 struct 定義, 遵從 protocol View
  12. 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
  13. 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
  14. 變數 body 的內容決定畫⾯顯⽰ 的東⻄ var body: some View { return

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

    return Text("Hello World") }
  16. 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 某⼀種 view
  17. 字串不是 view

  18. SwiftUI 設計 UI 元件樣式的 各種⽅法 http://bit.ly/2lPOgoh

  19. 練習 設定⽂字顏⾊ ps:不是 macOS 10.15,請⼿動輸入程式, 將 App 安裝到模擬器看畫⾯

  20. 客製 UI 元件樣式的 SwiftUI modifier http://bit.ly/2IJ7cyd 利⽤ Re-Indent 縮排 SwiftUI

    程式 http://bit.ly/2lPVRn5
  21. 練習 實驗各種 modifier ps:不是 macOS 10.15,請⼿動輸入程式, 將 App 安裝到模擬器看畫⾯

  22. 加入元件,ViewBuilder, Group,ForEach & KeyPath 畫⾯加入元件的⽅法 http://bit.ly/2lWiy8Y ViewBuilder & Ambiguous reference

    to member buildBlock() http://bit.ly/2lZTTR1 SwiftUI 的 ForEach (待會再說明) http://bit.ly/2kH7lsK 在結尾輸入 { } ⽣成 SwiftUI 元件 http://bit.ly/2lWVaIn
  23. 練習 加入元件 ps:不是 macOS 10.15,請⼿動輸入程式, 將 App 安裝到模擬器看畫⾯

  24. VStack VStack { Image("peter") Text("我要⼀步⼀步學App,等待朋友崇拜看著我的臉,⼩⼩的天有⼤⼤的夢想,重重的課做著輕輕的 App。我要⼀步⼀步學App,在最餓點吃著蘋果寫著Code,⼩⼩的天流過的淚和汗,總有⼀天我有屬於我的App。") }

  25. HStack HStack { Image("peter") Text("我要⼀步⼀步學App,等待朋友崇拜看著我的臉,⼩⼩的天有⼤⼤的夢想,重重的課做著輕輕的 App。我要⼀步⼀步學App,在最餓點吃著蘋果寫著Code,⼩⼩的天流過的淚和汗,總有⼀天我有屬於我的App。") }

  26. ZStack ZStack { Image("peter") Text("我要⼀步⼀步學App,等待朋友崇拜看著我的臉,⼩⼩的天有⼤⼤的夢想,重重的課做著輕輕的 App。我要⼀步⼀步學App,在最餓點吃著蘋果寫著Code,⼩⼩的天流過的淚和汗,總有⼀天我有屬於我的App。") } 可以將元件⼀層層疊上, 類似疊疊樂,聯想 addSubview

  27. 顯⽰美麗圖片的 SwiftUI image http://bit.ly/2jWn4DM

  28. iOS 13 的 SF Symbols • 超過 1500 個 •

    https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/ • https://developer.apple.com/design/ • https://sfsymbols.com • 沒有美術天份也能開發美美的 App 了 ! • 可以另外⾃⼰設計 • 圖片可調整樣式和搭配⽂字字型
  29. Mac 的 SF Symbols App Image(systemName: "magnifyingglass")

  30. ⽤狀態設計 SwiftUI 畫⾯ 認識 @State property & binding http://bit.ly/2lD8amE Live

    Preview,Preview on Device http://bit.ly/2m1KhoV
  31. 天⽣⽀援 dark mode 的 SwiftUI Color http://bit.ly/2meuJhy 搭配 Environment Overrides

    light mode & dark mode http://bit.ly/2LfNdcb
  32. 10YearChallenge App

  33. 下載參考範例 每個階段的練習都有範例, 來不及做完時,可下載前個 階段的範例繼續練習 最後 10 分鐘講解投影片來 不及操作練習的部分 https://www.dropbox.com/s/yddh6kas5q0tbez/TenYearApp.zip?dl=0

  34. 將圖片加到 Assets.xcassets 節省時間,先做三年就好

  35. 新增 SwiftUI 畫⾯ http://bit.ly/2lG62du 比⽅新增 EditPhotoView

  36. 控制 iOS App 的第⼀個畫⾯ http://bit.ly/2YWmU2J func scene(_ scene: UIScene, willConnectTo

    session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { let contentView = EditPhotoView() if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) window.rootViewController = UIHostingController(rootView: contentView) self.window = window window.makeKeyAndVisible() } } SceneDelegate.swift
  37. iOS 13 iPad 的 Multiple Windows https://apple.co/2THmkjv

  38. SceneDelegate & UISceneDelegate • iOS 13 開始,App 執⾏後可有多個 scene,每個 scene

    可以有多個 window • scene 有 delegate,型別是 UISceneDelegate • scene 進入前景 / 背景時將觸發 delegate 的相關 method • 當 App scene (window) 即將產⽣時將呼叫 scene delegate 的 scene(_:willConnectTo:options:) https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle
  39. 練習 加入三張圖片,建立 EditPhotoView & 設為第⼀個畫⾯

  40. 加入圖片

  41. 加入 GeometryReader & Image struct EditPhotoView: View { var body:

    some View { GeometryReader { geometry in Image("peter2019") .resizable() .scaledToFill() .frame(width: geometry.size.width, height: geometry.size.width / 4 * 3) .clipped() } } } 讓圖片寬度等於螢幕寬度,比例 4:3 SwiftUI 讓圖片等於螢幕寬度 & 設定比例的⽅法 http://bit.ly/2m0RjKw
  42. 練習 加入 GeometryReader & Image 資料夾 1_addPhoto

  43. ⽤ Slider 調整圖片亮度

  44. 滑動選值的 Slider http://bit.ly/2lAbcbh

  45. 利⽤ brightness 調整圖片亮度 http://bit.ly/2lZsfDC

  46. 加入 VStack 容納圖片和表單 Embed in VStack

  47. 加入 VStack 容納圖片和表單 GeometryReader { geometry in VStack { Image("peter2019")

    .resizable() .scaledToFill() .frame(width: geometry.size.width, height: geometry.size.width / 4 * 3) .clipped() } }
  48. 加入代表圖片亮度的 state property brightnessAmount struct EditPhotoView: View { @State private

    var brightnessAmount: Double = 0 搭配 Slider 調整亮度
  49. 利⽤ brightness 調整圖片亮度 GeometryReader { geometry in VStack { Image("peter2019")

    .resizable() .scaledToFill() .frame(width: geometry.size.width, height: geometry.size.width / 4 * 3) .clipped() .brightness(self.brightnessAmount) } }
  50. 因為 @escaping, 因此 closure 裡讀取 property 要加 self GeometryReader 的

    init init(@ViewBuilder content: @escaping (GeometryProxy) -> Content) http://bit.ly/2KfttUX
  51. 加入 Form(表單) & Slider VStack { Image("peter2019") .resizable() .scaledToFill() .frame(width:

    geometry.size.width, height: geometry.size.width / 4 * 3) .clipped() .brightness(self.brightnessAmount) Form { HStack { Text("亮度") Slider(value: self.$brightnessAmount, in: 0...1, minimumValueLabel: Image(systemName: "sun.max.fill").imageScale(.small), maximumValueLabel: Image(systemName: "sun.max.fill").imageScale(.large)) { Text("") } } } }
  52. Declarative(陳述) Syntax 更容易理解,⽤程式描述畫⾯長什麼樣⼦ VStack { Image("peter2019") .resizable() .scaledToFill() .frame(width: geometry.size.width,

    height: geometry.size.width / 4 * 3) .clipped() .brightness(self.brightnessAmount) Form { HStack { Text("亮度") Slider(value: self.$brightnessAmount, in: 0...1, minimumValueLabel: Image(systemName: "sun.max.fill").imageScale(.small), maximumValueLabel: Image(systemName: "sun.max.fill").imageScale(.large)) { Text("") } } } }
  53. 練習 ⽤ Slider 調整圖片亮度 資料夾 2_addSlider

  54. ⽤ DatePicker 選⽇期 點⽣⽇展開 DatePicker

  55. 選時間的歲⽉神偷 DatePicker http://bit.ly/2m02pPS

  56. 加入時間的相關 property struct EditPhotoView: View { @State private var brightnessAmount:

    Double = 0 @State private var selectDate = Date() let today = Date() let startDate = Calendar.current.date(byAdding: .year, value: -2, to: Date())! var year: Int { return Calendar.current.component(.year, from: selectDate) } selectDate: 選擇的⽇期 today: 今天⽇期 startDate: 開始⽇期 year: 選擇⽇期的年份
  57. 在 Form 加入 DatePicker

  58. 顯⽰對應年份的圖片 Image("peter\(self.year)") .resizable() .scaledToFill() .frame(width: geometry.size.width, height: geometry.size.width / 4

    * 3) .clipped() .brightness(self.brightnessAmount)
  59. 練習 ⽤ DatePicker 選⽇期 資料夾 3_addDatePicker

  60. 其它效果 • 圓⾓ • 邊框 • 透明度 • 陰影 https://github.com/fzhlee/SwiftUI-Guide/blob/master/README_English.md

  61. 利⽤ List 呈現⼗年照片列表

  62. 定義代表照片資料的型別 PhotoInfo struct PhotoInfo { var year: Int var name:

    String } 新增 PhotoInfo.swift
  63. 定義儲存照片資料的 array photoInfos 新增 Data.swift let photoInfos = [PhotoInfo(year: 2017,

    name: "純真"), PhotoInfo(year: 2018, name: "可愛"), PhotoInfo(year: 2019, name: "帥氣")] ⽅便測試的全域變數
  64. 新增表格的 row view 儲存要顯⽰的照片資料 新增 PhotoRow.swift

  65. 新增表格的 row view struct PhotoRow: View { var photoInfo: PhotoInfo

    var body: some View { Text("Hello World!") } } struct PhotoRow_Previews: PreviewProvider { static var previews: some View { PhotoRow(photoInfo: photoInfos[0]) } } 顯⽰第⼀個照片資料
  66. 設計 PhotoRow 的畫⾯

  67. 設計 PhotoRow 的畫⾯ var body: some View { HStack {

    Image("peter\(photoInfo.year)") .resizable() .scaledToFill() .frame(width: 50, height: 50) .clipShape(Circle()) .overlay(Circle().stroke(Color.white, lineWidth: 4)) .shadow(radius: 10) Text("\(photoInfo.year)") } } 讓東⻄變圓形的各種⽅法 http://bit.ly/2korkMz
  68. 利⽤ Spacer 讓 HStack 的寬度變 成螢幕寬度

  69. 利⽤ Spacer & Padding 調整元件位置 http://bit.ly/2kp9pFy

  70. 利⽤ Spacer 讓 HStack 的寬度變 成螢幕寬度 HStack { Image("peter\(photoInfo.year)") .resizable()

    .scaledToFill() .frame(width: 50, height: 50) .clipShape(Circle()) .overlay(Circle().stroke(Color.white, lineWidth: 4)) .shadow(radius: 10) Text("\(photoInfo.year)") Spacer() }
  71. 利⽤ previewLayout 調整 preview 的尺⼨ struct PhotoRow_Previews: PreviewProvider { static

    var previews: some View { PhotoRow(photoInfo: photoInfos[0]) .previewLayout(.fixed(width: 300, height: 70)) } }
  72. 練習 新增 PhotoRow 的畫⾯ 如果想在模擬器測試, 可修改 SceneDelegate.swift, 將 contentView 改成

    PhotoRow let contentView = PhotoRow(photoInfo: photoInfos[0]) 資料夾 4_PhotoRow
  73. 新增照片列表畫⾯

  74. ⽤ List 顯⽰表格 先介紹 ForEach http://bit.ly/2kH7lsK

  75. List(表格) 的 init init<Data, RowContent>(_ data: Data, @ViewBuilder rowContent: @escaping

    (Data.Element) -> RowContent) where Content == ForEach<Data, Data.Element.ID, HStack<RowContent>>, Data : RandomAccessCollection, RowContent : View, Data.Element : Identifiable init<Data, ID, RowContent>(_ data: Data, id: KeyPath<Data.Element, ID>, @ViewBuilder rowContent: @escaping (Data.Element) -> RowContent) where Content == ForEach<Data, ID, HStack<RowContent>>, Data : RandomAccessCollection, ID : Hashable, RowContent : View 類似 ForEach,data 參數可傳入 array,然後讓成員型別 遵從 Identifiable 或⽤ KeyPath 指定 id 區分成員
  76. 讓 PhotoInfo 遵從 protocol Identifiable struct PhotoInfo: Identifiable { var

    id = UUID() var year: Int var name: String } • 讓 PhotoInfo 遵從 protocol Identifiable • 遵從 protocol Identifiable 必須定義 property id 產⽣獨⼀無⼆的 ID http://bit.ly/2lZcJYl
  77. 新增照片列表畫⾯ struct PhotoList: View { var body: some View {

    List(photoInfos) { (photoInfo) in PhotoRow(photoInfo: photoInfo) } } } 新增 PhotoList.swift
  78. let contentView = PhotoList() 練習 如果想在模擬器測試, 可修改 SceneDelegate.swift, 將 contentView

    改成 PhotoList 資料夾 5_PhotoList
  79. 新增照片明細⾴

  80. 新增照片明細⾴ struct PhotoDetail: View { var photoInfo: PhotoInfo var body:

    some View { Image("peter\(photoInfo.year)") .resizable() .scaledToFit() } } struct PhotoDetail_Previews: PreviewProvider { static var previews: some View { PhotoDetail(photoInfo: photoInfos[0]) } } 新增 PhotoDetail.swift
  81. 利⽤ NavigationView & NavigationLink 切換⾴⾯

  82. 加入 NavigationView

  83. HStack { List(photoInfos) { (photoInfo) in PhotoRow(photoInfo: photoInfo) } }

    加入 NavigationView PhotoList.swift 按住 cmd 鍵點選 List,從選單選擇 Embed in HStack 1
  84. 加入 NavigationView var body: some View { NavigationView { List(photoInfos)

    { (photoInfo) in PhotoRow(photoInfo: photoInfo) } } } 將 HStack 改成 NavigationView 2
  85. 加入 NavigationLink destination: 前往的⾴⾯ label: 顯⽰的內容 init(destination: Destination, @ViewBuilder label:

    () -> Label) https://developer.apple.com/documentation/swiftui/navigationlink
  86. 加入 NavigationLink NavigationView { List(photoInfos) { (photoInfo) in NavigationLink(destination: PhotoDetail(photoInfo:

    photoInfo)) { PhotoRow(photoInfo: photoInfo) } } }
  87. 照片明細⾴顯⽰標題 struct PhotoDetail: View { var photoInfo: PhotoInfo var body:

    some View { Image("peter\(photoInfo.year)") .resizable() .scaledToFit() .navigationBarTitle(photoInfo.name) } }
  88. 練習 新增照片明細⾴ 資料夾 6_PhotoDetail

  89. ⽤ TabView 實現兩個 tab ⾴⾯

  90. ⽤ TabView 實現兩個 tab ⾴⾯ struct TenYearTab: View { var

    body: some View { TabView { EditPhotoView().tabItem { VStack { Image(systemName: "photo") Text("Edit") } } PhotoList().tabItem { VStack { Image(systemName: "photo.on.rectangle") Text("Photos") } } } } } 新增 TenYearTab.swift 利⽤ tabItem(_:) 設定 tab 的圖⽂ tabItem(_:) 只能顯⽰傳統 tab 的樣式,比⽅圖片在上,⽂字在下
  91. tabItem(_:) 可省略 VStack TabView { EditPhotoView().tabItem { Image(systemName: "photo") Text("Edit")

    } PhotoList().tabItem { Image(systemName: "photo.on.rectangle") Text("Photos") } } func tabItem<V>(@ViewBuilder _ label: () -> V) -> some View where V : View 當 tabItem { } 裡⽣成 image & text 時, 將⾃動以圖片在上,⽂字在下的⽅式呈現
  92. 將第⼀個畫⾯變成 TenYearTab SceneDelegate.swift

  93. 練習 ⽤ TabView 實現兩個 tab ⾴⾯ 資料夾 7_TabView

  94. 利⽤ extract subview 將 view 模組化 & @Binding 綁定變數

  95. extract subview 將某個 subview 變成獨立的型別 按住 cmd 鍵點選 HStack,然後點選 Extract

    Subview 好處: 增加程式可讀性,可重覆使⽤
  96. 輸入 view 的型別名 Form { BrightnessSlider() DatePicker("時間", selection: self. $selectDate,

    in: self.startDate...self.today, displayedComponents: .date) } 輸入 BrightnessSlider
  97. 錯了 ! 找不到 brightnessAmount

  98. 在 BrightnessSlider 加上 @State property brightnessAmount 可以嗎 ? struct BrightnessSlider:

    View { @State var brightnessAmount: Double var body: some View { HStack { Text("亮度") Slider(value: self.$brightnessAmount, in: 0...1, minimumValueLabel: Image(systemName: "sun.max.fill").imageScale(.small), maximumValueLabel: Image(systemName: "sun.max.fill").imageScale(.large)) { Text("") } } } }
  99. 建立 BrightnessSlider 時傳入 brightnessAmount Form { BrightnessSlider(brightnessAmount: self.brightnessAmount) DatePicker("時間", selection:

    self.$selectDate, in: self.startDate...self.today, displayedComponents: .date) } EditPhotoView.swift
  100. 問題 ? 滑動 slider 時, BrightnessSlider 改變它的 brightnessAmount,但是 EditPhotoView 的

    brightnessAmount 沒有更新
  101. 利⽤ @Binding 綁定 EditPhotoView 的 brightnessAmount @Binding 將讓 BrightnessSlider 的

    brightnessAmount 參考 EditPhotoView 的 brightnessAmount,修改時更新 EditPhotoView 的 brightnessAmount struct BrightnessSlider: View { @Binding var brightnessAmount: Double var body: some View { HStack { Text("亮度") Slider(value: self.$brightnessAmount, in: 0...1, minimumValueLabel: Image(systemName: "sun.max.fill").imageScale(.small), maximumValueLabel: Image(systemName: "sun.max.fill").imageScale(.large)) { Text("") } } } }
  102. ⽣成 BrightnessSlider 時傳入 $brightnessAmount Form { BrightnessSlider(brightnessAmount: self. $brightnessAmount) DatePicker("時間",

    selection: self.$selectDate, in: self.startDate...self.today, displayedComponents: .date) }
  103. source of truth 單⼀資訊來源 @State var brightnessAmount: Double @Binding var

    brightnessAmount1: Double @Binding var brightnessAmount2: Double ⽤ @State 宣告的 property 將是資料來源, 其它以 @Binding 宣告的 property 都是參考它, 存取同⼀份資料,避免資料不同步的問題 例⼦:
  104. 其它東⻄也可以 extract subview,比⽅上⽅的 Image struct TenYearImage: View { let width:

    CGFloat let selectDate: Date let brightnessAmount: Double var year: Int { return Calendar.current.component(.year, from: selectDate) } var body: some View { Image("peter\(self.year)") .resizable() .scaledToFill() .frame(width: width, height: width / 4 * 3) .clipped() .brightness(self.brightnessAmount) } } 不需要加 @Binding,因為不會有修改更新的問題
  105. 其它東⻄也可以 extract subview,比⽅上⽅的 Image struct EditPhotoView: View { @State private

    var brightnessAmount: Double = 0 @State private var selectDate = Date() let today = Date() let startDate = Calendar.current.date(byAdding: .year, value: -2, to: Date())! var body: some View { GeometryReader { geometry in VStack { TenYearImage(width: geometry.size.width, selectDate: self.selectDate, brightnessAmount: self.brightnessAmount) Form { BrightnessSlider(brightnessAmount: self.$brightnessAmount) DatePicker("時間", selection: self.$selectDate, in: self.startDate...self.today, displayedComponents: .date) } } } } }
  106. 練習 extract subview & @Binding 資料夾 8_extract subview

  107. ⽤ Picker 混合圖片

  108. 選擇項⽬的 Picker & 我要選李⽩ http://bit.ly/2knmI9K

  109. 利⽤ blendMode 混合圖片 http://bit.ly/2lIWNcw

  110. 將圖片加到 Assets.xcassets 取名 texture

  111. 加入 blend 的相關 property struct EditPhotoView: View { @State private

    var brightnessAmount: Double = 0 @State private var selectDate = Date() @State private var selectBlend = BlendMode.screen let today = Date() let startDate = Calendar.current.date(byAdding: .year, value: -2, to: Date())! let blendModes: [BlendMode] = [.screen, .colorDodge, .colorBurn] Picker 利⽤ binding 綁定 selectBlend,BlendMode 是 enum,所以遵從 protocol Hashable,可以被 Picker 綁定
  112. ⽤ ZStack 包含 2 個要混合的 Image VStack { ZStack {

    Image("texture") .resizable() .scaledToFill() .frame(width: geometry.size.width, height: geometry.size.width / 4 * 3) .clipped() TenYearImage(width: geometry.size.width, selectDate: self.selectDate, brightnessAmount: self.brightnessAmount) .blendMode(self.selectBlend) }
  113. 將 modifier 作⽤於 ZStack ZStack { Image("texture") .resizable() TenYearImage(selectDate: self.selectDate,

    brightnessAmount: self.brightnessAmount) .blendMode(self.selectBlend) } .scaledToFill() .frame(width: geometry.size.width, height: geometry.size.width / 4 * 3) .clipped()
  114. 將 modifier 作⽤於 ZStack struct TenYearImage: View { let selectDate:

    Date let brightnessAmount: Double var year: Int { return Calendar.current.component(.year, from: selectDate) } var body: some View { Image("peter\(self.year)") .resizable() .brightness(self.brightnessAmount) } }
  115. ⽤ ZStack 包含 2 個要混合的 Image

  116. 在 Form 裡加入 Picker 選擇 blend • ForEach 要求當 id

    的資料型別遵從 protocol Hashable, enum 定義的型別預設即遵從 Hashable protocol。(ps: 不過搭配 associated values 的 enum 是例外) • ViewBuilder 裡有些寫法會有問題。 http://bit.ly/2Y4isyc
  117. Picker 選項顯⽰的⽂字 extension BlendMode { var name: String { return

    "\(self)" } }
  118. 在 Form 裡加入 Picker 選擇 blend Picker("選擇 blend", selection: self.$selectBlend)

    { ForEach(self.blendModes, id: \.self) { (blendMode) in Text(blendMode.name) } }
  119. 問題: blend 不能點選

  120. 解法1: 搭配 WheelPickerStyle

  121. 解法1: 搭配 WheelPickerStyle Picker("選擇 blend", selection: self.$selectBlend) { ForEach(self.blendModes, id:

    \.self) { (blendMode) in Text(blendMode.name) } } .pickerStyle(WheelPickerStyle())
  122. 解法2: 搭配 SegmentedPickerStyle

  123. 解法2: 搭配 SegmentedPickerStyle Picker("選擇 blend", selection: self.$selectBlend) { ForEach(self.blendModes, id:

    \.self) { (blendMode) in Text(blendMode.name) } } .pickerStyle(SegmentedPickerStyle())
  124. 解法3: 搭配 Navigation View 在下⼀⾴設定

  125. 1. 從 VStack 選擇 Embed in HStack 2. 將 HStack

    改成 NavigationView 解法3: 搭配 Navigation View 在下⼀⾴設定 EditPhotoView.swift GeometryReader { geometry in NavigationView { VStack {
  126. 練習 ⽤ Picker 混合圖片 資料夾 9_Picker

  127. 利⽤ onTapGesture 縮放圖片 雙擊照片放⼤ / 縮⼩

  128. 利⽤ onTapGesture 縮放圖片 struct PhotoDetail: View { var photoInfo: PhotoInfo

    @State private var scale: CGFloat = 1 var body: some View { Image("peter\(photoInfo.year)") .resizable() .scaledToFit() .navigationBarTitle(photoInfo.name) .scaleEffect(scale) .onTapGesture(count: 2) { self.scale = self.scale == 1 ? 2 : 1 } } } scaleEffect(_:anchor:): 將 view 放⼤ / 縮⼩ onTapGesture(count:perform:): 設定點擊⼿勢觸發的程式 加 @State 才可以改變
  129. 設定圖片縮放的動畫 Image("peter\(photoInfo.year)") .resizable() .scaledToFit() .navigationBarTitle(photoInfo.name) .scaleEffect(scale) .onTapGesture(count: 2) { self.scale

    = self.scale == 1 ? 2 : 1 } .animation(.spring())
  130. 練習 雙擊照片放⼤ / 縮⼩ 資料夾 10_TapGesture

  131. One more thing

  132. 現在正是學習 SwiftUI 的 對的時間點 資料夾 11_music 預覽就會播⾳樂 ( 關掉預覽畫⾯可讓⾳樂停⽌) JJ

    YouTube 對的時間點網址會過期,記得更新 http://bit.ly/2ksvW4j
  133. Always Online 有問題歡迎 LINE 我 https://www.youtube.com/watch?v=ZPg9yv5LR3s 只要我沒有在約會就會回你

  134. 補充

  135. SwiftUI 的按鈕 button http://bit.ly/2k4j1pa

  136. 利⽤ Debug Preview 讓 print 發揮作⽤ http://bit.ly/2lwO6SJ

  137. preview 多個 device struct EditPhotoView_Previews: PreviewProvider { static var previews:

    some View { Group { EditPhotoView() .previewDevice(PreviewDevice(rawValue: "iPhone 11")) .previewDisplayName("iPhone 11") EditPhotoView() .previewDevice(PreviewDevice(rawValue: "iPhone SE")) .previewDisplayName("iPhone SE") } } }
  138. SwiftUI 猜數字 App http://bit.ly/2k4PMm1