IOS

3. IOS 강의 Gestures JSON

dennis 2021. 1. 19. 16:57

www.youtube.com/watch?v=mz-rNLWJ0bk

 

AnimatableSystemFontModifier.swift
0.00MB
Assignment 4.pdf
0.07MB
Assignment 5.pdf
0.07MB
Slides.pdf
0.82MB
EmojiArtExtensions.swift
0.01MB

소스

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!)
            }
        }
    }
}