你的Tag Locations屏幕的大部分功能都是完整的——除了能夠添加一個位置的照片。是時候解決這個問題了!
UIKit帶有一個內(nèi)置的視圖控制器UIImagePickerController,它允許用戶拍攝新的照片和視頻,或者從他們的照片庫中選擇它們。你要用它來保存一張照片和位置,這樣用戶就有了一張好看的照片。
這是你完成后屏幕的樣子:

在本章中,您將做以下工作:
- 添加一個圖像選擇器:添加一個圖像選擇器到您的應(yīng)用程序中,允許您使用相機拍照或從您的照片庫中選擇現(xiàn)有的圖像。
- 顯示圖像:在表視圖單元格中顯示選中的圖像。
- UI改進:當您的應(yīng)用程序被發(fā)送到后臺時,改進用戶界面功能。
- 保存圖像:保存通過設(shè)備上的圖像選擇器選擇的圖像,以便以后可以檢索到。
- 編輯圖像:如果位置有圖像,則在編輯屏幕上顯示圖像。
- 縮略圖:在位置列表屏幕上顯示位置的縮略圖。
1.添加一個圖像選擇器
就像你需要征得用戶的許可才能從設(shè)備上獲取GPS信息一樣,你也需要獲得訪問用戶照片庫的許可。
您不需要為此編寫任何代碼,但是您需要在應(yīng)用程序的Info.plist中聲明您的意圖。如果你不這樣做,當你嘗試使用UIImagePickerController時,應(yīng)用程序?qū)⒈罎?除了Xcode控制臺中的一條消息外,沒有可見的警告)。
Info.plist的更改
?打開Info.plist并添加一個新行——在現(xiàn)有行上使用plus(+)按鈕,或者右鍵單擊并選擇add row,或者使用Editor→add Item菜單選項。
對于密鑰key,使用NSPhotoLibraryUsageDescription,或者從下拉列表中選擇Privacy - PhotoLibraryUsageDescription。
對于這個值,輸入:Add photos to your locations

?同時添加key NSCameraUsageDescription(或者選擇Privacy - CameraUsageDescription)并給出相同的描述(description)。
現(xiàn)在,當應(yīng)用程序第一次打開照片選擇器或相機時,iOS會使用你剛剛添加到Info.plist中的描述,告訴用戶應(yīng)用程序打算用這些照片做什么。
使用相機添加圖片
?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和其他視圖控制器一樣,是一個視圖控制器,但它內(nèi)置在UIKit中,它負責拍攝新照片的整個過程或從用戶的照片庫中選取它們。
你需要做的就是創(chuàng)建一個UIImagePickerController實例,設(shè)置它的屬性來配置picker,設(shè)置它的委托,然后顯示它。當用戶關(guān)閉圖像選取器屏幕時,委托方法將讓您知道操作的結(jié)果。
這就是你設(shè)計自己視圖控制器的方式,除了你不需要將UIImagePickerController添加到故事板。
注意:您在extension中這樣做是因為它允許您將所有與拍照相關(guān)的功能組合在一起。
如果愿意,可以將這些方法放在主類主體中。這也可以很好地工作,但視圖控制器往往會變得非常大,有很多方法都做不同的事情。
為了保持頭腦清醒,最好提取與概念相關(guān)的方法——比如所有與挑選照片有關(guān)的方法——并將它們放在各自的extension中。
您甚至可以將每個擴展名移動到它們自己的源文件中,例如“LocationDetailsViewController+PhotoPicking.swift”。但就我個人而言,我發(fā)現(xiàn)管理更少的文件是一件好事:]
?將以下方法添加到擴展中:
// 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對象,但現(xiàn)在,您只想確保圖像選擇器出現(xiàn)。
請注意,視圖控制器——在本例中是擴展——必須同時符合UIImagePickerControllerDelegate和UINavigationControllerDelegate,但您不必實現(xiàn)任何UINavigationControllerDelegate方法。
?現(xiàn)在在類中改變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是第二部分的第一行。當它被點擊時,您調(diào)用剛才添加的takePhotoWithCamera()方法。
運行應(yīng)用程序,標記一個新位置或編輯一個現(xiàn)有位置,然后點擊Add Photo。
如果你在模擬器上運行應(yīng)用程序,嘭!它崩潰了。錯誤信息如下:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Source type 1 not available
事故的罪魁禍首是這一行:
imagePicker.sourceType = .camera
并不是所有的設(shè)備都有攝像頭,模擬器也沒有。如果你試圖使用UIImagePickerController和sourceType不被設(shè)備或模擬器支持,應(yīng)用會崩潰。
如果你在你的設(shè)備上運行這個應(yīng)用程序——如果它有一個攝像頭,如果是最近的型號,它很可能有——那么你應(yīng)該會看到這樣的東西:

