본문 바로가기
iOS

[iOS] 역값 전달 - 이전 화면에 값 전달하기

by seongminmon 2024. 7. 1.

안녕하세요. iOS 개발을 하다 보면 이전 화면에 값을 전달해야 할 때가 있습니다. 이전 화면에 값을 전달할 때는 다음 화면에 값을 전달할 때보다는 고려할 것이 더 많은 것 같습니다. 예제를 통해 역값 전달에 대한 몇 가지 방법을 알아보겠습니다.

 

 

첫 번째 화면은 이미지뷰와 다음 화면으로 이동하는 버튼을 가지고 있습니다. 버튼을 누르면 네비게이션 컨트롤러를 통해 push 방식으로 두 번째 화면으로 이동할 수 있습니다. 목적은 두 번째 화면에서 색을 선택하고 돌아왔을 때 이미지뷰의 배경색을 선택한 색상으로 만드는 것입니다.

 

 

두 번째 화면은 3개의 버튼을 가지고 있고, 버튼을 누르면 선택한 색을 label에 표시해 줍니다. 여기서 선택한 색을 첫 번째 화면에 전달해야 합니다.

 

 

첫 번째 화면의 코드입니다. imageColor라는 UIColor 타입의 프로퍼티를 가지고 있고, imageView의 backgroundColor를 imageColor로 설정해 주고 있습니다.

class FirstViewController: UIViewController {
    
    var imageView = UIImageView()
    var nextButton = UIButton()
    
    var imageColor = UIColor.white
    
    override func viewDidLoad() {
        super.viewDidLoad()
        configureView()
        imageView.backgroundColor = imageColor
    }
    
    // ...
}

 

 

두 번째 화면의 코드입니다. selectedColor라는 프로퍼티를 가지고 있고, 버튼을 누르면 그 버튼의 배경색을 저장하게 됩니다.

저희의 목적은 SecondViewController의 selectedColor의 값을 FirstViewController의 imageColor에 전달하는 것이 되겠네요.

class SecondViewController: UIViewController {

    let redButton = UIButton()
    let blueButton = UIButton()
    let yellowButton = UIButton()
    let resultLabel = UILabel()
    
    var selectedColor = UIColor.white
    
    override func viewDidLoad() {
        super.viewDidLoad()
        configureView()
    }
    
    @objc func colorButtonTapped(sender: UIButton) {
        switch sender {
        case redButton:
            resultLabel.text = "빨간색을 선택하였습니다."
        case blueButton:
            resultLabel.text = "파란색을 선택하였습니다."
        case yellowButton:
            resultLabel.text = "노란색을 선택하였습니다."
        default:
            break
        }
        selectedColor = sender.backgroundColor!
    }
    
    // ...
}

 

1. navigationController?.viewControllers.last!

첫 번째 방법은 SecondViewController의 viewWillDisappear 시점에 네비게이션 컨트롤러에 접근해서 직접 이전 화면을 찾아내 값을 넘겨주는 방법입니다. 네비게이션 컨트롤러가 가지고 있는 viewControllers에는 지금까지 화면 이동해 온 vc들이 차곡차곡 쌓여있고, 그 마지막 값에 바로 이전 화면이 담겨있게 됩니다. viewControllers는 [UIViewController] 타입이기 때문에 as를 통해 타입 캐스팅을 해주어야 합니다.

// SecondViewController
override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    
    let vc = navigationController?.viewControllers.last! as! FirstViewController
    vc.imageColor = selectedColor
}

 

 

'navigationController?.viewControllers에 자기 자신은 왜 안 담겨있는 거지?'라는 궁금증이 생겨서 print를 찍어보았습니다.

viewDidLoad()
[<Test.FirstViewController: 0x10060d170>, <Test.SecondViewController: 0x100612350>]

viewWillDisappear(_:)
[<Test.FirstViewController: 0x10060d170>]

 

viewDidLoad() 시점에서는 두 번째 화면도 담겨있고, viewWillDisappear() 시점에서는 자신은 사라지는 것을 알 수 있습니다.

