SwiftUI 에서는 onChange 를 제공하여 특정 값의 변화를 감지할 수 있습니다

 

 

문제 발생

이번에는 지도에서 특정 값이 변경되었을 때 sheet를 올라오게 만들기 위해서 onChange 내부에 isPresented 프로퍼티 값을 변경시키도록 코드를 만들었으나 showDetails의 값이 변경되었음에도 불구하고 sheet 가 올라오지 않는 문제를 발견하였습니다.

 

문제가 되는 코드를 먼저 살펴보겠습니다. 다음은 mapState의 selectedResult 값이 변경되었으면 showDetails 라는 값을 변경시켜 sheet 를 올려보내려는 코드입니다.

.onChange(of: mapState.selectedResult, { oldValue, newValue in
    if newValue != nil {
        showLists = false
        showDetails = true
    }
})

 

전체 코드를 살펴보겠습니다. 뷰를 객체화하여 깔끔하게 만들었으며 분명 문제가 없어보이는 코드입니다.

private var mapView: some View {
    Map(position: $mapState.position, selection: $mapFeature, scope: mapScope) {
		...
    }
    .onChange(of: mapState.selectedResult, { oldValue, newValue in
        if newValue != nil {
            showLists = false
            showDetails = true
        }
    })
    .sheet(isPresented: $showDetails) {
        SelectedDetailView()
            .presentationDetents([.fraction(0.45)])
            .presentationBackgroundInteraction(.enabled(upThrough: .fraction(0.45)))
    }

 

해결방법

🔵 onChange 로 프로퍼티 값을 변경할 때에는 onChange를 body 내부에 적용하여 변경할 것

하지만 한참을 구글링을 하였지만 문제를 찾지 못했습니다. 그러나 결국 삽질 끝에 찾은 원인은 위처럼 extension 에 위치한 뷰에 onChange 를 적용하여 값을 바꾸는 대신 직접 body 내부에 onChange 를 적용하는 것이 상태 변화를 오류없이 추적할 수 있다는 것을 확인하였습니다.

 

다음은 body 내부에 적용한 onChange 입니다. 다음과 같이 코드를 작성하게 된다면 이번 주제인 onChange 에서 프로퍼티의 변화와 변화에 대한 감지가 안되는 문제를 해결할 수 있습니다.

var body: some View {

    MapReader { reader in
        mapView
        ...
    }
    .onChange(of: mapState.selectedResult, { oldValue, newValue in
        if newValue != nil {
        	showLists = false
	        showDetails = true
    	}
    })

 

이후 발견된 또 다른 문제점

위에서 문제를 완벽하게 해결했다고 생각하였지만 Map 에서 Annotation 을 선택하였다가 다른 탭을 누르면 선택된 Annotation 을 취소시키는 방식을 채택하였습니다. 하지만 또다른 문제는 여기서 발생하였습니다.

 

Annotation 을 만들어 onTapGesture탭을 감지하여 프로퍼티의 값을 변경하는 방법을 사용하였지만 Map 에서도 onTapGesture 를 사용하여 다른 뷰를 탭하였을 때 프로퍼티의 값들을 초기화 시키는 코드를 작성하였습니다.

private func customAnnotation(result: MKMapItem) -> some MapContent {
    Annotation(result.name ?? "이름없음", coordinate: result.placemark.coordinate) {
        annotationImage(for: result)
            .scaleEffect(tappedAnnotation ? 1.5 : 1.0)
            .onTapGesture {
                tappedAnnotation = false
                mapState.selectedResult = result
                showDetails = true
                withAnimation(.easeIn) {
                    tappedAnnotation.toggle()
                }
            }
    }
}

 

Map 에 Annotation 을 선택하였지만 동작이 안됐던 이유는 Annotation 을 탭할 때 Annotation 뿐만 아니라 Map 의 onTapGesture 가 같이 실행되는 문제점이 확인되었습니다.

Map(position: $mapState.position, selection: $mapFeature) {
    /// 코드 생략
}
.onTapGesture {
    initalization()
}

또 다른 문제 해결 방법

이 문제를 해결하기 위해서 다음과 같이 동작하면 해결이 완료된다고 판단이 되었습니다.

1. Map의 onTapGesture 를 먼저 실행시킨다

2. 1번이 완료 후 Annotation 의 onTapGesture 를 실행시킨다

 

이 해결방법을 다음과 같이 asyncAfter 로 비동기적 코드를 작성하여 구현할 수 있었습니다.

private func customAnnotation(result: MKMapItem) -> some MapContent {
    Annotation(result.name ?? "이름없음", coordinate: result.placemark.coordinate) {
        annotationImage(for: result)
            .scaleEffect(tappedAnnotation ? 1.5 : 1.0)
            .onTapGesture {
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                    tappedAnnotation = false
                    mapState.selectedResult = result
                    showDetails = true
                    withAnimation(.easeIn) {
                        tappedAnnotation.toggle()
                    }
                }
            }
    }
}

 

 

 

 

 

 

 

ytw_developer