這和你用iPhone的拍照應(yīng)用拍照時看到的非常相似。MyLocations不允許你錄制視頻,但如果你愿意,你當然可以在自己的應(yīng)用中啟用這一功能。
使用照片庫添加圖像
你仍然可以在模擬器上測試圖像選擇器,但你必須使用照片庫,而不是相機。
?給擴展添加另一種方法:
func choosePhotoFromLibrary() {
let imagePicker = UIImagePickerController()
imagePicker.sourceType = .photoLibrary
imagePicker.delegate = self
imagePicker.allowsEditing = true
present(imagePicker, animated: true, completion: nil)
}
這個方法本質(zhì)上和takePhotoWithCamera做的是相同的事情,只不過現(xiàn)在將sourceType設(shè)置為.photolibrary。
將didSelectRowAt改為調(diào)用choosePhotoFromLibrary()而不是takePhotoWithCamera()。
?在模擬器中運行應(yīng)用程序,點擊Add Photo。
此時,根據(jù)您的iOS版本,您可能需要授予MyLocations訪問照片庫的權(quán)限。如果單擊“不允許”,則“照片選取器”屏幕仍然為空。如果你不小心這樣做了,你可以在設(shè)置應(yīng)用程序的Privacy → Photos下撤銷這個選擇。選擇OK允許應(yīng)用程序使用照片庫。
然而,在iOS 12中,你可能不會得到提示,所以你應(yīng)該可以看到一些庫存圖片。在較老的iOS版本中,你可能根本看不到任何圖像。
如果你因為某種原因沒有看到任何圖片,請停止應(yīng)用程序并點擊模擬器中的內(nèi)置照片應(yīng)用程序。這應(yīng)該會顯示一些示例照片。再次運行應(yīng)用程序,并嘗試選擇一張照片。您現(xiàn)在可能看到這些示例照片,也可能沒有看到。如果沒有,您必須添加您自己的。
有幾種方法可以將新照片添加到模擬器中。你可以進入Safari(在模擬器上),在互聯(lián)網(wǎng)上搜索圖片,按下圖片直到出現(xiàn)菜單,然后選擇Save image:

你也可以簡單地將一個圖像文件拖放到模擬器窗口,而不是上網(wǎng)尋找圖像。這將把圖片添加到照片應(yīng)用程序的庫中。
最后,您可以使用終端和simctl命令。在一行(最后一部分,~/Desktop/MyPhoto.JPG)中鍵入以下內(nèi)容。應(yīng)替換為要添加圖像的實際路徑):
/Applications/Xcode.app/Contents/Developer/usr/bin/simctl addmedia booted ~/Desktop/MyPhoto.JPG
simctl工具可以用來管理您的模擬器 - 輸入simctl help的選項列表。addmedia命令引導將指定的媒體文件添加到活動模擬器。
再次運行應(yīng)用程序?,F(xiàn)在你應(yīng)該可以從照片庫中選擇一張照片:

選擇其中一張照片。屏幕現(xiàn)在變成:

這是因為您將image picker的allowsEditing屬性設(shè)置為true。啟用此設(shè)置后,用戶可以在做出最終選擇之前對照片進行一些快速編輯——在模擬器中,您可以按住Alt/Option,同時拖動來旋轉(zhuǎn)和縮放照片。
因此,您可以使用兩種類型的圖像選擇器:相機和照片庫。相機不會在任何地方都能用。不過,將應(yīng)用程序限制為只能從庫中選擇照片也有點雞肋。
你必須讓應(yīng)用程序更智能一點,讓用戶在相機出現(xiàn)時選擇相機。
選擇相機和圖片庫
首先,你要檢查相機是否可用。如果是,則顯示一個動作表單,讓用戶在相機和照片庫之間進行選擇。
?將以下方法添加到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()方法來檢查是否存在攝像頭。如果沒有,則調(diào)用choosePhotoFromLibrary(),因為這是惟一的選項。但當設(shè)備有攝像頭時,你會在屏幕上顯示一個UIAlertController。
與您以前使用的Alert控制器不同,這個控制器具有. actionsheet樣式。動作表單的工作原理與警告視圖非常相似,不同之處在于它從屏幕底部滑動進來,并為用戶提供幾個選項之一。
在didSelectRowAt中,將調(diào)用choosePhotoFromLibrary()改為pickPhoto()。老實說,這是你最后一次改變路線了。
?在你的設(shè)備上運行這個應(yīng)用程序,看看動作表單是如何運行的:

點擊動作表單中的任何一個按鈕,都只會讓動作表單失效,而不會做任何其他事情。”
順便說一下,如果你想在模擬器中測試這個動作表單,那么你可以在pickPhoto()中編寫以下代碼來偽造相機的可用性:
if true || UIImagePickerController.isSourceTypeAvailable(.camera) {
這將始終顯示動作表單,因為條件現(xiàn)在總是正確的。
動作表中的選項由UIAlertAction對象提供。參數(shù)確定當您按下動作表中相應(yīng)的按鈕時發(fā)生了什么。
現(xiàn)在這三個選項的處理程序——拍照、從庫中選擇、取消——都是nil,所以不會發(fā)生任何事情。
把這幾行改成:
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:一個從擴展中調(diào)用相應(yīng)方法的閉包。您使用通配符來忽略傳遞給這個閉包的參數(shù)——對UIAlertAction本身的引用。
運行應(yīng)用程序并確保動作表單中的按鈕正常工作。
在圖像選擇器出現(xiàn)之前,按下這些按鈕之間可能會有一個小的延遲,但這是因為它是一個大組件,iOS需要幾秒鐘來加載它。
注意,當您取消操作表時,Add Photo單元格仍然被選中(深灰色背景)。看起來不太好。
?在tableView(:didSelectRowAt)中,在調(diào)用pickPhoto()之前添加以下行:
tableView.deselectRow(at: indexPath, animated: true)
這首先取消Add Photo行。試一下,這樣看起來更好。隨著動作表單滑進屏幕,單元格背景很快從灰色變回白色。
顯示圖像
既然用戶可以選擇照片,你就應(yīng)該把它顯示在某個地方——否則又有什么意義呢?”您將更改Add Photo單元格來保存照片,當選中一張照片時,單元格將會增長以適應(yīng)照片,而Add Photo標簽將會消失。
?在locationdetailsviewcontroller.swift中為這個類添加了兩個新的outlet。
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var addPhotoLabel: UILabel!
在storyboard中,拖拽一個圖像視圖到Add Photo單元格中。它有多大,放在哪里并不重要。稍后您將以編程方式將其移動到適當?shù)奈恢谩?這就是為什么你把它做成一個自定義單元格的原因,所以你可以添加這個圖像視圖到它。)

