@Something
은 모두 Property Wrapper!struct
임. 더 많은 property를 가질 수 도 있고, projectedValue가 없을 수도 있음!@State
:: View 내부에서 수정할 수 있도록 heap에 사는 변수 만들기
@Published
:: 변수의 변경사항을 공표(publish)하기
@ObservedObject
:: publish된 변경사항이 감지되면 View
를 다시 그리도록 하기
struct Published<Value> {
init(initialValue: Value)
var wrappedValue: Value
var projectedValue: Publisher<Value, Never> // 우리가 $ 사용해서 연결할 때 여기 접근함
}
@Published var emojiArt: EmojiArt = EmojiArt()
var _emojiArt: Published<EmojiArt> = Published(wrappedValue: EmojiArt())
var emojiArt: EmojiArt {
get { _emojiArt.wrappedValue }
set { _emojiArt.wrappedValue = newValue }
}
SwiftUI
를 사용하면서 필요한 주요 동작들이 있는데, 이를 템플릿화하여 사용자들이 잘~ 편하게~ 가져다 쓸 수 있도록 하기 위해서@Published
wrappedValue
가 set
되는 시점에 Publish
( projectedValue
)를 통해 변경사항을 전달한다. 이 변경사항은 $emojiArt
로 연결되어 있는 곳으로 전파됩니다. 그리고 이는 ObservableObject
에서 objectWillChange.send()
를 호출합니다.
@State
wrappedValue
가 값타입이건 참조타입이건(보통 스유에서는 값타입) heap에 저장합니다. 변경사항이 생기면, 연결된 View
를 다시 그립니다.
struct State<Value>: DynamicProperty {
init(initialValue: Value)
var wrappedValue: Value
var projectedValue: Binding<Value>
}
@ObservedObject
wrappedValue
는 ObservableObject
를 채택한 타입이여야 합니다.
wrappedValue
가 objectWillChange.send()
를 호출했을 때 View
를 다시 그립니다.
struct ObservedObject<ObjectType>: DynamicProperty where ObjectType : ObservableObject{
init(initialValue: Value)
var wrappedValue: Value
var projectedValue: ObservedObject<ObjectType>.Wrapper { get }
public struct Wrapper {
public subscript<Subject>(dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>) -> Binding<Subject> { get }
}
}
@Binding
wrappedValue
:: 다른 무언가와 연결된 값
다른 어떤 곳에서 wrappedValue
를 get/set
할 수 있습니다. 값이 변경되었을 때, View
를 다시 그립니다.
아주아주아주 많은 곳에서 쓸 수 있음.
진실의 단일 소스를 가지게 한다는 점에서, MVVM 패턴에서 매우 중요한 역할을 합니다.
ViewModel
이 가지고 있지만, 이 데이터는 View
에서도 제어하기도 함. (둘 중 뭐가 진짜게?)View
에서 stored property
를 추가하는 것이 아닌, ViewModel
의 변수를 @Biniding
하여 사용 할 수 있다. 한 쪽에서 바뀌면, 둘 다 바뀌기 때문에 둘 다 진짜가 된당!struct OtherView: View {
@Binding var sharedText: String // @State가 아닌 @Binding으로 선언
var body: View {
Text(sharedText)
}
}
struct MyView: View {
@State var myString = "Hello"
var body: View {
OtherView(sharedText: $myString) // myString을 OtherView와 연결
}
}
OtherView(sharedText: .constant("Howdy"))
OtherView(sharedText: Binding(get:, set:)
@EnvironmentObject
@ObservedObject
랑 유사한데, 넘겨주는 방식이 다릅니다.
struct MyView: View {
@ObservedObject var viewModel: ViewModelClass
...
}
let myView = MyView(viewModel: theViewModel)
struct MyView: View {
@EnvironmentObject var viewModel: ViewModelClass
...
}
let myView = MyView().environmentObject(theViewModel)
가장 큰 차이점!
상위 뷰에서 @EnvironmentObject
를 선언하면, body
내부의 모든 뷰에서 (모달로 띄운 거 말고) 접근할 수 있다!!!!!!
View
하나에서 같은 ObservableObject
타입의 @EnvironmentObject
는 하나만 존재할 수 있다.
기본적으로 wrappedValue 와 동작방식은 ObservableObject 와 같다!
EnvironmentObject 랑 연관없음!
View의 환경적인 요소값들에 대해 읽어오는 Property Wrapper.
[EnvironmentValue](https://developer.apple.com/documentation/swiftui/environmentvalues)
타입인 KeyPath를 받아서 해당 값에 접근한다.
struct Environment<Value> : DynamicProperty {
init(_ keyPath: KeyPath<EnvironmentValues, Value>)
var wrappedValue: Value { get }
// projected value는 없습니당.
}
// property
@Environment(\.lineLimit) var lineLimit
// 값을 변경하고 싶을 때는 이렇게
MyView()
.environment(\M.lineLimit, 2)
// 혹은 이렇게 축약해
private struct MyEnvironmentKey: EnvironmentKey {
static let defaultValue: String = "Default value"
}
extension EnvironmentValues { // EnvironmentValues 에 커스텀 키 값 추가
var myCustomValue: String {
get { self[MyEnvironmentKey.self] }
set { self[MyEnvironmentKey.self] = newValue }
}
}
extension View {
func myCustomValue(_ myCustomValue: String) -> some View {
environment(\.myCustomValue, myCustomValue)
}
}
일반적으로는 값을 내보내는데, 만약 실패한다면 실패 값을 내보내는 녀석. Combine
에 정의되어 있음.
public struct Publisher : Publisher {
public typealias Output = Wrapped
public typealias Failure = Never // Never Failure. Never ends.
public let output: Optional<Wrapped>.Publisher.Output?
public init(_ output: Optional<Wrapped>.Publisher.Output?)
public func receive<S>(subscriber: S) where Wrapped == S.Input, S : Subscriber, S.Failure == Never
}
비슷한 예를 찾자면, Rx의 Observable, Subject, Relay 와 유사함.
성공/실패/오류 에 대해 이전에는 오류를 던지는(throw
) 하는 방식으로 구현되었다면, 이제는 그러한 실패/오류 까지도 하나의 타입으로 명시하여 처리하는 방식
cancellable = myPublisher.sink( // Sink returns that implements the Cancellabe protocol
receiveCompletion: { result in ... },
receiveValue: { thingThePubliserPublishes in ... }
)
cancel()
을 호출하기 전까지 계속 연결되어 있을 것이라는점 🙂View
.onReceive(publisher) { thingThePublisherPublishes in
// do something!
// At this time your View will be invalidated automaticallly.
}
projectedValue
의 타입이 Publisher
입니당.URLSession.dataTaskPublisher
Timer.publish(every:)
NotificationCenter.publisher(for:)
class ViewModel {
@Published var backgroundImage: UIImage?
private var fetchImageCancellable: AnyCancellable?
private func fetchImage(url: URL) {
stopLoadImage()
let session = URLSession.shared
let publisher = session.dataTaskPublisher(for: url)
.map { data, urlResponse in UIImage(data: data) } // 이미지로 변환
.receive(on: DispatchQueue.main) // main queue에서 받기
.replaceError(with: nil) // Error가 떨어질 경우 nil을 넘겨줌.
// let canceller = publisher.assign(to: `\ViewModel.backgroundImage, on: self)
fetchImageCancellable = publisher.assign(to: `\.backgroundImage, on: self) // 어디에 있는지 명시해줬기 때문에 생략가능
}
private func stopLoadImage() {
fetchImageCancellable?.cancel()
}
}
class ViewModel {
@Published var backgroundImage: UIImage?
private var fetchImageCancellable: AnyCancellable?
private func fetchImage(url: URL) {
stopLoadImage()
fetchImageCancellable = URLSession.share.dataTaskPublisher(for: url)
.map { data, urlResponse in UIImage(data: data) } // 이미지로 변환
.receive(on: DispatchQueue.main) // main queue에서 받기
.replaceError(with: nil)
.assign(to: `\.backgroundImage, on: self) // 어디에 있는지 명시해줬기 때문에 생략가능
}
private func stopLoadImage() {
fetchImageCancellable?.cancel()
}
}
Chaining 가능!