[ swift ] 포켓몬App 과제해설을 보며 복습해보기

sonny·2024년 12월 12일

TIL

목록 보기
68/138

왜 둘 다 setupView()를 호출하지?

해설을 보다가 초반부에 왜 두가지 초기화 방식을 사용하는지 궁금했다.

   
override init(frame: CGRect) {
    super.init(frame: frame) // 부모 클래스의 init(frame:) 호출
    setupView() // 커스텀 초기화 작업 수행
}

required init?(coder: NSCoder) {
    super.init(coder: coder) // 부모 클래스의 init?(coder:) 호출
    setupView() // 커스텀 초기화 작업 수행
}

이런식으로 초기화를 진행했는데,

우선 init(frame: CGRect) 는 뷰가 프레임 정보를 받아서 초기화 될 때 호출되는 초기화 메서드이고, 여기서의 setupView()는 커스텀 초기화 작업을 수행하기 위해 작성된 것이다.

required init?(coder: NSCoder)는 스토리보드나 XIB 파일로부터 뷰가 로드될 때 호출된다고 한다.

자세히 짚어보지 않아서 몰랐는데 여기서 coder는 스토리보드에서 객체를 복원할 때 사용되는 객체라고 한다. super.init(coder: coder)도 마찬가지로 부모 클래스의 초기화를 호출한다.

둘 다 setupView()를 호출하는 이유

init(frame:)과 init?(coder)은 각각의 초기화 방법에 맞게 호출되지만, 뷰가 완전히 동일한 커스텀 초기화 과정을 거쳐야할 때 공통 작업을 바로 setupView()에서 처리한다고 한다.

예를 들면 배경색 설정이나 서브뷰 추가라던지, 레이아웃 구성 등등이 있다.

override에서 상속받은 메서드를 재정의하고

required에서 초기화 메서드는 서브클래스에서도 반드시 구현되어야함을 나타내는데 이 두가지 초기화 방식에서 모두 동일한 뷰의 설정 작업을 진행하기 위함이라고 보면된다.
.
.

쉽게 말해 두가지 초기화 방법이란 frame 기반과 스토리보드 기반을 말하는 건데 이 두가지를 처리하면서 공통작업으로 setupView()를 재사용하려고 작성이 됐다는 것이다.

그래서 나는 "그럼 SceneDelegate도 수정할 필요가 없는건가?" 라고 생각했지만 그건 아니었다.

init(frame:)과 init?(coder:)를 모두 구현한 뷰는 코드 기반과 스토리보드 기반에서 해당 뷰를 사용하는 데 문제가 없도록 만들어지는 것이 맞지만, 앱 전체의 초기 화면을 설정하는 작업(SceneDelegate 설정)은 별개의 문제라고 한다.

결론적으로 정리하자면,

  • init(frame:)와 init?(coder:)를 구현하면, 뷰 자체는 코드 기반과 스토리보드 기반 모두에서 동작할 수 있다.

  • 코드 기반 앱에서는 앱의 초기 화면(루트 뷰 컨트롤러)을 명시적으로 설정해야 하므로 SceneDelegate를 수정해야 한다.

  • 두 가지 초기화 메서드는 뷰를 유연하게 초기화하기 위한 것이며, 앱의 진입점을 설정하는 SceneDelegate의 작업을 대체하지 않는다.


loadView()

과제를 진행할 때 로드뷰 사용을 하지 않아서 해설영상을 보면서 알게됐다.

loadView는 UIKit에서 뷰 컨트롤러가 관리할 기본 뷰를 생성하고 초기화하는 메서드인데, 기본적으로 UIViewController는 이 메서드를 호출해 자신의 view 속성을 초기화한다고 한다.

import UIKit

class ListViewController: UIViewController {
    let listView = ListView()
    
    override func loadView() {
        view = listView
    }
}

아까 만들어놓은 listView를 UIview 타입으로 생성했기 때문에 클래스 안에 넣어주면 전체적으로 뷰를 그리는 코드가 ListView로 넘어간다.

왜 이렇게 한건지 의도를 알고 싶었는데

loadView를 오버라이드 하는 이유는 view를 커스텀뷰로 대체하려는 이유 때문이었다.

이렇게 하면 장점이 있다.

  • 코드 기반으로 커스텀 뷰 설정
    스토리보드나 XIB를 사용하지 않고, 완전히 코드 기반으로 UI를 설정할 수 있다.
    뷰 컨트롤러가 생성될 때, view에 ListView가 즉시 할당되기에 UI 생성 과정이 명확하다.

  • 뷰 컨트롤러와 뷰의 역할 분리
    ListView는 UI를 정의하고, ListViewController는 뷰의 동작과 데이터 처리를 담당한다.
    이건 MVC 패턴에서 뷰와 컨트롤러를 분리하는 중요한 설계 원칙이라 볼 수 있다.

  • 기본 viewDidLoad 활용
    loadView를 오버라이드하더라도 viewDidLoad 메서드는 여전히 호출된다.
    따라서 viewDidLoad에서는 listView의 설정과 관련 없는 동작(데이터 초기화, 델리게이트 설정 등)을 처리할 수 있게 된다.


extension 안에 계산 프로퍼티는 넣을 수 있는데 저장 프로퍼티는?

extension에는 계산 프로퍼티를 추가할 수 있다는 걸 알았다.

계산 프로퍼티는 실제 값을 저장하지 않고, 호출될 때 값을 계산해 반환하기 때문에 메모리 레이아웃에 영향을 주지 않아서 괜찮지만,

저장 프로퍼티의 경우는 허용되지 않는다.

물론 클래스, 구조체, 열거형에 새로운 기능을 추가할 수 있지만, 메모리 레이아웃에 영향을 미치는 저장 프로퍼티를 추가하는 것은 허용되지 않는다.

