- 原文地址: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、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划。