dev-signer
개자이너
dev-signer
전체 방문자
오늘
어제
  • 분류 전체보기 (20)
    • Project (8)
      • DIMODAMO (1)
      • Menual (6)
      • WatchOS (1)
    • iOS (9)
      • Swift (6)
      • UI (2)
    • 코딩테스트 (2)
    • 일상 (1)
      • 이야기 (1)

블로그 메뉴

  • 홈
  • 태그

공지사항

인기 글

태그

  • swfit
  • menual
  • 의존성주입
  • 코딩테스트
  • 스위프트
  • Swift
  • 디지털미디어디자인
  • Dependency Injection
  • watchos
  • 알고리즘
  • XCode
  • 사이드프로젝트
  • SnapKit
  • 파이썬
  • 리뷰요청하기
  • tuist
  • 메뉴얼
  • ios
  • 주니어개발자
  • 파이썬 알고리즘 인터뷰

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
dev-signer

개자이너

[iOS] LayoutDriven UI
iOS/UI

[iOS] LayoutDriven UI

2023. 6. 4. 16:31

1. LayoutDriven UI에 들어가기 앞서..

우리는 iOS에서 UI를 그리거나 업데이트할 때 아래와 같은 이슈를 많이 겪으셨을 것 같아요.
아래의 문제가 모두 UI의 문제라고 볼 수는 없지만, 분명 로직에 따라 UI 업데이트를 요청했는데 반영되지 않는 것이 가장 큰 이슈 같습니다.

 

  1. 🆕 뱃지가 있는 UI를 눌렀는데 뱃지가 사라지지 않음
  2. 📩 메세지를 읽었는데 '읽음' 으로 상태가 바뀌지 않음
  3. ✅ 버튼을 눌렀는데 Pressed 상태로 바뀌지 않음
  4. ☠️ UI를 업데이트 하다가 메인쓰레드에서 동작하지 않아 앱이 죽음

이런 상황의 공통점은 로직에 따라 UI 업데이트가 되지 않음으로 볼 수 있습니다.

 

이러한 문제점은 대부분 UI를 업데이트하는 코드가 바깥에 따로 존재하기 때문이라고 생각합니다.
UI를 작은 컴포넌트 단위로 쪼개고, 그 컴포넌트가 본인의 상태가 어떻게 변경될 지 알고 있다면 어떨까요?

그리고 굳이 '너 지금 상태 바꼈어!' 라고 개발자가 하나하나 세팅하지 않고 UI 컴포넌트가 본인의 상태를 바로 캐치할 수 있다면 어떨까요?

 

위와 같은 특성을 가진게 LayoutDriven UI라고 생각합니다.기본적으로 로직과 완전히 분리된 컴포넌트로 제작하게 되면 더욱 금상첨화일 것 같네요.

 


 

2. Button에게 청사진을 그려주자

사이드프로젝트 Menual의 디자인 시스템의 일부

위 이미지처럼 버튼 컴포넌트는 총 3가지의 상태를 가지는 요구사항을 구현하는 상황이라고 가정해봅시다.

더불어 이 3가지 상태를 구분하는 Swift에서의 변수를 상상해서 적어 볼게요.

 

  1. 활성화 상태
    - userinteractionenabled가 true일 경우
    - highlighted 값이 false일 경우

  2. 눌린 상태
    - userinteractionenabled가 true일 경우
    - highlighted 값이 true일 경우

  3. 비활성화 상태
    - userinteractionenabled가 false일 경우

이미 눌린 상태, 비활성화를 체크하는 변수는 제공해주고 있으니까 새로 만들지 않고도 가능합니다.
iOS에서 기본적으로 제공해주는 변수를 overriding 해서 우리가 원하는 정보로 바꿀 수 있을 것처럼 보이네요.

해당 변수를 적극 활용해서 구현 해보도록 하겠습니다.

 

 


 

 

3. 코드로 작성 해보자.

LayoutDriven UI는 코드로 작성하면 더욱 머리 속에 그림이 잘 그려지는데요.

아래의 코드를 확인하면서 LayoutDriven UI가 어떻게 구성이 되는지 상상하면서 확인 하시면 더욱 도움 될 것 같습니다.

 

3-1. 버튼 상태 3가지

가장 먼저 버튼 상태 3가지를 나타낼 enum을 작성합니다.
여기까지는 LayoutDriven UI뿐만 아니라 다양한 컴포넌트를 작성할 때 많이 사용하고 계실 것 같습니다.

public enum BoxButtonStatus {
    case active    /// 활성화 상태
    case inactive  /// 비활성화 상태
    case pressed   /// 눌린 상태
}
public class BoxButton: UIButton {
    public var btnStatus: BoxButtonStatus = .inactive {
        didSet { setNeedsLayout() }
    }
    
    public init(frame: CGRect, btnStatus: BoxButtonStatus) {
        self.btnStatus = btnStatus
        super.init(frame: frame)
    }
}

기본적으로 btnStatus는 비활성화 상태로 세팅합니다.
활성화 또는 눌린 상태는 최초 init 함수에서 바깥에서 주입할 수 있도록 하면 됩니다.

 

3-2. highlighted / userinteractionenabled 체크

실제로 버튼이 눌렸는지 UI 컴포넌트를 사용하는 곳에서 userinteractionenabled를 false로 바뀐 것을 어떻게 감지할 수 있을까요?

