From a18bfefa4d9a8b3b6303fa9dfe270fd0a0a795ce Mon Sep 17 00:00:00 2001 From: Rakuyo Date: Mon, 27 May 2024 12:47:09 +0800 Subject: [PATCH] refactor: Adjust the image setting method of ImageRow and ButtonRow --- .../Core/Utilities/AnyImageProviding.swift | 31 ++++++ .../Row/ButtonRow/AnyButtonImageContent.swift | 59 +++++++++++ .../ButtonImageContentProviding.swift | 44 +++++++++ .../Epoxy/Row/{ => ButtonRow}/ButtonRow.swift | 82 ++++++++------- .../ButtonRowStateContent.swift | 4 +- .../Epoxy/Row/ImageRow/AnyImageContent.swift | 59 +++++++++++ .../ImageRow/FastImageContentProviding.swift | 16 +++ .../Row/ImageRow/ImageContentProviding.swift | 44 +++++++++ .../Epoxy/Row/{ => ImageRow}/ImageRow.swift | 99 +++++-------------- 9 files changed, 322 insertions(+), 116 deletions(-) create mode 100644 Sources/Core/Utilities/AnyImageProviding.swift create mode 100644 Sources/Epoxy/Row/ButtonRow/AnyButtonImageContent.swift create mode 100644 Sources/Epoxy/Row/ButtonRow/ButtonImageContentProviding.swift rename Sources/Epoxy/Row/{ => ButtonRow}/ButtonRow.swift (81%) rename Sources/Epoxy/Row/{ => ButtonRow}/ButtonRowStateContent.swift (87%) create mode 100644 Sources/Epoxy/Row/ImageRow/AnyImageContent.swift create mode 100644 Sources/Epoxy/Row/ImageRow/FastImageContentProviding.swift create mode 100644 Sources/Epoxy/Row/ImageRow/ImageContentProviding.swift rename Sources/Epoxy/Row/{ => ImageRow}/ImageRow.swift (52%) diff --git a/Sources/Core/Utilities/AnyImageProviding.swift b/Sources/Core/Utilities/AnyImageProviding.swift new file mode 100644 index 0000000..e9e6c84 --- /dev/null +++ b/Sources/Core/Utilities/AnyImageProviding.swift @@ -0,0 +1,31 @@ +// +// AnyImageProviding.swift +// RakuyoKit +// +// Created by Rakuyo on 2024/5/27. +// Copyright © 2024 RakuyoKit. All rights reserved. +// + +import UIKit + +// MARK: - AnyImageProviding + +public protocol AnyImageProviding: Equatable { + associatedtype Value + + /// Storing the original object. + var value: Value? { get } + + /// Used to implement `Equatable`. + /// + /// When using this type, you do not need to care about the specifics of the value. + var equals: (Value?) -> Bool { get } +} + +// MARK: - Equatable + +extension AnyImageProviding { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.equals(rhs.value) + } +} diff --git a/Sources/Epoxy/Row/ButtonRow/AnyButtonImageContent.swift b/Sources/Epoxy/Row/ButtonRow/AnyButtonImageContent.swift new file mode 100644 index 0000000..de497d4 --- /dev/null +++ b/Sources/Epoxy/Row/ButtonRow/AnyButtonImageContent.swift @@ -0,0 +1,59 @@ +// +// AnyButtonImageContent.swift +// RakuyoKit +// +// Created by Rakuyo on 2024/5/27. +// Copyright © 2024 RakuyoKit. All rights reserved. +// + +import UIKit + +import RAKCore + +// MARK: - AnyButtonImageContent + +public struct AnyButtonImageContent: AnyImageProviding { + public typealias Value = ButtonImageContentProviding + + public typealias Input = Equatable & Value + + public let value: (any Value)? + + public let equals: ((any Value)?) -> Bool + + public let setForViewAction: (View?, UIControl.State) -> Void + + public init(_ value: T?) { + self.value = value + equals = { ($0 as? T == value) } + setForViewAction = { value?.setForView($0, state: $1) } + } +} + +// MARK: ButtonImageContentProviding + +extension AnyButtonImageContent: ButtonImageContentProviding { + public func setForView(_ view: V?, state: UIControl.State) { + setForViewAction(view as? View, state) + } +} + +// MARK: FastImageContentProviding + +extension AnyButtonImageContent: FastImageContentProviding { + public static func asset(name: String, bundle: Bundle = .main, with configuration: UIImage.Configuration? = nil) -> Self { + .init(UIImage(named: name, in: bundle, with: configuration)) + } + + public static func data(_ data: Data) -> Self { + .init(UIImage(data: data)) + } + + public static func file(path: String) -> Self { + .init(UIImage(contentsOfFile: path)) + } + + public static func sfSymbols(name: String, configuration: UIImage.SymbolConfiguration? = nil) -> Self { + .init(UIImage(systemName: name, withConfiguration: configuration)) + } +} diff --git a/Sources/Epoxy/Row/ButtonRow/ButtonImageContentProviding.swift b/Sources/Epoxy/Row/ButtonRow/ButtonImageContentProviding.swift new file mode 100644 index 0000000..cdf0d14 --- /dev/null +++ b/Sources/Epoxy/Row/ButtonRow/ButtonImageContentProviding.swift @@ -0,0 +1,44 @@ +// +// ButtonImageContentProviding.swift +// RakuyoKit +// +// Created by Rakuyo on 2024/5/27. +// Copyright © 2024 RakuyoKit. All rights reserved. +// + +import UIKit + +// MARK: - ButtonImageContentProviding + +/// Provider that can provide image for ``ButtonRow`` +public protocol ButtonImageContentProviding: Equatable { + func setForView(_ view: V?, state: UIControl.State) +} + +// MARK: - UIImage + ButtonImageContentProviding + +extension UIImage: ButtonImageContentProviding { + public func setForView(_ view: V?, state: UIControl.State) { + guard let view else { return } + + if let button = view as? UIButton { + button.setImage(self, for: state) + } else { + assertionFailure("UIImage.setForView(_:state:) has no implementation for the \(type(of: view)) type") + } + } +} + +// MARK: - String + ButtonImageContentProviding + +extension String: ButtonImageContentProviding { + public func setForView(_ view: V?, state: UIControl.State) { + guard let view else { return } + + if let button = view as? UIButton { + button.setImage(.init(named: self), for: state) + } else { + assertionFailure("String.setForView(_:state:) has no implementation for the \(type(of: view)) type") + } + } +} diff --git a/Sources/Epoxy/Row/ButtonRow.swift b/Sources/Epoxy/Row/ButtonRow/ButtonRow.swift similarity index 81% rename from Sources/Epoxy/Row/ButtonRow.swift rename to Sources/Epoxy/Row/ButtonRow/ButtonRow.swift index ee8c40b..a8fbac6 100644 --- a/Sources/Epoxy/Row/ButtonRow.swift +++ b/Sources/Epoxy/Row/ButtonRow/ButtonRow.swift @@ -16,7 +16,7 @@ import RAKCore /// Replace `UIButton` in Epoxy component /// /// If you want to extend, consider building your own view with -/// the help of `ButtonRow.Style`, `ButtonRow.Content` and `ButtonRow.Behaviors`. +/// the help of `ButtonRow.Style`, `ButtonRow.ButtonContent` and `ButtonRow.ButtonBehaviors`. public final class ButtonRow: UIButton { private lazy var size: OptionalCGSize? = nil @@ -135,20 +135,42 @@ extension ButtonRow: StyledView { // MARK: ContentConfigurableView extension ButtonRow: ContentConfigurableView { + public typealias Content = ButtonContent + /// UIButton's `image`, `title`, and `titleColor` have different values in different states. /// /// Considering that Epoxy will use `Style` as an identifier for reuse, /// some states of UIButton are not suitable to be placed in `Style`. /// /// So here, `Content` is designed as an enum, and the state and the content in that state are set at the same time. - public enum Content: Equatable, ButtonRowStateContent { + public enum ButtonContent: Equatable, ButtonRowStateContent { + /// Usage example: + /// ```swift + /// ButtonRow.groupItem( + /// dataID: DefaultDataID.noneProvided, + /// content: .init(UIImage()), + /// style: .init()) + /// ``` + /// + /// There are also some convenience methods provided in ``FastImageContentProviding``: + /// ```swift + /// ButtonRow.groupItem( + /// dataID: DefaultDataID.noneProvided, + /// content: .sfSymbols(name: ""), + /// style: .init()) + /// ``` + /// + /// You can implement your own data provider via the ``ButtonImageContentProviding`` protocol + public typealias ImageContent = AnyButtonImageContent + + /// public struct StateContent: Equatable, ButtonRowStateContent { - public let image: ImageRow.ImageType? + public let image: ImageContent? public let title: TextRow.Content? public let titleColor: UIColor public init( - image: ImageRow.ImageType? = nil, + image: ImageContent? = nil, title: TextRow.Content? = nil, titleColor: ConvertibleToColor = UIColor.label ) { @@ -165,7 +187,7 @@ extension ButtonRow: ContentConfigurableView { /// Conveniently set styles in `.normal` state public init( - image: ImageRow.ImageType? = nil, + image: ImageContent? = nil, title: TextRow.Content? = nil, titleColor: ConvertibleToColor = UIColor.label ) { @@ -176,17 +198,22 @@ extension ButtonRow: ContentConfigurableView { public func setContent(_ content: Content, animated _: Bool) { func _set(with stateContent: Content.StateContent, for state: UIControl.State) { if let image = stateContent.image { - setImage(image.image, for: state) + weak var this = self + image.setForView(this, state: state) + } else { + setImage(nil, for: state) } - if let title = stateContent.title { - switch title { - case .text(let value): - setTitle(value, for: state) + switch stateContent.title { + case .text(let value): + setTitle(value, for: state) + + case .attributedText(let value): + setAttributedTitle(value, for: state) - case .attributedText(let value): - setAttributedTitle(value, for: state) - } + case .none: + setTitle(nil, for: state) + setAttributedTitle(nil, for: state) } setTitleColor(stateContent.titleColor, for: state) @@ -225,12 +252,11 @@ extension ButtonRow: ContentConfigurableView { // MARK: BehaviorsConfigurableView extension ButtonRow: BehaviorsConfigurableView { + public typealias Behaviors = ButtonBehaviors + /// For a custom Row inherited from `UIControl`, you can also use this type to set the control behavior, /// and use the generic T to access the custom `UIImageView` that may exist in the control. - public struct Behaviors { - /// Asynchronously updates the image. - public let updateImage: ImageRow.Behaviors? - + public struct ButtonBehaviors { /// Closure for touch down event. public let didTouchDown: ButtonClosure? @@ -241,37 +267,17 @@ extension ButtonRow: BehaviorsConfigurableView { public let didTriggerMenuAction: ButtonClosure? public init( - updateImage: ImageRow.Behaviors? = nil, didTouchDown: ButtonClosure? = nil, didTap: ButtonClosure? = nil, didTriggerMenuAction: ButtonClosure? = nil ) { - self.updateImage = updateImage self.didTouchDown = didTouchDown self.didTap = didTap self.didTriggerMenuAction = didTriggerMenuAction } } - public func setBehaviors(_ behaviors: Behaviors?) { - if let updateImage = behaviors?.updateImage { - if let asyncUpdateImage = updateImage.asyncUpdateImage { - asyncUpdateImage { [weak self] in self?.imageView?.image = $0 } - } - - if let concurrencyUpdateImage = updateImage.concurrencyUpdateImage { - Task { - let _image = await concurrencyUpdateImage() - await MainActor.run { imageView?.image = _image } - } - } - - if let customUpdateImage = updateImage.customUpdateImage { - weak var this = self - customUpdateImage(this) - } - } - + public func setBehaviors(_ behaviors: Behaviors?) { didTouchDown = behaviors?.didTouchDown didTap = behaviors?.didTap diff --git a/Sources/Epoxy/Row/ButtonRowStateContent.swift b/Sources/Epoxy/Row/ButtonRow/ButtonRowStateContent.swift similarity index 87% rename from Sources/Epoxy/Row/ButtonRowStateContent.swift rename to Sources/Epoxy/Row/ButtonRow/ButtonRowStateContent.swift index 50f283b..9be521d 100644 --- a/Sources/Epoxy/Row/ButtonRowStateContent.swift +++ b/Sources/Epoxy/Row/ButtonRow/ButtonRowStateContent.swift @@ -14,8 +14,10 @@ import RAKCore /// /// Used internally to simplify the creation of `ButtonRow.Content` in `.normal` state. protocol ButtonRowStateContent { + associatedtype ImageContent + init( - image: ImageRow.ImageType?, + image: ImageContent?, title: TextRow.Content?, titleColor: ConvertibleToColor ) diff --git a/Sources/Epoxy/Row/ImageRow/AnyImageContent.swift b/Sources/Epoxy/Row/ImageRow/AnyImageContent.swift new file mode 100644 index 0000000..ffc8a68 --- /dev/null +++ b/Sources/Epoxy/Row/ImageRow/AnyImageContent.swift @@ -0,0 +1,59 @@ +// +// AnyImageContent.swift +// RakuyoKit +// +// Created by Rakuyo on 2024/5/27. +// Copyright © 2024 RakuyoKit. All rights reserved. +// + +import UIKit + +import RAKCore + +// MARK: - AnyImageContent + +public struct AnyImageContent: AnyImageProviding { + public typealias Value = ImageContentProviding + + public typealias Input = Equatable & Value + + public let value: (any Value)? + + public let equals: ((any Value)?) -> Bool + + public let setForViewAction: (View?) -> Void + + public init(_ value: T?) { + self.value = value + equals = { ($0 as? T == value) } + setForViewAction = { value?.setForView($0) } + } +} + +// MARK: ImageContentProviding + +extension AnyImageContent: ImageContentProviding { + public func setForView(_ view: V?) { + setForViewAction(view as? View) + } +} + +// MARK: FastImageContentProviding + +extension AnyImageContent: FastImageContentProviding { + public static func asset(name: String, bundle: Bundle = .main, with configuration: UIImage.Configuration? = nil) -> Self { + .init(UIImage(named: name, in: bundle, with: configuration)) + } + + public static func data(_ data: Data) -> Self { + .init(UIImage(data: data)) + } + + public static func file(path: String) -> Self { + .init(UIImage(contentsOfFile: path)) + } + + public static func sfSymbols(name: String, configuration: UIImage.SymbolConfiguration? = nil) -> Self { + .init(UIImage(systemName: name, withConfiguration: configuration)) + } +} diff --git a/Sources/Epoxy/Row/ImageRow/FastImageContentProviding.swift b/Sources/Epoxy/Row/ImageRow/FastImageContentProviding.swift new file mode 100644 index 0000000..f5ee934 --- /dev/null +++ b/Sources/Epoxy/Row/ImageRow/FastImageContentProviding.swift @@ -0,0 +1,16 @@ +// +// ImageContentProviding.swift +// RakuyoKit +// +// Created by Rakuyo on 2024/5/27. +// Copyright © 2024 RakuyoKit. All rights reserved. +// + +import UIKit + +public protocol FastImageContentProviding { + static func asset(name: String, bundle: Bundle, with configuration: UIImage.Configuration?) -> Self + static func data(_ data: Data) -> Self + static func file(path: String) -> Self + static func sfSymbols(name: String, configuration: UIImage.SymbolConfiguration?) -> Self +} diff --git a/Sources/Epoxy/Row/ImageRow/ImageContentProviding.swift b/Sources/Epoxy/Row/ImageRow/ImageContentProviding.swift new file mode 100644 index 0000000..500d5db --- /dev/null +++ b/Sources/Epoxy/Row/ImageRow/ImageContentProviding.swift @@ -0,0 +1,44 @@ +// +// ImageContentProviding.swift +// RakuyoKit +// +// Created by Rakuyo on 2024/5/27. +// Copyright © 2024 RakuyoKit. All rights reserved. +// + +import UIKit + +// MARK: - ImageContentProviding + +/// Provider that can provide content for ``ImageRow`` +public protocol ImageContentProviding: Equatable { + func setForView(_ view: V?) +} + +// MARK: - UIImage + ImageContentProviding + +extension UIImage: ImageContentProviding { + public func setForView(_ view: V?) { + guard let view else { return } + + if let _view = view as? UIImageView { + _view.image = self + } else { + assertionFailure("UIImage.setForView(_:) has no implementation for the \(type(of: view)) type") + } + } +} + +// MARK: - String + ImageContentProviding + +extension String: ImageContentProviding { + public func setForView(_ view: V?) { + guard let view else { return } + + if let _view = view as? UIImageView { + _view.image = .init(named: self) + } else { + assertionFailure("String.setForView(_:) has no implementation for the \(type(of: view)) type") + } + } +} diff --git a/Sources/Epoxy/Row/ImageRow.swift b/Sources/Epoxy/Row/ImageRow/ImageRow.swift similarity index 52% rename from Sources/Epoxy/Row/ImageRow.swift rename to Sources/Epoxy/Row/ImageRow/ImageRow.swift index 5669e0e..4bc0dc9 100644 --- a/Sources/Epoxy/Row/ImageRow.swift +++ b/Sources/Epoxy/Row/ImageRow/ImageRow.swift @@ -18,6 +18,7 @@ import RAKCore /// If you want to extend, consider building your own view with /// the help of `ImageRow.Style`, `ImageRow.Content` and `ImageRow.Behaviors`. public final class ImageRow: UIImageView { + /// Self-size private lazy var size: OptionalCGSize? = nil } @@ -88,87 +89,31 @@ extension ImageRow: StyledView { // MARK: ContentConfigurableView extension ImageRow: ContentConfigurableView { - public typealias Content = ImageType? - - public enum ImageType: Equatable { - case image(UIImage?) - case asset(String, bundle: Bundle = .main) - case data(Data) - case file(String) - case sfSymbols(String, configuration: UIImage.SymbolConfiguration? = nil) - - public var image: UIImage? { - switch self { - case .image(let image): - image - - case .asset(let name, let bundle): - .init(named: name, in: bundle, with: nil) - - case .data(let data): - .init(data: data) - - case .file(let path): - .init(contentsOfFile: path) - - case .sfSymbols(let name, let configuration): - .init(systemName: name, withConfiguration: configuration) - } - } - } + /// Usage example: + /// ```swift + /// ImageRow.groupItem( + /// dataID: DefaultDataID.noneProvided, + /// content: .init(UIImage()), + /// style: .init()) + /// ``` + /// + /// There are also some convenience methods provided in ``FastImageContentProviding``: + /// ```swift + /// ImageRow.groupItem( + /// dataID: DefaultDataID.noneProvided, + /// content: .sfSymbols(name: ""), + /// style: .init()) + /// ``` + /// + /// You can implement your own data provider via the ``ImageContentProviding`` protocol + public typealias Content = AnyImageContent public func setContent(_ content: Content, animated _: Bool) { - image = content?.image + weak var this = self + content.setForView(this) } } // MARK: BehaviorsConfigurableView -extension ImageRow: BehaviorsConfigurableView { - public struct Behaviors { - public typealias AsyncUpdateImage = ((UIImage?) -> Void) -> Void - - public typealias ConcurrencyUpdateImage = () async -> UIImage? - - public typealias CustomUpdateImage = (T?) -> Void - - /// Update asynchronously - public let asyncUpdateImage: AsyncUpdateImage? - - /// Use coroutine to update - public let concurrencyUpdateImage: ConcurrencyUpdateImage? - - /// Returns the view itself, which can be customized to update the view - /// - /// This view has a weak reference - public let customUpdateImage: CustomUpdateImage? - - public init( - asyncUpdateImage: AsyncUpdateImage? = nil, - concurrencyUpdateImage: ConcurrencyUpdateImage? = nil, - customUpdateImage: CustomUpdateImage? = nil - ) { - self.asyncUpdateImage = asyncUpdateImage - self.concurrencyUpdateImage = concurrencyUpdateImage - self.customUpdateImage = customUpdateImage - } - } - - public func setBehaviors(_ behaviors: Behaviors?) { - if let asyncUpdateImage = behaviors?.asyncUpdateImage { - asyncUpdateImage { [weak self] in self?.image = $0 } - } - - if let concurrencyUpdateImage = behaviors?.concurrencyUpdateImage { - Task { - let _image = await concurrencyUpdateImage() - await MainActor.run { image = _image } - } - } - - if let customUpdateImage = behaviors?.customUpdateImage { - weak var this = self - customUpdateImage(this) - } - } -} +extension ImageRow: BehaviorsConfigurableView { }