각자 구현한 UI의 병합과 상호작용을 논의할 때, 데이터를 처리하는 별도의 구조가 필요함이 상기되었다. SA 때는 화면에 보이는 UI만을 기준으로 기능을 분리했지만, 뒤늦게 MVC 아키텍처를 도입하여 각 View 요소를 분리하였다. 그런데 View와 Model, 그리고 Controller를 분리하면서 버튼액션(@objc func 등)을 어디에 구현하는지 형태가 갈라지게 되었다.
예를 들어, 상단 메뉴 바에서 카테고리를 선택했을 때 다른 메뉴(뷰)를 보여주는 기능, 그리고 메뉴를 추가했을 때 장바구니에 담기는 기능은 ViewController에 구현했지만, 장바구니를 비우거나 결제하는 버튼액션은 OrderSummaryView에 구현하였다. 결과적으로 비즈니스 로직은 카테고리 선택 기능을 제외하고는 모두 싱글톤 패턴이 적용된 OrderManager라는 Model에서 처리하게 되지만, 버튼을 통한 표면적인 상호작용은 서로 다른 곳에 구현이 된 셈이다.
그런 상황에서 '취소하기' 버튼에 Alert 창을 띄우는 기능을 추가하는 형태를 고민하게 되었는데, 이를 기회 삼아 여러 방법을 시도해보고 결론적으로는 버튼액션 구현의 장소가 통일될 수 있도록 하였다.
누르면 바로 반응하는 '취소하기' 버튼 |
---|
위와 같이 장바구니를 비워주는 취소하기 버튼을 구현했는데, 여러 항목을 고민해서 추가했다가 실수로 누르면 곤란하겠다는 생각이 들었다. 그래서 UIAlertController를 이용해 사용자의 의사를 확인하는 Alert를 구현하기로 했다.
그런데 기존의 코드 구조를 참조하며 @objc func
를 구현하려 했더니, MainViewController
와 OrderSummaryView
양측에 버튼액션이 구현되어 있었다.
@objc private func handleCancelOrder() {
// 주문 내역이 비어있는지 확인
guard !OrderManager.shared.orders.isEmpty else {
print("취소할 주문 내역이 없습니다.")
return
}
// UIAlertController로 사용자 확인
let alert = UIAlertController(
title: "주문 취소",
message: "정말로 주문을 취소하시겠습니까?",
preferredStyle: .alert
)
// "확인" 버튼 액션
let confirmAction = UIAlertAction(title: "확인", style: .destructive) { _ in
// 주문 취소 로직 수행
if OrderManager.shared.orders.isEmpty {
print("취소할 주문 내역이 없습니다.")
} else {
print("주문이 취소되었습니다.")
OrderManager.shared.resetOrders()
NotificationCenter.default.post(name: .orderUpdated, object: nil)
}
}
// "취소" 버튼 액션
let cancelAction = UIAlertAction(title: "취소", style: .cancel, handler: nil)
// 액션 추가
alert.addAction(confirmAction)
alert.addAction(cancelAction)
// Alert 뷰를 표시
self.present(alert, animated: true, completion: nil)
}
위가 Alert 뷰를 띄우기 위해 구현한 코드이다. 구현부의 끝에서 Alert 뷰를 표시하는 self
는 present
메서드를 가지고 있어야 한다. UIView 클래스를 상속한 OrderSummaryView
에는 이러한 메서드가 없다. 반대로 UIViewController 클래스는 present 메서드를 가지고 있다.
옳거니, 그럼 위 코드는 UIViewController
클래스를 상속한 MainViewController
에 구현하면 되겠다.
문제 해결 |
---|
같은 곳에서 버튼액션을 관리하기 위해 View에 있던 나머지 버튼액션도 MainViewController
에 옮겨 구현해주었다.
그런데 만약 해당 View에 속한 버튼의 액션은 모두 그 View 안에 구현하고 싶다면 어떻게 해야 할까?
방법이 없는지 찾아보았다.
// 뷰 컨트롤러에서 present
if let viewController = findViewController() {
viewController.present(alert, animated: true, completion: nil)
}
여기서 findViewController
함수는 extension UIView
를 통해 구현 가능하며, 그 View에서 가장 가까운 뷰컨트롤러를 찾아 반환한다.
extension UIView {
func findViewController() -> UIViewController? {
var responder: UIResponder? = self
while let nextResponder = responder?.next {
if let viewController = nextResponder as? UIViewController {
return viewController
}
responder = nextResponder
}
return nil
}
}
이것이 가능한 이유는 UIView와 UIViewController가 모두 UIResponder라는 클래스를 상속 받았고, 나아가 UIResponder
클래스는 next라는 연산 프로퍼티를 갖고 있기 때문이다. next
속성은 상위 뷰(superview) 또는 뷰 컨트롤러를 가리킨다.
이를 토대로 findViewController
함수를 차례대로 설명하면 이렇다:
extension UIView {
// 반환 타입은 UIViewController?로, UIView가 속한 뷰 컨트롤러를 반환하거나 없으면 nil을 반환한다.
func findViewController() -> UIViewController? {
// self는 메서드를 호출한 뷰(UIView)를 의미함
var responder: UIResponder? = self
// 상위 뷰 또는 뷰 컨트롤러를 다음 응답자로 반환함
while let nextResponder = responder?.next {
// 현재 응답자가 UIViewController인지 확인 (UIViewController에 present 메서드가 있기 때문)
if let viewController = nextResponder as? UIViewController {
return viewController // UIViewController가 맞다면 해당 컨트롤러 반환
}
responder = nextResponder // 다음 (상위) 응답자로 이동 (루프)
}
return nil // 루프가 끝나도 UIViewController가 없으면 nil 반환
}
}
present
호출), 매우 유용하다.UIView
만 사용되는 경우), 이 메서드는 항상 nil
을 반환한다.next
속성은 UIKit의 UIResponder
클래스에서 제공하는 속성으로, 현재 객체의 "다음 응답자"를 반환한다. UIKit의 뷰와 뷰 컨트롤러는 모두 UIResponder
를 상속하므로, 이벤트 처리 체계에서 다음으로 이벤트를 전달할 객체를 나타낸다.
next
의 역할UIResponder
는 터치 이벤트나 모션 이벤트와 같은 사용자 입력을 처리함next
는 이벤트가 전달될 다음 객체를 나타냄next
로 이벤트가 전파됨next
속성의 반환값next
는 현재 객체의 다음 응답자 객체를 반환하며, 반환값은 객체의 종류에 따라 다르다.
UIView
UIView
의 경우, next
는 소속된 상위 뷰(superview) 또는 뷰 컨트롤러를 반환한다.next
는 버튼을 포함하는 뷰.next
는 해당 뷰를 소유하는 뷰 컨트롤러.UIViewController
UIViewController
의 next
는 부모 뷰 컨트롤러 또는 해당 컨트롤러를 포함하는 뷰 계층의 상위 컨트롤러를 반환한다.next
는 내비게이션 컨트롤러.최상위 객체
UIResponder
(예: UIApplication
)의 경우, next
는 nil
을 반환한다.next
속성의 동작은 객체의 계층 구조에 따라 결정된다. 아래는 UIView
와 UIViewController
에서의 관계를 나타낸다.
UIView 계층 구조
UIViewController 계층 구조
UINavigationController
)next
를 확인하기let button = UIButton()
if let nextResponder = button.next {
print("Button's next responder is: \(nextResponder)")
}
next
는 상위 뷰를 반환한다.next
는 해당 뷰 컨트롤러를 반환한다.next
를 확인하기let viewController = UIViewController()
if let nextResponder = viewController.next {
print("ViewController's next responder is: \(nextResponder)")
}
next
는 내비게이션 컨트롤러를 반환한다.next
는 nil
이다.next
는 런타임에 동적으로 결정되며, 객체 계층에 따라 반환값이 달라진다.next
를 가지고 있는 것은 아니며, 최상위 객체는 nil
을 반환한다.