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 的概念一樣,這邊就不再多做解釋,

之後實作有遇到什麼事情再來補充(或是新文章)!

MKGeodesicPolyline

先來看看 MKGeodesicPolyline 在 Apple Developer Documentation 上的介紹:

A line-based shape that follows the contours of the Earth to create the shortest path between the specified points.

繪製 Polyline

首先我們在建置 MKGeodesicPolyline 的時候,
給予它一個 [CLLocationCoordinate2D],並宣告要繪製幾個點;
接著讓 MKMapView 新增進去。

let geodesicPolyline = MKGeodesicPolyline.init(coordinates: [start, end], count: 2)
mapView.add(geodesicPolyline)

再來我們需要透過 MKMapViewDelegate 的 function 來定義 MKGeodesicPolyline 的 UI:

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
    guard let polyline = overlay as? MKPolyline else {
        return MKOverlayRenderer(overlay: overlay)
    }
    let renderer = MKPolylineRenderer.init(polyline: polyline)
    renderer.lineWidth = 1
    renderer.strokeColor = .red
    return renderer
}

取得 Polyline 中間的經緯度

MKGeodesicPolyline 的繪製是由很多個點所連起來的,
可以利用 points() -> UnsafeMutablePointer 來取得。

@available(iOS 4.0, *)
open class MKMultiPoint : MKShape {


    open func points() -> UnsafeMutablePointer<MKMapPoint>

    open var pointCount: Int { get }


    // Unproject and copy points into the provided array of coordinates that
    // must be large enough to hold range.length coordinates.
    open func getCoordinates(_ coords: UnsafeMutablePointer<CLLocationCoordinate2D>, range: NSRange)
}

我們可以透過 pointCount 來得知有幾個點,
接著決定要使用哪個點的位置;

MKCoordinateForMapPoint(polyline.points()[index])

從我家到 AT&T Center 的範例

Demo

Siren – 通知使用者更新

我們時常可以在 App 之中看到,「目前有新版本可以提供下載」等相關的訊息;
而實作通知使用者更新的方法很多,這篇則是介紹一個開源的 Framework:

Siren

Siren 的運作邏輯是,你可以透過版號來決定跳出什麼通知來提醒使用者,
並且提供多語系的訊息內容。
它會透過 Bundle Identifier 去 App Store 上尋找資訊,
再來比對版號執行後續動作。

版號的定義

1.0.123.5678

  • 1:major
  • 0:minor
  • 123:patch
  • 5678:revision

一般我個人的習慣是:
major 會是在商業模式改變或是重大功能發布時,才會動到的;
而 minor 則是有必要的更新,像是嚴重的 bug 或是無法向下相容的異動。
patch 是更新一些 issue 或是修正 bug;
revision 則讓它跟著 commit 的數量。

Siren 的設定

舉個例子,在 major、minor 有提升時;
像是從 1.0.0 -> 2.0.0 或是 1.0.0 -> 1.1.0,
我會希望舊的使用者一定要更新 App 才能使用,
則會設為強制更新(.force)。

而 patch 則讓使用則決定要不要更新,或是可以跳過此次更新。

Siren.shared.majorUpdateAlertType = .force
Siren.shared.minorUpdateAlertType = .force
Siren.shared.patchUpdateAlertType = .skip
Siren.shared.revisionUpdateAlertType = .none

還沒上架前的測試

Siren 建議可以先將 Bundle Identifier 更改為 iTunes Connect Mobile 的 Bundle Identifier:com.apple.itunesconnect.mobile
並把 Siren 的 debugEnabled 調整為 true。

UIActivityViewController

這些話寫在前面⋯⋯

最近在開發的產品需要加入「分享」的功能,

希望將一些資訊及圖片分享到其他 App 或平台上;

這篇文章會先點出需求,再逐一闡述開發的過程。

需求

  • Facebook 分享 hash tag 及圖片
  • 其餘分享文字及圖片和網址

實作

我們利用 UIActivityViewController 來呈現分享的選單,並將分享的內容塞入 activityItems: [Any] 之中;

applicationActivities 則設為 nil,並沒有要客製 activity

