diff --git a/EmojiArt.xcodeproj/project.pbxproj b/EmojiArt.xcodeproj/project.pbxproj index 2deb78c..1694084 100644 --- a/EmojiArt.xcodeproj/project.pbxproj +++ b/EmojiArt.xcodeproj/project.pbxproj @@ -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 = ""; }; 248D14F129A2435000AE4C0D /* UtilityViews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UtilityViews.swift; sourceTree = ""; }; 248D14F529A243F200AE4C0D /* UtilityExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UtilityExtensions.swift; sourceTree = ""; }; + 24D4D3A629A9E02F0064E566 /* PaletteManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaletteManager.swift; sourceTree = ""; }; 24E5FE8829A7AB4000794732 /* PaletteStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaletteStore.swift; sourceTree = ""; }; + 24E5FE8A29A9944600794732 /* PaletteChooser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaletteChooser.swift; sourceTree = ""; }; + 24E5FE8C29A9B25800794732 /* PaletteEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaletteEditor.swift; sourceTree = ""; }; /* 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; }; diff --git a/EmojiArt.xcodeproj/xcuserdata/ching.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/EmojiArt.xcodeproj/xcuserdata/ching.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..47e5b1d --- /dev/null +++ b/EmojiArt.xcodeproj/xcuserdata/ching.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/EmojiArt/Constants.swift b/EmojiArt/Constants.swift index 9e74756..4481f8d 100644 --- a/EmojiArt/Constants.swift +++ b/EmojiArt/Constants.swift @@ -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 } diff --git a/EmojiArt/EmojiArtApp.swift b/EmojiArt/EmojiArtApp.swift index 9aeb66c..d2ae138 100644 --- a/EmojiArt/EmojiArtApp.swift +++ b/EmojiArt/EmojiArtApp.swift @@ -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) } } } + diff --git a/EmojiArt/EmojiArtDocument.swift b/EmojiArt/EmojiArtDocument.swift index bb697a4..f8856f7 100644 --- a/EmojiArt/EmojiArtDocument.swift +++ b/EmojiArt/EmojiArtDocument.swift @@ -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) + } } } } diff --git a/EmojiArt/EmojiArtDocumentView.swift b/EmojiArt/EmojiArtDocumentView.swift index a18dadc..db32566 100644 --- a/EmojiArt/EmojiArtDocumentView.swift +++ b/EmojiArt/EmojiArtDocumentView.swift @@ -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 { diff --git a/EmojiArt/PaletteChooser.swift b/EmojiArt/PaletteChooser.swift new file mode 100644 index 0000000..342f58a --- /dev/null +++ b/EmojiArt/PaletteChooser.swift @@ -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() + } +} diff --git a/EmojiArt/PaletteEditor.swift b/EmojiArt/PaletteEditor.swift new file mode 100644 index 0000000..b753642 --- /dev/null +++ b/EmojiArt/PaletteEditor.swift @@ -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)) + } +} diff --git a/EmojiArt/PaletteManager.swift b/EmojiArt/PaletteManager.swift new file mode 100644 index 0000000..1739244 --- /dev/null +++ b/EmojiArt/PaletteManager.swift @@ -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")) + } +} diff --git a/EmojiArt/PaletteStore.swift b/EmojiArt/PaletteStore.swift index 193693a..33d66f3 100644 --- a/EmojiArt/PaletteStore.swift +++ b/EmojiArt/PaletteStore.swift @@ -7,7 +7,7 @@ import SwiftUI -struct Palette: Identifiable, Codable { +struct Palette: Identifiable, Codable, Hashable { var name: String var emojis: String var id: Int