이유는 저장 프로퍼티를 추가하면 객체의 메모리 구조가 바뀌어 기존 인스턴스와의 호환성을 해칠 수 있기 때문이다.


addChild

강사님은 기존에 있던 ViewController를 건들지 않고 새로운 컨트롤러 파일과 뷰파일을 만들어 분리를 했다.

왜 그런가 했더니 나중에 UIViewController의 메서드인 addChild로 ViewController에 연결해 부모 뷰 컨트롤러에 자식 뷰 컨트롤러를 추가하려고 했던 것이었다.

세상에 이런 방법이......

import UIKit
import SnapKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        setup()
    }

    func setup() {
        let child = ListViewController()
        addChild(child)
        
        view.addSubview(child.view)
        
        child.view.snp.makeConstraints {
            $0.edges.equalToSuperview()
        }
        child.didMove(toParent: self)
    }
}

addChild(_:)의 역할

  • 부모 컨트롤러에 자식 컨트롤러를 추가한다.
  • 뷰 컨트롤러 계층 구조를 관리하는 데 사용된다.
  • 자식 뷰 컨트롤러의 뷰를 화면에 추가하려면 이 메서드와 함께 자식 뷰 컨트롤러의 view를 부모 뷰에 추가해야 한다.

그냥 부모뷰로 보여줘도 되는거 아냐?

굳이 왜 자식뷰를 거쳐서 보여주는 방식을 한 이유가 뭘까?

여기서 나는 "단순히 보여주는 것 이상" 으로의 이유가 있었던 걸 알게 됐다.

그리고 마지막에 child.didMove(toParent: self) 는 자식 뷰 컨트롤러가 부모 뷰 컨트롤러에 추가된 작업이 완료되었음을 알리는 메서드라고 한다.

addChild(_:)를 호출하면 부모-자식 관계가 설정되지만, 자식 뷰 컨트롤러는 아직 완전히 초기화되지 않은 상태이다.

그래서 이후 child.view를 부모 뷰 컨트롤러의 뷰 계층 구조에 추가하고 레이아웃을 설정한 뒤, didMove(toParent:)를 호출해 모든 작업이 완료되었음을 자식 뷰 컨트롤러에 알려줘야 한다.

이 과정을 통해 자식 뷰 컨트롤러가 자신의 라이프사이클도 올바르게 처리할 수 있다고 한다.

물론,

자식 뷰 컨트롤러를 사용하지 않아도 물론 화면을 구성할 수 있지만,

UI 모듈화, 라이프사이클 관리, 재사용성, 역할 분리와 같은 이유로 자식 뷰 컨트롤러를 활용하는 것이 훨씬 더 효율적이고 유지보수에 유리하다고 한다.

특히 복잡한 화면이나 여러 사람이 협업하는 프로젝트에서는 자식 뷰 컨트롤러를 사용하는 것이 좋은 설계 방식이라고 하니 꼭 기억해야겠다.


safeAreaLayoutGuide는 상단바, 네비게이션 바나 기타 시스템 UI에 가려지지 않도록 할 때 사용된다.

혼자 오토레이아웃을 잡아보다가

$0.top.equalTo(safeAreaLayoutGuide.snp.top).offset(16) 이 코드를 ,

$0.top.equalToSuperview().offset(16) 이렇게 작성하여 타이틀뷰가 제대로 나오지 않는 상황이 일어났다.

내가 볼 땐 둘다 똑같은 것 같아서 그냥 수퍼뷰에서 16만큼 떼면 되겠지 했지만 경기도 오산이었다.

두개 모두 SnapKit을 사용하여 뷰의 위치를 설정하는 코드이지만, 참조하는 기준이 다랐던 이유 때문이었다.

safeAreaLayoutGuide는 상단바, 네비게이션 바나 기타 시스템 UI에 가려지지 않도록 할 때 사용된다고 한다.

상단 여백을 Safe Area에 맞춰서 설정하고 싶은 경우에 유용하다.

반면 equalToSuperview의 경우 상단이 부모뷰에 맞춰졌찌만 부모뷰가 Safe Area를 고려하지 않기 때문에,

상단바나 시스템 UI 요소에 가려질 수 있다는 점에서 차이가 있다.

언제 어떤 코드를 사용해야 할까?

  • Safe Area를 고려해야 하는 경우 (상단바나 네비게이션 바 등 시스템 UI가 영향을 미칠 수 있는 경우)에는 safeAreaLayoutGuide를 사용하는 것이 좋다.

  • 만약 부모 뷰와의 관계에서만 위치를 설정하고 시스템 UI 요소가 영향을 미치지 않는다면 superview를 사용하는 것이 더 간단하고 직관적일 수 있다.


음...

오늘은 코드 기반과 스토리보드 기반 초기화를 동시에 지원하는 뷰를 구현하기 위해 init(frame:)과 init?(coder:)를 사용하는 방법을 알게 됐다.

Safe Area와 Superview를 기준으로 레이아웃을 설정하는 차이도 이해했는데, Safe Area는 시스템 UI를 피하기 위해 Superview는 부모 뷰 자체를 기준으로 레이아웃을 설정할 때 사용한다는 점이 중요했다.

마지막으로 addChild를 사용하여 자식 뷰 컨트롤러를 추가하고 didMove(toParent:)를 호출해 계층 관계를 완성하는 과정을 통해 뷰 컨트롤러 간 역할을 이해할 수 있던 시간이었다.

해설영상 진짜 재밌다.

profile
iOS 좋아. swift 좋아.

1개의 댓글

comment-user-thumbnail
2024년 12월 12일

착실하게 코딩하고 강의도 열심히 듣는 모범생이시네요
나중에 자식 뷰 다시 알ㄹ려주세여

답글 달기