봄날은 갔다. 이제 그 정신으로 공부하자

다양한 방법으로 동적 레이아웃 대응하기 본문

iOS Tip

다양한 방법으로 동적 레이아웃 대응하기

길재의 그 정신으로 공부하자 2022. 8. 29. 11:11

이번 글에서는 Text에 따라 동적으로 변하는 영역을 가지는 레이아웃을 만들어보도록 하겠습니다.

이전 글과는 다르게 이번에는 3가지 방법을 사용해볼까 합니다.

  - 첫번째는 스토리보드를 사용한 방법

  - 두번째는 스토리보드 사용을 최소화하고 프로그래밍으로 처리하는 방법

  - 마지막 세번째 방법은 프로그래밍으로 처리하지만 오토레이아웃을 사용하지 않는 방법

 

 

무엇을 만들 것인가?

아래와 같은 레이아웃을 만들어보겠습니다.

 

개발하다 보면 자주 접하는 아주 흔한 레이아웃이네요. ^^

정석적으로 하면 왼쪽에 UILabel 한개만 놓고 Text의 일부 색상만 변경해 레이아웃을 구성하는게 맞는 방법이지만 지금은 그렇게하면 곤란하므로 여기에서는 일부러 복잡하게 "총 1,234 건"을 3개의 UILabel로 나누어 만들어보도록 하겠습니다.

 

만들어볼 레이아웃의 상세한 구성은 아래와 같습니다.

 

우선 첫번째 스토리보드를 사용해 동적 레이아웃 만들기

1. 스토리보드에 가로 StackView를 추가한 후 Attributes Inspector에서 아래와 같이 속성을 설정합니다.

spacing에 5를 입력하는 이유는 글자가 너무 딱 붙어있으면 어색해보여 UILabel간에 여백을 주기 위해서 입니다.

 

2. Text를 구성하는 3개의 UILabel을 StackView에 추가하고 Text 및 색상을 입력해줍니다.

 

위의 설명대로 StackView에 UILabel 3개를 추가했다면 스토리보드에서 아래처럼 보여지는데 당황하지 마세요.

이부분은 Content Hugging Priority로 간단히 처리가능하니 일단 이건 무시합니다.

 

3. 우측에 위치할 버튼을 StackView에 추가해 줍니다.

 

Attributes Inspector에서 아래와 같이 버튼의 속성을 설정합니다.

 

버튼의 넓이를 조건에 맞게 120으로 고정합니다.

 

그럼 이제 스토리보드에 아래와 같이 보여지는 것을 확인할 수 있습니다.

 

4. 글자와 버튼 사이 여백을 차지할 View 추가하기

이제 요구조건에 맞게 글자는 왼쪽에 버튼은 오른쪽에 정렬을 해야 합니다. 

SwiftUI에는 Spacer가 있어 쉽게 해결할 수 있지만 여기에는 없기 때문에 여백을 차지할 View를 추가해 주어야 합니다.

"건"과 버튼 사이에 아래 그림과 같이 View를 한 개 추가해 줍니다.

 

그럼 놀랍게도 스토리보드에 아래와 같이 아름답게 정렬되는 것을 확인 할 수 있습니다.

 

* 참고

여기까지 잘 따라했다면,

XCode에서 스토리보드의 단말 해상도를 변경해도 해상도에 맞게 왼쪽 오른쪽 정럴이 잘 되는 것을 확인할 수 있습니다.

 

왜? 정렬에 되는지는 StackView의 특성과 각 UI 객체의 size 속성에 대한 간단한 이해가 필요합니다.

스토리보드에서 StackView에 UI 객체를 추가할 때 StackView는 맨마지막 추가된 UI 객체의 Content Hugging Priority 속성을

Low(250)으로 설정하고 이전 추가된 UI 객체의 Content Hugging Priority 속성을 Low보다 1높은 값인 251로 변경합니다.

 

