[필터 앱 만들기] 4. Back Thread 에서 Slider 이벤트 처리하기: OperationQueue

이대현·2024년 4월 7일
0

iOS

목록 보기
9/9

문제

슬라이더를 통해 필터의 강도를 조절할 때, 강도가 조절된 이미지를 유저에게 즉각적으로 보여줄 수 있도록 구성했다. UISlider의 값이 변경될 때마다 sliderValueChanged 함수가 호출되어 LUT를 적용하고, 결과 이미지를 업데이트하는 것이다.

문제는 이미지 사이즈가 클 때, 뚝뚝 끊김이 생길 정도로 슬라이더의 반응성이 느려진다는 점이었다. 한 스레드 내에서 이벤트도 받고 LUT 프로세싱도 처리하다보니까, 이벤트 쌓이는 속도를 조절할 필요가 있었다.

해결 방법

생각한 건 세 가지 있었다.

  1. 슬라이더가 멈추면 필터 적용시키기

  2. 이미지 리사이징을 하기

  3. 슬라이더 값을 받아서 필터 적용하는 작업을 비동기적으로 처리하기

1번은 필터 앱에 적절하지 못한 UX인 것 같아서 제외했고, 3번이 더 간단할 것 같아서 백스레드로 돌리는 작업을 했다.


주의할 점

이미지 크기가 커질수록 슬라이더 이벤트가 느려지기 때문에, 1) LUT 프로세싱 작업을 백그라운드 스레드로 돌리고, 2) 스레드가 해당 작업을 처리하는 동안에는 새 이벤트가 들어오지 못하도록 한다.

단순히 백그라운드로 돌리기만 하면 Queue에 쌓이는 task 순서를 보장할 수 없기 때문에, 마지막으로 적용되는 필터 값을 알 수 없게 된다. 50으로 맞췄는데 50까지 슬라이드 하는 과정의 어떤 값으로 필터가 먹어버리는 경우가 생기는 것이다.

그래서 2) 스레드가 해당 작업을 처리하는 동안에는 새 이벤트가 들어오지 못하도록 고려해야 한다.

이 작업을 할 수 있는 방법은 정말 많겠지만, 나는 그냥 isProcessing 플래그 변수를 사용하여 Background Queue가 작업을 직렬(한 번에 하나의 작업)로 실행할 수 있도록 코드를 짰다.

변경 전 코드

@objc private func sliderValueChanged(_ sender: UISlider) {
        guard let srcImage = srcImage, let lutImage = lutImage else { 
            return
        }
        let intensity = CGFloat(sender.value)
        
        // ...
        
        resultImage = LUTManager.applyLUT(image: srcImage, lut: lutImage, intensity: sender.value / 100)
        imageView.image = resultImage
    }

변경 후 코드

var isProcessing: Bool = false
let processingQueue = OperationQueue()

func opacitySliderEvent(_ filterView: FilterView, slider: UISlider) {
        guard let srcImage = srcImage, let lutImage = lutImage else { return }
        let intensity = CGFloat(slider.value)
        
        // ...
        
        if !isProcessing {
            isProcessing = true
            processingQueue.addOperation {
                let processedImage = LUTManager.applyLUT(image: srcImage, lut: lutImage, intensity: intensity / 100)
                
                DispatchQueue.main.async {
                    self.resultImage = processedImage
                    self.filterView.imageView.image = self.resultImage
                }
                self.isProcessing = false
            }
        }
    }
  • OperationQueue의 Background Thread에서 비동기적으로 LUT를 적용한다.
  • isProcessing 플래그를 사용하여 여러 번 실행되지 않도록 한다.
  • 작업이 완료되면 Main Thread에서 UI를 업데이트한다.

운영체제 시간에 배웠던, 경쟁 상태(Race condition) 를 방지하는 코드를 직접 필요성을 느끼고 짜볼수 있어서 좋았던 경험

profile
삽질의 기록들 👨‍💻

0개의 댓글