본문 바로가기

학습 노트/iOS (2021)

133 ~ 134. Notification Center and Notification

Notification Overview

Notification은 이벤트가 발생했음을 알리는 도구이다.
예를 들어 문자가 오면 알림음과 함께 알림이 표시된다.

사용자는 Notification의 동작이나 종류에 대해 생각할 필요가 없지만 개발자는 이들의 차이와 구현 방식의 차이를 정확히 알아야 한다.

iOS 앱 개발자 사용할 수 있는 Notification은 3가지이다.

  • Notification
    아무런 접두어가 없는 Notification이다.
    하나의 프로그램 내에서 객체들이 주고받는 메시지를 의미한다.
    특정 이벤트에 대한 Observer를 등록하고, 이벤트가 Broadcasting 되면 원하는 코드를 실행하는 방식으로 구현한다.
  • Local Notification
    시계 알람처럼 지정된 시간에 사용자에게 알림을 전달할 때 사용한다.
  • Remote Notification
    Push Notification이라고도 부른다.
    외부 서버에서 전달되는 알림을 의미한다.

Notification은 프로그램 내에서 객체들끼리 주고받기 때문에 사용자에게 노출되지 않지만,
Local Notification과 Remote Notification은 사용자에게 노출된다.

Notification은 Foundation Framework가 제공하는 API를 통해 구현된다.
중심이 되는 객체는 Notification Center이다.
이는 앱들 각각에게 주어지는 Singetone 인스턴스로, 객체가 전달한 Notification을 Observer로 전달한다.
Notification은 NSNotification 클래스로 구현되어있고, 이름을 통해 이들을 구별할 수 있다.
또한, user info dictionary를 통해 연관된 데이터를 함께 전달할 수 있다.

Notification은 객체들이 주고받는 메시지이고 사용자에게 시각적으로 노출되지 않는다.
따라서 별도의 허가 없이 자유롭게 사용할 수 있다.

Local Notification과 Remote Notification은 User Notification Framework를 통해 구현된다.
해당 Framework은 iOS 10에 도입됐다.
이전에는 API가 흩어져있었지만 이후 버전에선 통합 API를 사용해 다양한 플랫폼에서 동일한 API로 쉽게 개발할 수 있다.
해당 API에서 사용하는 User Notification Center는 권한 요청부터, 처리까지 모두 담당한다.

Local Notification은 하나의 앱에서 지역적으로 사용할 수 있는 Notification이다.
특정 시점에 알림이 필요할 때 UNMutableNotificationContent 인스턴스를 생성한다.
해당 인스턴스에 Notification을 받을 시간과 내용을 담아 User Notification Center에 예약을 요청한다.
이후엔 User Notification Center가 예약을 관리하고 예약된 시점에 Notification을 전달한다.

Remote Notification은 원격 서버에서 전달되는 Notification이다.
이를 구현하기 위해선 Provider라고 부르는 원격 서버를 직접 구축해야 한다.
Provider는 JSON에 Notification의 정보를 담아 Apple Push Notification Service에 전송을 요청한다.
해당 서비스는 APNs라고 부른다.
APNs는 인증된 Provider에서 요청이 전송되면 실제 기기에게 Notification을 전달한다.
APNs는 Push 인증서를 통해 Provider를 인증한다.
인증서는 iOS Procisioning Portal에서 생성할 수 있다.
해당 기능은 Apple Developer Program에 가입되어있어야 사용할 수 있다.

Local Notification과 Remote Notification은 사용자에게 시각적으로 청각적으로 노출되므로 반드시 사용자로부터 허가를 받아야 한다.
즉, 사용자가 허가한 앱만 이를 사용할 수 있다.
사용자자는 설정 앱을 통해 이를 언제든지 관리할 수 있고, 앱을 구현할 때는 사용자의 설정을 존중해야 한다.

  Notification Local Notification Remote Notification
