본문 바로가기

학습 노트/iOS (2021)

176. GCD in Action

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]

가능한 수 만큼 동시에 실행하고, 이후 해당 셀을 새로고침한다.