iOS apprentice中文版 Chapter 30(上): 图片选择器

你的Tag Locations屏幕的大部分功能都是完整的——除了能够添加一个位置的照片。是时候解决这个问题了!
UIKit带有一个内置的视图控制器UIImagePickerController,它允许用户拍摄新的照片和视频,或者从他们的照片库中选择它们。你要用它来保存一张照片和位置,这样用户就有了一张好看的照片。
这是你完成后屏幕的样子:



在本章中,您将做以下工作:

  1. 添加一个图像选择器:添加一个图像选择器到您的应用程序中,允许您使用相机拍照或从您的照片库中选择现有的图像。
  2. 显示图像:在表视图单元格中显示选中的图像。
  3. UI改进:当您的应用程序被发送到后台时,改进用户界面功能。
  4. 保存图像:保存通过设备上的图像选择器选择的图像,以便以后可以检索到。
  5. 编辑图像:如果位置有图像,则在编辑屏幕上显示图像。
  6. 缩略图:在位置列表屏幕上显示位置的缩略图。

1.添加一个图像选择器

就像你需要征得用户的许可才能从设备上获取GPS信息一样,你也需要获得访问用户照片库的许可。
您不需要为此编写任何代码,但是您需要在应用程序的Info.plist中声明您的意图。如果你不这样做,当你尝试使用UIImagePickerController时,应用程序将崩溃(除了Xcode控制台中的一条消息外,没有可见的警告)。

Info.plist的更改

➤打开Info.plist并添加一个新行——在现有行上使用plus(+)按钮,或者右键单击并选择add row,或者使用Editor→add Item菜单选项。
对于密钥key,使用NSPhotoLibraryUsageDescription,或者从下拉列表中选择Privacy - PhotoLibraryUsageDescription。
对于这个值,输入:Add photos to your locations



➤同时添加key NSCameraUsageDescription(或者选择Privacy - CameraUsageDescription)并给出相同的描述(description)。
现在,当应用程序第一次打开照片选择器或相机时,iOS会使用你刚刚添加到Info.plist中的描述,告诉用户应用程序打算用这些照片做什么。

使用相机添加图片

➤LocationDetailsViewController.swift,在源文件末尾添加以下extensin:

extension LocationDetailsViewController: 
    UIImagePickerControllerDelegate,
    UINavigationControllerDelegate {

  // MARK:- Image Helper Methods
  func takePhotoWithCamera() {
    let imagePicker = UIImagePickerController()
    imagePicker.sourceType = .camera
    imagePicker.delegate = self
    imagePicker.allowsEditing = true
    present(imagePicker, animated: true, completion: nil)
  }
}

UIImagePickerController和其他视图控制器一样,是一个视图控制器,但它内置在UIKit中,它负责拍摄新照片的整个过程或从用户的照片库中选取它们。
你需要做的就是创建一个UIImagePickerController实例,设置它的属性来配置picker,设置它的委托,然后显示它。当用户关闭图像选取器屏幕时,委托方法将让您知道操作的结果。
这就是你设计自己视图控制器的方式,除了你不需要将UIImagePickerController添加到故事板。

注意:您在extension中这样做是因为它允许您将所有与拍照相关的功能组合在一起。
如果愿意,可以将这些方法放在主类主体中。这也可以很好地工作,但视图控制器往往会变得非常大,有很多方法都做不同的事情。
为了保持头脑清醒,最好提取与概念相关的方法——比如所有与挑选照片有关的方法——并将它们放在各自的extension中。
您甚至可以将每个扩展名移动到它们自己的源文件中,例如“LocationDetailsViewController+PhotoPicking.swift”。但就我个人而言,我发现管理更少的文件是一件好事:]

➤将以下方法添加到扩展中:

// MARK:- Image Picker Delegates
func imagePickerController(_ picker: UIImagePickerController, 
                          didFinishPickingMediaWithInfo info: 
                   [UIImagePickerController.InfoKey : Any]) {
  dismiss(animated: true, completion: nil)
}

func imagePickerControllerDidCancel(_ picker: 
                      UIImagePickerController) {
  dismiss(animated: true, completion: nil)
}

