建立 Xcode 的檔案範本

什麼是範本?

Xcode 11 beta 2 新增檔案畫面

在新增檔案的時候,會有預設的這幾種類型可以選擇;而當我們有一些自己常用的架構,如 Coordinator、ViewModel 等,可以自建一個範本來使用。

下面舉例幾個預設的 Swift 檔案

Swift File

位置在
/Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File Templates/Source/Swift File.xctemplate

//___FILEHEADER___

import Foundation

SwiftUI View

位置在
/Applications/Xcode-beta.app/Contents/Developer/Library/Xcode/ExtraTemplates/File Templates/User Interface

//___FILEHEADER___

import SwiftUI

struct ___FILEBASENAMEASIDENTIFIER___ : View {
    var body: some View {
        Text(/*@START_MENU_TOKEN@*/"Hello World!"/*@END_MENU_TOKEN@*/)
    }
}

#if DEBUG
struct ___FILEBASENAMEASIDENTIFIER____Previews : PreviewProvider {
    static var previews: some View {
        ___FILEBASENAMEASIDENTIFIER___()
    }
}
#endif

這些是跟著 Xcode.app 走的,而若是我們使用者自定義要使用的,得放在
/Users/UserName/Library/Developer/Xcode/Templates
先建立一個資料夾,或是可以到上述的位置複製一份過來。
需要至少三個檔案

  • FILEBASENAME.swift
  • TemplateIcon.png
  • TemplateInfo.plist

我自己的使用方式是複製系統的 Swift file 來做修改 🔧
也比較好去熟悉可替換文字和 header 的使用方式 👍

Resign .ipa

前言

我們都知道 iOS 是使用 ipa 檔案來安裝 app,而該如何換掉 ipa 裡頭的憑證呢?

解壓縮

首先,我們先將 Application.ipa 解壓縮,可以得到一個 PayLoad 的資料夾,裡頭包含著一個應用程式 Application

移除舊有的簽章

接著我們透過 terminal 來進行移除的動作

rm -rf PayLoad/Application/_CodeSignature

找出你打算簽章的 provisionprofile

你可以透過 Apple developer 的網頁直接下載,或是查看本機裡頭的檔案,路徑會是在:

~/資源庫/MobileDevice/Provisioning Profiles/

用 Finder 到那路徑之後,可以使用空白鍵來進行預覽,
透過名稱以及 SHA-1 來辨別。

嵌入 mobileprovision 到 app 之中

接著就複製並改名字放入 app 中

cp yourProvisionProfile.mobileprovision Payload/Application.app/embedded.mobileprovision

來製作簽章所需要的 entitlements.plist

先將 .mobileprovision 輸入成 profile.plist

security cms -D -i yourProvisionProfile.mobileprovision  > profile.plist

再來利用 profile.plist 輸出成 entitlements.plist

/usr/libexec/PlistBuddy -x -c 'Print :Entitlements' profile.plist > entitlements.plist\

記得 App Bundle Identifier 得和簽章的名稱一致

/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier tw.Archie.Application" Payload/Application.app/Info.plist

就可以開始簽章了!

我們先透過 security 來找尋並複製剛剛 .mobileprovision 的 SHA-1

security find-identity -p codesigning -v

這邊記得要從 Frameworks 開始一個一個簽,再來才是 App 本身;
假設你的 mobileprovision SHA-1 為 B77ED97FBFC708A802BDB159CC2ED4E2A4472A09

codesign -f -s B77ED97FBFC708A802BDB159CC2ED4E2A4472A09 --entitlements entitlements.plist Payload/Application.app/Frameworks/someFramework.framework

一個一個簽章完之後,便可以簽署 app 了

codesign -f -s B77ED97FBFC708A802BDB159CC2ED4E2A4472A09 --entitlements entitlements.plist Payload/Application.app/Application

最後確認是否有簽對

我們可以透過 codesign 來查詢

codesign -vv -d Payload/Application.app

如果資訊都沒錯的話,就來輸出吧!🎉

zip -qr Resigned.ipa Payload

題外話,使用 Xcode 安裝

就在 Xcode 之中打開 Devices and Simulators 視窗,並將剛剛最後產出的 ipa 直接拖拉到你的裝置即可!

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 來遵循即可!

Info.plist localized

一些需要權限的功能都會需要在 Info.plist 裡頭加上說明,而這個說明要實作多語系則是建立一個 InfoPlist.strings(沒錯,名字就是這樣)
然後對相對應得 Key 填寫翻譯,像是:

NSLocationWhenInUseUsageDescription = "說明填寫";

就可以在要使用位置的說明處顯示多語系的結果了。
至於其他的 Key 可以從CocoaKey 官方文件 以及CoreFundationKey找找。

Git submodule

