[譯]開發者眼中 iOS 11 都更新了什麼?

Swants發表於2017-07-07

蘋果在 2017 年全球開發者大會上公佈了 iOS 11 , 其加入許多強大的功能,如 Core ML,ARKit,Vision,PDFKit,MusicKit 拖放等等。 我嘗試著把主要變化在接下來的文章裡總結了出來,並在可行的地方提供程式碼,這樣你就可以直接上手。

注意: 有些地方沒涉及到並不是因為懶,我已經盡我所能提供足夠多的程式碼來幫你在應用上快速上手這些特性。但是你最終還是免不了去額外瞭解更多 iOS 11 中大量複雜的設計功能。

在接著讀下去之前,你可能需要了解下這幾篇文章:

你可能想購買我的新書:《 Practical iOS 11 》。 你可以通過教程的形式獲得 7 個完整的專案程式碼,以及更多深入瞭解特定新技術的技術專案 - 這是熟悉 iOS 11最快的方式!

Buy Practical iOS 11 for $30

拖放

拖放是我們在桌面作業系統中認為理所當然的操作,但是拖放在 iOS 上直到 iOS 11 才出現,這真的阻礙了多工處理的發展。換句話說,在 iOS 11 上尤其是在 iPad 上,多工處理迎來了高速發展的時代。得益於拖放成為其中很大的一部分:你可以在 APP 內部和或 APP 之間移動內容,當你拖放的時候你可以用另一隻手對其他 app 進行操作.你甚至可以利用 全新的 dock 系統來啟用其他 app 的中間拖動。

注意: 在 iPhone 上拖放被限制在單個 app 內 —— 你不能把內容拖放到其他 app 裡。

令人欣喜的是,UITableViewUICollectionView 在一定程度上都支援拖拽內建。但是想要使用拖放功能仍舊需要寫相當多的程式碼。你也可以向其他元件新增拖放支援,而且你會發現實際上這隻需要少量的工作。

下面讓我們來看看如何使用簡單的拖放來實現在兩個列表之間拷貝行內容。首先,我們需要使用一個簡單的 app 。讓我們寫一些程式碼來建立兩個有示例資料的 tableview 供我們拷貝。

在 Xcode 內建立一個新的單一檢視 app 模板,然後開啟 ViewController.swift 類進行編輯。

現在我們需要在這裡放上兩個含有示例資料的 tableView 。我不打算使用 IB 的方式佈局, 因為全部使用程式碼來實現是更清楚的。順便提一下,我 不打算 詳細地解釋程式碼,因為這都是現成的 iOS 程式碼,我不想浪費你的時間。

這些程式碼將:

  • 建立兩個 tableView ,並且建立兩個分別包含LeftRight 元素的字串陣列。
  • 制定兩個 tableView 都使用 view controller 來作為它們的資料來源,給他們寫死位置寬高,註冊一個可重用的 cell ,把它們兩個都新增到這個 view 上。
  • 實現 numberOfRowsInSection 方法,確保每個 table view 都根據其字串陣列有正確的行數。
  • 實現 cellForRowAt 來排列,這時 cell根據 table 來從兩個字串陣列中選出對應的資料來源正確展示。

然後,這是 iOS 11 之前的所有程式碼,應該沒有你不熟悉的程式碼。將 ViewController.swift 類的內容用下面的程式碼替換:

import UIKit

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    var leftTableView = UITableView()
    var rightTableView = UITableView()

    var leftItems = [String](repeating: "Left", count: 20)
    var rightItems = [String](repeating: "Right", count: 20)

    override func viewDidLoad() {
        super.viewDidLoad()

        leftTableView.dataSource = self
        rightTableView.dataSource = self

        leftTableView.frame = CGRect(x: 0, y: 40, width: 150, height: 400)
        rightTableView.frame = CGRect(x: 150, y: 40, width: 150, height: 400)

        leftTableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        rightTableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")

        view.addSubview(leftTableView)
        view.addSubview(rightTableView)
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if tableView == leftTableView {
            return leftItems.count
        } else {
            return rightItems.count
        }
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)

        if tableView == leftTableView {
            cell.textLabel?.text = leftItems[indexPath.row]
        } else {
            cell.textLabel?.text = rightItems[indexPath.row]
        }

        return cell
    }
}複製程式碼

好:下面就是 的內容了。如果你現在執行 app 你就會看到兩個並列並且填滿資料的 tableView 。我們現在想要做的就是讓使用者可以從一個 table 上選擇一行並且複製到另一個 table 裡,或者反方向操作。

第一步就是就是設定兩個 tableView 的拖和放操作的代理為當前 view controller ,再把它們設定為可拖放。 最後把下面的程式碼加入到 viewDidLoad() 方法裡:

leftTableView.dragDelegate = self
leftTableView.dropDelegate = self
rightTableView.dragDelegate = self
rightTableView.dropDelegate = self

leftTableView.dragInteractionEnabled = true
rightTableView.dragInteractionEnabled = true複製程式碼

