feat(view, viewmodels): 增加调色板按钮,增加报错提示

1. 增加从调色板中增加、删除、跳转和编辑 emoji 的功能
2. 增加通过 url 获取背景图时失败带报错

Signed-off-by: Ching <loooching@gmail.com>
This commit is contained in:
Ching 2023-02-26 19:34:35 +08:00
parent ba56e249a0
commit 566f3539d1
10 changed files with 282 additions and 27 deletions

View File

@ -17,7 +17,10 @@
248D14F029A230FE00AE4C0D /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 248D14EF29A230FE00AE4C0D /* Constants.swift */; };
248D14F329A2435000AE4C0D /* UtilityViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 248D14F129A2435000AE4C0D /* UtilityViews.swift */; };
248D14F629A243F200AE4C0D /* UtilityExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 248D14F529A243F200AE4C0D /* UtilityExtensions.swift */; };
24D4D3A729A9E02F0064E566 /* PaletteManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24D4D3A629A9E02F0064E566 /* PaletteManager.swift */; };
24E5FE8929A7AB4000794732 /* PaletteStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E5FE8829A7AB4000794732 /* PaletteStore.swift */; };
24E5FE8B29A9944600794732 /* PaletteChooser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E5FE8A29A9944600794732 /* PaletteChooser.swift */; };
24E5FE8D29A9B25800794732 /* PaletteEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E5FE8C29A9B25800794732 /* PaletteEditor.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -33,7 +36,10 @@
248D14EF29A230FE00AE4C0D /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
248D14F129A2435000AE4C0D /* UtilityViews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UtilityViews.swift; sourceTree = "<group>"; };
248D14F529A243F200AE4C0D /* UtilityExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UtilityExtensions.swift; sourceTree = "<group>"; };
24D4D3A629A9E02F0064E566 /* PaletteManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaletteManager.swift; sourceTree = "<group>"; };
24E5FE8829A7AB4000794732 /* PaletteStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaletteStore.swift; sourceTree = "<group>"; };
24E5FE8A29A9944600794732 /* PaletteChooser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaletteChooser.swift; sourceTree = "<group>"; };
24E5FE8C29A9B25800794732 /* PaletteEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaletteEditor.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -66,12 +72,15 @@
24145E7A29A1489700ECB9D1 /* EmojiArt */ = {
isa = PBXGroup;
children = (
24D4D3A629A9E02F0064E566 /* PaletteManager.swift */,
248D14EF29A230FE00AE4C0D /* Constants.swift */,
24145E7B29A1489700ECB9D1 /* EmojiArtApp.swift */,
24145E7D29A1489700ECB9D1 /* EmojiArtDocumentView.swift */,
248D14F129A2435000AE4C0D /* UtilityViews.swift */,
24145E8A29A1498500ECB9D1 /* EmojiArtModel.swift */,
24E5FE8829A7AB4000794732 /* PaletteStore.swift */,
24E5FE8A29A9944600794732 /* PaletteChooser.swift */,
24E5FE8C29A9B25800794732 /* PaletteEditor.swift */,
248D14F529A243F200AE4C0D /* UtilityExtensions.swift */,
248D14EB29A227C700AE4C0D /* EmojiArtDocument.swift */,
24145E8C29A225E900ECB9D1 /* EmojiArtModel.Background.swift */,
@ -165,10 +174,13 @@
248D14EC29A227C700AE4C0D /* EmojiArtDocument.swift in Sources */,
248D14F029A230FE00AE4C0D /* Constants.swift in Sources */,
24E5FE8929A7AB4000794732 /* PaletteStore.swift in Sources */,
24D4D3A729A9E02F0064E566 /* PaletteManager.swift in Sources */,
24145E8B29A1498500ECB9D1 /* EmojiArtModel.swift in Sources */,
24E5FE8D29A9B25800794732 /* PaletteEditor.swift in Sources */,
24145E8D29A225E900ECB9D1 /* EmojiArtModel.Background.swift in Sources */,
248D14F629A243F200AE4C0D /* UtilityExtensions.swift in Sources */,
248D14F329A2435000AE4C0D /* UtilityViews.swift in Sources */,
24E5FE8B29A9944600794732 /* PaletteChooser.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "E709882D-ACBF-4819-A5E7-499B9D69B8A9"
type = "1"
version = "2.0">
</Bucket>

View File

@ -8,8 +8,9 @@
import Foundation
import SwiftUI
struct const {
static let defaultEmojiFontSize: CGFloat = 40
static let coalescingInterval = 5.0
static let paletteEditorMinWidth: CGFloat = 400
static let paletteEditorMinHeight: CGFloat = 400
}

View File

@ -9,12 +9,14 @@ import SwiftUI
@main
struct EmojiArtApp: App {
let document = EmojiArtDocument()
let paletteStore = PaletteStore(named: "Default")
@StateObject var document = EmojiArtDocument()
@StateObject var paletteStore = PaletteStore(named: "Default")
var body: some Scene {
WindowGroup {
EmojiArtDocumentView(document: document)
.environmentObject(paletteStore)
}
}
}

View File

@ -66,9 +66,10 @@ class EmojiArtDocument: ObservableObject {
@Published var backgroundImage: UIImage?
@Published var backgroundImageFetchStatus = BackgroundImageFetchStatus.idle
enum BackgroundImageFetchStatus {
enum BackgroundImageFetchStatus: Equatable {
case idle
case fetching
case failed(URL )
}
// MARK: - Intent(s)
@ -109,6 +110,9 @@ class EmojiArtDocument: ObservableObject {
if imageData != nil {
self?.backgroundImage = UIImage(data: imageData!)
}
if self?.backgroundImage == nil {
self?.backgroundImageFetchStatus = .failed(url)
}
}
}
}

View File

@ -8,14 +8,12 @@
import SwiftUI
struct EmojiArtDocumentView: View {
let testEmojis = "😀😃😄😁😆😅😂🤣🥲🥹☺️😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛😝😜🤪🤨🧐🤓😎🥸🤩"
@ObservedObject var document: EmojiArtDocument
var body: some View {
VStack(spacing: 0) {
documentBody
palette
PaletteChooser(emojiFontSize: const.defaultEmojiFontSize)
}
}
@ -44,9 +42,31 @@ struct EmojiArtDocumentView: View {
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))
@ -143,25 +163,6 @@ struct EmojiArtDocumentView: View {
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 {

View File

@ -0,0 +1,111 @@
//
// PaletteChooser.swift
// EmojiArt
//
// Created by ching on 2023/2/25.
//
import SwiftUI
struct PaletteChooser: View {
var emojiFontSize: CGFloat = const.defaultEmojiFontSize
var emojiFont: Font { .system(size: emojiFontSize) }
@EnvironmentObject var store: PaletteStore
@State private var chosenPaletteIndex = 0
var body: some View {
HStack {
paletteButton
body(for: store.palette(at: chosenPaletteIndex)
)
}.clipped()
}
var paletteButton: some View {
Button {
withAnimation {
chosenPaletteIndex = (chosenPaletteIndex + 1) % store.palettes.count
}
} label: {
Image(systemName: "paintpalette")
}
.font(emojiFont)
.contextMenu { contextMenu }
}
@ViewBuilder
var contextMenu: some View {
AnimatedActionButton(title: "Edit", systemImage: "pencil") {
editing = true
}
AnimatedActionButton(title: "New", systemImage: "plus") {
store.insertPalette(named: "New", emojis: "", at: chosenPaletteIndex)
editing = true
}
AnimatedActionButton(title: "Delete", systemImage: "minus.circle") {
chosenPaletteIndex = store.removePalette(at: chosenPaletteIndex)
}
AnimatedActionButton(title: "Manager", systemImage: "slider.vertical.3") {
managing = true
}
gotoMenu
}
var gotoMenu: some View {
Menu {
ForEach(store.palettes) {
palette in AnimatedActionButton(title: palette.name) {
if let index = store.palettes.index(matching: palette) {
chosenPaletteIndex = index
}
}
}
} label: {
Label("Go To", systemImage: "text.insert")
}
}
func body(for palette: Palette) -> some View {
HStack {
Text(palette.name)
SrcollingEmojiView(emojis: palette.emojis)
.font(emojiFont)
}
.id(palette.id)
.transition(rollTransaction)
.popover(isPresented: $editing) {
PaletteEditor(palette: $store.palettes[chosenPaletteIndex])
}
.sheet(isPresented: $managing) {
PaletteManager()
}
}
@State private var editing = false
@State private var managing = false
var rollTransaction: AnyTransition {
AnyTransition.asymmetric(insertion: .offset(x: 0, y: emojiFontSize), removal: .offset(x: 0, y: -emojiFontSize))
}
}
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 PaletteChooser_Previews: PreviewProvider {
static var previews: some View {
PaletteChooser()
}
}

View File

@ -0,0 +1,69 @@
//
// PaletteEditor.swift
// EmojiArt
//
// Created by ching on 2023/2/25.
//
import SwiftUI
struct PaletteEditor: View {
@Binding var palette: Palette
var body: some View {
Form {
nameSection
addEmojiSection
removeEmojiSection
}
.frame(minWidth: const.paletteEditorMinWidth, minHeight: const.paletteEditorMinHeight)
.navigationTitle("Edit \(palette.name)")
}
var nameSection: some View {
Section(header: Text("Name")) {
TextField("Name", text: $palette.name)
}
}
@State private var emojiToAdd = ""
var addEmojiSection: some View {
Section(header: Text("Add Emoji")) {
TextField("", text: $emojiToAdd)
.onChange(of: emojiToAdd) { emojis in
addEmojis(emojis)
}
}
}
func addEmojis(_ emojis: String) {
palette.emojis = (emojis + palette.emojis)
.filter { $0.isEmoji }
}
var removeEmojiSection: some View {
Section(header: Text("Remove Emoji")) {
let emojis = palette.emojis.map { String($0) }
LazyVGrid(columns: [GridItem(.adaptive(minimum: const.defaultEmojiFontSize))]) {
ForEach(emojis, id: \.self) { emoji in
Text(emoji)
.onTapGesture {
withAnimation {
palette.emojis.removeAll(where: { String($0) == emoji })
}
}
}
}
}
}
}
struct PaletteEditor_Previews: PreviewProvider {
static var previews: some View {
PaletteEditor(palette: .constant(PaletteStore(named: "Priview").palette(at: 1)))
.previewLayout(.fixed(width: 350, height: 350))
PaletteEditor(palette: .constant(PaletteStore(named: "Priview").palette(at: 1)))
.previewLayout(.fixed(width: 350, height: 650))
}
}

View File

@ -0,0 +1,49 @@
//
// PaletteManager.swift
// EmojiArt
//
// Created by ching on 2023/2/25.
//
import SwiftUI
struct PaletteManager: View {
@EnvironmentObject var store: PaletteStore
@Environment(\.colorScheme) var colorScheme
@State private var editMode: EditMode = .inactive
var body: some View {
NavigationView {
List {
ForEach(store.palettes) { palette in
NavigationLink(destination: PaletteEditor(palette: $store.palettes[palette])) {
VStack(alignment: .leading) {
Text(palette.name)
Text(palette.emojis)
}
}
}
.onDelete { indexSet in
store.palettes.remove(atOffsets: indexSet)
}
.onMove { indexSet, newOffset in
store.palettes.move(fromOffsets: indexSet, toOffset: newOffset)
}
}
.navigationTitle("Manage Palettes")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
EditButton()
}
.environment(\.editMode, $editMode)
}
}
}
struct PaletteManager_Previews: PreviewProvider {
static var previews: some View {
PaletteManager()
.previewDevice("iPhone 14 Pro Max")
.environmentObject(PaletteStore(named: "preview"))
}
}

View File

@ -7,7 +7,7 @@
import SwiftUI
struct Palette: Identifiable, Codable {
struct Palette: Identifiable, Codable, Hashable {
var name: String
var emojis: String
var id: Int