[iOS Programming Big Nerd Ranch] 2. 뷰와 뷰 계층


뷰와 뷰 계층에 대해 알아보자.


View는 버튼, 텍스트 필드, 슬라이더와 같이 사용자에게 시각적으로 보여지는 객체다.

  1. UIView의 인스턴스이거나 UIView의 자식 클래스다.
  2. 스스로 draw(화면에 출력)할 줄 안다.
  3. 이벤트, 터치를 다룰 수 있다.
  4. Root가 애플리케이션의 window 인 뷰들의 계층안에 존재한다.

View Hierarchy

위에서 “Root가 애플리케이션의 window인 뷰들의 계층”이라는 말을 썼는데 그렇다면 뷰 계층은 어떻게 되는가?

모든 앱은 애플리케이션에 있는 모든 뷰들의 컨테이너 역할을 하는 UIWindow의 단일 인스턴스를 가지고 있다. UIWindow는 UIView의 자식 클래스이기 때문에, window 자체도 뷰다. Window는 앱이 실행되었을 때 만들어진다. Window가 생성되면 다른 뷰들이 여기에 추가될 수 있다.

뷰가 윈도우에 추가되었을 때, 윈도우의 subview라고 표현한다. Window의 subview로 들어간 뷰들도 subview를 가질 수 있고, 그래서 윈도우와 뷰 객체들의 계층은 아래와 같이 표현할 수 있다.

image

뷰 객체가 생성되면, 화면에 그려진다. 이거는 두 단계로 나뉠 수 있다.

  1. 윈도우를 포함한 계층에 있는 각 view들이 스스로를 draw한다. layer에 스스로를 렌더링 한다.(bitmap 이미지처럼 생각하면 된다. layer는 CALayer의 인스턴스다.)
  2. 모든 뷰들의 layer는 화면에서 같이 구성된다.

즉 1. 스스로를 레이어에 렌더링하고, 2. 이 레이어들이 화면에 모여서 화면을 구성하는 것이다. 계산기를 예로 들면 아래와 같이 표현할 수 있다.

image

View와 Frame

뷰를 코드로 생성할 때, init(frame:) 이라는 지정 이니셜라이저를 사용한다. 이는 뷰의 프로퍼티인 frame이 될 CGRect를 argument로 받는다.

뷰의 frame은 뷰의 크기와 superview에 상대적인 위치를 명시한다. 뷰의 사이즈가 항상 frame에 의해 명시되기 때문에,뷰는 항상 사각형이다.

CGRectoriginsize라는 멤버를 가지고 있다.

  1. origin : CGPoint의 구조체 타입이며 두 개의 x, y라는 CGFloat 프로퍼티를 가지고 있다.
  2. size : CGSize의 구조체 타입이며 width, height라는 두개의 CGFLoat 프로퍼티를 갖고 있다.

image

앱이 실행되면, 초기 뷰 컨트롤러의 뷰는 root level window에 추가된다. VieController 클래스에 의해 뷰 컨트롤러는 표현된다.

ViewController.swift의 기본 코드 구조는 아래와 같다.

import UIKit

class ViewController: UIViewController {
}

UIKit는 프레임워크다. 프레임워크는 연관된 클래스와 리소스의 컬렉션이다. UIKit 프레임워크는 다른 iOS-전용 클래스와 비슷하게 사용자들이 볼 수 있는 UI dythemfdmf wjdmlgksek.

컨트롤러의 뷰가 메모리에 로드된 직후, viewDidLoad() 메서드가 호출된다. 이 메서드는 뷰 계층을 커스터마이징할 기회를 주기 떄문에, view를 추가하기에 좋은 곳이다.

여기서 뷰의 계층에 대해 좀 더 보자.

코드 두 개가 있다. 첫 번째 코드는 ViewController의 두 개의 뷰를 아래와 같이 추가했다.

override func viewDidLoad() {
    super.viewDidLoad()
    let firstFrame = CGRect(x: 160, y: 240, width: 100, height: 150)
    let firstView = UIView(frame: firstFrame)
    firstView.backgroundColor = UIColor.blue
    view.addSubview(firstView)

    let secondFrame = CGRect(x: 20, y: 30, width: 50, height: 50)
    let secondView = UIView(frame: secondFrame)
    secondView.backgroundColor = UIColor.green
    view.addSubview(secondView)
}

이 때 firstViewsecondView는 모두 UIView에 subview로 추가했다. 이때 계층은 아래와 같이 구성된다.

image

화면에는 아래와 같이 나온다.

image

이제 두 번째 코드는 아래와 같이 뷰들의 계층을 바꿨다.

override func viewDidLoad() {
    super.viewDidLoad()
    let firstFrame = CGRect(x: 160, y: 240, width: 100, height: 150)
    let firstView = UIView(frame: firstFrame)
    firstView.backgroundColor = UIColor.blue
    view.addSubview(firstView)

    let secondFrame = CGRect(x: 20, y: 30, width: 50, height: 50)
    let secondView = UIView(frame: secondFrame)
    secondView.backgroundColor = UIColor.green
    firstView.addSubview(secondView)
}

달라진 점은 firstView에 subView로 secondView를 추가했다. 그래서 뷰의 계층은 아래와 같이 된다.

image

화면에는 아래와 같이 나온다.

image

뷰의 frame은 superview에 상대적이기 때문에, secondView의 왼쪽 위가 기준이 되어서 position이 정해진 것이다.

