animations in emojirow

animated deleting/downloading emojis
stopped it glitching when downloading/deleting an emoji
removed refresh() and uiID cos its not necessary sob
moved downloadImage() to stickerprotocol extension
bundled an image so test emoji loading is faster
This commit is contained in:
neon443
2026-03-08 22:38:14 +00:00
parent 7d83ae2101
commit ccaaa8e519
10 changed files with 77 additions and 65 deletions

BIN
Resources/image.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

View File

@@ -55,6 +55,9 @@
A986A6CE2EB659E000B6E0FA /* StickerBrowserDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = A986A6CB2EB659E000B6E0FA /* StickerBrowserDataSource.swift */; }; A986A6CE2EB659E000B6E0FA /* StickerBrowserDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = A986A6CB2EB659E000B6E0FA /* StickerBrowserDataSource.swift */; };
A986A6CF2EB659E000B6E0FA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A986A6C62EB659E000B6E0FA /* Assets.xcassets */; }; A986A6CF2EB659E000B6E0FA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A986A6C62EB659E000B6E0FA /* Assets.xcassets */; };
A986A6D12EB659E000B6E0FA /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A986A6C92EB659E000B6E0FA /* MainInterface.storyboard */; }; A986A6D12EB659E000B6E0FA /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A986A6C92EB659E000B6E0FA /* MainInterface.storyboard */; };
A99589AF2F5E0D83003A4A8C /* image.png in Resources */ = {isa = PBXBuildFile; fileRef = A99589AE2F5E0D83003A4A8C /* image.png */; };
A99589B02F5E0D83003A4A8C /* image.png in Resources */ = {isa = PBXBuildFile; fileRef = A99589AE2F5E0D83003A4A8C /* image.png */; };
A99589B12F5E0D83003A4A8C /* image.png in Resources */ = {isa = PBXBuildFile; fileRef = A99589AE2F5E0D83003A4A8C /* image.png */; };
A9B9A82E2EB2CCBE004C9245 /* StickerSlackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B9A82C2EB2CCBE004C9245 /* StickerSlackTests.swift */; }; A9B9A82E2EB2CCBE004C9245 /* StickerSlackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B9A82C2EB2CCBE004C9245 /* StickerSlackTests.swift */; };
A9B9A82F2EB2CCED004C9245 /* EmojiHoarder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949B1F72EA04F2300215164 /* EmojiHoarder.swift */; }; A9B9A82F2EB2CCED004C9245 /* EmojiHoarder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949B1F72EA04F2300215164 /* EmojiHoarder.swift */; };
A9B9A8302EB2CD0B004C9245 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = A924C3712EA9127200F20781 /* Emoji.swift */; }; A9B9A8302EB2CD0B004C9245 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = A924C3712EA9127200F20781 /* Emoji.swift */; };
@@ -156,6 +159,7 @@
A986A6C82EB659E000B6E0FA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; }; A986A6C82EB659E000B6E0FA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
A986A6CA2EB659E000B6E0FA /* MessagesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewController.swift; sourceTree = "<group>"; }; A986A6CA2EB659E000B6E0FA /* MessagesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewController.swift; sourceTree = "<group>"; };
A986A6CB2EB659E000B6E0FA /* StickerBrowserDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerBrowserDataSource.swift; sourceTree = "<group>"; }; A986A6CB2EB659E000B6E0FA /* StickerBrowserDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerBrowserDataSource.swift; sourceTree = "<group>"; };
A99589AE2F5E0D83003A4A8C /* image.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = image.png; sourceTree = "<group>"; };
A9B9A8232EB2CCB5004C9245 /* StickerSlackTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StickerSlackTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; A9B9A8232EB2CCB5004C9245 /* StickerSlackTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StickerSlackTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
A9B9A82C2EB2CCBE004C9245 /* StickerSlackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerSlackTests.swift; sourceTree = "<group>"; }; A9B9A82C2EB2CCBE004C9245 /* StickerSlackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerSlackTests.swift; sourceTree = "<group>"; };
A9BBC5172EB8FA4500FFE82F /* ViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifiers.swift; sourceTree = "<group>"; }; A9BBC5172EB8FA4500FFE82F /* ViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifiers.swift; sourceTree = "<group>"; };
@@ -206,6 +210,7 @@
A9104C742EB3AE4700D160EA /* Resources */ = { A9104C742EB3AE4700D160EA /* Resources */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A99589AE2F5E0D83003A4A8C /* image.png */,
A9104C732EB3AE4700D160EA /* StickerSlack.icon */, A9104C732EB3AE4700D160EA /* StickerSlack.icon */,
A949B1EF2EA04E8200215164 /* Assets.xcassets */, A949B1EF2EA04E8200215164 /* Assets.xcassets */,
A9104C722EB3AE4700D160EA /* Icon.pxd */, A9104C722EB3AE4700D160EA /* Icon.pxd */,
@@ -504,6 +509,7 @@
A9104C762EB3AE4700D160EA /* StickerSlack.icon in Resources */, A9104C762EB3AE4700D160EA /* StickerSlack.icon in Resources */,
A949B1F32EA04E8200215164 /* Assets.xcassets in Resources */, A949B1F32EA04E8200215164 /* Assets.xcassets in Resources */,
A9104C752EB3AE4700D160EA /* Icon.pxd in Resources */, A9104C752EB3AE4700D160EA /* Icon.pxd in Resources */,
A99589AF2F5E0D83003A4A8C /* image.png in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -512,6 +518,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
A986A6CF2EB659E000B6E0FA /* Assets.xcassets in Resources */, A986A6CF2EB659E000B6E0FA /* Assets.xcassets in Resources */,
A99589B12F5E0D83003A4A8C /* image.png in Resources */,
A986A6D12EB659E000B6E0FA /* MainInterface.storyboard in Resources */, A986A6D12EB659E000B6E0FA /* MainInterface.storyboard in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -523,6 +530,7 @@
A9104C772EB3AE4700D160EA /* Icon.pxd in Resources */, A9104C772EB3AE4700D160EA /* Icon.pxd in Resources */,
A9104C7C2EB3AE6300D160EA /* Assets.xcassets in Resources */, A9104C7C2EB3AE6300D160EA /* Assets.xcassets in Resources */,
A9104C782EB3AE4700D160EA /* StickerSlack.icon in Resources */, A9104C782EB3AE4700D160EA /* StickerSlack.icon in Resources */,
A99589B02F5E0D83003A4A8C /* image.png in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@@ -13,7 +13,6 @@ import UniformTypeIdentifiers
struct Emoji: StickerProtocol { struct Emoji: StickerProtocol {
var id: UUID var id: UUID
var uiID: UUID
var name: String var name: String
var localImageURLString: String { var localImageURLString: String {
let urlString = remoteImageURL.absoluteString let urlString = remoteImageURL.absoluteString
@@ -32,7 +31,6 @@ struct Emoji: StickerProtocol {
init(from decoder: any Decoder) throws { init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(UUID.self, forKey: .id) self.id = try container.decode(UUID.self, forKey: .id)
self.uiID = id
self.name = try container.decode(String.self, forKey: .name) self.name = try container.decode(String.self, forKey: .name)
self.remoteImageURL = try container.decode(URL.self, forKey: .remoteImageURL) self.remoteImageURL = try container.decode(URL.self, forKey: .remoteImageURL)
} }
@@ -43,37 +41,12 @@ struct Emoji: StickerProtocol {
id: UUID = UUID() id: UUID = UUID()
) { ) {
self.id = id self.id = id
self.uiID = id
self.name = name self.name = name
self.remoteImageURL = url self.remoteImageURL = url
} }
nonisolated
func downloadImage() async throws {
if let data = try? await Data(contentsOf: localImageURL),
let _ = UIImage(data: data) {
return
}
var (data, _) = try await URLSession.shared.data(from: remoteImageURL)
if let uiImage = UIImage(data: data),
let cgImage = uiImage.cgImage,
await !self.localImageURLString.contains(".gif"),
cgImage.width < 300 || cgImage.height < 300 {
data = await resize(image: uiImage, to: CGSize(width: 300, height: 300)).pngData()!
}
try! await data.write(to: localImageURL)
return
}
@MainActor
mutating func refresh() {
withAnimation { self.uiID = UUID() }
}
static var test: Emoji = Emoji( static var test: Emoji = Emoji(
name: "s?", name: "a test slack emoji",
url: URL(string: "https://files.catbox.moe/d8go4n.png")! url: Bundle.main.url(forResource: "image", withExtension: "png")!
) )
} }

View File

@@ -37,7 +37,7 @@ class EmojiHoarder: ObservableObject {
self.showWelcome = !UserDefaults.standard.bool(forKey: "showWelcome") self.showWelcome = !UserDefaults.standard.bool(forKey: "showWelcome")
let localDB = loadLocalDB() let localDB = loadLocalDB()
withAnimation { self.emojis = localDB } withAnimation(.snappy) { self.emojis = localDB }
loadTrie() loadTrie()
if !skipIndex { buildTrie() } if !skipIndex { buildTrie() }
@@ -204,7 +204,7 @@ class EmojiHoarder: ObservableObject {
private func loadRemoteDB() async { private func loadRemoteDB() async {
async let fetched = self.fetchRemoteDB() async let fetched = self.fetchRemoteDB()
if let fetched = await fetched { if let fetched = await fetched {
withAnimation { self.emojis = fetched } withAnimation(.snappy) { self.emojis = fetched }
} }
} }
@@ -232,7 +232,7 @@ class EmojiHoarder: ObservableObject {
return return
} }
await MainActor.run { await MainActor.run {
withAnimation { self.emojis = fetched } withAnimation(.snappy) { self.emojis = fetched }
buildTrie() buildTrie()
} }
} }
@@ -241,11 +241,12 @@ class EmojiHoarder: ObservableObject {
try? await emoji.downloadImage() try? await emoji.downloadImage()
await MainActor.run { await MainActor.run {
if !skipStoreIndex { if !skipStoreIndex {
withAnimation(.snappy) {
self.downloadedEmojis.insert(emoji.name) self.downloadedEmojis.insert(emoji.name)
self.downloadedEmojisArr.append(emoji.name) self.downloadedEmojisArr.append(emoji.name)
}
self.storeDownloadedIndexes() self.storeDownloadedIndexes()
} }
self.trie.dict[emoji.name]?.refresh()
if !skipStoreIndex { Haptic.success.trigger() } if !skipStoreIndex { Haptic.success.trigger() }
} }
} }
@@ -254,11 +255,12 @@ class EmojiHoarder: ObservableObject {
func delete(emoji: Emoji, skipStoreIndex: Bool = false) { func delete(emoji: Emoji, skipStoreIndex: Bool = false) {
emoji.deleteImage() emoji.deleteImage()
if !skipStoreIndex { if !skipStoreIndex {
withAnimation(.snappy) {
downloadedEmojis.remove(emoji.name) downloadedEmojis.remove(emoji.name)
downloadedEmojisArr.removeAll(where: { $0 == emoji.name }) downloadedEmojisArr.removeAll(where: { $0 == emoji.name })
}
storeDownloadedIndexes() storeDownloadedIndexes()
} }
self.trie.dict[emoji.name]?.refresh()
if !skipStoreIndex { Haptic.heavy.trigger() } if !skipStoreIndex { Haptic.heavy.trigger() }
} }

View File

@@ -11,7 +11,6 @@ import UniformTypeIdentifiers
struct Gif: StickerProtocol { struct Gif: StickerProtocol {
var id: UUID var id: UUID
var uiID: UUID
var name: String var name: String
var localImageURLString: String { var localImageURLString: String {
let urlString = remoteImageURL.absoluteString let urlString = remoteImageURL.absoluteString
@@ -32,7 +31,6 @@ struct Gif: StickerProtocol {
id: UUID = UUID() id: UUID = UUID()
) { ) {
self.id = id self.id = id
self.uiID = id
self.name = name self.name = name
self.remoteImageURL = url self.remoteImageURL = url
} }
@@ -56,11 +54,6 @@ struct Gif: StickerProtocol {
return return
} }
@MainActor
mutating func refresh() {
withAnimation { self.uiID = UUID() }
}
static var test: Gif = Gif( static var test: Gif = Gif(
name: "doesheknow", name: "doesheknow",
url: URL(string: "https://media1.tenor.com/m/FN9udnZmQU8AAAAd/does-he-know-batman.gif")! url: URL(string: "https://media1.tenor.com/m/FN9udnZmQU8AAAAd/does-he-know-batman.gif")!

View File

@@ -33,6 +33,25 @@ extension StickerProtocol {
} }
} }
nonisolated
func downloadImage() async throws {
if let data = try? await Data(contentsOf: localImageURL),
let _ = UIImage(data: data) {
return
}
var (data, _) = try await URLSession.shared.data(from: remoteImageURL)
if let uiImage = UIImage(data: data),
let cgImage = uiImage.cgImage,
await !self.localImageURLString.contains(".gif"),
cgImage.width < 300 || cgImage.height < 300 {
data = await resize(image: uiImage, to: CGSize(width: 300, height: 300)).pngData()!
}
try! await data.write(to: localImageURL)
return
}
func deleteImage() { func deleteImage() {
try? FileManager.default.removeItem(at: localImageURL) try? FileManager.default.removeItem(at: localImageURL)
return return

View File

@@ -10,7 +10,6 @@ import Messages
protocol StickerProtocol: Codable, Identifiable, Hashable { protocol StickerProtocol: Codable, Identifiable, Hashable {
var id: UUID { get set } var id: UUID { get set }
var uiID: UUID { get set }
var name: String { get set } var name: String { get set }
var localImageURL: URL { get } var localImageURL: URL { get }
@@ -24,7 +23,6 @@ protocol StickerProtocol: Codable, Identifiable, Hashable {
func downloadImage() async throws func downloadImage() async throws
func deleteImage() func deleteImage()
mutating func refresh()
func resize(image: UIImage, to targetSize: CGSize) -> UIImage func resize(image: UIImage, to targetSize: CGSize) -> UIImage
static var test: Self { get } static var test: Self { get }
} }

View File

@@ -15,26 +15,38 @@ struct EmojiRow: View {
var body: some View { var body: some View {
HStack { HStack {
EmojiPreview(hoarder: hoarder, emoji: emoji) EmojiPreview(hoarder: hoarder, emoji: emoji)
.frame(maxWidth: 100, maxHeight: 100) .frame(width: 100, height: 100)
.padding(.trailing, 20) .padding(.trailing, 10)
VStack(alignment: .leading, spacing: 5) {
ZStack {
RoundedRectangle(cornerRadius: 5)
.foregroundStyle(.gray.opacity(0.1))
Text(emoji.name) Text(emoji.name)
.font(.caption)
Spacer() .bold(hoarder.downloadedEmojis.contains(emoji.name))
.foregroundColor(hoarder.downloadedEmojis.contains(emoji.name) ? .green : .primary)
.padding(3)
}
.fixedSize()
if hoarder.downloadedEmojis.contains(emoji.name) {
Image(systemName: "arrow.down.circle.fill") Image(systemName: "arrow.down.circle.fill")
.resizable().scaledToFit() .resizable().scaledToFit()
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
.symbolRenderingMode(.hierarchical) .symbolRenderingMode(.hierarchical)
.foregroundStyle(.gray) .foregroundStyle(.gray)
.padding(.trailing, 20) .transition(.scale)
.opacity(hoarder.downloadedEmojis.contains(emoji.name) ? 1 : 0) }
}
Spacer()
if hoarder.downloadedEmojis.contains(emoji.name) { if hoarder.downloadedEmojis.contains(emoji.name) {
Button("", systemImage: "trash") { Button("", systemImage: "trash") {
hoarder.delete(emoji: emoji) hoarder.delete(emoji: emoji)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.transition(.scale)
} else { } else {
Button("", systemImage: "arrow.down.circle") { Button("", systemImage: "arrow.down.circle") {
Task { Task {
@@ -42,14 +54,21 @@ struct EmojiRow: View {
} }
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.transition(.scale)
} }
} }
} }
} }
#Preview { #Preview {
List {
EmojiRow( EmojiRow(
hoarder: EmojiHoarder(localOnly: true), hoarder: EmojiHoarder(localOnly: true),
emoji: Emoji.test emoji: Emoji.test
) )
EmojiRow(
hoarder: EmojiHoarder(localOnly: true),
emoji: Emoji.test
)
}
} }

View File

@@ -18,7 +18,7 @@ struct SearchView: View {
EmojiRow(hoarder: hoarder, emoji: hoarder.trie.dict[name]!) EmojiRow(hoarder: hoarder, emoji: hoarder.trie.dict[name]!)
} }
.onChange(of: hoarder.searchTerm) { _ in .onChange(of: hoarder.searchTerm) { _ in
withAnimation { filterResult = hoarder.trie.search(prefix: hoarder.searchTerm) } withAnimation(.snappy) { filterResult = hoarder.trie.search(prefix: hoarder.searchTerm) }
} }
} }
.searchable(text: $hoarder.searchTerm) .searchable(text: $hoarder.searchTerm)

View File

@@ -55,7 +55,7 @@ struct TrieTestingView: View {
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.border(.orange) .border(.orange)
.onChange(of: filterTerm) { _ in .onChange(of: filterTerm) { _ in
withAnimation { filterResult = hoarder.trie.search(prefix: filterTerm) } withAnimation(.snappy) { filterResult = hoarder.trie.search(prefix: filterTerm) }
} }
Text("\(filterResult.count)") Text("\(filterResult.count)")
.modifier(numericTextCompat()) .modifier(numericTextCompat())
@@ -65,7 +65,7 @@ struct TrieTestingView: View {
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.border(.orange) .border(.orange)
.onChange(of: filterTerm2) { _ in .onChange(of: filterTerm2) { _ in
withAnimation { filterResult = hoarder.emojis.filter({ $0.name.localizedCaseInsensitiveContains(filterTerm2) }).map({ $0.name }) } withAnimation(.snappy) { filterResult = hoarder.emojis.filter({ $0.name.localizedCaseInsensitiveContains(filterTerm2) }).map({ $0.name }) }
} }
if uikit { if uikit {