본문 바로가기

iOS & Swift

XCode로 유튜브 앱 만들기 #1 레이아웃 작업

** 본 프로젝트는 해외 유튜버 "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
}

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

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

나는 스토리보드를 사용하지 않기 때문에, 이를 새로운 인스턴스로 덮어씌워준다. 방법은 간단하다. 위 코드와 같이 didFinishLaunchingWithOptions함수에서 window 객체를 새로 정의해고 rootViewController를 설정해주면 된다.
이 때 makeKeyandVisable은 해당 윈도우를 키윈도우(의미는 잘 모르겠지만, 메인으로 사용하는 윈도우라는 뜻인 것 같다.)로 지정하는 역할을 한다.

프로그래밍적으로 레이아웃 구현하기


결과화면

1. 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 {}

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

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

스토리보드에선 몇번의 클릭만으로 추가할 수 있는 오토레이아웃이, 코드로 구현하려고 하니, 처음엔 조금 복잡하다고 느껴졌다. 코드로 오토레이아웃을 구현할 때의 장점은 여러가지 있겠지만, 가장 큰 것은 협업에 용이하다는 것이다.
즉 처음 짤 땐, 까다롭지만 구현된 코드를 확인할 땐, 알아보기가 나름 편리하다. (들은 말인데 개인차가 있을 수 있다고 본다.) 두번째는 내가 직접 경험해보지는 못했지만, 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 배열을 반환하며, 그렇기 때문ㅇ에 당연히 addContraint's' 함수를 통해 추가하게 된다.
위의 표현식을 해석하자면, V(vertical)이므로 세로방향의 제약조건을 정의하고 있고, 컨테이너와 16만큼 떨어진곳에 v0이 위치하고, 그 아래에 8만큼의 거리에 44의 높이를 갖는 v1가, 그리고 16만큼 아래에 컨테이너의 바닥이 존재한다는 뜻이 된다.
v1과 v2가 무엇인지에 대해서는 마지막 인자인 views에 정의되어 있다. 대충 v0의 높이만 동적으로 표현된다고 해석하면 될 것 같다.

translatesAutoresizingMaskIntoConstraints

중요한 부분을 빼먹을 뻔 했다. 위와같이 코드로 제약조건을 설정해주려면, 가장 먼저 UIView를 상속하는 객체의 위 속성을 False로 설정해줘야만 한다. 여기서 다 설명하긴 길어질것같지만, 저 기다란 이름 안에 답이 들어있다.
앱은 AutoresizingMask에서 지정된 내용을 translate 하여 Constraints를 정의한다. 이 때의, 제약조건은 완전하게 객체의 위치를 고정해버리기 때문에 추가적인 Constraints의 추가를 막아버리게 된다. 그렇기 때문에 우리가 Constraint를 추가하려면 반드시 저 속성을 명시적으로 false처리 해줘야한다.
참고로 위 속성은, InterfaceBuilder로 만든 객체의 경우 기본값이 false지만, 프로그래밍적으로 만들면 기본적으로 true값이 들어간다.
자세한 내용은 Zedd님의 포스팅을 참조했다.

https://zeddios.tistory.com/37

UILabel과 UIImageView 설정


1. UIImageView 커스텀

이제 본격적으로 각 객체의 속성을 커스텀해보자. 나중에야 서버로 데이터를 받아올 것 같지만, 현재는 아쉬운대로 기본이미지를 넣어줘야 할텐데, 채널에선 테일러 스위프트 사진을 넣었지만, 나는 개인적으로 좋아하는 체인스모커스의 앨범아트를 삽입했다.

UIImageView.contentMode

먼저 가장 낯설고, 할때마다 외워지지 않는 contentMode부터 시작해보자. 사실 웹에서도 비슷한 개념이 있는데 이름이 달라서 그런지 전혀 다르게 느껴진다. contentMode에 대한 설명은 먼저 이 곳에서에서 제공하는 하나의 사진을 보고 계속하자.

