Delegate 패턴

- 위임 패턴

- iOS를 많이 겪어보지 않았다면 생소한 패턴이다.

 

패턴

반복해서 나타나는 형태를 의미한다. -> 유명한 싱글턴, 팩토리, 어댑터, 옵저버 등의 디자인 패턴도 이러한 '패턴' 인 것이다.

 

델리게이트 패턴

객체지향 프로그래밍(OOP)에서 하나의 객체가 모든 일을 처리하는 것이 아니라 일들 중 일부를 다른 객체에 넘기는 것

효율 성 관점에서 아주 중요하다 -> 기능을 위임할 수 있는 객체가 있다는 것은 그만큼 직접 구현해야 하는 부분이 적다는 것이기 때문.

 

GUI 프로그램에서의 예를 들어보자

  1. 마우스 버튼을 클릭한다
  2. 마우스는 시리얼 케이블이나 블루투스 통신을 통해 신호를 전달한다
  3. 신호는 RS232 통신 프로토콜을 이용해 메인보드를 거쳐 OS의 메시지 센터로 전달된다
  4. 신호를 받은 OS는 마우스 포인터의 화면상 좌표를 확인한다
  5. OS는 해당 좌표에서 활성화된 App을 확인한다
  6. Delegate를 이용하여 클릭 신호가 App의 이벤트 처리 함수 onClick()에 대응하였음을 App에게 알린다
  7. APp이 onClick()을 실행한다.

-> 이러한 순차적인 동작을 우리는 직접 처리하지 않는다. 하지만, 클릭 이벤트에 대응되는 메소드를 작성하면 동작한다.

-> 이것은 클릭을 인식하고 App에게 전달해주는 위임(Delegate) 객체가 있기 때문

 

in iOS

iOS에서도 같은 개념이다. 기능을 처리할 객체를 Delegate로 설정하고, 특정 이벤트가 발생할 때 이를 Delegate에 의해 위임된 본래의 객체로 전달해주는 역할이다.

 

예)

메세지를 전달하기 위해 사용되는 Delegate 프로토콜

protocol MessageDelegate: class{
	func printMessage(msg: String)
}

- class를 선언한 이유 : class에서만 사용이 가능하다

 

대리자(프로토콜 채택)

class MainViewController: UIViewController, MessageDelegate{
	...
    //채택한 프로토콜 준수하기 위한 메소드 구현
    func printMessage(msg: String){
    	print(msg)
    }
    
}

일을 넘긴 자(위임자)의 어떠한 부분을 대신 처리해주는 객체인 대리자이다

선언했던 Delegate 프로토콜을 채택하였으며, 채택한 프로토콜을 준수하기 위해 메소드를 구현했다

위임자의 어떠한 이벤트 발생 시, 해당 메소드가 호출된다고 생각하면 된다.

 

만약 SecondViewController가 있고 해당 객체에서 대리자(MainViewController)에게 일을 위임하고 싶다면, 서로의 '존재' 정도는 알아야하지 않겠는가

-> 즉, 위임자가 자신의 대리자가 누구인지는 알아야 일을 부탁하던 요청하던 짬때리던 할 것 아닌가?

-> 따라서 위임자의 대리자가 MainViewController라는 것을 설정해야 한다.

 

위임자의 대리자 설정

//SecondVC에 대리자를 할당하기 위한 변수 선언
//참조 사이클을 회피하기 위해 약한 참조로 선언
class SecondViewController: UIViewController{
	...
    weak var delegate: MessageDelegate?
    ...
    
    @IBAction func tappedPrintButton(_ sender: Any){
    	delegate?.printMessage("여기다 적고 싶은 말")
    }
    
}

//MainViewController에 추가

class MainViewController: ....{
	...
    
    @IBAction func tappedNextButton(_ sender: Any){
		let vc = SecondViewController()
        //SecondVC의 대리자가 self(자신)이라고 설정
        vc.delegate = self
        self.present(vc, animated: true, completion: nil)
    }
    
    ...
    
}

이렇게 작성하면 정상적으로 동작하는 것을 볼 수 있다.

 

