PinLayout, FlexLayout 사용하여 뷰 그리기(1)
머리말
MVVM 기반의 프로젝트를 구성하며 뷰를 작성하기 위해 오토 레이아웃을 활용했었다. SwiftUI 방식과 다르게
명령형으로 뷰를 그리는게 익숙치가 않다.. 오토 레이아웃을 공부하고, 헤딩해가며 뷰를 그리면서, 좀 더 생산성
높게 뷰를 그릴 수 없을까 고민하게 되었고, Snapkit 과 PinLayout 과 같은 레이아웃 관련 패키지를 사용하게
되었다. 결과적으로 PinLayout 과 FlexLayout 을 활용하여 뷰를 그리기로 했다.
오토 레이아웃 방식의 뷰 그리기
어떤 패키지를 사용하던 기본적인 뷰의 베이스는 오토 레이아웃이다. 그러므로, 기본적인 오토 레이아웃에 대한 이해가 없다면 해당 패키지들을 사용하기 어렵다. 기본적인 오토 레이아웃의 동작은 어렵지 않지만, 조금만 복잡한 레이아웃을 작성하고자해도, 상당히 길어진 코드를 확인할 수 있게 된다.
오토 레이아웃을 활용해 코드를 작성해보니 뷰 그리는 방식은 어느정도 틀이 정해져 있는 것 같다. 다음은 오토 레이아웃으로 뷰를 그리는 코드 예시다.
class MyViewController: UIViewController {
private let myView = UIView()
override viewDidLoad() {
super.viewDidLoad()
myView.translatesAutoresizingMaskIntoConstraints = false
myView.backgroundColor = .red
view.addSubview(myView)
NSLayoutConstraint.activate([
myView.widthAnchor.constraint(equalToConstant: 200),
myView.heightAnchor.constraint(equalToConstant: 200),
myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
}
위 코드를 실행하면 화면 상에 작고 귀여운 가로 세로 200 크기의 빨간색 네모가 그려진다. 뷰를 그리는 순서를 살펴보며, 코드의 역할을 살펴보자.
기본적인 뷰 그리는 순서
class MyViewController: UIViewController {
// 1. 생성할 뷰의 인스턴스를 생성한다.
private let myView = UIView()
// 2. 뷰 컨트롤러가 생성된 이후의 라이프 사이클 시점에 뷰를 그린다.
override viewDidLoad() {
super.viewDidLoad()
// 3. 오토 레이아웃의 제약조건(constraint) 충돌 방지를 위해 해당 뷰의 오토리사이징을 해제한다.
myView.translatesAutoresizingMaskIntoConstraints = false
// 4. 뷰를 스타일링 한다.
myView.backgroundColor = .red
// 5. 뷰를 루트 뷰 하위에 추가한다.
view.addSubview(myView)
// 6. 오토레이아웃을 사용해 뷰의 레이아웃을 조정한다.
NSLayoutConstraint.activate([
myView.widthAnchor.constraint(equalToConstant: 200),
myView.heightAnchor.constraint(equalToConstant: 200),
myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
}
3번과 4번 동작의 경우, 팩터리 메서드 등을 활용해 뷰 생성 시 함께 처리하는 것이 일반적이다. 3번 동작인 오토 리사이징을 해제하는 코드는 코드 기반으로 오토 레이아웃을 충돌 없이 작성하려하면 반드시 필요하다.
또한 뷰를 최초로 그리는 시점은 뷰가 로드된 이후를 의미하는 viewDidLoad 메서드 안에서 실행하는 것이
일반적이다. 보통 해당 라이프 사이클에는 뷰 뿐만 아니라 기본적인 뷰 컨트롤러의 세팅도 함께 한다.
코드를 통해 살펴본 뷰를 그리는 순서는 다음 그림과 같다.
위 순서에 맞게 코드가 실행된다면 문제없이 원하는 뷰를 화면에 그릴 수 있다. 기본적인 뷰 설정은 뷰 생성
시점에 진행할 수 있고, 뷰 컨트롤러의 viewDidLoad 메서드 안에는 다양한 설정과 관련된 코드들이 함께
작성되므로 뷰와 관련된 코드는 분리하는 것이 일반적이다. 그런고로 위 코드를 수정해보자.
class MyViewController: UIViewController {
// 1. 계산 프로퍼티로 생성 시점에 뷰와 관련된 기본적인 설정 처리
private let myView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
return view
}()
override viewDidLoad() {
super.viewDidLoad()
// 3. viewDidLoad 시점에 실행
setupUI()
}
// 2. 뷰 레이아웃 관련 작업 함수로 분리
private func setupUI() {
view.addSubview(myView)
NSLayoutConstraint.activate([
myView.widthAnchor.constraint(equalToConstant: 200),
myView.heightAnchor.constraint(equalToConstant: 200),
myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
}
해당 뷰가 그려지는 시점은 여전히 viewDidLoad 그대로지만 뷰 생성을 제외한 모든 작업이 viewDidLoad
안에서 진행되던 이전 코드와 달리, 생성 시 작업과 레이아웃 작업을 분리하였다. 이렇게 하면, viewDidLoad
안에서 뷰 수정을 할 필요 없어 코드 가독성도 좋아지고, 수정 시에도 용이하다. 반복적인 뷰 생성을 처리하기
위해 다음과 같은 팩토리 메서드를 활용할 수도 있다.
struct ViewFactory {
static func makeView() -> UIView {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
return view
}
}
class MyViewController: UIViewController {
let myVie1 = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
return view
}()
// myView1와 동일한 뷰를 생성한다.
let myView2 = ViewFactory.makeView()
}
위와 같은 방식으로 UIKit 에서는 오토 레이아웃을 활용해 뷰를 그릴 수 있으며, 위 처럼 뷰와 관련된
viewDidLoad 외부로 코드를 분리하는 것이 일반적인 작성 패턴 중에 하나이다.
여러 개의 뷰 추가하기
뷰 컨트롤러에 여러 개의 뷰를 추가해보자. 위에 설명했던 방식으로, 새롭게 UILabel 추가하여 뷰를 그렸다.
뷰가 추가 되었지만, 뷰 관련 코드가 분리되어 있으므로 생성과 레이아웃 부분만 수정하면 되었다.
class MyViewController: UIViewController {
private let myView = ViewFactory.makeView()
// 1. 팩토리 메서드로 뷰 생성
private let myLabel = ViewFactory.makeLabel(text: "Hello, AutoLayout",
font: .preferredFont(forTextStyle: .title1))
override viewDidLoad() {
super.viewDidLoad()
setupUI() // 영향을 받지 않는다.
}
private func setupUI() {
view.addSubview(myView)
// 2. 뷰 추가
view.addSubview(myLabel)
NSLayoutConstraint.activate([
myView.widthAnchor.constraint(equalToConstant: 200),
myView.heightAnchor.constraint(equalToConstant: 200),
myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
// 3. 뷰 레이아웃 배치
myLabel.topAnchor.constraint(equalTo: myView.bottomAnchor, constant: 16.0),
myLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
}
struct ViewFactory {
static func makeView() -> UIView {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
return view
}
static func makeLabel(text:String, font: UIFont) -> UILabel {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = font
label.text = text
return label
}
}
위와 같이 코드를 작성하면 다음과 같이 배치된 뷰를 확인할 수 있다.
지금까지는 크게 이상할 건 없지만, 아래와 같이 수정된 코드로 추가적인 뷰를 그려보면 몇 가지 신경쓰이는 점들을 발견할 수 있다.
class MyViewController: UIViewController {
private let myView = ViewFactory.makeView(bgColor: .red)
// 새로운 뷰 생성
private let myView2 = ViewFactory.makeView(bgColor: .blue)
private let myLabel = ViewFactory.makeLabel(text: "Hello, AutoLayout",
font: .preferredFont(forTextStyle: .title1))
override viewDidLoad() {
super.viewDidLoad()
setupUI() // 영향을 받지 않는다.
}
private func setupUI() {
view.addSubview(myView)
// 뷰 안의 뷰 추가
myView.addSubview(myView2)
view.addSubview(myLabel)
NSLayoutConstraint.activate([
myView.widthAnchor.constraint(equalToConstant: 200),
myView.heightAnchor.constraint(equalToConstant: 200),
myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
// 새로운 뷰 레이아웃 추가
myView2.widthAnchor.constraint(equalToConstant: 100),
myView2.heightAnchor.constraint(equalToConstant: 100),
myView2.topAnchor.constraint(equalTo: myLabel.bottomAnchor),
myView2.trailingAnchor.constraint(equalTo: myView.trailingAnchor, constant: 50),
myLabel.topAnchor.constraint(equalTo: myView.bottomAnchor, constant: 16.0),
myLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
}
struct ViewFactory {
static func makeView(bgColor: UIColor = .red) -> UIView {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = bgColor
return view
}
static func makeLabel(text:String, font: UIFont) -> UILabel {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = font
label.text = text
return label
}
}
기존의 뷰에 새로운 파란색 사각형 뷰를 추가하였다. 추가된 뷰를 살펴보면 다음과 같이 배치된 모습을 확인할 수 있다.
위처럼 파란색 뷰가 배치 된 것이 신경 쓰이지 않는가? 분명 파란색 뷰는 빨간색 뷰 안에 넣었는데, 상위 뷰인 라벨 뷰 밑에 배치되어 있다. 왜 이렇게 되었을까? 위처럼 배치된 이유는 제약조건 설정을 그렇게 했기 때문이다.
class MyViewController: UIViewController {
...
myView2.topAnchor.constraint(equalTo: myLabel.bottomAnchor),
myView2.trailingAnchor.constraint(equalTo: myView.trailingAnchor, constant: 50),
...
}
위처럼 myView2 제약조건을 topAnchor 는 myLabel 의 bottomAnchor 과 trailingAnchor 는 myView 의
trailingAnchor 와 동일하게 설정 했기 때문에, 설정한 대로 해당 위치에 배치된 것이다. 즉, 절대위치를
기준으로 뷰가 배치 되었다. 만일 위와 같이 제약조건을 설정하지 않았다면, 해당 뷰는 기본적으로 부모 뷰의 (x:
0, y:0) 위치에 그려질 것이다.
위처럼 동작하는 레이아웃 배치는 css 의 position: absolute 를 연상시키며 원하는 위치에 뷰를 그리게
만들어 주기도 하지만, 한편으로 의도치 않은 뷰를 작성하게 만들기도 한다. 또한 뷰 안에서 뷰가 여러개인 경우
레이아웃 구조를 파악에도 어려움을 준다. (해당 레이아웃이 배치된 이유를 확인하기 위해 인접한 뷰의
레이아웃을 모두 확인해야 한다.)
컨텐츠 허깅과 우선순위(priority), 고유 컨텐츠 크기와 같이 레이아웃에 영향을 주는 요소들의 관계도 잘 확인해야 한다. 이러한 레이아웃의 복잡성을 해결하기 위해 스택 뷰를 사용하거나, 하위 뷰를 쪼개는 등의 방법 등을 사용할 수도 있겠지만, 뷰 레이아웃 작성에 어려움은 여전히 남아 있다.
쉽게 뷰를 그릴 수 없을까?
오토 레이아웃 통해 뷰를 그리면서 느꼈던 점은, SwiftUI 방식의 뷰 작성이 정말 직관적이라는 점.. 이다.
너가 그리워 오토 레이아웃 방식의 뷰 작성 방식이 더 편할 때도 분명 있으나, 동적인 레이아웃의
변화를 작성하기는 품이 많이 든다는 생각이 들었다. 또한, 뷰 작성도 다양한 방식으로 할 수 있어, 사람들마다
다른 스타일의 뷰 작성 방식을 확인할 수도 있었다. 그래서, 좀 더 효율적으로 일관적인 방법으로 뷰를 그릴 수
없을까? 하는 마음에 뷰와 관련된 패키지를 살펴보게 되었다.some View
맺음말
글을 작성하다보니, 생각보다 길이져서 분리하고자 한다. 글을 작성하는 시점에서는 이미 PinLayout 과
FlexLayout 을 선택해 뷰를 그리고 있다.(취향이 강하게 작용함) 그러나 해당 패키지들을 사용하더라도 오토
레이아웃과 같은 방식으로 뷰를 그리기 경우도 충분히 많을 수 있기 때문에, 오토 레이아웃에 대한 이해는 반드시
필요하다. 가장 기본이기도 하고. 해당 패키지들을 쓰니 솔직히 많이 편하긴 하다. 이런걸 쓰고 있으면,
SwiftUI 쓸 필요 있나? 하는 생각이 들 만도? 다음 글에는 두 패키지를 소개하고 패키지를 사용해 뷰를
그려보려 한다.
여담으로 UIKit 으로 뷰를 그리다보면, . 찍고 뭘 쓸 수 있는지 찾아보는 게 일이다.. 생각하는 것들 왠만한
것들 다 되는건 좋은데, 하나 하나 되는지 안되는지 설명서를 찾아보는 기분이다..