有些時候,我們會需要幾個檔案和其他專案共用,而 iOS 的專案可以採取 CocoaPods / Carthage,但如果是要和其他語言共用的話該怎麼辦呢?
舉個例子,Server 和 Client 之間傳遞 Status Code,像是 code: 20000、20001、20002 之類的,收到 code 後要再做後續動作。
不過一份 Code 的定義散落在多個平台 / 專案之中,難免會有人雷的時候;不論是 client 記錯或是 server 回錯,而若是有個地方可以共同維護的話,便可以減少這種失誤。
所以就把那些文件(e.g .json)放到 repository 上,然後在你的專案之中:

git submodule add YourDocumentRepository.git

就會在你的專案資料夾中看到 clone 下來的結果,接著再將檔案拖拉至專案之中即可使用。
若要更新 submodule,則下

git submodule update

或是到 clone 下來的資料夾

git pull

像是如果懶得在每一個檔案都 import PodName,就直接弄成 Submodule 的方式來處理也行!
如我自己習慣的一些 Extension 就這麼弄。

Create CocoaPods by yourself

這篇文章會介紹建立自己的 CocoaPods 流程,而我當初是看了 David 的教學文所跑的流程。
首先,先建立一個新的 Pod

pod lib create YourPodName

接著依照自己的 Pod 內容回答問題,便會生成一個新的專案出來。
再來將 Code.Swift 丟到 Pods/Development Pods/YourPodName,也就是它預設 ReplaceMe.Swift 的那個地方。
最後 README.md 和 YourPodName.podspec 寫一寫就完成了 Pod 的準備。
而預設的 REAMME.md 裡頭,有一個 CI Status,你可以移除或者到 Travis.CI 建構;
從 Travis.CI 那邊可以得知如何建構一個 .travis.yml。
都準備好之後,在 GitHub 上開一個 repository 來放置,記得要放上 tag 標記目前的版本。

pod spec lint YourPodName.podspec

最後就送出去就好了!

pod trunk push YourPodName.podspec

如果你沒有註冊過的話,得先註冊一下:

pod trunk register email@domain.com 'Your Name'

而如果你有在 .podspec 裡頭填寫你的 Twitter 的話,就會收到 CocoaPods 貼的文!
如果你搜尋不到你的 Pod 的話,可以清除目前的 cache 就可以順利找到了!

rm -rf ~/Library/Caches/CocoaPods

UUID with version 3, 5 and name spaces

在 Swift 裏頭,預設的 UUID 只能從 UUID() 來產生,或者是從另一個 UUID 來產生,
這邊來記錄一下如何從 String 來產生 UUID。
首先,先在 Bridge-Header.h 裡頭加入

#import <CommonCrypto/CommonCrypto.h>

再來寫個 UUID 的 extension:

就可以使用新的 init method 來產生新的 UUID:

UUID(version: UUIDVersion, name: String, nameSpace: UUIDv5NameSpace)

Socket.IO

這篇文章會介紹有關於 Socket.IO 在 server 端以及 iOS 端的一些實作分享。

Server

首先我們可以先從 Socket.IO 的 Get started 開始,它是以 Node.JS 所編寫的,所以先在資料夾位置執行

npm init

邊可以取得基本的一些檔案和 package.json。
接著安裝 Socket.IO 所需要的 express

npm install --save express@4.15.2

然後新建一個 index.js 的檔案,貼上

var app = require('express')();
var http = require('http').Server(app);
app.get('/', function(req, res){
    res.send('<h1>Hello world</h1>');
});
http.listen(3000, function(){
    console.log('listening on *:3000');
});

這樣待會執行 node index.js 的時候便可以從 http://localhost:3000 來連上這個 server。
確認 server 目前是可以連上後,便開始安裝 Socket.IO 到其中

npm install --save socket.io

並在 index.js 裏頭加上 Socket.IO 的 code:

var io = require('socket.io')(http);
io.on('connection', function(socket){
    console.log('a user connected');
});

這樣便完成監聽 client 連接上 server 的事件了。
'connection' 是 socket.io 所定義的 event,而這個 event 會取得 socket 回來,所以以 function(socket) 的方式去接收並處理後續。
不過這裡的 io 可以想成是一個 server,所以每收到一個 client 的 connection 事件,都會執行 function(socket);而我們若要監聽個別連線的事件,則是使用 socket 來處理:

socket.on('disconnect', function(){
    console.log('user disconnected');
});

也就是說,像是登入、傳送訊息等,和個別使用者相關的動作,我們都是以 socket 來處理。
而除了監聽(on)外,發送(emit)的動作也是如此,像是我想推一段文字給特定的使用者:

socket.emit('new message', '嗨!');

在 client 以及 server 端先彼此定義好 event 名稱,這樣就可以知道要監聽的事件為哪些。
什麼時候該用 io 發送,什麼時候該用 socket 呢?
舉個例子,我們建立一個 Socket.IO 的大聊天室,任何人只要連上這個網址,便等同於加入這個聊天室。

  • 使用者上、下線會顯示提醒
  • 說髒話的人會個別收到「此訊息無法傳送」的訊息
