천원의 개발

Swift ReactorKit Pulse 기능을 학습하기 위한 과정 본문

iOS&Swift🍎/Swift

Swift ReactorKit Pulse 기능을 학습하기 위한 과정

천 원 2024. 6. 27. 16:50

안녕하세요. 천원입니다.

오늘은 ReactorKit을 학습하는 도중 직면했던 경험 이야기를 작성해 보려고 합니다.

 

회고 같은 느낌이라 편한 말투로 진행해 보겠습니당

 

Reactorkit의 예제 프로젝트를 확인해 보면 아래와 같이 propertyWrapper 기능을 활용하여 Pulse를 구현하였습니다.

  struct State {
    var value: Int
    var isLoading: Bool
    @Pulse var alertMessage: String?
  }

 

 

 

 

propertyWrapper에 대해 간단하게 설명드리면 재사용될 프로퍼티들을 반복적으로 작성하지 않기 위한 기능으로

 

예제를 작성해 보면 UseDefaults를 활용해 사용자의 정보를 저장하고 싶다면 아래와 같이 모든 프로퍼티에 동일한 get, set 구문을 작성해 주어야 합니다.

var phoneNumber: String {
    get {
        return UserDefaults.standard.object(forKey: "phoneNumber") as! String
    }
    set {
        UserDefaults.standard.setValue(newValue, forKey: "phoneNumber")
    }
}

var name: String {
    get {
        return UserDefaults.standard.object(forKey: "name") as! String
    }
    set {
        UserDefaults.standard.setValue(newValue, forKey: "name")
    }
}

 

 

 

그렇게 되면 저장해야할 정보가 많아 질수록 보일러플레이트 코드가 생기게 되니까 우리는 PropertyWrapper를 활용하여 기능을 구현해 보겠습니다.

import Foundation

@propertyWrapper
struct UserDefault<T> {
    let key: String
    
    init(key: String) {
        self.key = key
    }
    
    var wrappedValue: T {
        get {
            return UserDefaults.standard.object(forKey: key) as! T
        }
        set {
            UserDefaults.setValue(newValue, forKey: key)
        }
    }
}

 

우선 propertyWrapper에는 필수적으로 wrappedValue 프로퍼티를 구현해 주어야 합니다. 그러니 반복되는 코드를 구현해 주고 저장에 필요한 key값을 초기화 구문에서 지정해 줍니다. 그러면 아래와 같이 사용이 가능합니다.

 

// 선언
@UserDefault(key: "phoneNumber") var phoneNumber: String?
@UserDefault(key: "name") var name: String?

// 호출
name = "page"

 

 

자 이제 돌아와서 Pulse 구현부를 살펴보면

@propertyWrapper
public struct Pulse<Value> {

  public var value: Value {
    didSet {
      riseValueUpdatedCount()
    }
  }

  public internal(set) var valueUpdatedCount = UInt.min

  public init(wrappedValue: Value) {
    self.value = wrappedValue
  }

  public var wrappedValue: Value {
    get { value }
    set { value = newValue }
  }

  public var projectedValue: Pulse<Value> {
    self
  }

  private mutating func riseValueUpdatedCount() {
    valueUpdatedCount &+= 1
  }
}

 

잘 모르겠지만 value에 newValue를 저장하고 value 값이 변경이 되면 riseValueUpdatedCount를 통해서 Count 값을 증가 시켜주네요. 이번에는 Pulse로 만들어진 alertMessage를 사용하는 부분을 확인해 보겠습니다.

// bind
reactor.pulse(\.$alertMessage)
  .compactMap { $0 }
  .subscribe(onNext: { [weak self] message in
    let alertController = UIAlertController(
      title: nil,
      message: message,
      preferredStyle: .alert
    )
    alertController.addAction(UIAlertAction(
      title: "OK",
      style: .default,
      handler: nil
    ))
    self?.present(alertController, animated: true)
  })
  .disposed(by: disposeBag)

 

pulse라는 메서드에 매개변수로 \.$alertMessage가 들어가네요.. SwiftUI 학습할 당시에 종종 봤지만 오늘은 구체적으로 한번 살펴보겠습니다. 우선 $alertMessage는 무엇을 뜻할까요? 다시 Property Wrapper로 돌아와서 

 

@propertyWrapper
struct UserDefault<T> {
    let key: String
    
    init(key: String) {
        self.key = key
    }
    
    var wrappedValue: T {
        get {
            return UserDefaults.standard.object(forKey: key) as! T
        }
        set {
            UserDefaults.setValue(newValue, forKey: key)
        }
    }
    
