본 게시글은 How To: Pass Data Between View Controllers in Swift를 바탕으로 작성하였습니다.
여러분의 앱이 여러 개의 사용자 인터페이스(UI)를 가지고 있다면 여러분은 하나의 UI에서 다른 UI로 데이터를 전달해야 하는 경우도 생길 것입니다. Swift에서는 View Controller 사이에 어떤 방법으로 데이터를 전달할 수 있을까요?
뷰 컨트롤러(View Controller) 사이에 데이터를 주고 받는 것은 iOS 개발의 중요한 일부입니다. 여러분은 몇 가지 방법으로 이를 해낼 수 있고 각기 다른 이점과 약점을 가지고 있습니다.
뷰 컨트롤러 사이에 쉽게 데이터를 교환하는 방법을 선택하는 것은 여러분이 앱 구조를 어떻게 할 것인지에 달려 있습니다. 앱의 구조(App architecture)는 여러분이 뷰 컨트롤러 사이에 어떻게 작동이 이루어 질 것인지에 영향을 주고, 반대로 여러분이 뷰 컨트롤러 사이에 데이터 교환을 어떻게 할 것인지에 따라 앱의 구조가 달라집니다.
Swift에서 여러분은 뷰 컨트롤러 사이에 다음의 6가지 방법으로써 데이터를 주고 받을 수 있습니다.
- 인스턴스 프로퍼티(property)를 사용하는 방법(A → B 방향)
- 스토리보드(Storyboard)와 세그웨(segue)를 사용하는 방법
- 인스턴스 프로퍼티와 함수를 사용하는 방법(A ← B 방향)
- 델리게이션(delegation) 패턴을 사용하는 방법
- 클로저(closure) 또는 핸들러(completion handler)를 사용하는 방법
- NotificationCenter 또는 Observer 패턴을 사용하는 방법
이 게시글에서 여러분은 뷰 컨트롤러 사이에 데이터를 주고 받는 6가지의 각기 다른 방법에 대해 익히게 될 것입니다. 이 방법에는 프로퍼티(property)를 사용하는 방법, 세그웨(segue)를 사용하는 방법 및 NSNotificationCenter
를 사용하는 방법도 들어 있습니다. 비교적 간단한 이들 방법을 통해 좀 더 복잡한 응용까지 나아갈 수 있습니다. 준비가 되었다면 이제 시작하겠습니다.
델리게이션(delegation)을 사용하여 데이터를 되돌려 보내기
델리게이션(delegation)은 iOS SDK에서 중요하면서 빈번하게 사용되는 소프트웨어 디자인 패턴입니다. 여러분이 iOS 앱을 코딩할 것이라면 이를 이해하는 것이 필수적입니다!
델리게이션을 사용하면 부모 클래스(base class)는 파생 클래스(secondary class)의 기능에 대해 손을 떼어도 됩니다. 코더는 파생 클래스를 구현하면서 프로토콜(protocol)을 사용하기에 부모 클래스로부터 전해지는 이벤트에 반응만 하면 됩니다. 즉 부모 클래스와 파생 클래스 사이에 결합성 및 의존성이 줄어든다는 것입니다.
간단한 예를 통해 이해해 보겠습니다.
(1) 여러분이 피자 레스토랑에서 일하며 피자를 굽고 있다고 상상해 봅니다.
(2) 고객은 피자를 가지고 여러 가지를 할 수 있습니다. 바로 먹을 수도 있고, 나중에 먹으려고 얼려 놓을 수도 있고, 친구와 나눠먹을 수도 있습니다.
(3) 피자를 굽는 입장에서는 피자를 만드는 작업에 대해서만 생각하면 되고, 피자를 어떻게 먹을 것인지에 대한 작업과는 분리(decoupled)되어 있습니다. 단지 여러분은 피자가 완성되었을 때 고객에게 이를 가져갈 수 있게 알려주기만 하면 됩니다. 다시 말하면 피자를 먹는 방법에 대해서는 위임(delegate)해 버리고 여러분은 피자 굽는 일 자체에만 집중하면 됩니다.
피자를 굽는 여러분과 피자를 받아 먹게 될 고객이 서로를 이해하기 위하여 여러분은 “프로토콜(protocol)”을 정의할 필요가 있습니다.
// Swift
protocol PizzaDelegate {
func onPizzaReady(type: String)
}
// Objective-C
@protocol PizzaDelegate {
-(void) onPizzaReadyWithType:(NSString *)type;
}>
“프로토콜(protocol)”은 어떤 클래스가 구현해야 하는 함수(메소드)들에 대한 규약입니다. 클래스가 프로토콜을 준수하도록 하고 싶다면 다음과 같은 구문을 클래스 선언할 때 적어주면 됩니다.
// Swift
class MainViewController: UIViewController, PizzaDelegate {
// ...
}
// Objective-C
@interface MainViewController: UIViewController<PizzaDelegate>
// ...
@end
위와 같은 구문은,
(1) 클래스의 이름이 MainViewController
이고;
(2) UIViewController
에서 확장된 서브클래스이면서;
(3) PizzaDelegate
프로토콜을 준수(구현)하는 클래스라는 의미를 가지고 있습니다.
여러분이 이 클래스에 대해 프로토콜을 구현한다고 적었다면 이를 실제로 구현해주어야 합니다. 다음과 같이 MainViewController
에 메소드(함수)를 적어줍니다.
// Swift
func onPizzaReady(type: String) {
print("Pizza ready. type = \"\(type)\"")
}
// Objective-C
-(void) onPizzaReadyWithType:(NSString *)type {
NSLog(@"Pizza ready. type = \"%@\"", type)
}
여러분이 나중에 등장할 뷰 컨트롤러에 대한 클래스 인스턴스를 생성할 때, 앞서 살펴본 예제에서와 같이 델리게이트를 연결시켜줍니다.
// Swift
let secondaryViewController = SecondaryViewController()
secondaryViewController.delegate = self
// Objective-C
SecondaryViewController * secondaryViewController
= [[SecondaryViewController alloc] init];
[secondaryViewController setDelegate: self];
그리고나서 델리게이션의 진면목이 나타나게 됩니다. 나중에 등장하는 뷰 컨트롤러를 작성할 때 여러분은 프로퍼티를 추가하고 몇 가지 코드를 적습니다. 먼저 프로퍼티는 이렇게 적습니다.
// Swift
weak var delegate: PizzaDelegate?
// Objective-C
@interface ...
@property (atomic, weak) PizzaDelegate * delegate;
@end
@implement ...
@synthesize delegate;
@end
그리고 다음과 같이 메소드(함수)를 작성합니다.
// Swift
@IBAction func onButtonTap() {
delegate?.onPizzaReady(type: "Pizza di Mama")
}
// Objective-C
-(IBAction) onButtonTap {
[[self delegate] onPizzaReadyWithType: @"Pizza di Mama"];
}
onButtonTap()
이 피자가 완성되었을 때 호출된다고 가정하면 이 메소드(함수)는 델리게이트에서 선언된 onPizzaReady(type:)
을 호출합니다.
피자를 굽는 입장에서는 delegate
프로퍼티로 지정된 객체가 있는지 아니면 nil
인지 여부에는 관심이 없습니다. 그저 피자를 완성시켜서 고객을 향해 내어 놓기만 하면 됩니다. 이로써 여러분은 고객이 피자를 어떤 식으로 먹든 상관하지 않을 수 있게 됩니다.
번역자 추가 설명 만일 Swift로 weak var
델리게이트를 작성 시 “'weak' must not be applied to non-class-bound...”로 시작하는 오류가 발생한다면, 프로토콜을 선언할 때 무조건 참조 형식인 클래스에서만 사용되도록 제한함으로써 해소시킬 수 있다.
// Swift
protocol PizzaDelegate: class {
// ...
}
그러면 class
키워드를 써서 제한하는 것은 deprecated되었다는 경고가 뜰 수 있다. 이 때는 다음과 같이 수정한다.
// Swift
protocol PizzaDelegate: AnyObject {
// ...
}
델리게이션을 통해 뷰 컨트롤러 사이에 데이터를 주고 받는 핵심적인 과정을 짚어보면 다음과 같습니다.
(1) 델리게이트 프로토콜 선언이 필요합니다.
(2) 델리게이트 프로퍼티 선언이 필요합니다.
(3) 여러분이 데이터 처리에 관여하고 싶지 않은 클래스가 있다면, 데이터를 받을 그 클래스는 해당 프로토콜을 구현하고 있어야 합니다.
(4) 여러분이 데이터를 전달한 후의 과정에 관여하고 싶지 않다면 프로토콜에서 선언된 메소드(함수)를 호출함으로써 데이터를 전달하면 됩니다.
앞서 살펴보았던 프로퍼티만을 사용한 예제하고 여기서 다루어 본 델리게이션을 사용한 예자하고 비교해 보았을때 어떤 차이점이 있을까요? 다음과 같이 정리할 수 있습니다.
(1) 각각의 클래스 개발을 맡은 서로 다른 개발자들은 프로토콜 및 그 프로토콜이 갖는 메소드(함수)에 대해서만 합의를 보면 됩니다. 각 개발자들은 자신의 클래스가 프로토콜을 준수할 것인지 아닌지를 선택할 수 있고, 필요하다면 프로토콜 내 메소드들을 구현할 수 있습니다.
(2) MainViewController
와 SecondaryViewController
사이에는 직접적인 참조가 없습니다. 이는 앞선 예제에 비해 뷰 컨트롤러 객체들끼리 다소 느슨하게 결합되었음을 의미합니다.
(3) 여기서 선언한 프로토콜은 MainViewController
뿐만 아니라 어떤 클래스에서든 구현될 수 있습니다.
굉장합니다. 이제 클로저(closure)를 사용하는 또 다른 예제에 대해 살펴보도록 하겠습니다.
참고 weak
라고 표시된 델리게이트 프로퍼티는 무엇일까요? Automatic Reference Counting(ARC) in Swift를 참고해 보세요.