[UIKit] UITraitCollection을 활용한 Layout(Programmatically UI)

Uno·2021년 9월 11일
0

UIKit

목록 보기
3/9

Q. iPad와 iPhone 의 UI 배치를 다르게하고 싶은데, 어떻게하지?

작업을 하시면서 위와 같은 질문을 해보신 경험이 있으신가요?

그러면 보통 이런 검색어로 검색하셨을겁니다.

  • how to layout various device in swift
  • swift ipad iphone layout
    ...

그러다보면, 도달하는 종착지가 "UITraitCollection" 이 될 것 같아요.
이 객체는 위와 같은 문제가 있는 경우 좋은 해결책이라고 생각합니다.

Layout

먼저 공식문서에 있는 Adaptivity and Layout 자료를 보겠습니다.
(https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/adaptivity-and-layout/)

현재 애플 디바이스는 상당히 다양한 크기의 화면이 있습니다.
(물론 안드로이드에 비하면 적겠지만..)

그리고 공식문서를 조금 더 내리면, 다음과 같은 글을 볼 수 있습니다.

이렇게 공식문서에서부터 활용하도록 추천하고 있죠.

AutoLayout > UITraitCollection 순으로 공식문서를 보면 이해가 쉬워서 한 번 문서의 계층순서대로 따라가봤습니다.

UITraitCollection

다음과 같이 정의되어있습니다.

class UITraitCollection: NSObject

가로 및 세로 크기 클래스, 표시 크기 및 사용자 인터페이스 관용구와 같은 특성을 포함하는 앱의 iOS 인터페이스 환경입니다.

뭔가 이해가 된 것 같으면서 안된 것 같은 느낌입니다.

다시 풀어서 설명하면,

가로의 크기와 세로의 크기에 대한 어떤 class 가 있나봅니다.
그리고 그것들과 관련된 특성들을 포함한 환경을 나타내는 것이 UITraitCollection 이구요.

단어 자체가 이미 뜻을 함의하고 있죠.

뭔가 collection하고 있고 그것은 Trait이다.

정리할게요.

UITraitCollection에 가로의 크기와 세로의 크기에 대한 클래스가 있다. 이걸 통해서 iPhone, iPad Layout 구성에 활용하면 될 것 같다!

(자세한 설명은 번역에 지나지 않을 것 같아서 활용에 초점을 맞추겠습니다.)

활용

그러면 어떻게 이걸 사용하면 좋을까요?

무엇이든지, 개념만 보고 넘어가면 기억도안나고 사용할수도 없겠죠.

제가 작성한 코드를 보여주고 설명하는 식으로 진행하겠습니다.

class ViewController: UIViewController { ... }

기본적으로 이렇게 구성되어있죠.

저는 Code로 UI를 구성할 예정이므로 SnapKitThen 으로 UI를 구성해보겠습니다.

SnapKit : https://github.com/SnapKit/SnapKit
Then : https://github.com/devxoul/Then

먼저 UI Object를 선언합니다.

    // MARK: - UI Object
    private let redView = UIView().then {
        $0.backgroundColor = .red
    }
    
    private let greenView = UIView().then {
        $0.backgroundColor = .green
    }
    
    private let blueView = UIView().then {
        $0.backgroundColor = .blue
    }

기본적인 Then의 사용 방법입니다.

빨강 뷰 / 녹색 뷰 / 파랑 뷰
이렇게 3 개의 뷰를 선언했습니다.

그리고 가로화면과 세로화면에 따라서 다르게 AutoLayout을 설정하고 싶으므로 2 개의 오토레이아웃을 각각의 메소드로 생성합니다.

저는 여기서 2 개만 했습니다.

여기서는 본인의 구체적인 프로젝트의 상황에 따라 달라질 수 있겠죠.