정리

  1. 위임자가 대리자에게 어떤 일을 맡길 지 선언한다(위의 '메세지를 전달하기 위해 사용되는 Delegate 프로토콜')
  2. 위임자는 대리자에게 자신의 일들 중 일부를 맡긴다
    1. 따라서 위임자는 대리자가 누구인지 알아야한다 (위의 '위임자의 대리자 설정')
  3. 대리자는 위임자가 나에게 일을 시켰을 때, 작업할 동작을 구현한다. (위의 '대리자(프로토콜 채택) ')

같은 동작을 하는 클로저 콜백 방식

class MainViewController: UIViewController{
	...
    let secondVC = SecondViewController()
    
	func setClosureCallback(){
        secondVC.printMessage = { str in
        	print(str)
        }
    }
    
    @IBAction func tappedNextButton(_ sender: Any){
    	present(secondVC, animated: true, completion: nil)
    }
}


class SecondViewController: UIViewController{
	var printMessage: (String) -> Void)?
    
    @IBAction func tappedPrintButton(_ sender: Any){
    	printMessage?("여기다 적고 싶은 말")
    }
    
}

Delegate 방식과 클로저 콜백 방식 두가지 예제는 모두 같은 동작을 한다.

그러면 뭘 써?

사실 델리게이트 패턴이라는 말을 많이 들어서 만능인 줄 아는 사람들이 많다. 물론 나도 그렇다

하지만 위처럼 델리게이트 패턴이 아닌 클로저를 전달하므로써 똑같은 동작을 구현할 수 있다.

물론 이전 포스팅에서 알아본 NotificationCenter를 사용해도 가능하다.

각 방법을 사용하기 좋을 때가 있다고는 하는데 너무 다양한 의견이 많아서 정리하기가 힘들다.

그래서 나도 나만의 생각을 얘기해보겠다.

 

Delegate

장점 : 재사용할 수 있는 코드를 작성 가능(프로토콜의 장점)

단점 : delegate 사용을 위해 구현해야 하는 코드가 많다, 다수의 객체들에게 이벤트를 호출하는 방식이 비효율적

 

Notification

장점 : 옵저버에 추가한 다수의 객체들에게 동시에 이벤트를 발송 가능, 코드가 짧음

단점 : 코드의 흐름을 이해하기 어려울 수 있음

 

클로저 콜백

장점 : 쉽게 코드의 흐름을 파악할 수 있음

단점 : 많은 클로저를 사용해야 하는 경우 코드가 방대해짐

 

Local Notification

이전에 프로젝트를 진행하며, 사용자가 설정한 시각에 Push 알림을 진행한 경험이 있다. 그 당시에도 역시 프로젝트 기간으로 인해 자세히 공부하지 못하였기에, 이번 기회에 좀 더 자세히 알아보자

 

로컬 알림

  • 앱 내부에서 만든 특정 메시지를 iOS 알림센터를 통해 전달하는 방법
  • 앱이 종료되거나 백그라운드 상태일 때, 메세지를 전달할 수 있는 대표적인 방법
  • 알림은 iOS 스케줄러에 의해 발송되는데, 미리 메시지와 발송 시각을 구성해놓아야한다.-> 앱이 메모리에 살아있지 않아도 발송이 가능하다!
  • 만약 앱이 메모리에 살아있을 때, 어떠한 메시지를 발송하고 싶다면, 로컬 알림 보다는 다른 메세지 창을 이용하는 것이 효율적이다
    • 작업이 AppDelegate를 통해 작업이 이뤄지기 때문이다.
  • 다른 용도는 스케줄링이다 (CS에서의 스케줄링이 아닌 User의 시간을 관리해주는 스케줄링)
    • 아침에 일어날 때,  다들 알람을 맞춰놓죠? 그런 것을 생각하면 됩니다!

변화

이전(iOS 10 이전)에는 UILocalNotification을 사용하였습니다. 하지만 최근에는 Apple이 사용자 알림에 관한 모든 것을 전담 처리할 UserNotification 프레임워크를 제공하므로써 이를 사용합니다.

 

UserNotification 프레임워크

알림을 위한 여러가지 객체를 갖고 있으며, 내부에서는 UN이라는 접두어를 사용하여 객체 이름을 정의합니다.

 

import UserNotifications

 

