CoreData with background task

前言

雖然在 iOS 上開發了幾年的時間,但一直到最近才開始使用 CoreData;之前在第一份工作的專案之中是使用 FMDB 來處理資料存取,而後續則是用了 Realm

選擇的原因分別是當時 FMDB 的速度較其餘兩者快速,而後來看上 Realm 的跨平台特色,不過近期開發的感想是能以原生為主的話,就儘量降低對於第三方套件的依賴性。

這篇文章會記錄些什麼

其實這篇文章並不會從頭到尾寫下教學,而把重點放在一些我踩到的雷上,像是⋯⋯

記得要附上 sqlite 的路徑

原先我的 persistentContainer 的產生方式如下

lazy var persistentContainer: NSPersistentContainer = {
    let container = NSPersistentContainer(name: "OfflineWallet")
    let description = NSPersistentStoreDescription()
    description.shouldInferMappingModelAutomatically = true
    description.shouldMigrateStoreAutomatically = true
    container.persistentStoreDescriptions = [description]
    container.loadPersistentStores { _, error in
        if let error = error {
            fatalError("Unresolved error \(error), \(String(describing: error._userInfo))")
        }
    }
    return container
}()

在模擬器上存取了幾次,每次都有 save 且第二次進入畫面的時候,都可以 fetch 得到資料,但是只要重開 App 就會從頭來過⋯⋯

也就是說其實都只是像是存在 NSManagedObjectContext 上,而並沒有實際地轉成 sqlite

補上指定的 URL 即可解決。

...
let documentsDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
let sqliteURL = documentsDirectoryURL?.appendingPathComponent("OfflineWallet.sqlite")
description.url = sqliteURL
...

performBackgroundTask v.s newBackgroundContext

要實作 backgroundTask 有兩種作法

NSPersistentContainer.performBackgroundTask

persistentContainer.performBackgroundTask { context in
    //do something in background thread
}

NSPersistentContainer 會建立一個 context 在這個 closure 裡頭使用,而重點是當這條 thread 結束之後,這個 context 所管理的物件(NSManagedObjectModel)的所有變數會釋放掉,也就是都會變成 nil

適合用於取得資料並轉型成其他 class / structure 的時候使用。

newBackgroundContext

而如果你必須在後續的程式之中繼續使用 context 所產生的 NSManagedObjectModel 時,你就得要保存其 context;像是建立一個 backgroundContext 並存下來使用:

lazy var backgroundContext = persistentContainer.newBackgroundContext()

//you should use this context to do something what you want
backgroundContext.perform {
//do something like before
}

此時由於這個 context 並沒有被釋放掉,所以其 NSManagedObjectModel 的所有變數便也會持有著;而依然是在其他 thread 上進行,並不會佔據 main thread。

待續

分頁讀取

筆記一下 NSFetchRequest 有提供 fetchLimit 以及 fetchOffset 可以用來做分頁讀取的功能。

SwiftLint

SwiftLint

去年(2018)年末的時候,在 Twitter 上看到一些朋友們在討論著 SwiftLint 的使用,於是便也嘗試在目前公司的專案中導入,來解決 Coding Style 的問題。

Coding Style 的問題

剛進入到這家公司時,最痛苦的事情莫過於毫無章法的 Coding Style,這部分就不一一細數了,詳情可以看前陣子我的 Twitter 動態

專案裡頭每個人寫的格式不同,會造成其他人在閱讀專案時,大幅增加理解彼此想法的成本

簡單來說就是浪費一堆時間在猜你在寫什麼

為了讓專案裡頭的大家有差不多的 Coding Style,可以選擇使用 SwiftLint 來處理這個情況。

實際嘗試

首先先以 SwiftLint 的官方教學來安裝和建構環境,並利用 EthanSwiftLint rules 當作基底來調整,先以別人的規範來看看差異性。

一跑下去便是直接噴了 3000+ issues⋯⋯

可以透過 Rules.md 來逐條看看定義並透過 example 來看怎麼算是 Non TrggeringTriggering;再來一條一條調整改進。

一些心得

目前公司的專案從 3000+ ➡️ 17 warings,而這 17 條是我還沒 refactor 到的 features,所以就還沒那麼急著去調整。

