iOS 13 에서 변경된 UIModalPresentationStyle 해킹하기

스타일쉐어(StyleShare)

iOS 13에는 많은 변화가 있었습니다. 그 중 가장 개발자들을 당황스럽게 만든 변화는 UIViewController.modalPresentationStyle의 기본 값이 변경된 것입니다. 기존에는 present(_:animated:completion:) 메서드를 사용하면 뷰 컨트롤러가 전체 화면으로 표시되었습니다. 하지만 iOS 13부터는 모달 형태로 표시되면서 기존에 작성한 많은 인터페이스가 망가지게 됩니다.

모두를 당황시킨 iOS 13 (오른쪽이 정상)

이를 바로잡기 위해서는 UIViewController.modalPresentationStyle 속성을 기본값인 .automatic 이 아니라 .fullScreen 으로 변경해주어야 합니다. 네비게이션 컨트롤러 없이 뷰 컨트롤러를 표시하는 경우에는 프로젝트에서 사용하는 BaseViewController의 기본 속성을 바꿔주는 간단한 방법이 있습니다. 하지만 네비게이션 컨트롤러를 사용해서 뷰 컨트롤러를 띄우는 경우에는 모든 코드에 아래와 같은 변경을 추가해주어야 합니다.

let navigationController = UINavigationController(...)
navigationController.modalPresentationStyle = .fullScreen
self.present(navigationController, animated: true, completion: nil)

모든 코드를 찾기 어려울 뿐더러, 분명히 미래에 실수할 여지가 있습니다. 따라서 한 번에 모든 곳의 기본값을 변경할 수 있는 방법이 필요합니다. StyleShare에서는 이에 대응하기 위해 세 가지 방법을 고민해보았습니다.

1. Method Swizzling

UINavigationController 런타임을 조작하여 modalPresentationStyle의 값을 .fullScreen으로 바꿔치기 하는 방법입니다. 잘만 사용하면 굉장히 유용하지만 클래스의 기본 구현을 바꿔버리기 때문에 상당히 위험한 기술입니다. 의도하지 않게 시스템 프레임워크나 서드파티 프레임워크의 구현을 망가뜨릴 수 있어 채택하지 않았습니다.

2. 커스텀 UINavigationController 작성

modalPresentationStyle 기본 값을 .fullScreen 으로 설정한 MyFullScreenNavigationController를 작성하는 방법입니다. UINavigationController를 사용하는 곳을 모두 이 클래스로 치환해야 합니다. 가장 정직한 방법이지만, 너무나 많은 코드 변경이 생기게 됩니다. 또한 미래에 이 클래스를 사용하지 않고 UINavigationController를 직접 사용하게 되는 실수가 생길 수 있어 채택하지 않았습니다.

3. 스코프(Scope) 우선순위 해킹하기

Swift는 다른 언어와 마찬가지로 스코프 우선 순위가 있습니다. 가장 개발자의 의도와 가까운 곳부터 먼 순서대로 우선 순위가 정해집니다. 대표적인 예시는 아래와 같습니다. 같은 이름을 가진 변수를 전역 스코프, 클래스 멤버 스코프, 그리고 로컬 스코프에 정의하면 참조하는 곳과 가장 가까운 로컬 변수가 가장 높은 우선 순위를 가집니다.

let foo: String = 'A'
class Hello {
  let foo: String = 'B'
  func world() {
    let foo: String = 'C'
    print(foo) // 'C'
  }
}

변수 뿐만 아니라 타입을 정의할 때도 마찬가지입니다.

import struct ThirdParty.User
struct User {
  let name = 'B'
  func hello() {
    struct User {
      let name = 'C'
    }
    let user = User()
    print(user.name) // 'C'
  }
}

이를 이용해서 UINavigationController의 네임스페이스 우선순위를 해킹할 수 있습니다. 기존의 UINavigationController들은 임포트한 UIKit 네임스페이스를 가집니다. 타입 이름이 같다면 같은 모듈의 클래스가 더 높은 스코프 우선순위를 가지게 됩니다. 즉, 프로젝트에 또다른 UINavigationController를 정의하면 모든 UINavigationController에 대한 참조가 UIKit.UINavigationController 에서 MyApp.UINavigationController로 바뀌게 됩니다.

import UIKit
class UINavigationController: UIKit.UINavigationController {
}
let navigationController: UINavigationController // MyApp.UINavigationController

그리고 이 클래스 생성자에서 modalPresentationStyle의 값을 .fullScreen으로 바꿔주기만 하면 됩니다.

UINavigationController라고만 네이밍하면 의도가 드러나지 않을 수 있습니다. StyleShare에서는 의도를 명확히 드러내는 것을 중요하게 생각합니다. 따라서 _FullScreenModalNavigationController로 이름짓고, typealias를 사용해서 UINavigationController 네임스페이스를 덮어씌웠습니다. 아래는 최종 구현입니다.

import UIKit
typealias UINavigationController = _FullScreenModalNavigationController
class _FullScreenModalNavigationController: UIKit.UINavigationController {
  override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    self.modalPresentationStyle = .fullScreen
  }
  override init(rootViewController: UIViewController) {
    super.init(rootViewController: rootViewController)
    self.modalPresentationStyle = .fullScreen
  }
  convenience init() {
    self.init(nibName: nil, bundle: nil)
  }
  required init?(coder: NSCoder) {
    super.init(coder: coder)
    self.modalPresentationStyle = .fullScreen
  }
}

만약 UIKit의 UINavigationController를 사용할 필요가 있다면 네임스페이스를 명시해서 UIKit.UINavigationController 처럼 사용하면 됩니다.

기업문화 엿볼 때, 더팀스

로그인

/