當你做完這些後,Xcode 會丟擲幾個警告,因為我們當前的控制器類沒有遵從 UITableViewDragDelegateUITableViewDropDelegate 協議。通過給我們的類新增這兩個協議很容易就修復這些警告了 —— 滾動到檔案的最頂端並且改變類的定義:

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UITableViewDragDelegate, UITableViewDropDelegate {複製程式碼

但是這樣又會產生新的問題:我說過我們應該遵從這兩個新協議,但是我們沒有實現協議必須實現的方法,在過去修復這個常常是很麻煩的,但是 Xcode 9 可以自動完成這幾個協議必須實現的方法 —— 點選報紅色高亮程式碼行上的數字 2,這時你將會看到出現了更多的詳細解釋。點選 "fix" 來讓 Xcode 9 為我們插入兩個缺少的方法 —— 你將會看到你的類裡邊出現了下面的程式碼:

func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
    code
}

func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
    code
}複製程式碼

Xcode 總是把新的方法插在你的類最上面,至少在這次初始的 beta 版本里是。如果你和我一樣看這不順眼 —— 在繼續之前可以把它們移到更明智地方!

itemsForBeginning 方法是最簡單的,讓我們先從它開始。這個方法是在當使用者的手指在 tableView 某行 cell 上按下執行拖的操作的時候呼叫。如果你返回一個空陣列,你實際上就是拒絕了拖放操作。

我們打算為這個方法新增四行程式碼:

  1. 指出哪一個字串被拷貝,我們可以使用一個簡單的三元操作符來實現:如果當前的 tableView 是在左邊就從 leftItems 中讀取,否則就從 rightItems 中讀取。
  2. 試著將這個字串轉換成一個 Data 物件, 以便可以通過拖放進行傳遞。
  3. 將這個 data 放進一個 NSItemProvider 中,並且標記為儲存了一個純文字字串從而其他 app 可以知道如何去處理它。
  4. 最後, 把這個 NSItemProvider 放進一個 UIDragItem內,從而它可以用於 UIKit 的拖放。

為了把 data 元素標記為純文字字串 我們需要引入 MobileCoreServices 框架,所以請把下面的程式碼加入到 ViewController.swift 檔案最上面:

import MobileCoreServices複製程式碼

現在用下面的程式碼替換你的 itemsForBeginning 方法:

func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
    let string = tableView == leftTableView ? leftItems[indexPath.row] : rightItems[indexPath.row]
    guard let data = string.data(using: .utf8) else { return [] }
    let itemProvider = NSItemProvider(item: data as NSData, typeIdentifier: kUTTypePlainText as String)

    return [UIDragItem(itemProvider: itemProvider)]
}複製程式碼

接下來我們只需要實現 performDropWith 方法。我說 “只需要”,但是剩下的兩個潛在的複雜問題還是很棘手的。首先,如果有人拖放了很多東西我們就會同時獲得很多字串,我們需要把它們都正確插入。其次,我們可能被告知使用者想要插入到哪幾行,也可能不被告知 —— 使用者可能只是把字串拖放到 tableView 的空白處,這時需要我們決定該怎麼處理。

要解決這兩個問題需要寫比你期望中的更多的程式碼,但我會帶你一步一步編寫程式碼,讓它更容易些。

首先,是最簡單的部分:找出行被拖放到哪裡。 performDropWith 返回一個 UITableViewDropCoordinator 類物件,該物件有一個 destinationIndexPath 屬性 可以告訴我們使用者想把資料拖放到哪裡。然而 這個方法是 可選 實現:如果使用者把他們的資料拖放到我們 tableView 的空單元格上,方法返回的將會是 nil 。如果這真的發生了我們會認為使用者是想把資料拖放到 table 的最尾部。

所以,把下面的程式碼新增到 performDropWith 方法內繼續吧:

let destinationIndexPath: IndexPath

if let indexPath = coordinator.destinationIndexPath {
    destinationIndexPath = indexPath
} else {
    let section = tableView.numberOfSections - 1
    let row = tableView.numberOfRows(inSection: section)
    destinationIndexPath = IndexPath(row: row, section: section)
}複製程式碼

正如你所看到的那樣,如果 coordinator 的 destinationIndexPath 存在就直接用,如果不存在則建立一個最後一組最後一行的 destinationIndexPath

下一步就是讓拖放的 coordinator 來載入拖動的所有特定類物件。在我們的例子裡這個特定類是 NSString 。(然而,通常用 String 不起作用。)當所有拷貝的內容都就緒時我們需要傳送一個閉包來執行,這也是最複雜的地方:我們需要把內容一個接一個地在目標行下面插入,修改 leftItemsrightItems 陣列,最後呼叫我們 tableView 的 insertRows() 方法來展示拷貝後的結果。

那麼,接下來:我們剛剛寫了一些程式碼來指出拖放操作最終的目標行。但如果我們得到了 多個 拷貝物件,那麼我們所有的都是初始的 destination index path —— 第一個拷貝物件的目標行就是它,第二個拷貝物件的目標行比它低一行,第三個拷貝物件的目標行比它低兩行,等等。當我們移動每個拷貝物件時,我們會建立一個新的 index path 並且把它暫存到一個 indexPaths 陣列中,這樣我們就可以讓 tableView 只呼叫一次 insertRows() 方法就完成了全部插入操作 。

把程式碼新增到你的 performDropWith 方法中,放在我們剛才寫的程式碼下面:

