지난 글에서 서로 다른 객체간 상호작용을 하기 위해 Delegate Pattern을 공부하고 사용해봤습니다. 이번에 다뤄볼 NotificationCenter 또한 서로 다른 객체간 상호작용을 할 수 있는 수단으로써 사용됩니다.
Delegate와 NotificationCenter는 모두 객체간 상호작용을 구현할 수 있지만, 사용 목적과 의도에 차이가 있습니다.
- Delegate는 한 객체가 특정한 프로토콜을 채택한 다른 객체에게 자신의 작업을 대신하도록 하는 패턴입니다. 일반적으로 한 객체가 특정 이벤트가 발생하거나 특정 상황에서 다른 객체에게 처리를 요청하는 경우에 사용되고, 일대일(one-to-one) 관계를 갖는 경우가 일반적입니다.
- NotificationCenter는 한 객체가 다른 여러 객체들에게 이벤트를 알리고, 이벤트를 수신하는 Observer들이 해당 이벤트를 처리합니다. Delegate와는 다르게 일반적으로 일대다(one-to-many)관계를 가지고, 특정 이벤트가 발생했을 때, 모든 Observer들이 이를 처리할 수 있습니다.
요약해보면 Delegate는 일대일, NotificationCenter는 일대다 관계에 적합하다는 것입니다.
NotificationCenter의 동작방식을 간단하게 설명하자면,
특정 객체가 NotificationCenter에 이벤트를 발송(Post)하면, 그 이벤트를 구독(subscribe)하고 있는 Observer들이 이벤트를 전달받을 수 있고, 각자의 로직을 처리할 수 있습니다.
일상생활속 예를들면 우리가 어떤 유튜버를 구독했을 때, 해당 유튜버가 새로운 영상을 올리면, 추천 영상에 그 영상이 뜨거나 알림이 오죠? 그것과 비슷한 개념이라고 생각하시면 됩니다!
유튜버는 NotificationCenter에 이벤트를 발송(Post)하는 특정한 사람(객체)이고, 해당 유튜버를 구독해서 새로운 영상(이벤트)을 수신하는 우리들은 Observer인 것이죠!
하나도 어렵지 않쥬? :)
바로 예제를 통해 알아보도록 하겠습니다.
타이머가 시간초과 됐을 때, Alert를 띄우는 예제와 키보드가 올라왔을 때를 감지해서 이벤트를 처리하는 예제를 NotificationCenter로 구현해볼게요.
먼저 설정한 시간이 초과됐을 때 Alert를 띄우는 예제입니다!
다음과 같이 10초가 지나면 Alert를 띄우는 예제입니다.
전체코드로 쭉 훑어보고, 코드 설명하도록 하겠습니다.
import UIKit
class TimerViewController: UIViewController {
private let timeLabel: UILabel = {
let label = UILabel()
label.text = "남은시간 : 10 sec"
label.font = .systemFont(ofSize: 22)
return label
}()
private var time = 10
//MARK: - LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
configure()
startTimer()
addObservers()
}
deinit {
NotificationCenter.default.removeObserver(self) // Observer 제거
}
//MARK: - Helpers
func configure() {
view.backgroundColor = .white
view.addSubview(timeLabel)
timeLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
timeLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
timeLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
func addObservers() {
// Observer 추가
NotificationCenter.default.addObserver(self, selector: #selector(timeout), name: Notification.Name("timeout"), object: nil)
}
func startTimer() {
DispatchQueue.global().async {
for _ in 1...10 {
sleep(1)
DispatchQueue.main.async {
self.time-=1
if self.time == 0 {
// 이벤트 발송
NotificationCenter.default.post(name: Notification.Name("timeout"), object: nil)
}
self.timeLabel.text = "남은시간 : \(self.time) sec"
}
}
}
}
//MARK: - Actions
@objc func timeout(_ notification: Notification) {
let alert = UIAlertController(title: "시간 초과", message: "10초가 초과됐습니다!", preferredStyle: .alert)
let success = UIAlertAction(title: "재시작", style: .destructive) { action in
self.time = 10
self.timeLabel.text = "남은시간 : \(self.time) sec"
self.startTimer()
}
let cancel = UIAlertAction(title: "취소", style: .cancel) { _ in }
// UIAlertController에 Action 버튼 추가
alert.addAction(success)
alert.addAction(cancel)
// alert 띄워
present(alert, animated: true)
}
}
UI 관련 부분은 중요하지 않으니 넘어가도록 하고, startTime() 메서드 먼저 보겠습니다.
func startTimer() {
DispatchQueue.global().async {
for _ in 1...10 {
sleep(1)
DispatchQueue.main.async {
self.time-=1
if self.time == 0 {
// 이벤트 발송
NotificationCenter.default.post(name: Notification.Name("timeout"), object: nil)
}
self.timeLabel.text = "남은시간 : \(self.time) sec"
}
}
}
}
타이머는 DispatchQueue를 사용해서 간단하게 구현했습니다.
1초에 한번씩 UI를 업데이트해주기 위해서 sleep(1)은 메인 스레드가 아닌 다른 스레드에서 비동기적으로 처리하고 있고, UI 업데이트는 메인 스레드에서 처리하도록 구현했습니다.
이 부분이 이해가 되지 않으신다면 GCD(Grand Centeral Dispatch)에 대해 공부하시면 됩니다!
간략하게 얘기하면, 오래걸리는 작업을 메인스레드가 아닌 다른 스레드에서 비동기적(Async)으로 처리함으로써 작업의 효율성을 높이기 위해서 사용하는 iOS 비동기 프로그래밍 방법 중 하나입니다.
오래 걸리는 작업을 메인스레드에서 하면 안되는 이유는 메인 스레드에서 UI를 담당하기 때문입니다.
따라서 sleep(1)을 다른 스레드에게 비동기적으로 맡기고 메인 스레드는 UI작업만 하는 것이죠.
다시 돌아와서... 코드를 순서대로 확인해보겠습니다.
if self.time == 0 {
// 이벤트 발송
NotificationCenter.default.post(name: Notification.Name("timeout"), object: nil)
}
처음에 잠깐 설명했는데 특정 객체가 NotificationCenter에 이벤트를 발송(Post)한다고 했죠?
그 이벤트를 발송하는 코드가 이것입니다.
그리고 이 이벤트를 구독하는 Observer는 발송된 이벤트를 받을 수 있어야겠죠? 그러려면 이벤트의 식별값이 필요합니다. 이벤트를 식별하기 위해 name 파라미터에 timeout이라는 이름을 전달한 것입니다.
object: 매개변수를 통해 Event를 발생시킬 때 특정 객체를 같이 전달할 수 있습니다.
예제에서는 아무것도 넘기지 않기 때문에 nil로 선언했습니다.
func addObservers() {
// Observer 추가
NotificationCenter.default.addObserver(self, selector: #selector(timeout), name: Notification.Name("timeout"), object: nil)
}
위에서 이벤트를 발송했다면, 해당 이벤트를 수신할 Observer를 등록해야겠죠?
코드를 직역해보면,
이 이벤트는 이 객체(self)가 받을 것이고, 이벤트가 발생하면 selector에 등록된 timeout이라는 함수를 실행시키고, timeout이라는 이벤트를 받을것이며, 넘겨받는 객체(object)는 없다(nil)!! 라고 직역할 수 있습니다.
deinit {
NotificationCenter.default.removeObserver(self) // Observer 제거
}
그리고, Observer를 등록했으면, 적절한 시점에 Observer를 제거해줘야합니다.
그렇지 않으면 메모리에 계속 남아있어 memory leak 을 초래합니다.
다음 예제는 키보드가 올라왔을 때, 내려갔을 때의 이벤트를 전달하고 처리하는 예제입니다.
키보드가 올라왔는지 내려갔는지에 따라 UILabel의 text를 업데이트해주는 간단한 예제입니다.
class ViewController: UIViewController {
//MARK: - Properties
private let mainLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 22)
label.textColor = .black
label.text = "으아아!"
return label
}()
private let textField: UITextField = {
let tf = UITextField()
tf.placeholder = "input text..."
tf.backgroundColor = .white
tf.borderStyle = .roundedRect
tf.layer.cornerRadius = 8
tf.clipsToBounds = true
return tf
}()
//MARK: - LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
addObservers()
}
deinit {
NotificationCenter.default.removeObserver(self) // Observer 제거
}
//MARK: - Helpers
func configureUI() {
view.backgroundColor = .white
view.addSubview(mainLabel)
mainLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
mainLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
mainLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
view.addSubview(textField)
textField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
textField.topAnchor.constraint(equalTo: mainLabel.bottomAnchor, constant: 10),
textField.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 50),
textField.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -50),
])
}
func addObservers() {
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillShow),
name: UIResponder.keyboardWillShowNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillHide),
name: UIResponder.keyboardWillHideNotification,
object: nil
)
}
//MARK: - Actions
@objc func keyboardWillShow(_ notification: Notification) {
mainLabel.text = "키보드 올라왔다!!"
}
@objc func keyboardWillHide(_ notification: Notification) {
mainLabel.text = "키보드 내려갔다!!"
}
}
어렵지 않게 이해할 수 있죠?!
그런데 한가지 이상한점이 있습니다.
이벤트를 전달 받으려면 전송(Post)하는 코드가 있어야 되는데,,, 없네?
옵저버를 등록만하고 전달하는 객체가 보이지 않죠?
키보드 이벤트는 UIResponder에서 처리하기 때문에 개발자가 직접 이벤트를 발송하지 않아도 됩니다.
키보드 상태가 변화됐을 때, 어떤 키보드 이벤트 상태를 전달받을 것인지에 대해서 Observer만 등록해주면 됩니다.
우리는 키보드가 올라올 때(keyboardWillShow)와 키보드가 내려갈 때(keyboardWillHide)에 대한 이벤트를 전달받고 싶으니 해당 이벤트에 대한 Observer를 등록하면 됩니다.
NotificationCenter에 대해 알아봤으니 한가지 물음에 대해 답하고 끝내도록 하겠습니다.
.
.
.
.
.
.
.
.
.
.
.
.
NotificaitonCenter에 특정한 객체가 이벤트를 발송하고, 그 이벤트를 구독하는 Observer가 이벤트를 전달받아 처리합니다.
NotificationCenter는 데이터가 변경됐을 때, 키보드 상태가 변경됐을 때 등 특정 객체에서 이벤트가 발생했을 때, 다른 다수의 객체에게 이벤트를 전달하는데 활용할 수 있습니다.