일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
Tags
- OS
- World
- Swift
- 커스텀 뷰
- SwiftUI
- Opensource
- 후기
- 알고리즘
- Cocoa Internals
- ios #swift #uialertcontroller #메서드 스위즐링
- Design Pattern
- Tistory
- 부스트캠프
- 부트캠프
- 개발
- 단위 테스트
- boostcamp
- 디자인패턴
- development
- notion
- 코코아 인터널스
- rxswift
- WWDC
- IOS
- Hello
- Algorithm
- 네이버 부캠
Archives
- Today
- Total
꿈돌이랜드
메서드 스위즐링을 적용하여 실수로부터 벗어나기 본문
반응형
메서드 스위즐링이란?
- 메서드 스위즐링은 런타임에 함수의 구현부를 뒤섞는 방법을 말합니다.
- 구현 방법은 보통 다음과 같이 이뤄집니다.
- 보통
class_getInstanceMethod
나class_getClassMethod
와 같은 방법을 사용해서 각 메서드의 셀렉터를 가져오고 - 각각을
method_exchangeImplementation
으로 뒤바꾸어 구현을 바꾸어서 구현, appdelegate 같은 곳에서 swizzle을 한번 실행시켜줍니다. - 단, 각 메서드는 @objc 런타임에 노출되어야 하고, dynamic으로 마킹되어있어야 함
- extension에 정의된 경우는 자동으로 dynamic처리가 된 것으로 칩니다.
- 보통
import UIKit
fileprivate var swizzleEnabled = false
extension UIViewController {
static let methodSwizzled: Void = {
guard !swizzleEnabled else { return }
swizzleEnabled = true
// #selector: swift의 메소드나 함수를 참조하는 방법 (Selector 타입)
let originalSelector = #selector(viewWillAppear(_:))
// class_getInstanceMethod: Objecitve-C 런타임 라이브러리에서 제공하며, 메소드를 검색하는데 사용
let originalMethod = class_getInstanceMethod(UIViewController.self, originalSelector)
let swizzledSelector = #selector(swizzledViewWillAppear(_:))
let swizzledMethod = class_getInstanceMethod(UIViewController.self, swizzledSelector)
guard let originalMethod, let swizzledMethod else { return }
method_exchangeImplementations(originalMethod, swizzledMethod)
}()
@objc private func swizzledViewWillAppear(_ animated: Bool) {
// Swizzling된 메서드에서 추가적인 동작 수행
print("Swizzled viewWillAppear called for \(self)")
}
}
- 메서드 스위즐링의 단점
- 다른 프레임워크나 라이브러리를 가져와 사용할때 이미 스위즐링 되어있는 메서드를 또 스위즐링 하고 있지는 않은지 확인해야 합니다.
- 새로이 iOS 버전업 되면 스위즐링이 실패할 가능성이 있습니다.
- 이에 예상치 못한 버그를 발생시킬 수 있습니다.
- 이미 짜여진 standard 메서드가 아닌 본인이 작성한 메서드가 호출되는지 확인해야합니다.
적용 사례
- 현재 사내 앱은 아이패드를 지원하고 있는데, 아이패드에서 UIAlertController나, UIActivityController 사용시, 반드시 popoverPresentationController를 적용해줘야 하는 문제가 있습니다.
- Apple 개발자 포럼에서 이 문제가 여러 번 언급되었습니다. 특히 UIAlertController를 iPad에서 사용할 때 popoverPresentationController 구성은 필수적이며, 구성하지 않으면 다음과 같은 크래시 메시지가 발생할 수 있습니다
- 이러한 크래시를 방지하기 위해 iPad에서 액션 시트를 표시할 때는 항상 popoverPresentationController의 sourceView 및 sourceRect 또는 barButtonItem 속성을 설정해야 합니다.
- 그렇지 않으면 다음과 같은 오류가 발생합니다.
Terminating app due to uncaught exception 'NSGenericException', reason: 'Your application has presented a UIAlertController (<UIAlertController: ...>) of style UIAlertControllerStyleActionSheet. The modalPresentationStyle of a UIAlertController with this style is UIModalPresentationPopover. You must provide location information for this popover through the alert controller's popoverPresentationController. You must provide either a sourceView and sourceRect or a barButtonItem. If this information is not known when you present the alert controller, you may provide it in the UIPopoverPresentationControllerDelegate method -prepareForPopoverPresentation.'
- 그래서 초기에 다음과 같이 UIAlertController 사용부 마다 ensureActionSheet 라는 메서드를 만들어 적용시켜주어야 했습니다.
extension UIAlertController {
func ensureActionSheet(_ viewController: UIViewController) -> UIAlertController {
if UIDevice.current.userInterfaceIdiom != .pad { // 디바이스 타입이 iPad가 아닐때
return self
}
guard let popover = self.popoverPresentationController else {
return self
}
popover.sourceView = viewController.view
popover.sourceRect = CGRect(x: viewController.view.bounds.midX, y: viewController.view.bounds.midY, width: 0, height: 0)
popover.permittedArrowDirections = []
return self
}
}
// 사용법
let alert = UIAlertController(title: "알림", message: "알림창입니다.", preferredStyle: .alert).ensureActionSheet(self)
- 하지만 실수로
.ensureActionSheet(self)
호출을 누락하기라도 하면 크래시가 나는 단점이 있었습니다. - 그래서 이 부분에 메서드 스위즐링을 적용하면 좋겠다는 생각이 났고 다음과 같이 적용하게 되었습니다.
import UIKit
extension UIAlertController {
// 앱 시작 시 한 번만 호출되는 메서드
static let swizzleAlertController: Void = {
let originalSelector = #selector(UIAlertController.init(title:message:preferredStyle:))
let swizzledSelector = #selector(UIAlertController.swizzled_init(title:message:preferredStyle:))
guard let originalMethod = class_getInstanceMethod(UIAlertController.self, originalSelector),
let swizzledMethod = class_getInstanceMethod(UIAlertController.self, swizzledSelector) else {
return
}
method_exchangeImplementations(originalMethod, swizzledMethod)
}()
// UIAlertController 초기화 시 자동으로 iPadOS에서 액션 시트 설정을 보장하는 메서드
@objc private func swizzled_init(title: String?, message: String?, preferredStyle: UIAlertController.Style) -> UIAlertController {
// 원래 초기화 메서드 호출 (swizzle로 인해 실제로는 원래 구현을 호출)
let controller = self.swizzled_init(title: title, message: message, preferredStyle: preferredStyle)
// preferredStyle이 actionSheet일 경우에만 적용
if preferredStyle == .actionSheet && UIDevice.current.userInterfaceIdiom == .pad {
if let popover = controller.popoverPresentationController {
// 현재 키 윈도우에서 최상위 뷰 컨트롤러 가져오기
if let viewController = UIApplication.shared.keyWindow?.rootViewController?.topMostViewController() {
popover.sourceView = viewController.view
popover.sourceRect = CGRect(x: viewController.view.bounds.midX,
y: viewController.view.bounds.midY,
width: 0,
height: 0)
popover.permittedArrowDirections = []
}
}
}
return controller
}
}
// 최상위 뷰 컨트롤러를 가져오기 위한 확장
extension UIViewController {
func topMostViewController() -> UIViewController {
if let presented = self.presentedViewController {
return presented.topMostViewController()
}
if let tabBarController = self as? UITabBarController,
let selected = tabBarController.selectedViewController {
return selected.topMostViewController()
}
if let navigationController = self as? UINavigationController,
let visibleViewController = navigationController.visibleViewController {
return visibleViewController.topMostViewController()
}
return self
}
}
// 앱 시작 시 호출되어 swizzling 수행
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Method swizzling 실행
_ = UIAlertController.swizzleAlertController
return true
}
}
- 이를 통해, 사용처에서 특별한 처리 없이 메서드 스위즐링을 통해 크래시를 방지할 수 있게 되었습니다. 😊
반응형
'Programming > iOS' 카테고리의 다른 글
TCA 아키텍처에서 IdentifiedArray를 사용해야 하는 이유 (0) | 2025.03.21 |
---|---|
[번역] SwiftNIO Readme Conceptual Overview (0) | 2024.01.16 |
[iOS] Core Animation Basic (0) | 2023.09.30 |
[iOS] 객체 아카이빙과 iOS 12.0 이후의 변화 (0) | 2023.09.26 |
코코아 인터널스 - 9장 스위프트 타입 시스템 (0) | 2023.08.16 |