cleanUrl: /programming/layout-driven-ui
가장 빈번하면서도 크리티컬한 UI버그는, UI가 “최신 상태”의 데이터를 제대로 표현하지 못하는 현상일 것입니다. 네트워크에서 로딩이 끝난 후에도 로딩인디케이터가 계속 돌아가고 있다던지, 좋아요를 눌렀는데 좋아요 숫자가 올라가지 않는다던지, 메시지를 다 읽었는데도 “메시지 안 읽음” 표시가 남아있다던지…
이런 종류의 버그들은 왜 계속해서 튀어나오는 것일까요? 근본적인 이유는, 데이터가 바뀐다고 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개의 경우의 수가 생깁니다. 5개가 되면 5!=120 개…. 이 시점부터는 인간의 두뇌로 관리 할 수 있는 영역이 아닙니다. 우리가 UI버그를 자주 만나게 되는 이유입니다.
Data가 바뀔 때 UI가 자동으로 업데이트 되지 않는 이런 문제가 SwiftUI에서는 아예 발생하지 않습니다. 어떻게 이게 가능한 걸까요? 간단합니다. SwiftUI 프레임워크는 언제나 Data를 기준으로, 또 Data가 바뀔 때마다 UI를 그리기 때문입니다!
struct SwiftUIView: View {
// @State로 표시된 데이터가 바뀌면, SwiftUI가 뷰를 다시 그립니다.
@State var text:String = "Hello World"
@State var isLoading:Bool = false
var body: some View {
ZStack {
Text(text)
if isLoading {
LoadingIndicator()
}
}
}
}
SwiftUI프레임워크는 @State 로 표시된 Data들을 계속 감시하고 있다가, 변경사항이 발생하면 그 즉시 View struct의 새 인스턴스를 만들고, 이 인스턴스를 기준으로 화면을 새로 렌더링 합니다. Data가 바뀔 때마다 Data를 기준으로 화면을 그리니, 언제나 정확한 Data가 화면에 표시 될 수 밖에 없습니다.
이 처럼 SwiftUI를 쓰면 우리를 괴롭혔던 수많은 버그들과 이별 할 수 있습니다. 문제는 SwiftUI는 iOS13이상 부터 쓸 수 있고, 대부분의 앱은 iOS13을 아직 지원하지 않는다는 사실이죠. 우리는 그렇다면 iOS 13을 지원 할 때까지, 계속 기존의 버그들과 함께 살아야 하는 것일까요? 그렇지는 않습니다. 비록 SwiftUI를 프로덕션에서 바로 쓰지는 못해도, SwiftUI의 문제 해결 방식은 얼마든지 UIKit에서 흉내 낼 수 있기 때문입니다. 그 방법은 바로 WWDC2018의 Adding Delights to Your iOS App 에서도 소개되었던, LayoutDrivenUI 라는 방식입니다.
LayoutDrivenUI는 아주 간단한 개념입니다.
didSet
에 setNeedsLayout
을 겁니다setNeedsLayout
은 비동기적으로 layoutSubView
를 호출합니다.layoutSubView
안에서 호출되도록 합니다.끝입니다!코드로 보면 다음과 같습니다.
class CardView: UIView {
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)
}
}