본문 바로가기
iOS/Swift

[iOS] 데이터 전달 방식 4가지 - property, delegate, closure, NotificationCenter

by 안녕주 2022. 2. 13.

Swift에서 View Controller간의 데이터 전달을 하는 방식에는 크게 2가지로 나뉩니다.

 

(1) 직접 전달 방식(동기 방식) : 데이터를 직접 넘겨주는 방법

  • present,push시 프로퍼티에 접근해 넘겨주는 방식
  • Segue prepare 메소드를 활용해서 데이터를 넘겨주는 방식
  • Protocol / Delegation을 활용해서 데이터를 넘겨받는 방식
  • Closure를 활용해서 데이터를 넘겨받는 방식
  • NotificationCenter를 활용해 데이터를 넘기는 방식

(2) 간접 전달 방식(비동기 방식) : 데이터를 다른곳에 저장해두고, 필요할 때 꺼내가는 방식

  • AppDelegate.swift 활용
  • UserDefaults 사용하기
  • CoreData or Realm 활용하기

 

여기서 가장 많이 쓰이는 직접 전달 방식은

  1. “프로퍼티 접근”해서 데이터 넘기기
  2. delegate 패턴 사용하기
  3. Closure 사용하기
  4. Notification 사용하기

4가지 중에 상황별로 골라쓰는 것이 중요합니다.

 


1. 프로퍼티 접근해서 데이터 넘기기

프로퍼티란?

클래스/구조체/열거형에 연관되어 있는 값(변수,상수)등을 이야기합니다.

 

 

[실습]

1. FirstVC에서 TextField에 값을 입력하고 그 값을 SecondVC에 있는 프로퍼티에 값을 전달한다.

import UIKit

class FirstVC : UIViewController {

  @IBOutlet weak var dataTextField: UITextField!
  
  override func viewDidLoad() {
    super.viewDidLoad()
  }

  @IBAction func touchUpToSendData(_ sender: Any) {
    guard let nextVC = self.storyboard?.instantiateViewController(withIdentifier: "SecondVC") else {return}
    nextVC.message = dataTextField.text
    nextVC.modalPresentationStyle = .fullScreen
    self.present(nextVC, animated: true, completion: nil)
  }
  
}

2. SecondVC에서는 FirstVC에서 전달받은 Data를 안전하게 꺼낸뒤 Label에 넣는다.

import UIKit

class SecondVC: UIViewController {
  
  @IBOutlet weak var dataLabel: UILabel!
  
  //이전화면에서 전달되는 값을 받기 위해 프로퍼티를 추가
  //IBOutlet 변수는 외부에서 값을 직접 대입할 수 없음, 외부에서 직접 참조할 수 없음
  //그래서 두번째 화면으로 값을 대입하기 위해 따로 값을 지정해 줘야함
  var message: String?
  
  
  override func viewDidLoad() {
    super.viewDidLoad()
    setMessageInLabel()
  }
  
  func setMessageInLabel(){
    //옵셔널 바인딩으로 안전하게 값을 꺼낸 뒤 dataLabel의 text로 넣습니다
    if let msg = message {
      dataLabel.text = msg
      dataLabel.sizeToFit()
    }
  }
  
  @IBAction func touchUpToGoBack(_ sender: Any) {
    self.dismiss(animated: true, completion: nil)
  }
  
}

 

여기서 과정을 요약하자면 다음과 같습니다.

  1. SecondVC라는 타입을 가진 VC 인스턴스 생성
  2. 그 인스턴스 위에다가 text 프로퍼티에 값을 부착
  3. present를 명령하면, 실제 메모리에 해당 VC를 넣고 화면을 띄우도록 명령!

1,2번 과정에서 인스턴스에 값을 붙이고, 3번에서 “실제"화면 전환이 일어나기 때문에, 3번을 실행하지 않으면 절대로 데이터 전달이 일어나지 않는다. 즉, 해당 VC를 push하거나 present까지 진행되어야 데이터가 전달되는 형태

 

 

