小時候很喜歡Indiana Jones系列的電影, 對於它裡面的地圖片段也一直覺得很有趣

如果這樣的動畫, 用在遊記類的blog上, 應該也蠻酷的, 但好像也沒一個比較好的工具, 因此想說用MapKit來實作一個試試

功能需求

先來定義一個簡單的功能需求

  1. 在起點跟終點畫一條連結線
  2. 一架飛機延這條線飛到終點
  3. 地圖視角跟著飛機走

實作

在起點跟終點畫一條連結線

這部份要用到MKGeodesicPolyline, 給它兩個點, 它就會自動連結成一條線, 但這條線並不是完美的直線, 因為地球表面是曲面的, 所以它是一條弧線

let coords = [start, end]

geodesicPolyline = MKGeodesicPolyline(coordinates: coords, count: 2)

print(geodesicPolyline!.pointCount)
mapView.add(geodesicPolyline!)

這邊coors只要給訂起始點跟終點的位置就好, 印出pointCount就會發現它把經由的點都補足了(實際上印出來的會多出2很多很多)

MKGeodesicPolyline裡的參數coordinates是一個UnsafeMutablePointer, 在Swift 3之前要寫成&coords, 但在Swift 3大改之後, “&“就不需要了

由於MKGeodesicPolyline是一個Overlay, 因此最後只需要用mapView.add (Swift 3之前是addOverlay)加入mapView就可以了, 但加入之後, 會發現, 這條線根本沒被畫出來, 那是因為少寫了一部分

在MapView裡面要畫出Overlay, 就必須要跟MapView說怎麼畫出這個Overlay, 這就要實作MKMapViewDelegate裡的mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay)

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
    guard let polyline = overlay as? MKPolyline else {
        return MKOverlayRenderer()
    }
    
    let renderer = MKPolylineRenderer(polyline: polyline)
    renderer.lineWidth = 3.0
    renderer.alpha = 0.5
    renderer.strokeColor = UIColor.blue
    
    return renderer
}

由於我們是要畫Poly line, 因此這邊回傳給它一個MKPolylineRenderer, 線寬是3.0, 線的顏色是藍色(alpha = 0.5)

這樣就可以很完美的畫出那條線了

在地圖上畫出飛機

這部份就要借重到MKPointAnnotation

let thePlane = MKPointAnnotation()
thePlane.coordinate = start //給定起始座標
        
mapView.addAnnotation(thePlane)

一樣, 這邊如果沒告訴MapView怎畫這Annotation, 它是會用預設的取代, 因此我們一樣要去實作MKMapViewDelegate

func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
    let planeIdentifier = "Plane"
    
    let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: planeIdentifier)
        ?? MKAnnotationView(annotation: annotation, reuseIdentifier: planeIdentifier)
    
    annotationView.image = UIImage(named: "ic_flight_48pt")
    
    return annotationView
}

這邊用一個UIImage來指定飛機的圖標

地圖視角

把飛機置於地圖正中央, 我們才看得到他, 因此, 需要設定可視的區域, 包含中心點跟範圍, 如下:

let span = MKCoordinateSpanMake(8.0, 8.0)
let region1 = MKCoordinateRegion(center: start, span: span)
self.mapView.setRegion(region1, animated: true)

沿著線飛

