iOS

UIKit에서 ViewController간 데이터 전달

GREEN.1229 2024. 1. 22. 19:06

안녕하세요. 그린입니다 🍏

이번 포스팅에서는 UIKit에서 ViewController간에 데이터를 전달하는 방법에 대해 알아보겠습니다 🙋🏻

 

iOS 개발을 처음 접하면서 UIKit을 학습하시는분들이 종종 ViewController간에 데이터를 어떻게 전달하는지 막막해 하시는걸 리뷰나 멘토 활동을 하면서 봐서 한번 간단히 대표적인 방법들을 정리해보고자 했었는데 미루고 미루다가 이제서야 해보게 되었네요..!

 

데이터를 전달하는 방식들은 정말 다양하겠지만, 이번 포스팅에서는 가장 대표적인 직접 데이터를 전달하는 방법들을 몇가지 같이 알아볼까 합니다 😃

 

그럼 바로 슛들어갑니다~

 

갓강인 👍

 


직접 프로퍼티 접근

가장 먼저 쉽게 해볼 수 있는 방법은 VC의 프로퍼티에 직접적으로 접근해서 데이터를 전달하는 방법입니다.

간단하게 예시 코드에서는 상위 뷰인 ParentViewController가 존재하고 하위 뷰인 ChildViewController가 존재한다고 가정해볼께요.

 

먼저 하위인 ChildVC의 코드는 이렇습니다.

 

ChildViewController

import UIKit

class ChildViewController: UIViewController {
  @IBOutlet weak var titleLabel: UILabel!
  var titleText: String?
  
  override func viewDidLoad() {
    super.viewDidLoad()
    setTitleLabel()
  }
  
  private func setTitleLabel() {
    titleLabel.text = titleText
  }
}

 

titleText라는 프로퍼티가 존재하고 해당 VC가 나타날때 setTitleLabel 메서드를 통해 titleLabel 컴포넌트에 해당 titleText 프로퍼티의 값을 넣어줘서 원하는 텍스트를 띄워줍니다.

 

그럼 이제 상위뷰에서 이 ChildVC를 띄울때 직접 프로퍼티 값을 접근하여 데이터를 넘겨보시죠!

 

ParentViewController

import UIKit

class ParentViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
  }

  @IBAction func moveButtonTapped(_ sender: Any) {
    guard let childVC = self.storyboard?.instantiateViewController(withIdentifier: "ChildViewController") as? ChildViewController else { return }
    
    childVC.titleText = "GREEN"
    childVC.modalPresentationStyle = .automatic
    self.present(childVC, animated: true)
  }
}

 

ChildVC를 식별자로 가지는 친구를 먼저 인스턴스로 사용할 수 있도록 형 변환하여 가져옵니다.

그리고 해당 인스턴스에서 titleText를 직접 접근하여 값을 넣어주고 present, push 등 원하는 방식으로 화면을 노출시켜주면 됩니다!

 

아주 간단하죠?

 

가장 기본적으로 하위 VC 프로퍼티에 직접 접근하여 값을 주입해줌으로 데이터를 전달해줄 수 있습니다.

이 방식은 주로 상 하위 뷰 관계에서 하위 뷰를 직접 호출하여 노출시켜줄때 사용됩니다.

 

그럼 반대로 하위 -> 상위로 화면이 이동될때는 하위에서 어떻게 상위에 데이터를 전달해줘야 할까요?

 

두번째로 프로토콜과 딜리게이트를 이용하여 전달해줄 수 있습니다.


Delegate를 이용하여 데이터 전달

먼저 하위뷰인 ChildViewController의 구현부터 살펴볼까요?

 

ChildViewController

import UIKit

protocol DataProtocol {
  func sendData(_ data: String)
}

class ChildViewController: UIViewController {
  @IBOutlet weak var titleLabel: UILabel!
  var titleText: String?
  var dataDelegate: DataProtocol?
  
  override func viewDidLoad() {
    super.viewDidLoad()
    setTitleLabel()
  }
  
  override func viewDidDisappear(_ animated: Bool) {
    dataDelegate?.sendData(titleText ?? "TEST")
  }
  
  private func setTitleLabel() {
    titleLabel.text = titleText
  }
}

 

가장 먼저 DataProtocol이라는 프로토콜을 정의해줍니다.

해당 프로토콜에서는 데이터를 전달하는 역할을 하는 메서드인 sendData의 선언을 가지고 있죠.

 

그리고 하위 ChildVC에서 해당 프로토콜을 딜리게이트 프로퍼티로 가져갑니다.

 