위에서 설명한 프로퍼티에 접근해서 전달하는 방식은 대부분 화면전환이 이루어지는 과정에서 일어나는 데이터 전달이다.

그리고 부모 → 자식간 관계를 형성할때 (VC - View 관계) 사용하기도 한다.

 

직접 프로퍼티 전달을 사용하는 경우

1. 이전 화면 → 다음 화면으로 화면전환을 할 때 (present, push)

2. 부모 → 자식 관계일 때

 


2. Delagate로 데이터 전달

 

Protocol?

  • protocol은 특정한 작업이나 기능 부분적인 부분에 적합한 메소드, 프로퍼티 그리고 다른 요구사항의 청사진을 의미합니다. 그런 다음 프로토콜을 클래스, 구조체, 열거형에서 채택하여 해당 요구사항의 실제 구현을 제공할 수 있다.
  • 즉 프로토콜에서는 해야할일만 정의하고, 구현은 프로토콜을 채택한 객체에서 이루어진다.

 

Delegate Pattern?

  • Delegate Pattern은 쉽게 말해서 객체지향 프로그래밍에서 하나의 객체가 모든 일을 처리하는 것이 아니라 처리 해야 하는 일 중 일부를 다른 객체에 넘기는것
  • 주로 프레임워크 객체가 위임을 요청하며, 커스텀 컨트롤러 객체가 위임을 받아 특정 이벤트에 대한 기능을 구현

 

[실습]

1. SecondVC먼저 코드 작성 - Protocol 작성

import UIKit

//1. 데이터를 넘기는 함수 원형만 적고, 구현부는 FirstVC에서 진행
protocol SampleProtocol {
  func dataSend(data: String)
}

class SecondVC: UIViewController {

  @IBOutlet weak var dataTextField: UITextField!
  
  //2. SampleProtocol형의 delegate 프로퍼티 생성
  var delegate : SampleProtocol?
  
  override func viewDidLoad() {
        super.viewDidLoad()
    }
    
  @IBAction func dataSendBtnClicked(_ sender: Any) {
    //3. 버튼을 눌렀을 때, 선언한 delegate의 dataSend에 textField의 텍스트를 담아주세요!
    if let text = dataTextField.text{
      delegate?.dataSend(data: text)
    }
    
    //4. delegate 처리가 끝난 뒤에, navigation pop처리
    self.navigationController?.popViewController(animated: true)
  }
  
}

2. FirstVC에서 protocol 채택

import UIKit

//1. SampleProtocol 채택
class FirstVC: UIViewController, SampleProtocol {
  
  @IBOutlet weak var dataLabel: UILabel!
  
  override func viewDidLoad() {
    super.viewDidLoad()
  }

  //2. protocol의 구현부 작성
  func dataSend(data: String) {
    dataLabel.text = data
  }
  
  @IBAction func nextBtnClicked(_ sender: Any) {
    guard let nextVC = self.storyboard?.instantiateViewController(withIdentifier: "SecondVC") as? SecondVC else {return}
    
    //3. SecondVC에서 선언해둔 delegate가 self. 즉 대신해서 처리할 부분이 FirstVC라는 것을 아래의 구문을 통해 선언
    nextVC.delegate = self
    
    self.navigationController?.pushViewController(nextVC, animated: true)
  }
}

여기서 각 VC에서의 하는 역할을 요약하자면 다음과 같다.

FirstVC : Protocol 채택, Protocol의 구현부 구현, delegate 위임(채택)

SecondVC : Protocol 만들기, 타입이 Protocol인 property 생성, delegate 사용

 

 

 

Delegate로 데이터 전달을 사용하는 경우

1. 이전 화면 ← 다음화면 으로 (먼저 메모리에 올라와 있는 VC) ← (나중에 메모리에 올라오는 객체)