Content Hugging Priority는 UI 객체의 최대 크기에 대한 설정값으로 

맨 마지막에 추가한 UIView가 우선 순위가 가장 낮으므로 해상도에 따라 사이즈가 제일 먼저 줄어들게 되는 것 입니다.

 

 

5. 왼쪽 오른쪽 여백추가하기

요구 사항을 보면 글자의 왼쪽과 버튼의 오른쪽이 화면에 딱 붙어 있지 않고 8dp 정도 떨어져 있는 것을 확인 할 수 있습니다.

위 방법처럼 StackView의 맨 처음과 마지막에 width가 8인 View 한개씩 추가합니다.

 

View를 추가할때 스토리보드에서 레이아웃이 깨질 수도 있지만 View의 width를 8로 고정하면 자연스레 해결되는 문제이므로 신경쓰지 않아도 됩니다.

 

 

두번째 방법은 스토리보드 사용을 최소화하고 프로그래밍으로 만들기입니다.

프로그래밍 방식이라도 추가되는 UI 구성요소는 스토리보드 방식과 동일합니다.

설명은 코드에 주석으로 추가하였습니다.

import UIKit

class MyViewController: ViewController {
    
    @IBOutlet var vContents: UIView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // UI객체가 들어갈 StackView를 생성합니다.
        let stvHeader = UIStackView()
        
        // 화면에 StackView를 추가합니다.
        self.vContents.addSubview(stvHeader)
        
        // 스토리보드에서 추가한 것과 비교될 수 있도록 적당이 아래에 위치하도록 오토레이아웃을 설정합니다.
        stvHeader.translatesAutoresizingMaskIntoConstraints = false
        stvHeader.topAnchor.constraint(equalTo: self.vContents.topAnchor, constant: 200.0).isActive = true
        stvHeader.leftAnchor.constraint(equalTo: self.vContents.leftAnchor).isActive = true
        stvHeader.rightAnchor.constraint(equalTo: self.vContents.rightAnchor).isActive = true
        stvHeader.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
        
        // StackView의 나머지 설정도 추가합니다.
        stvHeader.axis = .horizontal
        stvHeader.distribution = .fill
        stvHeader.alignment = .center
        stvHeader.spacing = 5
        stvHeader.backgroundColor = .clear
        
        // 왼쪽 여백을 표시할 View를 추가합니다.
        let vLeftSpacer = UIView()
        vLeftSpacer.backgroundColor = .clear
        vLeftSpacer.widthAnchor.constraint(equalToConstant: 8.0).isActive = true
        vLeftSpacer.setContentHuggingPriority(UILayoutPriority.init(rawValue: 251.0), for: .horizontal)
        stvHeader.addArrangedSubview(vLeftSpacer)
        
        // Text를 표시할 UILabel들을 추가합니다.
        let lbTotal = UILabel()
        lbTotal.text = "총"
        lbTotal.font = .systemFont(ofSize: 17.0, weight: .bold)
        lbTotal.textColor = .black
        lbTotal.backgroundColor = .clear
        lbTotal.setContentHuggingPriority(UILayoutPriority.init(rawValue: 251.0), for: .horizontal)
        stvHeader.addArrangedSubview(lbTotal)
        
        // Text를 표시할 UILabel들을 추가합니다.
        let lbCount = UILabel()
        lbCount.text = "1,234"
        lbCount.font = .systemFont(ofSize: 17.0, weight: .bold)
        lbCount.textColor = .red
        lbCount.backgroundColor = .clear
        lbCount.setContentHuggingPriority(UILayoutPriority.init(rawValue: 251.0), for: .horizontal)
        stvHeader.addArrangedSubview(lbCount)
        
        // Text를 표시할 UILabel들을 추가합니다.
        let lbEa = UILabel()
        lbEa.text = "건"
        lbEa.font = .systemFont(ofSize: 17.0, weight: .bold)
        lbEa.textColor = .black
        lbEa.backgroundColor = .clear
        lbEa.setContentHuggingPriority(UILayoutPriority.init(rawValue: 251.0), for: .horizontal)
        stvHeader.addArrangedSubview(lbEa)
        
