.onReceive

SteadySlower·2022년 6월 3일
0

SwiftUI

목록 보기
10/64

.onReceive()

정의

onReceive는 2가지 인자를 받는 메소드입니다. 첫 번째 인자는 Publisher 타입이고 두 번째 인자는 해당 Publisher가 발행되었을 때 실행할 클로저입니다.

사용법

일단 메인 View와 디테일 View를 정의합니다.

메인 뷰에는 디테일 뷰를 모달로 띄워주는 버튼이 하나 있습니다.

디테일 뷰에는 버튼이 하나 있는데요. 이 버튼은 서버에 데이터를 보내는 버튼이라고 합시다.

이 버튼은 뷰모델에 있는 submitData와 연결되어 있는데요. submitData메소드는 데이터를 서버로 보낸 이후에 didSubmit이라는 Bool 변수를 true로 바꾸어 줍니다. (서버에 보내는 데 걸리는 시간은 0.1초라고 가정하고 asynAfter를 활용해서 가상의 메소드를 만들었습니다.)

우리는 해당 변수를 onReceive에 연결해서 데이터를 보낸 후에 modal을 dismiss해보겠습니다.

struct MainView: View {
    
    @State var shouldShowModal = false

    var body: some View {
		// DetailView를 모달로 띄우는 버튼
        Button {
            shouldShowModal.toggle()
        } label: {
            Text("To Detail Page")
        }
        .sheet(isPresented: $shouldShowModal) {
            DetailView()
				}
    }
}
struct DetailView: View {
    
    @ObservedObject var viewModel = DetailViewModel()
    @Environment(\.presentationMode) var mode
    
    var body: some View {
        VStack {
            Text("Detail View")
			// 서버에 데이터를 보내는 버튼
            Button {
                viewModel.submitData()
            } label: {
                Text("Press to Submit Data")
            }
        }
    }
}
class DetailViewModel: ObservableObject {
    
    @Published var didSubmit: Bool = false
    
	// 서버에 데이터를 보내는 메소드
    func submitData() {
				DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { //👉 서버에 보내는 시간 0.1초 이후에
					  print("Submitted Data to Server 🌎")
            self.didSubmit = true //👉 didSubmit을 true로 바꾸어줌.
        }
    }
}

자주 발생하는 버그

Case 1 : Modal이 뜨자마자 dismiss

처음에는 아래처럼 viewModel에 있는 데이터를 Binding으로 연결해서 발행되면 모달이 dismiss되도록 클로저를 전달했습니다.

struct DetailView: View {
    
    @ObservedObject var viewModel = DetailViewModel()
    @Environment(\.presentationMode) var mode
    
    var body: some View {
        VStack {
            Text("Detail View")
            Button {
                viewModel.submitData()
            } label: {
                Text("Press to Submit Data")
            }
        }
        .onReceive(viewModel.$didSubmit) { _ in
            mode.wrappedValue.dismiss() //👉 모달을 dismiss하는 코드
        }
    }
}

🚫 하지만 이렇게 하는 경우 아래와 같이 모달이 뜨자마자 dismiss 되는 것을 볼 수 있습니다.

🤔  아마도 처음에 초기값을 선언할 때 발행되는 false값 때문에 그런 것이 아닐까요?

Case 2 : 두 번 눌러야지 Modal이 dismiss

그렇다면 이번에는 viewModel.didSubmit이 true일 때만 Modal이 dismiss되도록 해봅시다.

struct DetailView: View {
    
    @ObservedObject var viewModel = DetailViewModel()
    @Environment(\.presentationMode) var mode
    
    var body: some View {
        VStack {
            Text("Detail View")
            Button {
                viewModel.submitData()
            } label: {
                Text("Press to Submit Data")
            }
        }
        .onReceive(viewModel.$didSubmit) { _ in
            if viewModel.didSubmit {
                mode.wrappedValue.<dismiss()
            }
        }
    }
}

🚫  이번에는 버튼을 두번 눌려야지 실행되는 것을 볼 수 있습니다.

didSubmit의 상태를 알아보기 위해서 콘솔에 didSubmit 값을 찍어봅시다. DetailView의 코드를 아래와 같이 수정했습니다.

struct DetailView: View {
    
    @ObservedObject var viewModel = DetailViewModel()
    @State var buttonCount = 0 //👉 버튼을 누른 횟수
    @Environment(\.presentationMode) var mode
    
    var body: some View {
        VStack {
            Text("Detail View")
            Button {
                buttonCount += 1 //👉 버튼 누른 횟수 세기
                viewModel.submitData()
            } label: {
                Text("Press to Submit Data")
            }
        }
        .onReceive(viewModel.$didSubmit) { _ in
            print("버튼을 \(buttonCount)번 눌렀을 때 didSubmit 값: \(viewModel.didSubmit)") //👉 didSubmit이 발행되면 출력
            if viewModel.didSubmit {
                mode.wrappedValue.dismiss()
            }
        }
    }
}

Case 1에서 예상을 했듯이 처음에 버튼을 누르지 않은 상태에서도 false 값을 발행하는 군요. (그래도 Case 1의 버그가 일어난 것입니다.) 그리고 버튼을 1번 누른 상태에서도 여전히 false입니다. 2번 눌렀을 때야 비로소 true로 바뀌면서 원하는 대로 모달이 dismiss 되는군요.

🤔  이러한 버그는 비동기 코드에서만 발생합니다. async 블록 밖에서는 case 2와 같은 버그는 발생하지 않습니다. 하지만 우리는 서버에 데이터를 보내는 상황을 가정했으므로 async 상황에서도 이 버그를 해결할 수 있어야 합니다.

해결 방법

onReceive가 인자로 받는 클로저에는 인자가 하나 있습니다. 이 인자의 타입은 아래와 같습니다.

간단하게 이야기하면 bool 값을 발행하는 publisher의 발행 결과를 의미합니다.

⭐️ Publisher는 발행하는 과정은 내부적으로 여러 단계를 거쳐서 일어납니다. 따라서 언제 didSubmit 값을 참조하느냐에 따라서 위 같은 버그들이 발생할 수 있는 것이죠. 따라서 더 안전하게 하기 위해서는 해당 Publisher가 최종적으로 발행하는 값을 참조하는 것이 가장 좋습니다.

콘솔에 출력한 것과 실제 실행결과가 우리가 의도했던 것과 일치하는 것을 볼 수 있습니다.

struct DetailView: View {
    
    @ObservedObject var viewModel = DetailViewModel()
    @State var buttonCount = 0
    @Environment(\.presentationMode) var mode
    
    var body: some View {
        VStack {
            Text("Detail View")
            Button {
                buttonCount += 1 //👉 버튼 누른 횟수 세기
                viewModel.submitData()
            } label: {
                Text("Press to Submit Data")
            }
        }
        .onReceive(viewModel.$didSubmit) { completed in
            print("버튼을 \(buttonCount)번 눌렀을 때 completed 값: \(completed)") //👉 didSubmit이 발행되면 출력
            if completed {
                mode.wrappedValue.dismiss()
            }
        }
    }
}

profile
백과사전 보다 항해일지(혹은 표류일지)를 지향합니다.

0개의 댓글