Foreground ⭕️ ⭕️ ⭕️
Background ⭕️ ⭕️ ⭕️
Not Running ⭕️ ⭕️

Notification은 Posting 즉시 등록돼 Observer에게 전달된다.
따라서 코드가 실행 중인 상태에서는 언제든지 주고받을 수 있다.
Local Notification은 앱에서 예약을 요청해야 한다.
이후에는 User Notification Center가 관리하기 때문에 실행상태에 상관없이 작동한다.
Remote Notification은 Provider가 전송을 요청한다.
APNs는 요청 즉시 기기로 전달하지만 네트워크 상태에 따라 지연이 발생할 수 있다.
전달된 후에는 Local Notification과 마찬가지로 실행상태에 관계없이 전달된다.

Local Notification과 Remote Notification은 공통점이 많다.
이는 둘이 User Notification을 통해 관리되기 때문이다.
Notification이 User Notifiation Center에 전달되면 앱이 실행 중인 상태인지 확인한다.
앱이 실행 중이라면 Delegate로 이를 전달하고, 이후의 과정은 Delegate에 위임한다.
앱이 실행 중이 아니라면 직접 Banner를 표시한다.
사용자는 이를 Pull Down 해 상세정보를 확인하거나 연관된 Action을 실행할 수 있다.

사용자가 확인하지 않은 Notification은 Notification Center에 자동으로 분류된다.
사용자는 여기에서 Notification을 확인하고 연관된 Action을 실행하거나 직접 삭제할 수 있다.
User Notification Center를 통해 확인하지 않은 Notification 목록을 확인하고 앱에서 직접 삭제할 수도 있다.

 

Notification Center & Notification

모든 앱은 Notification Center를 하나씩 가지고 있다.
이는 Foundation Framework에 구현된 인스턴스를 의미하며,
같은 이름의 iOS Notification Center와 혼동하지 않도록 주의한다.

객체가 주고받는 이벤트는 Notification 구조체의 인스턴스이다.
name 속성을 통해 이벤트의 종류를 지정하고, userInfo 속성에 연관된 데이터를 담아서 전달할 수 있다.

Notification Center는 센터의 역할을 수행한다.
Notification을 보내고 싶은 객체는 이름과 데이터를 하나로 묶어 Notification Cneter로 전달한다.
Notification을 처리하고 싶은 객체는 Notification Cneter에 Observer를 등록한다.
Notification Center는 Observer가 처리하는 Notification 이름과 코드를 Dispatch Table에 저장한다.
Notification이 전달되면 동일한 Notification을 처리하는 모든 Observer에게 전달한다

실습에 사용할 Storyboard는 위와 같다.
Scene에는 버튼과 레이블이 존재하며, 버튼을 누르면 Compose Scene이 표시된다.
Compose Scene에는 Text Field가 존재하고, 우측 상단의 Post를 누르면 Text Field의 값을 Notification으로 전달하고,
Notification Center Scene의 레이블에 전달된 값을 출력하도록 구현한다.

//
//  ComposeViewController.swift
//  Notification Practice
//
//  Created by Martin.Q on 2021/11/22.
//

import UIKit

class ComposeViewController: UIViewController {
    
    @IBOutlet weak var inputField: UITextField!
    
    @IBAction func close(_ sender: Any) {
        dismiss(animated: true, completion: nil)
    }
    
    @IBAction func post(_ sender: Any) {
        dismiss(animated: true, completion: nil)
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        inputField.becomeFirstResponder()
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        inputField.resignFirstResponder()
    }
}

Compose Scene의 클래스 파일은 위와 같다.

//
//  ComposeViewController.swift
//  Notification Practice
//
//  Created by Martin.Q on 2021/11/22.
//

import UIKit

extension NSNotification.Name {
    static let NewValueDidInput = NSNotification.Name("NewValueDidInput")
}

class ComposeViewController: UIViewController {
    
    @IBOutlet weak var inputField: UITextField!
    
