第十五章——相機【譯】

在本章中,您將要添加照片到 Homepwner 應用程序。 您將呈現一個 UIImagePickerController,以便用戶可以拍攝并保存每個 item 的圖片。 然后,圖像將與 Item 實例相關聯,并在 item 的詳情視圖中查看(圖15.1)。

圖15.1帶攝像頭的 Homepwner

圖像往往非常大,所以將圖像與其他數據分開存儲是個好主意。 因此,您將要創建第二個 store 來存儲圖像。 ImageStore 將根據需要獲取并緩存圖像。

顯示圖像和 UIImageView

您的第一步是讓 DetailViewController 獲取并顯示圖像。 顯示圖像的簡單方法是將 UIImageView 的實例放在屏幕上。

打開 Homepwner.xcodeprojMain.storyboard。 然后將 UIImageView 的實例拖動到棧視圖底部的視圖上。 選擇 圖像視圖 并打開其尺寸檢查器。 您希望圖像視圖的 Vertical Content Hugging PriorityVertical Content Compression Resistance Priority 低于其他視圖。 將 Vertical Content Hugging Priority 更改為 248,Vertical Content Compression Resistance Priority 為 749。

您的布局將如圖15.2所示。

圖15.2 DetailViewController 的視圖中的 UIImageView

UIImageView 根據圖像視圖的 contentMode 屬性顯示圖像。 此屬性決定圖像視圖框架內的位置和如何調整大小。 UIImageViewcontentMode 的默認值是 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 符合 UINavigationControllerDelegateUIImagePickerControllerDelegate 協議。

class DetailViewController: UIViewController, UITextFieldDelegate, UINavigationControllerDelegate, UIImagePickerControllerDelegate {

為什么是 UINavigationControllerDelegateUIImagePickerControllerdelegate 屬性實際上是從其父類 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 類型的 NSPhotoLibraryUsageDescriptionValueThis 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章中為 ItemsViewControllerItemStore 所做的那樣。

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。 ImageStoreItem 都將知道圖像的 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:注釋,您將強制自己組織您的代碼。 如果你組織的好,這將使您的代碼更易于閱讀,更易于使用。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容