GCD in Action
//
// ImageFilterViewController.swift
// Concurrency Practice
//
// Created by Martin.Q on 2021/12/23.
//
import UIKit
class ImageFilterViewController: UIViewController {
@IBOutlet weak var collectionView: UICollectionView!
var isCancelled = false
@IBAction func start(_ sender: Any) {
PhotoDataSource.shared.reset()
collectionView.reloadData()
isCancelled = false
}
@IBAction func cancel(_ sender: Any) {
isCancelled = true
}
override func viewDidLoad() {
super.viewDidLoad()
PhotoDataSource.shared.reset()
}
}
extension ImageFilterViewController {
func reloadCollectionView(at indexPath: IndexPath? = nil) {
guard !isCancelled else { print("Reload: Cancelled"); return }
print("Reload: Start", indexPath ?? "")
defer {
if isCancelled {
print("Reload: Cancelled", indexPath ?? "")
} else {
print("Reload: Done", indexPath ?? "")
}
}
if let indexPath = indexPath {
if collectionView.indexPathsForVisibleItems.contains(indexPath) {
collectionView.reloadItems(at: [indexPath])
}
} else {
collectionView.reloadData()
}
}
func downloadAndResize(target: PhotoData) {
print("Download & Resize: Start")
defer {
if isCancelled {
print("Download & Resize: Cancelled")
} else {
print("Download & Resize: Done")
}
}
guard !Thread.isMainThread else { fatalError() }
guard !isCancelled else { print("Download & Resize: Cancelled"); return}
do {
let data = try Data(contentsOf: target.url)
guard !isCancelled else { print("Dhownload & Resize: Cancelled"); return}
if let image = UIImage(data: data) {
let size = image.size.applying(CGAffineTransform(scaleX: 0.5, y: 0.5))
UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
let frame = CGRect(origin: CGPoint.zero, size: size)
image.draw(in: frame)
let resultImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard !isCancelled else { print("Download & Resize: Cancelled"); return }
target.data = resultImage
}
} catch {
print(error.localizedDescription)
}
}
func applyFilter(target: PhotoData) {
print("Filter: Start")
defer {
if isCancelled {
print("Filter: Cancelled")
} else {
print("Filter: Done")
}
}
guard !Thread.isMainThread else { fatalError() }
guard !isCancelled else { print("Filter: Cancelled"); return }
guard let source = target.data?.cgImage else { fatalError() }
let ciImage = CIImage(cgImage: source)
guard !isCancelled else { print("Filter: Cancelled"); return }
let filter = CIFilter(name: "CIPhotoEffectNoir")
filter?.setValue(ciImage, forKey: kCIInputImageKey)
guard !isCancelled else {print("Filter: Cancelled"); return }
guard let ciResult = filter?.value(forKey: kCIOutputImageKey) as? CIImage else { fatalError() }
guard !isCancelled else { print("Filter: Cancelled"); return }
guard let cgImg = PhotoDataSource.shared.filterContext.createCGImage(ciResult, from: ciResult.extent) else {
fatalError()
}
target.data = UIImage(cgImage: cgImg)
Thread.sleep(forTimeInterval: TimeInterval(arc4random_uniform(3)))
}
}
extension ImageFilterViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return PhotoDataSource.shared.list.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
let target = PhotoDataSource.shared.list[indexPath.item]
if let imageView = cell.contentView.viewWithTag(100) as? UIImageView {
imageView.image = target.data
}
return cell
}
}
extension ImageFilterViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let w = collectionView.bounds.width / 3
return CGSize(width: w, height: w * (768 / 1024))
}
}
이전에 Operation에서 작성했던 코드이다.
이미지를 다운로드 받고, 절반으로 줄인 뒤 이를 Collection View에 표시한다.
이후 Filter를 적용하고, 적용된 사진들을 순서대로 Cell에 업데이트한다.
func reloadCollectionView(at indexPath: IndexPath? = nil) {
guard !isCancelled else { print("Reload: Cancelled"); return }
print("Reload: Start", indexPath ?? "")
defer {
if isCancelled {
print("Reload: Cancelled", indexPath ?? "")
} else {
print("Reload: Done", indexPath ?? "")
}
}
if let indexPath = indexPath {
if collectionView.indexPathsForVisibleItems.contains(indexPath) {
collectionView.reloadItems(at: [indexPath])
}
} else {
collectionView.reloadData()
}
}
reloadColelctionView 메서드는
전달된 indexPath에 따라 전체를 새로고침 하거나, 특정 Cell만 새로고침한다.
func downloadAndResize(target: PhotoData) {
print("Download & Resize: Start")
defer {
if isCancelled {
print("Download & Resize: Cancelled")
} else {
print("Download & Resize: Done")
}
}
guard !Thread.isMainThread else { fatalError() }
guard !isCancelled else { print("Download & Resize: Cancelled"); return}
do {
let data = try Data(contentsOf: target.url)
guard !isCancelled else { print("Dhownload & Resize: Cancelled"); return}
if let image = UIImage(data: data) {
let size = image.size.applying(CGAffineTransform(scaleX: 0.5, y: 0.5))
UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
let frame = CGRect(origin: CGPoint.zero, size: size)
image.draw(in: frame)
let resultImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard !isCancelled else { print("Download & Resize: Cancelled"); return }
target.data = resultImage
}
} catch {
print(error.localizedDescription)
}
}
downloadAndResize 메서드는 이미지를 다운로드하고, 이를 절반으로 줄인다.
func applyFilter(target: PhotoData) {
print("Filter: Start")
defer {
if isCancelled {
print("Filter: Cancelled")
} else {
print("Filter: Done")
}
}
guard !Thread.isMainThread else { fatalError() }
guard !isCancelled else { print("Filter: Cancelled"); return }
guard let source = target.data?.cgImage else { fatalError() }
let ciImage = CIImage(cgImage: source)
guard !isCancelled else { print("Filter: Cancelled"); return }
let filter = CIFilter(name: "CIPhotoEffectNoir")
filter?.setValue(ciImage, forKey: kCIInputImageKey)
guard !isCancelled else {print("Filter: Cancelled"); return }
guard let ciResult = filter?.value(forKey: kCIOutputImageKey) as? CIImage else { fatalError() }
guard !isCancelled else { print("Filter: Cancelled"); return }
guard let cgImg = PhotoDataSource.shared.filterContext.createCGImage(ciResult, from: ciResult.extent) else {
fatalError()
}
target.data = UIImage(cgImage: cgImg)
Thread.sleep(forTimeInterval: TimeInterval(arc4random_uniform(3)))
}
applyFilter 메서드는 필터를 적용한다.
모든 메서드는 Class의 isCancelled 속성에 따라 작업을 취소하도록 돼있고,
일정 시점마다 로그를 출력한다.
class ImageFilterViewController: UIViewController {
@IBOutlet weak var collectionView: UICollectionView!
let downloadQueue = DispatchQueue(label: "DownloadQueue", attributes: .concurrent)
let downloadGroup = DispatchGroup()
let filterQueue = DispatchQueue(label: "FilterQueue", attributes: .concurrent)
var isCancelled = false
@IBAction func start(_ sender: Any) {
PhotoDataSource.shared.reset()
collectionView.reloadData()
isCancelled = false
}
@IBAction func cancel(_ sender: Any) {
isCancelled = true
}
override func viewDidLoad() {
super.viewDidLoad()
PhotoDataSource.shared.reset()
}
}
Class에서 다운로드와 필터 작업에 사용할 Concurrent Queue를 생성한다.
다운로드 작업 이후 다음 작업을 실핼할 수 있도록 DownloadGroup도 생성했다.
@IBAction func start(_ sender: Any) {
PhotoDataSource.shared.reset()
collectionView.reloadData()
isCancelled = false
PhotoDataSource.shared.list.forEach { data in
self.downloadQueue.async(group: self.downloadGroup) {
self.downloadAndResize(target: data)
}
}
}
시작 버튼을 누르면 모든 Data를 열거하고, 해당 Data를 downloadAndResize에 전달한다.
해당 작업이 완료된 이후 다음 작업으로 이동할 수 있게 앞서 작성한 downloadGroup에 추가한다.
@IBAction func start(_ sender: Any) {
PhotoDataSource.shared.reset()
collectionView.reloadData()
isCancelled = false
PhotoDataSource.shared.list.forEach { data in
self.downloadQueue.async(group: self.downloadGroup) {
self.downloadAndResize(target: data)
}
}
self.downloadGroup.notify(queue: DispatchQueue.main) {
self.reloadCollectionView()
}
}
group의 작업이 완료되면 CollectionView를 새로고침한다.
@IBAction func start(_ sender: Any) {
PhotoDataSource.shared.reset()
collectionView.reloadData()
isCancelled = false
PhotoDataSource.shared.list.forEach { data in
self.downloadQueue.async(group: self.downloadGroup) {
self.downloadAndResize(target: data)
}
}
self.downloadGroup.notify(queue: DispatchQueue.main) {
self.reloadCollectionView()
}
self.downloadGroup.notify(queue: self.filterQueue) {
DispatchQueue.concurrentPerform(iterations: PhotoDataSource.shared.list.count) { (index) in
let data = PhotoDataSource.shared.list[index]
self.applyFilter(target: data)
}
}
}
새로고침 함과 동시에 FilterQueue에서 작업을 실행한다.
대상 데이터를 가져와 data 상수에 저장하고, 해당 데이터를 applyFilter 메서드를 호출하고, 파라미터로 전달한다.
@IBAction func start(_ sender: Any) {
PhotoDataSource.shared.reset()
collectionView.reloadData()
isCancelled = false
PhotoDataSource.shared.list.forEach { data in
self.downloadQueue.async(group: self.downloadGroup) {
self.downloadAndResize(target: data)
}
}
self.downloadGroup.notify(queue: DispatchQueue.main) {
self.reloadCollectionView()
}
self.downloadGroup.notify(queue: self.filterQueue) {
DispatchQueue.concurrentPerform(iterations: PhotoDataSource.shared.list.count) { (index) in
let data = PhotoDataSource.shared.list[index]
self.applyFilter(target: data)
let targetIndexPath = IndexPath(item: index, section: 0)
DispatchQueue.main.async {
self.reloadCollectionView(at: targetIndexPath)
}
}
}
}
index에 맞게 IndexPath를 생성하고,
UI를 업데이트 할 수 있도록 mainQueue에서 reloadColelctionView 메서드를 호출한다.
Download & Resize: Start
Download & Resize: Start
Download & Resize: Start
Download & Resize: Start
Download & Resize: Start
Download & Resize: Start
Download & Resize: Start
Download & Resize: Start
Download & Resize: Start
Download & Resize: Start
Download & Resize: Start
Download & Resize: Start
Download & Resize: Start
Download & Resize: Start
Download & Resize: Start
Download & Resize: Start
Download & Resize: Start
Download & Resize: Start
Download & Resize: Start
Download & Resize: Start
Download & Resize: Done
Download & Resize: Done
Download & Resize: Done
Download & Resize: Done
Download & Resize: Done
Download & Resize: Done
Download & Resize: Done
Download & Resize: Done
Download & Resize: Done
Download & Resize: Done
Download & Resize: Done
Download & Resize: Done
Download & Resize: Done
Download & Resize: Done
Download & Resize: Done
Download & Resize: Done
Download & Resize: Done
Download & Resize: Done
Download & Resize: Done
Download & Resize: Done
Download Queue에 추가된 작업이 동시에 실행되고,
이들이 모두 완료 되면 다음으로 넘어간다.
Filter: Start
Reload: Start
Filter: Start
Filter: Start
Reload: Done
Filter: Start
Filter: Start
Filter: Start
Filter: Done
Reload: Start [0, 4]
Filter: Start
Reload: Done [0, 4]
Filter: Done
Filter: Start
Reload: Start [0, 6]
Reload: Done [0, 6]
Filter: Done
Filter: Start
Reload: Start [0, 7]
Reload: Done [0, 7]
Filter: Done
Filter: Start
Reload: Start [0, 8]
Reload: Done [0, 8]
Filter: Done
Filter: Done
Filter: Start
Filter: Start
Reload: Start [0, 2]
Reload: Done [0, 2]
Reload: Start [0, 0]
Reload: Done [0, 0]
Filter: Done
Reload: Start [0, 1]
Filter: Start
Reload: Done [0, 1]
Filter: Done
Filter: Start
Reload: Start [0, 5]
Reload: Done [0, 5]
Filter: Done
Filter: Start
Reload: Start [0, 3]
Reload: Done [0, 3]
Filter: Done
Filter: Start
Reload: Start [0, 14]
Reload: Done [0, 14]
Filter: Done
Filter: Start
Reload: Start [0, 10]
Reload: Done [0, 10]
Filter: Done
Filter: Start
Reload: Start [0, 16]
Reload: Done [0, 16]
Filter: Done
Filter: Start
Reload: Start [0, 12]
Filter: Done
Filter: Start
Reload: Done [0, 12]
Reload: Start [0, 17]
Reload: Done [0, 17]
Filter: Done
Reload: Start [0, 18]
Reload: Done [0, 18]
Filter: Done
Reload: Start [0, 9]
Reload: Done [0, 9]
Filter: Done
Reload: Start [0, 11]
Reload: Done [0, 11]
Filter: Done
Reload: Start [0, 19]
Reload: Done [0, 19]
Filter: Done
Reload: Start [0, 13]
Reload: Done [0, 13]
Filter: Done
Reload: Start [0, 15]
Reload: Done [0, 15]
가능한 수 만큼 동시에 실행하고, 이후 해당 셀을 새로고침한다.
'학습 노트 > iOS (2021)' 카테고리의 다른 글
178. File Manager #1 (0) | 2022.01.12 |
---|---|
177. Data Persistence Overview (0) | 2022.01.10 |
175. Dispatch Group, Dispatch Semaphore (0) | 2022.01.10 |
174. Dispatch Work Item & Dispatch Source Timer (0) | 2022.01.09 |
173. GCD #1 (Grand Central Dispatch) (0) | 2022.01.07 |