객체

  1. UNMutableNotificationContent
    1. 알림에 필요한 메세지와 같은 속성을 담는 콘텐츠 역할
    2. 로컬 알림 타이틀, 서브 타이틀, 알림 메시지 설정 가능
    3. 앱 아이콘에 표시될 뱃지나 사운드 설정도 해당 객체를 통해 설정한다
  2. UNTimeIntervalNotificationTrigger
    1. 알림 발송 조건을 관리
    2. 발생 시각과 반복 여부를 설정 가능
    3. "몇 초 후" 단위로 설정한다. 하루 중 특정 시각에 맞추고 싶다면 UNCalendarNotificationTrigger를 사용한다
  3. UNNotificationRequest
    1. 위의 2가지를 설정한 후, 그것들을 모아서 전달하기 위한 알림 요청 객체를
  4. UNUserNotificationCenter
    1. 실제 발송을 담당
    2. 싱글턴 방식으로 동작한다 -> 객체 생성이 아닌 current()를 통해 정보에 접근 가능

 

Example

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        if #available(iOS 10.0, *) {
            let notiCenter = UNUserNotificationCenter.current()
            notiCenter.requestAuthorization(options: [.alert, .badge, .sound], completionHandler: { didAllow, e in
            	if didAllow{
                	//사용자가 Allow
                } else{
                	//사용자가 Not Allow
                }
            })
        }

        return true
    }

 

사용자에게  승인을 받는 과정을 거쳐야 한다. 당연히 사용자가 허용하지 않으면 알림을 발송하더라도 메시지를 받을 수 없다.

 

또한, 승인을 굳이 App이 켜지는 동안 받을 필요는 없다. 하지만 대부분의 앱이 그런식으로 받고 있어서 여기다가 넣었다...

 

func sceneWillResignActive(_ scene: UIScene) {//알림 동의 여부를 확인
        print("WillResign")
        if #available(iOS 10.0, *){
            UNUserNotificationCenter.current().getNotificationSettings { settings in
                if settings.authorizationStatus == UNAuthorizationStatus.authorized{
                    let content = UNMutableNotificationContent()
                    content.badge = 1
                    content.title = "로컬 푸시"
                    content.subtitle = "서브 타이틀"
                    content.body = "바디바디바디받비ㅏ디바딥다비답디ㅏㅂ딥다비다비답다ㅣ"
                    content.sound = .default
                    content.userInfo = ["name": "tree"]
                    
                    //5초 후, 반복하지 않음
                    let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
                    
                    let request = UNNotificationRequest(identifier: "firstPush", content: content, trigger: trigger)
                    
                    //발송을 위한 센터에 추가
                    UNUserNotificationCenter.current().add(request)
                }else{
                    //사용자가 알림을 허용하지 않음
                    print("알림 허용 않음")
                }
            }
        }else{
            //사용자의 폰이 iOS 9 이하임
            print("9버전 이하")
        }
    }

아까 준비 코드는 AppDelegate에 넣어놓고 왜 이 코드는 SceneDelegate에 넣어?

-> 음... appDelegate에도 같은 수명주기 메소드가 존재한다. 넣고 실행을 해보면 왜 넣는지에 대해서는 이해할 것이다.

 

실행을 한 후에, Application을 Resign 즉 Background상태로 보내게 되면 메시지가 등록이 되고 5초 후에 센터가 스스로 메시지를 발송하여 정상적으로 동작한다.

 


SceneDelegate vs AppDelegate

WWDC 2019

그림과 같이 iOS 13버전 이전과 이후로 SceneDelegate가 다르다.

위의 그림에서는 AppDelegate가 Process 수명주기와 UI 수명주기 모두를 관리하였다.

하지만, 아래의 그림을 보면 AppDelegate에서는 Process 수명주기와 Session 수명주기를 관리하고,

SceneDelegate에서 UI 수명주기를 관리한다.

 

그렇다면 왜 SceneDelegate가 나왔는가? 그리고 역할을 구분하였는가?

 

Scene

UIKit는 UIWindowScene 객체를 사용하는 앱 UI의 각 인스턴스를 관리합니다. 
Scene에는 UI의 하나의 인스턴스를 나타내는 windows와 view controllers가 들어있습니다.
 또한 각 scene에 해당하는 UIWindowSceneDelegate 객체를 가지고 있고,
