티스토리 뷰
Property Wrappers
@something은 다 프로퍼티 래퍼이다.
각 Property Wrapper는 구조체이며 이는 'template'동작을 캡슐화한다.
예를 들어
@State는 변수가 힙에 살도록 하고
@Published는 변경이 있을 때 publish 하도록 하고
@ObservedObject는 published 변경이 감지되면 뷰를 다시 그리도록 하고
Property Wrapper는 이 구조체들을 생성하고 사용하기 쉽게 해준다.
Property Wrapper 안 쓰면 아래처럼 사용해야 한다.
Property Wrapper를 사용하기 위해 구조체를 만들고 그에 맞게 구현해줘야 한다.
또한 Property Wrapper안에 또 다른 변수가 존재하는데 projectedValue이다.
이 projectedValue값에 접근하려면 $를 사용해야 한다.
@Published에서 wrappedValue가 설정(변경)되면 무엇을 할까?
publisher인 projectValue를 통해 변경 사항을 publish 한다.
또한 ObservableObject에서 objectWillChange.send()를 호출한다.
@State
바인딩 $ 힙에 바인딩 역할을 한다.
뷰가 변경되면 무효화된다.
@ObservedObject
바인딩 $WrappedValue에 바인딩한다 이는 뷰모델이다.
WrappedValue가 objectWillChange.send()를 수행할 때 View를 무효화한다.
@Binding
바인딩 $ 스스로에게 바인딩한다.
바인딩된 값이 변경되면 뷰가 무효화된다.
바인딩은 TextField에서 텍스트 가져오기, Picker에서 선택하거나 Toggle 된 상태, NavigationView에서 선택된 아이템 찾기 등에서 많이 사용된다.
하지만 바인딩은 단일 소스를 갖는 사실을 알아야 한다.
@State 또는 @ObservedObject는 다른 뷰와 공유한다.
struct MyView: View {
@State var myString = “Hello”
var body: View {
OtherView(sharedText: $myString)
}
}
struct OtherView: View {
@Binding var sharedText: String
var body: View {
Text(sharedText)
}
}
OtherView body는 myView에서 문자열은 항상 MyView에 있는 myString값이고
Other Viewers sharedText는 MyView의 myString에 바인딩된다.
상수 값에 바인딩이 가능하다.
OtherView(sharedText: .constant(“Howdy”))
연산 바인딩도 가능하다. (Binding(get:, set:))
@EnvironmentObject
@ObservedObject와 동일하지만 뷰에게 다른 방법으로 전달한다.
let myView = MyView().environmentObject(theViewModel)
let myView = MyView(viewModel: theViewModel)
// 뷰 내부
@EnvironmentObject var viewModel: ViewModelClass
@ObservedObject var viewModel: ViewModelClass
다른점은 EnvironmentObject 는 모든 뷰의 body에 표시된다. (모달 표시되는 뷰 제외)
따라서 뷰가 동일한 뷰모델을 공유할 때 가끔 사용된다.
모달로 띄울 때 @EnvironmentObject를 사용하는 것이 좋다.
각 ObservableObject타입의 뷰에 각 @EnvironmentObject를 사용할 수 있다.
WrappedValue는 .environmentObject()를 통해 ObservableObject를 얻을 때 View로 보낸다.
@Environment
()를 사용해서 다른 변수에게 값을 넘겨준다.
@Environment(\.colorScheme) var colorScheme
위와 같이 컬러 스킴을 keyPath로 전달할 수 있다.
EnvironmentValues 구조체에 저장된다.
뷰모델
import SwiftUI
import Combine
class EmojiArtDocument: ObservableObject {
static let palette: String = "👩💻🙋♀️🙇♀️💃🏄♀️🧘♂️"
@Published private var emojiArt: EmojiArt
private static let untitled = "EmojiArtDocument.Untitled"
private var autosaveCancellable: AnyCancellable?
init() {
emojiArt = EmojiArt(json: UserDefaults.standard.data(forKey: EmojiArtDocument.untitled)) ?? EmojiArt()
autosaveCancellable = $emojiArt.sink { emojiArt in
UserDefaults.standard.data(forKey: EmojiArtDocument.untitled)
}
fetchBackgroundImageData()
}
@Published private(set) var backgroundImage: UIImage?
var emojis: [EmojiArt.Emoji] { emojiArt.emojis }
// MARK: - Intent(s)
func addEmoji(_ emoji: String, at location: CGPoint, size: CGFloat) {
emojiArt.addEmoji(emoji, x: Int(location.x), y: Int(location.y), size: Int(size))
}
func moveEmoji(_ emoji: EmojiArt.Emoji, by offset: CGSize) {
if let index = emojiArt.emojis.firstIndex(matching: emoji) {
emojiArt.emojis[index].x += Int(offset.width)
emojiArt.emojis[index].y += Int(offset.height)
}
}
func scaleEmoji(_ emoji: EmojiArt.Emoji, by scale: CGFloat) {
if let index = emojiArt.emojis.firstIndex(matching: emoji) {
emojiArt.emojis[index].size = Int((CGFloat(emojiArt.emojis[index].size) * scale).rounded(.toNearestOrEven))
}
}
var backgroundURL: URL? {
get {
emojiArt.backgroundURL
}
set {
emojiArt.backgroundURL = newValue?.imageURL
fetchBackgroundImageData()
}
}
private var fetchImageCancellable: AnyCancellable?
private func fetchBackgroundImageData() {
backgroundImage = nil
if let url = self.emojiArt.backgroundURL {
fetchImageCancellable?.cancel()
fetchImageCancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { data, urlResponse in UIImage(data: data) }
.receive(on: DispatchQueue.main)
.replaceError(with: nil)
.assign(to: \EmojiArtDocument.backgroundImage, on: self)
}
}
}
뷰
struct EmojiArtDocumentView: View {
@ObservedObject var document: EmojiArtDocument
@State private var chosenPalette: String = ""
var body: some View {
VStack {
HStack {
PaletteChooser(document: document, chosenPalette: $chosenPalette)
ScrollView(.horizontal) {
HStack {
ForEach(chosenPalette.map { String($0) }, id: \.self) { emoji in
Text(emoji)
.font(Font.system(size: defaultEmojiSize))
.onDrag { return NSItemProvider(object: emoji as NSString) }
}
}
}
}
GeometryReader { geometry in
ZStack{
Color.white.overlay(
OptionalImage(uiImage: self.document.backgroundImage)
.scaleEffect(self.zoomScale)
.offset(self.panOffset)
)
.gesture(self.doubleTapToZoom(in: geometry.size))
if self.isLoading {
Image(systemName: "hourglass").imageScale(.large).spinnig()
} else {
ForEach(self.document.emojis) { emoji in
Text(emoji.text)
.font(animatableWithSize: emoji.fontSize * zoomScale)
.position(self.position(for: emoji, in: geometry.size))
}
}
}
.clipped()
.gesture(self.panGesture())
.gesture(self.zoomGesture())
.edgesIgnoringSafeArea([.horizontal, .bottom])
.onReceive(self.document.$backgroundImage) { image in
self.zoomToFit(image, in: geometry.size)
}
.onDrop(of: ["public.image","public.text"], isTargeted: nil) { providers, location in
var location = geometry.convert(location, from: .global)
location = CGPoint(x: location.x - geometry.size.width/2,
y: location.y - geometry.size.height/2)
location = CGPoint(x: location.x - self.panOffset.width,
y: location.y - self.panOffset.height)
location = CGPoint(x: location.x / self.zoomScale,
y: location.y / self.zoomScale)
return self.drop(providers: providers, at: location)
}
}
}
}
var isLoading: Bool {
document.backgroundURL != nil && document.backgroundImage == nil
}
@State private var steadyStatezoomScale: CGFloat = 1.0
@GestureState private var gestureZoomScale: CGFloat = 1.0
// any type
private var zoomScale: CGFloat {
steadyStatezoomScale * gestureZoomScale
}
private func zoomGesture() -> some Gesture {
MagnificationGesture()
.updating($gestureZoomScale) { latestGestureScale, gestureZoomScale, trasaction in
gestureZoomScale = latestGestureScale
}
.onEnded{ finalGestureScale in
self.steadyStatezoomScale *= finalGestureScale
}
}
@State private var steadyStatePanOffset: CGSize = .zero
@GestureState private var gesturePanOffset: CGSize = .zero
private var panOffset: CGSize {
(steadyStatePanOffset + gesturePanOffset) * zoomScale
}
private func panGesture() -> some Gesture {
DragGesture()
.updating($gesturePanOffset) { latestDragGestureValue, gesturePanOffset, transaction in
gesturePanOffset = latestDragGestureValue.translation / self.zoomScale
}
.onEnded { finalDragGestureValue in
self.steadyStatePanOffset = self.steadyStatePanOffset + (finalDragGestureValue.translation / self.zoomScale)
}
}
private func doubleTapToZoom(in size: CGSize) -> some Gesture {
TapGesture(count: 2)
.onEnded {
withAnimation {
self.zoomToFit(self.document.backgroundImage, in: size)
}
}
}
private func zoomToFit(_ image: UIImage?, in size: CGSize) {
if let image = image, image.size.height > 0 {
let hZoom = size.width / image.size.width
let vZoom = size.height / image.size.height
self.steadyStatePanOffset = .zero
self.steadyStatezoomScale = min(hZoom, vZoom)
}
}
private func position(for emoji: EmojiArt.Emoji, in size: CGSize) -> CGPoint {
var location = emoji.location
location = CGPoint(x: location.x * zoomScale,
y: location.y * zoomScale)
location = CGPoint(x: emoji.location.x + size.width/2,
y: emoji.location.y + size.height/2)
location = CGPoint(x: location.x + panOffset.width,
y: location.y + panOffset.height)
return location
}
private func drop(providers: [NSItemProvider], at location: CGPoint) -> Bool {
var found = providers.loadFirstObject(ofType: URL.self) { url in
print("dropped \(url)")
self.document.backgroundURL = url
}
if !found {
found = providers.loadObjects(ofType: String.self) { string in
self.document.addEmoji(string, at: location, size: self.defaultEmojiSize)
}
}
return found
}
private let defaultEmojiSize: CGFloat = 40
}
팔레트 선택할 수 있는 코드 추가
struct PaletteChooser: View {
@ObservedObject var document: EmojiArtDocument
@Binding var chosenPalette: String
var body: some View {
HStack {
Stepper(onIncrement: {
self.chosenPalette = self.document.palette(after: self.chosenPalette)
}, onDecrement: {
self.chosenPalette = self.document.palette(after: self.chosenPalette)
}, label: { EmptyView() })
Text(self.document.paletteNames[self.chosenPalette] ?? "")
}
.fixedSize(horizontal: true, vertical: false)
}
}
struct PaletteChooser_Previews: PreviewProvider {
static var previews: some View {
PaletteChooser(document: EmojiArtDocument(), chosenPalette: Binding.constant(""))
}
}
이번에 전체적으로 Property Wrappers에 대한 설명이 주를 이뤘고 코드적으로 보완된 것은
인터넷에서 backgroundImage를 로드할 때 더 나은 피드백은 제공 해주는 것
.onReceive를 통해 backgroundImage가 변경될 때 자동으로 확대/축소하는 것
팔레트 선택하는 코드가 추가된 것.
resource:
cs193p.sites.stanford.edu/
'Tech > SwiftUI' 카테고리의 다른 글
SwiftUI - Image (0) | 2022.07.09 |
---|---|
SwiftUI - Spacer (2) | 2022.03.27 |
스탠포드 SwiftUI강의 복습하기 Lecture8 (0) | 2021.02.28 |
스탠포드 SwiftUI강의 복습하기 Lecture7 (0) | 2021.02.21 |
스탠포드 SwiftUI강의 복습하기 Lecture6 (0) | 2021.02.14 |
- Total
- Today
- Yesterday
- 책 후기
- Algorithm
- objc
- SWIFT
- 책 추천
- 독서
- 딥러닝
- 알고리즘
- 애니메이션
- string
- 문자열
- Deep learning
- 머신러닝
- ReactiveX
- Xcode
- swiftUI
- Animation
- ios
- leetcode
- 스위프트
- wwdc
- 책
- swift5
- RX
- objective-c
- 스위프트UI
- rxswift
- iOS SwiftUI
- stanford SwiftUI
- ARC
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |