cs193p-emojiart/EmojiArt/EmojiArtDocumentView.swift
Ching 566f3539d1 feat(view, viewmodels): 增加调色板按钮,增加报错提示
1. 增加从调色板中增加、删除、跳转和编辑 emoji 的功能
2. 增加通过 url 获取背景图时失败带报错

Signed-off-by: Ching <loooching@gmail.com>
2023-02-26 19:34:35 +08:00

173 lines
6.3 KiB
Swift

//
// EmojiArtDocumentView.swift
// EmojiArt
//
// Created by ching on 2023/2/19.
//
import SwiftUI
struct EmojiArtDocumentView: View {
@ObservedObject var document: EmojiArtDocument
var body: some View {
VStack(spacing: 0) {
documentBody
PaletteChooser(emojiFontSize: const.defaultEmojiFontSize)
}
}
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()))
.alert(item: $alertToShow) { alertToShow in
alertToShow.alert()
}
.onChange(of: document.backgroundImageFetchStatus) { status in
switch status {
case .failed(let url):
showBackgroundImageFetchFailedAlert(url)
default:
break
}
}
}
}
@State private var alertToShow: IdentifiableAlert?
private func showBackgroundImageFetchFailedAlert(_ url: URL) {
alertToShow = IdentifiableAlert(id: "Fetch failed: " + url.absoluteString, alert: {
Alert(
title: Text("Background Image Fetch"),
message: Text("Couldn't load image from \(url)."),
dismissButton: .default(Text("OK")))
})
}
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)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
EmojiArtDocumentView(document: EmojiArtDocument())
}
}