Operation 사이에 의존성을 추가해서 원하는 순서로 작업 실행하는 방법
오퍼레이션 큐에 오퍼레이션을 추가하면 기본적으로 최대한 많은 오퍼레이션을 동시에 실행한다.
실행 순서를 정하고싶다면 오퍼레이션 사이에 의존성을 추가하면 된다.
의존성은 단방향만 가능 하며 상호 의존성을 넣어 통신하는 것은 불가능하다.
import UIKit
class PhotoData{
let url: URL
var data: UIImage?
init(url: String){
self.url = URL(string: url)!
}
}
import UIKit
struct PhotoDataSource{
var list: [PhotoData]
init(){
var list = [PhotoData]()
for num in 1...20 {
let url = "https://kxcodingblob.blob.core.windows.net/mastering-ios/\(num).jpg"
let data = PhotoData(url: url)
list.append(data)
}
self.list = list
}
}
import UIKit
class ImageListViewController: UIViewController {
@IBOutlet weak var imageCollectionView: UICollectionView!
var ds = PhotoDataSource()
//compositional Layout. 한 줄에 아이템 3개씩 표시. 여백 10
func setupLayout(){
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3), heightDimension: .estimated(100))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
group.interItemSpacing = .flexible(10)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 10
section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
let layout = UICollectionViewCompositionalLayout(section: section)
imageCollectionView.collectionViewLayout = layout
}
override func viewDidLoad() {
super.viewDidLoad()
setupLayout()
}
}
extension ImageListViewController: UICollectionViewDataSource{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return ds.list.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCollectionViewCell", for: indexPath) as! ImageCollectionViewCell
cell.imageView.image = ds.list[indexPath.item].data
return cell
}
}
import UIKit
class DownloadOperation: Operation{
let target: PhotoData
init(target: PhotoData){
self.target = target
super.init()
}
override func main(){
print(target.url, "Start")
//블록이 포함된 코드가 바로 실행되지 않고 스코프가 끝나는 시점으로 연기됨. 즉, 메인메소드가 끝나기 직전 실행
defer{
if isCancelled {
print(target.url, "Cancelled")
} else {
print(target.url, "Done")
}
}
//메인스레드에서 실행하면 안되므로 강제 종료
guard !Thread.isMainThread else {
fatalError()
}
//취소 기능
guard !isCancelled else {
print(target.url, "Cancelled")
return
}
//보통 URLSession을 사용하는 것이 바람직하나, 간단히 하기 위해 data생성자로 구현
do{
let data = try Data(contentsOf: target.url)
//취소 기능 구현 시 중요한 작업 사이사이에 분기확인문을 넣어주는 것이 좋다.
guard !isCancelled else {
print(target.url, "Cancelled")
return
}
if let image = UIImage(data: data){
//이미지 크기 30% 줄이기
let size = image.size.applying(CGAffineTransform(scaleX: 0.3, y: 0.3))
//이미지 리사이징- 이미지 컨텍스트
UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
let frame = CGRect(origin: .zero, size: size)
image.draw(in: frame)
let resultImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard !isCancelled else {
print(target.url, "Cancelled")
return
}
target.data = resultImage
}
} catch {
print(target, error.localizedDescription)
}
}
override func cancel() {
super.cancel()
print(target.url, "Cancel")
}
}
throws
가 있는 메소드는 반드시 try
를 붙여야 한다.
import UIKit
class ReloadOperation: Operation {
weak var collectionView: UICollectionView!
//여기에 저장된 값이 nil이면 전체 reload
var indexPath: IndexPath?
init(collectionView: UICollectionView, indexPath: IndexPath? = nil) {
self.collectionView = collectionView
self.indexPath = indexPath
super.init()
}
override func main() {
print(self, "Start", indexPath)
defer{
if isCancelled {
print(self, "Cancelled", indexPath)
} else {
print(self, "Done", indexPath)
}
}
//UI 리로드이므로 메인스레드에서만 해야함
guard Thread.isMainThread else{
fatalError()
}
guard !isCancelled else{
print(self, "Cancelled")
return
}
if let indexPath {
//indexPath가 nil이 아니라면 해당 indexPath에 있는 셀만 리로드. 셀이 화면에 표시된 상태가 아니라면 reload할 필요가 없음.
if collectionView.indexPathsForVisibleItems.contains(indexPath){
collectionView.reloadItems(at: [indexPath])
}
} else {
collectionView.reloadData()
}
}
override func cancel() {
super.cancel()
print(self, "Cancel")
}
}
import UIKit
class FilterOperation: Operation {
let target: PhotoData
let context = CIContext(options: nil)
init(target: PhotoData) {
self.target = target
super.init()
}
override func main() {
print(target.url, "Start")
defer{
if isCancelled{
print(target.url, "Cancelled")
} else {
print(target.url, "Done")
}
}
guard !Thread.isMainThread else {
fatalError()
}
guard !isCancelled else {
print(target.url, "Cancelled")
return
}
//filter을 적용하려면 UIImage -> CGImage 타입 변환이 필요
guard let source = target.data?.cgImage else {
fatalError()
}
let ciImage = CIImage(cgImage: source)
guard !isCancelled else {
print(target.url, "Cancelled")
return
}
let filter = CIFilter(name: "CIPhotoEffectNoir")
filter?.setValue(ciImage, forKey: kCIInputImageKey)
guard !isCancelled else {
print(target.url, "Cancelled")
return
}
guard let ciResult = filter?.value(forKey: kCIOutputImageKey) as? CIImage else {
fatalError()
}
guard !isCancelled else {
print(target.url, "Cancelled")
return
}
guard let cgImage = context.createCGImage(ciResult, from: ciResult.extent) else {
fatalError()
}
target.data = UIImage(cgImage: cgImage)
}
override func cancel() {
super.cancel()
print(target.url, "Cancel")
}
}
class ImageListViewController: UIViewController {
@IBOutlet weak var imageCollectionView: UICollectionView!
let backgroundQueue = OperationQueue() //백에서 실행
let mainQueue = OperationQueue.main // 메인에서 실행
var ds = PhotoDataSource()
...
@IBAction func startOperation(_ sender: Any) {
var uiOperations = [Operation]()
var backgroundOperations = [Operation]()
//Reload Operation
let reloadOp = ReloadOperation(collectionView: imageCollectionView)
uiOperations.append(reloadOp)
for index in 0..<20 {
let data = ds.list[index]
//Download Operation
let downloadOp = DownloadOperation(target: data)
//의존성 추가. 다운로드 완료 후 리로드 op 실행됨.
reloadOp.addDependency(downloadOp)
backgroundOperations.append(downloadOp)
//Filter Operation
let filterOp = FilterOperation(target: data)
//리로드가 끝난 후 필터 적용해야 하므로 의존성 추가
filterOp.addDependency(reloadOp)
backgroundOperations.append(filterOp)
//개별 셀 리로드
let reloadItemOp = ReloadOperation(collectionView: imageCollectionView, indexPath: IndexPath(item: index, section: 0))
//필터가 끝난 후 리로드 실행
reloadItemOp.addDependency(filterOp)
uiOperations.append(reloadItemOp)
}
//waitUntilFinished를 true로 하면 모든 오퍼레이션이 끝날 때까지 메소드가 리턴되지 않음.
//백그라운드에서 실행하거나 의존성이 없으면 true를 해도 무방하지만 그게 아니라면 false
//그렇지 않으면 메인이 블락되거나 op가 안끝남
backgroundQueue.addOperations(backgroundOperations, waitUntilFinished: false)
mainQueue.addOperations(uiOperations, waitUntilFinished: false)
}
...
@IBAction func cancelOperation(_ sender: Any) {
mainQueue.cancelAllOperations()
backgroundQueue.cancelAllOperations()
}
override func viewDidLoad() {
super.viewDidLoad()
setupLayout()
//동시 실행 작업 수 최대 값 설정
backgroundQueue.maxConcurrentOperationCount = 20
}