본문 바로가기
iOS

Method Swizzling in iOS

by SeoB-P 2022. 7. 10.

기업채용공고를 보다가 Method Swizzling에 대한 지식을 요구하는 곳이 있었다.
공고에 대놓고 써져있는 용어인데 정말 처음 들어서 한번 알아 보려한다.

Method Swizzling

Method swizzling is the process of changing the implementation of an existing selector. It’s a technique made possible by the fact that method invocations in Objective-C can be changed at runtime, by changing how selectors are mapped to underlying functions in a class’s dispatch table.  - nshipster

Method swizzling은 기존에 있던 Selector의 구현을 변경하는 process이다.

Objective-C의 메소드의 호출은 런타임에 변경 될수 있기 때문에 가능한 기술이며, Selector가 Class들의 Dispatch Table에 매핑되어 있는 함수들을 변경하는 방법으로 변경 할 수 있다.

 

쉽게 말해서, Objective-C의 특성을 이용해서, 런타임에 함수의 기능을 변경 할 수 있는 기능이다.

사용 예시

정의만 들어서는 잘 이해가 안되니 예시를 들어서 이해를 해보자.

예를 들어서 모든 ViewController가 viewDidLoad될 때 마다, Log를 찍고싶다고 해보자. 

여러 방법이 있겠지만, 지금 생각나는 방법으론 두 가지 정도가 있을 것 같다.

 

  1. 모든 VC의 viewDidLoad 안에 Log를 찍는 함수를 추가한다.
  2. viewDidLoad에서 Print문을 찍는 Class를 하나 더 만들고 subClassing한다.

- 1번의 방법은 아주 심플하지만, 만약 우리 앱의 VC가 수십개씩 있다면?? 상당히 귀찮고 매번 VC가 생성될 때마다 코드를 써야한다.

- 2번의 방법은 1번보다는 편하다. 하지만, 1번과 마찬가지로 이미 앱의 VC가 상당 수 있다면 하나하나 확인해가면서 SubClassing해줘야하는 불편함이 있다.

 

이럴 때, 쓰기 편리 한 것이 Method Swizzling이다.

위의 정의에서 런타임에 함수의 기능을 바꿀 수 있다고 했다.

따라서, viewDidLoad의 기능을 우리가 런타임에 바꾼다면 굳이 모든 VC의 코드를 건드리지 않아도 전역적으로 우리가 원하는 기능을 넣을 수 있다.

 

실제 코드

대충 어떤 상황에서 쓰면 좋을지 감이 오니 실제 구현부를 살펴보자.

ViewWillAppear시 해당 ViewController가 print되는 기능을 추가하고 싶었다.

extension UIViewController {
    static let swizzleMethod: Void = {
        let originalSelector = #selector(viewDidLoad)               // 기존 Selector
        let swizzledSelector = #selector(printAppearedView)       // 바꾸고 싶은 Selector
        let originalMethod = class_getInstanceMethod(UIViewController.self, originalSelector) // Method를 가지고옴.
        let swizzledMethod = class_getInstanceMethod(UIViewController.self, swizzledSelector)
        if let origin = originalMethod,
           let swizzle = swizzledMethod {
            method_exchangeImplementations(origin, swizzle)             // 바꾸기
        }
    }()
    
    @objc func printAppearedView() {
        print("viewAppear \(self) ")
    }
}

???

처음보면 어지럽지만 하나씩 떼고 보면 어렵지 않다.

하나씩 살펴보자.

 

먼저 Selector의 구현을 바꾼다고 했으니.. Selector를 가지고 와야된다.(실제로 타입명도 Selector임.)

  • 바꾸고 싶은 기본 Selector를 선언 (originalSelector)
  • Swizzle할 Selector를 정의 및 선언 (swizzledSelector & printViewAppeared)
let originalSelector = #selector(viewDidLoad)               // 기존 Selector
let swizzledSelector = #selector(printAppearedView)       // 바꾸고 싶은 Selector

    @objc func printAppearedView() {
        print("viewAppear \(self) ")
    }

 

실제 인스턴스의 메서드를 class_getInstanceMethod를 이용해 가지고온다.(얘는 타입명이 Method ㄷ ㄷ)

  • 바꾸고 싶은 Method (originalMethod)
  • Swizzle할 Method (swizzledMethod)

 