Auto Layout System

앞에서 본 Auto Layout System을 다시 보도록 하겠다. 정확한 위치 좌표는 스크린 사이즈를 이미 알고 있다고 가정하기 때문에 레이아웃을 불완전하게 만든다.

Auto Layout을 사용하면, runtime에 frame이 정해지는 것이 가능해지도록 뷰의 레이아웃을 상대적으로 명시할 수 있기 때문에 frame의 정의는 앱이 구동되는 기기의 스크린 사이즈에 따라 정해질 수 있다.

Alginment rectangle과 layout attributes

Auto Layout 시스템은 alignment rectangle을 기반으로 하고 있다. 이 사각형은 여러 layout attribute에 의해 정해진다.

image

여기서 좀 헷갈릴 수 있는 것들만 찝어보겠다.

  • FirstBaseline / LastBaseline : bottom attribute랑 비슷할 때가 있지만 그렇지 않을 때도 있다. 예를 들어, UITextField는 alignment rectangle의 bottom보다는 텍스트의 밑을 baseline으로 잡는다. 여러 줄이 있는 텍스트 라벨과 텍스트 뷰에 대해서, 첫번째와 마지막 baseline은 글의 첫 줄과 끝 줄을 가리킨다. 다른 상황에서는 first와 last baseline은 같다.
  • leading / trailing : 이 값들은 언어에 따라 다른 attribute다. 만약 기기가 왼쪽에서 오른쪽으로 읽는 언어에 맞춰져 있다면, leading attribute는 left attribute와 같고 trailing attribute는 right attribute와 같다. 만약 반대라면 attribute도 반대가 된다.

기본적으로, 모든 뷰는 alignment rectangle을 가지고 있고, 모든 뷰 계층은 Auto Layout을 사용한다.

Alignment rectangle은 frame과 비슷하다. 사실 같을 때가 많다. Frame이 전체 뷰를 에워 싼다면, alignment rectangle은 정렬을 목적으로 사용하고 싶은 컨텐츠를 에워싼다.

image

뷰의 alignment rectangle을 직접적으로 정의할 수는 없다. 대신, constraints의 목록을 제공한다.

Constraints

Constraint는 뷰 계층에서 하나 이상의 뷰의 레이아웃 attribute를 정의하는데 사용될 수 있는 특정 관계를 정의한다. 모든 레이아웃 attribute에 대해 constraint를 정의할 필요는 없다. 어떤 값들은 constraint에 의해 직접적으로 얻어질 수 있다. 다른 값들은 관련된 레이아웃 attribute에 의해 계산된다. 예를들어 한 뷰의 constraint의 왼쪽 끝과 길이가 정해진다면, 오른쪽 끝은 이미 정해진게 된 것이다.(left edge + width = right edge)

Nearest neighbor

뷰가 특정 방향에 아무런 형제 뷰들이 없다면, 가장 가까운 이웃은 superview가 될 것이다.

image

Adding constraints in Interface Builder

  1. 맨 위 라벨의 canvas 에 있는 nearest neighbor부터의 위쪽 여백을 고정한다. image
  2. 현재 canvas 값에 표시된 대로의 값으로 라벨의 길이와 높이를 고정한다. image

여기까지 하면 Horizontal ambiguity를 확인할 수 있다.

image

vertical constraints 두 개(top과 height), horizontal constraint 한 개(width)를 정의 했는데, 하나의 horizontal constraint를 갖는게 label의 위치를 모호하게 만드는 것이다. 이 이슈는 label와 superview 사이에 center alignment constraint를 추가하면 없어진다.(당연함) 중앙 정렬은 앞에서도 했으니 넘어간다. 이를 정의해주면 아까 떴던 모호하다는 메세지가 없어지게 된다.

Intrinsic content size

위에서 맨 위의 라벨의 포지션은 유동 적이지만, 크기는 그렇지 않다. 왜냐하면 우리가 라벨에 width와 height constraint를 지정해줬기 때문이다. 만약 텍스트와 폰트가 바뀐다면, 라벨은 같은 위치를 유지한다. 하지만 프레임의 사이즈는 고정되어 있기 때문에, 프레임이 내용물을 감싸지 않을 것이다.

여기서 뷰의 intrinsic content size가 작용한다. Intrinsic content size를 뷰가 되길 “바라는” 사이즈라고 생각해도 된다. 라벨을 예로 들면, 이 사이즈 주어진 폰트로 렌더링된 텍스트의 사이즈가 된다. 이미지같은 경우는 이미지의 사이즈가 된다. 뷰의 intrinsic content size는 암묵적인 width와 height constraint로 작용한다. 만약 명시적으로 너비를 지정하지 않는다면, 뷰가 암묵적인 너비가 되는 것이다.

따라서 우리가 위에서 지정한 heigth와 width constraint를 없애서 유동적인 크기를 가질 수 있게 해야 한다.

Misplaced views

내가 만약 스토리보드에 있는 맨 위의 라벨의 위치를 이동하면 아래와 같이 파란색 선들이 주황색 선으로 변한다. 이거는 misplaced view를 나타내는데, Auto Layout이 계산한 거와 Interface Builder안에 있는 뷰의 프레임이 다르다는 것이다.

image

이럴 때 다시 설정한 Auto Layout 대로 스토리 보드를 나타내고 싶으면 아래 메뉴에서 첫 번째 아이콘을 클린다.

image