    @IBAction func close(_ sender: Any) {
        dismiss(animated: true, completion: nil)
    }
    
    @IBAction func post(_ sender: Any) {
        guard let text = inputField.text else { return }
        
        NotificationCenter.default.post(name: NSNotification.Name.NewValueDidInput, object: nil, userInfo: ["NewValue": text])
        
        dismiss(animated: true, completion: nil)
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        inputField.becomeFirstResponder()
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        inputField.resignFirstResponder()
    }
}

postValue 메서드에서 textField의 값을 가져오도록 구현한다.

이후 기본으로 제공되는 NotificationCenter의 인스턴스는 default 속성으로 얻을 수 있다.

Notification을 전달할 때는 post 메서드를 사용한다.
userInfo를 파라미터로 사용하는 메서드를 사용한다.

extension UIResponder {

    
    public class let keyboardWillShowNotification: NSNotification.Name

    public class let keyboardDidShowNotification: NSNotification.Name

    public class let keyboardWillHideNotification: NSNotification.Name

    public class let keyboardDidHideNotification: NSNotification.Name

    
    @available(iOS 3.2, *)
    public class let keyboardFrameBeginUserInfoKey: String // NSValue of CGRect

    @available(iOS 3.2, *)
    public class let keyboardFrameEndUserInfoKey: String // NSValue of CGRect

    @available(iOS 3.0, *)
    public class let keyboardAnimationDurationUserInfoKey: String // NSNumber of double

    @available(iOS 3.0, *)
    public class let keyboardAnimationCurveUserInfoKey: String // NSNumber of NSUInteger (UIViewAnimationCurve)

    @available(iOS 9.0, *)
    public class let keyboardIsLocalUserInfoKey: String // NSNumber of BOOL

    
    // Like the standard keyboard notifications above, these additional notifications include
    // a nil object and begin/end frames of the keyboard in screen coordinates in the userInfo dictionary.
    @available(iOS 5.0, *)
    public class let keyboardWillChangeFrameNotification: NSNotification.Name

    @available(iOS 5.0, *)
    public class let keyboardDidChangeFrameNotification: NSNotification.Name
}

keyboardDidShowNotification은 키보드가 화면에 표시된 다음 전달되는 Notification이다.
외에도 여러 가지 Notification이 선언되어있는데 하나같이 NSNotification.Name을 상속하고 있다.
만약 새로운 Notification을 만든다면 위와 같은 규칙을 따라 만드는 것이 좋다.

extension NSNotification.Name {
    static let NewValueDidInput = NSNotification.Name("NewValueDidInput")
}

새로운 Notification을 생성한다.
이때 생성자로 전달하는 문자열은 다른 Notification의 이름과 중복되지 않는 이름으로 전달해야 한다.

새로 만든 Notification을 post의 첫 번째 파라미터로 전달한다.
두 번째 파라미터에는 Notification을 전달하는 객체를 전달한다. 실습에선 nil을 전달한다.
Notification Any의 형식을 가지고 있기 때문에 형식의 제한이 없어 Notification과 연관된 데이터를 전달하는 데 사용하기도 한다.
세 번째 파라미터에는 Notification과 연관된 데이터를 Dictionary로 전달한다.
하나의 값을 전달해도 Dictionary를 사용해야 하고, 사용할 때도 Key를 통해 꺼내야 한다.

여기까지 하면 Notification Center로 Notification이 전달된다.

//
//  NotificationCenterViewController.swift
//  Notification Practice
//
//  Created by Martin.Q on 2021/11/22.
//

import UIKit

class NotificationCenterViewController: UIViewController {
    
    @IBOutlet weak var valueLabel: UILabel!
    

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    deinit {
        print(#function)
    }

}

이번엔 Notification Center에 Observer를 등록하고 Notification을 전달받았을 때 사용할 코드를 작성한다.
사용할 Notification Center Scene에 연결된 클래스 파일은 위와 같다.

Observer는 두 가지 방식으로 등록할 수 있다.

