SwiftUI 방식으로 UIKit 코드를 짤 수 있다면?

12277 단어 swiftiOSiOS

데이터 의존성 관리

프론트 개발에서 신경 써야 할 부분 중 하나는 데이터의 변화를 제때에 보여주는 것이다.
그런 의미에서 가장 빈번하면서도 크리티컬한 UI 버그는 앱의 화면이 데이터의 최신상태를 보여주지 못하는 것이다.

네트워크에서 로딩이 끝난 후에도 로딩 인디케이터가 계속 돌아가고 있거나 사용자의 입력값을 바로 업데이트 하지 않을 때 사용자들은 불편함을 느끼게 되고 이런 경험이 쌓여 서비스 이탈로 이어지는 것이다.

그렇다면 이러한 버그가 나타나는 이유는 무엇일까? 근본적인 이유는 데이터가 바뀐다고 해서 UI가 자동으로 업데이트 되는 것이 아니기 때문이다.
즉, 데이터가 바뀌었을 때를 캐치해서 이를 UI에 하나씩 반영해야 하는데 이 중 하나라도 빼먹게 되면 앞서 말한 버그가 나타나게 되는 것이다.

그리고 개발자가 의도한 순서와 다르게 코드가 실행된다면 UI 버그가 발생하게 된다.

즉, UI 프로그래머는 데이터를 일일이 관리해야 할 뿐만 아니라 순서에 맞게 관리해야 한다.

그러나 이러한 현상이 SwiftUI에서는 발생하지 않는다. 이를그 이유는 SwiftUI 프레임워크의 경우 언제나 Data를 기준으로, Data가 바뀌었을 때마다 UI를 그리기 때문이다.

SwiftUI

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의 새로운 인스턴스를 만들고 이 인스턴스를 기준으로 화면을 새로 랜더링 한다.
즉, 데이터가 바뀔 때마다 그것을 기준으로 화면을 그리기 때문에 정확한 데이터가 화면에 표시될 수 밖에 없다.

LayoutDriven UI

그렇다면 이와 같은 방식을 UIKit에서는 구현할 수 없을까?
이를 해결할 수 있는 방식이 바로 LayoutDrivenUI이다.
(이 방법은 wwdc2018에서 소개되었다.)

What is LayoutDriven UI

LayoutDrivenUI 개념을 정리하면 크게 세가지 단계로 나눌 수 있다.

  1. UIView에서 값이 변하는 모든 데이터 관련 변수들의 didSetsetNeedsLayout함수를 설정한다.
  2. setNeedsLayout은 비동기적으로 layoutSubview를 호출한다.
  3. 해당 View를 최신화하는 코드를 모두 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)
    }

}

앞서 말한 개념을 바탕으로 위와 같은 코드를 작성할 수 있다.
layoutSubview가 불리는 순간, 표현해야 하는 Data들을 기준으로 UIView를 최신화하는 것이 핵심이다.

CardView의 text가 바뀌든, fontSize가 바뀌든 그 순서에 상관 없이 layoutSubview 안에서는 똑같은 순서로 최신화가 일어나므로 언제나 일관된 순서로 UIView가 갱신됩니다.

Wow Point Of LayoutDriven UI

class ViewController: UIViewController {

    @IBOutlet var cardView: CardView!
    @IBOutlet var slider: UISlider!

    @IBAction func valueChanged(_ sender: Any) {
        cardView.text = "\(slider.value)"
        cardView.fontSize = CGFloat(slider.value * 36)
    }

}

해당 방식을 사용하면 애니메이션을 쉽게 구현할 수 있다.
적절한 주기에 데이터가 업데이트가 되므로 자연스러운 변화가 가능하기 때문이다. 이에 더해, animate options 중 beginFromCurrentState를 활용해 데이터 업데이트에 대한 유려한 애니메이션 효과도 구현 가능하다.

주의할 사항

  1. layoutSubviews 안에서 didSet으로 관리되는 변수들을 업데이트 하면 안된다.
    이 함수 안에서 변수들의 값을 업데이트 하게 되면, setNeedsLayout()을 호출하게 되고 다시 layoutSubviews를 부르게 되어 무한루프에 빠지게 된다.

  2. 객체를 생성하는 코드를 layoutSubviews에 넣으면 안된다. 이 함수는 화면이 갱신 될 때마다 불리는 함수이기에 그 때마다 새로운 객체가 만들어지면 메모리 스택이 많이 쌓이게 되어 예상치 못한 버그를 만날 확률이 높다.
    -> 그러므로 해당 함수에서는 가급적 기존에 만들어 두었던 객체들에 대한 업데이트를 해야 한다.
    즉, 초기화 코드와 최신화 코드를 확실하게 분리하는 것이 중요하다.
    (한가지 좋은 예시는 BaseView와 같은 뼈대를 두고 이를 UIView 서브클래스들이 상속 받게 하는 방법이 있다.)

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

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

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

class MyView: BaseView {

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

좋은 웹페이지 즐겨찾기