2. 이미 메모리에 올라와 있는 상태에서 데이터 전달할 때 사용되는 방식

 

예를 들어,

(1) A —present—> B, A 뷰컨을 밑에 깔고 B라는 뷰컨을 present한 상황에서 B에서 A로 데이터 전달을 하고 싶을 때 사용합니다!

(2) A라는 뷰컨위에 TableViewCell, CollectionViewCell같은거를 얹을 때, TVC,CVC를 쥐고 있는 상위 VC한테 데이터 전달을 할 때 사용합니다.

주의할점은 XX.delegate = self로 대리자 선언해주는거 잊으면 안된다!

 


3. Closure로 데이터 전달

Closure에 대해서는 자세히 설명하지 않을 예정입니다... 넘 어렵거든요.....

Closure는 서로 객체간의 데이터 전달 통로를 만든다는 개념으로 생각하고 데이터 전달을 진행하면 됩니다.

 

[실습]

1. SecondVC에 closure 만들기

import UIKit

class SecondVC: UIViewController {

  @IBOutlet weak var dataTextField: UITextField!
  
  //1. String을 넘겨주는 연결통로
  var completionHandler: ((String) -> ())?
  
  override func viewDidLoad() {
        super.viewDidLoad()
    }
    
  @IBAction func backBtnClicked(_ sender: Any) {
    //2. 연결통로에 데이터 넣고 전달하기
    completionHandler?(self.dataTextField.text ?? "")
    self.navigationController?.popViewController(animated: true)
  }
}

2. FirstVC에 데이터 처리 방식 정의

import UIKit

class FirstVC: UIViewController {

  @IBOutlet weak var dataLabel: UILabel!
  
  override func viewDidLoad() {
    super.viewDidLoad()
  }

  @IBAction func nextBtnClicked(_ sender: Any) {
    guard let nextVC = self.storyboard?.instantiateViewController(withIdentifier: "SecondVC") as? SecondVC else {return}
    
    //1. SecondVC로 화면전환을 하기 이전에 해당 연결통로에서 데이터가 들어오면 어떻게 처리할지 정의하기
		//“클로저” 형태로 연결된다는 것을 선언함과 동시에, 데이터 처리 어떻게 하겠다는걸 정의
    nextVC.completionHandler = {text in
      self.dataLabel.text = text
    }
    
    self.navigationController!.pushViewController(nextVC, animated: true)
  }
  
}

 

Closure로 데이터를 전달하는 경우

1. delegate를 사용하는 방법과 같이 이전 화면 ← 다음화면 으로 (먼저 메모리에 올라와 있는 VC) ← (나중에 메모리에 올라오는 객체)

2. 이미 메모리에 올라와 있는 상태에서 데이터 전달할 때 사용되는 방식

 

예를 들어

(1) A —present—> B, A 뷰컨을 밑에 깔고 B라는 뷰컨을 present한 상황에서 B에서 A로 데이터 전달을 하고 싶을 때 사용합니다!

(2)  A라는 뷰컨위에 TableViewCell, CollectionViewCell같은거를 얹을 때, TVC,CVC를 쥐고 있는 상위 VC한테 데이터 전달을 할 때 사용합니다.

 

 


4. NotificationCenter로 데이터 전달

Notification의 근복적인 원리는

  1. 지금 메모리에 올라와 있는 객체 모두에게 신호를 보내고
  2. 혹시 해당 객체에서 같은 신호이름을 가진 옵저버가 존재한다면
  3. 데이터를 수신하는 방식

위의 delegate, closure방식은 직접 데이터 처리를 누가할지 지정을 했다면, Notification 방식은 그냥 다 쏴버리고, 옵저버가 있다면 받아서 알아서 처리해 가는 방식!

 

 

Notification Center에서는 꼭알아야하는 2가지가 있는데

1. NotificationCenter로 post 하는 방법