目前,这些委托方法只是简单地从屏幕上删除图像选取器。很快,您将获取用户选择的图像并将其添加到Location对象,但现在,您只想确保图像选择器出现。
请注意,视图控制器——在本例中是扩展——必须同时符合UIImagePickerControllerDelegate和UINavigationControllerDelegate,但您不必实现任何UINavigationControllerDelegate方法。
➤现在在类中改变tableView(_:didSelectRowAt:)如下:

override func tableView(_ tableView: UITableView,
           didSelectRowAt indexPath: IndexPath) {
  if indexPath.section == 0 && indexPath.row == 0 {
    . . . 
  } else if indexPath.section == 1 && indexPath.row == 0 {
    takePhotoWithCamera()
  }
}

Add Photo是第二部分的第一行。当它被点击时,您调用刚才添加的takePhotoWithCamera()方法。
运行应用程序,标记一个新位置或编辑一个现有位置,然后点击Add Photo。

如果你在模拟器上运行应用程序,嘭!它崩溃了。错误信息如下:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Source type 1 not available

事故的罪魁祸首是这一行:

imagePicker.sourceType = .camera

并不是所有的设备都有摄像头,模拟器也没有。如果你试图使用UIImagePickerController和sourceType不被设备或模拟器支持,应用会崩溃。
如果你在你的设备上运行这个应用程序——如果它有一个摄像头,如果是最近的型号,它很可能有——那么你应该会看到这样的东西:


这和你用iPhone的拍照应用拍照时看到的非常相似。MyLocations不允许你录制视频,但如果你愿意,你当然可以在自己的应用中启用这一功能。

使用照片库添加图像

你仍然可以在模拟器上测试图像选择器,但你必须使用照片库,而不是相机。
➤给扩展添加另一种方法:

func choosePhotoFromLibrary() {
  let imagePicker = UIImagePickerController()
  imagePicker.sourceType = .photoLibrary
  imagePicker.delegate = self
  imagePicker.allowsEditing = true
  present(imagePicker, animated: true, completion: nil)
}

这个方法本质上和takePhotoWithCamera做的是相同的事情,只不过现在将sourceType设置为.photolibrary。
将didSelectRowAt改为调用choosePhotoFromLibrary()而不是takePhotoWithCamera()。
➤在模拟器中运行应用程序,点击Add Photo。
此时,根据您的iOS版本,您可能需要授予MyLocations访问照片库的权限。如果单击“不允许”,则“照片选取器”屏幕仍然为空。如果你不小心这样做了,你可以在设置应用程序的Privacy → Photos下撤销这个选择。选择OK允许应用程序使用照片库。
然而,在iOS 12中,你可能不会得到提示,所以你应该可以看到一些库存图片。在较老的iOS版本中,你可能根本看不到任何图像。

如果你因为某种原因没有看到任何图片,请停止应用程序并点击模拟器中的内置照片应用程序。这应该会显示一些示例照片。再次运行应用程序,并尝试选择一张照片。您现在可能看到这些示例照片,也可能没有看到。如果没有,您必须添加您自己的。
有几种方法可以将新照片添加到模拟器中。你可以进入Safari(在模拟器上),在互联网上搜索图片,按下图片直到出现菜单,然后选择Save image:

你也可以简单地将一个图像文件拖放到模拟器窗口,而不是上网寻找图像。这将把图片添加到照片应用程序的库中。
最后,您可以使用终端和simctl命令。在一行(最后一部分,~/Desktop/MyPhoto.JPG)中键入以下内容。应替换为要添加图像的实际路径):

/Applications/Xcode.app/Contents/Developer/usr/bin/simctl addmedia booted ~/Desktop/MyPhoto.JPG

simctl工具可以用来管理您的模拟器 - 输入simctl help的选项列表。addmedia命令引导将指定的媒体文件添加到活动模拟器。
再次运行应用程序。现在你应该可以从照片库中选择一张照片:



选择其中一张照片。屏幕现在变成:



这是因为您将image picker的allowsEditing属性设置为true。启用此设置后,用户可以在做出最终选择之前对照片进行一些快速编辑——在模拟器中,您可以按住Alt/Option,同时拖动来旋转和缩放照片。

因此,您可以使用两种类型的图像选择器:相机和照片库。相机不会在任何地方都能用。不过,将应用程序限制为只能从库中选择照片也有点鸡肋。
你必须让应用程序更智能一点,让用户在相机出现时选择相机。

选择相机和图片库

