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