한 컨트롤러에서 다른 컨트롤러로 변수를 전달하고 싶었다.

첫 번째 시도

첫 번째 뷰에서 버튼을 클릭한 횟수를 내비게이션으로 전환되는 두 번째 뷰에 띄우고 싶었다.

먼저, 스토리보드에서 내비게이션 뷰 컨트롤러, 첫 번째 뷰, 두 번째 뷰의 각각 요소들을 설정해 줬다.

storyboard

첫 번째 화면 라벨 아래에 Count 버튼을 누른 만큼의 횟수가 저장되고, 그 값을 두 번째 화면 라벨에 띄우고자 했다.

우선 꼼꼼한 재은 씨의 Swift: 기본편을 참고해 코드를 작성했다.

// First View Controller

import UIKit

class FirstViewController: UIViewController {

    @IBOutlet var uiTitle: UILabel!
    var count: Int = 0
   
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
    
    @IBAction func countUp(_ sender: Any) {
        self.count += 1
        print("countUp function... \(count)")
    }
    
    @IBAction func moveNextByBarButton(_ sender: Any) {
        guard let SecondVC = self.storyboard?.instantiateViewController(withIdentifier: "SecondVC") else {
			return
		}
		SecondVC.countText = String(self.count)
        self.navigationController?.pushViewController(SecondVC, animated: true)
    }
// Second View Controller

import UIKit

class SecondViewController: UIViewController {
    
    @IBOutlet var uiSecondLabel: UILabel!

    var countText: String!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        uiSecondLabel.text = "count: " + countText
    }
}

에러 발생^^..

내 생각대로라면, moveNextByBarButton 함수에서 생성한 SecondVCSecondViewController니까 해당 객체 안에 있는 countText에 접근해 값을 추가할 수 있을거라 생각했다. 근데 이렇게 했더니 다음과 같은 오류가 떴다.

Value of type ‘UIViewController’ has no member ‘countText’

UIViewControllercountText라는 멤버가 없다는 건데… 내 의도는 SecondViewController를 선언하고, 그 안에 있는 countText에 값을 집어넣는 것이었다.

시도한 (잘못된) 방법

나는 방법이 잘못된 줄 알고 아예 SecondViewController()로 인스턴스를 생성했고, 이렇게 하니 @IBOutlet으로 만들어진 uiSecondLabel이 보이지 않았다.

내 생각엔 SecondVC 라는 스토리보드 ID를 가진 컨트롤러에 커스텀 클래스를 SecondViewController로 지정해줬기 때문에 문제가 없을 거라고 생각했지만, 아울렛 변수들은 뷰 컨트롤러를 선언한다고 해서 자동적으로 생기는 친구가 아니라는 걸 알았다.

다운캐스팅

어떤 키워드로 검색해야될지 모르겠어서 별의 별 검색을 다 하는 과정에서 스택 오버플로우나 애플 디벨로퍼 포럼에서 꾸준하게 봤던 키워드를 인지하기 시작했다.

as

상위에 있는 부모클래스의 타입을 자식클래스의 타입으로 타입캐스팅 하는 것을 다운캐스팅이라 하는데, Swift에서 이 역할을 하는 키워드가 as라고 한다.

as? 와 as!의 차이
전자는 성공시 옵셔널 타입을, 실패시 nil을 반환한다. 후자는 성공시 타입 그 자체를 반환하고, 실패시 런타임 에러를 발생시킨다.

상위 클래스가 UIViewController인 SecondViewController를 정의할 때, 해당 인스턴스가 SecondViewController임을 확실히 하여 그 안의 멤버에 접근하고자 했던 내 의도에 꼭 맞는 키워드였다.

코드 수정

그래서 문제가 됐던 부분을 다음과 같이 수정해 주었다.

// First View Controller

import UIKit

class FirstViewController: UIViewController {

    ...
    
    @IBAction func moveNextByBarButton(_ sender: Any) {
        let SecondVC = self.storyboard?.instantiateViewController(withIdentifier: "SecondVC")as! SecondViewController

        SecondVC.countText = String(self.count)
        self.navigationController?.pushViewController(SecondVC, animated: true)
    }
}

as!로 다운캐스팅을 해주었기 때문에 SecondVC는 UIViewController가 아닌 SecondViewController 타입으로 지정될 수 있고, 그 때문에 SecondViewController에 포함돼있는 countText 변수에 접근할 수 있게 되었다.

!는 옵셔널 값을 강제로 처리하는 것이므로, 해당 코드를 guard else로 바꾸어 주면 다음과 같이 할 수 있다.

// First View Controller

import UIKit

class FirstViewController: UIViewController {

    ...
    
    @IBAction func moveNextByBarButton(_ sender: Any) {
        guard let uvc = self.storyboard?.instantiateViewController(withIdentifier: "SecondVC")as? SecondViewController else {
            return
        }

        SecondVC.countText = String(self.count)
        self.navigationController?.pushViewController(SecondVC, animated: true)
    }
}

결론

  • 내가 지정한 ViewController를 직접 초기화해주는 것은 인터페이스 빌더 친구들을 제대로 불러들이지 못할 수 있다는 점,

  • Swift의 타입캐스팅, 그 중 as 키워드를 사용한 다운캐스팅으로 상위 클래스의 타입을 하위 클래스 타입으로 캐스팅할 수 있다는 점을 알았다.

그 외 TIL…

is

Swift의 타입캐스팅 키워드로, 인스턴스의 타입을 확인할 때 사용한다.

class Human {
    var name: String
    var age: Int

    init(name: String, birthYear: Int) {
        self.name = name
        self.age = 2021 - birthYear + 1
    }
}

class Student: Human {
    var major: String

    init(major: String, name: String, birthYear: Int) {
        self.major = major
        super.init(name: name, birthYear: birthYear)
    }
}

위와 같은 클래스가 있을 때, 다음과 같이 타입을 확인하는 데 사용된다.

let ekwon = Human(name: "Eunbin", birthYear: 1996)

print(ekwon is Human) // true
print(ekwon is Student) // false

strong & weak

아울렛 변수를 선언할 때 storage에 strong 또는 weak 을 선택할 수 있게 되어있다. 두 친구는 메모리 회수 정책에 차이를 보인다.

strong은 말 그대로 센 녀석이고, weak은 말 그대로 약한 녀석이라고 생각하면 될 것 같다. 전자는 다른 곳에서 참조되고 있을 경우 메모리에서 해제되지 않고, 후자는 그런 경우일지라도 메모리에서 제거가 가능하다.

불시에 메모리에서 해제될 수 있는 위험성이 있는 weak은 왜 쓸까?

반대로 생각했을 때, strong 타입끼리만 상호 참조될 경우, 단 하나도 해제되지 않고 영원히 남게 된다. 이는 메모리 누수라는 또 다른 위험성을 가지고 있기 때문에 어떠한 변수는 weak으로 선언한다고 한다.

이 과정에 대해선 ARC(Auto Referencing Counter)를 참조하면 된다.

댓글남기기