        // Text와 버튼 사이에 가변으로 변경될 여백 View를 추가합니다.
        let vDynamicSpacer = UIView()
        vDynamicSpacer.backgroundColor = .clear
        vDynamicSpacer.setContentHuggingPriority(UILayoutPriority.init(rawValue: 250.0), for: .horizontal)
        stvHeader.addArrangedSubview(vDynamicSpacer)
        
        // 오늘쪽 정렬될 버튼을 추가합니다.
        let btnPeriod = UIButton()
        btnPeriod.titleLabel!.font = .systemFont(ofSize: 18)
        btnPeriod.backgroundColor = UIColor(red: 60/255, green: 255/255, blue: 255/255, alpha: 1)
        btnPeriod.setTitle("1주일", for:.normal)
        btnPeriod.setTitleColor(.black, for: .normal)
        btnPeriod.widthAnchor.constraint(equalToConstant: 120.0).isActive = true
        btnPeriod.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
        stvHeader.addArrangedSubview(btnPeriod)
        
        // 오른쪽 여백을 표시할 View를 추가합니다.
        let vRightSpacer = UIView()
        vRightSpacer.backgroundColor = .clear
        vRightSpacer.widthAnchor.constraint(equalToConstant: 8.0).isActive = true
        vRightSpacer.setContentHuggingPriority(UILayoutPriority.init(rawValue: 251.0), for: .horizontal)
        stvHeader.addArrangedSubview(vRightSpacer)
    }
}

 

위와 같이 프로그래밍한 후 해당 앱을 실행하면 아래와 같이 보여집니다.

 

위 그림의 "윗 부분"이 스토리보드에서 추가한 부분이고 "아랫 부분"이 프로그래밍으로 추가한 레이아웃 입니다.

차이가 없는 것을 알 수 있습니다.

 

스토리보드로 추가하는 것은 눈으로 보면서 추가할 수 있는 장점이 있지만 여기 저기 옮겨 다니며 값을 설정해주어야 하므로 매우 큰 번거로움이 있습니다.

하지만 코드로 추가하면 눈으로 보이지는 않는다는 단점이 있지만 한 곳에서 깔끔하게 추가 및 관리할 수 있다는 장점이 있습니다.

또한 여러명이 스토리보드를 한꺼번에 만질 때 생기는 문제를 최소화 할 수 있어 여러명이 동시에 개발할 때 코드 관리가 쉬운 장점이 있습니다.

 

 

마지막 세번째 방식 오토레이아웃 없이 프로그래밍으로 직접 가변 화면만들기입니다.

이번에 설명할 방식은 오토레이아웃 없이 직접 화면 해상도를 구해 한땀 한땀 수동으로 가변 화면에 대응하는 방식입니다.

별로 필요 없지만 혹시나 해서 한번 올려봅니다.

 

우선 입력되는 Text에 따라 가변으로 변하는 UILabel이 있으므로 아래와 같이 화면에 보여지는 Text의 width를 구하는 함수를 한개 만들어 줍니다.

    /**
    Text가 노출되는 영역의 넓이를 계산해서 반환하는 함수
     
     @param
     text: 출력되는 문구
     drawArea: Text가 노출되는 영역
     fontSize: 폰트 사이즈
     weight: 폰트의 weight 속성
     */
    func getTextWidth(text: String, drawArea: CGSize, fontSize: Int, weight: UIFont.Weight)-> CGFloat {
        let rect = NSString(string: text).boundingRect(with: drawArea, options: .usesLineFragmentOrigin, attributes: [.font: UIFont.systemFont(ofSize: CGFloat(fontSize), weight: weight)], context: nil)
        return rect.width
    }

 

