본 프로젝트는 해외 유튜버 "Lets Build that App" 님의 영상을 참고하여 주요 내용만 요약했습니다.

코드 깃허브 링크

StoryBoard와 이별하기

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        self.window = UIWindow(frame: UIScreen.main.bounds)
        self.window?.makeKeyAndVisible()
        self.window?.rootViewController = UINavigationController(rootViewController: HomeController())

        return true
}

StoryBoard의 품을 벗어나, 프로그래밍적으로 레이아웃을 구현하기 위해 가장 먼저 할 일은 AppDelegate를 설정해주는 것이다. 그 중 가장 중요한 작업은 AppDelegate 클래스의 내부 프로퍼티로 지정되어있는 window 객체에 값을 새로 할당해주는 것이다.

window 객체는 일반적인 모바일 앱에선 하나만 동작하며, root뷰컨트롤러를 가리키고 있는데, 보통 외부 이벤트를 전달받아서 뷰에 전달하는 역할을 하는 객체다. window는 따로 만들지 않아도 기본적으로 appDelegate 내에 선언되어있는데, storyboard가 실행될 때 내부적으로 window객체에 값이 지정되는 방식이다.

나는 스토리보드를 사용하지 않기 때문에, 이를 새로운 인스턴스로 덮어씌워준다. 방법은 간단하다. 위 코드와 같이 didFinishLaunchingWithOptions함수에서 window 객체를 새로 정의해고 rootViewController를 설정해주면 된다.

이 때 makeKeyandVisable은 해당 윈도우를 키윈도우(의미는 잘 모르겠지만, 메인으로 사용하는 윈도우라는 뜻인 것 같다.)로 지정하는 역할을 한다.

TableViewController 구현

원래 "Lets Build That App" 채널의 강의에선 CollectionsViewController를 사용해서 세로로 구현하지만, 나는 대신 TableViewController를 사용하기로 했다. TableViewcontroller를 사용할 때, 핵심적으로 사용하는 메서드는 다음과 같다.

참고로 아래의 함수들은 모두 UITableViewController 클래스에 정의되어있고, 일반적으로 ViewController에 이 클래스를 상속하게 한 뒤 아래 함수들을 Override한다.

필수

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {}

각 Section에 몇개의 행이 들어갈 것인지 반환한다.

func tableView(\_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{ }

적절한 `TableViewCell`을 반환한다. 반드시 위에서 정의한 숫자만큼 반환해줘야한다.

선택

func tableView(\_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {}

각 인덱스에 해당하는 행의 높이를 정의한다.

addConstraint를 통해 오토레이아웃 구현

스토리보드에선 몇번의 클릭만으로 추가할 수 있는 AutoLayout이, 코드로 구현하려고 하니, 처음엔 조금 복잡하다고 느껴졌다. 코드로 오토레이아웃을 구현할 때의 장점은 여러가지 있겠지만, 가장 큰 것은 협업에 용이하다는 것이다.

즉 처음 짤 땐, 까다롭지만 구현된 코드를 확인할 땐, 알아보기가 나름 편리하다. (들은 말인데 개인차가 있을 수 있다고 본다.)

두번째는 내가 직접 경험해보지는 못했지만, git을통한 협업에서 충돌위험이 적다고 들었다. 스토리보드의 오토레이아웃은 보통 내부적인 xml코드로 자동생성되는데, 이 때 다른사람의 코드와 merge시 충돌이 발생하면 알아보기 좀 까다롭다고 한다.

어쨌거나 오토레이아웃의 핵심함수는 다음과 같다.

"addConstraint" or "addConstraints"

말그대로 제약조건을 하나 추가하는 함수와 여러개를 추가하는 함수이다. 코드에서 제약조건은 NSLayoutConstraint라는 객체로 구현되어있다.

NSLayoutContraint() 생성자

addConstraint(NSLayoutConstraint(item: subTitleLabel, attribute: .left, relatedBy: .equal, toItem: titleLabel, attribute: .left, multiplier: 1, constant: 0))

생긴것 복잡하지만 자세히보면 스토리보드로 구현했을 때와 상당히 유사한 형태를 갖는다. 위 제약조건을 해석하면 "subTItleLabel의 left와 titleLabel의 left가 같은 값을 갖는다" 정도 되겠다. 만약 같지않고 특정 길이만큼 차이를 주고싶으면 constant 인자를 건들면 된다.

NSLayoutConstraint.constraints

addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-16-[v0]-8-[v1(44)]-16-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["v0" : thumbnailImageView, "v1" : profileImageView]))

적절한 인자를 받아서 [NSLayoutConstraint] 배열을 반환하는 함수. 특이한점은 withVisualFormat이라는 인자로 독특한 표현식의 문자열을 받는다는 것이다. 다행히 따로 찾아보지 않아도 나름 보다보면 이해가 가는 모양을 하고 있다.

이 때, 해당 표현식이 여러가지 제약조건을 반환할 수 있으므로 여러개의 Constraint 배열을 반환하며, 그렇기 때문ㅇ에 당연히 addContraints함수를 통해 추가하게 된다.

위의 표현식을 해석하자면, V(vertical)이므로 세로방향의 제약조건을 정의하고 있고, 컨테이너와 16만큼 떨어진곳에 v0이 위치하고, 그 아래에 8만큼의 거리에 44의 높이를 갖는 v1가, 그리고 16만큼 아래에 컨테이너의 바닥이 존재한다는 뜻이 된다.
v1v2가 무엇인지에 대해서는 마지막 인자인 views에 정의되어 있다. 대충 v0의 높이만 동적으로 표현된다고 해석하면 될 것 같다.

translatesAutoresizingMaskIntoConstraints

중요한 부분을 빼먹을 뻔 했다. 위와같이 코드로 제약조건을 설정해주려면, 가장 먼저 UIView를 상속하는 객체의 위 속성을 False로 설정해줘야만 한다. 여기서 다 설명하긴 길어질것같지만, 저 기다란 이름 안에 답이 들어있다.

앱은 AutoresizingMask에서 지정된 내용을 translate 하여 Constraints를 정의한다. 이 때의, 제약조건은 완전하게 객체의 위치를 고정해버리기 때문에 추가적인 Constraints의 추가를 막아버리게 된다. 그렇기 때문에 우리가 Constraint를 추가하려면 반드시 저 속성을 명시적으로 false처리 해줘야한다.

참고로 위 속성은, InterfaceBuilder로 만든 객체의 경우 기본값이 false지만, 프로그래밍적으로 만들면 기본적으로 true값이 들어간다.
자세한 내용은 Zedd님의 포스팅을 참조했다.

🚀구현결과

R1280x0.png