    func delete() {
        UserDefaults.standard.removeObject(forKey: key)
    }
}

 

아까 작성했던 UserDefault Property Wrapper에 delete라는 메서드를 추가하게 된다면 어떤식으로 호출할 수 있을까요?

평범한 구조체라면 우리는 인스턴스를 생성 후 해당 인스턴스로 접근하겠지만 

@UserDefault(key: "phoneNumber") var phoneNumber: String?

해당 프로퍼티로는 접근이 불가능하겠죠? phoneNumber는 String type일 뿐이니까요 우리가 delete를 호출하기 위해서는 UserDefault<T>타입이 필요합니다. 그럴때 사용하는게 projectedValue 입니다.

 

@propertyWrapper
struct UserDefault<T> {
    let key: String
    
    init(key: String) {
        self.key = key
    }
    
    var wrappedValue: T {
        get {
            return UserDefaults.standard.object(forKey: key) as! T
        }
        set {
            UserDefaults.setValue(newValue, forKey: key)
        }
    }
    
    var projectedValue: UserDefault<T> { // projectedValue 추가
        self
    } 
    
    func delete() {
        UserDefaults.standard.removeObject(forKey: key)
    }
}

 

이렇게 projectedValue를 만들면 우리는 $키워드를 통해서 projectedValue에 접근이 가능합니다. 그렇다면 delete 메서드도 호출이 가능하겠죠

$phoneNumber.delete()

 

 

Pulse로 돌아와서 확인해 보면 projectedValue가 구현되어 있으니 $표기를 통해서 Pulse<Value> 타입으로 접근이 가능해 보입니다.

@propertyWrapper
public struct Pulse<Value> {

  public var value: Value {
    didSet {
      riseValueUpdatedCount()
    }
  }

  public internal(set) var valueUpdatedCount = UInt.min

  public init(wrappedValue: Value) {
    self.value = wrappedValue
  }

  public var wrappedValue: Value {
    get { value }
    set { value = newValue }
  }

  public var projectedValue: Pulse<Value> {
    self
  }

  private mutating func riseValueUpdatedCount() {
    valueUpdatedCount &+= 1
  }
}

 

 

그러면 우리는 \.$alertMessage에서 $alertMessage가 Pulse<Value>를 뜻하는걸 알았으니 \.에 대해서 확인해 봅시다! 우선 해당 문법은 KeyPath 방법으로 프로퍼티에 접근하는 방식으로 KeyPath 포스팅 을 통해서 확인하실 수 있습니다. 자자 그렇다면 우리는 KeyPath방식으로 매개변수를 받으니 pulse 메서드의 매개변수를 추측해 보면

 

public func test<T>(_ keyPath: KeyPath<State, Pulse<T>>) {} // 구현

reactor.test(\.$alertMessage) // 호출

 

이런식으로 생겼으니까 test(\.$alertMessage) 로 호출이 가능하겠다! 라고 생각했습니다. 

 

 

 

그런데.. 두둥.. 실제 pulse 메서드는 아래와 같이 생겼습니다.

  public func pulse<Result>(_ transformToPulse: @escaping (State) throws -> Pulse<Result>) -> Observable<Result> {
    state.map(transformToPulse).distinctUntilChanged(\.valueUpdatedCount).map(\.value)
  }

 

엥..? \.$alertMessage가 (State) throws -> Pulse<Result> 타입이라고? 이게 가능한거야???? 라고 열심히 찾아봤는데.. 

가능하네요! https://forums.swift.org/t/key-path-expressions-as-functions/19587 해당 링크를 통해서 학습한 내용인데 

 

struct User {
	let name: String
}

\User.name = (User) -> String // 똑같당

let a = \User.name as (User) -> String // 캐스팅도 가능

 

KeyPath는 클로저 형식으로 표현이 가능합니다! 

 

 

여기까지가 ReactorKit의 Pulse 기능을 학습하기 위한 과정이였는데 개인적으로 많은 도움이 되었어서 공유하고자 이렇게 글로 남겨둡니당~~~

 

 

 

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

Swift 6 새로운 기능  (4) 2024.10.31
Swift Clean Architecture 정리  (1) 2023.11.23
iOS Tuist 프로젝트에 적용하기  (0) 2023.10.22
Swift RIBs RootRIB 설정하기  (0) 2023.09.09
Swift RIBs 튜토리얼 (with StoryBorad)  (0) 2023.08.26