아래 코드에서 setNeedsLayout에 집중해주세요

public class BoxButton: UIButton {
    public var btnStatus: BoxButtonStatus = .inactive {
        didSet { setNeedsLayout() }
    }

    public override var isHighlighted: Bool {
        didSet { setNeedsLayout() }
    }
    
    public override var isUserInteractionEnabled: Bool {
        didSet { setNeedsLayout() }
    }
    
    public override func layoutSubviews() {
    	super.layoutSubviews()
        // 비동기적으로 layoutSubviews가 호출됨.
    }
}

 

btnStatus와 isHighlighted, isUserInteractionEnabled 변수가 세팅될 때마다 setNeedsLayout()이 호출됩니다.
setNeedsLayout은 비동기적으로 layoutSubviews()를 호출하게 되는데요.

 

즉 해당 변수가 세팅될 때마다, UI를 업데이트하게 됩니다.
각 상태에 맞게 그림이 그려질 수 있도록 layoutSubviews() 함수에서 처리해주면,
UI 컴포넌트는 자신의 상태가 바뀔 때마다 상태 변화를 감지하고 변화하게 됩니다.

 

 

3-3. layoutSubviews()

코드 길이가 길어져 isUserInteractionEnabled에 대한 처리는 생략하고 나타냈습니다.

아래처럼 layoutSubviews가 호출되고 본인의 상태 값에 따라서 비동기적으로 UI가 다시 그려지는 것을 확인할 수 있습니다.

    public override func layoutSubviews() {
        super.layoutSubviews()
        
        // 버튼의 상태마다 컬러나 디자인 등을 변화 시킴
        switch btnStatus {
        case .active:
            btnLabel.textColor = Colors.grey.g800
            backgroundColor = Colors.tint.sub.n400

        case .inactive:
            backgroundColor = Colors.grey.g700
            btnLabel.textColor = Colors.grey.g500
            
        case .pressed:
            btnLabel.textColor = Colors.grey.g800
            backgroundColor = Colors.tint.sub.n600
        }
        
        // isHighlighted가 되었을 경우 컬러 변경 
        switch isHighlighted {
        case true:
            backgroundColor = Colors.tint.sub.n600

        case false:
            backgroundColor = Colors.tint.sub.n400
        }
    }

 

 

3-4. 성능 개선

"어 이거.. 변수 하나만 바꼈는데 모든 UI를 다 업데이트하는데 성능에 영향이 있지 않을까?"

저도 처음 LayoutDriven UI를 구성할 때 위와 같은 고민을 한 적이 있습니다.

대부분의 UI 컴포넌트는 매우 작은 단위로 구성되기 때문에,
작은 컴포넌트의 UI 업데이트가 성능에 영향을 주는 부분보다 얻는 득이 더 많아 무시할 수 있는 수준이라고 생각했습니다.

 

다만, 버튼/뱃지와 같은 작은 컴포넌트가 아닌 많은 정보를 담고 변경해야 하는 복잡한 컴포넌트같은 경우에는
분명 성능상 문제가 있을 것처럼 보입니다.
만약 약 50개의 UI 변수가 존재하는데, 1개의 변수만 변경되었는데 변경되지 않는 49개의 UI도 다시 그리게 되니까요.

 

이전 상태와 현재 상태를 비교해서 변함이 없다면 유지하는 예외 코드를 넣거나,
UI 업데이트가 필요한 변수에 setNeedsLayout() 을 모두 넣지 않고,
각 개별 UI 업데이트가 필요한 별도의 layoutSubviews() 역할을 하는 함수를 만들고
메인 쓰레드에서 동작하게 하면 비슷하게 흉내낼 수 있습니다.

 

컴포넌트의 역할이 비대해져 하나에서 모두 해결할 수 없다거나
협업을 위한 코드 가독성 개선 방법 중 하나로 위처럼 해결할 수 있습니다.

 

 


 

 

4. 간단하지만 강력한 친구

현업에서 UI 작업을 하게 될 때 가장 많이 고민하는 것은 UI를 메인 쓰레드에서 효율적으로 호출하게 하는 방법입니다.

API 로직은 최대한 백그라운드에서 실행될 수 있도록 하고,
최종적으로 UI반영만 필요할 때 메인 쓰레드로 변경하고자 하는 것인데요.

 

최초에는 올바르게 구현했을지라도, 이후에 다른 개발자가 API나 UI 로직을 수정하게 되면서
백그라운드 쓰레드로 실행된 API가 UI를 직접 세팅하게 되면 앱이 죽게 됩니다 ☠️

 

이러한 상황을 미연에 방지하고자, LayoutDriven UI를 활용한다면
서버 응답에 따라 비동기적으로 UI를 세팅해야 할 경우 개발자의 실수로 백그라운드 쓰레드에서 동작되어

앱이 Crash 되거나 UI 업데이트가 지연되는 현상을 방지할 수 있습니다.

반응형
저작자표시 비영리 (새창열림)

'iOS > UI' 카테고리의 다른 글

[iOS] SnapKit 사용해보기  (0) 2021.05.08
    'iOS/UI' 카테고리의 다른 글
    • [iOS] SnapKit 사용해보기
    dev-signer
    dev-signer

    티스토리툴바