在本章中,您将要添加照片到 Homepwner
应用程序。 您将呈现一个 UIImagePickerController,以便用户可以拍摄并保存每个 item 的图片。 然后,图像将与 Item 实例相关联,并在 item 的详情视图中查看(图15.1)。
图15.1带摄像头的 Homepwner
图像往往非常大,所以将图像与其他数据分开存储是个好主意。 因此,您将要创建第二个 store 来存储图像。 ImageStore 将根据需要获取并缓存图像。
显示图像和 UIImageView
您的第一步是让 DetailViewController 获取并显示图像。 显示图像的简单方法是将 UIImageView 的实例放在屏幕上。
打开 Homepwner.xcodeproj
和 Main.storyboard
。 然后将 UIImageView 的实例拖动到栈视图底部的视图上。 选择 图像视图 并打开其尺寸检查器。 您希望图像视图的 Vertical Content Hugging Priority
和 Vertical Content Compression Resistance Priority
低于其他视图。 将 Vertical Content Hugging Priority
更改为 248,Vertical Content Compression Resistance Priority
为 749。
您的布局将如图15.2所示。
图15.2 DetailViewController 的视图中的 UIImageView
UIImageView 根据图像视图的 contentMode
属性显示图像。 此属性决定图像视图框架内的位置和如何调整大小。 UIImageView 的 contentMode
的默认值是 UIViewContentMode.scaleToFill
,它可以调整图像,使之与图像视图的边界完全匹配。 如果保持默认值,相机拍摄的图像将被缩放以适应 UIImageView。 要保持图像的宽高比,您必须更新 contentMode
。
选择 UIImageView 后,打开属性检查器。 查找 Content Mode
属性,并将其更改为 Aspect Fit
(图15.3)。 您不会在故事板上看到更改,但是现在,图像的大小将被调整以适应 UIImageView 的尺寸。
图15.3将 UIImageView 的模式更改为 Aspect Fit
接下来,单击选中项目导航器中的 DetailViewController.swift
,在助手编辑器中打开它。 右键从 UIImageView 拖动到 DetailViewController.swift
的顶部。 命名为 imageView
并确保 存储(storage) 类型为 Strong
。 单击 Connect
(图15.4)。
图15.4创建 imageView outlet
DetailViewController.swift
的顶部应该如下所示:
class DetailViewController: UIViewController, UITextFieldDelegate {
@IBOutlet var nameField: UITextField!@IBOutlet var serialNumberField: UITextField!
@IBOutlet var valueField: UITextField!
@IBOutlet var dateLabel: UILabel!
@IBOutlet var imageView: UIImageView!
添加相机按钮
现在您需要一个按钮来启动拍照过程。 您将创建一个 UIToolbar 的实例,并将其放在 DetailViewController 视图的底部。
在 Main.storyboard
中,按 Command-Return
关闭助理编辑器,给自己更多的空间在故事板上工作。 您将需要暂时中断您的界面以将工具栏添加到界面。
选择栈视图的底部约束,然后按 Delete
将其删除。 您需要为底部的工具栏腾出空间。 从Xcode 8.1开始,很难调整栈视图的大小。 因此我们将栈视图拖曳一点(图15.5)。 现在的视图会出错,但是你很快就会解决这个问题。
图15.5 将栈视图移开
现在将 工具栏(Toolbar) 从对象库拖动到视图的底部。 选择 Toolbar 并打开自动布局 Add New Constraints
菜单。 精确地配置约束,如图15.6所示,然后单击 Add 5 Constraints
。 因为您选择了 update frame
的选项,栈视图将重新定位到其正确的位置。
图15.6 Toolbar 约束
UIToolbar 的工作原理很像 UINavigationBar——您可以向其添加 UIBarButtonItem 的实例。 但是,导航栏只有两个 Bar Button Item,而工具栏可以有多个 Bar Button Item。 您可以在工具栏中放置尽可能多的 Bar Button Item,以适应屏幕。
默认情况下,在界面文件中创建的一个新的 UIToolbar 实例带有一个 UIBarButtonItem。 选择此 Bar Button Item 并打开属性检查器。 将 System Item
更改为 Camera
,item 将显示相机图标(图15.7)。
图15.7带相机按钮项的 UIToolbar
构建并运行应用程序并导航到 item 的详细信息,以使用工具栏上的 Camera Bar Button Item 。 您尚未将相机按钮连接到操作,因此点击它将不会执行任何操作。
相机按钮需要一个目标和一个动作。 在 Main.storyboard
仍然打开的情况下,在项目导航器中选中 DetailViewController.swift
,在助理编辑器中重新打开它。
在 Main.storyboard
中,首先单击工具栏上的按钮本身,选择相机按钮。 右键从所选按钮拖动到 DetailViewController.swift
。
在 Connection
弹出菜单中,选择 Action
作为连接类型,命名为 takePicture,选择 UIBarButtonItem
作为类型,然后单击 Connect
(图15.8)。
图15.8 创建动作
如果在进行此连接时发生任何错误,则需要打开 Main.storyboard
并断开任何不良连接。 (在连接检查器中查找黄色警告标志。)
拍照和 UIImagePickerController
在 takePicture(_ :) 方法中,您将实例化一个 UIImagePickerController 并将其显示在屏幕上。 创建 UIImagePickerController 的实例时,必须设置其 源类型(sourceType
) 属性并为其分配委托。 因为这是图像选择器控制器必需的,所以您需要以编程方式创建并显示它,而不是通过故事板。
设置图像选择器的 sourceType
sourceType
常量告诉图像选择器在哪里获取图像。 它有三个可能的值:
UIImagePickerControllerSourceType.camera
允许用户拍摄新照片。
UIImagePickerControllerSourceType.photoLibrary
提示用户选择相册,然后选择该相册中的照片。
UIImagePickerControllerSourceType.savedPhotosAlbum
提示用户从最近拍摄的照片中进行选择。
图15.9三种 sourceType 的例子
第一种源类型 .camera
,将不会在没有相机的设备上工作。 所以在使用这种类型之前,您必须通过调用 UIImagePickerController 类上的方法 isSourceTypeAvailable(_ :) 来检查摄像头:
class func isSourceTypeAvailable(_ type: UIImagePickerControllerSourceType) -> Bool
调用此方法将返回一个布尔值,以查看设备是否支持传入源类型。
在 DetailViewController.swift
中,找到 takePicture(_ :) 的位置。 添加以下代码以创建图像选择器并设置其 sourceType
。
@IBAction func takePicture(_ sender: UIBarButtonItem) {
let imagePicker = UIImagePickerController()
// If the device has a camera, take a picture; otherwise,
// just pick from photo library
if UIImagePickerController.isSourceTypeAvailable(.camera) {
imagePicker.sourceType = .camera
} else {
imagePicker.sourceType = .photoLibrary
}
}
设置图像选择器的委托
除了源类型,UIImagePickerController 实例需要一个委托。 当用户从 UIImagePickerController 的界面中选择一个图像时,委托将发送消息 imagePickerController(_:didFinishPickingMediaWithInfo :)。 (如果用户点击取消按钮,则委托会收到消息 imagePickerControllerDidCancel(_ :))
图像选择器的委托将是 DetailViewController 的实例。 在 DetailViewController.swift
的顶部,声明 DetailViewController 符合 UINavigationControllerDelegate 和 UIImagePickerControllerDelegate 协议。
class DetailViewController: UIViewController, UITextFieldDelegate,
UINavigationControllerDelegate, UIImagePickerControllerDelegate
{
为什么是 UINavigationControllerDelegate?UIImagePickerController 的 delegate
属性实际上是从其父类 UINavigationController 继承的,虽然 UIImagePickerController 具有自己的 委托协议,但其继承的 delegate
属性被声明为引用符合 UINavigationControllerDelegate 的对象。
在 DetailViewController.swift
中,将 DetailViewController 的实例设置为 takePicture(_ :) 中的图像选择器代理。
@IBAction func takePicture(_ sender: UIBarButtonItem) {
let imagePicker = UIImagePickerController()
// If the device has a camera, take a picture; otherwise,
// just pick from photo library
if UIImagePickerController.isSourceTypeAvailable(.camera) {
imagePicker.sourceType = .camera
} else {
imagePicker.sourceType = .photoLibrary
}
imagePicker.delegate = self
}
以模态方式呈现图片选择器
一旦 UIImagePickerController 具有源类型和代理,您可以通过模式显示视图控制器来显示它。
在 DetailViewController.swift
中,将代码添加到 takePicture(_ :) 的末尾以呈现 UIImagePickerController。
imagePicker.delegate = self
// Place image picker on the screen
present(imagePicker, animated: true, completion: nil)
}
构建并运行应用程序。 选择 item 以查看其详细信息,然后点击 UIToolbar 上的相机按钮,... 应用程序崩溃。 看看在控制台中的崩溃的描述。
Homepwner[3575:64615] [access] This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an NSPhotoLibraryUsageDescription key with a string value explaining to the user how the app uses this data.
Homepwner [3575:64615] [访问]此应用程序已崩溃,因为它尝试访问隐私敏感数据而没有使用说明。 应用程序的 Info.plist 必须包含一个 NSPhotoLibraryUsageDescription 键,其中的字符串值向用户解释应用程序如何使用此数据。
当尝试访问私人信息(例如用户的照片)时,iOS会向用户询问是否要允许访问应用程序。 在此提示中包含一个描述为什么应用程序想要访问此信息。 Homepwner
缺少此描述,因此应用程序会崩溃。
权限
iOS上有许多功能需要用户批准才能使用。 以下是这些功能的一个子集:
- 相机和照片
- 位置
- 麦克风
- HealthKit 数据
- 日历
- 提醒
对于每一个权限,您的应用程序必须提供一个 使用说明(usage description)
,指定您的应用程序要访问此信息的原因。 每当应用程序访问该功能时,将向用户呈现此描述。
在项目导航器中,选中最顶层的 Homepwner
,并打开顶部的 Info
选项卡(图15.10)。
图15.10打开项目信息
将鼠标悬停在此 Custom iOS Target Properties
列表中的最后一项上,然后单击 +
按钮。 将此新条目的 key
设置为 NSCameraUsageDescription
,将 Type
设置为 String
。
双击此行的值,然后输入字符串 “This app uses the camera to associate photos with items.”。这是将呈现给用户的字符串。
现在重复上述相同的步骤添加照片库的使用说明。Key
将是 String
类型的 NSPhotoLibraryUsageDescription
,Value
为 This app uses the Photos library to associate photos with items.
。
Custom iOS Target Properties
部分现在将如图15.11所示。 (列表中的项可能有不同的顺序。)
图15.11 添加新 key
构建并运行应用程序并选择一个 item。 点击相机按钮,您将看到您提供的使用说明一起显示的权限对话框(图15.12显示了库的描述)。 接受后,UIImagePickerController 的界面将出现在屏幕上(图15.13显示了摄像头界面),您可以拍照,如果您的设备没有相机则可以选择现有的图片。
图15.12 照片库使用说明
(如果您正在使用模拟器,照片库中已经有一些默认图片,如果要添加自己的图片,可以将图片从计算机拖动到模拟器上,并将它添加到模拟器的照片库,或者,您可以在模拟器中打开 Safari
,并导航到带有图片的页面。右击图片,然后选择 图片存储为(Save Image)
将其保存在模拟器的照片库中。)
图15.13 UIImagePickerController 的预览界面
保存图片
选择一个图片会关闭 UIImagePickerController 并返回到详情视图。 但是,一旦图片选择器被关闭,您就没有了图片的引用。 要解决这个问题,你将要实现委托方法 imagePickerController(_:didFinishPickingMediaWithInfo :)。 当选择照片时,图片选择器的委托将调用此方法。
在 DetailViewController.swift
中,实现此方法以将图片放入 UIImageView,然后调用方法关闭图片选择器。
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String: Any]) {
// Get picked image from info dictionary
let image = info[UIImagePickerControllerOriginalImage] as! UIImage
// Put that image on the screen in the image view
imageView.image = image
// Take image picker off the screen -
// you must call this dismiss method
dismiss(animated: true, completion: nil)
}
再次构建并运行应用程序。 拍摄(或选择)照片。 图片选择器被关闭,您将返回到 DetailViewController 的视图,您将在其中看到所选照片。
Homepwner
的用户可以有数百个 item ,每个用户可以拥有与之相关联的大图像。 保存数百个 Item 的内容不难,但要在内存中保存数百张图像则是不好的:首先,您将获得低内存警告。 然后,如果您的应用程序的内存占用量继续增长,则操作系统将会终止它。 您将在下一节中实现的解决方案是将 图片 存储到磁盘,并在需要时将其存入RAM。 这个做法将由一个新类 ImageStore 完成。 当应用程序接收到低内存通知时,ImageStore 的缓存将被刷新以释放所获取的图片占用的内存。
创建 ImageStore
在第16章中,您将创建一个 Item 的实例来将它们的属性写入一个文件,然后在应用程序启动时被读入。 然而,由于图片往往非常大,将它们与其他数据分开是一个好主意。 您将存储用户在 ImageStore 类的实例中使用的图片。 图片存储将根据需要提取和缓存图片。 如果设备内存不足,则可以刷新缓存。
创建一个名为 ImageStore
的新的 Swift 文件。 在 ImageStore.swift
中,定义 ImageStore 类并添加一个属性,该属性是 NSCache 的一个实例。
import Foundation
import UIKit
class ImageStore {
let cache = NSCache<NSString,UIImage>()
}
缓存的运作非常像一本字典(在第2章中看到)。 您可以添加,删除和更新给定的 key 相关联的值。 与字典不同,如果系统内存不足,缓存将自动删除对象。 虽然这可能是本章中的一个问题(因为图片只会存在于缓存中),当您还想将图片写入文件系统时,您将在第16章中解决这个问题。
请注意,缓存将 NSString 的实例与 UIImage 相关联。 NSString 是 Objective-C 的 String 版本。 由于 NSCache 的实现方式所限(它是一个 Objective-C 类,大多数Apple的类一直在使用),它要求您使用 NSString 而不是 String。
现在实现从字典添加,检索和删除图像的三种方法。
class ImageStore {
let cache = NSCache<NSString,UIImage>()
func setImage(_ image: UIImage, forKey key: String) {
cache.setObject(image, forKey: key as NSString)
}
func image(forKey key: String) -> UIImage? {
return cache.object(forKey: key as NSString)
}
func deleteImage(forKey key: String) {
cache.removeObject(forKey: key as NSString)
}
}
这三种方法都采用 String 类型的关键字,以便其余的代码库不必考虑 NSCache 的底层实现。 然后,当将每个 String 传递到缓存时,将每个 String 转换为 NSString。
让视图控制器访问 ImageStore
DetailViewController 需要一个 ImageStore 实例来获取和存储图片。 您将把这个依赖项注入到 DetailViewController 的指定的构造器中,就像在第10章中为 ItemsViewController 和 ItemStore 所做的那样。
在 DetailViewController.swift
中,为 ImageStore 添加一个属性。
var item: Item! {
didSet {
navigationItem.title = item.name
}
}
var imageStore: ImageStore!
现在在 ItemsViewController.swift
中执行相同操作。
var itemStore: ItemStore!
var imageStore: ImageStore!
接下来,仍然在 ItemsViewController.swift
中,更新 prepare(for:sender :) 在 DetailViewController 上设置 imageStore
属性。
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// If the triggered segue is the "showItem" segue"
switch segue.identifier {
case "showItem"?:
// Figure out which row was just tapped
if let row = tableView.indexPathForSelectedRow?.row {
// Get the item associated with this row and pass it along
let item = itemStore.allItems[row]
let detailViewController = segue.destination as! DetailViewController
detailViewController.item = item
detailViewController.imageStore = imageStore
}
default:
preconditionFailure("Unexpected segue identifier.")
}
}
最后,更新 AppDelegate.swift
创建并注入 ImageStore。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// Create an ItemStore
let itemStore = ItemStore()
// Create an ImageStore
let imageStore = ImageStore()
// Access the ItemsViewController and set its item store
and image store
let navController = window!.rootViewController as! UINavigationController
let itemsController = navController.topViewController as! ItemsViewController
itemsController.itemStore = itemStore
itemsController.imageStore = imageStore
创建和使用 key
当一个图片被添加到 store 时,它要有唯一的 key 并被放入到缓存中,关联的 Item 对象也应该有这个 key。 当 DetailViewController 想要从 store 获得图片时,它会请求其 item
获得 key 并搜索缓存中的图像。
添加一个属性到 Item.swift 存储 key。
let dateCreated: Date
let itemKey: String
缓存中的 key 必须保证是唯一的。 虽然有许多方法可以将一个唯一的字符串组合在一起,您将使用 Cocoa Touch 机制来创建通用唯一标识符 universally unique identifier
(UUID),也称为全局唯一标识符 globally unique identifier
(GUID)。
类型 NSUUID 的对象表示 UUID,并使用时间,计数器和硬件标识符生成,通常是 Wi-Fi 卡的MAC地址。 当以字符串的形式表示时,UUID看起来像这样:
4A73B5D2-A6F4-4B40-9F82-EA1E34C1DC04
在 Item.swift
中,生成一个 UUID 并将其设置为 itemKey
。
init(name: String, serialNumber: String?, valueInDollars: Int) {
self.name = name
self.valueInDollars = valueInDollars
self.serialNumber = serialNumber
self.dateCreated = Date()
self.itemKey = UUID().uuidString
super.init()
}
在 DetailViewController.swift
中,更新 imagePickerController(_:didFinishPickingMediaWithInfo :) 将图片存储在 ImageStore 中。
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
// Get picked image from info dictionary
let image = info[UIImagePickerControllerOriginalImage] as! UIImage
// Store the image in the ImageStore for the item's key
imageStore.setImage(image, forKey: item.itemKey)
// Put that image on the screen in the image view
imageView.image = image
// Take image picker off the screen -
// you must call this dismiss method
dismiss(animated: true, completion: nil)
}
每当捕捉到一张图片,它都将被添加到 store。 ImageStore 和 Item 都将知道图像的 key,所以两者都可以根据需要进行访问(图15.14)。
图15.14 从缓存访问图像
类似地,当 item 被删除时,您需要从图像存储库中删除其图像。 在 ItemsViewController.swift
中,更新 tableView(_:commit:forRowAt :) 以从 ImageStore 中删除 item 的图片。
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
// If the table view is asking to commit a delete command...
if editingStyle == .delete {
let item = itemStore.allItems[indexPath.row]
let title = "Delete \(item.name)?"
let message = "Are you sure you want to delete this item?"
let ac = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
ac.addAction(cancelAction)
let deleteAction = UIAlertAction(title: "Delete", style: .destructive, handler: { (action) -> Void in
// Remove the item from the store
self.itemStore.removeItem(item)
// Remove the item's image from the image store
self.imageStore.deleteImage(forKey: item.itemKey)
// Also remove that row from the table view with an animation
self.tableView.deleteRows(at: [indexPath], with: .automatic)
})
ac.addAction(deleteAction)
// Present the alert controller
present(ac, animated: true, completion: nil)
}
}
封装 ImageStore
现在 ImageStore 可以存储图片和 Item 的实例并且由一个 key 来获取图片(图15.14),您需要使 DetailViewController 获取所选 Item 的图片并将其放在其 imageView
中。
当用户在 ItemsViewController 中点击一行和 UIImagePickerController 被关闭时,DetailViewController 的视图将会出现。 在这两种情况下,imageView 都应该显示正在显示的 item 的图片。 而目前仅在 UIImagePickerController 被关闭时才会发生。
在 DetailViewController.swift
中,在 viewWillAppear(_ :) 中进行此操作。
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
nameField.text = item.name
serialNumberField.text = item.serialNumber
valueField.text = numberFormatter.string(from: NSNumber(value: item.valueInDollars))
dateLabel.text = dateFormatter.string(from: item.dateCreated)
// Get the item key
let key = item.itemKey
// If there is an associated image with the item
// display it on the image view
let imageToDisplay = imageStore.image(forKey: key)
imageView.image = imageToDisplay
}
运行应用程序。 创建一个 item 并从表格视图中选择它。 然后点击相机按钮拍照。 图片将按原样出现。 从 item 的 详情 返回到 item 列表。 与以前不同,如果您点击并向下钻取以查看添加了图片的 item 的详情时,您将看到图片。
青铜挑战:编辑图片
UIImagePickerController 具有内置界面,用于在选择图像后对其进行编辑。 允许用户编辑图片,并使用编辑的图片,而不是 DetailViewController 中的原始图像。
白银挑战:删除图片
添加一个清除 item 图片的按钮。
黄金挑战:相机覆盖线
UIImagePickerController 具有 cameraOverlayView
属性。 用它来使 UIImagePickerController 在图像捕获区域的中间显示十字准线。
更多:浏览实现类文件
您的两个视图控制器在其实现类文件中都有很多方法。 要成为一名高效的 iOS 开发人员,您必须能够快速便捷地浏览您正在寻找的代码。 Xcode中的源代码编辑器跳转栏是您可以使用的一个工具(图15.15)。
图15.15 源代码编辑器跳转栏
跳转条显示您在项目中的完整位置(以及光标在给定文件中的位置)。 跳转详细信息如图15.16。
图15.16 跳转栏详情
跳栏的导航痕迹导航反映了项目导航层级。 如果您单击任何部分,将在项目层次结构中显示该部分的 弹出窗口。 从那里,您可以轻松导航到项目的其他部分。
图15.17显示了 Homepwner
文件夹的文件弹出窗口。
图15.17 文件弹出窗口
也许最有用的是在实现类文件中轻松浏览的能力。 如果您点击导航痕迹中的最后一个元素,您将获得一个包含文件内容的弹出窗口,包括该文件中实现的所有方法。
当弹出窗口可见时,您可以输入文本以过滤列表项。 在任何时候,您可以使用上下箭头键,然后按 Return(回车) 键在代码中跳转到该方法。 图15.18显示了在 ItemsViewController.swift
中搜索 tableview
时获得的内容。
图15.18文件 弹出窗口 与搜索 “tableview”
// MARK:
随着你的类越来越长,找到一个埋在一大堆方法中的方法将变得越来越困难。 组织您的方法的一个好方法是使用 // MARK:
注释。
两个有用的 // MARK: 注释是分隔符和标签:
// This is a divider
// MARK: -
// This is a label
// MARK: My Awesome Methods
分隔线和标签可以组合使用 :
// MARK: - View life cycle
override func viewDidLoad() { ... }
override func viewWillAppear(_ animated: Bool) { ... }
// MARK: - Actions
func addNewItem(_ sender: UIBarButtonItem) {...}
添加 // MARK:
注释到你的代码不会改变代码本身; 它只是告诉 Xcode 如何可视化组织你的方法。 您可以通过在跳转栏中打开当前文件项来查看结果。 图15.19 是一个组织良好的组织的 ItemsViewController.swift
图15.19 带有 //MARK: 的文件弹出窗口
如果您习惯使用 // MARK:
注释,您将强制自己组织您的代码。 如果你组织的好,这将使您的代码更易于阅读,更易于使用。