티스토리 뷰

프로젝트 도중에 Combine을 이용하던 중 

Subject를 써야할 일이 있었는데, Published는 프로퍼티 래퍼로 감싸져 있는데 Subject는 그냥 써야해서 좀 아쉽더라구요. typealias 이용하고 있다가 이번에 바꿔보기로 했습니다.

 

프로퍼티 래퍼(Property Wrapper)란?


우선 프로퍼티 래퍼는 SwiftUI를 쓰시는 분들이라면 모두 익숙하실 @State, @Binding 등 '@' 표시가 붙은 어노테이션을 말합니다.

 

이 프로퍼티 래퍼는 어떻게 구현되어 있을까요? 바로 @propertyWrapper를 이용해서 만들어집니다.

@State의 정의부를 보면

이렇게 프로퍼티 래퍼를 쓴 struct 형태로 정의되어 있는 것을 보실 수 있습니다. 따라서 @propertyWrapper를 사용한다면 나만의 프로퍼티 래퍼도 만들 수 있습니다.

 

@propertyWrapper 사용법


@propertyWrapper
struct MyWrapper<Value> {
    private var value: Value
    
    init(wrappedValue: Value) {
        self.value = wrappedValue
    }
    
    var wrappedValue: Value {
        get { value }
        set { value = newValue }
    }
    
    // 추가적인 동작을 수행할 수 있는 메서드나 연산 프로퍼티 등을 정의
    // 예를 들어, 유효성 검사를 수행하는 메서드를 추가할 수 있습니다.
    func validate() -> Bool {
        // 유효성 검사를 수행하는 코드
        return true
    }
}

MyWrapper라는 프로퍼티 래퍼는 @propertyWrapper 어노테이션선언하고 제네릭을 받는 struct를 구현해주시면 됩니다. 위 struct는 다음과 같이 쓸 수 있습니다.

struct Person {
    @MyWrapper var age: Int
}

var person = Person()
person.age = 20 // MyWrapper 구조체의 wrappedValue 프로퍼티를 사용하여 값을 저장
person.age += 1 // MyWrapper 구조체의 wrappedValue 프로퍼티를 사용하여 값을 읽고 수정
let isValid = person.age.validate() // MyWrapper 구조체의 validate() 메서드를 호출하여 유효성을 검사

 

실전! Subject 감싸기


@propertyWrapper
struct Subject<Value> {
    private var subject: PassthroughSubject<Value, Never>
    private var value: Value
    
    init(wrappedValue: Value) {
        self.value = wrappedValue
        self.subject = PassthroughSubject<Value, Never>()
    }

    var wrappedValue: Value {
        get { value }
        set {
            value = newValue
            subject.send(newValue)
        }
    }
    
    var projectedValue: PassthroughSubject<Value, Never> {
        return self.subject
    }
    
    func send(_ event: Value) {
        self.subject.send(event)
    }
}

Subject를 감싸주었습니다! proejctedValue를 구현해주셔야 $subject 같은 문법을 사용할 수 있습니다!

 

@Subject var tapLoginButton: Void = ()

$tapLoginButton.sink { _ in
    print("로그인 시도")
}.store(in: &cancellables)

이제 @Subject를 이용해서 선언하고, $를 이용해서 접근할 수 있습니다!

 

Button에 바인딩 하는 방법은

struct DefaultButton<Content>: View where Content: View {
    @Subject var subject: Void
    @ViewBuilder let content: Content
    
    var body: some View {
        Button(action: { $subject.send() }) {
           	content
        }
    }
}

이렇게 추상화된 DefaultButton을 만들어주신 뒤에

@ObservedObject var viewModel: LoginViewModel

// ...
DefaultButton(
    subject: viewModel.tapLoginButton,
    content: {
        Text("로그인하기")
    }
)

이렇게 선언하신 뒤 바인딩해주시면 됩니다!! 

 

 

기존 작업방식과 충돌..🥲

더보기

기존에는 let action: () -> Void로 액션을 받아서 처리했기 때문에 모든 코드의 tap event를 subject로 바꿔야하는 일이 벌어졌습니다. subject가 준비안된 곳에서는 매개변수가 없다고 컴파일 에러가 떠서...임시방편으로 다음처럼 바꿔주었습니다. 나중에 action이 없어질 수 있도록 리팩토링해보려고 합니다.

// ContentView.swift
@ObservedObject var viewModel: LoginViewModel

// ...
DefaultButton(
    subject: viewModel.$tapLoginButton,
    content: {
        Text("로그인하기")
    }
)

// DefaultButton.swift
struct DefaultButton<Content>: View where Content: View {
    let action: () -> Void
    var subject: PassthroughSubject<Void, Never>? = nil
    @ViewBuilder let content: Content
    
    var body: some View {
        Button(action: {
            action()
            subject?.send() }) {
            content
        }
    }
}

 

 

결론


너무 귀엽네요..!!

 

 

감사합니다.

 

ref.

Property Wrapper(Zed0202)

Cosmo/OpenSwiftUI

 

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
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
글 보관함