Do.

SwiftUI - SwiftUI 환경에서 ViewController Life Cycle 사용하기 본문

iOS

SwiftUI - SwiftUI 환경에서 ViewController Life Cycle 사용하기

Hey_Hen 2022. 10. 26. 02:46

Intro

SwiftUI 프레임워크로 넘어오면서, 가장 불편했던 것 중 하나는 기존 UIViewController가 제공했던 ViewController Life Cycle이 SwiftUI View에는 없다는 것입니다.
View Modifier에도 onAppear와 onDisappear가 존재하지만 기존 LifeCycle을 대체하기에는 부정확한 동작이 많았죠

특히나 didAppear와 didDisappear의 빈자리가 비교적 크게 느껴집니다.

SwiftUI를 프로젝트에 적용하는 방법중에는 UIKit을 기반으로 HostingController를 활용하는 법과
순수 SwiftUI 베이스로 시작하는 방법이 있는데요!

이번에 설명드릴 방법은 SwiftUI 베이스에서 UIViewController의 Life Cycle 사용하는 방법입니다.

Concept

우선 컨셉부터 보여드리면, 위 그림과 같이 View가 ViewModel에 의존해서 Presentation Logic을 처리합니다. 이때 ViewController도 동시에 존재하는데요, ViewController 또한 View가 가진 동일한 ViewModel에 의존합니다. 그리고 ViewController에서는 LifeCycle을 ViewModel로 보내면, ViewModel은 그에 따른 동작을 수행해서 상태를 View로 방출하면 됩니다.

UIKit환경과는 달리 View Controller가 많은 일을 담당하지 않습니다. 단지 Life Cycle 신호를 ViewModel로 보낼 뿐이죠

Implemenation

구현은 어떻게 하면 될까요?
우선 LifeCycleController를 선언하겠습니다.

enum LifeCycle {
  case viewDidLoad
  case viewWillAppaer
  case viewDidAppear
  case viewWillDisappear
  case viewDidDisappear
}

protocol LifeCycleHandlerProtocol: AnyObject {
  var lifeCycle: PassthroughSubject<LifeCycle, Never> { get }
}

final class LifeCycleController: NiblessViewController {
  
  private weak var handler: LifeCycleHandlerProtocol?
  
  init(handler: LifeCycleHandlerProtocol) {
    self.handler = handler
    super.init(nibName: nil, bundle: nil)
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()
    handler?.lifeCycle.send(.viewDidLoad)
  }
  
  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    handler?.lifeCycle.send(.viewWillAppaer)
  }
  
  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    handler?.lifeCycle.send(.viewDidAppear)
  }
  
  override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    handler?.lifeCycle.send(.viewWillDisappear)
  }
  
  override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    handler?.lifeCycle.send(.viewDidDisappear)
  }
  
  deinit {
    print(self, #function)
  }
  
  struct Representable: UIViewControllerRepresentable {
    typealias UIViewControllerType = LifeCycleController
    private let handler: LifeCycleHandlerProtocol
    
    init(handler: LifeCycleHandlerProtocol) {
      self.handler = handler
    }
    
    func makeUIViewController(context: Context) -> LifeCycleController {
      LifeCycleController(handler: handler)
    }
    
    func updateUIViewController(_ uiViewController: LifeCycleController, context: Context) {
      
    }
  }
}

LifeCycleController의 역할은 Handler에게 액션을 보내주기만 하면 되는 형태입니다.
지금은 Combine을 써서 넘겨주고 있는데 어떤 방식이든 크게 상관없습니다.

그 다음은 ViewModel 입니다. 앞에서 컨셉에서 설명드렸듯 ViewModel은 LifeCycle을 핸들링 하면 됩니다.
기존 ViewModel이 LifeCycleHandlerProtocol을 채용합니다.

extension ContentView {
  final class ViewModel: ObservableObject, LifeCycleHandlerProtocol {

    private var cancellables: Set<AnyCancellable> = []
    let lifeCycle = PassthroughSubject<LifeCycle, Never>()
    
    init() {
      bind()
    }
    
    private func bind() {
      lifeCycle
        .sink(receiveValue: { [weak self] lifeCylce in
          self?.lifeCycleHandling(lifeCylce)
        })
        .store(in: &cancellables)
    }
    
    private func lifeCycleHandling(_ lifeCycle: LifeCycle) {
      print(lifeCycle)
      switch lifeCycle {
        case .viewDidLoad:
          return
        case .viewWillAppaer:
          return
        case .viewDidAppear:
          return
        case .viewWillDisappear:
          return
        case .viewDidDisappear:
          return
      }
    }
        
    deinit {
      print(self, #function)
    }
  }
}

이런 식으로요, 지금은 블럭들이 비어있지만 실제로 사용할 때 알맞는 코드를 넣어주면 되겠죠?
그리고 이를 화면에 배치해야 비로서 ViewController의 LifeCycle 이 시작되는데요, View에 어떻게 배치하면 좋을까요?

Modifier를 이용하겠습니다.

struct LifeCycleModifier: ViewModifier {
  let handler: LifeCycleHandlerProtocol
  func body(content: Content) -> some View {
    content
      .overlay(
        LifeCycleController.Representable(handler: handler)
        	.frame(width: .zero, height, .zero)
      )
  }
}

extension View {
  func lifeCycle(handler: LifeCycleHandlerProtocol) -> some View {
    modifier(LifeCycleModifier(handler: handler))
  }
}

Modifier로 content에 overlay로 작성하게 되면 화면 배치에 영향도 없고, 아무것도 보이지 않는 Empty View가 완성됩니다.
이제 View Extension으로 lifeCycle을 호출한 다음 handler만 전달해주면 되죠!

View Content에서는 아래처럼 선언하면 이제 완성됩니다.

struct ContentView: View {
  @ObservedObject private var viewModel: ViewModel
  var body: some View {
    VStack(spacing: 0) {
      Image(systemName: "globe")
        .imageScale(.large)
        .foregroundColor(.accentColor)
      Text("Hello, world!")
    }
    .padding()
    .lifeCycle(handler: viewModel)
  }
  
  init(viewModel: ViewModel) {
    self.viewModel = viewModel
  }
}

Conclusion

저는 SwiftUI 베이스 보다는 UIKit 베이스로 SwiftUI를 더 많이 작성하긴 하는데
순수하게 해당 뷰의 ViewLifeCycle만 받고, 재사용 가능한 코드로 구성할때는 이 방법이 깔끔하고 좋은 것 같습니다.
애플에서는 onAppear왜 onDisappear 그리고 ObservableObject가 뷰의 생명주기를 컨트롤한다고 얘기하지만, 정말 단순 명료하게 VC에서 제공하던 LifeCycle이 필요할 때가 정말 많아서 생각하게 된 방법입니다!

Sample Proejct.

https://github.com/urijan44/SwiftUIViewLifeCycle

Comments