  • 특정 객체와 메서드를 Observer로 등록하기
  • Closure를 Observer로 등록하기

두 방법은 Observer의 해제 방법이 다르고, 구현 패턴도 조금씩 다르다.

//
//  NotificationCenterViewController.swift
//  Notification Practice
//
//  Created by Martin.Q on 2021/11/22.
//

import UIKit

class NotificationCenterViewController: UIViewController {
    
    @IBOutlet weak var valueLabel: UILabel!
    
    @objc func process(notification: Notification) {
        guard let value = notification.userInfo?["NewValue"] as? String else { return }
        valueLabel.text = value
        print("#1",#function)
    }
    

    override func viewDidLoad() {
        super.viewDidLoad()
        
        NotificationCenter.default.addObserver(self, selector: #selector(process(notification:)), name: NSNotification.Name.NewValueDidInput, object: nil)
    }
    
    deinit {
        print(#function)
    }

}

첫 번째 방법으로 구현할 때는 selector, name, object를 파라미터로 받는 addObserver 메서드를 사용한다.
첫 번째 파라미터에 Observer로 지정할 객체를 전달한다.
두 번째 파라미터에 실행할 메서드를 selector로 전달한다.

@objc func process(notification: Notification) {
    guard let value = notification.userInfo?["NewValue"] as? String else { return }
    valueLabel.text = value
    print("#1",#function)
}

Observer의 selector로 지정하는 메서드는 Notification 형식의 파라미터를 가져야 한다.
이는 Notification을 전달하는 Compose Scene에서 입력된 값을 NewValue를 키로 갖는 Dictionary에 저장하고,
이것을 다시 userInfo에 담아 Notification의 형태로 전달하기 때문이다.

세 번째 파라미터에는 Oberver가 처리할 Notification의 이름을 전달한다.
네버째 파라미터에는 Sender를 제한할 때 사용한다.
Notification을 전달하는 Compose Scene에서 sender를 제한하고 있지 않기 때문에 지금은 사용하지 않는다.
만약 제한했다면 다른 객체가 Notification을 보냈는지 Compose Scene이 보냈는지 파악할 수 있게 된다.
만약 동일한 Notification이라도 Sender에 따라 실행 결과가 달라져야 한다면 해당 파라미터를 활용해야 한다.

결과를 확인해 보면 입력한 값을 사용해 Label을 업데이트하는 것을 확인할 수 있다.

또한 콘솔에도 메서드가 실행됐음을 알리는 로그가 출력된다.

이전 화면으로 돌아가면 View Controller 인스턴스가 메모리에서 해제된다.
따라서 소멸자가 실행되고 콘솔에 로그가 출력된다.
해당 상태에서 Notification을 전달하면 어떻게 되는지 확인해 본다.

//
//  ListTableViewController.swift
//  Notification Practice
//
//  Created by Martin.Q on 2021/11/22.
//

import UIKit

class ListTableViewController: UITableViewController {
    
    @objc func postNotification() {
        
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Post", style: .plain, target: self, action: #selector(postNotification))
    }
    
}

Scene으로 분기하기 전 화면인 메인화면의 Root View에 연결된 ListTableViewController 클래스는 위와 같다.
해당 클래스의 viewDidLoad에는 RightBarButtonItem을 추가하고, 해당 버튼을 누르면 Notification을 전달하도록 구현되어있다.

//
//  ListTableViewController.swift
//  Notification Practice
//
//  Created by Martin.Q on 2021/11/22.
//

import UIKit

class ListTableViewController: UITableViewController {
    
    @objc func postNotification() {
        NotificationCenter.default.post(name: NSNotification.Name.NewValueDidInput, object: nil, userInfo: ["NewValue": "Test"])
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Post", style: .plain, target: self, action: #selector(postNotification))
    }
    
}

selector로 연결된 메서드에서 Notification을 전송하도록 구현했다,

버튼을 누르면 Notification은 전송되지만 이를 처리할 Observer가 존재하지 않기 때문에 반응이 없다.
Notification Center는 Notification을 전달하기 전에 등록된 Observer가 실제로 존재하는지 확인한다.
존재한다면 이를 전달하고, 존재하지 않는다면 Dispatch Table에서 제거한다.

이러한 과정은 iOS 9 이전 전에서는 지원하지 않으므로 더 이전 버전에서 실행해야 한다면 Observer를 직접 해제해야 한다.
또한 특정 시점에 더 이상 Notification을 처리하지 않는 경우에도 직접 해제해야 한다.

//
//  NotificationCenterViewController.swift
//  Notification Practice
//
//  Created by Martin.Q on 2021/11/22.
//

import UIKit

class NotificationCenterViewController: UIViewController {
    
    @IBOutlet weak var valueLabel: UILabel!
    
    @objc func process(notification: Notification) {
        guard let value = notification.userInfo?["NewValue"] as? String else { return }
        valueLabel.text = value
        print("#1",#function)
    }
    

    override func viewDidLoad() {
        super.viewDidLoad()
        
        NotificationCenter.default.addObserver(self, selector: #selector(process(notification:)), name: NSNotification.Name.NewValueDidInput, object: nil)
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
        print(#function)
    }

}

Observer를 해제할 때는 removeObserver 메서드를 사용한다.
Observer를 생성한 객체의 소멸자에서 생성한다.

name을 파라미터로 가지는 removeOberver 메서드를 호출해 특정 Notification만 처리하는 Observer를 해제할 수 있다.
실습에서는 기본형을 사용해 해당 객체의 모든 Observer를 해제한다.

@objc func process(notification: Notification) {
    print(Thread.isMainThread ? "Main" : "Background")
    
    guard let value = notification.userInfo?["NewValue"] as? String else { return }
    valueLabel.text = value
    print("#1",#function)
}

이번엔 process 메서드에서 thread를 확인하는 코드를 작성하고 결과를 확인한다.

Compose Scene에서 Button을 누르고 Notification을 post 하면 Main이 출력된다.
해당 메서드가 Main Thread에서 실행되기 때문인데 이는 Notification을 Main Thread에서 전달했기 때문이다.

@IBAction func post(_ sender: Any) {
    guard let text = inputField.text else { return }
    
    DispatchQueue.global().async {
        NotificationCenter.default.post(name: NSNotification.Name.NewValueDidInput, object: nil, userInfo: ["NewValue": text])
    }
    
    dismiss(animated: true, completion: nil)
}

Compose Scene의 클래스 파일에서 post 메서드를 다른 Tread로 분리했다.

실행해 보면 동작은 하지만 반응이 느리고, 이상한 로그가 출력된 뒤 앱은 충돌로 종료된다.
Main이 아닌 Background가 출력된 뒤,
UI API를 호출했다는 로그가 출력된다.
Xcode는 Main Thread의 Checker를 통해 UI 코드를 Background에서 실행할 때 경고를 출력한다.

Issue를 확인해 보면 UILabel의 text 속성은 반드시 main thread에서 사용해야 한다는 경고가 표시된다.

코드에도 같은 경고가 표시된다.
충돌로 종료됐지만 경우에 따라 Label이 업데이트되지 않거나 굼뜨게 작동하고 마는 경우도 있다.
이렇게 되는 이유는 Notification을 Background Thread에서 전달했고, Observer도 동일한 코드에서 실행했기 때문이다.
Notification을 전달하는 시점에 적절한 Thread에서 실행되도록 할 수도 있지만
Notification을 처리하는 쪽에서 구현하는 것이 보편적이다.

UI 코드가 Main Thread에서 실행되도록 수정한다.

@objc func process(notification: Notification) {
    print(Thread.isMainThread ? "Main" : "Background")
    
    guard let value = notification.userInfo?["NewValue"] as? String else { return }
    
    DispatchQueue.main.async {
        self.valueLabel.text = value
    }
    
    print("#1",#function)
}

결과를 확인해 보면 정상적으로 동작한다.
어떤 Thread에서 Notification을 보내더라도 Label의 text 속성은 항상 Main Thread에서 실행되기 때문에 문제는 사라진다.

두 번째 방법으로 구현할 때는 ForName을 첫 번째 파라미터로 가지는 addObserver 메서드를 사용한다.
또한 해당 메서드는 NSObjectProtocol 인스턴스를 반환한다.

//
//  NotificationCenterViewController.swift
//  Notification Practice
//
//  Created by Martin.Q on 2021/11/22.
//

import UIKit

class NotificationCenterViewController: UIViewController {
    
    @IBOutlet weak var valueLabel: UILabel!
    
    @objc func process(notification: Notification) {
        print(Thread.isMainThread ? "Main" : "Background")
        
        guard let value = notification.userInfo?["NewValue"] as? String else { return }
        
        DispatchQueue.main.async {
            self.valueLabel.text = value
        }
        
        print("#1",#function)
    }
    

    override func viewDidLoad() {
        super.viewDidLoad()
        
        NotificationCenter.default.addObserver(self, selector: #selector(process(notification:)), name: NSNotification.Name.NewValueDidInput, object: nil)
        
        NotificationCenter.default.addObserver(forName: NSNotification.Name.NewValueDidInput, object: nil, queue: OperationQueue.main) { (notification) in
            
        }
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
        print(#function)
    }

}

첫 번째 파라미터에는 Oberver가 처리할 Notification의 이름을 전달한다.
두 번째 파라미터에는 Sender를 제한할 때 사용한다.

세 번째 파라미터에는 Closure를 실행할 Operation Queue를 전달한다.
실습에서는 Main Queue를 전달하고, 이는 Closure가 Main Thread에서 실행됨을 의미한다.

override func viewDidLoad() {
    super.viewDidLoad()
    
    NotificationCenter.default.addObserver(self, selector: #selector(process(notification:)), name: NSNotification.Name.NewValueDidInput, object: nil)
    
    NotificationCenter.default.addObserver(forName: NSNotification.Name.NewValueDidInput, object: nil, queue: OperationQueue.main) { (notification) in
        guard let value = notification.userInfo?["NewValue"] as? String else { return }
        
        DispatchQueue.main.async {
            self.valueLabel.text = value
        }
        
        print("#1",#function)
    }
}

네 번째 파라미터에는 실행할 코드를 전달한다.
첫 번째 방법에서는 selector를 통해 process 메서드의 파라미터로 Notification을 전달했지만
이번에는 Closure의 파라미터로 전달된다.

override func viewDidLoad() {
    super.viewDidLoad()
    
    NotificationCenter.default.addObserver(self, selector: #selector(process(notification:)), name: NSNotification.Name.NewValueDidInput, object: nil)
    
    NotificationCenter.default.addObserver(forName: NSNotification.Name.NewValueDidInput, object: nil, queue: OperationQueue.main) { (notification) in
        guard let value = notification.userInfo?["NewValue"] as? String else { return }
        
        self.valueLabel.text = value
        
        print("#2 \(notification.name)")
    }
}

또한 항상 Main Thread에서 실행되므로 async를 해제한다.

결과를 확인해 보면 모든 로그가 출력된다.
두 개의 Observer가 존재하므로 각각의 로그가 표시된다.

하지만 아까와는 다르게 이전 화면으로 돌아가도 소멸자가 호출되지 않는다.
이는 인스턴스가 해제되지 않았음을 의미한다.

이유는 Label의 text속성을 지속해서 capture 하고 있기 때문으로 해제가 되지 않는 것이다.

NotificationCenter.default.addObserver(forName: NSNotification.Name.NewValueDidInput, object: nil, queue: OperationQueue.main) { [weak self] (notification) in
    guard let value = notification.userInfo?["NewValue"] as? String else { return }
    self?.valueLabel.text = value
    
    print("#2 \(notification.name)")
}

이를 약한 참조로 바꾸어 문제를 해결하면 정상적으로 해제된다.

하지만 List의 Post 버튼을 누르면 두 번째 Observer의 로그가 출력된다.

심지어 한 번 들어갔다 나올 때마다 그 수가 늘어난다.

첫 번째 방법에서는 self를 Observer로 등록한다.
따라서, self가 해제되면 Observer도 따라서 해제된다.
하지만 두 번째 방법은 self와는 관계가 없다.
따라서 self가 해제돼도 여전히 Dispatch Table에 존재한다.
Compose Scene으로 진입할 때마다 같은 Observer를 중복해서 생성되기 때문에 로그가 늘어나는 것이다.
즉, 두 번째 방법으로 Observer를 생성한다면 적절한 시점에 해제하는 것이 중요하다.

var token: NSObjectProtocol?
    
    override func viewDidLoad() {
    super.viewDidLoad()
    
    NotificationCenter.default.addObserver(self, selector: #selector(process(notification:)), name: NSNotification.Name.NewValueDidInput, object: nil)
    
    token = NotificationCenter.default.addObserver(forName: NSNotification.Name.NewValueDidInput, object: nil, queue: OperationQueue.main) { [weak self] (notification) in
    guard let value = notification.userInfo?["NewValue"] as? String else { return }
        self?.valueLabel.text = value
    
        print("#2 \(notification.name)")
    }
}

반환 값을 저장할 속성을 선언하고 해당 속성에 메서드가 반환하는 인스턴스를 저장한다.

deinit {
    if let token = token {
        NotificationCenter.default.removeObserver(token)
    }
    NotificationCenter.default.removeObserver(self)
    print(#function)
}

이후 소멸자에서 Observer를 해제하도록 구현한다.

결과를 확인해 보면 더 이상 List에서 Post 버튼을 눌러도 더 이상 두 번째 Observer가 호출되지 않는다.

System Notification

iOS에서는 다양한 이벤트가 발생한다.
충전상태가 바뀌거나 기기가 회전하기도 한다.
이러한 모든 이벤트가 발생할 때마다 Notification이 전달된다.

NSNotification.Name 구조체의 개발 문서에서는 사용할 수 있는 모든 System Notification을 확인할 수 있다.

예를 들어 keyboardWillShowNotification은 키보드가 화면에 표시되기 직전에 전달된다.
Discussion 항목에는 이와 관련된 키와 값에 대한 설명이 되어있다.
UserInfo를 사용해 키보드의 위치와 사이즈를 얻을 수 있다.

//
//  SystemNotificationViewController.swift
//  Notification Practice
//
//  Created by Martin.Q on 2021/11/22.
//

import UIKit

class SystemNotificationViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

사용할 Scene과 연결된 클래스는 위와 같다.

//
//  SystemNotificationViewController.swift
//  Notification Practice
//
//  Created by Martin.Q on 2021/11/22.
//

import UIKit

class SystemNotificationViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: OperationQueue.main) { (noti) in
            print(noti.userInfo)
        }
    }

}

키보드가 표시되면 userInfo의 데이터가 콘솔에 표시되게 된다.

//
//  SystemNotificationViewController.swift
//  Notification Practice
//
//  Created by Martin.Q on 2021/11/22.
//

import UIKit

class SystemNotificationViewController: UIViewController {
    
    var token: NSObjectProtocol?

    override func viewDidLoad() {
        super.viewDidLoad()
        
        token = NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: OperationQueue.main) { (noti) in
            print(noti.userInfo)
        }
    }
    
    deinit {
        NotificationCenter.default.removeObserver(token)
        print(#function)
    }

}

Closure를 사용한 Observer이기 때문에 마찬가지로 직접 해제해줘야 한다.