Notice
Recent Posts
Recent Comments
Link
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

cchanmi

[iOS] TextView Responder 권한이 남아 있어 Keyboard 감지 Notification이 중복 호출 되는 이슈 해결 본문

iOS

[iOS] TextView Responder 권한이 남아 있어 Keyboard 감지 Notification이 중복 호출 되는 이슈 해결

cchanmi 2024. 8. 4. 16:13

최근 탈퇴 사유를 수집하는 기능을 구현하다가 큰 버그를 발견하게 되어서 글을 작성하게 되었습니다.

해당 뷰는 TextView 클릭시 키보드가 활성화 및 비활성화 되면서 키보드 높이 만큼 화면을 이동시켜야 했고, 탈퇴버튼 클릭시 키보드를 감지하는 notification이 중복 호출되면서 두 배 이상의 키보드 높이 크기가 이동되는 버그가 발생했습니다.

 

당황...

break point를 찍어 보았을 때, 키보드가 사라지는 걸 감지하는 notification, 올라오는 걸 감지하는 notification가 여러 번 호출되는 것을 알 수 있었습니다.

 

output.notEnabledButtonResult
    .sink { [weak self] type in
        self?.resignButton.changeButtonType(buttonType: type)
        self?.summaryTextView.becomeFirstResponder()
    }
    .store(in: &cancelBag)

 

 

 

TextView 혹은 TextFeild를 통해 키보드를 활성화할 때 항상 becomeFirstResponder(), resignFirstResponder() 두 메서드를 사용을 해 왔는데, 해당 두 메서드는 해당 객체에게 Responder 권한을 주어서 이벤트 처리를 도와주도록 하죠.

 

becomeFirstResponder : UIKit에게 현 Window에서 해당 객체를 first responder가 되도록 요청한다 (무조건 되는 것은 아니라고 함)

resignFirstResponder : 현 Window에서 객체의 상태가 first responder에 대한 소유권을 포기하도록 객체에게 알린다

 

TextView에 계속해서 Responder 권한이 있는 상태

다시 본론으로 돌아오면, 현재 코드에서는 TextView를 눌렀을 때 First Responder 권한은 주었지만, 이따른 권한을 resign 하는 코드를 추가해 주지 않았기 때문에 TextView에 계속해서 Responder 권한이 있는 상태인 것을 발견했습니다.

 

resign 코드를 추가해 주지 않아도 키보드가 비활성화되었던 이유는 버튼 클릭시 UIAlertController 팝업이 불러와지고, 팝업 삭제 버튼 클릭시, 삭제 API가 호출이 되면서 초기 화면으로 보내는 과정에서 UIApplication 객체에 접근하기 때문이었습니다.

resignButton.tapPublisher
    .sink { [weak self] _ in
        print("탈퇴 버튼 클릭!")
        
        let alert = UIAlertController(title: "계정을 삭제하시겠습니까?", message: "이전에 작성했던 일기는 모두 사라집니다.", preferredStyle: .alert)
        let cancel = UIAlertAction(title: "취소", style: .cancel) { _ in }
        let delete = UIAlertAction(title: "삭제", style: .destructive) { _ in
            self?.resignButtonTapped.send(())
        }
        alert.addAction(cancel)
        alert.addAction(delete)
        self?.present(alert, animated: true, completion: nil)
    }
    .store(in: &cancelBag)

 

 

func changeRootViewController(_ viewController: UIViewController) {
    guard let window = UIApplication.shared.windows.first else { return }
    UIView.transition(with: window, duration: 0.5, options: .transitionCrossDissolve, animations: {
        let rootVC = UINavigationController(rootViewController: viewController)
        window.rootViewController = rootVC
    })
}

 

 

그대로 print문을 찍어보았을 때, 키보드 notification이 여러 번 호출되는 것을 확인할 수 있었습니다.

 

탈퇴 버튼 클릭시, UIAlertController가 표시되면서 TextView가 가지고 있던 first responder 소유권을 포기하게 됩니다. 그로인해 resign 코드를 추가해 주지 않아도 자연스럽게 키보드가 비활성화되는 모습을 볼 수 있지만, Alert 버튼을 클릭 후에 Alert창이 사라질 때, TextView에게 다시 first responder 소유권이 돌아가기 때문에 키보드가 다시 활성화되는 것을 알 수 있었습니다.

 

또한, API 통신 후 홈 화면으로 루트 뷰를 바꿔 주는 과정에서 UIApplication 객체에 접근하는 코드로인해 TextView의 first responder 소유권이 다시금 빼앗기게 되면서 키보드가 다시 또 내려가는 것을 볼 수 있었습니다.

 

사실상 UI적으로만 TextView가 잠시 내려간다는 것이고, Responder를 resign을 하지는 않았기 때문에 다른 이벤트 처리를 끝내고 나면 다시금 TextView로 Responder 소유권이 돌아온다는 것을 알 수 있었고, 그로인해 계속해서 키보드가 활성화되는 notification이 다시 호출되는 것을 알 수 있었습니다.

 

해결하기 위해서는 alert 창이 뜨기 전에 직접 TextView의 Responder 권한을 해지해 주기

resignButton.tapPublisher
    .handleEvents(receiveOutput: {[weak self] _ in
        self?.summaryTextView.resignFirstResponder()
    })
    .sink { [weak self] _ in
        print("탈퇴 버튼 클릭!")
        
        let alert = UIAlertController(title: "계정을 삭제하시겠습니까?", message: "이전에 작성했던 일기는 모두 사라집니다.", preferredStyle: .alert)
        let cancel = UIAlertAction(title: "취소", style: .cancel) { _ in }
        let delete = UIAlertAction(title: "삭제", style: .destructive) { _ in
            self?.resignButtonTapped.send(())
        }
        alert.addAction(cancel)
        alert.addAction(delete)
        self?.present(alert, animated: true, completion: nil)
    }
    .store(in: &cancelBag)

 

탈퇴하기 버튼이 tap 되었을 때, 해당 이벤트를 감지하고, 결과를 받기 전에 handlerEvents operator를 사용하여 textView의 소유권을 직접 resign 해 주는 코드를 추가하였고, 이로인해 alert 창이 뜨고 사라져도 responder의 권한이 다시 textView로 넘어가지 않게 되었습니다.

 

의도대로 잘 동작하는 것을 볼 수 있었습니다.

 

Comments