자 이제 오토레이아웃 없이 레이아웃을 구성해 보도록 하겠습니다.

코드는 아래와 같습니다.

func insertMyLayout() {
        // 기기의 화면 넓이
        let SCREEN_WIDTH = UIScreen.main.bounds.width
        // 콘텐츠를 그려줄 Top 포지션
        let CONTENT_TOP_POS = 300.0
        // 콘텐츠의 높이
        let CONTENT_HEIGHT = 50.0
        // 콘텐츠간 기본 여백
        let CONTENT_MARGIN = 5.0
        // 버튼 넓이
        let BUTTON_WIDTH = 120.0
        // 왼쪽 오른쪽 여백
        let LR_MARGIN = 8.0
        
        // 콘텐츠가 그려질 left 포지션 계산
        var left = LR_MARGIN + CONTENT_MARGIN
        
        // Text를 표시할 UILabel들을 추가합니다.
        let lbTotal = UILabel()
        lbTotal.text = "총"
        lbTotal.font = .systemFont(ofSize: 17.0, weight: .bold)
        lbTotal.textColor = .black
        lbTotal.backgroundColor = .clear
        // 표시될 영역을 지정합니다.
        var width = self.getTextWidth(text: "총", drawArea: CGSize(width: 1024, height: CONTENT_HEIGHT), fontSize: 17, weight: .bold)
        lbTotal.frame = CGRect(x: left, y: CONTENT_TOP_POS, width: width, height: CONTENT_HEIGHT)
        self.vContents.addSubview(lbTotal)
        
        // content의 left 위치 재계산
        left += (width + CONTENT_MARGIN)
        
        // Text를 표시할 UILabel들을 추가합니다.
        let lbCount = UILabel()
        lbCount.text = "1,234"
        lbCount.font = .systemFont(ofSize: 17.0, weight: .bold)
        lbCount.textColor = .red
        lbCount.backgroundColor = .clear
        // 콘텐츠의 사이즈 계산
        width = self.getTextWidth(text: "1,234", drawArea: CGSize(width: 1024, height: CONTENT_HEIGHT), fontSize: 17, weight: .bold)
        lbCount.frame = CGRect(x: left, y: CONTENT_TOP_POS, width: width, height: CONTENT_HEIGHT)
        self.vContents.addSubview(lbCount)
        
        // content의 left 위치 재계산
        left += (width + CONTENT_MARGIN)
        
        // Text를 표시할 UILabel들을 추가합니다.
        let lbEa = UILabel()
        lbEa.text = "건"
        lbEa.font = .systemFont(ofSize: 17.0, weight: .bold)
        lbEa.textColor = .black
        lbEa.backgroundColor = .clear
        // 표시될 영역을 지정합니다.
        width = self.getTextWidth(text: "건", drawArea: CGSize(width: 1024, height: CONTENT_HEIGHT), fontSize: 17, weight: .bold)
        lbEa.frame = CGRect(x: left, y: CONTENT_TOP_POS, width: width, height: CONTENT_HEIGHT)
        self.vContents.addSubview(lbEa)
        
        // content의 left 위치 재계산
        // 버튼의 width가 120이고 우측 여백이 8이고 콘텐츠간 space가 5이므로 이를 감안하여 left 위치 계산
        left = SCREEN_WIDTH - (BUTTON_WIDTH + LR_MARGIN + CONTENT_MARGIN)
        
        // 오늘쪽 정렬될 버튼을 추가합니다.
        let btnPeriod = UIButton()
        btnPeriod.titleLabel!.font = .systemFont(ofSize: 18)
        btnPeriod.backgroundColor = UIColor(red: 60/255, green: 255/255, blue: 255/255, alpha: 1)
        btnPeriod.setTitle("1주일", for:.normal)
        btnPeriod.setTitleColor(.black, for: .normal)
        btnPeriod.widthAnchor.constraint(equalToConstant: 120.0).isActive = true
        btnPeriod.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
        // 표시될 영역을 지정합니다.
        btnPeriod.frame = CGRect(x: left, y: CONTENT_TOP_POS, width: BUTTON_WIDTH, height: CONTENT_HEIGHT)
        self.vContents.addSubview(b

tnPeriod)
    }

 

