슬라이더를 통해 필터의 강도를 조절할 때, 강도가 조절된 이미지를 유저에게 즉각적으로 보여줄 수 있도록 구성했다. UISlider의 값이 변경될 때마다 sliderValueChanged
함수가 호출되어 LUT를 적용하고, 결과 이미지를 업데이트하는 것이다.
문제는 이미지 사이즈가 클 때, 뚝뚝 끊김이 생길 정도로 슬라이더의 반응성이 느려진다는 점이었다. 한 스레드 내에서 이벤트도 받고 LUT 프로세싱도 처리하다보니까, 이벤트 쌓이는 속도를 조절할 필요가 있었다.
생각한 건 세 가지 있었다.
슬라이더가 멈추면 필터 적용시키기
이미지 리사이징을 하기
슬라이더 값을 받아서 필터 적용하는 작업을 비동기적으로 처리하기
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
플래그를 사용하여 여러 번 실행되지 않도록 한다.운영체제 시간에 배웠던, 경쟁 상태(Race condition) 를 방지하는 코드를 직접 필요성을 느끼고 짜볼수 있어서 좋았던 경험