Skip to content

Commit bf5344a

Browse files
authored
Merge pull request #65 from RakuyoKit/feature/enhance
Feature/enhance
2 parents c23ff39 + 9316a78 commit bf5344a

23 files changed

+521
-98
lines changed

Sources/Config/Mediator/ConfigSourceProtocol.swift

+1-3
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ extension ConfigSourceProtocol {
4444
public func appDownloadURL(with area: String = "cn") -> URL? {
4545
let baseURLString = "https://itunes.apple.com/\(area)/app/id"
4646
let urlString = baseURLString + appStoreConnectAppleID
47-
48-
guard let url = URL(string: urlString) else { return nil }
49-
return url
47+
return .init(string: urlString)
5048
}
5149
}
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//
2+
// CGPoint+RAK.swift
3+
// RakuyoKit
4+
//
5+
// Created by Rakuyo on 2025/2/17.
6+
// Copyright © 2024 RakuyoKit. All rights reserved.
7+
//
8+
9+
import UIKit
10+
11+
// MARK: - CGPoint + ExpressibleByStringLiteral
12+
13+
extension CGPoint: @retroactive ExpressibleByStringLiteral {
14+
public init(stringLiteral value: String) {
15+
self = NSCoder.cgPoint(for: value)
16+
}
17+
18+
public init(extendedGraphemeClusterLiteral value: String) {
19+
self = NSCoder.cgPoint(for: value)
20+
}
21+
22+
public init(unicodeScalarLiteral value: String) {
23+
self = NSCoder.cgPoint(for: value)
24+
}
25+
}
26+
27+
// MARK: - Extendable
28+
29+
extension Extendable where Base == CGPoint {
30+
public var halfX: CGFloat {
31+
get { base.x * 0.5 }
32+
set { base.x = newValue * 2 }
33+
}
34+
35+
public var halfY: CGFloat {
36+
get { base.y * 0.5 }
37+
set { base.y = newValue * 2 }
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//
2+
// DirectlyConvertibleToImage.swift
3+
// RakuyoKit
4+
//
5+
// Created by Rakuyo on 2025/2/17.
6+
// Copyright © 2024 RakuyoKit. All rights reserved.
7+
//
8+
9+
import UIKit
10+
11+
// MARK: - DirectlyConvertibleToImage
12+
13+
/// Encapsulates types that can be converted into a `UIImage` object
14+
///
15+
/// This protocol is responsible for encapsulating **the synchronous process** of converting an object to `UIImage`.
16+
/// If you need to obtain a `UIImage` **asynchronously**, consider using the `ConvertibleToImage` protocol.
17+
public protocol DirectlyConvertibleToImage {
18+
typealias ConversionResult = Result<UIImage, Error>
19+
20+
var directImage: UIImage? { get }
21+
22+
func getDirectImage() -> ConversionResult
23+
}
24+
25+
// MARK: - Default Implementation
26+
27+
extension DirectlyConvertibleToImage {
28+
public func getDirectImage() -> ConversionResult {
29+
if let directImage { .success(directImage) } else { .failure(ConversionImageError.failure) }
30+
}
31+
}
32+
33+
// MARK: - UIImage + DirectlyConvertibleToImage
34+
35+
extension UIImage: DirectlyConvertibleToImage {
36+
public var directImage: UIImage? { self }
37+
}
38+
39+
// MARK: - UIColor + DirectlyConvertibleToImage
40+
41+
#if !os(watchOS)
42+
extension UIColor: DirectlyConvertibleToImage {
43+
public var directImage: UIImage? { .rak.color(self) }
44+
}
45+
#endif
46+
47+
// MARK: - Data + DirectlyConvertibleToImage
48+
49+
extension Data: DirectlyConvertibleToImage {
50+
public var directImage: UIImage? { .init(data: self, scale: 0.9) }
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// FinalTextAttachment.swift
3+
// RakuyoKit
4+
//
5+
// Created by Rakuyo on 2025/2/17.
6+
// Copyright © 2024 RakuyoKit. All rights reserved.
7+
//
8+
9+
#if !os(watchOS)
10+
import UIKit
11+
12+
import Then
13+
14+
/// A blank subclass of `NSTextAttachment`, used to conform to the `HigherOrderFunctionalizable` protocol
15+
public final class FinalTextAttachment: NSTextAttachment { }
16+
17+
// MARK: - HigherOrderFunctionalizable
18+
19+
extension FinalTextAttachment: HigherOrderFunctionalizable { }
20+
#endif
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//
2+
// ForceError.swift
3+
// RakuyoKit
4+
//
5+
// Created by Rakuyo on 2025/2/17.
6+
// Copyright © 2024 RakuyoKit. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
/// Forceful error type conversion
12+
public enum ForceError {
13+
/// Forcefully converts an error type to the target type
14+
///
15+
/// Suitable for APIs that do not yet provide typed errors
16+
@discardableResult
17+
public static func convert<Success, Error: Swift.Error>(
18+
to _: Error.Type,
19+
_ body: () async throws -> Success
20+
) async throws(Error) -> Success {
21+
do {
22+
return try await body()
23+
} catch {
24+
// swiftlint:disable:next force_cast
25+
throw error as! Error
26+
}
27+
}
28+
}

Sources/Core/Utilities/OptionalSize+CG.swift

+12
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,15 @@ extension OptionalSize where T == Float {
3737
height.flatMap { .init($0) }
3838
}
3939
}
40+
41+
extension OptionalSize where T == Double {
42+
public init(_ size: CGSize) {
43+
self.init(width: .init(size.width), height: .init(size.height))
44+
}
45+
}
46+
47+
extension OptionalSize where T == Float {
48+
public init(_ size: CGSize) {
49+
self.init(width: .init(size.width), height: .init(size.height))
50+
}
51+
}

Sources/Core/Utilities/Retry.swift

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//
2+
// Retry.swift
3+
// RakuyoKit
4+
//
5+
// Created by Rakuyo on 2025/2/17.
6+
// Copyright © 2024 RakuyoKit. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public enum Retry {
12+
/// Allows retrying a task executed within `task`
13+
///
14+
/// - Parameters:
15+
/// - maxAttempts: Maximum number of retry attempts
16+
/// - delay: Delay for each retry, default is one second
17+
/// - useExponentialBackoff: Whether to use "exponential backoff" to increase the interval between retries. Defaults to `false`, meaning a fixed interval
18+
/// - task: The task to be executed
19+
/// - Returns: The result of the task
20+
@discardableResult
21+
public static func run<T>(
22+
maxAttempts: Int,
23+
delay: TimeInterval = 1,
24+
useExponentialBackoff: Bool = false,
25+
task: @escaping () async throws -> T
26+
) async throws -> T {
27+
var attempts = 0
28+
while attempts < maxAttempts {
29+
do {
30+
return try await task()
31+
} catch {
32+
attempts += 1
33+
if attempts >= maxAttempts {
34+
throw error
35+
}
36+
37+
// Choose between exponential backoff or fixed delay based on the parameter
38+
let delayDuration = useExponentialBackoff ? delay * pow(2, Double(attempts - 1)) : delay
39+
if delayDuration > 0 {
40+
try await Task.sleep(nanoseconds: UInt64(delayDuration * 1_000_000_000))
41+
}
42+
}
43+
}
44+
throw NSError(
45+
domain: "RetryError",
46+
code: -1,
47+
userInfo: [NSLocalizedDescriptionKey: "Failed after \(maxAttempts) attempts"]
48+
)
49+
}
50+
}

Sources/Epoxy/CollectionView/CollectionView.swift

-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ open class CollectionView: EpoxyCollectionView.CollectionView {
2222
super.init(layout: layout, configuration: configuration)
2323

2424
autoDeselectItems = false
25-
showsVerticalScrollIndicator = false
26-
showsHorizontalScrollIndicator = false
2725
}
2826
}
2927

