SwiftUI + Google AdMob

這篇是一個簡單介紹 UIViewControllerRepresentable 的範例,
由於 Google AdMob 的 GADBannerView 不像上次提及的 UITextField 一樣,可以直接使用 UIViewRepresentable 來包裝;原因是它必須設置一個 rootViewController,也就意味著我們需要使用 UIViewControllerRepresentable 才能完成它。

Interfacing with UIKit

透過這個 Apple 官方的教學當中,我們可以從 UIPageViewController 的範例來做發想,故我的實作方式會是這樣:

import GoogleMobileAds
import SwiftUI
import UIKit

struct GADBannerViewController: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        let view = GADBannerView(adSize: kGADAdSizeBanner)
        let viewController = UIViewController()
        view.adUnitID = "your ad unit id in there."
        view.rootViewController = viewController
        viewController.view.addSubview(view)
        viewController.view.frame = CGRect(origin: .zero, size: kGADAdSizeBanner.size)
        view.load(GADRequest())
        return viewController
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

如果需要處理 Delegate 的部分

你可以參閱上次 TextField 的處理方式,建構一個 Coordinator 來進行相關的作業👌

如果還有問題的話

歡迎直接透過各種方式找到我,一起來討論討論 SwiftUI 的應用👍
程式碼會同步到 GitHub 上,有任何想法都可以直接留言📒

對於 SwiftUI onDisappear 的誤解?

在接觸 SwiftUI 的這段時間以來,我一直在試著釐清一件事情,那就是

onDisappear 到底是不是壞的!

這件事情很玄,畢竟網路上大部分的資訊都告訴我們 onAppear 類似於 viewDidAppearonDisappear 類似於 ViewDidDisappear,然後再補上一句

Note: In the current SwiftUI beta onDisappear will never be called.
by HackingWithSwift

或是你可以看到在 StackOverFlow 上大家是這麼討論的

file

接著,在這一路以來,你又曾經碰過真的是 Apple 的 bug,所以你就會很理所當然地認為⋯⋯

沒錯,onDisappear 就是壞的!

直到認真找找官方文件到底有沒有使用到 onDisappear 的範例,於是找到了這篇 並下載下來研究發現

onDisappear 是會動的⋯

這代表著一件事,就是其實是我誤解它的使用方式,而非它是壞的。
來看看官方的這個 View

struct ProfileHost: View {
    @Environment(\.editMode) var mode
    @State var profile = Profile.default
    @State var draftProfile = Profile.default

    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            HStack {
                if self.mode?.wrappedValue == .active {
                    Button(action: {
                        self.profile = self.draftProfile
                        self.mode?.animation().wrappedValue = .inactive
                    }) {
                        Text("Done")
                    }
                }

                Spacer()

                EditButton()
            }
            if self.mode?.wrappedValue == .inactive {
                ProfileSummary(profile: profile)
            } else {
                ProfileEditor(profile: $draftProfile)
                    .onDisappear {
                        self.draftProfile = self.profile
                    }
            }
        }
        .padding()
    }
}

想了一下,若是我的話我會將 onDisappear 寫在哪裡?應該會是在 ProfileEditorbody 裡頭吧。而實際上測試了,在 ProfileEditor.body 裡頭實作

.onDisappear { print("disappear") }

是沒有效果的,我才得到一個結論

onDisappear 和 viewDidDisappear 不同

onDisappear 的概念是監聽你底下那個消失的動作

Adds an action to perform when this view disappears.

但並不是像 UIViewController.viewDidDisappear 一樣,是對物件本身消失去做動作。

因為大多數的文章都習慣將 onAppearviewDidAppear 做對比,也就造成我自己對於使用方式上產生誤解;實際上 onDisappear 的動作應該做在 superView 之中,而非直接寫在那個 struct 裡頭。

這大概就是一個從 UIKit 轉到 SwiftUI 才會誤解的地方了⋯⋯如果是一開始就從官方文件開始用 SwiftUI 學習寫 iOS 的人,應該不會陷入這種迷思🤷‍♂️

