版本记录
版本号 | 时间 |
---|---|
V1.0 | 2018.08.21 |
前言
信号量机制是多线程通信中的比较重要的一部分,对于
NSOperation
可以设置并发数,但是对于GCD
就不能设置并发数了,那么就只能靠信号量机制了。接下来这几篇就会详细的说一下并发机制。感兴趣的可以看这几篇文章。
1. iOS与多线程(一) —— GCD中的信号量及几个重要函数
2. iOS与多线程(二) —— NSOperation实现多并发之创建任务
3. iOS与多线程(三) —— NSOperation实现多并发之创建队列和开启线程
4. iOS与多线程(四) —— NSOperation的串并行和操作依赖
5. iOS与多线程(五) —— GCD之一个简单应用示例(一)
6. iOS与多线程(六) —— GCD之一个简单应用示例(二)
源码
1. PhotoCollectionViewController.swift
import UIKit
import Photos
private let reuseIdentifier = "photoCell"
private let backgroundImageOpacity: CGFloat = 0.1
final class PhotoCollectionViewController: UICollectionViewController {
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
let backgroundImageView = UIImageView(image: UIImage(named:"background"))
backgroundImageView.alpha = backgroundImageOpacity
backgroundImageView.contentMode = .center
collectionView?.backgroundView = backgroundImageView
NotificationCenter.default.addObserver(
self,
selector: #selector(contentChangedNotification(_:)),
name: PhotoManagerNotification.contentUpdated,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(contentChangedNotification(_:)),
name: PhotoManagerNotification.contentAdded,
object: nil)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
showOrHideNavPrompt()
}
// MARK: - IBAction Methods
@IBAction func addPhotoAssets(_ sender: Any) {
let alert = UIAlertController(title: "Get Photos From:", message: nil, preferredStyle: .actionSheet)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alert.addAction(cancelAction)
let libraryAction = UIAlertAction(title: "Photo Library", style: .default) { _ in
let viewController = self.storyboard?.instantiateViewController(withIdentifier: "AlbumsStoryboard") as? UINavigationController
if let viewController = viewController,
let albumsTableViewController = viewController.topViewController as? AlbumsTableViewController {
albumsTableViewController.assetPickerDelegate = self
self.present(viewController, animated: true, completion: nil)
}
}
alert.addAction(libraryAction)
let internetAction = UIAlertAction(title: "Le Internet", style: .default) { _ in
self.downloadImageAssets()
}
alert.addAction(internetAction)
present(alert, animated: true, completion: nil)
}
}
// MARK: - Private Methods
private extension PhotoCollectionViewController {
func showOrHideNavPrompt() {
let delayInSeconds = 2.0
DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) { [weak self] in
guard let self = self else {
return
}
if PhotoManager.shared.photos.count > 0 {
self.navigationItem.prompt = nil
} else {
self.navigationItem.prompt = "Add photos with faces to Googlyify them!"
}
self.navigationController?.viewIfLoaded?.setNeedsLayout()
}
}
func downloadImageAssets() {
PhotoManager.shared.downloadPhotos() { [weak self] error in
let message = error?.localizedDescription ?? "The images have finished downloading"
let alert = UIAlertController(title: "Download Complete", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
self?.present(alert, animated: true, completion: nil)
}
}
}
// MARK: - Notification handlers
extension PhotoCollectionViewController {
@objc func contentChangedNotification(_ notification: Notification!) {
collectionView?.reloadData()
showOrHideNavPrompt()
}
}
// MARK: - UICollectionViewDataSource
extension PhotoCollectionViewController {
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return PhotoManager.shared.photos.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! PhotoCollectionViewCell
let photoAssets = PhotoManager.shared.photos
let photo = photoAssets[indexPath.row]
switch photo.statusThumbnail {
case .goodToGo:
cell.thumbnailImage = photo.thumbnail
case .downloading:
cell.thumbnailImage = UIImage(named: "photoDownloading")
case .failed:
cell.thumbnailImage = UIImage(named: "photoDownloadError")
}
return cell
}
}
// MARK: - UICollectionViewDelegate
extension PhotoCollectionViewController {
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let photos = PhotoManager.shared.photos
let photo = photos[indexPath.row]
switch photo.statusImage {
case .goodToGo:
let viewController = storyboard?.instantiateViewController(withIdentifier: "PhotoDetailStoryboard") as? PhotoDetailViewController
if let viewController = viewController {
viewController.image = photo.image
navigationController?.pushViewController(viewController, animated: true)
}
case .downloading:
let alert = UIAlertController(title: "Downloading",
message: "The image is currently downloading",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
present(alert, animated: true, completion: nil)
case .failed:
let alert = UIAlertController(title: "Image Failed",
message: "The image failed to be created",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
present(alert, animated: true, completion: nil)
}
}
}
// MARK: - AssetPickerDelegate
extension PhotoCollectionViewController: AssetPickerDelegate {
func assetPickerDidCancel() {
dismiss(animated: true, completion: nil)
}
func assetPickerDidFinishPickingAssets(_ selectedAssets: [PHAsset]) {
for asset in selectedAssets {
let photo = AssetPhoto(asset: asset)
PhotoManager.shared.addPhoto(photo)
}
dismiss(animated: true, completion: nil)
}
}
2. PhotoCollectionViewCell.swift
import UIKit
final class PhotoCollectionViewCell: UICollectionViewCell {
@IBOutlet var imageView: UIImageView!
var representedAssetIdentifier: String!
var thumbnailImage: UIImage! {
didSet {
imageView.image = thumbnailImage
}
}
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
}
}
3. PhotoDetailViewController.swift
import UIKit
private struct ScaleFactor {
static let retinaToEye: CGFloat = 0.5
static let faceBoundsToEye: CGFloat = 4.0
}
final class PhotoDetailViewController: UIViewController {
@IBOutlet weak var photoImageView: UIImageView!
var image: UIImage!
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
assert(image != nil, "Image not set; required to use view controller")
photoImageView.image = image
// Resize if neccessary to ensure it's not pixelated
if image.size.height <= photoImageView.bounds.size.height &&
image.size.width <= photoImageView.bounds.size.width {
photoImageView.contentMode = .center
}
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else {
return
}
let overlayImage = self.faceOverlayImageFrom(self.image)
DispatchQueue.main.async { [weak self] in
self?.fadeInNewImage(overlayImage)
}
}
}
}
// MARK: - Private Methods
private extension PhotoDetailViewController {
func faceOverlayImageFrom(_ image: UIImage) -> UIImage {
let detector = CIDetector(ofType: CIDetectorTypeFace,
context: nil, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])
// Get features from the image
let newImage = CIImage(cgImage: image.cgImage!)
let features = detector?.features(in: newImage) as! [CIFaceFeature]
UIGraphicsBeginImageContext(image.size)
let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
// Draws this in the upper left coordinate system
image.draw(in: imageRect, blendMode: .normal, alpha: 1.0)
let context = UIGraphicsGetCurrentContext()
for faceFeature in features {
let faceRect = faceFeature.bounds
context!.saveGState()
// CI and CG work in different coordinate systems, we should translate to
// the correct one so we don't get mixed up when calculating the face position.
context!.translateBy(x: 0.0, y: imageRect.size.height)
context!.scaleBy(x: 1.0, y: -1.0)
if faceFeature.hasLeftEyePosition {
let leftEyePosition = faceFeature.leftEyePosition
let eyeWidth = faceRect.size.width / ScaleFactor.faceBoundsToEye
let eyeHeight = faceRect.size.height / ScaleFactor.faceBoundsToEye
let eyeRect = CGRect(x: leftEyePosition.x - eyeWidth / 2.0,
y: leftEyePosition.y - eyeHeight / 2.0,
width: eyeWidth,
height: eyeHeight)
drawEyeBallForFrame(eyeRect)
}
if faceFeature.hasRightEyePosition {
let leftEyePosition = faceFeature.rightEyePosition
let eyeWidth = faceRect.size.width / ScaleFactor.faceBoundsToEye
let eyeHeight = faceRect.size.height / ScaleFactor.faceBoundsToEye
let eyeRect = CGRect(x: leftEyePosition.x - eyeWidth / 2.0,
y: leftEyePosition.y - eyeHeight / 2.0,
width: eyeWidth,
height: eyeHeight)
drawEyeBallForFrame(eyeRect)
}
context!.restoreGState();
}
let overlayImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return overlayImage!
}
func faceRotationInRadians(leftEyePoint startPoint: CGPoint, rightEyePoint endPoint: CGPoint) -> CGFloat {
let deltaX = endPoint.x - startPoint.x
let deltaY = endPoint.y - startPoint.y
let angleInRadians = CGFloat(atan2f(Float(deltaY), Float(deltaX)))
return angleInRadians;
}
func drawEyeBallForFrame(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
context?.addEllipse(in: rect)
context?.setFillColor(UIColor.white.cgColor)
context?.fillPath()
let eyeSizeWidth = rect.size.width * ScaleFactor.retinaToEye
let eyeSizeHeight = rect.size.height * ScaleFactor.retinaToEye
var x = CGFloat(arc4random_uniform(UInt32(rect.size.width - eyeSizeWidth)))
var y = CGFloat(arc4random_uniform(UInt32(rect.size.height - eyeSizeHeight)))
x += rect.origin.x
y += rect.origin.y
let eyeSize = min(eyeSizeWidth, eyeSizeHeight)
let eyeBallRect = CGRect(x: x, y: y, width: eyeSize, height: eyeSize)
context?.addEllipse(in: eyeBallRect)
context?.setFillColor(UIColor.black.cgColor)
context?.fillPath()
}
func fadeInNewImage(_ newImage: UIImage) {
let tmpImageView = UIImageView(image: newImage)
tmpImageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
tmpImageView.contentMode = photoImageView.contentMode
tmpImageView.frame = photoImageView.bounds
tmpImageView.alpha = 0.0
photoImageView.addSubview(tmpImageView)
UIView.animate(withDuration: 0.75, animations: {
tmpImageView.alpha = 1.0
}, completion: { finished in
self.photoImageView.image = newImage
tmpImageView.removeFromSuperview()
})
}
}
4. Photo.swift
import UIKit
import Photos
typealias PhotoDownloadCompletionBlock = (_ image: UIImage?, _ error: NSError?) -> Void
typealias PhotoDownloadProgressBlock = (_ completed: Int, _ total: Int) -> Void
enum PhotoStatus {
case downloading
case goodToGo
case failed
}
private let scale = UIScreen.main.scale
private let cellSize = CGSize(width: 64, height: 64)
private let thumbnailSize = CGSize(width: cellSize.width * scale, height: cellSize.height * scale)
private let imageManager = PHImageManager()
protocol Photo {
var statusImage: PhotoStatus { get }
var statusThumbnail: PhotoStatus { get }
var image: UIImage? { get }
var thumbnail: UIImage? { get }
}
class AssetPhoto: Photo {
var statusImage: PhotoStatus = .downloading
var statusThumbnail: PhotoStatus = .downloading
var image: UIImage?
var thumbnail: UIImage?
let asset: PHAsset
let representedAssetIdentifier: String
private let targetSize: CGSize = CGSize(width:600, height:600)
init(asset: PHAsset) {
self.asset = asset
representedAssetIdentifier = asset.localIdentifier
fetchThumbnailImage()
fetchImage()
}
private func fetchThumbnailImage() {
let options = PHImageRequestOptions()
options.isNetworkAccessAllowed = true
imageManager.requestImage(for: asset, targetSize: thumbnailSize,
contentMode: .aspectFill, options: options) { image, info in
if let image = image {
if self.representedAssetIdentifier == self.asset.localIdentifier {
self.thumbnail = image
self.statusThumbnail = .goodToGo
}
} else if let info = info,
let _ = info[PHImageErrorKey] as? NSError {
self.statusThumbnail = .failed
}
}
}
private func fetchImage() {
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
options.isNetworkAccessAllowed = true
PHImageManager.default().requestImage(for: asset, targetSize: targetSize,
contentMode: .aspectFill, options: options) { image, info in
if let image = image {
if image.size.width < self.targetSize.width / 2 {
DispatchQueue.main.asyncAfter(deadline: .now(), execute: self.fetchImage)
}
if self.representedAssetIdentifier == self.asset.localIdentifier {
self.image = image
self.statusImage = .goodToGo
}
} else if let info = info,
let _ = info[PHImageErrorKey] as? NSError {
self.statusImage = .failed
}
}
}
}
private let downloadSession = URLSession(configuration: URLSessionConfiguration.ephemeral)
class DownloadPhoto: Photo {
var statusImage: PhotoStatus = .downloading
var statusThumbnail: PhotoStatus = .downloading
var image: UIImage?
var thumbnail: UIImage?
let url: URL
init(url: URL, completion: PhotoDownloadCompletionBlock?) {
self.url = url
downloadImage(completion)
}
convenience init(url: URL) {
self.init(url: url, completion: nil)
}
private func downloadImage(_ completion: PhotoDownloadCompletionBlock?) {
let task = downloadSession.dataTask(with: url) { data, response, error in
if let data = data {
self.image = UIImage(data: data)
}
if error == nil && self.image != nil {
self.statusImage = .goodToGo
self.statusThumbnail = .goodToGo
} else {
self.statusImage = .failed
self.statusThumbnail = .failed
}
self.thumbnail = self.image?.thumbnailImage(Int(cellSize.width), transparentBorder: 0,
cornerRadius: 0, interpolationQuality: CGInterpolationQuality.default)
completion?(self.image, error as NSError?)
DispatchQueue.main.async {
NotificationCenter.default.post(name: PhotoManagerNotification.contentUpdated, object: nil)
}
}
task.resume()
}
}
5. PhotoManager.swift
import UIKit
struct PhotoManagerNotification {
// Notification when new photo instances are added
static let contentAdded = Notification.Name("com.raywenderlich.GooglyPuff.PhotoManagerContentAdded")
// Notification when content updates (i.e. Download finishes)
static let contentUpdated = Notification.Name("com.raywenderlich.GooglyPuff.PhotoManagerContentUpdated")
}
struct PhotoURLString {
// Photo Credit: Devin Begley, http://www.devinbegley.com/
static let overlyAttachedGirlfriend = "https://i.imgur.com/UvqEgCv.png"
static let successKid = "https://i.imgur.com/dZ5wRtb.png"
static let lotsOfFaces = "https://i.imgur.com/tPzTg7A.jpg"
}
typealias PhotoProcessingProgressClosure = (_ completionPercentage: CGFloat) -> Void
typealias BatchPhotoDownloadingCompletionClosure = (_ error: NSError?) -> Void
class PhotoManager {
private init() {}
static let shared = PhotoManager()
private let concurrentPhotoQueue =
DispatchQueue(
label: "com.raywenderlich.GooglyPuff.photoQueue",
attributes: .concurrent)
private var unsafePhotos: [Photo] = []
var photos: [Photo] {
var photosCopy: [Photo] = []
concurrentPhotoQueue.sync {
photosCopy = self.unsafePhotos
}
return photosCopy
}
func addPhoto(_ photo: Photo) {
concurrentPhotoQueue.async(flags: .barrier) { [weak self] in
guard let self = self else {
return
}
self.unsafePhotos.append(photo)
DispatchQueue.main.async { [weak self] in
self?.postContentAddedNotification()
}
}
}
func downloadPhotos(withCompletion completion: BatchPhotoDownloadingCompletionClosure?) {
var storedError: NSError?
for address in [PhotoURLString.overlyAttachedGirlfriend,
PhotoURLString.successKid,
PhotoURLString.lotsOfFaces] {
let url = URL(string: address)
let photo = DownloadPhoto(url: url!) { _, error in
if error != nil {
storedError = error
}
}
PhotoManager.shared.addPhoto(photo)
}
completion?(storedError)
}
private func postContentAddedNotification() {
NotificationCenter.default.post(name: PhotoManagerNotification.contentAdded, object: nil)
}
}
6. AssetPickerDelegate.swift
import Foundation
import Photos
protocol AssetPickerDelegate {
func assetPickerDidFinishPickingAssets(_ selectedAssets: [PHAsset])
func assetPickerDidCancel()
}
7. AlbumsTableViewController.swift
import UIKit
import Photos
private let reuseIdentifier = "AlbumsCell"
class AlbumsTableViewController: UITableViewController {
var selectedAssets: SelectedAssets?
var assetPickerDelegate: AssetPickerDelegate?
private let sectionNames = ["","Albums"]
private var userLibrary: PHFetchResult<PHAssetCollection>!
private var userAlbums: PHFetchResult<PHCollection>!
private var doneButton: UIBarButtonItem!
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
if selectedAssets == nil {
selectedAssets = SelectedAssets()
}
PHPhotoLibrary.requestAuthorization { status in
DispatchQueue.main.async {
switch status {
case .authorized:
self.fetchCollections()
self.tableView.reloadData()
default:
self.showNoAccessAlertAndCancel()
}
}
}
doneButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(donePressed(_:)))
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
updateDoneButton()
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let destination = segue.destination
as! AssetsCollectionViewController
// Set up AssetCollectionViewController
destination.selectedAssets = selectedAssets
let cell = sender as! UITableViewCell
destination.title = cell.textLabel!.text
destination.assetPickerDelegate = self
let options = PHFetchOptions()
options.sortDescriptors =
[NSSortDescriptor(key: "creationDate", ascending: true)]
let indexPath = tableView.indexPath(for: cell)!
switch (indexPath.section) {
case 0:
// Camera Roll
let library = userLibrary[indexPath.row]
destination.assetsFetchResults =
PHAsset.fetchAssets(in: library,
options: options) as? PHFetchResult<AnyObject>
case 1:
// Albums
let album = userAlbums[indexPath.row] as! PHAssetCollection
destination.assetsFetchResults =
PHAsset.fetchAssets(in: album,
options: options) as? PHFetchResult<AnyObject>
default:
break
}
}
@IBAction func cancelPressed(_ sender: Any) {
assetPickerDelegate?.assetPickerDidCancel()
}
@IBAction func donePressed(_ sender: Any) {
// Should only be invoked when there are selected assets
if let assets = selectedAssets?.assets {
assetPickerDelegate?.assetPickerDidFinishPickingAssets(assets)
// Clear out selections
selectedAssets?.assets.removeAll()
}
}
}
// MARK: - Private Methods
extension AlbumsTableViewController {
func fetchCollections() {
userAlbums = PHCollectionList.fetchTopLevelUserCollections(with: nil)
userLibrary = PHAssetCollection.fetchAssetCollections(
with: .smartAlbum,
subtype: .smartAlbumUserLibrary,
options: nil)
}
func showNoAccessAlertAndCancel() {
let alert = UIAlertController(title: "No Photo Permissions", message: "Please grant photo permissions in Settings", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
alert.addAction(UIAlertAction(title: "Settings", style: .default, handler: { _ in
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
return
}))
self.present(alert, animated: true, completion: nil)
}
private func updateDoneButton() {
guard let selectedAssets = selectedAssets else { return }
// Add a done button when there are selected assets
if selectedAssets.assets.count > 0 {
navigationItem.rightBarButtonItem = doneButton
} else {
navigationItem.rightBarButtonItem = nil
}
}
}
// MARK: - Table view data source
extension AlbumsTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return sectionNames.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch (section) {
case 0:
return userLibrary?.count ?? 0
case 1:
return userAlbums?.count ?? 0
default:
return 0
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath)
cell.textLabel!.text = ""
switch(indexPath.section) {
case 0:
let library = userLibrary[indexPath.row]
var title = library.localizedTitle!
if (library.estimatedAssetCount != NSNotFound) {
title += " (\(library.estimatedAssetCount))"
}
cell.textLabel!.text = title
case 1:
let album = userAlbums[indexPath.row] as! PHAssetCollection
var title = album.localizedTitle!
if (album.estimatedAssetCount != NSNotFound) {
title += " (\(album.estimatedAssetCount))"
}
cell.textLabel!.text = title
default:
break
}
cell.accessoryType = .disclosureIndicator
return cell
}
}
// MARK: - AssetPickerDelegate
extension AlbumsTableViewController: AssetPickerDelegate {
func assetPickerDidCancel() {
assetPickerDelegate?.assetPickerDidCancel()
}
func assetPickerDidFinishPickingAssets(_ selectedAssets: [PHAsset]) {
assetPickerDelegate?.assetPickerDidFinishPickingAssets(selectedAssets)
// Clear out selections
self.selectedAssets?.assets.removeAll()
}
}
8. AssetsCollectionViewController.swift
import UIKit
import Photos
private let reuseIdentifier = "AssetCell"
class AssetsCollectionViewController: UICollectionViewController {
// MARK: - Variables
var assetsFetchResults: PHFetchResult<AnyObject>?
var selectedAssets: SelectedAssets!
var assetPickerDelegate: AssetPickerDelegate?
private var thumbnailSize = CGSize.zero
private let imageManager = PHImageManager()
private var doneButton: UIBarButtonItem!
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
collectionView!.allowsMultipleSelection = true
doneButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(donePressed(_:)))
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Determine the size of the thumbnails to request from the PHCachingImageManager
let scale = UIScreen.main.scale
let cellSize = (collectionViewLayout as! UICollectionViewFlowLayout).itemSize
thumbnailSize = CGSize(width: cellSize.width * scale, height: cellSize.height * scale)
collectionView!.reloadData()
updateSelectedItems()
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
collectionView!.reloadData()
updateSelectedItems()
}
// MARK: - Button handlers
@objc func donePressed(_ sender: UIBarButtonItem) {
assetPickerDelegate?.assetPickerDidFinishPickingAssets(selectedAssets.assets)
}
}
// MARK: - Private Methods
extension AssetsCollectionViewController {
private func updateSelectedItems() {
guard selectedAssets != nil else { return }
// Select the selected items
if let fetchResult = assetsFetchResults {
for asset in selectedAssets.assets {
let index = fetchResult.index(of: asset)
if index != NSNotFound {
let indexPath = IndexPath(item: index, section: 0)
collectionView!.selectItem(at: indexPath,
animated: false, scrollPosition: UICollectionView.ScrollPosition())
}
}
} else {
for i in 0..<selectedAssets.assets.count {
let indexPath = IndexPath(item: i, section: 0)
collectionView!.selectItem(at: indexPath,
animated: false, scrollPosition: UICollectionView.ScrollPosition())
}
}
updateDoneButton()
}
private func updateDoneButton() {
guard selectedAssets != nil else { return }
// Add a done button when there are selected assets
if selectedAssets.assets.count > 0 {
navigationItem.rightBarButtonItem = doneButton
} else {
navigationItem.rightBarButtonItem = nil
}
}
}
// MARK: - UICollectionViewDataSource
extension AssetsCollectionViewController {
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return assetsFetchResults?.count ?? 0
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! AssetCollectionViewCell
// Configure the cell
if let fetchResults = assetsFetchResults {
let asset = fetchResults[indexPath.item] as! PHAsset
let options = PHImageRequestOptions()
options.isNetworkAccessAllowed = true
cell.representedAssetIdentifier = asset.localIdentifier
imageManager.requestImage(for: asset,
targetSize: thumbnailSize,
contentMode: .aspectFill,
options: options) { image, _ in
if cell.representedAssetIdentifier == asset.localIdentifier {
cell.thumbnailImage = image
}
}
}
return cell
}
}
// MARK: - UICollectionViewDelegate
extension AssetsCollectionViewController {
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// Update selected Assets
if let asset = assetsFetchResults?[indexPath.item] as? PHAsset {
selectedAssets.assets.append(asset)
updateDoneButton()
}
}
override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
// Update de-selected Assets
if let assetToDelete = assetsFetchResults?[indexPath.item] as? PHAsset {
selectedAssets.assets = selectedAssets.assets.filter { asset in
!(asset == assetToDelete)
}
if assetsFetchResults == nil {
collectionView.deleteItems(at: [indexPath])
}
updateDoneButton()
}
}
}
9. AssetCollectionViewCell.swift
import UIKit
class AssetCollectionViewCell: UICollectionViewCell {
@IBOutlet var imageView: UIImageView!
@IBOutlet private var checkMark: UIView?
var representedAssetIdentifier: String!
var thumbnailImage: UIImage! {
didSet {
imageView.image = thumbnailImage
}
}
override var isSelected: Bool {
didSet {
checkMark?.isHidden = !isSelected
}
}
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
}
}
10. CheckMark.swift
import UIKit
import CoreGraphics
class CheckMark: UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
drawRectChecked()
}
func drawRectChecked() {
// General Declarations
let context = UIGraphicsGetCurrentContext()
// Color Declarations
let checkmarkBlue2 = UIColor(red: 0.078, green: 0.435, blue: 0.875, alpha: 1.0)
// Shadow Declarations
let shadow2 = UIColor.black
let shadow2Offset = CGSize(width: 0.1, height: -0.1)
let shadow2BlurRadius = 2.5
// Frames
let frame = bounds
// Subframes
let group = CGRect(x: frame.minX + 3, y: frame.minY + 3, width: frame.width - 6, height: frame.height - 6)
// CheckedOval Drawing
let checkedOvalPath = UIBezierPath(ovalIn:CGRect(x: group.minX + 0.5, y: group.minY + 0.5, width: group.width + 1, height: group.height + 1))
context?.saveGState()
context?.setShadow(offset: shadow2Offset, blur: CGFloat(shadow2BlurRadius), color: shadow2.cgColor)
checkmarkBlue2.setFill()
checkedOvalPath.fill()
context?.restoreGState()
UIColor.white.setStroke()
checkedOvalPath.lineWidth = 1
checkedOvalPath.stroke()
// Bezier Drawing
let bezierPath = UIBezierPath()
bezierPath.move(to: CGPoint(x: group.minX + 0.27083 * group.width, y: group.minY + 0.54167 * group.height))
bezierPath.addLine(to: CGPoint(x: group.minX + 0.41667 * group.width, y: group.minY + 0.68750 * group.height))
bezierPath.addLine(to: CGPoint(x: group.minX + 0.75000 * group.width, y: group.minY + 0.35417 * group.height))
bezierPath.lineCapStyle = .square
UIColor.white.setStroke()
bezierPath.lineWidth = 1.3
bezierPath.stroke()
}
}
11. SelectedAssets.swift
import Foundation
import Photos
class SelectedAssets: NSObject {
var assets: [PHAsset]
override init() {
assets = []
}
init(assets:[PHAsset]) {
self.assets = assets
}
}
12. UIImage+Alpha.swift
import UIKit
extension UIImage {
var hasAlpha: Bool {
guard let cgImage = self.cgImage else { return false }
let alpha = cgImage.alphaInfo
return alpha == .first || alpha == .last || alpha == .premultipliedFirst || alpha == .premultipliedLast
}
func imageWithAlpha() -> UIImage {
if hasAlpha { return self }
let scale = max(self.scale, 1)
guard let imageRef = self.cgImage, let colorSpace = imageRef.colorSpace else { return self }
let width = imageRef.width * Int(scale)
let height = imageRef.height * Int(scale)
guard let offscreenContext = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: UInt32(CGBitmapInfo.alphaInfoMask.rawValue) & UInt32(CGImageAlphaInfo.premultipliedLast.rawValue)) else {
return self
}
offscreenContext.draw(imageRef, in: CGRect(x: 0, y: 0, width: width, height: height))
guard let imageRefWithAlpha = offscreenContext.makeImage() else { return self }
return UIImage(cgImage: imageRefWithAlpha, scale: self.scale, orientation: .up)
}
func transparentBorderImage(borderSize: CGFloat) -> UIImage {
let image = imageWithAlpha()
let scale = max(self.scale, 1)
let scaledBorderSize = borderSize * scale
let newRect = CGRect(x: 0, y: 0, width: image.size.width * scale + scaledBorderSize * 2, height: image.size.height * scale + scaledBorderSize * 2)
guard let imageRef = self.cgImage, let colorSpace = imageRef.colorSpace else {
return self
}
guard let bitmap = CGContext(data: nil, width: Int(newRect.size.width), height: Int(newRect.size.height), bitsPerComponent: imageRef.bitsPerComponent, bytesPerRow: imageRef.bytesPerRow, space: colorSpace, bitmapInfo: imageRef.bitmapInfo.rawValue) else {
return self
}
let imageLocation = CGRect(x: scaledBorderSize, y: scaledBorderSize, width: image.size.width * scale, height: image.size.height * scale)
bitmap.draw(imageRef, in: imageLocation)
guard let borderImageRef = bitmap.makeImage() else {
return self
}
let maskImageRef = newBorderMask(borderSize: scaledBorderSize, size: newRect.size)
guard let transparentBorderImageRef = borderImageRef.masking(maskImageRef) else {
return self
}
return UIImage(cgImage: transparentBorderImageRef, scale: self.scale, orientation: .up)
}
func newBorderMask(borderSize: CGFloat, size: CGSize) -> CGImage {
let colorSpace = CGColorSpaceCreateDeviceGray()
let maskContext = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: 8, space: colorSpace, bitmapInfo: UInt32(CGImageAlphaInfo.none.rawValue))!
maskContext.setFillColor(UIColor.black.cgColor)
maskContext.fill(CGRect(x: 0, y: 0, width: size.width, height: size.height))
maskContext.setFillColor(UIColor.white.cgColor)
maskContext.fill(CGRect(x: borderSize, y: borderSize, width: size.width - borderSize * 2, height: size.height - borderSize * 2))
return maskContext.makeImage()!
}
}
13. UIImage+Resize.swift
import UIKit
extension UIImage {
func croppedImage(bounds: CGRect) -> UIImage {
let scale = max(self.scale, 1)
let scaledBounds = CGRect(x: bounds.origin.x * scale, y: bounds.origin.y * scale, width: bounds.size.width * scale, height: bounds.size.height * scale)
guard let imageRef = cgImage?.cropping(to: scaledBounds) else {
return self
}
let croppedImage = UIImage(cgImage: imageRef, scale: self.scale, orientation: .up)
return croppedImage
}
func thumbnailImage(_ size: Int, transparentBorder borderSize: CGFloat, cornerRadius: CGFloat, interpolationQuality quality: CGInterpolationQuality) -> UIImage {
let resizedImage = self.resizedImage(withContentMode: .scaleAspectFill, bounds: CGSize(width: size, height: size), interpolationQuality: quality)
let cropRect = CGRect(x: round((resizedImage.size.width - CGFloat(size)) / 2), y: round((resizedImage.size.height - CGFloat(size)) / 2), width: CGFloat(size), height: CGFloat(size))
let croppedImage = resizedImage.croppedImage(bounds: cropRect)
let transparentBorderImage = borderSize != 0 ? croppedImage.transparentBorderImage(borderSize: borderSize) : croppedImage
return transparentBorderImage.roundedCornerImage(cornerSize: cornerRadius, borderSize: borderSize)
}
func resizedImage(newSize: CGSize, interpolationQuality quality: CGInterpolationQuality) -> UIImage {
let drawTransposed: Bool
switch imageOrientation {
case .left, .leftMirrored, .right, .rightMirrored:
drawTransposed = true
default:
drawTransposed = false
}
let transform = transformForOrientation(newSize: newSize)
return resizedImage(newSize: newSize, transform: transform, drawTransposed: drawTransposed, interpolationQuality: quality)
}
func resizedImage(withContentMode contentMode: UIView.ContentMode, bounds: CGSize, interpolationQuality quality: CGInterpolationQuality) -> UIImage {
let horizontalRatio = bounds.width / size.width
let verticalRatio = bounds.height / size.height
let ratio: CGFloat
switch contentMode {
case .scaleAspectFill:
ratio = max(horizontalRatio, verticalRatio)
case .scaleAspectFit:
ratio = min(horizontalRatio, verticalRatio)
default:
fatalError("Unsupported content mode: \(contentMode)")
}
let newSize = CGSize(width: size.width * ratio, height: size.height * ratio)
return resizedImage(newSize: newSize, interpolationQuality: quality)
}
func resizedImage(newSize: CGSize, transform: CGAffineTransform, drawTransposed: Bool, interpolationQuality quality: CGInterpolationQuality) -> UIImage {
let scale = max(self.scale, 1)
let newRect = CGRect(x: 0, y: 0, width: newSize.width * scale, height: newSize.height * scale).integral
let transposedRect = CGRect(x: 0, y: 0, width: newRect.size.height, height: newRect.size.width)
guard let imageRef = self.cgImage else {
return self
}
let colorSpace = CGColorSpaceCreateDeviceRGB()
guard let bitmap = CGContext(data: nil, width: Int(newRect.size.width), height: Int(newRect.size.height), bitsPerComponent: 8, bytesPerRow: Int(newRect.size.width) * 4, space: colorSpace, bitmapInfo: UInt32(CGBitmapInfo.alphaInfoMask.rawValue) & UInt32(CGImageAlphaInfo.premultipliedLast.rawValue)) else {
return self
}
bitmap.concatenate(transform)
bitmap.interpolationQuality = quality
bitmap.draw(imageRef, in: drawTransposed ? transposedRect : newRect)
guard let newImageRef = bitmap.makeImage() else {
return self
}
return UIImage(cgImage: newImageRef, scale: self.scale, orientation: .up)
}
func transformForOrientation(newSize: CGSize) -> CGAffineTransform {
var transform = CGAffineTransform.identity
switch imageOrientation {
case .down, .downMirrored:
transform = transform.translatedBy(x: newSize.width, y: newSize.height)
transform = transform.rotated(by: CGFloat.pi)
case .left, .leftMirrored:
transform = transform.translatedBy(x: newSize.width, y: 0)
transform = transform.rotated(by: CGFloat.pi / 2)
case .right, .rightMirrored:
transform = transform.translatedBy(x: 0, y: newSize.height)
transform = transform.rotated(by: -CGFloat.pi / 2)
default:
break
}
switch imageOrientation {
case .upMirrored, .downMirrored:
transform = transform.translatedBy(x: newSize.width, y: 0)
transform = transform.scaledBy(x: -1, y: 1)
case .leftMirrored, .rightMirrored:
transform = transform.translatedBy(x: newSize.height, y: 0)
transform = transform.scaledBy(x: -1, y: 1)
default:
break
}
return transform
}
}
14. UIImage+RoundedCorner.swift
import UIKit
extension UIImage {
func roundedCornerImage(cornerSize: CGFloat, borderSize: CGFloat) -> UIImage {
let image = imageWithAlpha()
let scale = max(self.scale, 1.0)
let scaledBorderSize = CGFloat(borderSize) * scale
guard let imageRef = cgImage, let colorSpace = imageRef.colorSpace else {
return self
}
guard let context = CGContext(data: nil, width: Int(image.size.width * scale), height: Int(image.size.height * scale), bitsPerComponent: imageRef.bitsPerComponent, bytesPerRow: 0, space: colorSpace, bitmapInfo: imageRef.bitmapInfo.rawValue) else {
return self
}
context.beginPath()
addRoundedRectToPath(rect: CGRect(x: scaledBorderSize, y: scaledBorderSize, width: image.size.width * scale, height: image.size.height * scale), context: context, ovalWidth: cornerSize * scale, ovalHeight: cornerSize * scale)
context.closePath()
context.clip()
context.draw(imageRef, in: CGRect(x: 0, y: 0, width: image.size.width * scale, height: image.size.height * scale))
guard let clippedImage = context.makeImage() else {
return self
}
return UIImage(cgImage: clippedImage, scale: self.scale, orientation: .up)
}
func addRoundedRectToPath(rect: CGRect, context: CGContext, ovalWidth: CGFloat, ovalHeight: CGFloat) {
if ovalWidth == 0 || ovalHeight == 0 {
context.addRect(rect)
return
}
context.saveGState()
context.translateBy(x: rect.minX, y: rect.minY)
context.scaleBy(x: ovalWidth, y: ovalHeight)
let fw = rect.width / ovalWidth
let fh = rect.height / ovalHeight
context.move(to: CGPoint(x: fw, y: fh / 2))
context.addArc(tangent1End: CGPoint(x: fw, y: fh), tangent2End: CGPoint(x: fw / 2, y: fh), radius: 1)
context.addArc(tangent1End: CGPoint(x: 0, y: fh), tangent2End: CGPoint(x: 0, y: fh / 2), radius: 1)
context.addArc(tangent1End: CGPoint(x: 0, y: 0), tangent2End: CGPoint(x: fw / 2, y: 0), radius: 1)
context.addArc(tangent1End: CGPoint(x: fw, y: 0), tangent2End: CGPoint(x: fw, y: fh / 2), radius: 1)
context.closePath()
context.restoreGState()
}
}
后记
本篇主要讲述了GCD之一个简单应用示例源码,感兴趣的给个赞或者关注~~~