KVO에 대해 알아보고 적용해 보자 !!

2020. 8. 7. 13:46iOS/Swift

오늘은 KVO에 대해서 알아보겠습니다.

KVO는 Key-Value Observing의 약자로 특정한 변수가 변경될 때마다 코드가 실행되도록 하는 기능입니다.

일반적으로 프로퍼티 감시자인 willSet이나 didSet과 비슷하지만, 모델과 뷰 등 논리적으로 분리된 앱 부분간에 변경사항을 전달하는데 유용하다고 합니다. 이 기능은 Objective-C 런타임에 의존하기 때문에 순수한 Swift 코드에서는 그리 좋지 않다고 합니다..!

따라서 NSObject를 상속받는 클래스로 정의해 주어야 하며 각각의 프로퍼티에는 @objc dynamic 이라는 표시를 해야합니다. dynamic 이 자세히 어떤건지는 추후 다루어 보겠습니다!

KVO를 적용시켜 볼 프로젝트는 다음과 같습니다.

via GIPHY

상단의 탭바가 존재하고 화면 안에서도 스크롤통해 탭 이동이 가능해집니다. 이에따라 상단 탭의 하이라이팅과 현재 탭을 표시하는 인디케이터 가 존재합니다.

화면의 구성을 간단하게 말씀드리면 상단 탭은 컬렉션 뷰를 통해 구현하였고, 아래에 보여지는 화면은 컨테이너뷰와 PageViewController를 통해 페이징을 하였습니다. 

자세한 구현 내용은 

lidium.tistory.com/14?category=873871

 

[Crecker, iOS] PageViewController (android : ViewPager)

저번주에 이어서, ViewPager 만들고 있습니다. 동시에 ActionSheet도 만들고 있는데, 그거는 다음주 주제로 하기로 했습니다...ㅎㅎ. 구조를 말씀드려 보겠습니다 안드로이드의 ViewPager를 구현해야 합��

lidium.tistory.com

이곳을 참고하였습니다.

페이징을 담당하는 PageViewController와 부모 ViewController가 달라도 PageViewController 내부에서 현재 페이지 인덱스를 저장하는 KVO Object를 생성하여 이 값이 변경될 때마다 상단 탭의 인디케이터를 바꾸어주는 동작을 수행하였습니다.

가장 먼저 해야할 것은 관찰할 프로퍼티를 다음과 같이 만드는 것 입니다.

class KVOObject : NSObject {
    @objc dynamic var curPresentViewIndex: Int = 0
    
}

앞서 설명드렸다 시피 값을 관찰하기 위해서는 NSObject를 상속받는 클래스 내부의 프로퍼티로 @objc dynamic 이라는 표시를 해 주어야 합니다. 따라서 현재 PageViewController에서 보여지고 있는 화면의 인덱스 값을 저장시켜줄 프로퍼티를 생성하였습니다.

그리고 페이징을 담당하는 페이지 뷰컨트롤러의 프로퍼티로 만들어 두었던 클래스의 인스턴스를 생성해 둡니다.

class PageVC: UIPageViewController {
    ...
    var keyValue = KVOObject()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        ...
        
    }
    ...
}

이렇게 생성을 해둔 다음페이지 뷰컨트롤러 내부에서 페이징이 됨에 따라 위의 프로퍼티 내부에 존재하는 'curPresentViewIndex' 변경해 주었습니다.

func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        if completed {
            print(previousViewControllers[0])
            if previousViewControllers[0] is CustomTopTab.ThemaVC {
                self.keyValue.curPresentViewIndex = 1
            }
            else {
                self.keyValue.curPresentViewIndex = 0
            }
        }
    }

테스트로 탭이 두개인 상황만 가정했기 때문에 간단하게 다음과 같이 변경해 주었습니다.

메인으로 돌아와서 해당 값이 바뀔 때 마다 인디케이터 바를 변경시켜주는 동작을 수행시켜 주면 됩니다.

