FirebaseDatabase – Read

之前有寫過 FirebaseDatabase REST API的文章,
而這篇則會是在 iOS 上的使用。

安裝套件

由於 Google 認為 Carthage 的方式不符合他們的使用模式,
畢竟 Firebase 的 framework 並非是開源的,
所以只有提供 CocoaPods 的安裝方式或是直接下載檔案;
而我這邊就以 CocoaPods 來安裝 Firebase 相關的套件,其他則用 Carthage 來管理。

設定

我們在 Firebase console 那先建立好專案並匯入 GoogleService-Info.plist,
如果你有多個 Target 要使用的話,建議放在不同的資料夾,並且設定好 Target Membership。
並且要注意 Firebase console 內的 Database rules,
若沒有做 auth 相關內容的話,記得要調整;
如我開放給 App 讀取但不可寫入的話:

{
  "rules": {
    ".write": "auth != null",
    ".read": true
  }
}

接著在 AppDelegate.swift 中加入

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    FirebaseApp.configure()
    // --**--
}

順道提醒一下,若要讓 Database 的資料在離線也能使用上一次的 cache 的話,
需要在 AppDelegate.swift 裡頭加入

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // --**--
    Database.database().isPersistenceEnabled = true
    // --**--
}

官方文件中沒有特別註記,但若在其他地方執行這段程式,則會直接報錯。

讀取資料

Firebase Database 所提供的是一個可監聽的資料庫,
來做到 Realtime Database 的效果;
Reference 便是 path 的概念,
假設我的資料長這樣:

{
    "test": {
        "user": {
            "name": "Archie"
        }
    }
}

則 ref 有幾種設法;

  • 觀察全部的資料: Database.database().reference.observe
  • 只看 test 下的異動: Database.database().reference.child("test").observe
  • 只看 user:Database.database().reference.child("test").child("user").observe

隨著資料的結構,我們可以讓每個地方只專注它需要監聽的部分就好。

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。

Status bar style

Status bar style

一般在 iOS 裡頭,status bar style 可以被分成兩種

  • default
  • lightContent

設定方式

我們可以在 info.plist 做全域的設定,讓整個 app 在執行的時候,都是同樣的 style;
這取決於
View controller-based status bar appearance - YES / NO
若設為 NO,則是整個 app 都會是同樣的 style;
而若設為 YES,就能在個別的 viewController 來做設定。

View controller-base status bar appearance - YES

實作方式是更改 UIViewController 底下的 preferredStatusBarStyle
而因為它是 { get } 的,所以我們只能透過覆寫的方式來更改:

UINavigationController

如果你要更改的 UIViewController 是包在 UINavigationController 裡頭的話,
則需要更改的是 UINavigationController preferredStatusBarStyle
而非是當前的 UIViewController

動態更改

當我們執行 setNeedsStatusBarAppearanceUpdate(),可以讓系統再去讀一次 preferredStatusBarStyle,並更改狀態;
所以若要做更改的動畫,則可以在 UIView.animated 裡頭執行 self.setNeedsStatusBarAppearanceUpdate()

ViewModel with POP

ViewModel with POP

這邊有兩個名詞需要解釋一下,一個是 ViewModel,另一個則是 POP

ViewModel

在 iOS 的開發模式中,從你新建一個新專案的時候,它的預設內容是以 MVC 的架構為底;
而在 MVVM 的架構中,會在 View 以及 Model 之間,多夾帶一層 ViewModel 來分工。
在實作 ViewModel 的時候,我是以 structure 的方式才建構 ViewModel,
並在裡頭宣告 init(model: Model) 的方式,來將 Model 轉成 ViewModel。

POP

POP(Protocol-Oriendted Programing)是以 Protocol 來傳遞的方式;
以 UITableViewCell 來說,我會建立一個:

然後讓 UserViewModel 或是相關的 CellViewModel 來遵守這個 protocol,
這樣 CellViewModel 都可以直接取用出相對應的 Cell!
UITableViewDataSource 的 cellForRowAt 就可以簡單一些:

iOS UIView shadow

最近在寫的專案,使用到比較多的陰影效果,就來列列有關陰影效果的一些內容。

CALayer

陰影相關的實作方式,是以 CALayer 底下的這五個變數來控制

  • shadowColor: CGColor?
  • shadowPath: CGPath?
  • shadowOffset: CGSize
  • shadowRadius: CGFloat
  • shadowOpacity: Float

shadowColor

預設值為不透明的黑色,我們可以透過這個參數來調整陰影的顏色。

shadowPath

預設值為 null,並支援 animated,可以在這邊調整陰影的路徑,
所以當 UIView 在透過 animated 調整大小時,我們可以一併調整 shadowPath 來跟著做變化。

shadowOffset

預設值為 [0, -3],來調整陰影的位移。

shadowRadius

預設值為 3,系統本身會進行發散的動作。

shadowOpacity

預設值為 0,也就是完全透明;
所以如果只是要陰影,並沒有需要客製化的話,
只需要修改 shadowOpacity,畫面上就會有 offset = [0, -3], radius = 3 的不透明黑色陰影出現。
 

cornerRadius

這邊有點需要注意到的是,若要有 shadow 效果的話,下面兩者必須為 false

  • layer.masksToBounds
  • clipsToBounds

因為若將 layer 只關閉在 bounds 的話,則無法呈現陰影的效果;
但偏偏若需要圓角效果的話,則必須更改為 true
而這邊,我的做法是,
先建立一個透明色的 UIView,來做陰影的效果,
再疊加一層 subview 來做圓角的效果,
這樣便可以做出有陰影效果的圓角 UIView 了!