這時候要利用到 perform(#selector(updatePlane), with: self, afterDelay: 0.4) 讓它每隔0.4秒去更新一次飛機位置, 直到到終點為止, 當然也要一直更新region, 以讓飛機維持在正中央

func updatePlane() {
    planePositionIndex = planePositionIndex + step;
    
    if (planePositionIndex >= geodesicPolyline!.pointCount)
    {
        //plane has reached end, stop moving
        planePositionIndex = 0
        return;
    }
    
    let s = 8.0
    
    let nextMapPoint = geodesicPolyline?.points()[planePositionIndex]

    thePlane.coordinate = MKCoordinateForMapPoint(nextMapPoint!) 
    mapView.region = MKCoordinateRegionMake(thePlane.coordinate, MKCoordinateSpan(latitudeDelta: s, longitudeDelta: s))

    perform(#selector(updatePlane), with: self, afterDelay: 0.4)
}

結果

這邊有幾個缺點尚待改進

  1. 飛機閃爍
  2. 如果把可視區域範圍縮小, 或是位置更新過快, 就會造成地圖來不及載入的現象(可能需要預跑幾次將map tile載入cache內)
  3. 飛機頭永遠保持往上, 應該要朝著線方向轉

完整程式碼

承續上篇的用icon font來製作圖示, 之前所提到的都是利用現成的icon font, 但似乎大部分的icon font都沒有像material icon有支援ligatures, 沒支援的話, 在xcode裡面就無法像上一篇一樣, 直接在UI designer顯示對應的圖示, 另外如果需要使用自己的圖示呢?其實是有方法用SVG圖檔來製作自己的icon font的, 這篇就來介紹兩種用SVG圖檔製作一個有ligatures支援的字型檔

grunt-webfont

第一個方法就是利用grunt-webfont, Grunt是一個前端常用的建構工具, 而grunt-webfont是一個用來產生字型的task

安裝相關工具

由於需要使用Grunt, node.js是必須的, 另外由於需要使用到fontforge, 所以python也是必須的, 雖然說grunt-webfont也可以純nodejs的module來產生字型, 但那並無法支援ligatures, 所以fontforge是一定需要的

npm i grunt --global來安裝grunt

製作字型
  1. 建立一個空的目錄
  2. 在這個目錄執行npm init來產生package.json
  3. npm i grunt-webfont --save來安裝grunt-webfont並且把這個dependency 加到package.json
  4. 建立一個svg子目錄(目錄名稱隨你高興, 這邊以svg當例子), 把所有圖示的svg檔案全部放到這目錄去
  5. 建立Gruntfile.js , 這檔案就像是Makefile, 或像是build.gradle這樣的角色, 內容就像下面
module.exports = function(grunt) {

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    webfont: {
        icons: {
                src: 'svg/*.svg',
                dest: 'build/fonts',
                options: {
                        engine: 'fontforge',
                        htmlDemo: true,
                        fontHeight: 96,
                        normalize: false,
                        ascent: 84,
                        descent: 12,
                        font: 'octicon',
                        fontFamilyName: 'octicon',
                        types: 'ttf',
                        ligatures: true,
                        startCodepoint: 0xF101
                    }
                }
    },
    clean: [
        'build/fonts/*'
      ]
  });

  grunt.loadNpmTasks('grunt-contrib-clean');
  grunt.loadNpmTasks('grunt-webfont');
  grunt.registerTask('font', ['clean', 'webfont']);
  grunt.registerTask('default', [ 'font']);
};

這邊最主要也最重要的task就是webfont這個, 這裡面src是svg檔的目錄, dest是字型輸出的目錄, engine的部分指名fontforge, ligatures設定必須要是true(產生的字型的ligature的名字其實就是沿用svg的檔名)

建立好這個檔後執行grunt即可

icomoon

上面的方法還是有點麻煩, 蠻手動的, 還有一個更方便的工具就是icomoon, 這東西方便更多, 它是一個相當強大的工具

Icomoon

從畫面上看, 它其實很簡單操作, 選定你所需要的圖示後, 按右下角的Generate Font即可, 除了你可以自己import自己的svg檔案外, 它也提供很多付費跟免費的圖示供選用:

Icomoon

按下Generate Font後, 並不會馬上讓你下載字型回家, 它會先讓你檢視字型將會包含的圖示, 這邊有件事很重要, 左上角有個fi圖示(參照下圖), 按下去後, 下面的圖示下面會多一個fi的欄位, 這就是讓你設定這些圖示的ligature的, 如果需要一個有支援ligature的字型, 就需要去設定這邊

Icomoon

所有都沒問題後按下右下角的Download就沒問題了

寫App有一個讓人頭痛的是App大小的問題, 而這大小有部分是由App裡面所用的圖所貢獻, 為了減少這部份消耗掉的資源, 不管是用較大壓縮率的格式來壓縮圖檔, 還是其他, 大家都想盡辦法想解決這問題, 現在由於平面化的UI設計, 使得又有不錯的方法來解決這問題, 平面化UI設計的特色是大部分的圖檔都是單色而非五顏六色, 這使得用向量圖, 甚至用字型來解決這問題變得可行

