Skip to content

Commit

Permalink
Merge pull request #32 from RakuyoKit/fix/update-ButtonRow-image
Browse files Browse the repository at this point in the history
Adjust the image setting method of `ImageRow` and `ButtonRow`
  • Loading branch information
rakuyoMo authored May 27, 2024
2 parents 743dfd8 + a18bfef commit 4080642
Show file tree
Hide file tree
Showing 9 changed files with 322 additions and 116 deletions.
31 changes: 31 additions & 0 deletions Sources/Core/Utilities/AnyImageProviding.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
59 changes: 59 additions & 0 deletions Sources/Epoxy/Row/ButtonRow/AnyButtonImageContent.swift
Original file line number Diff line number Diff line change
@@ -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<View>: 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<T: Input>(_ value: T?) {
self.value = value
equals = { ($0 as? T == value) }
setForViewAction = { value?.setForView($0, state: $1) }
}
}

// MARK: ButtonImageContentProviding

extension AnyButtonImageContent: ButtonImageContentProviding {
public func setForView<V>(_ 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))
}
}
44 changes: 44 additions & 0 deletions Sources/Epoxy/Row/ButtonRow/ButtonImageContentProviding.swift
Original file line number Diff line number Diff line change
@@ -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<V>(_ view: V?, state: UIControl.State)
}

// MARK: - UIImage + ButtonImageContentProviding

extension UIImage: ButtonImageContentProviding {
public func setForView<V>(_ 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<V>(_ 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")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -135,20 +135,42 @@ extension ButtonRow: StyledView {
// MARK: ContentConfigurableView

extension ButtonRow: ContentConfigurableView {
public typealias Content = ButtonContent<ButtonRow>

/// 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<T>: 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<T>

///
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
) {
Expand All @@ -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
) {
Expand All @@ -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)
Expand Down Expand Up @@ -225,12 +252,11 @@ extension ButtonRow: ContentConfigurableView {
// MARK: BehaviorsConfigurableView

extension ButtonRow: BehaviorsConfigurableView {
public typealias Behaviors = ButtonBehaviors<ButtonRow>

/// 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<T: UIImageView> {
/// Asynchronously updates the image.
public let updateImage: ImageRow.Behaviors<T>?

public struct ButtonBehaviors<T> {
/// Closure for touch down event.
public let didTouchDown: ButtonClosure?

Expand All @@ -241,37 +267,17 @@ extension ButtonRow: BehaviorsConfigurableView {
public let didTriggerMenuAction: ButtonClosure?

public init(
updateImage: ImageRow.Behaviors<T>? = 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<UIImageView>?) {
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 imageView = imageView
customUpdateImage(imageView)
}
}

public func setBehaviors(_ behaviors: Behaviors?) {
didTouchDown = behaviors?.didTouchDown
didTap = behaviors?.didTap

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
59 changes: 59 additions & 0 deletions Sources/Epoxy/Row/ImageRow/AnyImageContent.swift
Original file line number Diff line number Diff line change
@@ -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<View>: 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<T: Input>(_ value: T?) {
self.value = value
equals = { ($0 as? T == value) }
setForViewAction = { value?.setForView($0) }
}
}

// MARK: ImageContentProviding

extension AnyImageContent: ImageContentProviding {
public func setForView<V>(_ 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))
}
}
16 changes: 16 additions & 0 deletions Sources/Epoxy/Row/ImageRow/FastImageContentProviding.swift
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 4080642

Please sign in to comment.