이는 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는 언제나 데이터를 기준으로 UI를 그리기 떄문에 위 같은 문제가 발생하지 않습니다.
이를 UIKit 에서 흉내낼 수 있는 방법이 바로 Layout Driven UI입니다.
WWDC 2018 Adding Delights to Your iOS App 에서 소개가 되었습니다.
이의 개념을 살펴보면
didSet
에 setNeedsLayout
을 겁니다.setNeedsLayout
은 비동기적으로 layoutSubView
를 호출합니다.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의 핵심입니다.
layoutSubViews
안에서 didSet
으로 관리되는 변수들을 업데이트하면 안됩니다. 그렇게 하면 무한루프에 빠지기 때문입니다.(setNeedsLayout
-> layoutSubViews
-> setNeedsLayout
-> ...)
객체를 생성하는 코드를 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의 방식으로 사용하는 방법에 대해 알게되어 신기하였고, 이러한 방법이 나오게 된 배경? 을 알게되어서 좋았다. 문제를 해결하는 방식에 있어 접근하는 방법이 매우 여러가지이기 때문에 알아두는게 좋다고 생각한다.
참조