// attempt to load strings from the drop coordinator
coordinator.session.loadObjects(ofClass: NSString.self) { items in
    // convert the item provider array to a string array or bail out
    guard let strings = items as? [String] else { return }

    // create an empty array to track rows we've copied
    var indexPaths = [IndexPath]()

    // loop over all the strings we received
    for (index, string) in strings.enumerated() {
        // create an index path for this new row, moving it down depending on how many we've already inserted
        let indexPath = IndexPath(row: destinationIndexPath.row + index, section: destinationIndexPath.section)

        // insert the copy into the correct array
        if tableView == self.leftTableView {
            self.leftItems.insert(string, at: indexPath.row)
        } else {
            self.rightItems.insert(string, at: indexPath.row)
        }

        // keep track of this new row
        indexPaths.append(indexPath)
    }

    // insert them all into the table view at once
    tableView.insertRows(at: indexPaths, with: .automatic)
}複製程式碼

這就是完成的所有程式碼了 —— 你現在能夠執行這個 app 並且在兩個 tableView 之間拖動行內容來完成拷貝。完成這個花費了這麼多的工作量,但令人感到驚喜的是:你所做的這些工作你能夠支援整個系統的拖放:譬如如果你試著用 iPad 模擬器的話,你就會發現你可以把這些文字拖放到 Apple News 內的任何一個列表上,或者把 tableView 上的文字拖放到 Safari 的搜尋條上。非常酷!

在你試著去完成拖放操作之前,我想再展示一件事:如何實現為其他 View 新增拖放支援。其實比在 tableView 上實現要容易,那就讓我們快速做一遍吧。

在開始之前,我們需要一個簡單的控制元件來讓我們有可以新增拖放的東西。這次我們打算建立一個 UIImageView 並且渲染一個簡單的紅色圓圈作為圖片。你可以保留已存在的單檢視 APP 模板 並把 ViewController.swift 的內容用新程式碼替換:

import UIKit

class ViewController: UIViewController {
    // create a property for our image view and define its size
    var imageView: UIImageView!
    let size = 512

    override func viewDidLoad() {
        super.viewDidLoad()

        // create and add the image view
        imageView = UIImageView(frame: CGRect(x: 50, y: 50, width: size, height: size))
        view.addSubview(imageView)

        // render a red circle at the same size, and use it in the image view
        let renderer = UIGraphicsImageRenderer(size: CGSize(width: size, height: size))
        imageView.image = renderer.image { ctx in
            let rectangle = CGRect(x: 0, y: 0, width: size, height: size)
            ctx.cgContext.setFillColor(UIColor.red.cgColor)
            ctx.cgContext.fillEllipse(in: rectangle)
        }
    }
}複製程式碼

像之前一樣,這都是些 iOS 的老程式碼所以我不打算給你詳細解釋它。如果你試著在 iPad 模擬器上執行,你就會在控制器裡看到一個大的紅色圓圈 —— 這對供我們測試來說足夠了。

自定義檢視的拖放是通過一個新的叫作 UIDragInteraction 類來實現的。 你告訴它在哪裡傳送資訊(在我們這個例子裡,我們用的是當前的控制器),然後將它和用來互動的 View 繫結。

重要提示: 千萬不要忘了開啟相關檢視的互動,否則當拖放最後不起作用時,你會感到非常困惑。

首先, 在 viewDidLoad() 的最末尾新增這三行程式碼,就在之前的程式碼後面。你就會看到 Xcode 提示我們的 View Controller 沒有遵循
UIDragInteractionDelegate 協議,所以把類的定義改成下面這樣:

class ViewController: UIViewController, UIDragInteractionDelegate {複製程式碼

Xcode 將會繼續提示我們沒有實現 UIDragInteractionDelegate 協議的一個必要方法,所以重複之前我們所做的 —— 在出錯行上單擊錯誤提示,然後選擇 "Fix" 來插入下面的程式碼:

func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {
    code
}複製程式碼

這就像我們之前為我們的 tableView 實現的 itemsForBeginning 方法一樣:當使用者開始拖動我們的 imageView 的時候,我們需要返回我們想要分享的影象。

這些程式碼是非常好並且簡單的:我們會使用 guard 來防止我們在 imageView 上拉取圖片時出現問題,先用一個 NSItemProvider 包裝 image,然後返回資料的時候再使用 UIDragItem 包裝下。

itemsForBeginning 方法用下面的程式碼替換:

func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {
    guard let image = imageView.image else { return [] }
    let provider = NSItemProvider(object: image)
    let item = UIDragItem(itemProvider: provider)
    return [item]
}複製程式碼

這就完成了! 嘗試使用 ipad 多工處理功能來將相簿放在螢幕的右端 —— 你能夠通過拖放圖片來將圖片從你的 APP 拷貝到相簿裡。

擴增實境

擴增實境 (AR) 已經出現有一段時間了,但是蘋果在 iOS 11 上做了一些可圈可點的事情:他們創造了一個卓越的實現就是讓 AR 開發可以和現有的遊戲開發技術無縫整合。這就意味著你不需要做太多的工作就能把你 SpriteKit 或 SceneKit 技能和 AR 整合起來,這是個非常誘人的前景。

Xcode 自帶了一個非常棒可以立即使用的 ARKit 模板,因此我鼓勵你去嘗試一下 —— 你會驚奇地發現實現它是多麼的容易!

我想快速地演示下模板的使用,這樣你就可以瞭解到這一切是如何融合在一起的。首先,使用虛擬現實模板建立一個新的 Xcode 工程,然後選擇 SpriteKit 作為內容技術。是的,SpriteKit 是一個 2D 框架,但它仍能夠在 ARKit 中用得很好,因為它可以像 3D 一樣通過扭曲或旋轉來展示你的精靈。

如果你開啟了 Main.storyboard ,你會發現這個 ARKit 模板與普通的 SpriteKit 模板有所不同:它使用了一個新的 ARSKView 介面物件,將 ARKit 和 SpriteKit 兩個世界融合在一起。這個物件通過一個 outlet 和 ViewController.swift 連線在一起,在這個控制器中的 viewWillAppear() 方法中構建 AR 追蹤,並在 viewWillDisappear() 方法中暫停追蹤。

但是,真正起作用的是在兩個地方:Scene.swift 檔案的 touchesBegan() 方法內,和 ViewController.swift 檔案的 nodeFor 方法。 在通常的 SpriteKit 中你建立節點並把節點直接新增到你的場景中,但是使用 ARKit 後建立的是 錨點 —— 包含場景位置和識別符號的佔位,但它沒有實際的內容。根據需要的時候使用 nodeFor 方法轉換為 SpriteKit 節點。如果你曾使用過 MKMapView ,會發現這和 MKMapView 新增大頭針和標註的方式是類似的 —— 標註是你的模型資料,大頭針是 view。

在 Scene.swift 類的 touchesBegan() 方法你會看到從 ARKit 拉出當前幀的程式碼,先計算放入一個新敵人的位置。這是通過矩陣乘法實現:如果你建立一個單位矩陣(表示位置 X:0, Y:0, Z:0 的東西),再將它的 Z 座標移回 0.2(相當於 0.2 米),你可以乘以當前場景相機位置來實現向使用者指向的方向移動。

所以,當使用者指向前方錨點就會被放在前方,如果他們指向上方,錨點就會放在上方。一旦錨點被放在那,它就會呆在那:ARKit 將會自動移動,旋轉或扭曲來確保當使用者的裝置移動時與錨點始終正確對齊。

所有的操作可以用三行程式碼來實現:

var translation = matrix_identity_float4x4
translation.columns.3.z = -0.2
let transform = simd_mul(currentFrame.camera.transform, translation)複製程式碼

一旦計算出來轉換,位移就會包裝成一個錨點並新增到回話中,就像這樣:

let anchor = ARAnchor(transform: transform)
sceneView.session.add(anchor: anchor)複製程式碼

最後會呼叫 ViewController.swift 類的 nodeFor 方法。之所以會呼叫是因為當前 ViewController 被設定成了 ARSKView 的代理,
當前 ViewController 就會在需要的時候負責把錨點轉換成節點。你 不需要 擔心定位這些節點:記住,錨點已經放置到真實世界的具體座標上了,ARKit 負責對映錨點的位置並轉換成 SpriteKit 節點。

總之,nodeFor 方法很簡單:

func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
    // Create and configure a node for the anchor added to the view's session.
    let labelNode = SKLabelNode(text: "Enemy")
    labelNode.horizontalAlignmentMode = .center
    labelNode.verticalAlignmentMode = .center
    return labelNode;
}複製程式碼

如果你想知道,ARKit 錨點有一個 identifier 屬性可以讓你知道建立了什麼樣的節點。在 Xcode 模板中所有的節點都是未知的。但是在你自己的工程中你幾乎肯定會想把事物唯一標識出來。

就是這些!這麼少的程式碼帶來的結果是非常有效的 —— ARKit 註定是一個大的飛躍。

插播廣告

如果你喜歡這篇文章,你可能對我新寫的 iOS 11 實踐教程新書感興趣。你將會實際開發基於 Core ML , PDFView , ARKit , 拖拽等更多新技術的工程。 —— 這是學習 iOS 11 最快的方式!

Buy Practical iOS 11 for $30

PDF 渲染

自從 OS X 10.4 開始受益於幾乎不需要提供任何程式碼就可以提供 PDF 渲染,操作,標註甚至更多的 PDFKit 框架後,macOS 就始終對 PDF 渲染有著一流的支援。

至於,到了 iOS 11 也可以在系統中使用 PDF 框架的全部功能了:你可以使用 PDFView 類來顯示 PDF,讓使用者瀏覽文件,選擇並且分享內容,放大縮小等等操作。或者,你可以使用獨立的類比如: PDFDocument , PDFPagePDFAnnotation 來建立你自己自定義的 PDF 閱讀器。

和拖放一樣,我們可以建立一個簡單的 app 來演示 PDFKIT 是多麼的簡單。如果你願意的話,你可以繼續使用你剛才建立的單檢視 app 工程,但你需要向工程中匯入一個 PDF 檔案來供 PDFKit 去讀取。

你需要學習兩個新的比較小的類來編寫程式碼,第一個是 PDFView ,它負責所有的負責工作,包括 PDF 渲染,滾動和縮放手勢響應,選擇文字等。它也是 iOS 系統中常見的 UIView 子類,所以你可以不使用任何引數地建立 PDFView 例項物件,然後使用自動佈局來約束它的位置來滿足你的需求。第二個是新的類是 PDFDocument ,它可以通過一個 URL 來載入一個在其他地方可以被渲染或者操作 PDF 文件。