io.on('connection', function(socket) {
    io.emit('new user', '有人加入了這個聊天室!');
    socket.on('add message', function(from, message) {
        if (message.indexOf('馬的') > -1) {
            socket.emit('new message', '此訊息無法傳送');
        } else {
            io.emit('new message', from + ' 說:' + message);
        }
    })
    socket.on('disconnect', function() {
        io.emit('user leave', '有人離開了這個聊天室!');
    });
});

上面便可以得知哪種時候應該要大家都可以接收到、而哪種是只會有個別的使用者接收到。
再來介紹一下 ack,在發送一個 event 時,可以在後面補上一個 ack,而當對方收到的時候,可以透過 ack 來傳遞 data,而非再發送一個新的 event。
有點像是 HTTP request 的概念,發送一個 request(emit),接著對方會回傳 response(ack),不過這並不一定會有,也就是說你發送(emit)了一個 event 過去,雖然有夾帶著 ack,但是對方若沒接收那個 ack 的話也是沒用。
這邊是一個例子,我們從 server 給沒有收過這則廣告的使用者傳送一則過去,並希望使用者真的有收到,若沒收到下次就再傳一次:

// 判斷使用者是否有收到過廣告,若沒有的話執行
socket.emit('new advertise', '恭喜您獲得 $1,000 元折扣!', function(userID) {
    console.log('使用者(' + userID + ')收到廣告了!');
    //去資料庫更新,下次不用再推給這個 userID
)};

其中,function(userID) 便是一個 ack 或者可以說是 callback。
相對的,收到訊息的時候,server 也可以回傳 ack 回去給 client 告知:

socket.on('add message', function(text, ack) {
    console.log('收到: ' + text);
    ack('伺服器收到你的訊息了!請放心!);
});

如此一來,便會在收到訊息之後,以 ack 的方式回傳訊息回去。
而 ack 和 emit 所發送出去有什麼不同?
在 Socket.IO 的 protocol 裏頭有定義:

  • Packet#CONNECT ( 0 )
  • Packet#DISCONNECT ( 1 )
  • Packet#EVENT ( 2)
  • Packet#ACK ( 3 )
  • Packet#ERROR ( 4 )
  • Packet#BINARY_EVENT ( 5 )
  • Packet#BINARY_ACK ( 6 )

所以其實雖然動作類似,但 Socket.IO 可以辨別其中的差異,進而可以在 Socket 之中達到 Request、Response 的概念。

Client

Client 的部分,可以使用 Socket.IO 所提供的 Swift framework,它是基於 Starscream 所開發出來的,如同上述說的有使用到 WebSocket 來連接。
以 Swift 來說:

let manager = SocketManager(socketURL: URL(string: "http://localhost:3000")!, config: [configs])
let socket = manager.defaultSocket

這邊介紹幾個我所使用到的 configs

  • .log(true):開啟 LOG 的功能。
  • .forceWebsockets(true):若沒有使用這個的話,會以 HTTP polling 的方式連接,從 header 來看的話,就會顯示 connection: keep-alive;而使用了 .forceWebsockets(true) 的話,則會使用 WebSocket 來連線,則會顯示 connection: upgrade。
  • .reconnectAttempts(int):重新嘗試連線 n 次,超過就放棄。
  • .connectParams([String: Any]):這邊可以放 token 來做 Authentication。

為什麼不在 header 裏頭加上 Authentication 的欄位?
這邊 Socket.IO 有做解釋,為什麼不建議在 extraHeaders 加東西。
而 on / emit / emitWithAck 這幾個的用法就和 server 的概念一樣,這邊就不再多做解釋,
之後實作有遇到什麼事情再來補充(或是新文章)!

StatusBarStyle

最近改寫了 StatusBarStyle 的控制方式,從 iOS 9 之前是使用

UIApplication.shared.statusBarStyle = .default

並在 info.plist 的 View controller-based status bar appearance 欄位設為 NO。
而後來則改為覆寫 preferredStatusBarStyle 的方式,就不用在 viewWillAppear / viewWillDisappear 的時候手動控制 StatusBarStyle。
首先先確認 info.plist 之中 View controller-based status bar appearance 是為 YSE,
代表我們是透過 View controller-based 的方式來更改 status bar style。

override var preferredStatusBarStyle: UIStatusBarStyle {
    return .default
}

而配合 UINavigationController 的使用,可以以 Subclass 或是 Extension 的方式來實作,
這就得看專案需求了。

Subclass

在 BaseNavigationController 裏頭,利用 childForStatusBarStyle 回傳要呼叫哪個 UIViewController 的 preferredStatusBarStyle

override var childForStatusBarStyle: UIViewController? {
    return topViewController
}

這樣每當 UINavigationController 畫面在切換時,便會將 topViewController 叫出來問問它 preferredStatusBarStyle 要什麼樣式。

Extension

我們透過 extension UINavigationController 的方式也可以達到一樣的效果

extension UINavigationController {
    override open var childForStatusBarStyle: UIViewController? {
        return topViewController
    }
}

如此就不用為了 childForStatusBarStyle 來多寫一個 subclass。