免費的圖標字型(icon font)

把所有的向量圖示變成字型檔可以節省不少空間, 以流行的Font Awesome 來說, 它包含了634個圖標, 卻只佔了153KB, 這在以往可能是不到十個圖標的檔案就會達到的大小, 相較之下節省了不少空間, 像這樣開放的圖示字型, 可以找到不少:

  • Font Awesome : 蠻流行的一個開放icon組, 提供了ttf, woff等字型檔格式
  • Google material icons : Google開放源碼的免費icon組, 它不只提供Android, iOS可使用的圖檔外, 也提供了字型檔的部分, 而且它的字型檔支援了Ligatures (後面會再提到它好用的地方),這也使得它比Font Awesome來的好用
  • Weather Icons : 顧名思義, 這提供了222個可以用於表示天氣的icons, 不過對於風向的表示的部分, 它是用同一個圖示只是在web上利用css旋轉來顯示不同方向的風, 這一點應用到App上的話, 我是還沒找到比較適合表達的方式
  • Octicons : 由GitHub開源出來的圖標字型, 圖標不多, 但自己新增應該蠻方便的(自己增加svg檔用grunt去build)

除了這些之外, 應該還可以找到不少免費的圖標字型(icon font)可以用, IconFontKit這邊就列了不少(它也整合了)可用的圖標字型

使用這些, 除了可以節省app的大小, 也可以省下不少設計圖標的時間, 但也不是沒缺點, 因為是字型的關係, 它每一個icon都是對應到一個unicode字元, 這字元大多數跟icon的形狀沒關係, 也就不是那麼好對應, 通常都要查一下對照表找出字元碼

利用現成的framework整合

要在iOS上使用這些圖標字型(icon font)的方式好幾種,寫程式去load字型是一種, 當然就有不少大德, 寫好包裝可以讓你用cocoapods或是cathage直接引入, 這邊有幾個不錯的:

用這些現成的framework的好處是, 一來減去自己手動包裝字型進app的複雜度, 二來是, 這些已經幫你定義好一些對應圖標的常數, 讓你用比較方便的方式而不是記憶unicode字元來對應這些圖標

但它也是有缺點的, 大部分這些的作法都是runtime才去載入跟註冊字型, 因此你必須是在程式內設定你的UILabel, UIButton的字型, 無法事先就在Interface Builder做預覽, 所以個人比較喜歡的方式就是自己動手來

手動在xcode上使用自訂字型

自己手動加的好處就是, Interface Builder上就可以套用, 直接就可以看到結果, 但就是稍微繁瑣了一點

Interface Builder直接看結果

首先, 要把字型檔拖入你的Project裡面:

ttf files in project

接著打開Info.plist, 加上一個新的項目叫做*“Fonts provided by application”*

這個是一個陣列(Array), 它的內容就是你要加入的字型檔檔名, 把你要加的每一個都列進去

Info.plist

接著, 在Interface Builder裡你所要使用icon font的地方, 比如說UILabel設定你的字型, 原本的字型是設定為*“System”, 把它改成“Custom”*, 並選定你所需要的字型名稱, 例如FontAwesome, 要注意的是, 字型名稱不一定等同於你字型檔的名字:

Interface builder

Interface builder

接下來在Text的部分輸入這個圖示的代表的Unicode字元就好, 不是Unicode碼, 而是那個字元本身, 這挺不方便的, 可能用copy paste的才有辦法, 這是這個方法最大的缺點

這問題還是有方法克服, 這也就是前面為何提到會比較推薦使用Google material icons而不是Font Awesome , 這原因就是Ligatures

Ligatures

Ligatures是一個字型上蠻方便的特色的, 關於Ligatures可以先看一下這篇, 這是在Google material icons提到的一篇文章:

The Era of Symbol Fonts

剛剛提到的一個很大的缺點是, 你要知道圖示對應的Unicode碼才可以在你的UI上顯示你想要的圖示, 這相當不方便, 尤其那些Unicode碼可能根本完全不代表任何意義