이 객체는 UIKit와 앱 간의 상호 작용을 조정하는 데 사용합니다.
Scene들은 같은 메모리와 앱 프로세스 공간을 공유하면서 서로 동시에 실행됩니다.
결과적으로 하나의 앱은 여러 scene과 scene delegate 객체를 동시에 활성화할 수 있습니다.
(Scenes - Apple Developer Document 참고)

 

해당의 이유로 UI 수명주기를 SceneDelegate에서 관리하므로써 App의 수명주기에 해당하는 메소드들은 SceneDelegate에서 사용할 수 있다.

 

 

NotificationCenter

애플 공식 문서에는 이렇게 적혀있다

A notification dispatch mechanism that enables the broadcast of information to registered observers.

등록된 Observer들에게 정보를 BroadCast할 수 있는 알림 발송 메커니즘

 

이전에 사용했을 때는 주로 키보드가 올라올 때, 내려갈 때 TextField가 가려지는 문제를 해결하기 위해서 사용한 경험이 있다.

오늘은 좀 더 자세히 알아보자


출처 : https://baked-corn.tistory.com/42

어떤 객체가 Post를 하면 Observe하고 있던 Observer들에게 BroadCast를 한다.

(....? Delegate랑 똑같은 거 아냐?)

맞다. 어떤 객체가 다른 객체와 소통/송수신 한다는 메커니즘은 같다

 

Delegate 패턴의 사용 예

protocol SomeDelegate{
	func sayName(name: String)
}

class FirstView: UIView{
	// 순환 참조 사이클을 막기 위해 강한 참조가 아닌 약한 참조로 선언한다
    // 이벤트를 발생할(책임을 전가하는) 타입에서는 Delegate를 프로퍼티로 선언한다
	weak var delegate: SomeDelegate?
    
    func tappedButton(){
    	delegate?.sayName(name: "tree")
    }
}

// 동작을 수행할 타입에는 Delegate를 채택한다
class SomeController: SomeDelegate{
	var view: SomeView = SomeView()
    
    init(){
    	view?.delegate = self
    }
    
    // SomeDelegate 채택 후, 준수
    func sayName(name: String){
    	print(name) // tree
    }
    
} 

크게 5가지만 생각하면 된다

  1. Delegate 패턴을 위한 Protocol 선언
  2. 책임을 전가할 타입(위임자)에서 delegate를 프로퍼티로 선언 -> 이때, 순환 참조 사이클을 막기 위해 약한 참조 사용
  3. 동작을 수행할 타입(대리자)에서 delegate를 채택
  4. 위임자에 있는 delegate를 대리자의 객체로 초기화
  5. 대리자에서 프로토콜을 준수

장점 : 코드를 이해하며 따라가기 쉬움

단점 : 많은 객체들에게 이벤트를 알려주는 것이 어렵고 비효율적

 

 

Notification

- NotificationCenter 라는 싱글톤 객체를 통해 이벤트들의 발생 여부를 옵저버에게 알림.

- Notification.Name 이라는 Key 값을 통해 Post/Observe 가능

 

class PostVC: UIViewController{
	@IBOutlet var button: UIButton!
    
    @IBAction func sendNotification(_ sender: UIButton){
    	NotificationCenter.default.post(name: Notification.Name("buyEmoticon"), object: nil)
    }
}


class ViewController: UIViewController{
	override func viewDidLoad(){
    	super.viewDidLoad()
        
        NotificationCenter.default.addObserver(self, selector: #selector(sayHello(noti:)), name: Notification.Name("buyEmoticon"), object: nil)
        
    }
    
    deinit{
    	NotificationCenter.default.removeObserver(self)
    }
    
    @objc func sayHello(noti: Notification){
    	print("Hello") // Hello
    }
}

 

장점

  • 많은 줄의 코드가 필요없이 이벤트/동작을 구현할 수 있다
  • 다수의 객체들에게 동시에 이벤트의 발생을 알려줄 수 있음

단점

  • 추적이 쉽지 않음
  • post 이후 정보를 받을 수 없음

 

결론

  1. 가능하다면 Delegate로 구현하되, 하나의 이벤트로 다수의 객체에서 어떠한 동작을 해야하는 경우에만 NotificationCenter를 사용
  2. NotificationCenter를 사용해야 하는 이벤트들의 경우 NotificationCenter 사용

 

 

+ Recent posts