Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct AvatarGroupPreview: View {
$0.placeholder = .text("IM")
},
.init {
$0.placeholder = .sfSymbol("person.circle")
$0.placeholder = .image(.init(systemName: "person.circle"))
},
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import UIKit

struct AvatarPreview: View {
@State private var model = AvatarVM {
$0.placeholder = .icon("avatar_placeholder")
$0.placeholder = .image(.init("avatar_placeholder"))
}

var body: some View {
VStack {
PreviewWrapper(title: "UIKit") {
Expand All @@ -23,13 +23,19 @@ struct AvatarPreview: View {
}
Picker("Image Source", selection: self.$model.imageSrc) {
Text("Remote").tag(AvatarVM.ImageSource.remote(URL(string: "https://i.pravatar.cc/150?img=12")!))
Text("Local").tag(AvatarVM.ImageSource.local("avatar_image"))
Text("Local").tag(
AvatarVM.ImageSource.local(UniversalImage("avatar_image"))
)
Text("None").tag(Optional<AvatarVM.ImageSource>.none)
}
Picker("Placeholder", selection: self.$model.placeholder) {
Text("Text").tag(AvatarVM.Placeholder.text("IM"))
Text("SF Symbol").tag(AvatarVM.Placeholder.sfSymbol("person"))
Text("Icon").tag(AvatarVM.Placeholder.icon("avatar_placeholder"))
Text("SF Symbol").tag(
AvatarVM.Placeholder.image(UniversalImage(systemName: "person"))
)
Text("Asset").tag(
AvatarVM.Placeholder.image(UniversalImage("avatar_placeholder"))
)
}
SizePicker(selection: self.$model.size)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,26 @@ struct ButtonPreview: View {
Text("Leading").tag(ButtonVM.ImageLocation.leading)
Text("Trailing").tag(ButtonVM.ImageLocation.trailing)
}
Picker("Image Rendering Mode", selection: self.$model.imageRenderingMode) {
Text("Default").tag(Optional<ImageRenderingMode>.none)
Text("Template").tag(ImageRenderingMode.template)
Text("Original").tag(ImageRenderingMode.original)
}
Picker("Image Source", selection: self.$model.imageSrc) {
Text("SF Symbol").tag(ButtonVM.ImageSource.sfSymbol("camera.fill"))
Text("Local").tag(ButtonVM.ImageSource.local("avatar_placeholder"))
Text("None").tag(Optional<ButtonVM.ImageSource>.none)
Picker("Image", selection: self.$model.image) {
Text("SF Symbol").tag(UniversalImage(systemName: "camera.fill"))
Text("SF Symbol (Template)").tag(
UniversalImage(systemName: "camera.fill")
.withRenderingMode(.template)
)
Text("SF Symbol (Original)").tag(
UniversalImage(systemName: "camera.fill")
.withRenderingMode(.original)
)
Text("Local").tag(UniversalImage("avatar_placeholder"))
Text("Local (Template)").tag(
UniversalImage("avatar_placeholder")
.withRenderingMode(.template)
)
Text("Local (Original)").tag(
UniversalImage("avatar_placeholder")
.withRenderingMode(.original)
)
Text("None").tag(Optional<UniversalImage>.none)
}
Toggle("Loading", isOn: self.$model.isLoading)
Toggle("Show Title", isOn: Binding<Bool>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ final class AvatarImageManager: ObservableObject {
case .remote(let url):
self.avatarImage = model.placeholderImage(for: size)
self.downloadImage(url: url)
case let .local(name, bundle):
self.avatarImage = UIImage(named: name, in: bundle, compatibleWith: nil) ?? model.placeholderImage(for: size)
case .local(let image):
self.avatarImage = image.uiImage ?? model.placeholderImage(for: size)
case .none:
self.avatarImage = model.placeholderImage(for: size)
}
Expand All @@ -33,8 +33,8 @@ final class AvatarImageManager: ObservableObject {
self.avatarImage = model.placeholderImage(for: size)
self.downloadImage(url: url)
}
case let .local(name, bundle):
self.avatarImage = UIImage(named: name, in: bundle, compatibleWith: nil) ?? model.placeholderImage(for: size)
case .local(let image):
self.avatarImage = image.uiImage ?? model.placeholderImage(for: size)
case .none:
self.avatarImage = model.placeholderImage(for: size)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ extension AvatarVM {
/// - Note: Ensure the URL is valid and accessible to prevent errors during image fetching.
case remote(_ url: URL)

/// An image loaded from a local asset.
/// An image referenced locally using a `UniversalImage`.
///
/// - Parameters:
/// - name: The name of the local image asset.
/// - bundle: The bundle containing the image resource. Defaults to `nil`, which uses the main bundle.
case local(_ name: String, _ bundle: Bundle? = nil)
/// - Parameter image: See ``UniversalImage``.
case local(_ image: UniversalImage)

/// An image loaded from a local asset.
@available(*, deprecated, message: "Use `local(_:)` instead.")
public static func local(_ name: String, _ bundle: Bundle? = nil) -> Self {
return .local(.init(name, bundle: bundle))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,21 @@ extension AvatarVM {
/// - Note: Only 3 first letters are displayed.
case text(String)

/// A placeholder that displays an SF Symbol.
///
/// This option allows you to use Apple's system-provided icons as placeholders.
/// A placeholder that displays an image.
///
/// - Parameter name: The name of the SF Symbol to display.
/// - Note: Ensure that the SF Symbol name corresponds to an existing icon in the system's symbol library.
case sfSymbol(_ name: String)
/// - Parameter image: See ``UniversalImage``.
case image(_ image: UniversalImage)

/// A placeholder that displays an SF Symbol.
@available(*, deprecated, message: "Use `image(_:)` instead.")
public static func sfSymbol(_ name: String) -> Self {
return .image(.init(systemName: name))
}

/// A placeholder that displays a custom icon from an asset catalog.
///
/// This option allows you to use icons from your app's bundled resources or a specified bundle.
///
/// - Parameters:
/// - name: The name of the icon asset to use as the placeholder.
/// - bundle: The bundle containing the icon resource. Defaults to `nil`, which uses the main bundle.
case icon(_ name: String, _ bundle: Bundle? = nil)
@available(*, deprecated, message: "Use `image(_:)` instead.")
public static func icon(_ name: String, _ bundle: Bundle? = nil) -> Self {
return .image(.init(name, bundle: bundle))
}
}
}
10 changes: 3 additions & 7 deletions Sources/ComponentsKit/Components/Avatar/Models/AvatarVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public struct AvatarVM: ComponentVM, Hashable {
public var imageSrc: ImageSource?

/// The placeholder that is displayed if the image is not provided or fails to load.
public var placeholder: Placeholder = .icon("avatar_placeholder", Bundle.module)
public var placeholder: Placeholder = .image(.init("avatar_placeholder", bundle: .module))

/// The predefined size of the avatar.
///
Expand Down Expand Up @@ -54,12 +54,8 @@ extension AvatarVM {
switch self.placeholder {
case .text(let value):
return self.drawName(value, size: size)
case .icon(let name, let bundle):
let icon = UIImage(named: name, in: bundle, with: nil)
return self.drawIcon(icon, size: size)
case .sfSymbol(let name):
let systemIcon = UIImage(systemName: name)
return self.drawIcon(systemIcon, size: size)
case .image(let image):
return self.drawIcon(image.uiImage, size: size)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public struct AvatarItemVM: ComponentVM {
public var imageSrc: AvatarVM.ImageSource?

/// The placeholder that is displayed if the image is not provided or fails to load.
public var placeholder: AvatarVM.Placeholder = .icon("avatar_placeholder", Bundle.module)
public var placeholder: AvatarVM.Placeholder = .image(.init("avatar_placeholder", bundle: .module))

/// Initializes a new instance of `AvatarItemVM` with default values.
public init() {}
Expand Down
25 changes: 18 additions & 7 deletions Sources/ComponentsKit/Components/Button/Models/ButtonVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,20 @@ public struct ButtonVM: ComponentVM {
/// If not provided, the font is automatically calculated based on the button's size.
public var font: UniversalFont?

/// The image to be displayed.
public var image: UniversalImage?

/// The position of the image relative to the button's title.
///
/// Defaults to `.leading`.
public var imageLocation: ImageLocation = .leading

/// Defines how image is rendered.
@available(*, deprecated, message: "Use `image.withRenderingMode(_:)` instead.")
public var imageRenderingMode: ImageRenderingMode?

/// The source of the image to be displayed.
@available(*, deprecated, message: "Use `image` instead.")
public var imageSrc: ImageSource?

/// A Boolean value indicating whether the button is enabled or disabled.
Expand Down Expand Up @@ -182,25 +187,31 @@ extension ButtonVM {
}
}
}
}
var imageWithLegacyFallback: UniversalImage? {
if let image { return image }

extension ButtonVM {
var image: UIImage? {
guard let imageSrc else { return nil }

let image = switch imageSrc {
case .sfSymbol(let name):
UIImage(systemName: name)
UniversalImage(systemName: name)
case .local(let name, let bundle):
UIImage(named: name, in: bundle, compatibleWith: nil)
UniversalImage(name, bundle: bundle)
}
if let imageRenderingMode {
return image.withRenderingMode(imageRenderingMode)
} else {
return image
}
return image?.withRenderingMode(self.imageRenderingMode)
}
}

// MARK: UIKit Helpers

extension ButtonVM {
var isImageHidden: Bool {
return self.isLoading || self.imageWithLegacyFallback.isNil
}
func preferredSize(
for contentSize: CGSize,
parentWidth: CGFloat?
Expand Down Expand Up @@ -232,7 +243,7 @@ extension ButtonVM {
|| self.font != oldModel.font
|| self.isFullWidth != oldModel.isFullWidth
|| self.isLoading != oldModel.isLoading
|| self.imageSrc != oldModel.imageSrc
|| self.imageWithLegacyFallback != oldModel.imageWithLegacyFallback
|| self.contentSpacing != oldModel.contentSpacing
|| self.title != oldModel.title
}
Expand Down
75 changes: 34 additions & 41 deletions Sources/ComponentsKit/Components/Button/SUButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,38 +50,32 @@ public struct SUButton: View {

@ViewBuilder
private var content: some View {
switch (self.model.isLoading, self.model.image, self.model.imageLocation) {
switch (self.model.isLoading, self.model.imageWithLegacyFallback, self.model.imageLocation) {
case (true, _, _) where self.model.title.isEmpty:
SULoading(model: self.model.preferredLoadingVM)
case (true, _, _):
SULoading(model: self.model.preferredLoadingVM)
Text(self.model.title)
case (false, let uiImage?, .leading) where self.model.title.isEmpty:
ButtonImageView(
image: uiImage,
tintColor: self.model.foregroundColor.uiColor
case (false, let image?, _) where self.model.title.isEmpty:
ButtonImage(
image: image,
tintColor: self.model.foregroundColor,
side: self.model.imageSide
)
.frame(width: self.model.imageSide, height: self.model.imageSide)
case (false, let uiImage?, .leading):
ButtonImageView(
image: uiImage,
tintColor: self.model.foregroundColor.uiColor
case (false, let image?, .leading):
ButtonImage(
image: image,
tintColor: self.model.foregroundColor,
side: self.model.imageSide
)
.frame(width: self.model.imageSide, height: self.model.imageSide)
Text(self.model.title)
case (false, let uiImage?, .trailing) where self.model.title.isEmpty:
ButtonImageView(
image: uiImage,
tintColor: self.model.foregroundColor.uiColor
)
.frame(width: self.model.imageSide, height: self.model.imageSide)
case (false, let uiImage?, .trailing):
case (false, let image?, .trailing):
Text(self.model.title)
ButtonImageView(
image: uiImage,
tintColor: self.model.foregroundColor.uiColor
ButtonImage(
image: image,
tintColor: self.model.foregroundColor,
side: self.model.imageSide
)
.frame(width: self.model.imageSide, height: self.model.imageSide)
case (false, _, _):
Text(self.model.title)
}
Expand All @@ -90,28 +84,27 @@ public struct SUButton: View {

// MARK: - Helpers

private struct ButtonImageView: UIViewRepresentable {
class InternalImageView: UIImageView {
override var intrinsicContentSize: CGSize {
return .zero
}
}
private struct ButtonImage: View {
let universalImage: UniversalImage
let tintColor: UniversalColor
let side: CGFloat

let image: UIImage
let tintColor: UIColor

func makeUIView(context: Context) -> UIImageView {
let imageView = InternalImageView()
imageView.image = self.image
imageView.tintColor = self.tintColor
imageView.contentMode = .scaleAspectFit
imageView.isUserInteractionEnabled = true
return imageView
init(
image: UniversalImage,
tintColor: UniversalColor,
side: CGFloat
) {
self.universalImage = image
self.tintColor = tintColor
self.side = side
}

func updateUIView(_ imageView: UIImageView, context: Context) {
imageView.image = self.image
imageView.tintColor = self.tintColor
var body: some View {
self.universalImage.image
.resizable()
.scaledToFit()
.tint(self.tintColor.color)
.frame(width: self.side, height: self.side)
}
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/ComponentsKit/Components/Button/UKButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,9 @@ extension UKButton {
view.isVisible = model.isLoading
}
static func imageView(_ imageView: UIImageView, model: Model) {
imageView.image = model.image
imageView.image = model.imageWithLegacyFallback?.uiImage
imageView.contentMode = .scaleAspectFit
imageView.isHidden = model.isLoading || model.imageSrc.isNil
imageView.isHidden = model.isImageHidden
imageView.tintColor = model.foregroundColor.uiColor
imageView.isUserInteractionEnabled = true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,6 @@ extension ImageRenderingMode {
}
}

extension UIImage {
func withRenderingMode(_ mode: ImageRenderingMode?) -> UIImage {
if let mode {
return self.withRenderingMode(mode.uiImageRenderingMode)
} else {
return self
}
}
}

// MARK: - SwiftUI Helpers

extension ImageRenderingMode {
Expand Down
Loading