- 原文地址:What's new in iOS 11 for developers
- 原文做者:Paul Hudson
- 譯文出自:掘金翻譯計劃
- 譯者: Swants
- 校對者: Danny1451 RichardLeeH
蘋果在 2017 年全球開發者大會上公佈了 iOS 11 , 其加入許多強大的功能,如 Core ML
,ARKit
,Vision
,PDFKit
,MusicKit
拖放等等。 我嘗試着把主要變化在接下來的文章裏總結了出來,並在可行的地方提供代碼,這樣你就能夠直接上手。前端
注意: 有些地方沒涉及到並非由於懶,我已經盡我所能提供足夠多的代碼來幫你在應用上快速上手這些特性。可是你最終仍是免不了去額外瞭解更多 iOS 11 中大量複雜的設計功能。node
在接着讀下去以前,你可能須要瞭解下這幾篇文章:react
你可能想購買個人新書:《 Practical iOS 11 》。 你能夠經過教程的形式得到 7 個完整的項目代碼,以及更多深刻了解特定新技術的技術項目 - 這是熟悉 iOS 11最快的方式!android
Buy Practical iOS 11 for $30ios
拖放是咱們在桌面操做系統中認爲理所固然的操做,可是拖放在 iOS 上直到 iOS 11 纔出現,這真的阻礙了多任務處理的發展。換句話說,在 iOS 11 上尤爲是在 iPad 上,多任務處理迎來了高速發展的時代。得益於拖放成爲其中很大的一部分:你能夠在 APP 內部和或 APP 之間移動內容,當你拖放的時候你能夠用另外一隻手對其餘 app 進行操做.你甚至能夠利用 全新的 dock 系統來激活其餘 app 的中間拖動。git
注意: 在 iPhone 上拖放被限制在單個 app 內 —— 你不能把內容拖放到其餘 app 裏。github
使人欣喜的是,UITableView
和 UICollectionView
在必定程度上都支持拖拽內置。可是想要使用拖放功能仍舊須要寫至關多的代碼。你也能夠向其餘組件添加拖放支持,並且你會發現實際上這隻須要少許的工做。算法
下面讓咱們來看看如何使用簡單的拖放來實如今兩個列表之間拷貝行內容。首先,咱們須要使用一個簡單的 app 。讓咱們寫一些代碼來建立兩個有示例數據的 tableview
供咱們拷貝。swift
在 Xcode 內建立一個新的單一視圖 app 模板,而後打開 ViewController.swift
類進行編輯。後端
如今咱們須要在這裏放上兩個含有示例數據的 tableView 。我不打算使用 IB 的方式佈局, 由於所有使用代碼來實現是更清楚的。順便提一下,我 不打算 詳細地解釋代碼,由於這都是現成的 iOS 代碼,我不想浪費你的時間。
這些代碼將:
tableView
,而且建立兩個分別包含Left
和 Right
元素的字符串數組。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 會拋出幾個警告,由於咱們當前的控制器類沒有聽從 UITableViewDragDelegate
和 UITableViewDropDelegate
協議。經過給咱們的類添加這兩個協議很容易就修復這些警告了 —— 滾動到文件的最頂端而且改變類的定義:
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 上按下執行拖的操做的時候調用。若是你返回一個空數組,你實際上就是拒絕了拖放操做。
咱們打算爲這個方法添加四行代碼:
leftItems
中讀取,不然就從 rightItems
中讀取。Data
對象, 以即可以經過拖放進行傳遞。NSItemProvider
中,而且標記爲存儲了一個純文本字符串從而其餘 app 能夠知道如何去處理它。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
不起做用。)當全部拷貝的內容都就緒時咱們須要發送一個閉包來運行,這也是最複雜的地方:咱們須要把內容一個接一個地在目標行下面插入,修改 leftItems
或 rightItems
數組,最後調用咱們 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 最快的方式!
自從 OS X 10.4 開始受益於幾乎不須要提供任何代碼就能夠提供 PDF 渲染,操做,標註甚至更多的 PDFKit 框架後,macOS 就始終對 PDF 渲染有着一流的支持。
至於,到了 iOS 11 也能夠在系統中使用 PDF 框架的所有功能了:你可使用 PDFView
類來顯示 PDF,讓用戶瀏覽文檔,選擇而且分享內容,放大縮小等等操做。或者,你可使用獨立的類好比: PDFDocument
, PDFPage
和 PDFAnnotation
來建立你本身自定義的 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 進行文本選擇操做那樣。
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 還有大量的其餘更新 —— 這些是我最喜歡的:
UITableViewAutomaticDimension
做爲行高來觸發自適應行爲。但如今不再須要設置了。performBatchUpdates()
方法,它可讓你一次性對多行的插入、刪除、移動操做進行動畫處理,甚至能夠在動畫完成以後當即執行結束閉包。navigationController?.navigationBar.prefersLargeTitles = true
來設置。safeAreaLayoutGuide
topLayoutGuide
屬性被棄用了。它提供了全部邊的邊緣而不只僅是頂部和底部,這可能預示將來的 iPhone 爲非矩形佈局 —— 帶有沉浸式相機的全屏幕 iPhone 8,有人有異議嗎?setCustomSpacing(_:after:)
方法,這可讓你在 stack view 添加你想要的而不是統一大小的空白。Xcode 9 是我見過的最使人興奮的 Xcode 版本 —— 它充滿了使人難以置信的新功能,甚至可使最堅決的 Xcode 抱怨者從新考慮。
這些是最吸引個人功能更新:
UIColor(named:)
方法初始化。認真的,我但願我今年在 WWDC 現場,這樣我就給 Xcode 工程師一個熊抱 —— 這是一個煊赫一時的版本,讓 Xcode 在奔向偉大的路上越行越遠。
如今你已經瞭解了 iOS 11 中的新功能,你也應該看一看個人新書:Practical iOS 11。這是一本用實際項目講解 iOS 11 中全部主要變化的書籍,擁有它你能夠儘量快地熟悉 iOS 11。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃。