Skip to content

Commit

Permalink
Merge pull request #26 from divadretlaw/feature/EmojiProvider
Browse files Browse the repository at this point in the history
Add EmojiProvider
  • Loading branch information
divadretlaw authored Aug 16, 2024
2 parents 8da968c + b7f989b commit 0586a1b
Show file tree
Hide file tree
Showing 24 changed files with 412 additions and 132 deletions.
7 changes: 2 additions & 5 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ opt_in_rules: # some rules are turned off by default, so you need to opt-in
- accessibility_label_for_image
- accessibility_trait_for_button
- anonymous_argument_in_multiline_closure
- anyobject_protocol
- array_init
# - attributes
- balanced_xctest_lifecycle
Expand Down Expand Up @@ -150,12 +149,11 @@ opt_in_rules: # some rules are turned off by default, so you need to opt-in
- implicit_return
# - implicitly_unwrapped_optional
# - indentation_width
- inert_defer
- joined_default_parameter
- last_where
- legacy_multiple
- legacy_objc_type
- let_var_whitespace
# - let_var_whitespace
- literal_expression_end_indentation
- local_doc_comment
- lower_acl_than_parent
Expand All @@ -176,7 +174,7 @@ opt_in_rules: # some rules are turned off by default, so you need to opt-in
# - nslocalizedstring_require_bundle
# - number_separator
- object_literal
- one_declaration_per_file
# - one_declaration_per_file
- operator_usage_whitespace
- optional_enum_case_matching
- overridden_super_call
Expand Down Expand Up @@ -226,7 +224,6 @@ opt_in_rules: # some rules are turned off by default, so you need to opt-in
- unneeded_parentheses_in_closure_argument
- unowned_variable_capture
- untyped_error_in_catch
- unused_capture_list
# - unused_declaration
# - unused_import
- vertical_parameter_alignment_on_call
Expand Down
4 changes: 4 additions & 0 deletions Sources/EmojiText/Domain/EmojiError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import Foundation