把 ViewController.swift 類的全部程式碼用這個代替:

import PDFKit
import UIKit

class ViewController: UIViewController {
    // store our PDFView in a property so we can manipulate it later
    var pdfView: PDFView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // create and add the PDF view
        pdfView = PDFView()
        pdfView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(pdfView)

        // make it take up the full screen
        pdfView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        pdfView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        pdfView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        pdfView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true

        // load our example PDF and make it display immediately
        let url = Bundle.main.url(forResource: "your-pdf-name-here", withExtension: "pdf")!
        pdfView.document = PDFDocument(url: url)
    }
}複製程式碼

如果執行 app 你應該可以看到你可以使用連續的滾動機制垂直滾動頁面。如果你在真機上測試,你也可以通過捏合操作進行縮放 —— 這時你就會發現 PDF 以更高的解析度重新渲染。如果你想要更改 PDF 的佈局樣式,你可以試著去設定 displayMode, displayDirection, 和 displaysAsBook 屬性。

例如,你可以將頁面以雙頁的模式展現,而封面預設就是這樣的:

pdfView.displayMode = .twoUpContinuous
pdfView.displaysAsBook = true複製程式碼

PDFView 提供了一系列有用的方法來讓使用者瀏覽和操作 PDF。為了試驗,我們會在我們的控制器上新增一些導航欄按鈕,因為這是新增互動最簡單的方式。

總共三步,我們先新增一個 navigation controller, 這樣我們就有了一個現成的導航欄來使用。所以,開啟你的 Main.storyboard ,在大綱檢視裡選中 View Controller Scene 。再進入編輯選單選擇 Embed In > Navigation Controller 。

接下來,在 ViewController.swift 中的 viewDidLoad() 方法中新增以下程式碼:

