[TIL] Layout Driven UI

rbw·2022년 9월 14일
0

TIL

목록 보기
44/98
post-thumbnail

Layout Driven UI

이는 UIKit 코드를 SwiftUI 같이 작성하는데 도움을 주는 방법이다.

보통 UI를 작성하고 생기는 버그 중 빈번한 문제는 데이터의 최신 상태를 표시하지 못하는 문제일 것이다. 이는 보통 데이터가 업데이트가 되었다고 UI가 업데이트 되는건 아니기 때문에 일어나는 문제이다.

더 어려운 상황으로는 업데이트 순서에 로직이 영향을 받는 경우 입니다.

extension ViewController {

    func viewDidAppear () {
        self.startkNetworkLoading()
    }

    func networkLoadingDidStart () {
        self.showLoadingIndicator()
    }

    func networkLoadingDidEnd() {
        self.removeLoadingIndicator()
    }

    func viewDidDisAppear() {
        self.cleanUpThings()}
}

이런 코드에서 우리는 viewDidAppear -> networkLoadingDidStart -> networkLoadingDidEnd -> viewDidDisAppear 의 순서대로 실행이 될 것이라 기대 하지만, 이 순서는 얼마든지 바뀔 수 있다.

예를 들어 네트워크 로딩이 끝나기 전에 유저가 뒤로가기 버튼을 누르면 networkLoadingDidEnd 보다 viewDidDisAppear가 먼저 불리게 됩니다. 그 결과로 self.removeLoadingIndicator가 불리지 않아 로딩 인디케이터가 사라지지 않는 버그가 발생합니다.

즉, UI프로그래머는 데이터를 관리할 뿐만 아니라 순서에 맞게 관리해야 합니다. 만약 데이터의 개수가 3개라면 업데이트 순서는 3! = 6 개의 경우의 수가 생기고, 4개라면 4! = 24 개의 경우의 수가 생깁니다.

SwiftUI의 해결 방법

SwiftUI는 언제나 데이터를 기준으로 UI를 그리기 떄문에 위 같은 문제가 발생하지 않습니다.

이를 UIKit 에서 흉내낼 수 있는 방법이 바로 Layout Driven UI입니다.

Layout Driven UI

WWDC 2018 Adding Delights to Your iOS App 에서 소개가 되었습니다.

이의 개념을 살펴보면

  1. UIView에 영향을 미치는 모든 데이터 관련 변수들의 didSetsetNeedsLayout을 겁니다.
  2. setNeedsLayout은 비동기적으로 layoutSubView를 호출합니다.
  3. 해당 View를 최신화 하는 코드를 모두 layoutSubview안에서 호출되도록 합니다.

코드로 살펴보면

var text:String = "" {
    didSet {
        // SwiftUI의 @State와 비슷합니다.
        setNeedsLayout()
    }
}
var fontSize: CGFloat = 14 {
    didSet {
        setNeedsLayout()
    }
}

@IBOutlet private var textLabel:UILabel!

override func layoutSubviews() {
    super.layoutSubviews()
    textLabel.text = text
    textLabel.font = textLabel.font.withSize(fontSize)
}

layoutSubView가 불리는 순간, 표현해야 하는 데이터들을 기준으로 UIView를 최신화 하는 것이 Layout Driven UI의 핵심입니다.

주의할 사항

  1. layoutSubViews안에서 didSet으로 관리되는 변수들을 업데이트하면 안됩니다. 그렇게 하면 무한루프에 빠지기 때문입니다.(setNeedsLayout -> layoutSubViews -> setNeedsLayout -> ...)

  2. 객체를 생성하는 코드를 layoutSubViews에 넣으면 안됩니다. 대표적으로 NSLayoutConstraints들을 생성하는 경우 입니다. 왜냐하면 layoutSubView는 그야말로 화면이 갱신 될 때마다 불리는데, 그 때마다 새로운 객체가 만들어지게 되면 커다란 메모리 족적을 남기게 될 뿐만 아니라 예상치 못한 버그들을 만나게 될 것입니다. layoutSubView에서는 가급적 "기존에 만들어 두었던 객체들에 대한 업데이트"를 해야 합니다.

따라서 초기화 코드와 최신화 코드를 확실하게 분리하는 것이 중요합니다. 예로, 아래의 BaseView를 모든 UIView 서브 클래스들이 상속받게 하는 방법이 있겠습니다.

class BaseView: UIView {
  init(frame: CGRect) {
    initializeLayout()
  }

  override func layoutSubViews() {
     super.layoutSubViews()
     updateLayout()
  }

  func initializeLayout() {}
  func initializeProperties() {}
  func updateLayout() {}
}

class MyView: BaseView {

  override func initializeLayout() {
     /// 레이아웃 초기화 코드
  }
  override func initializeProperties() {
     /// 그외 속성 초기화 코드
  }
  override func updateLayout() {
     /// 최신화 코드
  }
}

이런 방법이 나오게된 계기도 아마 React의 문제해결 방식에 많은 영향을 받은 것 같다고 하심. 그런 면에서 보았을 때 멋진 점은, "리액트의 방식으로 문제를 해결하기 위해 리액트 네이티브 같은 거대한 프레임워크를 들여올 필요는 없다"는 점이 멋진 것 같다고 하심.

문제 해결을 위해 "어떤 프레임 워크를 쓸까" 보다 이 문제를 그 프레임워크는 어떻게 해결했나 위주로 더 질문하고, 그 해결방식을 각자의 도메인에서 각자의 방식으로 구현하다보면 종종 우아하고 깔끔한 해결책이 나오기도 하는 것 같다고 하심.

UIKit을 SwiftUI의 방식으로 사용하는 방법에 대해 알게되어 신기하였고, 이러한 방법이 나오게 된 배경? 을 알게되어서 좋았다. 문제를 해결하는 방식에 있어 접근하는 방법이 매우 여러가지이기 때문에 알아두는게 좋다고 생각한다.


참조

https://www.sungdoo.dev/programming/layout-driven-ui

https://developer.apple.com/videos/play/wwdc2018/233

profile
hi there 👋

0개의 댓글