cs193p-emojiart/EmojiArt/EmojiArtDocumentView.swift
Ching 89d38faea4 feat(view, viewmodel, model): 增加 document 和 palette,增加手势
1. 可以从另一个 app 中拖入图片或图片链接作为背景
2. 可以从 palette 中拖入 emoji
3. 可以拖动和双击缩放背景图

Signed-off-by: Ching <loooching@gmail.com>
2023-02-20 21:45:33 +08:00

172 lines
6.1 KiB
Swift

//
// EmojiArtDocumentView.swift
// EmojiArt
//
// Created by ching on 2023/2/19.
//
import SwiftUI
struct EmojiArtDocumentView: View {
let testEmojis = "😀😃😄😁😆😅😂🤣🥲🥹☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛😝😜🤪🤨🧐🤓😎🥸🤩"
@ObservedObject var document: EmojiArtDocument
var body: some View {
VStack(spacing: 0) {
documentBody
palette
}
}
var documentBody: some View {
GeometryReader { geometry in
ZStack {
Color.yellow.overlay(
OptionalImage(uiImage: document.backgroundImage)
.scaleEffect(zoomScale)
.position(convertFromEmojiCoordinates((0, 0), in: geometry))
)
.gesture(doubleTapToZoom(in: geometry.size))
if document.backgroundImageFetchStatus == .fetching {
ProgressView().scaleEffect(2)
} else {
ForEach(document.emojis) { emoji in
Text(emoji.text)
.font(.system(size: fontSize(for: emoji)))
.scaleEffect(zoomScale)
.position(position(for: emoji, in: geometry))
}
}
}
.clipped()
.onDrop(of: [.plainText, .url, .image], isTargeted: nil) { providers, location in
drop(providers: providers, at: location, in: geometry)
}
.gesture(panGesture().simultaneously(with: zoomGesture()))
}
}
private func drop(providers: [NSItemProvider], at location: CGPoint, in geometry: GeometryProxy) -> Bool {
var found = providers.loadObjects(ofType: URL.self) { url in
document.setBackground(EmojiArtModel.Background.url(url.imageURL))
}
if !found {
found = providers.loadObjects(ofType: UIImage.self) { image in
if let data = image.jpegData(compressionQuality: 1.0) {
document.setBackground(.imageData(data))
}
}
}
if !found {
found = providers.loadObjects(ofType: String.self) { string in
if let emoji = string.first, emoji.isEmoji {
document.addEmoji(
String(emoji),
at: convertToEmojiCoordinates(location, in: geometry),
size: const.defaultEmojiFontSize / zoomScale)
}
}
}
return found
}
private func fontSize(for emoji: EmojiArtModel.Emoji) -> CGFloat {
CGFloat(emoji.size)
}
private func position(for emoji: EmojiArtModel.Emoji, in geometry: GeometryProxy) -> CGPoint {
convertFromEmojiCoordinates((emoji.x, emoji.y), in: geometry)
}
private func convertToEmojiCoordinates(_ location: CGPoint, in geometry: GeometryProxy) -> (x: Int, y: Int) {
let center = geometry.frame(in: .local).center
let location = CGPoint(
x: (location.x - center.x - panOffset.width) / zoomScale,
y: (location.y - center.y - panOffset.height) / zoomScale)
return (Int(location.x), Int(location.y))
}
private func convertFromEmojiCoordinates(_ location: (x: Int, y: Int), in geometry: GeometryProxy) -> CGPoint {
let center = geometry.frame(in: .local).center
return CGPoint(
x: center.x + CGFloat(location.x) * zoomScale + panOffset.width,
y: center.y + CGFloat(location.y) * zoomScale + panOffset.height)
}
@State private var steadyStatePanOffset: CGSize = .zero
@GestureState private var gesturePanOffset: CGSize = .zero
private var panOffset: CGSize {
(steadyStatePanOffset + gesturePanOffset) * zoomScale
}
@State private var steadyStateZoomScale: CGFloat = 1
@GestureState private var gestureZoomScale: CGFloat = 1
private var zoomScale: CGFloat {
steadyStateZoomScale * gestureZoomScale
}
private func panGesture() -> some Gesture {
DragGesture()
.updating($gesturePanOffset) { latestDragGestureValue, gesturePanOffset, _ in
gesturePanOffset = latestDragGestureValue.translation / zoomScale
}
.onEnded { finalDragGestureValue in
steadyStatePanOffset = steadyStatePanOffset + (finalDragGestureValue.translation / zoomScale)
}
}
private func zoomGesture() -> some Gesture {
MagnificationGesture()
.updating($gestureZoomScale) { latestGestureScale, gestureZoomScale, _ in
gestureZoomScale = latestGestureScale
}
.onEnded { gestureScaleAtEnd in
steadyStateZoomScale *= gestureScaleAtEnd
}
}
private func doubleTapToZoom(in size: CGSize) -> some Gesture {
TapGesture(count: 2)
.onEnded {
withAnimation {
zoomToFit(document.backgroundImage, in: size)
}
}
}
private func zoomToFit(_ image: UIImage?, in size: CGSize) {
if let image = image, image.size.width > 0, image.size.height > 0, size.width > 0, size.height > 0 {
let hZoom = size.width / image.size.width
let vZoom = size.height / image.size.height
steadyStatePanOffset = .zero
steadyStateZoomScale = min(hZoom, vZoom)
}
}
var palette: some View {
SrcollingEmojiView(emojis: testEmojis)
.font(.system(size: const.defaultEmojiFontSize))
}
}
struct SrcollingEmojiView: View {
let emojis: String
var body: some View {
ScrollView(.horizontal) {
HStack {
ForEach(emojis.map { String($0) }, id: \.self) { emoji in
Text(emoji)
.onDrag { NSItemProvider(object: emoji as NSString) }
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
EmojiArtDocumentView(document: EmojiArtDocument())
}
}