NotificationCenter.default.post(name:NSNotification.Name("신호이름"),
                                  object:"전달하고싶은 데이터",
                                  userInfo:[KEY:VALUE])

// name : 전달하고자 하는 신호 이름을 적어주세요
// object : 전달하고자 하는 데이터를 적어주세요. (데이터형 상관없이, 구조체같은 것도 가능), 없으면 nil
// userInfo : 노티피케이션과 관련된 값 또는 객체의 저장소를 넣어주세요. 없으면 nil

2. NotificationCenter에 Observer를 등록하는 방법

NotificationCenter.default.addObserver(self,
                                   selector: #selector("실행할 함수"),
                                   name: Notification.Name("신호이름"),
                                   object : nil)

// self : 현재 자기자신 뷰컨에 옵저버를 달겠다. 
// selector : 해당 신호를 받으면 실행하는 함수 부분
// name : 신호를 구분하기 위한 이름
/// object : 해당 신호를 걸러주는 필터같은 역할, nil 사용시 해당 신호를 모두 받겠다는 의미

@objc func 실행할 함수(notification: NSNotification)  {
  // 실행할 부분
}
//observer에서 다음과 같이 실행할 함수를 등록해놓으면, 특정 신호 이름을 가진 신호가 온다면, 여기에 있는 함수 부분이 실행됩니다.

 

 

[실습]

1. SecondVC에서 특정 신호를 NotificationCenter를 통해 post

import UIKit

class SecondVC: UIViewController {

  @IBOutlet weak var notiTextField: UITextField!
  override func viewDidLoad() {
        super.viewDidLoad()

    }

  @IBAction func backBtnClicked(_ sender: Any) {
    NotificationCenter.default.post(name: NSNotification.Name("notiData"), object: notiTextField.text)
    
    self.navigationController?.popViewController(animated: true)
  }

}

2. 해당 신호를 Observer로 등록해 놓고 있는 함수가 실행

import UIKit

class FirstVC: UIViewController {

  @IBOutlet weak var dataLabel: UILabel!
  override func viewDidLoad() {
    super.viewDidLoad()
    addNotiObserver()
  }

  @IBAction func nextBtnClicked(_ sender: Any) {
    guard let nextVC = self.storyboard?.instantiateViewController(withIdentifier: "SecondVC") as? SecondVC else {return}
    self.navigationController!.pushViewController(nextVC, animated: true)

  }
  
  private func addNotiObserver(){
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(BtnClicked),
                                           name: NSNotification.Name("notiData"),
                                           object: nil)
  }
  
  @objc func BtnClicked(notification : NSNotification){
    if let text = notification.object as? String{
      dataLabel.text = text
    }
  }
  
}

 

NotificationCenter로 데이터를 전달하는 경우

전혀연관성이 없는(화면전환을 햇거나, VC-Cell 관계가 아니라 정말 서로 떨어져있는) 객체들끼리 데이터를 전달할 때 사용하는 방식

왜냐면 서로 연결되어 있는 경우에는 delegate,closure를 활용하면 되지만, 서로 연관없는 객체들끼리는 데이터를 바로 전달할 방법이 없기 때문!

 

물론, 연관되어 있는 경우에도 Notification을 사용해도 작동하지만, Notification을 사용해도 데이터 전달이 안되는 경우는

(먼저 메모리에 올라와있는 객체) ← (나중에 메모리에 올라오는 객체) 가 되어야하는데, 메모리에 없는 객체에다가 노티를 쏘면 반응이 없다는 점!을 주의해야한다.

 


5. 4가지 방식을 적용한 예시

아래와 같은 상황으로 구성되어 있다면

아래와 같이 데이터 전달을 사용할 수 있다.

여기서 초록색 표시를 한것은, Cell에서 이전이전 VC으로 데이터 전달을 하기 위해서는 NotificationCenter를 활용해야한다.

댓글