Neoself의 기술 블로그

UIViewRepresentable에서 발생하는 콜백 중복 호출 문제 해결 본문

개발지식 정리/Swift

UIViewRepresentable에서 발생하는 콜백 중복 호출 문제 해결

Neoself 2025. 2. 25. 09:38

동일한 이미지를 담는 NeoImage의 onSuccess 콜백이 반복호출되는 이슈가 있었습니다.

@State private var imageTask: ImageTask?

func updateUIView(_ uiView: UIImageView, context: Context) {
    let url: URL? = {
        switch source {
        case .url(let url):
            return url
        case .urlString(let string):
            if let string = string {
                return URL(string: string)
            }
            return nil
        }
    }()

    Task {
        do {
            let result = try await uiView.neo.setImage(with: url, options: options)

            if let onSuccess = onSuccess {
                await MainActor.run {
                    print("onSuccess called")
                    onSuccess(result)
                }
            }
        } catch {
            if let onFailure = onFailure {
                await MainActor.run {
                    onFailure(error)
                }
            }
        }
    }
}

SwiftUI의 UIViewRepreesentable 프로토콜에서 필수로 작성해야하는 updateUIView 메서드는 뷰의 상태가 변경될 때마다 SwiftUI의 뷰 업데이트 사이클에 맞춰 자동으로 호출됩니다. 때문에, 초기 뷰를 띄우는 makeUIView 메서드 호출 이후, 바로 이미지 다운로드 혹은 로딩 로직은 Task 블록으로 감싸 updateUIView 메서드에 구현하였습니다. 하지만 이로 인해 뷰 업데이트를 야기하는 외부 트리거가 발생할때마다, 이미지를 새로 로드하는 Task를 불필요하게 호출하게 되었습니다.

 

때문에, 이 뷰 업데이트 트리거의 위치를 찾는 것이 필요했습니다.

struct NeoImageTestView: View {
    let url: URL?
    @State private var loadingTime: TimeInterval = 0 // 뷰 업데이트에 대한 트리거
    @State private var startTime: Date = Date()
    
    var body: some View {
        Text(String(loadingTime))
            .padding(.bottom,12)
        
        NeoImage(url: url)
            .onSuccess { _ in
                // onSuccess 콜백함수 호출 시 loadingTime 변수값을 변경시킴
                loadingTime = Date().timeIntervalSince(startTime)
            }
    }
}

트리거의 위치는 패키지 외부에서 이미지 로딩 시간을 저장하고 렌더하는 데에 사용된 @State var loadingTime 변수였는데요.

이미지 로드 Task 완료 -> onSuccess 콜백함수 호출 -> 패키지 외부 loadingTime 변수 변경 및 렌더 -> 뷰 업데이트 -> updateUIView 메서드 호출

위 사이클이 순환되면서 불필요하게 onSuccess가 호출되는 것이였습니다.

 

가설 검증을 위해 아래와 같이 loadingTime 변수를 표시하는 Text 컴포넌트를 주석처리하여, onSuccess 콜백함수가 재호출되지 않음을 확인해볼 수 있었습니다.

struct NeoImageTestView: View {
    let url: URL?
    @State private var loadingTime: TimeInterval = 0
    @State private var startTime: Date = Date()
    
    var body: some View {
//        Text(String(loadingTime))
//            .padding(.bottom,12)
        
        NeoImage(url: url)
            .onSuccess { _ in
                loadingTime = Date().timeIntervalSince(startTime)
            }
    }
}

 

이를 해결하기 위해, updateUIView 메서드에서 이미지 URL이 변경되었을 때만 새로운 Task를 시작하게끔 예외처리를 추가하였습니다. 

func updateUIView(_ uiView: UIImageView, context: Context) {
        // URL 추출
        let url: URL? = {
            switch source {
            case .url(let url):
                return url
            case .urlString(let string):
                if let string = string {
                    return URL(string: string)
                }
                return nil
            }
        }()
        
        // URL이 변경되었거나 아직 로드되지 않은 경우에만 이미지 로딩
        if url != loadedURL {
            // 이전 작업이 있다면 취소
            imageTask?.cancel()
            
            // 새 이미지 로딩 작업 시작
            imageTask = Task {
                do {
                    let result = try await uiView.neo.setImage(with: url, options: options)
                    
                    // 작업이 취소되지 않았고 성공했을 경우에만 콜백 호출
                    if !Task.isCancelled, let onSuccess = onSuccess {
                        await MainActor.run {
                            loadedURL = url // URL 상태 업데이트
                            onSuccess(result)
                        }
                    }
                } catch {
                    if !Task.isCancelled, let onFailure = onFailure {
                        await MainActor.run {
                            onFailure(error)
                        }
                    }
                }
            }
        }
    }