당연한 말이지만 각각의 장단점이 있다. 말로 외워두는 것보다 해당 Mode에 대한 위 사진을 떠올려보면 좋을 것 같다. 그래도 한번더 정리해보자

  • ScalesAspectFill

이미지 비율을 유지하고 화면도 가득채우지만 잘린 부분을 희생한다. 본 프로젝트에선 이 옵션을 채택했다.

  • ScaleToFill

이미지 비율을 희생하고 화면을 가득 채운다. 대신 이미지가 프레임에 맞게 뭉개진다.

  • AspectFit

이미지 비율을 유지하고 화면을 가득채우지 않는다. 비율이 유지되는 대신 화면에 빈공간이 노출될 수 있다.

  비율유지 O 비율유지 X
화면채움O ScalesAspectFill(대신 화면이 잘림) ScaleToFill
화면채움X AspectFit 이런게 있을리가..

흠..
대충 외우기 위해서 억지로 외워보면 Aspect가 들어가면 비율을 유지하고, Fill이 들어가면 화면을 가득 채운다고 이해하면 될 것 같다. 한가지 더, 만약 ScalesAspectFill 속성을 사용한다면 한가지 더 고려해야할 사항이 있다.

  • imageView.clipsToBounds
    바로 이 속성이다. ScalesAspectFill의 경우, 잘린 부분을 제외해버리는데, SubView들에게도 이러한 커팅을 적용할 것인지에 대한 확인이 필요하며 그것을 결정하는게 바로 이 속성이다. 이 속성을 true로 해두지 않으면 subview들이 아래와 같이 미쳐날뛰게된다.

Profile ImageView를 둥글게

프로필을 설정하는 부분을 둥글게 만들어주는 부분이다. 너무 간단하기 때문에 간략하게 설명하고 넘어가겠다.

  • imageView.layer.cornerRadius
    ImageView Layer 겉의 반지름을 정하는 부분. 만약 이번 프로젝트처럼 정확하게 원모양으로 만들고 싶다면, 동적으로 (혹은 정적으로) 해당 이미지뷰의 크기의 절반을 반지름으로 지정해주면 될 것이다.
  • imageView.layer.masksToBounds = true;
    위의 cliptsToBounds속성과 같다. 차이점은 이 경우, subView가 아니라 subLayer를 자를것인지를 묻는다. imageView의 subLayer에 해당하는 이미지는 이 속성을 통해 둥글게 잘린다.

2. UILabel 커스텀

이번엔 UILabel을 커스텀 해볼텐데, 이것도 너무 쉬워서 자잘한부분은 생략하려고 한다. 참고로 본 프로젝트의 (현재까지)모든 폰트는 System폰트를 적용했고 부분적으로 Bold속성을 주었다.

label.lineBreakMode

UILabel을 설정할 때, 가장 핵심적인 부분. 사실 UILabel에서 설명할 부분은 이게 다다. 이 프로젝트에서 동영상의 설명파트의 경우 길어질 수가 있어서 끝 부분을 어떻게 처리할지를 결정해야 한다.
나는 최대 2줄까지 표현하고 싶었기 때문에 먼저 numberOfLines속성을 2로 지정하였다. 이제 위의 LineBreakMode를 설정할 시간이다.

  • Truncating [Head, Middle, Tail]
    간단하게 초과되는 부분을 "..." 으로 처리한다. head의 경우엔 앞부분을, Middle은 중간을, Tail은 뒷부분을 잘라버린다.
  • by[Char, Word]Wraping
    Wraping 즉 줄바꿈이 일어난다. 이 때 Char는 문자단위, Word는 단어 단위의 줄바꿈을 뜻한다. 본 프로젝트에선 byWordWraping옵션을 채택하였다.

NavigationBar 설정하기


어느새 첫페이지 레이아웃 설정의 마지막 단계다. 이번엔 NavigationBar와 StatusBar를 조금 만져볼거다.

1. Title대신 TitleView