若沒有需要依照不同類別做出不同的判斷,我們可以將內容放置進去;

像是 URL、String、UIImage等等。

而幾個特點要注意一下:

Facebook:

  • String 只支援一個 hash tag(像是:“#Archie"),若超過或其他一般文字則不會顯示
  • 有網址的話,就會顯示連結;意思是圖片和連結無法同時出現,會優先顯示連結

iMessage

  • 若 String 裡頭的時間格式為 dd/MM/yyyy HH:mm 則可以點擊,並加入行事曆
  • 圖片會以另一則訊息傳送
  • 網址會以縮圖顯示(就如一般訊息收到的邏輯)

依照類別提供不同內容

首先,我們可以先看 UIActivityType

extension UIActivityType {

    @available(iOS 6.0, *)
    public static let postToFacebook: UIActivityType

    @available(iOS 6.0, *)
    public static let postToTwitter: UIActivityType

    @available(iOS 6.0, *)
    public static let postToWeibo: UIActivityType // SinaWeibo

    @available(iOS 6.0, *)
    public static let message: UIActivityType

    @available(iOS 6.0, *)
    public static let mail: UIActivityType

    @available(iOS 6.0, *)
    public static let print: UIActivityType

    @available(iOS 6.0, *)
    public static let copyToPasteboard: UIActivityType

    @available(iOS 6.0, *)
    public static let assignToContact: UIActivityType

    @available(iOS 6.0, *)
    public static let saveToCameraRoll: UIActivityType

    @available(iOS 7.0, *)
    public static let addToReadingList: UIActivityType

    @available(iOS 7.0, *)
    public static let postToFlickr: UIActivityType

    @available(iOS 7.0, *)
    public static let postToVimeo: UIActivityType

    @available(iOS 7.0, *)
    public static let postToTencentWeibo: UIActivityType

    @available(iOS 7.0, *)
    public static let airDrop: UIActivityType

    @available(iOS 9.0, *)
    public static let openInIBooks: UIActivityType

    @available(iOS 11.0, *)
    public static let markupAsPDF: UIActivityType
}

而我們可以自行創建一個類別來實作 UIActivityItemSource
不過因為它是屬於 NSObjectProtocol,所以得一併繼承 NSObject

例如,我現在要寫一個給 Facebook 以及其他類分別不同 String 的 ItemSource:

class ActivityStringItemSource: NSObject, UIActivityItemSource {

    func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
        return ""
    }

    func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivityType?) -> Any? {
        guard let type = activityType else {
            return ""
        }
        switch type {
        case UIActivityType.postToFacebook, UIActivityType.postToTwitter:
            return "#Archie"
        default:
            return "Archie 的 UIActivityViewController 實作心得 #Swift"
        }
    }
}

並在創建 UIActivityViewController 的時候,宣告進去:

 let activityViewController = UIActivityViewController.init(activityItems: [ActivityStringItemSource()], applicationActivities: nil)

這樣便會依照不同的 UIActivityType 來給予特定的 String 內容;
而當然你也可以建立一個有關 URLUIImage 相關的 UIActivityItemSource
在分享出去的時候,便可以決定讓 Facebook 僅分享圖片,而在其他地方則帶有連結。

3D Touch

這些寫在前面⋯⋯

最近剛從 iPhone 6 Plus 晉升到 iPhone X,其中一項硬體差異便是「3D Touch」;

而這也讓我花了一些時間,來加入 3D Touch 的相關功能開發。

這篇文章前半部會寫 3D Touch 在主畫面上的一些差異,

後半部才會補上 Swift 的相關寫法。

 

iOS 版本:11.1.1(15B150)

首先,先將 App 分成有 Today extension 以及沒有 Today extension 的這兩種:

郵件 – 有 Today extension

鬧鐘 – 沒有 Today extension

從這兩張圖的比較下,我們可以得知在 iOS 11.1.1 之中,

若有 Today extension 的 App,在 3D Touch 的快捷鍵之中,

會顯示 Today extension,而反之則僅會露出原本的 App icon。

另一點是,快捷鍵的順序是依 index 越小則越靠近 App icon

