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

SwiftUI 跨平台 App 開發初體驗 - 開發 iOS, macOS, watchOS ...

SwiftUI 跨平台 App 開發初體驗 - 開發 iOS, macOS, watchOS 的 MOPCON App

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

Other Decks in Programming

Transcript

  1. 彼得潘簡介 超級喜歡 Apple,因此從事 Apple 相關的各種⼯作。開發 了⼀點點 iOS App,寫了⼀點點 Swift 程式的書和⽂章,

    開了⼀點點 iOS App 開發的課,認識了很多同樣熱愛開發 iOS App 的可愛學⽣。 ps: 還有幫偶像 Penny 製作 App 彼得潘網站
  2. ⼤綱 • SwiftUI 介紹 & 入⾨ • 開發 MOPCON SwiftUI

    App 的過程和各種問題 • MOPCON API 串接,連網跟讀取檔案測試 • 開發 iOS App( iPhone & iPad) • 開發 macOS App • 開發 watchOS App 我們要開發 4 種平台的 App,Go !
  3. 製作 iOS App 畫⾯的新技術 - SwiftUI • 2019 推出的新技術 (UI:

    User Interface) • 同樣的功能,可以寫更少的程式實現。 • 程式愈少,問題(bug)也會愈少。 • 程式更容易理解,可讀性更好。 • Preview: 可以預覽 App 畫⾯ & 執⾏ App,加快 App 開發的速度 • Dynamic replacement: 修改程式後,不⽤重新 compile,在 Live Preview 可操作新版程式⽣成的 App • 資料跟畫⾯有⽅便的同步⽅法,減少問題發⽣的機率。 • iOS,iPadOS,macOS,watchOS,tvOS App 可以採⽤類似的寫法。 • https://developer.apple.com/xcode/swiftui/
  4. The best way to build an app is with Swift

    and SwiftUI WWDC22 Platforms State of the Union SwiftUI is the preferred app-builder technology Planning your iOS app
  5. The shortest path to building great apps on every device

    WWDC19 SwiftUI On All Devices 從前要學四種不同的技術才能開發不同平台的 App 現在學會 SwiftUI 就能製作不同平台的 App
  6. Learn once, apply anywhere • 學會 SwiftUI,就能應⽤在不同的平台 • share code:

    部分的程式可在不同平台使⽤,不⽤修改 
 • 並不是寫⼀份 code 就能產⽣每個平台的 App 
 • 每個平台的尺⼨差很多,也有各⾃的強項跟功能,還是要針對不同平台加入 
 ⼀些程式
  7. ⽀援多個平台的 UI 元件 WWDC19 SwiftUI On All Devices Picker Toggle

    同⼀份程式,⽣成的元件在不同的平台會以平台上標準的樣式呈現
  8. ⽤ SwiftUI 開發跨平台的 MOPCON App • 串接 MOPCON API 抓取議程

    & 講者資料 • 模仿 2022 的 MOPCON iOS App,製作議程 & 講者⾴⾯ • 開發 iOS,macOS,watchOS 版,⽀援 iPhone, iPad, Mac, Apple Watch • 使⽤ Xcode 14 開發,搭配 iOS 16 SDK,使⽤第 4 代的 SwiftUI API
  9. 如果⼀開始建立的是 iOS App 專案 可以另外新增 Mac Destination Designed for iPad

    可以在 Apple silicon 的 Mac 執⾏,畫⾯會是 iPad App 的樣⼦ 製作原⽣的 Mac App,可使⽤ macOS SDK 各種 Mac App 的差異 https://developer.apple.com/documentation/xcode/con f iguring-a-multiplatform-app-target
  10. SwiftUI UI 元件的型別以 struct 定義,遵從 protocol View Button,Text & Image

    遵從 View,所以 Button,Text 和 Image 都是 View,具有 View 的功能和特性。
  11. some View • body 裡回傳的東⻄必須是某⼀種 view,須遵從 protocol View, 比⽅⽂字,圖片等 •

    some 跟 Swift 的 Opaque Types 有關 
 https://docs.swift.org/swift-book/LanguageGuide/ OpaqueTypes.html
  12. modi f ier 是 function,呼叫 function 後得到新的 UI 元件,有著新的樣式跟功能 客製

    UI 元件樣式 & 功能的 SwiftUI modi f ier http://bit.ly/2IJ7cyd struct ContentView: View { var body: some View { Text("The best way to build an app is with Swift and SwiftUI") .font(.largeTitle) .foregroundColor(.orange) .padding() .background(.blue) .cornerRadius(10.0) .rotationEffect(.degrees(10)) .shadow(radius: 20) .padding() } } func cornerRadius(_ radius: CGFloat, antialiased: Bool = true) -> some View 得到新的 view,加了圓⾓效果
  13. ViewBuilder: 從 closure 建立 view • @ViewBuilder 參數可以寫的程式 
 https://cutt.ly/SlYHZy4

    
 • ViewBuilder 
 https://developer.apple.com/documentation/swiftui/viewbuilder VStack 的 init
  14. 利⽤ Spacer,padding,offset 調整元件位置 • Spacer: 長出看不⾒的空間 • padding: 加入間距 •

    offset: 從原本的位置偏移 • http://bit.ly/2mSS6xv struct ContentView: View { var body: some View { HStack { Image("peter") .resizable() .scaledToFill() .frame(width: 100, height: 100) Spacer() Text("與 🍎 相遇好幸運") .font(.largeTitle) .offset(x: 10, y: 40) } .padding() .background(.yellow) } }
  15. 在 asset 加入 App Icon • 設定 iOS & macOS

    的 App Icon • Xcode 14 的 iOS App icon 只要設 定 1024 * 1024 
 https://cutt.ly/MBYfU0V
  16. 在 asset 加入開頭畫⾯圖片 & App 使⽤的顏⾊ • AccentColor: App 的主⾊

    • background: 背景顏⾊ • light blue: 淡藍⾊ logo
  17. 從 Info 的 Launch Screen 設定開頭畫⾯ 設定 Image Name &

    Background color https://cutt.ly/7V7Ek9U 輸入圖片和顏⾊的名字
  18. App 寫法參考的 Apple 範例 • Develop in Swift Data Collections

    (Xcode 13) 
 使⽤ generic,protocol & associatedtype 定義 APIRequest & APIService (網路相關程式) 
 https://cutt.ly/3VA3I3R • Earthquake App (網路相關程式) 
 https://cutt.ly/DVA38TW • Food Truck: Building a SwiftUI multiplatform app 
 https://cutt.ly/rVA8kDl • Landmarks App 
 https://cutt.ly/dVA8dtS • Fruta: Building a Feature-Rich App with SwiftUI 
 https://cutt.ly/UV7VW5x • Bringing robust navigation structure to your SwiftUI app (iOS 16 的 NavigationStack & NavigationSplitView) 
 https://cutt.ly/GV7BoVm
  19. 定義時間的 extension DateIntervalFormatter: 取得時間區間的字串 extension DateIntervalFormatter { static let hourMinFormatter

    = { let formatter = DateIntervalFormatter() formatter.dateTemplate = "HH:mm" return formatter }() } DateIntervalFormatter.hourMinFormatter.string(from: .now, to: .now.advanced(by: 600)) 13:57 – 14:07
  20. 把 JSON 放在 development assets ⽅便開發時不連網路測試,從 preview 測試 App 時也不⽤連網路

    從 Safari 抓取 JSON 檔 • 加到 Preview Content 下,可以加 到 Preview Assets.xcassets,或是 放在 Preview Content 下 
 • 只有開發時使⽤,檔案不會包在上 架的 App 裡 
 • https://cutt.ly/uVA56TX 將 JSON 檔加入專案裡
  21. MOPCON JSON 對應的 Decodable 型別 session & speaker 清單 API

    回傳的 JSON 第⼀層都是 success, message, data
  22. MOPCON API 串接的相關檔案 定義 3 個 API 的 request,遵從 protocol

    APIRequest 此寫法的相關說明可參考 Apple 的 Develop in Swift Data Collections 
 https://cutt.ly/3VA3I3R protocol APIRequest 宣告網路的相關功能
  23. HTTPDataDownloader & TestDownloader 呼叫 httpData 抓資料 連到網路抓資料 判斷 url 的

    lastPathComponent,然 後讀取 App 裡的 JSON 檔 比⽅ https://mopcon.org/api/2022/ session 會讀取 session.json
  24. 顯⽰⼤頭照的 SpeakerImage • 使⽤抓取網路圖片的 Nuke 套件 
 https://github.com/kean/Nuke 
 •

    使⽤ SPM 安裝第三⽅套件 
 https://cutt.ly/yBxABnn 
 • LazyImage 來⾃ NukeUI
  25. 指定 preview 尺⼨的 previewLayout 顯⽰剛好⼤⼩的 sizeThatFits https://cutt.ly/MV6OGzh • 點選箭頭切換到 Selectable

    模式,在 Selectable 模式才能讓 preview 變成 previewLayout 指定的⼤⼩。 • 在 Selectable 模式不會連到網路抓資料
  26. 提供假資料,⽅便 preview 顯⽰資料 Period, Session, Speaker 都有 提供給 preview 的假資料

    讀取 JSON 檔,解碼成 Decodable 型別 傳入 Speaker.preview preview 顯⽰時不⽤連到網路抓資料
  27. function load 讀取 JSON 檔,解碼成 Decodable 型別 讀取 main bundle

    下的檔案 Apple Landmark 範例 https://cutt.ly/IBYmQiq
  28. 4 個⾴⾯共同的樣式 ⼀樣的背景⾊ ⼀樣⾼度的 navigation bar ⼀樣的 tab bar &

    navigation bar 顏⾊ 同樣的 modi f ier 要寫很多次,有精簡的⽅法嗎 ?
  29. ⾃訂 modi f ier,遵從 protocol ViewModi f ier,定義 body •

    background(Color.background) 
 深藍⾊的背景 • toolbarBackground(.visible, for: .tabBar, .navigationBar) 
 顯⽰ navigation bar & tab bar 的背景⾊ • toolbarBackground(Color.background, for: .tabBar, .navigationBar) 
 將 navigation bar & tab bar 設為深藍⾊ 
 • navigationBarTitleDisplayMode(.inline) navigation bar ⾼度正常,不要放⼤ 套⽤到每個⾴⾯的 CommonStyleModi f ier
  30. tab bar 沒設定顯⽰背景⾊時,可能會在 tab bar 的背後顯⽰⾴⾯的內容 • TabView 
 顯⽰分⾴,類似

    tab bar controller • tabItem 
 設定 tab 的 icon & ⽂字 
 • List 
 顯⽰表格 • ForEach 將集合裡的資料變成⼀個個 view 
 https://cutt.ly/9BsAQpy
  31. dark mode & preferredColorScheme(.dark) • MOPCON App 的時間電量是⽩⾊,⽽且許多⽂字也以 ⽩⾊為主,因此很適合 dark

    mode 
 • preferredColorScheme(.dark) 可讓整個 view 變 dark mode 
 • ⽅便 preview 畫⾯套⽤的 custom modi fi er • preview 呼叫 preferredColorScheme(.dark) 讓 preview 顯⽰的畫⾯變成 dark mode 
 • 之後會再說明如何讓整個 App 變 dark mode
  32. The compiler is unable to type-check this expression in reasonable

    time; try breaking up the expression into distinct sub-expressions 錯誤的原因是因為 ViewBuilder 參數傳入的 closure 程式太複雜,解法是將某些程式另外 
 包成 function 或 computed property https://cutt.ly/sBczRMo 為什麼剛剛要另外定義 function calculateRange ? 要是你敢不定義,可怕的事會發⽣呀
  33. 抓取 speakers 資料的 SpeakersProvider • SwiftUI view 可觀察 ObservableObject,ObservableObject 的

    published property 改變時會通知觀察它的 view 更新畫⾯ • 呼叫 send 抓資料,抓到資料後存在 published property speakers,觸發 觀察它的 view 更新畫⾯
  34. @MainActor 讓類別 SpeakerProvider 的程式在 main thread 執⾏,確保在 main thread 更新畫⾯

    Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates. 如果沒有加 @MainActor,會出現在背景 thread 發布資料改變的錯誤
  35. 畫⾯出現時抓資料 • task 
 畫⾯出現時執⾏非同步的 await 程式抓資料 • if provider.speakers.isEmpty

    
 沒有資料時才抓資料,確保資料只抓⼀次 
 • SpeakersProvider 抓到資料,存入 speakers, 觸發 SpeakersView 更新畫⾯
  36. NavigationStack,NavigationLink & List • NavigationStack 
 類似 UIKit 的 navigation

    controller,控制⾴⾯ 的切換,上⽅顯⽰ navigation bar 
 • List 
 SwiftUI 的表格 
 • ⽤ ForEach 讀取講者的 array,產⽣每個講者 的 row 
 https://cutt.ly/sBYOg3d • NavigationLink 
 切換⾴⾯的按鈕,點選跳到 SpeakerDetailView 
 https://cutt.ly/uBsSeRD
  37. 如果沒有呼叫 listRowBackground 設定 row 的顏⾊ row 的顏⾊在 light mode 為⽩⾊,dark

    mode 為⿊⾊ 如果忘了寫,沒有設定 row 的顏⾊ light mode dark mode
  38. 顯⽰講者詳細資訊的 SpeakerDetailView • 在 preview 也能看到標題 
 包在 NavigationStack 裡

    
 • Link 
 點選打開連結的按鈕,比⽅打開 Safari 顯⽰網⾴。此功能要從模 擬器測試,無法從 preview 測試 
 https://cutt.ly/KBm5KIN 
 • speaker.link
  39. ⽤狀態設計 SwiftUI 畫⾯: State property struct ContentView: View { @State

    private var number = 1 var body: some View { Image(systemName: "die.face.\(number).fill") .resizable() .scaledToFit() .frame(width: 200, height: 200) .onTapGesture { number = Int.random(in: 1...6) } } } State property 改變時會觸發畫⾯更新,執⾏ body { } 裡的程式 
 範例: 骰⼦ App 
 https://cutt.ly/DBEMI9C
  40. 綁定資料的 Binding 元件 struct ContentView: View { @State private var

    isRain = true var body: some View { VStack { Image(systemName: isRain ? "cloud.rain.fill" : "sun.max.fill") .resizable() .frame(width: 100, height: 100) Toggle(isOn: $isRain) { Text("今天下雨嗎?") } } .padding() } } @State private var isRain = true binding • 操作元件時將同步修改資料,資料內容改變時,它也會同步更新元件。 
 https://cutt.ly/SBEMFV9 • State property 加 $ 可產⽣ Binding 型別的資料
  41. 選擇⽇期的 PeriodPicker @Binding var periodData: PeriodData? binding • 選擇項⽬的 Picker

    是⼀種綁定資料的元件 
 https://cutt.ly/SBs8bwM • Picker 綁定的資料必須是 Hashable • Picker 綁定變數 periodData,Picker 選擇的⽇期改 變時,periodData 會跟著改變,periodData 將儲存 當天的相關議程資訊 • tag($0 as PeriodData?) 是為了綁定 optional • 以 @Binding 宣告 periodData 的原因 參考資料來源的 Binding 
 https://cutt.ly/SBEMVkG
  42. SessionsView • 切換⾴⾯的 NavigationSplitView, NavigationSplitView 在 iPad 會變成分割畫⾯ 
 (iOS

    16 ⽤ NavigationSplitView,舊版⽤ NavigationView) 
 https://cutt.ly/uBsSeRD • @State private var periodData: PeriodData? 
 選擇的⽇期 & 相關議程 
 • @State private var session: Session? 
 選擇的議程 
 • ZStack & frame(minWidth: 0, maxWidth: .in fi nity, minHeight: 0, maxHeight: .in fi nity) 
 全螢幕的背景顏⾊ 
 • sessionContentView 顯⽰議程清單 • 點選議程後顯⽰ detail closure 裡的 SessionDetailView
  43. 顯⽰議程清單的 sessionsContentView • 從 PeriodPicker 選⽇期,periodData 將儲存當天的相關議程資訊 
 • 判斷

    event 名稱顯⽰ SessionRow 或 PeriodRow 
 • List 搭配的參數 selection 傳入 $session,點選 SessionRow 後, State property session 會儲存選到的 議程,觸發 NavigationSplitView 的 detail 畫⾯顯⽰ 
 • Session 必須遵從 Identi fi able & Hashable,因為 List 參數 selection 的型別條件為 Hashable,ForEach 的 參數條件為 Identi fi able
  44. App 顯⽰的第⼀個畫⾯ • 遵從 protocol App,在 WindowGroup { } 裡⽣成第⼀個畫⾯

    • 呼叫 preferredColorScheme(.dark),讓整個 App 採⽤ dark mode
  45. iPad List 選取的邊框問題 ? 問題: 選取的 row 背後會變成 accent color,但因為設定

    listRowBackground, 因此只會看到框框 沒設定 listRowBackground 時 整個 row 在選取時會變成 accent color
  46. 依據 row 是否選取顯⽰不同的背景顏⾊ • 點選 row 時 State property session

    會儲存選到的 session,因此比對 self.session & 參數 session 可判斷是否選取。 • 將 listRowInsets 設為 0,否則 row 的左右會有間距 listRowInsets 調為 0 沒有調整 listRowInsets @State private var session: Session?
  47. macOS App 的網路連線問題 抓不到資料 • networkd_settings_read_from_ f ile Sandbox is

    preventing this process from reading networkd settings f ile at "/Library/Preferences/com.apple.networkd.plist", please add an exception • macOS App 很重視安全,所以預設不讓我們接觸網路,畢竟網路上藏著許多可怕的東⻄呀
  48. • listRowBackground(Color("background")) 選擇 row 時 row 依然維持 listRowBackground 設定的背景⾊。只在 macOS

    呼叫, 避免剛剛 iPad 選取 row 的邊框問題 • scrollContentBackground(.hidden) 移除 List 背後的顏⾊
  49. 設定 iOS, macOS & watchOS App 共⽤的檔案 在 Target Membership

    勾選 Watch App • 選擇 swift,asset,json • 按 cmd 鍵⼀次選多個檔案,然後再勾選 Watch App
  50. 修正 var sessionsContentView 的 List watchOS App 下另外新增 SessionsView,不使⽤ iOS

    App 的 SessionsView watchOS 的 List 不⽀援搭配參數 selection
  51. Command CompileAssetCatalog failed with a nonzero exit code : error:

    There are multiple stickers icon set or app icon set instances named "AppIcon". /* com.apple.actool.compilation-results */ /Users/peterpan/Library/Developer/Xcode/DerivedData/MOPCONSwiftUIMultiplatform-cfaczzmiqqawtodqwqecwaayybgp/Build/Intermediates.noindex/ MOPCONSwiftUIMultiplatform.build/Debug-watchsimulator/WatchMOPCON Watch App.build/assetcatalog_generated_info.plist /Users/peterpan/Library/Developer/Xcode/DerivedData/MOPCONSwiftUIMultiplatform-cfaczzmiqqawtodqwqecwaayybgp/Build/Products/Debug-watchsimulator/ WatchMOPCON Watch App.app/Assets.car
  52. 設定 watchOS 的 AppIcon 從 iOS App 的 asset 設定

    watchOS AppIcon 以 iOS App asset 的 App Icon 當 watchOS App 的 App Icon
  53. ⼼得 • 寫 SwiftUI 很開⼼,很好玩 • 學習到許多 SwiftUI 製作畫⾯的細節 •

    Learn once, apply anywhere • 不同平台還是要做⼀些⼩⼩的調整 • 有些 API 和功能只⽀援某個平台 • 有些寫法在某個平台正常,但在另⼀個平台會出問題 • 可能要依據不同裝置的尺⼨調整畫⾯ • SwiftUI 要特別注意版本問題 
 MOPCON App 使⽤到⼀些 iOS 16 才⽀援的 API,⽽且同樣的 API 在不同版本的 iOS 可能會 有不同的⾏為和 bug • 未來可以增加的功能: 收藏,下拉更新,search