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。

Xcode beta with CocoaPods

Xcode 10 beta 的 Swift 版本為 4.2,而若你目前所使用的 Pods 多數為 Swift 4 的話,該怎麼辦呢?

你可以在 Podfile 裏頭加上全域的參數來規範所有 Pods 的 Swift version:

post_install do |installer|
    installer.pods_project.targets.each do |target|
        target.build_configurations.each do |config|
            config.build_settings['SWIFT_VERSION'] = '4'
        end
    end
end

如此一番便可以輕鬆地在 Pods 還沒全面支援 Swift 4.2 時就可以使用 Xcode 10 Beta 開發👏