마지막으로, 뷰가 사라지면 해당 딜리게이트의 sendData 메서드를 호출하여 현재 하위 뷰에 있는 titleText 프로퍼티 데이터 값을 넘겨주도록 구현해줍니다.

 

이제 하위 뷰에서 받은 데이터를 상위 뷰에서 어떻게 다루는지 볼까요?

 

ParentViewController

import UIKit

class ParentViewController: UIViewController, DataProtocol {
  @IBOutlet weak var titleLabel: UILabel!
  
  override func viewDidLoad() {
    super.viewDidLoad()
  }
  
  @IBAction func moveButtonTapped(_ sender: Any) {
    guard let childVC = self.storyboard?.instantiateViewController(withIdentifier: "ChildViewController") as? ChildViewController else { return }
    
    childVC.titleText = "GREEN"
    childVC.dataDelegate = self
    childVC.modalPresentationStyle = .automatic
    self.present(childVC, animated: true)
  }
  
  func sendData(_ data: String) {
    titleLabel.text = data
  }
}

 

해당 상위 ParentVC에선 DataProtocol을 채택해줍니다.

그리고 하위 뷰를 띄울때 dataDelegate를 자기 자신인 self로 선언해주죠.

그럼 하위 뷰에서 sendData가 호출되면 그 처리를 상위 뷰에서 위임받아 처리를 해주는것입니다.

결국 sendData의 구현체는 상위 뷰가 가진 titleLabel에 전달받은 data를 넣어주도록 구현되어 있죠.

 

이렇게 구성한다면 어렵지 않게 하위 -> 상위로 데이터를 전달할 수 있습니다 😃

 

이와 비슷하게 클로저를 이용하여 하위 -> 상위로 데이터를 전달할 수도 있습니다!

 


클로저를 이용하여 데이터 전달

마찬가지로 하위 뷰인 ChildVC 코드부터 볼까요?

 

ChildViewController

import UIKit

class ChildViewController: UIViewController {
  @IBOutlet weak var titleLabel: UILabel!
  var titleText: String?
  var dismissAction: ((String) -> ())?
  
  override func viewDidLoad() {
    super.viewDidLoad()
    setTitleLabel()
  }
  
  override func viewDidDisappear(_ animated: Bool) {
    dismissAction?(titleText ?? "TEST")
  }
  
  private func setTitleLabel() {
    titleLabel.text = titleText
  }
}

 

프로퍼티가 하나 더 추가되었어요!

클로저인 dismissAction입니다.

즉, 해당 코드 블럭의 구현을 담고 있죠.

마찬가지로, 해당 뷰가 사라지면 해당 클로저를 실행시켜줍니다.

현재 코드는 dismissAction을 통해 하위 뷰의 titleText 프로퍼티를 인자로 전달하여 어떠한 기능을 해주는 시그니쳐를 볼 수 있습니다.

String -> ()이 해당 클로저 시그니쳐이고 쉽게 생각해서 String을 인자로 전달해 dismissAction에 구현된 코드를 실행시켜주는것입니다.

 

이제 그럼 상위 뷰를 살펴볼까요?

 

ParentViewController

import UIKit

class ParentViewController: UIViewController {
  @IBOutlet weak var titleLabel: UILabel!
  
  override func viewDidLoad() {
    super.viewDidLoad()
  }
  
  @IBAction func moveButtonTapped(_ sender: Any) {
    guard let childVC = self.storyboard?.instantiateViewController(withIdentifier: "ChildViewController") as? ChildViewController else { return }
    
    childVC.titleText = "GREEN"
    childVC.dismissAction = { data in
      self.titleLabel.text = data
    }
    childVC.modalPresentationStyle = .automatic
    self.present(childVC, animated: true)
  }
}

 

상위 뷰에서 childVC 인스턴스에 직접 프로퍼티를 접근하는것처럼 추가된 dismissAction 클로저를 접근해 기능 구현을 해줍니다.

즉, data가 String 인자로 전달 받으면 클로저 블럭에서 해당 titleLabel의 text에 해당 전달받은 String을 넣어주는 기능을 해주는것이죠.

 

간단하죠..!?

즉, 하위 > 상위 상황에선 딜리게이트를 사용해도 되고 클로저를 사용해도 되겠죠?

 

마지막으로 현재와 일대일의 상황 말고도 여러 뷰에서 한번에 데이터를 전달받아 처리하는 방법도 있습니다.

 

바로 노티피케이션센터를 활용하는 방법입니다!