比較人性點的作法是當你想要一個圖示代表藍芽, 用bluetooth就可以找到對應圖示, 而Ligatures就是一個這樣的存在

我們先來看看, 如果使用沒有而Ligatures的FontAwesome, 你在Text打上**“Contacts”**會是怎樣一個情形?

Ligatures1

它會直接一字不漏的呈現**“Contacts”**,這還是因為FontAwesome有包含原本英數字字型在裡面, 有些其他的自行更慘, 根本就是一片白

讓我們再看看用Google material icons的字型,同樣的東西會有什麼結果

Ligatures2

因為這個字型有支援Ligatures, 所以在這邊contacts就會被直接代換成它對應的圖示了, 我們就不用寄那種完全看不懂的unicode碼了

但大部分的字型其實也都沒有, 所以自訂字型該怎做?那就留待之後研究了

去年為了參加WWDC, 開始練了Swift, 寫了兩個library, 不過好像一直都沒寫過完整的App, 連UI好像都沒真的去刻過(去年寫的東西跟UI比較無關), 因此最近利用了一些時間開始了個side project, 做side project就常常會把時間花在一些枝微末節的地方, 比如說, 為了做一個像Android那樣的Floating action button, 去找來一個現成的3rd party lib - yoavlt/LiquidFloatingActionButton , 那個像水珠一樣突出去的效果, 我還蠻愛的:

但缺點是, 後面缺一個擋住背景元件的, 以致於要去點伸上來的小按鈕容易誤按後面的元件, 因此就想自己改一個後面多一個overlay的版本, 當然也不想隨便貼一張白白的就交差, 起碼要像這樣:

這邊不是單純蓋一個深色半透明的背景而已, 還需要作一點模糊的部分

在iOS上(iOS8 之後), 作這樣的東西很簡單, 只要利用UIVisualEffectViewUIBlurEffect這兩個東西, 寫法很簡單:

    let blurEffect = UIBlurEffect(style: .Dark)
    let uiEffectView = UIVisualEffectView(effect: blurEffect)
    uiEffectView.frame = overlayView.bounds
    overlayView.addSubview(uiEffectView)

UIBlurEffect有三種樣式, Dark, Light, 和ExtraLight, 上面的範例是Dark, 蠻適合用在這地方的, 利用這個方法就可以不用自己擷取screenshot再算模糊化了

(通車的路上寫blog最討厭碰到網路出狀況呀!) KVO (Key Value Observing) 是用來監控一個變數是否有改變的技巧, 在做UI的data binding來說是蠻有用的, 舉個例子, 如果有個Label是一直顯示網路蒐集過來的讀數會一直變化, 一般的作法是收到這讀數再直接去操作設定這個label, 缺點是會把這類的資料邏輯跟UI邏輯綁死, 而且如果是有多個以上要跟這資料連動, 較不易擴充 (還沒完全醒, 例子舉得很爛), KVO提供一個比較好的方式利用監控數值變化來處理這件事

網路上找到一篇文章, 或許這篇解釋的比我好: 了解 Objective-C 上的 KVO(Key-Value Observing) 機制

不過這篇主要是針對Objective C的, 觀察者利用實作observerValueForKeyPath來得到狀態, 而被觀察者利用了addObserver加入觀察者

當數值變動時, 必須要透過willChangeValueForKey 和 didChangeValueForKey 來通知觀察者, 這點算比較麻煩, Swift則不需要自己去呼叫, Swift在語言本身來說就已經設計了監看變數變化的特性, 最簡單的方式是透過willSet和didSet, 例如:

  var myVar:Int {
     willSet {   
       ....
     }
     didSet {
       ....
     }     
  }

不過這樣其實還沒達到KVO的效果, 只算是一個hook來處理數值變化時要做的事, 在Swift中也是透過addObserver和observerValueForKeyPath, 唯一不同的是不需要willChangeValueForKey 和 didChangeValueForKey, 基本上只要把變數宣告成dynamic就好

dynamic var myVar: String = "ssss"

這邊很重要的是一定要宣告成dynamic, 不然會沒效果喔!

(剛好到站, 寫完下車)