3. IOS 강의 Gestures JSON
www.youtube.com/watch?v=mz-rNLWJ0bk
소스
EmojiArt.swift
//
// EmojiArt.swift
// EmojiArt
//
// Created by user on 2021/01/18.
// Copyright © 2021 CS193p Instructor. All rights reserved.
//
import Foundation
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))
}
}
Codable Protocol
데이터를 다른 형식으로 만들어 저장하고자 한다면, Codable 이라는 프로토콜을 따르게 해주면 됩니다.
일반적으로 Swift의 대부분의 자료형은 Codable 프로토콜을 준수하기 때문에, 따로 선언이 필요없이 Codable
프로토콜의 채택이 가능합니다.
JSONDecoder(): Codable 프로토콜을 준수하는 객체를 JSON 형식으로 만드는 방법 - 인코딩, 디코딩 가능
Hashable?
velog.io/@dev-lena/Hashable-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C
Hashable 프로토콜
Dictionary의 Key 타입을 커스텀 타입으로 정할 때 필요한 Hashable 프로토콜에 대해서 알아봤습니다 :)
velog.io
EmojiArtDocument.swift
//
// EmojiArtDocument.swift
// EmojiArt
//
// Created by user on 2021/01/18.
// Copyright © 2021 CS193p Instructor. All rights reserved.
//
import SwiftUI
class EmojiArtDocument: ObservableObject
{
static let palette: String = "⭐️⛈🍎🌏🥨⚾️"
private static let untitled = "EmojiArtDocument.Untitled"
@Published private var emojiArt: EmojiArt {
// @Published - not work
// willSet {
// objectWillChange.send()
// }
didSet {
print("json = \(emojiArt.json?.utf8 ?? "nil")")
UserDefaults.standard.set(emojiArt.json, forKey: 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)) }
}
UserDefaults
간단한 정의
UserDefaults 는 Key - Value 스타일, 즉 키 값에 해당하는 값을 읽거나 쓸 수 있는 기능을 제공하는 사전형
(Dictionary)과 비슷한 클래스인데, 여기다 파일에 기록하고 나중에 불러올 수 있는 기능까지 제공한다.
대체로 standard 라는 이름의 싱글톤을 이용해 데이터에 엑세스 하기 때문에
UserDefaults 자체를 인스턴스화 하거나 하는 일은 거의 없을 거라 생각된다.
UserDefaults를 사용하는 방법은 다음 순서를 따른다.
- UserDefaults를 통해 plist에 데이터를 저장한다.
- UserDefaults는 사용자의 정보를 저장하는 싱글톤 인스턴스이다.
- 간단한 사용자 정보를 저장 및 불러오는게 가능하지만,
- 내부적으로 plist 파일에 저장되기 때문에 보안상 강력하지는 않다.
didSet, willSet in Swift
스위프트는 프로퍼티 옵저버로 didSet, willSet을 제공합니다. 얘네들의 역할은 프로퍼티의 값이 변경되기 직전,
직후를 감지하는 것입니다. 따라서 이때 원하는 작업을 수행 할 수 있습니다.
프로퍼티 옵저버를 사용하기 위해서는 프로퍼티의 값이 반드시 초기화 되어 있어야합니다.
내부적으로 초기화된 프로퍼티의 값을 감시하기 때문이지 않을까 싶습니다. 또한 클래스의 init()안에서 값을
할당 할 때는 didSet, willSet은 호출 되지않습니다.
EmojiArtDocumentView.swift
//
// EmojiArtDocumentView.swift
// EmojiArt
//
// Created by user on 2021/01/18.
// Copyright © 2021 CS193p Instructor. All rights reserved.
//
import SwiftUI
struct EmojiArtDocumentView: View {
@ObservedObject var document: EmojiArtDocument
var body: some View {
VStack {
ScrollView(.horizontal) {
HStack {
ForEach(EmojiArtDocument.palette.map { String($0) }, id: \.self) { emoji in
Text(emoji)
.font(Font.system(size: self.defaultEmojiSize))
.onDrag { 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))
ForEach(self.document.emojis) { emoji in
Text(emoji.text)
.font(animatableWithSize: emoji.fontSize * self.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 = CGPoint(x: location.x, y: geometry.convert(location, from: .global).y)
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, transaction 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.width > 0, 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: location.x + size.width/2, y: 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
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
}
@GestureState
- 제스쳐 상태를 추적하는 특정 property wrapper를 제공
- @GestureState. 간단한 @State속성 래퍼를 사용하여 동일한 작업을 수행 할 수 있지만 @GestureState제스처가 종료되면 속성을
초기값으로 자동으로 다시 설정하는 추가 기능이 제공
@state
https://developer.apple.com/documentation/swiftui/state
Apple Developer Documentation
developer.apple.com
- SwiftUI는 state로 선언한 모든 프로퍼티의 스토리지를 관리.
- state 값이 변경되면 view가 appearance를 invalidates하고 body를 다시 계산(recomputes)합니다.
- 주어진 view에서 state를 single source of truth로 사용 할 것.
- state인스턴스는 value자체가 아님. 값을 읽고 변경하는 수단. state의 기본값에 접근하려면 value 프로퍼티를 사용 할 것.
- view의 body에서만 state프로퍼티에 접근 할 것. 따라서 view의 클라이언트에서 state에 접근하지 못하도록 state프로퍼티를 private으로 선언할 것. (state는 특정 view에 속하고, view 외부에서 "절대" 사용되지 않은 간단한 프로퍼티에 적합 -> 해당 상태가 절대로 escape되지 않도록 설계되었다는 것을 강조하기 위해 private으로 표시하는 것이 중요함.)
- @State를 앞에 추가하면 SwiftUI가 자동으로 변경사항을 observe하고 해당 state를 사용하는 view부분을 업데이트.
OptionalImage.swift
//
// OptionalImage.swift
// EmojiArt
//
// Created by user on 2021/01/19.
// Copyright © 2021 CS193p Instructor. All rights reserved.
//
import SwiftUI
struct OptionalImage: View {
var uiImage: UIImage?
var body: some View {
Group {
if uiImage != nil {
Image(uiImage: uiImage!)
}
}
}
}