해당 방법은 여러곳에서 동일한 옵저버를 post하고 있으면 모두 적용된다는 점에서 차이는 있지만 유용하게 쓰일 수 있는 방법이기에 살펴볼께요!

 


NotificationCenter를 이용하여 데이터 전달

우선 노피티케이션 센터를 활용하려면 해당 옵저버를 등록하고 post하여 처리하는 흐름만 알면 됩니다!

 

먼저 동일하게 하위 -> 상위 뷰로 넘어갈때 하위의 프로퍼티 값을 상위 Label에 적용하도록 하는 구현을 토대로 수정해볼까요?

 

그럴려면 우선 상위 뷰에서 구독하고 있을 옵저버를 추가해봐야겠죠?

 

ParentViewController

import UIKit

class ParentViewController: UIViewController {
  @IBOutlet weak var titleLabel: UILabel!
  
  override func viewDidLoad() {
    super.viewDidLoad()
    registerObserver()
  }
  
  @IBAction func moveButtonTapped(_ sender: Any) {
    guard let childVC = self.storyboard?.instantiateViewController(withIdentifier: "ChildViewController") as? ChildViewController else { return }
    
    childVC.titleText = "GREEN"
    childVC.modalPresentationStyle = .automatic
    self.present(childVC, animated: true)
  }
  
  private func registerObserver() {
    NotificationCenter.default.addObserver(
      self,
      selector: #selector(setTitleLabel),
      name: NSNotification.Name("sendData"),
      object: nil
    )
  }
  
  @objc private func setTitleLabel(notification: NSNotification) {
    titleLabel.text = notification.object as? String
  }
}

 

registerObserver 메서드에서 옵저버를 등록해줍니다.

해당 옵저버의 이름은 sendData입니다.

즉, sendData 옵저버를 관찰하고 있다가 post되면 setTitleLabel 메서드를 동작시켜주는것이죠!

 

setTitleLabel 메서드에서는 titleLabel의 text를 옵저버 post를 통해 전달받은 object 값으로 주입시켜주어 해당 기능을 수행하게 합니다.

 

이제 상위에서 옵저버를 추가하여 관찰할 준비가 끝났다면 하위 뷰에서 실제로 해당 옵저버를 이용해 post를 쏴볼까요?

 

ChildViewController

import UIKit

class ChildViewController: UIViewController {
  @IBOutlet weak var titleLabel: UILabel!
  var titleText: String?
  
  override func viewDidLoad() {
    super.viewDidLoad()
    setTitleLabel()
  }
  
  override func viewDidDisappear(_ animated: Bool) {
    NotificationCenter.default.post(
      name: Notification.Name("sendData"),
      object: titleText
    )
  }
  
  private func setTitleLabel() {
    titleLabel.text = titleText
  }
}

 

간단합니다.

뷰가 사라지면 노티피케이션센터를 활용해 post해주면 되죠.

post하려는 옵저버는 아까 설정한 sendData입니다.

그리고 object 정보에 현재 하위 뷰에 존재하는 titleText 프로퍼티를 담아주면 되죠!

 

그럼 이제 하위 뷰가 dismiss되면 하위 뷰의 프로퍼티를 object 데이터로 담아 post를 쏘게 됩니다.

해당 옵저버를 관찰하고 있던 상위 뷰에서는 object 데이터를 가지고 적절한 처리를 해줍니다.

 

간단하지 않나요!?

 

주로, 노피티케이션센터는 직접적인 화면 간 연관이 없어도 데이터를 전달하고 그에 맞춰 기능을 수행할 수 있게 해줍니다.

다만, 해당 같은 옵저버를 관찰하고 있다면 post가 될 때 모두 심어놓은 로직을 동작시키니 잘 구성을 해야 합니다.

 


마무리

이렇게 UIKit에서 VC간 데이터 전달하는 여러 방식들에 대해 살펴봤습니다.

물론, 이 외에도 segue의 prepare 메서드를 활용한다던지, 실제 다른 VC 인스턴스를 가지고 메서드를 활용한다던지

더 나아가서 이런 데이터들을 관리하는 객체를 만들어 의존성 주입받아 사용한다던지 등 정말 데이터를 전달하는 방법은 아주 많습니다.

이번 포스팅에서는 iOS를 처음 접하는 분들에게 가장 기본적이고 직관적인 방법들을 소개해봤습니다 🚀

 

더 좋은 데이터 전달 방식이 있으면 같이 공유해봐도 좋겠습니다 😃