let printSelectionBtn = UIBarButtonItem(title: "Selection", style: .plain, target: self, action: #selector(printSelection))
let firstPageBtn = UIBarButtonItem(title: "First", style: .plain, target: self, action: #selector(firstPage))
let lastPageBtn = UIBarButtonItem(title: "Last", style: .plain, target: self, action: #selector(lastPage))

navigationItem.rightBarButtonItems = [printSelectionBtn, firstPageBtn, lastPageBtn]複製程式碼

這些程式碼新增了三個按鈕來實現一些基本的功能。最後,我們只需要寫這三個按鈕的響應方法就好了,那麼把下面這些方法新增到 ViewController 類中:

func printSelection() {
    print(pdfView.currentSelection ?? "No selection")
}

func firstPage() {
    pdfView.goToFirstPage(nil)
}

func lastPage() {
    pdfView.goToLastPage(nil)
}複製程式碼

現在,如果是在 Swift 3 下,我們可以這麼做。但是到了 Swift 4 你將會看到報 "Argument of '#selector' refers to instance method 'firstPage()' that is not exposed to Objective-C" 錯誤。換句話說就是 Swift 的方法對 Objective-C 不可見的,而 UIBarButtonItem 是 Objective-C 程式碼實現。

當然在每個方法之前加上 @objc 是個有效的辦法,我猜大部分人可能就聳聳肩(我有什麼辦法,我也很絕望啊),然後在類之前加上一個 @objcMembers 的定義 —— 這會像之前 Swift 3 那樣自動將類的所有東西都暴露給 Objective-C 。所以,把類的定義修改成這樣:

@objcMembers
class ViewController: UIViewController {複製程式碼

現在這就正確地編譯了,現在你將會看到跳轉到首頁和末頁的功能可以直接使用了。至於選擇按鈕,你只需要在點選按鈕之前在 PDF 之前選擇一些文字 —— 就像在 iBooks 進行文字選擇操作那樣。

開始支援 NFC 讀取

iPhone 7 引入了針對 NFC 的硬體支援,至於 iOS 11,NFC 開始支援讓我們在自己的 APP 內使用:你現在可以編寫程式碼來檢測附近的 NFC NDEF 標籤,而且出乎意料地簡單 —— 至少在 程式碼層面 。然而在我們看程式碼之前,你需要繞過一些坑,所有的我都希望在正式版消失。

Step 1: 在 Xcode 裡建立一個新的 單檢視 APP 模板。

Step 2: 去 iTunes 配置網站 developer.apple.com/account 為你的 APP 建立一個 包含 NFC 標籤讀取的 APP ID。

Step 3: 為這個 APP ID 建立一個描述檔案,並將其安裝到 Xcode 中。取消 "Automatically manage signing" 選項卡,並且選擇你剛才安裝的描述檔案。你可以點選描述檔案旁邊的小 “i” 按鈕來在許可權列表裡檢視 "com.apple.developer.nfc.readersession.formats"。

Step 4: 使用 快捷鍵 Cmd+N 為工程新增一個新的檔案,先選擇屬性列表。把它命名為 "Entitlements.entitlements" ,並且確保 "Group" 旁邊有一個藍色的圖示。

Step 5: 開啟 Entitlements.entitlements 進行編輯,右擊空白處選擇 "Add Row"。鍵值為 "com.apple.developer.nfc.readersession.formats" 並把它的型別改為陣列。點選 "com.apple.developer.nfc.readersession.formats" 左側的指示箭頭,再點選右邊的 + 標記。這時應該會插入一個帶有空值的 "Item 0" 鍵 —— 把它的值改為 "NDEF"。

Step 6: 定位到你的 target 的 build settings 找到 Code Signing Entitlements 。在文字框裡填入 "Entitlements.entitlements" 。

Step 7: 開啟你的 Info.plist 檔案,再右擊空白處選擇 "Add Row" 。新增鍵為 "Privacy - NFC Scan Usage Description" ,值為 "SwiftyNFC" 。

是的,就是一團糟。我不知道為什麼——能夠掃描 NFC 幾乎沒有比訪問某人的健康記錄更私密,而且更容易做到。在你思考惡意應用會不會暗地裡掃描 NFC 之前,還是省省吧:就像剛才看到的那樣,這是根本不可能做到的。

在混亂的設定之後,很高興地告訴你使 NFC 工作的程式碼幾乎是微不足道的:建立一個屬性來儲存一個代表當前 NFC 掃描會話的 NFCNDEFReaderSession 物件,再建立這個物件並要求它開始掃描。

當你建立讀取會話時,你需要給它提供三條資料:它能夠傳送資訊的代理,它應該用於傳送這些訊息的佇列和當它掃描到一個 NFC 標籤的時候是否結束掃描。我們會用 self 作為代理,DispatchQueue.main 作為佇列,將值設定為 false 當掃描到一個標籤後不停止掃描,所以它會繼續掃描直到60秒結束。

開啟 ViewController.swift,匯入 CoreNFC,再把這個屬性新增到 ViewController 類:

var session: NFCNDEFReaderSession!複製程式碼

接下來,在 viewDidLoad() 方法中新增這兩行程式碼:

session = NFCNDEFReaderSession(delegate: self, queue: DispatchQueue.main, invalidateAfterFirstRead: false)
session.begin()複製程式碼

ViewController 現在還沒有正確地遵循 NFCNDEFReaderSessionDelegate 協議,你需要修改你的類定義來包含它:

class ViewController: UIViewController, NFCNDEFReaderSessionDelegate {複製程式碼

按照慣例,Xcode 將會報你缺失一些必要方法的錯,所以使用它建議的修復來插入下面這兩個方法:

func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
    code
}

func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
    code
}複製程式碼

兩個方法都是特別簡單的,但是錯誤的處理也非常簡單——我們只是把錯誤列印到 Xcode 的控制檯。在 didInvalidateWithError 方法內像這樣新增內容:

func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
    print(error.localizedDescription)
}複製程式碼

現在對於 didDetectNDEFs 方法。當它被呼叫的時候你會得到一個檢測到的訊息的陣列,陣列每一個元素都可以包含描述單個資料的一個或更多記錄。例如,你可能會看到 NFC 被用作啟動 Google Cardboard app: Cardboard 裝置有一個簡單的包含絕對 URL "cardboard://V1.0.0" 的 NFC 標籤,當裝置檢測到標籤後會喚起 APP 顯示。

用 NFC 資料的處理就是你需要做的事了,我們只是把他列印出來了,把你的 didDetectNDEFs 修改成這樣:

func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
    for message in messages {
        for record in message.records {
            if let string = String(data: record.payload, encoding: .ascii) {
                print(string)
            }
        }
    }
}複製程式碼

所有的程式碼就完成了,那麼繼續開始執行這個 app 吧!如果所有的部分都起作用了,你將立即看到系統使用者介面出現提示使用者將其裝置靠近要掃描的位置。這就是為什麼惡意應用程式濫用 NFC 掃描是不可能的 - 不僅我們無法控制使用者介面,而且 60 秒後掃描也會因為超時結束以避免浪費電量。

機器學習和視覺識別

機器學習是現在最時髦的流行語,就是讓計算機根據過去接觸到的處理規則來適應新的資料。比如,如果你只有一張吉他畫和一個空的 Swift 類,那麼”這幅畫中有吉他嗎?“是個非常難回答的問題,但是如果你使用大量包含吉他的圖片樣本來構建一個訓練模型,這時你就可以有效地訓練計算機識別出包含吉他的新影象。

聽上去很無聊,但實際上是 iOS 11 上大量的先進技術的基礎:Siri,照相機,Quick Type 都使用了機器學習來幫助它們更好的理解我們所在的世界。iOS 11 還引入了一個新的 Vision 框架,這是一個從 Core Image ,機器學習功能和所有新技術組成的一個有點模糊的組合。

在 iOS 11 裡所有的這些都是由一個叫做 Core ML 的機器學習框架提供,該框架旨在支援各種各樣的模型,而不僅僅是識別影象。信不信由你,編寫 Core ML 的程式碼是很少的,然而這只是事情的一面。

你清楚的,Core ML 需要訓練模型才能工作,而模型是用演算法在大量資料訓練得出的。這些模型可以從幾千位元組到數百兆位元組甚至更多,而且明顯需要一定的專業知識才能訓練,特別是當你處理影象識別的時候。令人欣喜的是,蘋果提供了一些可以用來快速上手和執行的模型,所以如果你只是想要嘗試下使用 Core ML ,實際上是非常簡單的。

難過的是,還有事情還有另外一面:第三方框架總是非常噁心的,你明白的,Core ML 模型為我們自動生成接收一些輸入資料並返回一些輸出資料的程式碼 - 這部分是非常友好的。但悲傷的是,處理影象時所需的輸入資料不是 “UIImage”,也不是 “CGImage”,更不是 “CIImage” 。

相反,蘋果選擇讓我們使用 “CVPixelBuffer” 輸入。CVPixelBuffer 放進我的程式碼中就像血友病聚會上來了頭豪豬一樣不受歡迎。沒有把 UIImage 轉換為 CVPixelBuffer 的完美有效的方法,我是很有資格說的,因為我浪費了幾個小時來尋求解決方案。幸運的是 Chris Cieslak 非常慷慨把他的程式碼分享給我,在他的 WTFPL 下轉換是非常有效的,所以你也可以使用它進行轉換。

現在讓我們嘗試下 Core ML 吧。先建立一個新的單檢視 APP 工程(或者繼續使用你現有的工程),再在工程裡新增一張圖片 —— 我新增的是維基百科裡的 華盛頓杜勒斯國際機場 。把這張圖片重新命名為 "test.jpg" 以避免拼寫錯誤。

現在我們有一些輸入測試,我們需要新增一個訓練好的模型。它可能沒有看到過我們確切的照片,但它需要接觸些類似的圖片以便識別出這個機場。蘋果在 developer.apple.com/machine-lea… 上提供了一些預配置的模型 —— 現在進入網站,並下載 “Places205-GoogLeNet” 模型。 模型只有 25MB,所以它不會佔用你使用者裝置上太多空間。

當你下載好模型後,先把它拖到你的 Xcode 工程中,再選擇它,這時你就可以看到 Core ML 的模型檢視器。你會看到它是由 MIT 製作的神經網路分類器,還有可以根據知識共享許可證使用。在這個下面,你將看到它有 “sceneImage” 作為輸入,還有 “sceneLabelProbs ” 和 “sceneLabel” 作為輸出 —— 輸入一張圖片,輸出一些計算機識別這張圖片的文字描述。

你還將看到 “Model class” 和 “Swift generated source” —— Xcode為我們生成了一個類,只包含幾行程式碼,這一點非常顯著,你將很快看到。

現在,我們有一個可以識別的影象和一個可以檢查它的訓練好的模型。 我們現在需要做的是將兩者放在一起:載入圖片,為模型準備圖片,最後詢問模型的預測。

為了使這個程式碼更容易理解,我把它分成了一些塊。 首先,開啟 ViewController.swift 並將其修改為:

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let image = UIImage(named: "test.jpg")!

        // 1
        // 2
        // 3
    }
}複製程式碼

