Lessons learned with Swift and iOS development

Ellie Huxtable

Swift 與 iOS 開發的經驗心得

我想先說明,我完全不是專家。雖然我寫程式已經很久了,但對於 Swift 和 iOS 的世界來說還是新手,而且從來沒有開發過行動應用程式 :) 這是我在打造 Pillion 第一個 iOS 版本的過程中所摸索與學到的一切!

Pillion 是一款協助尋找機車停車位的應用程式——目前在全球收錄了約 10,000 個停車地點,有些來自 OpenStreetMaps,有些來自線上資料集,其餘則由使用者提供。除了停車資訊外,它也提供騎乘軌跡記錄功能。

其中很多都是我在還在摸索階段時發現的,所以在不少情況下,我的做法並不是「正確」的做法。在我已經學到更好方法的地方,我會把兩種做法都列出來。

如果這篇文章反應不錯,未來我可能會針對每個部分(尤其是 MapBox)寫更詳細的文章。

如果你發現任何錯誤,請告訴我 😊

SwiftUI

我一開始就直接採用 SwiftUI,這是 Apple 隨 iOS 13 推出的全新 UI 系統。這裡有個限制是,只支援執行 iOS >= 13 的裝置——不過這涵蓋了 iPhone 6S 以後的所有機型。

由於我已經使用 React 多年,SwiftUI 用起來感覺非常不錯。雖然兩者有所不同,但在元件/子元件/狀態等方面的設計方式很自然。我其實更喜歡 SwiftUI 的狀態綁定方式勝過 React 處理狀態的方式,也覺得程式碼看起來乾淨許多。

我沒試過 UIKit,所以不太能比較,但就我對 SwiftUI 的觀察來看,它乾淨許多。

我在使用 SwiftUI 時遇到一些問題,似乎大多與它還不夠成熟有關——我在下面會提到。

比較惱人的是,我在 GitHub 上找到的大多數漂亮的 UI 元件都只支援 UIKit。或許之後我會試著將它們包裝起來,但 SwiftUI 的生態系目前看起來有點稀疏——正好反映出它還不夠成熟。這點需要留意。

除錯

XCode 除錯器是個令人驚喜的亮點!雖然我已經習慣用 Python 的 ipdb 進行除錯,但沒想到在程式碼於手機上執行且已編譯的情況下,還能有這麼靈活的工具。

我發現除錯器可以輕鬆地檢查數值、執行 Swift 運算式,完全符合我的需求!Tab 自動補齊也運作得非常順暢。工具真棒 :)

偶爾會有點慢,但我想這是可以預期的。

UI 執行緒相關

我的應用程式很大一部分基本上就是向我的 API 發出請求,然後渲染結果。我發現如果嘗試從背景執行緒(例如 HTTP 回應的非同步處理常式)更新 UI,就會產生錯誤。應該改用這種寫法:

DispatchQueue.main.async {
    // do ui
}

這讓避免阻塞 UI 執行緒變得非常容易 😁

這其實是我在剛開始使用 Swift 時很早就讀到的東西。後來我學到,在可行的情況下,使用帶有 Published 屬性的 ObservableObjects 來擷取與更新資料才是更推薦的做法!不過以防萬一,我還是把它留在這裡。我對目前專案中處理 HTTP 請求的方式還是不太滿意,所以未來可能會再重新檢視這部分。

隱藏鍵盤

這只是個小細節,但我希望能在某些互動時隱藏鍵盤。例如,當搜尋列處於聚焦狀態而你點擊清單中的某個項目時,就隱藏鍵盤。