let originalMethod = class_getInstanceMethod(UIViewController.self, originalSelector) // Method를 가지고옴.
let swizzledMethod = class_getInstanceMethod(UIViewController.self, swizzledSelector)

 

마지막으로  method_exchangeImplementations를 이용 메서드를 바꿔준다.

(위 값이 옵셔널이기 때문에 바인딩한 모습)

if let origin = originalMethod,
   let swizzle = swizzledMethod {
    method_exchangeImplementations(origin, swizzle)             // 바꾸기
}

 

그리고 이 값을 AppDelegate에 넣어주면 App이 실행할때 적용이 된다.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    UIViewController.swizzleMethod
    return true
}

 

구현 모습 

 

우리가 정의한 Method Swizzling하기

ViewDidLoad처럼 시스템에서 정의한 것 말고 우리가 정의한 Method를 Swizzling할 수는 없나??

 

가능.

 

class ViewController1: UIViewController {

    @objc dynamic func printSomething() {
        print("Something!")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        printSomething()
    }
}

 

여기서 유의할 점은 @objc dynamic 키워드이고 꼭 써줘야한다.

@objc dynamic를 붙여줘야 objective-C 런타임을 사용한다고 명시해주는 것이라 꼭 써야함.

나머지는 똑같다.

static let swizzlePrintMethod: Void = {
    let originalSelector = #selector(ViewController1.printSomething)         // 기존 Selector
    let swizzledSelector = #selector(printAnotherThing)                     // 바꾸고 싶은 Selector
    let originalMethod = class_getInstanceMethod(ViewController1.self, originalSelector) // Method를 가지고옴.
    let swizzledMethod = class_getInstanceMethod(ViewController1.self, swizzledSelector)
    if let origin = originalMethod,
       let swizzle = swizzledMethod {
        method_exchangeImplementations(origin, swizzle)             // 바꾸기
    }
}()

@objc func printAnotherThing() {
    print("Another Thing!")
}

👍

 

 

유의점

오.. 개꿀? 이러면서 오남용 하다가는 큰 코를 다칠 수 있다.

  • Swizzle이라는 단어에 유의해보자. Replace(바꾸다)가 아닌 왜 Swizzle(휘젓다)이라는 단어를 사용했을까?

SDK에서 정의한 viewDidLoad, viewWillAppear와 같은 메서드들은 Swizzle된다고 해서 완전히 교체 되지 않는다.

Swizzle된 메서드를 호출한 뒤에 기존에 정의된 메서드의 구현부를 실행할 것이다.

Ex)

class ViewController1: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        print("viewWill!")
    }
}

Swizzling이 된 이 후 에 내가 정의한 viewWill이 print됨을 확인 할 수 있다.

 

  • 이미 Swizzling된 메서드가 아닌지 확인을 해야한다.

다른 라이브러리나 프레임워크를 쓸 때 내부적으로 이미 Swizzling을 해놨을 수도 있다.

이미 Swizzling이 된 함수를 다시 Swizzling하려 하면 잘 작동하지 않을 수 있다.

 

  • iOS 버전관리를 잘 해야한다.
  • Swizzling이 일어났을때 어떤 일이 벌어지는지 개발자가 명확히 알고 있어야 한다.

이는 어떤 코드든 마찬가지겠지만, iOS가 업데이트 될때마다 한번씩 확인을 해줘야하고, Swizzling이 일어났을때 어떤 변화가 생길지를 개발자가 충분히 예측가능할 때 사용해야한다.(위 코드만 봐도 전역적으로 ViewController의 기능을 바꾸고 있으니..)

 

 

참고: https://nshipster.com/method-swizzling/

https://www.innominds.com/blog/method-swizzling-in-ios-development

https://abhimuralidharan.medium.com/method-swizzling-in-ios-swift-1f38edaf984f

https://ios-development.tistory.com/911

https://zeddios.tistory.com/554

'iOS' 카테고리의 다른 글

Multiple Window를 이용하여 Cover Window 만들기.  (0) 2023.02.05
iOS Bluetooth  (1) 2022.10.07
iOS의 뷰가 그려지는 과정  (0) 2022.06.19
서버없이 Networking Test하기 with URLProtocol  (2) 2022.05.15
iOS Cache  (2) 2022.04.24

댓글