navigationController?.viewControllers를 사용하려면 생명주기를 잘 생각하면서 사용해야 할 것 같습니다!

 

 

첫 번째 화면에서 값을 전달받을 때는 viewWillAppear() 시점에 받아온 데이터를 바탕으로 뷰를 업데이트해 주었습니다.

// FirstViewController
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    imageView.backgroundColor = imageColor
}

 

 

성공적으로 동작합니다.

 

 

다만, viewWillDisappear()에서 값을 전달하고 있기 때문에, 제스처를 통해 실제로 화면을 이동하지 않더라도 호출이 되어 불필요한 호출이 늘어날 수 있을 것 같았습니다.

// SecondViewController
override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)

    let vc = navigationController?.viewControllers.last! as! FirstViewController
    vc.imageColor = selectedColor
}

 

 

그래서 단 한 번만 호출되는 viewDidDisappear()에서 값을 전달하고 싶다는 생각이 들어서 코드를 옮겨보니...

오류가 납니다.

 

확인해 보니 viewDidDisappear() 시점에서는 네비게이션 컨트롤러가 nil이 되더라고요? 이미 화면이 사라진 이후이니 당연한 결과일 수 있겠네요. 그래서 viewDidDisappear()에서 값을 전달할 수 있는 다른 방법을 찾아보게 되었습니다.

 

2. 클로저

두 번째 화면에서 sendData라는 클로저를 프로퍼티로 만들고, 제가 원하는 viewDidDisappear() 시점에 데이터를 담아 함수를 실행시킵니다. swift에서 클로저는 1급 객체이기 때문에 변수나 상수에 담는 것이 가능합니다. 이 부분이 이해가 안 된다면 1급 객체에 대해 알아보세요!

// SecondViewController
class SecondViewController: UIViewController {
    var sendData: (UIColor) -> Void = { _ in }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        sendData(selectedColor)
    }
}

 

 

그리고 첫 번째 화면에서 화면 이동을 할 때 sendData를 정의해 줍니다.

// FirstViewController
@objc func nextButtonTapped() {
    let vc = SecondViewController()
    vc.sendData = { data in
        self.imageColor = data
    }
    navigationController?.pushViewController(vc, animated: true)
}

 

 

데이터가 바뀌지 않았을 때도 viewWillAppear()는 호출이 되기 때문에,

뷰를 업데이트하는 부분도 viewWillAppear()에서 imageColor의 didSet으로 옮겨주었습니다.

// FirstViewController
var imageColor = UIColor.white {
    didSet {
        imageView.backgroundColor = imageColor
    }
}

 

3. delegate

세 번째 방법은 protocol을 이용한 방법입니다. 클로저 방식과 유사합니다.

 

먼저 프로토콜을 생성해 줍니다.

protocol SecondViewControllerDelegate {
    func sendData(data: UIColor) -> Void
}

 

클로저 때와 마찬가지로 두 번째 화면에서 delegate를 프로퍼티로 갖고, viewDidDisappear()에서 delegate가 가진 sendData를 실행하여 데이터를 넘겨줍니다. 프로토콜을 변수에 담을 수 있는 이유는 프로토콜도 swift에서 1급 객체처럼 사용할 수 있기 때문입니다.

// SecondViewController
var delegate: SecondViewControllerDelegate?
    
override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    delegate?.sendData(data: selectedColor)
}

 

첫 번째 화면에서 delegate를 채택해 주고, 화면 이동을 합니다. 함수의 내용은 프로토콜을 채택한 곳에서 정의해 줍니다.

// FirstViewController
@objc func nextButtonTapped() {
    let vc = SecondViewController()
    vc.delegate = self
    navigationController?.pushViewController(vc, animated: true)
}
extension FirstViewController: SecondViewControllerDelegate {
    func sendData(data: UIColor) {
        imageColor = data
    }
}

 

오늘은 이전 화면에 값을 전달하는 방법에 대해 알아보았습니다. 이외에도 다양한 방법이 있을 것 같아요. 긴 글 읽어주셔서 감사합니다! :)