Sources/Epoxy/CollectionView/DecorationView/BaseDecorationView.swift

+15-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
#if !os(watchOS) && !os(tvOS)
1010
import UIKit
1111

12+
// MARK: - BaseDecorationView
13+
1214
open class BaseDecorationView: UICollectionReusableView {
15+
open var decorationViewConfig: DecorationViewConfig { .init() }
16+
1317
override public init(frame: CGRect) {
1418
super.init(frame: frame)
1519

@@ -22,6 +26,16 @@ open class BaseDecorationView: UICollectionReusableView {
2226
config()
2327
}
2428

25-
func config() { }
29+
open func config() {
30+
if let backgroundColor = decorationViewConfig.backgroundColor {
31+
self.backgroundColor = backgroundColor
32+
}
33+
34+
if let cornerRadius = decorationViewConfig.cornerRadius {
35+
layer.cornerRadius = cornerRadius
36+
layer.maskedCorners = decorationViewConfig.maskedCorners
37+
layer.masksToBounds = true
38+
}
39+
}
2640
}
2741
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//
2+
// DecorationViewConfig.swift
3+
// RakuyoKit
4+
//
5+
// Created by Rakuyo on 2025/2/17.
6+
// Copyright © 2024 RakuyoKit. All rights reserved.
7+
//
8+
9+
import UIKit
10+
11+
// MARK: - DecorationViewConfig
12+
13+
public struct DecorationViewConfig {
14+
public let backgroundColor: UIColor?
15+
16+
public let cornerRadius: CGFloat?
17+
18+
public let maskedCorners: CACornerMask
19+
20+
public init(backgroundColor: UIColor? = nil, cornerRadius: CGFloat? = nil, maskedCorners: CACornerMask = []) {
21+
self.backgroundColor = backgroundColor
22+
self.cornerRadius = cornerRadius
23+
self.maskedCorners = maskedCorners
24+
}
25+
}
26+
27+
extension DecorationViewConfig {
28+
public static func topCornerRadiusDecorationView(with cornerRadius: CGFloat, backgroundColor: UIColor? = nil) -> Self {
29+
.init(backgroundColor: backgroundColor, cornerRadius: cornerRadius, maskedCorners: [
30+
.layerMinXMinYCorner,
31+
.layerMaxXMinYCorner,
32+
])
33+
}
34+
35+
public static func bottomCornerRadiusDecorationView(with cornerRadius: CGFloat, backgroundColor: UIColor? = nil) -> Self {
36+
.init(backgroundColor: backgroundColor, cornerRadius: cornerRadius, maskedCorners: [
37+
.layerMinXMaxYCorner,
38+
.layerMaxXMaxYCorner,
39+
])
40+
}
41+
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//
2+
// GrayBackgroundDecorationView.swift
3+
// RakuyoKit
4+
//
5+
// Created by Rakuyo on 2025/2/17.
6+
// Copyright © 2024 RakuyoKit. All rights reserved.
7+
//
8+
9+
#if !os(watchOS) && !os(tvOS)
10+
import UIKit
11+
12+
import RAKConfig
13+
14+
// MARK: - WhiteBackgroundDecorationVGray
15+
16+
/// Set a gray background View for UICollectionView Section
17+
open class GrayBackgroundDecorationView: BaseDecorationView {
18+
override open var decorationViewConfig: DecorationViewConfig {
19+
.customDecorationViewConfig(cornerRadius: nil)
20+
}
21+
}
22+
23+
// MARK: - GrayBackgroundAndCornerRadiusDecorationView
24+
25+
open class GrayBackgroundAndCornerRadiusDecorationView: BaseDecorationView {
26+
/// The default rounded corners.
27+
/// The caller can modify this property globally to achieve the purpose of modifying the global configuration.
28+
public static let defaultCornerRadius: CGFloat = 12
29+
30+
override open var decorationViewConfig: DecorationViewConfig {
31+
.customDecorationViewConfig()
32+
}
33+
}
34+
35+
// MARK: - GrayBackgroundAndTopCornerRadiusDecorationView
36+
37+
open class GrayBackgroundAndTopCornerRadiusDecorationView: BaseDecorationView {
38+
override open var decorationViewConfig: DecorationViewConfig {
39+
.customDecorationViewConfig(maskedCorners: [
40+
.layerMinXMinYCorner,
41+
.layerMaxXMinYCorner,
42+
])
43+
}
44+
}
45+
46+
// MARK: - GrayBackgroundAndBottomCornerRadiusDecorationView
47+
48+
open class GrayBackgroundAndBottomCornerRadiusDecorationView: BaseDecorationView {
49+
override open var decorationViewConfig: DecorationViewConfig {
50+
.customDecorationViewConfig(maskedCorners: [
51+
.layerMinXMaxYCorner,
52+
.layerMaxXMaxYCorner,
53+
])
54+
}
55+
}
56+
57+
extension DecorationViewConfig {
58+
fileprivate static func customDecorationViewConfig(
59+
cornerRadius: CGFloat? = GrayBackgroundAndCornerRadiusDecorationView.defaultCornerRadius,
60+
maskedCorners: CACornerMask = []
61+
) -> Self {
62+
.init(
63+
backgroundColor: Config.color.white,
64+
cornerRadius: cornerRadius,
65+
maskedCorners: maskedCorners
66+
)
67+
}
68+
}
69+
#endif

0 commit comments

Comments
 (0)