티스토리 뷰
[SwiftUI][Combine] 프로퍼티 래퍼 어노테이션 만들기(Property Wrapper Anotation)
_dodo 2023. 4. 7. 20:54프로젝트 도중에 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.
'iOS > SwiftUI' 카테고리의 다른 글
[SwiftUI] NavigationStack으로 여러 종류의 뷰 Push하기 (0) | 2023.04.29 |
---|---|
[iOS] 너무 커진 ViewModel 분리하기 (MVVM) (0) | 2023.04.28 |
[SwiftUI] 데이터 바인딩 어노테이션(프로퍼티 래퍼) 정리- @State, @Binding, @ObservedObject, @StateObject, @EnvironmentObject (0) | 2022.07.29 |
[SwiftUI] 둥근 테두리 버튼 만들기 (0) | 2022.07.26 |
[SwiftUI] Group, GroupBox을 알아보자 (0) | 2022.07.08 |
- Total
- Today
- Yesterday
- Swift Concurrency
- SwiftUI
- DocC
- 노션
- RX
- Flux
- Flutter
- SWM
- Bloking/Non-bloking
- combine
- 프로그래머스
- notion
- 아키텍쳐 패턴
- reactive programming
- Architecture Pattern
- 비동기/동기
- coordinator pattern
- design pattern
- 코디네이터 패턴
- programmers
- MVC
- MVI
- MVVM
- healthkit
- GetX
- ios
- swift
- TestCode
- 소프트웨어마에스트로
- 리액티브 프로그래밍
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |