티스토리 뷰
앱을 유지할 수 있는 여러 가지 방법
- FileManager
- CoreData
- CloudKit
- UserDefault
오늘 사용할 유저디폴트를 살펴본다.
유저 디폴트를 사용하기 위해서 인스턴스가 필요하다.
let defualts = UserDefaults.standard
데이터 저장하는 방법
defaults.set(object, forKey: “SomeKey”)
여기서 object는 property list여야 한다.
(Bool, Int, Double, String 등)
데이터 추출하는 방법
let i: Int = defaults.integer(forKey: “MyInteger”)
let d: Data? = defaults.data(forKey: “MyData”)
let a = array(forKey: “MyArray”)
하지만 마지막 줄과 같이 배열 추출에는 Array<Any>로 리턴될 것이다.
리턴된 Array Element를 as로 타입 캐스팅을 해야 한다.
이렇게 하기 귀찮으면 codable을 사용해 data로 넣는 방법이 있다.
제스처
탭, 클릭, 스와이프 세미한 제스처에 대한 상호 작용을 정의할 수 있다.
아래는 제스처 구조체 종류이다.
유저에 대해서 제스처 인풋을 받고 처리를 한다.
콜백에서는 뷰의 상태를 업데이트하거나 특정 작업을 수행한다.
SwiftUI에서 Gesture modifiers는 3가지 방식으로 업데이트를 받을 수 있다.
- updating(_:body:)
- onChanged(_:)
- onEnded(_:)
Discrete Gesture
더블 탭을 사용하려면 아래와 같이 사용하면 된다.
핸들링 필요하면 아래와 같이 사용하면 된다.
var theGesture: some Gesture {
return TapGesture(count: 2)
.onEnded { /do something
}
}
쉬운 방법 easy to use
myView.onTapGesture(count: Int) { /do something }
myView.onLongPressGesture(...) { /do something }
Non-Discrete Gesture
DragGesture, MagnificationGesture, RotationGesture, LongPressGesture
이는 argument를 준다. (discrete랑 다른 점)
value는 드래그 제스처가 끝났을 때의 상태를 알려준다.
var theGesture: some Gesture {
return TapGesture(count: 2)
.onEnded { value in
/do something
}
}
핸들링하는 방법 여기서 타입은 어떤 타입이든 가능하다.
@GestureState var myGestureState: MyGestureStateType = <starting value>
var theGesture: some Gesture {
DragGesture(...)
.updating($myGestureState) { value, myGestureState, transaction in
myGestureState = /usally something related to value
}
.onEnded { value in
/do something
}
}
.updating에서는 손가락이 움직이면 클로저를 호출한다.
@GestureState 변수에는 $가 앞에 붙어야 하는 것 잊지 마라.
myGestureState argument는 클로저에 필수적이다.
(여기서 @GestureState를 변경할 수 있는 유일한 곳)
제스처가 액티브할 때만 가능하니깐 클로저 내부에서만 상태를 변경할 수 있다.
@GestureState에 대한 변경이 필요 없다면 .onChanged로 처리해라.
var theGesture: some Gesture {
DragGesture(...)
.onChanged { value in
/ do something with value
}
.onEnded { value in
/do something
}
}
손가락 위치가 크게 상관없을 때 사용하면 좋음.
제스처가 일어날 때 정보를 얻으려면 @GestureState
.updating을 제스처에 추가하고 .updating에서 value를 사용해서 처리해라.
.onEnded로 스테이트를 변경해 상황에 맞는 처리를 해준다.
EmojiArt 키워드
- JSON/Codable
- UserDefaults
- Optional Image
- Animatable Font Size
- Gesture
모델
struct EmojiArt: Codable {
var backgroundURL: URL?
var emojis = [Emoji]()
struct Emoji: Identifiable, Codable, Hashable {
let text: String
var x: Int
var y: Int
var size: Int
let id: Int
fileprivate init(text: String, x: Int, y: Int, size: Int, id: Int) {
self.text = text
self.x = x
self.y = y
self.size = size
self.id = id
}
}
var json: Data? {
return try? JSONEncoder().encode(self)
}
init?(json: Data?) {
if json != nil, let newEmojiArt = try? JSONDecoder().decode(EmojiArt.self, from: json!) {
self = newEmojiArt
} else {
return nil
}
}
init() {}
private var uniqueEmojiId = 0
mutating func addEmoji(_ text: String, x: Int, y: Int, size: Int) {
uniqueEmojiId += 1
emojis.append(Emoji(text: text,
x: x,
y: y,
size: size,
id: uniqueEmojiId))
}
}
json변수가 추가되었다.
EmojiArt에 인코딩 디코딩이 가능하도록 Codable을 붙여주고
변수 emojis 타입이 [Emoji]니 Emoji한테도 Codable을 붙여준다.
초기화 init은 두 가지로 초기화 실패 시 빈 EmojiArt()를 생성해준다.
뷰모델
class EmojiArtDocument: ObservableObject {
static let palette: String = "👩💻🙋♀️🙇♀️💃🏄♀️🧘♂️"
private var emojiArt: EmojiArt {
willSet {
objectWillChange.send()
}
didSet {
UserDefaults.standard.set(emojiArt.json, forKey: EmojiArtDocument.untitled)
}
}
private static let untitled = "EmojiArtDocument.Untitled"
init() {
emojiArt = EmojiArt(json: UserDefaults.standard.data(forKey: EmojiArtDocument.untitled)) ?? EmojiArt()
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))
}
}
func setBackgroundURL(_ url: URL?) {
emojiArt.backgroundURL = url?.imageURL
fetchBackgroundImageData()
}
private func fetchBackgroundImageData() {
backgroundImage = nil
if let url = self.emojiArt.backgroundURL {
DispatchQueue.global(qos: .userInitiated).async {
if let imageData = try? Data(contentsOf: url) {
DispatchQueue.main.async {
if url == self.emojiArt.backgroundURL {
self.backgroundImage = UIImage(data: imageData)
}
}
}
}
}
}
}
extension EmojiArt.Emoji {
var fontSize: CGFloat { CGFloat(self.size) }
var location: CGPoint { CGPoint(x: CGFloat(x), y: CGFloat(y)) }
}
emojiArt변수에 @Published를 제거하고 변수를 옵저빙 하다가 objectWillChange.send()를 호출해주거나 유저 디폴트에 저장한다.
유저 디폴트에 저장된 값의 여부에 따른 초기화를 해준다.
뷰
struct EmojiArtDocumentView: View {
@ObservedObject var document: EmojiArtDocument
var body: some View {
ScrollView(.horizontal) {
HStack {
ForEach(EmojiArtDocument.palette.map { String($0) }, id: \.self) { emoji in
Text(emoji)
.font(Font.system(size: defaultEmojiSize))
.onDrag { return NSItemProvider(object: emoji as NSString) }
}
}
}
.padding(.horizontal)
GeometryReader { geometry in
ZStack{
Color.white.overlay(
OptionalImage(uiImage: self.document.backgroundImage)
.scaleEffect(self.zoomScale)
.offset(self.panOffset)
)
.gesture(self.doubleTapToZoom(in: geometry.size))
.gesture(self.zoomGesture())
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])
.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)
}
}
}
@State private var steadyStatezoomScale: CGFloat = 1.0
@GestureState private var gestureZoomScale: CGFloat = 1.0
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.setBackgroundURL(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
}
핀치 제스처와, 더블 탭 제스처가 추가되었다.
스탠포드에서 제공해주는 애니메이션 관련 코드
아래 코드는
ViewModifier는 주어진 크기의 폰트로 시스템 폰트를 설정한다. (weight, design도)
크기 변경에 애니메이션 효과를 적용할 수 있다.
struct AnimatableSystemFontModifier: AnimatableModifier {
var size: CGFloat
var weight: Font.Weight = .regular
var design: Font.Design = .default
func body(content: Content) -> some View {
content.font(Font.system(size: size, weight: weight, design: design))
}
var animatableData: CGFloat {
get { size }
set { size = newValue }
}
}
extension View {
func font(animatableWithSize size: CGFloat, weight: Font.Weight = .regular, design: Font.Design = .default) -> some View {
self.modifier(AnimatableSystemFontModifier(size: size, weight: weight, design: design))
}
}
resource:
cs193p.sites.stanford.edu/
'Tech > SwiftUI' 카테고리의 다른 글
SwiftUI - Spacer (2) | 2022.03.27 |
---|---|
스탠포드 SwiftUI강의 복습하기 Lecture9 (0) | 2021.02.28 |
스탠포드 SwiftUI강의 복습하기 Lecture7 (0) | 2021.02.21 |
스탠포드 SwiftUI강의 복습하기 Lecture6 (0) | 2021.02.14 |
스탠포드 SwiftUI강의 복습하기 Lecture5 (0) | 2021.02.12 |
- Total
- Today
- Yesterday
- Animation
- 문자열
- ReactiveX
- 딥러닝
- swift5
- wwdc
- Xcode
- ARC
- Deep learning
- RX
- rxswift
- leetcode
- 알고리즘
- 책
- swiftUI
- objc
- string
- Algorithm
- SWIFT
- iOS SwiftUI
- 머신러닝
- 독서
- 스위프트
- objective-c
- stanford SwiftUI
- ios
- 애니메이션
- 책 추천
- 스위프트UI
- 책 후기
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |