[iOS] Interoperation Dependencies

RudinP·2024년 7월 11일
0

Study

목록 보기
251/258

Operation 사이에 의존성을 추가해서 원하는 순서로 작업 실행하는 방법

오퍼레이션 큐에 오퍼레이션을 추가하면 기본적으로 최대한 많은 오퍼레이션을 동시에 실행한다.
실행 순서를 정하고싶다면 오퍼레이션 사이에 의존성을 추가하면 된다.
의존성은 단방향만 가능 하며 상호 의존성을 넣어 통신하는 것은 불가능하다.

작업의 흐름 예시(이미지 다운로드 후 필터 적용하기)

이미지 표시 CollectionView 설정


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
    }
}

커스텀 오퍼레이션 생성

Download Operation

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를 붙여야 한다.

ReloadOperation


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")
    }
}

Filter Operation

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)
    }
...

Operation 취소

@IBAction func cancelOperation(_ sender: Any) {
        mainQueue.cancelAllOperations()
        backgroundQueue.cancelAllOperations()
}

동시 실행 작업 수 설정

override func viewDidLoad() {
        super.viewDidLoad()
        
        setupLayout()
        
        //동시 실행 작업 수 최대 값 설정
        backgroundQueue.maxConcurrentOperationCount = 20
}
  • 시스템이 실행 가능한 수와 설정한 수 중 더 작은 값으로 실행된다.
  • 1로 해둔다면 메인 큐와 같아지며, concurrent queue 를 serial queue로 바꾸자 할 때 사용한다.

정리

  • 동시에 실행된다.
  • 기본적으로 동시 실행 작업 수에는 제한이 없다.

메인

  • 메인 큐는 백과 다르게 serial이다.
  • 즉, 이전에 추가한 오퍼레이션이 끝나야 다음 오퍼레이션이 실행된다.
profile
iOS 개발자가 되기 위한 스터디룸...

0개의 댓글