해설을 보다가 초반부에 왜 두가지 초기화 방식을 사용하는지 궁금했다.
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)
도 마찬가지로 부모 클래스의 초기화를 호출한다.
init(frame:)과 init?(coder)
은 각각의 초기화 방법에 맞게 호출되지만, 뷰가 완전히 동일한 커스텀 초기화 과정을 거쳐야할 때 공통 작업을 바로 setupView()에서 처리한다고 한다.
예를 들면 배경색 설정이나 서브뷰 추가라던지, 레이아웃 구성 등등이 있다.
override
에서 상속받은 메서드를 재정의하고
required
에서 초기화 메서드는 서브클래스에서도 반드시 구현되어야함을 나타내는데 이 두가지 초기화 방식에서 모두 동일한 뷰의 설정 작업을 진행하기 위함이라고 보면된다.
.
.
쉽게 말해 두가지 초기화 방법이란 frame
기반과 스토리보드
기반을 말하는 건데 이 두가지를 처리하면서 공통작업으로 setupView()
를 재사용하려고 작성이 됐다는 것이다.
그래서 나는 "그럼 SceneDelegate도 수정할 필요가 없는건가?" 라고 생각했지만 그건 아니었다.
init(frame:)과 init?(coder:)를 모두 구현한 뷰는 코드 기반과 스토리보드 기반에서 해당 뷰를 사용하는 데 문제가 없도록 만들어지는 것이 맞지만, 앱 전체의 초기 화면을 설정하는 작업(SceneDelegate 설정)은 별개의 문제라고 한다.
init(frame:)와 init?(coder:)를 구현하면, 뷰 자체는 코드 기반과 스토리보드 기반 모두에서 동작할 수 있다.
코드 기반 앱에서는 앱의 초기 화면(루트 뷰 컨트롤러)을 명시적으로 설정해야 하므로 SceneDelegate를 수정해야 한다.
두 가지 초기화 메서드는 뷰를 유연하게 초기화하기 위한 것이며, 앱의 진입점을 설정하는 SceneDelegate의 작업을 대체하지 않는다.
과제를 진행할 때 로드뷰 사용을 하지 않아서 해설영상을 보면서 알게됐다.
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
에는 계산 프로퍼티를 추가할 수 있다는 걸 알았다.
계산 프로퍼티는 실제 값을 저장하지 않고, 호출될 때 값을 계산해 반환하기 때문에 메모리 레이아웃에 영향을 주지 않아서 괜찮지만,
저장 프로퍼티의 경우는 허용되지 않는다.
물론 클래스, 구조체, 열거형에 새로운 기능을 추가할 수 있지만, 메모리 레이아웃에 영향을 미치는 저장 프로퍼티를 추가하는 것은 허용되지 않는다.
이유는 저장 프로퍼티를 추가하면 객체의 메모리 구조가 바뀌어 기존 인스턴스와의 호환성을 해칠 수 있기 때문이다.
강사님은 기존에 있던 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)
}
}
굳이 왜 자식뷰를 거쳐서 보여주는 방식을 한 이유가 뭘까?
여기서 나는 "단순히 보여주는 것 이상" 으로의 이유가 있었던 걸 알게 됐다.
그리고 마지막에 child.didMove(toParent: self)
는 자식 뷰 컨트롤러가 부모 뷰 컨트롤러에 추가된 작업이 완료되었음을 알리는 메서드라고 한다.
addChild(_:)
를 호출하면 부모-자식 관계가 설정되지만, 자식 뷰 컨트롤러는 아직 완전히 초기화되지 않은 상태이다.
그래서 이후 child.view
를 부모 뷰 컨트롤러의 뷰 계층 구조에 추가하고 레이아웃을 설정한 뒤, didMove(toParent:)
를 호출해 모든 작업이 완료되었음을 자식 뷰 컨트롤러에 알려줘야 한다.
이 과정을 통해 자식 뷰 컨트롤러가 자신의 라이프사이클도 올바르게 처리할 수 있다고 한다.
자식 뷰 컨트롤러를 사용하지 않아도 물론 화면을 구성할 수 있지만,
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:)
를 호출해 계층 관계를 완성하는 과정을 통해 뷰 컨트롤러 간 역할을 이해할 수 있던 시간이었다.
해설영상 진짜 재밌다.
착실하게 코딩하고 강의도 열심히 듣는 모범생이시네요
나중에 자식 뷰 다시 알ㄹ려주세여