feat(view, viewmodels): 增加调色板按钮,增加报错提示
1. 增加从调色板中增加、删除、跳转和编辑 emoji 的功能 2. 增加通过 url 获取背景图时失败带报错 Signed-off-by: Ching <loooching@gmail.com>
This commit is contained in:
parent
ba56e249a0
commit
566f3539d1
@ -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;
|
||||
};
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Bucket
|
||||
uuid = "E709882D-ACBF-4819-A5E7-499B9D69B8A9"
|
||||
type = "1"
|
||||
version = "2.0">
|
||||
</Bucket>
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
111
EmojiArt/PaletteChooser.swift
Normal file
111
EmojiArt/PaletteChooser.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
69
EmojiArt/PaletteEditor.swift
Normal file
69
EmojiArt/PaletteEditor.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
49
EmojiArt/PaletteManager.swift
Normal file
49
EmojiArt/PaletteManager.swift
Normal 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"))
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct Palette: Identifiable, Codable {
|
||||
struct Palette: Identifiable, Codable, Hashable {
|
||||
var name: String
|
||||
var emojis: String
|
||||
var id: Int
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user