[iOS] 기기 회전 전 / 후 감지 + SwiftUI에서 UIViewController LifeCycle

유인호·2025년 1월 12일
0

iOS

목록 보기
79/81

0. 서론

회사일이 적은듯 많은듯 알 수 없다. 버그가 많은듯 적은듯 알 수 없다. 이게 지금 우리 회사 iOS 프로젝트의 가장 큰 문제점이라고 본다. 정확하게는 QA를 셀프로 하기 때문에 버그를 찾을 수 없다.

이번에 생긴 버그는 기기 회전시 앱이 터져 버린다는 것.

유니티 관련 화면에서 이 문제가 발생한다. 가로모드를 왔다 갔다 하면 어쩌다 한번 유니티 화면의 Frame이 0이되어 터져버림.

그러나 나는 이 버그를 이미 알고있었음. 왜냐하면 엑코에 디버깅 연결해두고 할떄만 이 버그가 발생했기 때문이고, 엑코 연결을 끄고 테스트 하면 단 한번도 터진적이 없었기 떄문이다.

적어도 그런줄 알았다. 이사님이 가로모드할때 앱이 꺼진다고 전화가 오기 전까진...

iOS 18 아이폰 15, iOS 16 아이폰 8에서 단 한번도 터진적이 없었는데, 다른 기종에선 문제가 생긴다는 것.

문제 해결이 필요했음.

1. Unity 화면 자체를 Hidden

가로 전환을 하기 전에 Hidden 시키고, 전환이 완료되면 다시 Unity를 켜주면 되지 않을까?

그러기 위해서 전환하기 '전' 호출되는 API를 찾아야하고, 전환 '후' 호출되는 API를 찾았어야 했다. 귀찮으니 GPT의 도움을 받아서 여러 예제를 작성해보다가 이 예제가 제일 나은 것 같아 이렇게 한번 만들어보고자 했음.

class ViewController: UIViewController {
    
    // 🔥 상태를 표시할 UILabel 생성
    private let statusLabel: UILabel = {
        let label = UILabel()
        label.text = "준비 완료"  // 초기 텍스트
        label.font = UIFont.systemFont(ofSize: 24, weight: .bold)
        label.textColor = .white
        label.textAlignment = .center
        label.translatesAutoresizingMaskIntoConstraints = false
        label.numberOfLines = 0
        return label
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 🔥 배경색 추가 (가시성을 위해)
        view.backgroundColor = .black
        
        // 🔥 UILabel 추가 및 중앙 배치
        view.addSubview(statusLabel)
        NSLayoutConstraint.activate([
            statusLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            statusLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
    
    // 🔥 화면 회전 감지 및 상태 업데이트
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        
        // 🔥 회전 시작 직전
        updateStatus("📱 화면 회전 시작 직전")
        
        coordinator.animate(alongsideTransition: { _ in
            // 🔄 회전 중...
            self.updateStatus("🔄 화면 회전 중...")
        }) { _ in
            // ✅ 회전 완료
            self.updateStatus("✅ 화면 회전 완료")
        }
    }
    
    // 🔥 상태 업데이트 함수
    private func updateStatus(_ text: String) {
        DispatchQueue.main.async {
            self.statusLabel.text! += "\n\(text)"
        }
    }
}

의도한대로 잘된다.

이걸 스유 뷰에 붙여야 하는데, 이 방법은 이미 내가 플젝에 어느정도 만들어둔게 있었다. 정확히는 어디 티스토리 블로그에서 만든거 그대로 가져온건데, 여기에 붙여보기로 했음.


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

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

enum LifeCycle {
    case viewDidLoad
    case viewWillAppaer
    case viewDidAppear
    case viewWillDisappear
    case viewDidDisappear
    case willTransition
    case transitioning
    case didTransition
}

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

final class LifeCycleController: UIViewController {
    
    private weak var handler: LifeCycleHandlerProtocol?
    
    init(handler: LifeCycleHandlerProtocol) {
        self.handler = handler
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    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)
    }
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        
        // 회전 시작 직전
        handler?.lifeCycle.send(.willTransition)
        
        coordinator.animate(alongsideTransition: { _ in
            self.handler?.lifeCycle.send(.transitioning)
        }) { _ in
            self.handler?.lifeCycle.send(.didTransition)
        }
    }
    
    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) { }
    }
}

요롬코롬 만들어서 SwiftUI의 ViewModel에서 ViewController의 LifeCycle과 viewWillTransition API를 통해 원하는 기능을 구현시킬 수 있었다.

profile
🍎Apple Developer Academy @ POSTECH 2nd, 🌱SeSAC iOS 4th

0개의 댓글

관련 채용 정보