這只是載入我們準備被處理的測試圖片。 接下來的步驟是從 “// 1” 開始逐個填寫這三個註釋。

基於影象的 Core ML 模型要求以精確的尺寸接收圖片,這是他們接受過訓練的尺寸。 對於 GoogLeNetPlaces 模型尺寸應該是 224 x 224 而其他模型有它們各自的尺寸,而 Core ML 會告訴你是否以錯誤的尺寸輸入了東西。

所以,我們需要的第一件事是縮小我們的影象,讓圖片恰好是 224 x 224 ,而不管我們是使用視網膜屏裝置還是其他的裝置。 這可以使用 “UIGraphicsBeginImageContextWithOptions()” 方法來強制 1.0 的比例。 用下面的程式碼替換這個 // 1 註釋:

let modelSize = 224
UIGraphicsBeginImageContextWithOptions(CGSize(width: modelSize, height: modelSize), true, 1.0)
image.draw(in: CGRect(x: 0, y: 0, width: modelSize, height: modelSize))
let newImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()複製程式碼

這給了我們一個新的叫做 “newImage” 常量,它是一個符合模型中正確尺寸的 “UIImage”。

現在第二部分要做的是從 “UIImage” 到 “CVPixelBuffer” 之間噁心的轉換。 因為這是毫無意義的複雜操作,所以我不打算試圖解釋所有的各個步驟。除了拷貝下面的程式碼,我不建議你做任何事情。 用下面的程式碼替換這個 // 2 註釋:

let attrs = [kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue] as CFDictionary
var pixelBuffer : CVPixelBuffer?
let status = CVPixelBufferCreate(kCFAllocatorDefault, Int(newImage.size.width), Int(newImage.size.height), kCVPixelFormatType_32ARGB, attrs, &pixelBuffer)
guard (status == kCVReturnSuccess) else { return }

CVPixelBufferLockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))
let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer!)

let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
let context = CGContext(data: pixelData, width: Int(newImage.size.width), height: Int(newImage.size.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!), space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue)

context?.translateBy(x: 0, y: newImage.size.height)
context?.scaleBy(x: 1.0, y: -1.0)

UIGraphicsPushContext(context!)
newImage.draw(in: CGRect(x: 0, y: 0, width: newImage.size.width, height: newImage.size.height))
UIGraphicsPopContext()
CVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0))複製程式碼

如果可能使用很多次上面的程式碼,你可能想要把這些複雜程式碼封裝到一個函式裡邊。但無論你如何操作,請不要試圖去記住它。