?將圖像視圖連接到視圖控制器的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 = ""
}
這將把參數(shù)中的圖像放到圖像視圖中,使圖像視圖可見,并從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中選擇了一張照片時,就會調(diào)用這個方法。
你可以通過符號[UIImagePickerController.InfoKey : Any] info參數(shù)是一個字典。每當您看到[A: B]時,您正在處理的是一個具有“A”類型鍵和“B”類型值的字典。
info字典包含描述用戶選擇的圖像的數(shù)據(jù)。你使用UIImagePickerController.InfoKey.editedImage鍵來檢索包含用戶移動和/或縮放后的最終圖像的UIImage對象——如果你愿意,你也可以使用不同的鍵來獲取原始圖像。
一旦你有了照片,你就把它存儲在image實例變量中,這樣你以后就可以使用它了。
字典總是返回optionals,因為理論上有一種可能性,您要求的鍵- UIImagePickerController.InfoKey.editedImage在本例中實際上并不存在于字典中。
由于image實例變量是可選的,所以只需從字典中分配值。
如果信息[UIImagePickerController.InfoKey.editedImage]為nil,那么image也將為nil。您確實需要使用as?將值從無意義的Any轉(zhuǎn)換為UIImage類型。在這種情況下,您需要使用可選的強制轉(zhuǎn)換,比如?而不是!,因為image是一個可選的實例變量。
一旦你有了圖像并且它不是nil, 調(diào)用show(image:)會把它放到Add Photo單元格中。
練習:看看是否可以重寫上面的邏輯,在image實例變量上使用didSet屬性觀察者。如果你成功了,那么把照片放到image中會自動更新UIImageView,而不需要調(diào)用show(image:)
運行應(yīng)用程序并選擇一張照片。哎呀,看起來你有個小問題:

如果你還記得,我們在之前設(shè)置自動布局約束時,將圖像視圖的高度設(shè)置為22點左右,因為這是匹配原始行所需的圖像高度。然而,當我們顯示圖像時,我們需要一個更大的值——大約260點。
當然,如果我們一開始就將圖像視圖高度設(shè)置為260,那么圖像選擇器單元格一開始就會太高。那么我們?nèi)绾谓鉀Q這個問題呢?
非常簡單——你也可以為自動布局約束建立連接,并在運行時通過代碼更改約束值!
調(diào)整表格視圖單元格大小以顯示圖像
?為locationdetailsviewcontroller.swift添加了一個新的圖像高度限制的outlet。
@IBOutlet weak var imageHeight: NSLayoutConstraint!
切換到storyboard,然后將新的輸出口連接到圖像的高度約束——最簡單的方法是通過文檔大綱,因為你可以從那里選擇你想要的精確約束。只需從視圖控制器的圓圈中control -拖動到文檔大綱中的正確約束,然后從彈出菜單中選擇outlet名稱imageHeight:

現(xiàn)在,當您顯示圖像時,您所要做的就是將圖像視圖的高度限制更改為260 !
?改變show(image:)的方法:
func show(image: UIImage) {
...
// Add the following lines
imageHeight.constant = 260
tableView.reloadData()
}
只需將圖像的高度更改為260點,然后刷新表格視圖,將照片行設(shè)置為適當?shù)母叨取?br> 試一試?,F(xiàn)在,細胞的大小和足夠大的整個照片。

你可以做一個小小的調(diào)整。默認情況下,圖像視圖將拉伸圖像以適應(yīng)整個內(nèi)容區(qū)域。這可能不是你想要的。
設(shè)置圖像正確顯示
進入storyboard并選擇圖像視圖(由于它被隱藏了,所以可能很難看到,但是你仍然可以在文檔大綱中找到它)。在屬性檢查器中,將其 Content Mode內(nèi)容模式設(shè)置為Aspect Fit。

這將保持圖像的長寬比不變,因為它是調(diào)整大小,以適應(yīng)圖像視圖。嘗試一下其他內(nèi)容模式,看看它們是怎么做的。(Aspect Fill類似于Aspect Fit,只是它試圖填充整個視圖。)

這看起來好多了,但現(xiàn)在圖像的頂部和底部有了更大的空白。
練習:使照片表視圖單元格的高度動態(tài),這取決于圖像的長寬比。這是一個棘手的問題!您可以將圖像視圖的寬度保持在260點。這應(yīng)該對應(yīng)于UIImage對象的寬度。通過image.size.width / image.size.height得到長寬比。使用這個比例,您可以計算圖像視圖和單元格的高度。你可以在forums.raywenderlich.com上從其他讀者那里找到解決方案。
UI的改進
用戶現(xiàn)在可以拍照——或者選擇一張——但應(yīng)用程序還沒有把它保存到數(shù)據(jù)存儲中。在此之前,對于圖像選擇器還有一些改進要做。
蘋果建議,當用戶按下Home鍵將應(yīng)用程序移到后臺時,應(yīng)用程序應(yīng)將屏幕上的任何警告或動作表單移除。
用戶可能會在幾小時或幾天后回到應(yīng)用程序,他們會忘記自己要做什么。警告或動作表單的出現(xiàn)令人困惑,用戶可能會想,它在這里做什么?
為了防止這種情況發(fā)生,您將使標記位置屏幕更加專注。當應(yīng)用程序轉(zhuǎn)到后臺時,如果當前正在顯示動作表單,它會取消它。你會對圖像選擇器做同樣的事情。
處理背景模式
你在Checklists應(yīng)用程序中看到,當應(yīng)用程序要通過它的applicationDidEnterBackground(_:)方法進入后臺時,操作系統(tǒng)會通知AppDelegate。
視圖控制器沒有這樣的方法,但幸運的是,iOS通過NotificationCenter發(fā)送" going to the background "通知,你可以配置視圖控制器來監(jiān)聽。
早些時候,您使用通知中心來觀察來自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將調(diào)用閉包。
注意,這里使用的是“尾隨”閉包語法;閉包不是addObserver(forName,…)的參數(shù),而是直接跟隨方法調(diào)用。
如果有一個活動的圖像選取器或動作表,則將其關(guān)閉。如果文本視圖是活動的,還可以隱藏鍵盤。
圖像選擇器和動作表都是作為模態(tài)視圖控制器呈現(xiàn)的,它們出現(xiàn)在所有其他控件之上。如果這樣一個模態(tài)視圖控制器是活動的,UIViewController的presentedViewController屬性有一個對那個模態(tài)視圖控制器的引用。
因此,如果presentedViewController不是nil,你調(diào)用dismiss()來關(guān)閉模態(tài)屏幕。(順便說一下,這對類別選擇器沒有影響;它不使用模態(tài)segue,而是使用push segue
?從viewDidLoad()中調(diào)用listenForBackgroundNotification()方法。
?試一試。打開圖像選擇器(如果你使用的是帶有攝像頭的設(shè)備,也可以打開動作表單),退出主屏幕,讓應(yīng)用進入休眠狀態(tài)。
然后點擊應(yīng)用程序的圖標再次激活應(yīng)用程序。您現(xiàn)在應(yīng)該回到標簽位置屏幕-或編輯位置屏幕,如果您選擇編輯一個現(xiàn)有的。圖像選擇器——或動作表單——已經(jīng)自動關(guān)閉。
刪除通知觀察者
在這一點上,隨著iOS版本升級到iOS 9.0,你還需要做一件事——當標簽/編輯位置屏幕關(guān)閉時,你應(yīng)該告訴NotificationCenter停止發(fā)送這些后臺通知。您不希望NotificationCenter將通知發(fā)送到不再存在的對象,這是自找麻煩!
然而,從iOS 9.0開始,這就不再是必要的了,因為系統(tǒng)會為您處理所有這些。但是,我們將繼續(xù)取消觀察者的注冊,這樣您就可以看到它是如何工作的了——同時,我們還將演示另一個問題,我們很快就會講到:]
deinit方法是取消注冊觀察者的好地方。
首先,添加一個新的實例變量:
var observer: Any!
這將載有對observer的一個引用,這是以后取消register所必需的。
這個變量的類型是Any!,意思是你并不真正關(guān)心這是什么類型的物體。
?在listenForBackgroundNotification()中,更改第一行,以便它將調(diào)用addObserver()的返回值存儲到這個新的實例變量中:
func listenForBackgroundNotification() {
observer = NotificationCenter.default.addObserver(forName: . . .
最后,添加deinit方法:
deinit {
print("*** deinit \(self)")
NotificationCenter.default.removeObserver(observer)
}
你在這里添加一個print(),這樣你就有證據(jù)證明當你關(guān)閉標簽/編輯位置屏幕時視圖控制器確實被銷毀了。
?運行應(yīng)用程序,編輯一個現(xiàn)有的位置,點擊Done關(guān)閉屏幕。
我不知道您的情況,但是我在Xcode控制臺的任何地方都沒有看到*** deinit消息。
你猜怎么著?LocationDetailsViewController不會因為某種原因被銷毀。這意味著該應(yīng)用程序正在泄漏內(nèi)存……當然,這對我來說是一個很大的設(shè)置,所以我可以告訴您關(guān)于閉包和捕獲:]
還記得在閉包中,當您想訪問實例變量或調(diào)用方法時,總是必須指定self嗎?這是因為閉包捕獲閉包內(nèi)使用的任何變量。
當它捕獲一個變量時,閉包只存儲對該變量的引用。這允許它在稍后實際執(zhí)行閉包時使用該變量。
為什么這很重要?如果閉包內(nèi)的代碼使用局部變量,則在執(zhí)行閉包時,創(chuàng)建該變量的方法可能不再是活動的。畢竟,當一個方法結(jié)束時,所有的局部變量都會被銷毀。但是,當這樣一個局部被閉包捕獲時,它將一直保持活動狀態(tài),直到閉包也完成為止。
因為閉包需要在捕獲和實際執(zhí)行閉包之間保持對象不被捕獲的變量激活,所以它存儲了對這些對象的強引用。換句話說,捕獲意味著閉包成為捕獲對象的共享所有者。
可能不是很明顯的是,self也是這些變量之一,因此被閉包捕獲。卑鄙的!這就是為什么Swift要求您顯式地在閉包中寫出self,這樣您就不會忘記正在捕獲這個值。
在LocationDetailsViewController上下文中,self引用到視圖控制器本身。因此,當閉包捕獲self時,它創(chuàng)建對LocationDetailsViewController對象的強引用,閉包成為這個視圖控制器的共同所有者。我敢打賭你沒想到!
記住,只要一個物體有主人,它就會一直活著。這個閉包讓視圖控制器保持活動,即使在你關(guān)閉它之后!
這被稱為所有權(quán)循環(huán),因為視圖控制器本身通過觀察者變量有一個對閉包的強引用。

如果你想知道,視圖控制器的另一個所有者是UIKit。NotificationCenter也讓觀察者保持活動狀態(tài)。
這聽起來像一個經(jīng)典的catch-22問題!幸運的是,有一種方法可以打破所有權(quán)循環(huán)。您可以給閉包一個捕獲列表。你問什么?一切將很快得到解釋!
?將 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()
}
}
}
這里有一些新東西。讓我們看看結(jié)尾的第一部分:
{ [weak self] _ in
. . .
}
[weak self] 位是閉包的捕獲列表。它告訴閉包變量self仍然會被捕獲,但是是作為一個弱引用。因此,閉包不再使視圖控制器保持活動狀態(tài)。
弱引用被允許變?yōu)閚il,這意味著捕獲的self現(xiàn)在是閉包內(nèi)的一個可選的。在向視圖控制器發(fā)送消息之前,需要使用if let打開它。
除此之外,這次關(guān)閉仍然和以前一樣。
試一試。打開標記/編輯位置屏幕,然后再次關(guān)閉它。現(xiàn)在,您應(yīng)該在Xcode控制臺中看到來自deinit的print()。
這意味著視圖控制器被正確銷毀,通知觀察者被從NotificationCenter中移除。終于解脫了!
請注意,從ios9.0及以上版本開始,即使您沒有顯式地刪除觀察者,系統(tǒng)也會為您處理這個問題,并在視圖控制器被釋放時自動刪除觀察者。這樣你就不用再擔心錯誤觀察者的副作用了。
但是自己清理總是一個好主意。使用print() ' s確保對象被釋放!Xcode還附帶了工具,這是一種方便的工具,您可以使用它來檢測此類問題。