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

sonny·2024년 12월 12일
2

TIL

목록 보기
69/133

왜 둘 다 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
    }
}

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

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

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

이렇게 하면 장점이 있다.

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

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

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


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

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

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

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

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

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


addChild

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

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

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

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

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

해설영상 진짜 재밌다.

profile
iOS 좋아. swift 좋아.

1개의 댓글

comment-user-thumbnail
2024년 12월 12일

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

답글 달기

관련 채용 정보