現在開始重要的,有趣的和微不足道的部分:實際使用 Core ML 框架,這隻有三行程式碼,相當坦率地說,非常簡單。 就像我所說的,Xcode 自動根據 Core ML 模型生成一個 Swift 類,所以我們可以立即例項化一個 “GoogLeNetPlaces” 物件。

最後我們可以將我們的圖片快取傳遞給它的 “prediction()” 方法,這個方法將返回預測結果或丟擲一個錯誤。 在實踐中,你可能會發現使用 `try?' 更容易獲得一個值或是 nil 。 最後,我們將列印出預測結果,以便你瞭解到 Core ML 的表現。

用下面程式碼替換替換這個 // 3 註釋:

let model = GoogLeNetPlaces()
guard let prediction = try? model.prediction(sceneImage: pixelBuffer!) else { return }
print(prediction.sceneLabel)複製程式碼

不管你相不相信,這就是使用 Core ML 的所有程式碼; 這簡單的三行程式碼做完了所有的工作。 你列印出來的結果取決於你的輸入內容和你的訓練模型,但 GoogLeNetPlaces 正確地將我的圖片識別為機場航站樓,這一切完全在裝置上完成 —— 無需將圖片傳送到遠端伺服器處理,因此在這個黑盒子裡你得到了極好的隱私保護。

更多其他的更新。。。

iOS 11 還有大量的其他更新 —— 這些是我最喜歡的:

  • Metal 2 被設定成提高整個系統的圖形效能。我沒在這提供程式碼示例是因為這實在是一個高深的話題 —— 大多數人只會很高興看到他們的 SpriteKit ,SceneKit 和 Unity 應用程式無需額外的工作就可以獲得更快的速度。
  • TableView Cell 現在自動支援自適應。以前都是設定 UITableViewAutomaticDimension 作為行高來觸發自適應行為。但現在再也不需要設定了。
  • TableView 增加了一個 新的基於閉包的 performBatchUpdates() 方法,它可以讓你一次性對多行的插入、刪除、移動操作進行動畫處理,甚至可以在動畫完成之後立即執行結束閉包。
  • 在 Apple Music 第一次出現的新的加粗黑標題現在可以再整個系統使用了,同時支援通過一個細小的改動在我們自己的 APP 使用:在 IB 內為我們的導航條選擇 "Prefers Large Titles" ,或者如果你更喜歡使用程式碼的話使用 navigationController?.navigationBar.prefersLargeTitles = true 來設定。
  • 為了支援 safeAreaLayoutGuide topLayoutGuide屬性被棄用了。它提供了所有邊的邊緣而不僅僅是頂部和底部,這可能預示未來的 iPhone 為非矩形佈局 —— 帶有沉浸式相機的全螢幕 iPhone 8,有人有異議嗎?
  • Stack views 增加了一個 setCustomSpacing(_:after:) 方法,這可以讓你在 stack view 新增你想要的而不是統一大小的空白。

接下來就是 Xcode

Xcode 9 是我見過的最令人興奮的 Xcode 版本 —— 它充滿了令人難以置信的新功能,甚至可以使最堅定的 Xcode 抱怨者重新考慮。

這些是最吸引我的功能更新:

  • 可以在編輯器內進行 Swift 和 Objective-C 的重構,這意味著你只需點選幾下滑鼠就可以對你的程式碼進行徹底的更改(例如對方法重新命名)。
  • iOS 和 tvOS 支援無線除錯了。為了使用這個功能,先使用 USB 連線你的裝置,再在 Window 選單裡選擇 Devices and Simulators 。
    選擇你的裝置,最後選擇 "Connect via network" 。如果第一次不能成功 不必感到驚奇 —— 這還是 beta 1 版本!
  • 原始碼編輯器使用 Swift 進行了重寫,帶來了滾動和搜尋的速度極大的提升。以及一些其他有用的功能,比如按住 Ctrl 鍵時的範圍高亮顯示。
  • 你現在可以將命名顏色新增到 asset catalogs,這樣你可以定義一次顏色, 在任何地方使用 UIColor(named:) 方法初始化。
  • 預設情況下啟用了一個新的主執行緒檢查器,當檢測到任何不在主執行緒上執行的 UIKit 方法呼叫時,它將自動發出警告 - 這是常見的錯誤源頭。
  • 你現在可以同時執行多個模擬器,甚至可以自由調整它們的大小。 蘋果在模擬器周圍新增了額外的使用者介面,以便我們訪問硬體控制元件。
  • 如果您不想立即使用 Swift 4,則會有一個新的 “Swift Language Version” 構建設定,您可以選擇 Swift 4.0 或 Swift 3.2。 兩者都使用相同的編譯器,但在內部啟用不同的選項

認真的,我希望我今年在 WWDC 現場,這樣我就給 Xcode 工程師一個熊抱 —— 這是一個炙手可熱的版本,讓 Xcode 在奔向偉大的路上越行越遠。

還在等什麼?

現在你已經瞭解了 iOS 11 中的新功能,你也應該看一看我的新書:Practical iOS 11。這是一本用實際專案講解 iOS 11 中所有主要變化的書籍,擁有它你可以儘可能快地熟悉 iOS 11。

Practical iOS 11
Practical iOS 11

Buy Practical iOS 11 for $30


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃

相關文章