private_outlet 和 private_action

而其中一條規則 private_outlet 給我的感觸挺深刻的,由於我大多數的工作經歷是獨立開發居多,而每個 IBOutlet 都必須為 private 是第一份工作時所踩到的坑;當時公司共有兩位 iOS 工程師,彼此皆為 Junior 的程度,故沒什麼規範和概念,於是便會出現一些神秘的情境⋯⋯

像是不知道為什麼你負責的 UIViewController 刻出來的畫面就是和你想的不太一樣,才發現另一個地方(別人寫的 code)在直接修改畫面的 Layout / value⋯⋯

後來就體會到物件的每個變數和 functions 的 access 問題,而這條規則便是解釋著 IBOutlet 不應該可以直接從其他地方呼叫、修改,像是被這麼做:

fooViewController.fooLabel.text = "Test"

從那時候開始養成的習慣到現在,當發覺其實有其他人也是這麼做,並將它視為一條 rule 的時候真的覺得有點小感動!

整體來說

你可以透過這個 SwiftLint 來反覆思考一些寫程式上的問題,像是 function 的長度、class 的長度以及 Swift 檔案的長度等;如何切割每個物件和 function 等,都是相當值得去思索的習慣問題。

不過也不需要逐條都導入到專案之中,建議是花一些時間找到你最認同的那幾條 rules 來遵循即可!

Firebase Database REST API

這篇主要的內容會是簡單地記錄一下 Firebase Database RESTful API
所提供的相關內容和使用方式。

Firebase Database

Firebase database 的儲存資料方式是屬於 NoSQL 的方式,
利用一組 key 配對一組 value 的模式來建構資料庫;
而在 Database 的介面中,我們可以清楚地看到資料是以 JSON 的格式呈現。

在 iOS 開發的過程中,如果要使用 Firebase 的相關內容,
可以使用官方所提供的 Firebase iOS SDK
或是在使用 Python 開發的時候,我會選擇使用官方推薦的 Pyrebase
那若你目前的開發方式沒有相對應的 SDK 或是 third party 可以使用的話呢?
那麼你就只能一起用 REST API 來完成要做的事情了!

REST API

Firebase 提供了五種 Http method

  • GET
  • PUT
  • POST
  • PATCH
  • DELETE

其中 GET 和 DELETE 就沒什麼特別好說的,你就是取得一個 JSON 或是刪除一個。

PUT

PUT 就和平常使用 PUT 的方式一樣,
它會把整個 JSON 覆蓋成你目前丟上去的 JSON。

POST

POST 的話就有些不一樣,當你 POST 一個資料到某個 JSON 的時候,
它會自行建立一組 key 並回傳 name 回來。
而你所丟的資料會是那組 key 所對應的 value,
所以簡單來說 POST 就是在做新增物件時使用,且 ID 是由它所建立的。

PATCH

PATCH 則是負責更新內容,它會先找到匹配的 key,再更新其 value;
若沒有找到相對應的 key 的話,則會建立一筆新的 key-value。

如果有其中一個無法使用呢?

若遇到你所使用的開發語言或是瀏覽器等等,無法使用其中一項 method 時;
舉個例子,DELETE 無法使用的話,Firebase 有提供你覆寫 method 的功能,
method 使用 POST,而在 header 加上:
X-HTTP-Method-Override: DELETE
便可以等同於上述所說的 DELETE。
或是加在 url 裡頭也可以:
https://[PROJECT_ID].firebaseio/[JSON_NAME].json?x-http-method-override=DELETE
 

Firebase ETag

Firebase 也有支援 ETag,
在 header 上加上 X-Firebase-ETag: true,它便會在回傳的 headers 中加上
Access-Control-Expose-Headers = ETag
ETag = kmHkuKx9sCx742tosJOV4oH+JBQ=
而我們可以下一次的 request 中,在 header 放上 if-match:[ETAG_VALUE]
伺服器端便會驗證是否可以執行這次的要求;
若最後一次的 ETag 和 if-match 的值相符的話,Firebase 便會回傳 412 Precondition failed。
而 ETag 相關的資訊可以看這邊
 

最後

整理完這些資訊後,就可以著手寫一些 database 的存取方法到 Vapor 的專案了!