📒 SwiftUI + CoreData 的實戰心得🔥

SwiftUI + CoreData

file

這篇文章會紀錄我在目前的 side project 上,如何在 SwiftUI 下導入 CoreData;
而如同 在 SwiftUI 處理中文輸入法所會遇上的問題 所提及的,SwiftUI 身為一個還在測試階段的 framework,我們必須將當下的開發環境紀錄下來,以避免造成日後官方修正所造成的誤解。

開發環境

  • Xcode 11 Beta 7(但顯示為 Beta 6 - 11M392r )
  • macOS Catalina - 19A546d
  • iOS 13.1 Beta

使用情境

我要做一個貨幣的列表,並讓使用者可以對相對應的貨幣做隱藏與否,所以規格大概是需要一個 List,而 Row 裡頭呈現貨幣的名稱以及用 Toggle 來做控制隱藏的開關。

CoreData Model - Currency

file

Row 的部分

Xcode 11 Beta 5 之後NSManagedObject 可以視為一個 @ObservedObject,所以我們可以不必再弄一個 ManageRowModel,而是直接使用 NSManagedObject 來連動。
這邊我需要以英文大寫來顯示貨幣名稱以及一個控制是否顯示在主畫面上的開關。

struct ManageRow: View {
    @ObservedObject var currency: Currency

    var body: some View {
        HStack {
            VStack {
                Text(currency.name?.uppercased() ?? String())
                    .font(.title)
                    .fontWeight(.bold)
            }
            .padding()
            Toggle(isOn: $currency.isPresented) {
                Text(String())
            }
            .padding()
        }
    }
}

畫面如下方所呈現的樣式:

file

我們可以直接讓 Toggle(isOn: _)Currency.isPresented 連動,這樣便可以直接修改到相對應的值。

View 的部分

