Info.plist localized

一些需要權限的功能都會需要在 Info.plist 裡頭加上說明,而這個說明要實作多語系則是建立一個 InfoPlist.strings(沒錯,名字就是這樣)

然後對相對應得 Key 填寫翻譯,像是:

NSLocationWhenInUseUsageDescription = "說明填寫";

就可以在要使用位置的說明處顯示多語系的結果了。

至於其他的 Key 可以從CocoaKey 官方文件 以及CoreFundationKey找找。

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

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

DeviceSupport

每當 iOS Beta 更新時,原先的 Xcode  便會無法支援,需要透過從 Xcode Beta 的 DeviceSupport 複製新的版本到正式版之中才能使用;

反正都會做這件事,不如就將 Xcode Beta 裡頭的 DeviceSupport 上傳到 GitHub 上提供給 iOS 有更新,但還沒下載新的 Xcode Beta 的人使用吧!

傳送門點我

ProvisionedDevices

前言

這篇文章的內容會是記錄如何確認目前的 Provisioning Profile 擁有哪些測試裝置,
以便在使用 adhoc 打包時,確保裝置可以執行。

Provisioning Profile Path

~/Library/MobileDevice/Provisioning Profiles/

在 terminal 下執行

security cms -D -I /path/to/MyProfile.mobileprovision

便可以看到相關的資訊,如下圖:

iamge

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 僅分享圖片,而在其他地方則帶有連結。