/// Internal errors for loading images
internal enum EmojiError: LocalizedError {
/// The data was corrupted.
case invalidData
/// The static fallback data was corrupted.
case staticData
/// The animated image data was corrupted.
Expand All @@ -18,6 +20,8 @@ internal enum EmojiError: LocalizedError {

var errorDescription: String? {
switch self {
case .invalidData:
return "The image data could not be read"
case .staticData:
return "The static fallback image could not be read"
case .animatedData:
Expand Down
15 changes: 15 additions & 0 deletions Sources/EmojiText/Domain/EmojiProviderError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// EmojiProviderError.swift
// EmojiText
//
// Created by David Walter on 13.07.24.
//

import Foundation

public enum EmojiProviderError: Swift.Error {
/// Thrown when the fetched data is invalid
case invalidData
/// Thrown when an unsupported emojis is trying to be fetched
case unsupportedEmoji
}
74 changes: 40 additions & 34 deletions Sources/EmojiText/EmojiText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import OSLog
@Environment(\.displayScale) var displayScale
@Environment(\.colorScheme) var colorScheme

@Environment(\.emojiText.imagePipeline) var imagePipeline
@Environment(\.emojiText.syncEmojiProvider) var syncEmojiProvider
@Environment(\.emojiText.asyncEmojiProvider) var asyncEmojiProvider

@Environment(\.emojiText.placeholder) var placeholder
@Environment(\.emojiText.size) var size
@Environment(\.emojiText.baselineOffset) var baselineOffset
Expand Down Expand Up @@ -117,37 +119,48 @@ import OSLog

for emoji in emojis {
switch emoji {
case let localEmoji as LocalEmoji:
// Local emoji don't require a placeholder as they can be loaded instantly
renderedEmojis[emoji.shortcode] = RenderedEmoji(
from: localEmoji,
targetHeight: targetHeight,
baselineOffset: baselineOffset
)
case let sfSymbolEmoji as SFSymbolEmoji:
// SF Symbol emoji don't require a placeholder as they can be loaded instantly
renderedEmojis[emoji.shortcode] = RenderedEmoji(
from: sfSymbolEmoji
)
case let remoteEmoji as RemoteEmoji:
case let emoji as any SyncCustomEmoji:
if let image = syncEmojiProvider.emojiImage(emoji: emoji, height: targetHeight) {
renderedEmojis[emoji.shortcode] = RenderedEmoji(
from: emoji,
image: RawImage(image: image),
animated: shouldAnimateIfNeeded,
targetHeight: targetHeight,
baselineOffset: baselineOffset
)
} else {
// Sync emoji wasn't loaded and a placeholder will be used instead
renderedEmojis[emoji.shortcode] = RenderedEmoji(
from: emoji,
placeholder: placeholder,
targetHeight: targetHeight,
baselineOffset: baselineOffset
)
}
case let emoji as any AsyncCustomEmoji:
// Try to load remote emoji from cache
let resizeHeight = targetHeight * displayScale
let request = ImageRequest(
url: remoteEmoji.url,
processors: [.resize(height: resizeHeight)]
)
if let imageContainer = imagePipeline.cache[request] {
// Remote emoji is available in cache and can be loaded instantly
renderedEmojis[remoteEmoji.shortcode] = RenderedEmoji(
from: remoteEmoji,
image: RawImage(image: imageContainer.image),
if let image = asyncEmojiProvider.lazyEmojiCached(emoji: emoji, height: resizeHeight) {
renderedEmojis[emoji.shortcode] = RenderedEmoji(
from: emoji,
image: RawImage(image: image),
animated: shouldAnimateIfNeeded,
targetHeight: targetHeight,
baselineOffset: baselineOffset
)
} else {
// Remote emoji wasn't found in cache and a placeholder will be used instead
fallthrough
// Async emoji wasn't found in cache and a placeholder will be used instead
renderedEmojis[emoji.shortcode] = RenderedEmoji(
from: emoji,
placeholder: placeholder,
targetHeight: targetHeight,
baselineOffset: baselineOffset
)
}
default:
// Set a placeholder for all other emoji
Expand All @@ -168,33 +181,28 @@ import OSLog
let baselineOffset = baselineOffset ?? -(font.pointSize - font.capHeight) / 2
let resizeHeight = targetHeight * displayScale

return await withTaskGroup(of: RenderedEmoji?.self, returning: [String: RenderedEmoji].self) { [imagePipeline, targetHeight, shouldAnimateIfNeeded] group in
return await withTaskGroup(of: RenderedEmoji?.self, returning: [String: RenderedEmoji].self) { [asyncEmojiProvider, targetHeight, shouldAnimateIfNeeded] group in
for emoji in emojis {
switch emoji {
case let remoteEmoji as RemoteEmoji:
case let emoji as any AsyncCustomEmoji:
_ = group.addTaskUnlessCancelled {
do {
let image: RawImage
let request = ImageRequest(
url: remoteEmoji.url,
processors: [.resize(height: resizeHeight)]
)
let data = try await asyncEmojiProvider.lazyEmojiData(emoji: emoji, height: resizeHeight)
if shouldAnimateIfNeeded {
let (data, _) = try await imagePipeline.data(for: request)
image = try RawImage(data: data)
image = try RawImage(animated: data)
} else {
let data = try await imagePipeline.image(for: request)
image = RawImage(image: data)
image = try RawImage(static: data)
}
return RenderedEmoji(
from: remoteEmoji,
from: emoji,
image: image,
animated: shouldAnimateIfNeeded,
targetHeight: targetHeight,
baselineOffset: baselineOffset
)
} catch {
Logger.emojiText.error("Unable to load custom emoji \(emoji.shortcode): \(error.localizedDescription)")
Logger.emojiText.error("Unable to load '\(type(of: emoji))' with code '\(emoji.shortcode)': \(error.localizedDescription)")
return nil
}
}
Expand Down Expand Up @@ -302,8 +310,6 @@ import OSLog
return view
}

// MARK: - Modifier

/// Enable animated emoji
///
/// - Parameter value: Enable or disable the animated emoji
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ public struct EmojiTextEnvironmentValues: CustomStringConvertible {
/// Accesses the environment value associated with a custom key.
///
/// Create custom environment values by defining a key
/// that conforms to the ``EnvironmentKey`` protocol, and then using that
/// key with the subscript operator of the ``EnvironmentValues`` structure
/// that conforms to the `EnvironmentKey` protocol, and then using that
/// key with the subscript operator of the `EnvironmentValues` structure
/// to get and set a value for that key:
///
/// private struct MyEnvironmentKey: EnvironmentKey {
Expand All @@ -42,8 +42,8 @@ public struct EmojiTextEnvironmentValues: CustomStringConvertible {
/// }
///
/// You use custom environment values the same way you use system-provided
/// values, setting a value with the ``View/environment(_:_:)`` view
/// modifier, and reading values with the ``Environment`` property wrapper.
/// values, setting a value with the `View/environment(_:_:)` view
/// modifier, and reading values with the `Environment` property wrapper.
/// You can also provide a dedicated view modifier as a convenience for
/// setting the value:
///
Expand Down
51 changes: 0 additions & 51 deletions Sources/EmojiText/Environment/Environment+Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,54 +51,3 @@ public extension EmojiTextNamespace where Content: View {
content.environment(\.emojiText.baselineOffset, offset)
}
}

// MARK: - Deprecations

public extension View {
/// Set the placeholder emoji
///
/// - Parameters:
/// - systemName: The SF Symbol code of the emoji
/// - symbolRenderingMode: The symbol rendering mode to use for this emoji
/// - renderingMode: The mode SwiftUI uses to render this emoji
@available(*, deprecated, renamed: "emojiText.placeholder(systemName:symbolRenderingMode:renderingMode:)")
func emojiPlaceholder(systemName: String, symbolRenderingMode: SymbolRenderingMode? = nil, renderingMode: Image.TemplateRenderingMode? = nil) -> some View {
environment(\.emojiText.placeholder, SFSymbolEmoji(shortcode: systemName, symbolRenderingMode: symbolRenderingMode, renderingMode: renderingMode))
}

/// Set the placeholder emoji
///
/// - Parameters:
/// - image: The image to use as placeholder
/// - renderingMode: The mode SwiftUI uses to render this emoji
@available(*, deprecated, renamed: "emojiText.placeholder(systemName:renderingMode:)")
func emojiPlaceholder(image: EmojiImage, renderingMode: Image.TemplateRenderingMode? = nil) -> some View {
environment(\.emojiText.placeholder, LocalEmoji(shortcode: "placeholder", image: image, renderingMode: renderingMode))
}

/// Set the size of the inline custom emojis
///
/// - Parameter size: The size to render the custom emojis in
///
/// While ``EmojiText`` tries to determine the size of the emoji based on the current font and dynamic type size
/// this only works with the system text styles, this is due to limitations of `SwiftUI.Font`.
/// In case you use a custom font or want to override the calculation of the emoji size for some other reason
/// you can provide a emoji size
@available(*, deprecated, renamed: "emojiText.size(_:)")
func emojiSize(_ size: CGFloat?) -> some View {
environment(\.emojiText.size, size)
}

/// Overrides the baseline for custom emojis
///
/// - Parameter offset: The size to render the custom emojis in
///
/// While ``EmojiText`` tries to determine the baseline offset of the emoji based on the current font and dynamic type size
/// this only works with the system text styles, this is due to limitations of `SwiftUI.Font`.
/// In case you use a custom font or want to override the calculation of the emoji baseline offset for some other reason
/// you can provide a emoji baseline offset
@available(*, deprecated, renamed: "emojiText.baselineOffset(_:)")
func emojiBaselineOffset(_ offset: CGFloat?) -> some View {
environment(\.emojiText.baselineOffset, offset)
}
}
31 changes: 22 additions & 9 deletions Sources/EmojiText/Environment/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,10 @@
//

import SwiftUI
import Nuke
import Combine

// MARK: - Environment Keys

private struct EmojiImagePipelineKey: EnvironmentKey {
static var defaultValue: ImagePipeline { .shared }
}

private struct EmojiPlaceholderKey: EnvironmentKey {
static var defaultValue: any CustomEmoji {
#if os(iOS) || targetEnvironment(macCatalyst) || os(tvOS) || os(watchOS) || os(visionOS)
Expand Down Expand Up @@ -49,6 +44,18 @@ private struct EmojiAnimatedModeKey: EnvironmentKey {
}
}

private struct SyncEmojiProviderKey: EnvironmentKey {
static var defaultValue: SyncEmojiProvider {
DefaultSyncEmojiProvider()
}
}

private struct AsyncEmojiProviderKey: EnvironmentKey {
static var defaultValue: AsyncEmojiProvider {
DefaultAsyncEmojiProvider(pipeline: .shared)
}
}

#if os(watchOS) || os(macOS)
private struct EmojiTimerKey: EnvironmentKey {
typealias Value = Publishers.Autoconnect<Timer.TimerPublisher>
Expand All @@ -66,10 +73,16 @@ private struct EmojiTimerKey: EnvironmentKey {
// MARK: - Environment Values

public extension EmojiTextEnvironmentValues {
/// The image pipeline used to fetch remote emojis.
var imagePipeline: ImagePipeline {
get { self[EmojiImagePipelineKey.self] }
set { self[EmojiImagePipelineKey.self] = newValue }
/// The ``SyncEmojiProvider`` used to fetch emoji
var syncEmojiProvider: SyncEmojiProvider {
get { self[SyncEmojiProviderKey.self] }
set { self[SyncEmojiProviderKey.self] = newValue }
}

/// The ``AsyncEmojiProvider`` used to fetch lazy emoji
var asyncEmojiProvider: AsyncEmojiProvider {
get { self[AsyncEmojiProviderKey.self] }
set { self[AsyncEmojiProviderKey.self] = newValue }
}

/// The ``AnimatedEmojiMode`` that animated emojis should use
Expand Down
3 changes: 2 additions & 1 deletion Sources/EmojiText/Model/Emoji/LocalEmoji.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation
import SwiftUI

/// A custom local emoji
public struct LocalEmoji: CustomEmoji {
public struct LocalEmoji: SyncCustomEmoji {
/// Shortcode of the emoji
public let shortcode: String
/// The image representing the emoji
Expand All @@ -20,6 +20,7 @@ public struct LocalEmoji: CustomEmoji {
public let color: EmojiColor?
/// The mode SwiftUI uses to render this emoji
public let renderingMode: Image.TemplateRenderingMode?
/// The emoji baseline offset
public let baselineOffset: CGFloat?

/// Initialize a local custom emoji
Expand Down
13 changes: 13 additions & 0 deletions Sources/EmojiText/Model/Emoji/Protocols/AsyncCustomEmoji.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// AsyncCustomEmoji.swift
// EmojiText
//
// Created by David Walter on 14.07.24.
//

import Foundation
import SwiftUI

/// A custom emoji that requires lazy loading
public protocol AsyncCustomEmoji: CustomEmoji {
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public protocol CustomEmoji: Hashable, Equatable, Identifiable, Sendable {
var renderingMode: Image.TemplateRenderingMode? { get }
/// The symbol rendering mode to use for this emoji
var symbolRenderingMode: SymbolRenderingMode? { get }
/// The symbols baseline offset
/// The emoji baseline offset
var baselineOffset: CGFloat? { get }
}

Expand Down
13 changes: 13 additions & 0 deletions Sources/EmojiText/Model/Emoji/Protocols/SyncCustomEmoji.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// SyncCustomEmoji.swift
// EmojiText
//
// Created by David Walter on 14.07.24.
//

import Foundation
import SwiftUI

/// A custom emoji that can be loaded immediately
public protocol SyncCustomEmoji: CustomEmoji {
}
Loading

0 comments on commit 0586a1b

Please sign in to comment.