extension UIApplication {
    func hideKeyboard() {
        sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}
UIApplication.shared.hideKeyboard()

Just

在 Swift 中發送 HTTP 請求不算太糟,但我已經習慣 Python 的 requests——所以我想找類似的東西。

Just 似乎正符合需求!它小巧、快速又好用,讓你可以寫出像這樣的程式碼:

Just.get("https://example.com") { r in
	print(r.text)
}

……這非常方便!

它還能讓你建立一個預先設定好標頭的 session,這對於需要授權的 HTTP 請求很有用。

Mapbox

我在這個專案的大部分時間都在使用 MapBox Maps SDK for iOS。Mapbox 在 JavaScript 上用起來很愉快,但老實說 iOS 版本就有些不足。客製化某些功能時會有點麻煩,而且文件也不算好。我常常發現有多種做法可以達成同一件事,卻幾乎沒有說明原因。不過 Android 版本在這方面似乎好一些!

我不是說它很差,只是相較於 JS 版本,感覺有點未完成或不夠成熟——就我看到的 Android 版本來說也是如此。等我完成 Android 應用程式後,會再做個比較。

還有一點是,有些文件比較偏向 UIKit——這也很合理,畢竟 SwiftUI 還非常新!這點要留意。不過也有一些文件會教你如何為 SwiftUI 包裝地圖。

Swift 型別檢查

從 Rust 轉過來,Swift 的型別檢查器感覺有點……不夠力。有時候它完全無法推斷出看似顯而易見的型別,或是花太久時間,然後要求開發者把程式碼拆解一下。這不算是大問題,但有點煩人。據說這部分一直在持續改進中!

在某些情況下,它會在某個位置標示錯誤,但那裡其實沒有問題。把那段程式碼刪掉後,才會顯示真正的底層錯誤。我想這比較跟 SwiftUI DSL 有關。當 DSL 中出現型別錯誤時,如果它無法推斷型別,整個就會垮掉。我現在找不到好的範例,但找到時會再更新這裡。

更新到 Catalina 和最新版 XCode 後,我遇到了很多以前從未出現過的「ambiguous member」錯誤。Swift 似乎傾向於給出誤導性的錯誤訊息,這……有點奇怪。我從未用過其他會這樣的現代語言,尤其這還是一個由如此大的公司支援的語言。

我發現自己後來逐漸更會寫出 Swift 能夠理解的程式碼,即使我之前寫的就語意上來說也是正確的。

SwiftFormat

我習慣在其他專案中使用格式化工具,所以在這裡也找了一個。SwiftFormat 用起來似乎不錯。

SwiftUI DSL 問題

所以在 SwiftUI 中,View 的 body 是用一種類似 DSL 的東西來宣告的

var body: some View {
	ComponentOne {
    	AnotherOne {
        	// so much nesting :O
        }
    }
}

……這樣其實沒什麼問題。

然而,當你想做條件渲染時,這個 DSL 似乎還不太到位。

if thing.ouch {
   RenderMe(thing.ouch.thing)
}

然而,像是 if let 這樣的語法,在撰寫本文時是無法運作的

if let o = thing.ouch {
	RenderMe(o.thing)
}

雖然不是必要的,但有會更好!

這個 DSL 還有一些其他小問題需要解決,但 SwiftUI 還非常新/處於測試階段,所以我並不意外。除了這些問題之外,用起來還是很愉快的。正常運作時,體驗非常好!只是不正常時就…… ;)

Auth0

我目前使用 Auth0 lock,並搭配 Google 與 Apple OAuth 登入。雖然我現在已經改用自己的解決方案,但因為仍具參考價值,所以還是保留在這裡。

Apple 的登入其實非常酷!使用者可以選擇使用 Apple 轉寄地址來隱藏自己的電子郵件,並使用 Face ID 登入。

Lock 並未針對 SwiftUI 進行設定,但要包裝起來相當容易:

struct Auth0Lock: UIViewControllerRepresentable {
    typealias UIViewControllerType = LockViewController
    var lock: Lock = Lock.passwordless().withOptions {
        $0.passwordlessMethod = .magicLink
    }
    .onAuth { credentials in
        print(credentials)
        // save access token and use it to do stuff :)
    }
    .onError{ error in
        print(error)
        // you should probably do something other than just print errors
    }
    
    func makeUIViewController(context: UIViewControllerRepresentableContext<Auth0Lock>) -> LockViewController {
        return lock.controller
    }
    
    func updateUIViewController(_ uiViewController: LockViewController, context: UIViewControllerRepresentableContext<Auth0Lock>) {
        // there isn't really much updating I need to do atm
    }
}

Auth0 附帶一個憑證管理工具,會處理 refresh token、請求新的 access token、儲存與驗證 token 等。所以你不需要花太多功夫就能讓使用者保持登入狀態!

稍微補充一下我為什麼放棄 Auth0:我想要在處理驗證的方式上有更多彈性,最後我把後端重寫成 Django 應用程式——所以直接整合 Django 現有的功能對我來說就很容易了。我改用 Django 是為了更快地打造產品,而不需要花時間思考有趣的架構問題。

Apple 開發者

為我的公司(Pillion Software Ltd)註冊 Apple Developer 帳號大概花了一到兩週——線上表單填寫很快,但審核花了一點時間,而且他們還打電話給我。不過之後就一切順利了!

我曾嘗試使用 Fastlane 來擷取螢幕截圖,但發現模擬器/測試運作得不太順利(Mapbox 在 UI 測試中會當掉),所以唯一可行的方法就是手動擷取幾張。不是很理想,我希望能解決這個問題,之後會找時間處理。這也跟我的筆電非常慢又有點舊(主要是 RAM 不足)有關,所以模擬器跑起來非常糟。

我在第一次提交 App Store 後被告知我的 App 功能不夠豐富。真令人沮喪。研究之後發現,任何可以做成網頁應用程式的東西,Apple 似乎都不太願意核准。所以我就把路線圖上的一些功能提前實作了。

實際上,我有一個版本從我按下「提交」到正式上線,整個審核流程只花了約 7 分鐘,讓我非常驚艷。

結論

回想起來,我想我應該會在沿用舊網頁版的相同技術之前,先研究一下其他解決方案。或許 MapKit 就足以勝任,我就不需要使用 MapBox 了。

不過在這個過程中我確實學到很多,所以我不覺得會改變太多。我很快就會開始為 Android 打造同樣的東西(而且我其實也還不太會 Android 開發 ;P),到時候再看看情況如何!可以期待一篇類似的文章。

總之,Pillion 現已在 App Store 上架!

如果你有騎車,不妨試試看,我很想知道你的想法——所以請不要猶豫,隨時與我聯繫!

原文由 Ellie Huxtable 發布

本文章由 muse-spark-1.2-contributor 進行翻譯