所以使用者將 App 放置在畫面上半部或下半部會影響由上到下的順序。

鬧鐘 – 放置畫面下半部

所以便不必太在意快捷鍵的排序問題,包含 Today extension 順序也是。

接著來看看程式碼⋯⋯

畫面上的快捷鍵,在 iOS 裡頭是 UIKit 裡頭的 UIApplicationShortcutItem

它提供了一種建構的方式:

其中 icon 的部分要使用 UIApplicationShortcutIcon 來建構,

而它有內建的 UIApplicationShortcutIconType 可以使用;

或者從你自行提供的 templateImageName 去抓取也行,

不過一個重點是它會使用 template 模式呈現,所以你無法在圖片中自行決定色彩。

再來再設定 UIApplication.shared.shortcutItems 即可完成新增 3D Touch 快捷鍵的畫面。

一個小小重點是,只要處理有關 UI 方面的設置,都需要在 Main thread 下完成。

 

畫面上有了快捷鍵之後⋯⋯

我們需要在 AppDelegate 裡頭攥寫點擊後的動作,

透過剛剛建構時,所定義的不同 shortcutItem.type 來決定要做些什麼事情,

這部分就有點像使用者點推播進來後要怎麼做後續一樣,

整體大致上就是這樣!

 

未完待續⋯⋯

還沒研究在 App 裡頭的 3D Touch 內容,待之後開發有需求會再補在這篇!

UIImage memory issue

今天要來分享的內容,是有關於 UIImage 的一個記憶體爆炸情況,

而我先闡述一下發現這問題的情境:

專案內有個功能會需要匯入大型圖片做縮放以及拖拉功能,

使用者可以切換大型圖片,而在點擊過多的圖片時,便會造成記憶體爆炸。

原先的做法

在使用者點擊叫出某張圖時,會使用

UIImage(name: ImageName) 來產生 UIImage 物件,並將畫面上的 UIImageView.image 設為它。

看起來蠻合理的,當使用者切換後,我會再生成一個新的 UIImage 物件,並取代前者;

這樣前者就應該會釋放掉記憶體空間了!

但⋯⋯事情並不是這樣發展

在使用者切換幾次後,發現記憶體只有一直往上增長,而未釋放掉;

意思是指雖然我將畫面上的 UIImageView.image 取代掉了,

不過實際上仍然佔據著記憶體空間⋯⋯

為什麼?

上網查了一下後,發現 UIImage(named: ImageName) 這種生成方式,

會自行將取出來的圖片放置到 cache;

而上述的使用情況就會變成當使用者一切換,便會將另一張大型圖片放置到 cache 而未釋放掉前一張。

改良的做法

Data 在建構的時候,有一種選項是 .uncached

也就是說,我們可以先將圖片以 Data 的方式打開,再轉回 UIImage,

則就可以避免掉它自動放置到 cache 而記憶體爆掉的情況。

這樣就可以解決 UIImage 的 cache 導致記憶體爆炸的情況。

 

題外話

至於圖片本身就已經大到放不進來,則可以先 resize 一下:

Swift 4 JSON

在 Swift 之中,JSON (JavaScript Object Notation) 的型別為 [String: Any]

也就是一個 Dictionary 的概念,以一組 Key 對應一組 Value。

大多數會碰到 JSON 格式是在做網路溝通時,

使用 JSON 格式並 encode 成 Data 的型態在傳輸資料。

Swift 3 以前

我們從 URLSession.shared.dataTask 的 handler 之中,

會得到 data、response error,其中的 data 會使用下面的方式才轉換成 JSON:

接著我們再來對 JSON 的 Key 和 Value 來進行拆解:

而巢狀式的 JSON 格式,寫起來的 code 就會越長,進而衍生出 SwiftyJSON,有興趣的可以自行看一下。

Swift 4

那 Swift 4 做了哪種改變呢?

在 Foundation 裡頭,增加了 Encoder/Decoder 相關的內容,

而這邊先以 JSON 作為主要的內容。

以 User 作為例子:

其中,Codable 是指 EncodableDecodable

若你只需要做 data parse 的話,也可以僅宣告 Decodable。