컨테이너뷰의 segue를 통해 페이지뷰컨의 인스턴스를 생성하여 observe 를 등록해주면 됩니다.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "pageSegue" {
            pageInstance = segue.destination as? PageVC
            
            let ob = pageInstance?
                .keyValue
                .observe(\.curPresentViewIndex,
                         options: [.new, .old]) {
                            [weak self] (changeObject, value) in
                            
                            self?.tabBarCollectionView.selectItem(at: IndexPath(item: value.newValue!,
                                                                                section: 0),
                                                                  
                                                                  animated: false,
                                                                  scrollPosition: .bottom)
                            if (value.newValue == 0){
                                UIView.animate(withDuration: 0.1){
                                    self!.underBarView.transform = CGAffineTransform(translationX:0 ,y: 0)
                                }
                            }
                            else {
                                UIView.animate(withDuration: 0.1){
                                    self!.underBarView.transform = CGAffineTransform(translationX:200 ,y: 0)
                                }
                            }
            }
            
            observingList.append(ob!)
            pageInstance?.pageControlDelegate = self
        }
    }
}

pageInstance는 메인 뷰컨트롤러에서 PageVC의 옵셔널 타입으로 선언한 프로퍼티 변수 입니다. prepare를 통해 해당 프로퍼티를 초기화 시킨 다음 pageInstance에 존재하는 keyValue에 observe 함수를 호출해 주면 됩니다.

observe 함수는

observe(keyPath: KeyPath<KVOObject, Value>, options: NSKeyValueObservingOptions, changeHandler: (KVOObject, NSKeyValueObservedChange<Value>) -> Void) 다음과 같은 형식으로 호출할 수가 있습니다.

keyPath는 관찰할 변수를 설정해 줍니다 '.\변수명' 과 같이 넣어주면 되고, option에는 NSKeyValueObservingOptions 라는 타입을 넣어주어야 하는데 .new, .old, .initial, prior 의 옵션을 줄 수가 있습니다.

.new는 적용 가능한 경우 change dirctionaly가 새 속성 값을 제공해야 함을 나타낸다고 합니다.

.old는 new 와 비슷하지만 이전 속성 값이 포함되어야 함을 뜻한다고 합니다.

.initial은 지정한 경우 옵저버 registration method가 반환되지 전에 알림을 옵저버에게 즉시 보내야한다는 옵션이고

.prior는 변경 후 단일 알람 대신에 각 변경 전 후에 별도의 통지를 관찰자에게 보내야 하는지 여부를 결정해 주는 옵션입니다.

이 옵션 값에 따라 뒤에 정의해줄 handler 클로져로 들어오는 파라미터 중 NSKeyValueObservedChange<Value> 의 값이 조금 씩 변경 됩니다. 

이 값을 print 할 경우 

NSKeyValueObservedChange<Int>(kind: __C.NSKeyValueChange, newValue: nil, oldValue: Optional(0), indexes: nil, isPrior: false)

다음과 같이 나옵니다. old와 new옵션에 따라 newValue 또는 oldValue 에 nil 값이 들어오기도 하며 isPrior와 같은 속성이 변경되기도 합니다. 따라서 이 값에서 필요한 정보를 가져와 동작을 수행시켜 주면 됩니다.

저는 이 값에 따라 상단 탭을 구성하는 컬렉션뷰의 selectItem을 호출해 주고, 그 밑에 있는 인디케이터 바를 애니메이션함수를 통해 이동시켜 주었습니다.

KVO를 써보다 보니 이전에 공부했던 RxSwift와 유사한 구조로 동작한다는 것을 알 수 있었습니다.

아쉬운 점은 동작의 반응속도가 어느정도 delay가 존재하는 점 이였습니다. 구현에는 아쉬운 점이 있었지만 KVO 방식을 이해해 보는 좋은 시간이였습니다.

 

'iOS > Swift' 카테고리의 다른 글

UIBezierPath 사용해보기  (0) 2020.10.16
Swift Combine은 또 뭐야 ..?  (1) 2020.09.04
Swift Lint 사용해보기 !!  (2) 2020.04.10