在 SwiftUI 裡頭有提供一個 @FetchRequest(fetchRequest: 的 propertyWrapper,而若要使用的話,記得要一併宣告 NSManagedObjectContext@Environment 之中,否則會報錯:

struct ManageView: View {
    @Environment(\.managedObjectContext) private var context
    @FetchRequest(fetchRequest: fetchRequest()) var currencies: FetchedResults

    var body: some View {
        List(currencies, rowContent: ManageRow.init)
    }

    static func fetchRequest() -> NSFetchRequest<Currency> {
        let request: NSFetchRequest = Currency.fetchRequest()
        request.sortDescriptors = [NSSortDescriptor(keyPath: \Currency.name, ascending: true)]
        return request
    }
}

這樣就可以在畫面建立時,透過 SwiftUI 的機制去執行 fetchRequest 並呈現出來。

⚠️ 注意事項

Identifible 的使用

若要在 List 之中直接使用 NSManagedObject 作為 RandomAccessCollection,可以讓它符合 Identifible

extension Currency: Identifible {}

但這個意味著我們會以預設的 id,如 NSManagedObject.objectId 作為是否需要重新繪製畫面的依據,而在這邊會遇上一個問題:
當我們將這個 List 作為另一個畫面的 Sheet 時,當它出現時,並不會重新繪製 List
也就是說已經在畫面上的 Row,會出現第一次畫面的狀態且之後 Sheet 出現都還會保持一樣的畫面。

簡單來說就是開關的狀態不會改變,除非使用者自己上下滑動觸動 SwiftUI 重新繪製的機制

所以我必須讓系統知道,isPresented 若有改變過的話,需要更新 row 的畫面

extension Currency: Identifiable {
    public var id: String { "\(String(describing: name)) \(isPresented)" }
}

這邊我目前的作法是將 id 包含了 isPresented 的狀態,所以當同一個 objectId 有不同的 isPresented 時,對於 List 是不同的 Row,這時就會重新繪製成正確的畫面。

@Environment(.managedObjectContext) 的使用

我這邊的使用情境是,使用者點擊一個設定的按鈕會跳出一個 ManageView,而 ManageView 可以點擊儲存或是直接滑掉放棄當前操作。
所以在前一個畫面上,我們需要為它設立 NSManagedObjectContext,並在 onDismiss 時捨棄掉這次的操作。

struct ContentView: View {
    ...
    var body: some View {
        ...
        .sheet(isPresented: $isManagePresented,
                   onDismiss: { CoreDataStack.shared.backgroundContext.rollback() },
                   content: manageView)
    }

    private func manageView() -> some View {
        ManageView().environment(\.managedObjectContext, CoreDataStack.shared.backgroundContext)
    }
}

以上便是目前在 SwiftUI 上實作 CoreData 的分享📒

SwiftUI 上的鍵盤處理方式

在 iOS 的開發過程之中,難免會碰到一個狀況,那便是 UITextField/ UITextView 被鍵盤所遮住了⌨️
在 UIKit 之下,多數人會使用套件來做全域的處理,如 IQKeyboardManager 就是一個十分經典的解決方案。

來說說 SwiftUI 上的鍵盤處理方式

在 SwiftUI 上,我們也可以很優雅地處理這一塊,如在 List 元件中,只需要分別監聽 UIWindow.keyboardWillShowNotificationUIWindow.keyboardWillHideNotification,以及加上個 .animation(.default) 來優化使用者體驗。

var body: some View {
        List(viewModel.rowModels, rowContent: DemoRow.init)
            .padding(EdgeInsets(top: 0, leading: 0, bottom: bottomPadding, trailing: 0))
            .onReceive(NotificationCenter.default.publisher(for: UIWindow.keyboardWillShowNotification),
                       perform: updateFrame)
            .onReceive(NotificationCenter.default.publisher(for: UIWindow.keyboardWillHideNotification),
                       perform: updateFrame)
            .animation(.default)
    }

完整的 struct 可以在 GitHub 上查看👍

成果動畫

有任何問題歡迎在底下留言👏有寫法上的建議可以直接在 GitHub 上反應👍
有想看看一些廢話的話則是可以在 Twitter 直接找到我喔!😂

在 SwiftUI 處理中文輸入法所會遇上的問題

最近開始在嘗試把玩 SwiftUI 並打算作為一個 side project 的主要 UI 編寫方式,這篇文章則是其中一個使用情境所遇上的困難處。

開發環境

  • Xcode 11 Beta 7(但顯示為 Beta 6 - 11M392r )
  • macOS Catalina - 19A546d
  • iOS 13.1 Beta

記錄這點蠻重要的,因為可能過個幾版這篇文章就沒參考價值了😂
還沒有下載更新的朋友,可以快去更新一下!

使用情境

我要來做一個搜尋的功能,讓使用者可以輸入關鍵字,並自動搜尋完後將結果呈現在下方👏

於是我們可以用兩個 UI 元件來達成這件事

  • TextField
  • List
    並寫一個 @Binding var 或是 @ObservedObject var 來讓 TextField 的 text 有個 binding 的地方,當它的值有更新時,觸發搜尋的動作🚀
    而搜尋完的結構再來更新畫面上的 List

聽起來沒什麼毛病,對吧?

實際上你會遇上的問題⋯

當你想和我一樣,用拼音的輸入方式時;不論是你拼音還是注音,當你按下第一個音時,便會跑一次上述的流程了⋯⋯
而我們所期望的流程應該是使用者選完字後再進行搜尋,這點在目前的 TextField 是做不到的,因為它不會判別目前是否有還沒拼完音的狀況就發動了。

那該怎麼辦呢?

喵神這麼說

你可以查看一下當時的相關推文

我的作法就是以 UIViewRepresentable 來包裝,詳細的程式碼可以到 GitHub 查看👍

成果

當輸入了 luo dong 但是還沒選字的話並不會有動作。


而當選字後便會進行搜尋🔍

如果還有什麼問題的話,歡迎留言討論👏

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

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 的概念一樣,這邊就不再多做解釋,
之後實作有遇到什麼事情再來補充(或是新文章)!

Bitnami