// MARK: - Helpers
    private func setupDefaultLayout() {
        
        view.removeConstraints(view.constraints)
        view.backgroundColor = .white
        view.addSubview(redView)
        redView.snp.makeConstraints {
            $0.top.equalTo(view.snp.top)
            $0.bottom.equalTo(view.snp.bottom)
            $0.left.equalTo(view.snp.left)
            $0.width.equalTo(view.snp.width).multipliedBy(0.22)
        }
        
        view.addSubview(greenView)
        greenView.snp.makeConstraints {
            $0.top.equalTo(view.snp.top)
            $0.bottom.equalTo(view.snp.bottom)
            $0.centerX.equalTo(view.snp.centerX)
            $0.width.equalTo(view.snp.width).multipliedBy(0.22)
        }
        
        view.addSubview(blueView)
        blueView.snp.makeConstraints {
            $0.top.equalTo(view.snp.top)
            $0.bottom.equalTo(view.snp.bottom)
            $0.right.equalTo(view.snp.right)
            $0.width.equalTo(view.snp.width).multipliedBy(0.22)
        }
    }
    
    private func setupLayoutForCompactEnvironment() {
        
        view.removeConstraints(view.constraints)
        view.addSubview(redView)
        greenView.snp.makeConstraints {
            $0.top.equalTo(view.snp.top)
            $0.bottom.equalTo(view.snp.bottom)
            $0.left.equalTo(view.snp.left)
            $0.width.equalTo(view.snp.width).multipliedBy(0.22)
        }
        
        view.addSubview(greenView)
        redView.snp.makeConstraints {
            $0.top.equalTo(view.snp.top)
            $0.bottom.equalTo(view.snp.bottom)
            $0.centerX.equalTo(view.snp.centerX)
            $0.width.equalTo(view.snp.width).multipliedBy(0.22)
        }
        
        view.addSubview(blueView)
        blueView.snp.makeConstraints {
            $0.top.equalTo(view.snp.top)
            $0.bottom.equalTo(view.snp.bottom)
            $0.right.equalTo(view.snp.right)
            $0.width.equalTo(view.snp.width).multipliedBy(0.22)
        }
        
    }

setupDefaultLayout : 기본 레이아웃을 정의한 메소드
setupLayoutForCompactEnvironment : 가로나 세로의 크기가 변경되었을 때 적용할 레이아웃을 정의한 메소드

간단하죠?

removeConstraints 메소드를 통해서 ViewController의 View에 적용한 Constraint를 모두 제거 했습니다.

저는 그냥 귀찮아서 모두 제거한 것입니다. 자신의 상황이 일부분만 변경하면 된다면, 해당 Constraints만 제거하면 됩니다.

저처럼 모두 제거하면, 모두 다시 Constraints를 설정해주어야하기 때문에 코드가 길어지겠죠. 보통은 일부분만 변경해주곤 합니다.

여기서 의문이 드실 수 있습니다.

layoutIfNeeded()
setNeedsLayout()
이런 메소드에 대해서 이해가 있으신 분들은 왜 굳이 아예 제거했다가 다시 추가하냐 (Cosntraints를)

이렇게 의문이 드실 수 있겠죠.

해당 부분은 제가 따로 글로 작성해보겠습니다.
이번에는 다양한 디바이스에 오토레이아웃을 다르게 적용하는 방법에만 집중하기 위해서 위처럼 구성했습니다.

지금까지 "2 개의 조건에 적용될 오토레이아웃을 구현" 한 상태입니다.

앞으로 기기의 크기가 변동이 있을때, 어디선가 호출해줄 친구가 필요하겠죠?

    // MARK: - Adaptivity and Layout
    /// 기기의 동작환경이 변경되면 호출되는 메소드
    /// - 첫 시작때는 호출되지 않고, 가로 세로로 변경되면 호출되고 있음
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        
        // 수평사이즈 클래스가 똑같으면 막는다. == 다시 그릴 필요가 없는 상태
        guard previousTraitCollection?.horizontalSizeClass != traitCollection.horizontalSizeClass else { return }
        
        switch traitCollection.horizontalSizeClass {
        case .compact:
            setupDefaultLayout()
        case .regular:
            setupLayoutForCompactEnvironment()
        case .unspecified:
            break
        }
    }

traitCollectionDidChange 이 친구가 그 친구입니다.

여기에 위처럼 선언해주면 끝입니다.

추가로 내가 지금 실행하고 있는 디바이스가 compact인지 reguler인지 궁금하시면 아래 코드를 작성해서 확인해보세요.

        if (self.traitCollection.horizontalSizeClass.rawValue) == 0 {
            print("수평사이즈 클래스: Regular")
        } else { // 1 == Compact
            print("수평사이즈 클래스: Compact")
        }
        
        if (self.traitCollection.verticalSizeClass.rawValue) == 0 {
            print("수직사이즈 클래스: Compact")
        } else { // 1 == Reguler
            print("수직사이즈 클래스: Regular")
        }

(맨 아래 전체코드를 두겠습니다.)

추후에 이 주제로 다시 영상을 찍을 예정입니다.