일반적으로 NavigationBar에 제목을 지정할 땐, title속성을 사용한다. 이 때 단점은, title속성이 String?값을 설정되어 있기 때문에, 폰트, 컬러를 포함한 다양한 속성들을 마음대로 집어넣지 못한다는 것이다.
이를 보완하기 위해 NavigationBar에는 TitleView라는 속성이 존재한다. 이는 UIVIew형태로 선언되어 있기때문에 UIView를 서브클래싱하는 대부분의 객체가 이 자리에 들어갈 수 있다.

          let titleView = UILabel(frame: CGRect(x: 0, y: 0, width: self.view.frame.width - 32, height: (self.navigationController?.navigationBar.frame.height)!))
        titleView.textAlignment = .left
        titleView.textColor = .white
        titleView.font = .boldSystemFont(ofSize: 20)
        titleView.text = "WanTube"
        self.navigationItem.titleView = titleView

가볍게 titleView속성을 설정해보자.

먼저 titleview에 들어갈 UiLabel 객체를 정의한다. 이 때 넓이는 전체 뷰의 크기에서 버튼이 들어갈 오른쪽부분을 제외한 크기가 되며, 높이는 네비게이션바의 크기가 된다.
안에있는 text는 흰색으로 왼쪽정렬, 폰트는 boldSystemFont로 20사이즈로 설정했다.

만약 titleView가 아닌 단순 title로만 설정했다면, 커스텀할 수 있는 부분은 "text"부분 하나밖에 없다. 하지만 titleView로 설정했기 때문에 위처럼 다양한 부분들을 커스텀할 수 있었다.

2. NavigationBar와 StatusBar 색 변경

네비게이션바

현재 내 유튜브의 경우 확인해보니 상단 NavigationBar가 흰색이다. (...) 하지만 얼마전까지만 해도 빨간색이었던 것 같고, 강의에서도 빨간색으로 진행하고 있고, 무엇보다 흰색으로 한다면 이걸 커스텀하는 의미가 없으니, 연습하는셈치고 빨간색으로 해보려고한다.

self.navigationController?.navigationBar.barTintColor = .red

사실 NavigationBar의 색을 변경하는 방법은 너무 간단하다. 위처럼 설정해주면 끝이다. 한가지 의문인 점은 원랜, AppDelegate파일에서 apperance옵션을 설정하여 할 수도 있는데, 왜인지 변경되지가 않았다. 이 부분은 더 찾아봐야 할 것 같다.

self.navigationController?.navigationBar.isTranslucent = true

네비게이션 바의 투명도를 true값으로 설정해준다.

application.statusBarStyle = .lightContent

statusBar의 속성을 설정해준다. 사실 속성이래봤자 default와 lightContent 두가지 밖에 없다. 전자는 검은색, 후자는 하얀색이 될 뿐이다.

UIViewControllerBasedStatusBarAppearance = NO

statusBar의 속성을 설정할 때 필수적으로 지정해야 하는 부분이다. 특이하게 Info.plist에서 정의되고 있으며, 이 속성을 NO로 설정하지 않으면, 위에서 설정한 옵션이 적용되지 않는다.

계속


1차 결과화면

 이로서 "유튜브 클론앱" 첫 페이지에 대한 레이아웃을 완성했다. 사실 영상을 보면 이 외에도 커스텀 메뉴바를 추가해서 다른 뷰컨트롤러로 이동하는 부분이 있긴 하다. 하지만 아예 모르는 부분도 아니고, 여기까지 진행하다가는 원래 이 프로젝트를 시작한 이유인 영상을 다루는 부분까지 너무 오래걸릴 것 같아서, 일단 생략할 생각이다.
 이후 앱을 완성하고 나면 메뉴바를 포함해서 영상에 없는 기능들 까지 개인적으로 계속 업데이트 해 나가볼생각이다.

'iOS & Swift' 카테고리의 다른 글

[iOS] URL에서 가져온 JSON, Parsing 하기  (0) 2019.08.01
Swift 문법 정리 [객체 관련]  (0) 2019.07.31
Swift 문법 정리 [기본]  (0) 2019.07.28