首先,你要检查相机是否可用。如果是,则显示一个动作表单,让用户在相机和照片库之间进行选择。
➤将以下方法添加到LocationDetailsViewController.swift中。在照片的扩展名中写道:

func pickPhoto() {
  if UIImagePickerController.isSourceTypeAvailable(.camera) {
    showPhotoMenu()
  } else {
    choosePhotoFromLibrary()
  }
}

func showPhotoMenu() {
  let alert = UIAlertController(title: nil, message: nil, 
                       preferredStyle: .actionSheet)

  let actCancel = UIAlertAction(title: "Cancel", style: .cancel, 
                              handler: nil)
  alert.addAction(actCancel)

  let actPhoto = UIAlertAction(title: "Take Photo", 
                               style: .default, handler: nil)
  alert.addAction(actPhoto)

  let actLibrary = UIAlertAction(title: "Choose From Library", 
                                 style: .default, handler: nil)
  alert.addAction(actLibrary)

  present(alert, animated: true, completion: nil)
}

你使用UIImagePickerController的isSourceTypeAvailable()方法来检查是否存在摄像头。如果没有,则调用choosePhotoFromLibrary(),因为这是惟一的选项。但当设备有摄像头时,你会在屏幕上显示一个UIAlertController。

与您以前使用的Alert控制器不同,这个控制器具有. actionsheet样式。动作表单的工作原理与警告视图非常相似,不同之处在于它从屏幕底部滑动进来,并为用户提供几个选项之一。
在didSelectRowAt中,将调用choosePhotoFromLibrary()改为pickPhoto()。老实说,这是你最后一次改变路线了。
➤在你的设备上运行这个应用程序,看看动作表单是如何运行的:



点击动作表单中的任何一个按钮,都只会让动作表单失效,而不会做任何其他事情。”
顺便说一下,如果你想在模拟器中测试这个动作表单,那么你可以在pickPhoto()中编写以下代码来伪造相机的可用性:

if true || UIImagePickerController.isSourceTypeAvailable(.camera) {

这将始终显示动作表单,因为条件现在总是正确的。
动作表中的选项由UIAlertAction对象提供。参数确定当您按下动作表中相应的按钮时发生了什么。
现在这三个选项的处理程序——拍照、从库中选择、取消——都是nil,所以不会发生任何事情。

把这几行改成:

let actPhoto = UIAlertAction(title: "Take Photo", 
      style: .default, handler: { _ in 
        self.takePhotoWithCamera() 
      })
let actLibrary = UIAlertAction(title: "Choose From Library", 
      style: .default, handler: { _ in 
        self.choosePhotoFromLibrary() 
      })

这给出了handler:一个从扩展中调用相应方法的闭包。您使用通配符来忽略传递给这个闭包的参数——对UIAlertAction本身的引用。
运行应用程序并确保动作表单中的按钮正常工作。
在图像选择器出现之前,按下这些按钮之间可能会有一个小的延迟,但这是因为它是一个大组件,iOS需要几秒钟来加载它。
注意,当您取消操作表时,Add Photo单元格仍然被选中(深灰色背景)。看起来不太好。
➤在tableView(
:didSelectRowAt)中,在调用pickPhoto()之前添加以下行:

tableView.deselectRow(at: indexPath, animated: true)

这首先取消Add Photo行。试一下,这样看起来更好。随着动作表单滑进屏幕,单元格背景很快从灰色变回白色。


显示图像

既然用户可以选择照片,你就应该把它显示在某个地方——否则又有什么意义呢?”您将更改Add Photo单元格来保存照片,当选中一张照片时,单元格将会增长以适应照片,而Add Photo标签将会消失。
➤在locationdetailsviewcontroller.swift中为这个类添加了两个新的outlet。

@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var addPhotoLabel: UILabel!

在storyboard中,拖拽一个图像视图到Add Photo单元格中。它有多大,放在哪里并不重要。稍后您将以编程方式将其移动到适当的位置。(这就是为什么你把它做成一个自定义单元格的原因,所以你可以添加这个图像视图到它。)


➤将图像视图连接到视图控制器的imageView outlet。还将Add Photo标签连接到addPhotoLabel outlet。
➤选择图像视图。在属性检查器中,检查它的Hidden属性(在Drawing section绘图部分)。这使得图像视图最初是不可见的,直到您有一张照片提供给它。
➤给图像视图添加左、上、右、下和高度的自动布局约束:


我们将使用一些自动布局约束来移动一些东西,或者在显示图像时扩展图像视图来填充单元格。但是首先,我们需要一个变量来保存选中的图像。
➤给locationdetailsviewcontroller.swift添加一个新的实例变量。

var image: UIImage?

如果还没有选择照片,image将为nil,因此变量必须是可选的。
➤给类添加一个新方法:

func show(image: UIImage) {
  imageView.image = image
  imageView.isHidden = false
  addPhotoLabel.text = ""
}

这将把参数中的图像放到图像视图中,使图像视图可见,并从Add Photo标签中删除标题,以便自动布局约束将图像移到标签所占用的空间中。
➤将imagePickerController(_:didFinishPickingMediaWithInfo:)方法从照片选择扩展更改为:

func imagePickerController(_ picker: UIImagePickerController, 
     didFinishPickingMediaWithInfo info: 
                   [UIImagePickerController.InfoKey : Any]) {

  image = info[UIImagePickerController.InfoKey.editedImage] 
                                                as? UIImage
  if let theImage = image {
    show(image: theImage)
  }

  dismiss(animated: true, completion: nil)
}

当用户在image picker中选择了一张照片时,就会调用这个方法。
你可以通过符号[UIImagePickerController.InfoKey : Any] info参数是一个字典。每当您看到[A: B]时,您正在处理的是一个具有“A”类型键和“B”类型值的字典。
info字典包含描述用户选择的图像的数据。你使用UIImagePickerController.InfoKey.editedImage键来检索包含用户移动和/或缩放后的最终图像的UIImage对象——如果你愿意,你也可以使用不同的键来获取原始图像。

一旦你有了照片,你就把它存储在image实例变量中,这样你以后就可以使用它了。
字典总是返回optionals,因为理论上有一种可能性,您要求的键- UIImagePickerController.InfoKey.editedImage在本例中实际上并不存在于字典中。

由于image实例变量是可选的,所以只需从字典中分配值。

如果信息[UIImagePickerController.InfoKey.editedImage]为nil,那么image也将为nil。您确实需要使用as?将值从无意义的Any转换为UIImage类型。在这种情况下,您需要使用可选的强制转换,比如?而不是!,因为image是一个可选的实例变量。

一旦你有了图像并且它不是nil, 调用show(image:)会把它放到Add Photo单元格中。

练习:看看是否可以重写上面的逻辑,在image实例变量上使用didSet属性观察者。如果你成功了,那么把照片放到image中会自动更新UIImageView,而不需要调用show(image:)

运行应用程序并选择一张照片。哎呀,看起来你有个小问题:

如果你还记得,我们在之前设置自动布局约束时,将图像视图的高度设置为22点左右,因为这是匹配原始行所需的图像高度。然而,当我们显示图像时,我们需要一个更大的值——大约260点。

当然,如果我们一开始就将图像视图高度设置为260,那么图像选择器单元格一开始就会太高。那么我们如何解决这个问题呢?
非常简单——你也可以为自动布局约束建立连接,并在运行时通过代码更改约束值!


调整表格视图单元格大小以显示图像

➤为locationdetailsviewcontroller.swift添加了一个新的图像高度限制的outlet。

@IBOutlet weak var imageHeight: NSLayoutConstraint!

切换到storyboard,然后将新的输出口连接到图像的高度约束——最简单的方法是通过文档大纲,因为你可以从那里选择你想要的精确约束。只需从视图控制器的圆圈中control -拖动到文档大纲中的正确约束,然后从弹出菜单中选择outlet名称imageHeight:

现在,当您显示图像时,您所要做的就是将图像视图的高度限制更改为260 !
➤改变show(image:)的方法:

func show(image: UIImage) {
  ...
  // Add the following lines
  imageHeight.constant = 260
  tableView.reloadData()
}

只需将图像的高度更改为260点,然后刷新表格视图,将照片行设置为适当的高度。
试一试。现在,细胞的大小和足够大的整个照片。


你可以做一个小小的调整。默认情况下,图像视图将拉伸图像以适应整个内容区域。这可能不是你想要的。

设置图像正确显示

进入storyboard并选择图像视图(由于它被隐藏了,所以可能很难看到,但是你仍然可以在文档大纲中找到它)。在属性检查器中,将其 Content Mode内容模式设置为Aspect Fit。


这将保持图像的长宽比不变,因为它是调整大小,以适应图像视图。尝试一下其他内容模式,看看它们是怎么做的。(Aspect Fill类似于Aspect Fit,只是它试图填充整个视图。)


这看起来好多了,但现在图像的顶部和底部有了更大的空白。

练习:使照片表视图单元格的高度动态,这取决于图像的长宽比。这是一个棘手的问题!您可以将图像视图的宽度保持在260点。这应该对应于UIImage对象的宽度。通过image.size.width / image.size.height得到长宽比。使用这个比例,您可以计算图像视图和单元格的高度。你可以在forums.raywenderlich.com上从其他读者那里找到解决方案。


UI的改进

用户现在可以拍照——或者选择一张——但应用程序还没有把它保存到数据存储中。在此之前,对于图像选择器还有一些改进要做。
苹果建议,当用户按下Home键将应用程序移到后台时,应用程序应将屏幕上的任何警告或动作表单移除。
用户可能会在几小时或几天后回到应用程序,他们会忘记自己要做什么。警告或动作表单的出现令人困惑,用户可能会想,它在这里做什么?
为了防止这种情况发生,您将使标记位置屏幕更加专注。当应用程序转到后台时,如果当前正在显示动作表单,它会取消它。你会对图像选择器做同样的事情。

处理背景模式

你在Checklists应用程序中看到,当应用程序要通过它的applicationDidEnterBackground(_:)方法进入后台时,操作系统会通知AppDelegate。
视图控制器没有这样的方法,但幸运的是,iOS通过NotificationCenter发送" going to the background "通知,你可以配置视图控制器来监听。
早些时候,您使用通知中心来观察来自Core Data的通知。这次你会听到UIApplicationDidEnterBackground通知
➤LocationDetailsViewController.swift,添加了一个新方法:

func listenForBackgroundNotification() {
  NotificationCenter.default.addObserver(forName: 
      UIApplication.didEnterBackgroundNotification, 
      object: nil, queue: OperationQueue.main) { _ in

    if self.presentedViewController != nil {
      self.dismiss(animated: false, completion: nil)
    }

    self.descriptionTextView.resignFirstResponder()
  }
}

这为UIApplication.didEnterBackgroundNotification添加了一个观察者。当收到此通知时,NotificationCenter将调用闭包。

注意,这里使用的是“尾随”闭包语法;闭包不是addObserver(forName,…)的参数,而是直接跟随方法调用。
如果有一个活动的图像选取器或动作表,则将其关闭。如果文本视图是活动的,还可以隐藏键盘。

图像选择器和动作表都是作为模态视图控制器呈现的,它们出现在所有其他控件之上。如果这样一个模态视图控制器是活动的,UIViewController的presentedViewController属性有一个对那个模态视图控制器的引用。

因此,如果presentedViewController不是nil,你调用dismiss()来关闭模态屏幕。(顺便说一下,这对类别选择器没有影响;它不使用模态segue,而是使用push segue

➤从viewDidLoad()中调用listenForBackgroundNotification()方法。
➤试一试。打开图像选择器(如果你使用的是带有摄像头的设备,也可以打开动作表单),退出主屏幕,让应用进入休眠状态。

然后点击应用程序的图标再次激活应用程序。您现在应该回到标签位置屏幕-或编辑位置屏幕,如果您选择编辑一个现有的。图像选择器——或动作表单——已经自动关闭。

删除通知观察者

在这一点上,随着iOS版本升级到iOS 9.0,你还需要做一件事——当标签/编辑位置屏幕关闭时,你应该告诉NotificationCenter停止发送这些后台通知。您不希望NotificationCenter将通知发送到不再存在的对象,这是自找麻烦!

然而,从iOS 9.0开始,这就不再是必要的了,因为系统会为您处理所有这些。但是,我们将继续取消观察者的注册,这样您就可以看到它是如何工作的了——同时,我们还将演示另一个问题,我们很快就会讲到:]

deinit方法是取消注册观察者的好地方。
首先,添加一个新的实例变量:

var observer: Any!

这将载有对observer的一个引用,这是以后取消register所必需的。

这个变量的类型是Any!,意思是你并不真正关心这是什么类型的物体。
➤在listenForBackgroundNotification()中,更改第一行,以便它将调用addObserver()的返回值存储到这个新的实例变量中:

func listenForBackgroundNotification() {
  observer = NotificationCenter.default.addObserver(forName: . . .

最后,添加deinit方法:

deinit {
  print("*** deinit \(self)")
  NotificationCenter.default.removeObserver(observer)
}

你在这里添加一个print(),这样你就有证据证明当你关闭标签/编辑位置屏幕时视图控制器确实被销毁了。

➤运行应用程序,编辑一个现有的位置,点击Done关闭屏幕。
我不知道您的情况,但是我在Xcode控制台的任何地方都没有看到*** deinit消息。
你猜怎么着?LocationDetailsViewController不会因为某种原因被销毁。这意味着该应用程序正在泄漏内存……当然,这对我来说是一个很大的设置,所以我可以告诉您关于闭包和捕获:]

还记得在闭包中,当您想访问实例变量或调用方法时,总是必须指定self吗?这是因为闭包捕获闭包内使用的任何变量。

当它捕获一个变量时,闭包只存储对该变量的引用。这允许它在稍后实际执行闭包时使用该变量。

为什么这很重要?如果闭包内的代码使用局部变量,则在执行闭包时,创建该变量的方法可能不再是活动的。毕竟,当一个方法结束时,所有的局部变量都会被销毁。但是,当这样一个局部被闭包捕获时,它将一直保持活动状态,直到闭包也完成为止。
因为闭包需要在捕获和实际执行闭包之间保持对象不被捕获的变量激活,所以它存储了对这些对象的强引用。换句话说,捕获意味着闭包成为捕获对象的共享所有者。

可能不是很明显的是,self也是这些变量之一,因此被闭包捕获。卑鄙的!这就是为什么Swift要求您显式地在闭包中写出self,这样您就不会忘记正在捕获这个值。
在LocationDetailsViewController上下文中,self引用到视图控制器本身。因此,当闭包捕获self时,它创建对LocationDetailsViewController对象的强引用,闭包成为这个视图控制器的共同所有者。我敢打赌你没想到!

记住,只要一个物体有主人,它就会一直活着。这个闭包让视图控制器保持活动,即使在你关闭它之后!
这被称为所有权循环,因为视图控制器本身通过观察者变量有一个对闭包的强引用。

如果你想知道,视图控制器的另一个所有者是UIKit。NotificationCenter也让观察者保持活动状态。
这听起来像一个经典的catch-22问题!幸运的是,有一种方法可以打破所有权循环。您可以给闭包一个捕获列表。你问什么?一切将很快得到解释!

➤将 listenForBackgroundNotification() 更改为:

func listenForBackgroundNotification() {
  observer = NotificationCenter.default.addObserver(
    forName: UIApplication.didEnterBackgroundNotification, 
    object: nil, queue: OperationQueue.main) { [weak self] _ in
    
    if let weakSelf = self {
      if weakSelf.presentedViewController != nil {
        weakSelf.dismiss(animated: false, completion: nil)
      }
      weakSelf.descriptionTextView.resignFirstResponder()
    }
  }
}

这里有一些新东西。让我们看看结尾的第一部分:

{ [weak self] _ in
  . . . 
}

[weak self] 位是闭包的捕获列表。它告诉闭包变量self仍然会被捕获,但是是作为一个弱引用。因此,闭包不再使视图控制器保持活动状态。
弱引用被允许变为nil,这意味着捕获的self现在是闭包内的一个可选的。在向视图控制器发送消息之前,需要使用if let打开它。
除此之外,这次关闭仍然和以前一样。

试一试。打开标记/编辑位置屏幕,然后再次关闭它。现在,您应该在Xcode控制台中看到来自deinit的print()。
这意味着视图控制器被正确销毁,通知观察者被从NotificationCenter中移除。终于解脱了!

请注意,从ios9.0及以上版本开始,即使您没有显式地删除观察者,系统也会为您处理这个问题,并在视图控制器被释放时自动删除观察者。这样你就不用再担心错误观察者的副作用了。
但是自己清理总是一个好主意。使用print() ' s确保对象被释放!Xcode还附带了工具,这是一种方便的工具,您可以使用它来检测此类问题。

你可能感兴趣的:(iOS apprentice中文版 Chapter 30(上): 图片选择器)