글로 설명은 여기까지만하고 영상제작되면 아래에 링크도 함께 추가하겠습니다.

읽어주셔서 감사합니다^^

전체코드


//
//  ViewController.swift
//  UITraitCollection_StudyProject
//
//  Created by 김우성 on 2021/09/11.
//

import UIKit
import SnapKit
import Then

class ViewController: UIViewController {

    // MARK: - UI Object
    private let redView = UIView().then {
        $0.backgroundColor = .red
    }
    
    private let greenView = UIView().then {
        $0.backgroundColor = .green
    }
    
    private let blueView = UIView().then {
        $0.backgroundColor = .blue
    }
    
    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setupDefaultLayout()
        
        let compactTraitCollection = UITraitCollection(traitsFrom: [UITraitCollection(verticalSizeClass: .compact), UITraitCollection(horizontalSizeClass: .compact)])
        
        let regularTraitCollection = UITraitCollection(traitsFrom: [UITraitCollection(verticalSizeClass: .regular), UITraitCollection(horizontalSizeClass: .regular)])
        
        if (self.traitCollection.horizontalSizeClass.rawValue) == 0 {
            print("수평사이즈 클래스: Regular")
        } else { // 1 == Compact
            print("수평사이즈 클래스: Compact")
        }
        
        if (self.traitCollection.verticalSizeClass.rawValue) == 0 {
            print("수직사이즈 클래스: Compact")
        } else { // 1 == Reguler
            print("수직사이즈 클래스: Regular")
        }
        
        
    }

    // MARK: - Adaptivity and Layout
    /// 기기의 동작환경이 변경되면 호출되는 메소드
    /// - 첫 시작때는 호출되지 않고, 가로 세로로 변경되면 호출되고 있음
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        
        // 수평사이즈 클래스가 똑같으면 막는다. == 다시 그릴 필요가 없는 상태
        guard previousTraitCollection?.horizontalSizeClass != traitCollection.horizontalSizeClass else { return }
        
        switch traitCollection.horizontalSizeClass {
        case .compact:
            setupDefaultLayout()
        case .regular:
            setupLayoutForCompactEnvironment()
        case .unspecified:
            break
        }
    }
    
    // MARK: - Helpers
    private func setupDefaultLayout() {
        
        view.removeConstraints(view.constraints)
        view.backgroundColor = .white
        view.addSubview(redView)
        redView.snp.makeConstraints {
            $0.top.equalTo(view.snp.top)
            $0.bottom.equalTo(view.snp.bottom)
            $0.left.equalTo(view.snp.left)
            $0.width.equalTo(view.snp.width).multipliedBy(0.22)
        }
        
        view.addSubview(greenView)
        greenView.snp.makeConstraints {
            $0.top.equalTo(view.snp.top)
            $0.bottom.equalTo(view.snp.bottom)
            $0.centerX.equalTo(view.snp.centerX)
            $0.width.equalTo(view.snp.width).multipliedBy(0.22)
        }
        
        view.addSubview(blueView)
        blueView.snp.makeConstraints {
            $0.top.equalTo(view.snp.top)
            $0.bottom.equalTo(view.snp.bottom)
            $0.right.equalTo(view.snp.right)
            $0.width.equalTo(view.snp.width).multipliedBy(0.22)
        }
    }
    
    private func setupLayoutForCompactEnvironment() {
        
        view.removeConstraints(view.constraints)
        view.addSubview(redView)
        greenView.snp.makeConstraints {
            $0.top.equalTo(view.snp.top)
            $0.bottom.equalTo(view.snp.bottom)
            $0.left.equalTo(view.snp.left)
            $0.width.equalTo(view.snp.width).multipliedBy(0.22)
        }
        
        view.addSubview(greenView)
        redView.snp.makeConstraints {
            $0.top.equalTo(view.snp.top)
            $0.bottom.equalTo(view.snp.bottom)
            $0.centerX.equalTo(view.snp.centerX)
            $0.width.equalTo(view.snp.width).multipliedBy(0.22)
        }
        
        view.addSubview(blueView)
        blueView.snp.makeConstraints {
            $0.top.equalTo(view.snp.top)
            $0.bottom.equalTo(view.snp.bottom)
            $0.right.equalTo(view.snp.right)
            $0.width.equalTo(view.snp.width).multipliedBy(0.22)
        }
    }
    
    private func setupLayoutForRegulerEnvironment() {
        
    }
}
profile
iOS & Flutter

0개의 댓글