이 방식은 오토레이아웃을 사용하지 않고 직접 위치를 계산해 UI 객체를 위치시키는 방식이므로 정렬을 위해 StactView나 여백을 위한 UIView들을 추가할 필요가 없이 딱 필요한 UI 객체만 추가할 수 있다는 장점이 있습니다만...

계산을 아주 잘해야 한다는 아주 큰 단점이 있습니다.

 

앱을 실행하면 아래와 같이 보여집니다.

 

세가지 방식 간에 차이가 느껴 지나요?

세가지 모두 서로 다른 방법으로 레이아웃을 구성하지만 모두 가변 레이아웃에 대응하도록 만들어져 있으므로 다른 해상도에서도 모두 동일하게 보여집니다.

 

 

정리

iOS도 다양한 해상도 대응을 위해 여러가지 방법을 제공하고 있습니다.

개발자는 스토리보드를 사용해도 되고 프로그래밍으로 오토레이아웃을 사용해도 되고 그도 아니고 계산에 자신이 있으면 노출될 콘텐츠의 위치를 계산해 화면에 뿌려줘도 됩니다.

중요한 것은 다양한 해상도에 대응할 수 있도록 개발하는 것 입니다.

 

1. 스토리 보드를 사용해 레이아웃을 구성한 방법

  - 장점

      -> 스토리보드에서 눈으로 보면서 레이아웃을 배치할 수 있어 직관적임.

      -> 레이아웃 구성을 위한 코드와 나머지 코드가 분리되어 MVVM or MVP와 같은 구조로 개발하는데 편리함.

  - 단점

      -> 화면에 레이아웃을 배치하기 위해 의외로 많은 작업을 해야 함.

      -> 여러명이 작업할 때 동시에 스토리보드에 commit하게 되면 문제가 복잡해질 수 있음.

 

2. 프로그래밍으로 오토레이아웃을 적용해 레이아웃을 구성한 방법

  - 장점

      -> 레이아웃 코드를 한 곳에서 처리할 수 있어 관리가 쉬움.

      -> 여러명이 작업하더라도 스토리보드 관련 Conflict 문제가 발생할 우려가 적음.

  - 단점

      -> 머릿속으로 화면을 예상하면서 프로그래밍하므로 레이아웃 계산이 조금은 어려움.

      -> 레이아웃 구성을 위한 코드가 나머지 코드와 분리가 어려워 MVVM or MVP와 같은 구조로 개발하는데 추가 처리가 필요함.

 

3. 프로그래밍으로 레이아웃에 맞게 수동으로 직접 레이아웃을 구성한 방법

  - 장점

      -> 레이아웃 코드를 한 곳에서 처리할 수 있어 관리가 쉬움.

      -> 여러명이 작업하더라도 스토리보드 관련 Conflict 문제가 발생할 우려가 적음.

      -> 세가지 방법 중 자유도는 가장 높음.

  - 단점

      -> 개발 난이도가 가장 높음.

      -> 레이아웃과 좌표에 대한 이해도가 매우 높아야 함.

      -> 레이아웃 구성을 위한 코드가 나머지 코드와 분리가 어려워 MVVM or MVP 같은 구조로 개발하는데 추가 처리가 필요함.

'iOS Tip' 카테고리의 다른 글

Dispatch Queue  (0) 2022.11.29
라운드 프레임 버튼 만들기  (0) 2022.08.24
숫자에 콤마 추가하기  (0) 2022.08.23
Padding이 적용되는 Custom UILabel 만들기  (0) 2022.08.19
IOS 앱 디버깅 방지  (0) 2022.04.20
Comments