Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/enhance #65

Merged
merged 3 commits into from
Feb 17, 2025
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
4 changes: 1 addition & 3 deletions Sources/Config/Mediator/ConfigSourceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ extension ConfigSourceProtocol {
public func appDownloadURL(with area: String = "cn") -> URL? {
let baseURLString = "https://itunes.apple.com/\(area)/app/id"
let urlString = baseURLString + appStoreConnectAppleID

guard let url = URL(string: urlString) else { return nil }
return url
return .init(string: urlString)
}
}
39 changes: 39 additions & 0 deletions Sources/Core/Extensions/CGPoint+RAK.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// CGPoint+RAK.swift
// RakuyoKit
//
// Created by Rakuyo on 2025/2/17.
// Copyright © 2024 RakuyoKit. All rights reserved.
//

import UIKit

// MARK: - CGPoint + ExpressibleByStringLiteral

extension CGPoint: @retroactive ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self = NSCoder.cgPoint(for: value)
}

public init(extendedGraphemeClusterLiteral value: String) {
self = NSCoder.cgPoint(for: value)
}

public init(unicodeScalarLiteral value: String) {
self = NSCoder.cgPoint(for: value)
}
}

// MARK: - Extendable

extension Extendable where Base == CGPoint {
public var halfX: CGFloat {
get { base.x * 0.5 }
set { base.x = newValue * 2 }
}

public var halfY: CGFloat {
get { base.y * 0.5 }
set { base.y = newValue * 2 }
}
}
51 changes: 51 additions & 0 deletions Sources/Core/Utilities/DirectlyConvertibleToImage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// DirectlyConvertibleToImage.swift
// RakuyoKit
//
// Created by Rakuyo on 2025/2/17.
// Copyright © 2024 RakuyoKit. All rights reserved.
//

import UIKit

// MARK: - DirectlyConvertibleToImage

/// Encapsulates types that can be converted into a `UIImage` object
///
/// This protocol is responsible for encapsulating **the synchronous process** of converting an object to `UIImage`.
/// If you need to obtain a `UIImage` **asynchronously**, consider using the `ConvertibleToImage` protocol.
public protocol DirectlyConvertibleToImage {
typealias ConversionResult = Result<UIImage, Error>

var directImage: UIImage? { get }

func getDirectImage() -> ConversionResult
}

// MARK: - Default Implementation

extension DirectlyConvertibleToImage {
public func getDirectImage() -> ConversionResult {
if let directImage { .success(directImage) } else { .failure(ConversionImageError.failure) }
}
}

// MARK: - UIImage + DirectlyConvertibleToImage

extension UIImage: DirectlyConvertibleToImage {
public var directImage: UIImage? { self }
}

// MARK: - UIColor + DirectlyConvertibleToImage

#if !os(watchOS)
extension UIColor: DirectlyConvertibleToImage {
public var directImage: UIImage? { .rak.color(self) }
}
#endif

// MARK: - Data + DirectlyConvertibleToImage

extension Data: DirectlyConvertibleToImage {
public var directImage: UIImage? { .init(data: self, scale: 0.9) }
}
20 changes: 20 additions & 0 deletions Sources/Core/Utilities/FinalTextAttachment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// FinalTextAttachment.swift
// RakuyoKit
//
// Created by Rakuyo on 2025/2/17.
// Copyright © 2024 RakuyoKit. All rights reserved.
//

#if !os(watchOS)
import UIKit

import Then

/// A blank subclass of `NSTextAttachment`, used to conform to the `HigherOrderFunctionalizable` protocol
public final class FinalTextAttachment: NSTextAttachment { }

// MARK: - HigherOrderFunctionalizable

extension FinalTextAttachment: HigherOrderFunctionalizable { }
#endif
28 changes: 28 additions & 0 deletions Sources/Core/Utilities/ForceError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// ForceError.swift
// RakuyoKit
//
// Created by Rakuyo on 2025/2/17.
// Copyright © 2024 RakuyoKit. All rights reserved.
//

import Foundation

/// Forceful error type conversion
public enum ForceError {
/// Forcefully converts an error type to the target type
///
/// Suitable for APIs that do not yet provide typed errors
@discardableResult
public static func convert<Success, Error: Swift.Error>(
to _: Error.Type,
_ body: () async throws -> Success
) async throws(Error) -> Success {
do {
return try await body()
} catch {
// swiftlint:disable:next force_cast
throw error as! Error
}
}
}
12 changes: 12 additions & 0 deletions Sources/Core/Utilities/OptionalSize+CG.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,15 @@ extension OptionalSize where T == Float {
height.flatMap { .init($0) }
}
}

extension OptionalSize where T == Double {
public init(_ size: CGSize) {
self.init(width: .init(size.width), height: .init(size.height))
}
}

extension OptionalSize where T == Float {
public init(_ size: CGSize) {
self.init(width: .init(size.width), height: .init(size.height))
}
}
50 changes: 50 additions & 0 deletions Sources/Core/Utilities/Retry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// Retry.swift
// RakuyoKit
//
// Created by Rakuyo on 2025/2/17.
// Copyright © 2024 RakuyoKit. All rights reserved.
//

import Foundation

public enum Retry {
/// Allows retrying a task executed within `task`
///
/// - Parameters:
/// - maxAttempts: Maximum number of retry attempts
/// - delay: Delay for each retry, default is one second
/// - useExponentialBackoff: Whether to use "exponential backoff" to increase the interval between retries. Defaults to `false`, meaning a fixed interval
/// - task: The task to be executed
/// - Returns: The result of the task
@discardableResult
public static func run<T>(
maxAttempts: Int,
delay: TimeInterval = 1,
useExponentialBackoff: Bool = false,
task: @escaping () async throws -> T
) async throws -> T {
var attempts = 0
while attempts < maxAttempts {
do {
return try await task()
} catch {
attempts += 1
if attempts >= maxAttempts {
throw error
}

// Choose between exponential backoff or fixed delay based on the parameter
let delayDuration = useExponentialBackoff ? delay * pow(2, Double(attempts - 1)) : delay
if delayDuration > 0 {
try await Task.sleep(nanoseconds: UInt64(delayDuration * 1_000_000_000))
}
}
}
throw NSError(
domain: "RetryError",
code: -1,
userInfo: [NSLocalizedDescriptionKey: "Failed after \(maxAttempts) attempts"]
)
}
}
2 changes: 0 additions & 2 deletions Sources/Epoxy/CollectionView/CollectionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ open class CollectionView: EpoxyCollectionView.CollectionView {
super.init(layout: layout, configuration: configuration)

autoDeselectItems = false
showsVerticalScrollIndicator = false
showsHorizontalScrollIndicator = false
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
#if !os(watchOS) && !os(tvOS)
import UIKit

// MARK: - BaseDecorationView

open class BaseDecorationView: UICollectionReusableView {
open var decorationViewConfig: DecorationViewConfig { .init() }

override public init(frame: CGRect) {
super.init(frame: frame)

Expand All @@ -22,6 +26,16 @@ open class BaseDecorationView: UICollectionReusableView {
config()
}

func config() { }
open func config() {
if let backgroundColor = decorationViewConfig.backgroundColor {
self.backgroundColor = backgroundColor
}

if let cornerRadius = decorationViewConfig.cornerRadius {
layer.cornerRadius = cornerRadius
layer.maskedCorners = decorationViewConfig.maskedCorners
layer.masksToBounds = true
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// DecorationViewConfig.swift
// RakuyoKit
//
// Created by Rakuyo on 2025/2/17.
// Copyright © 2024 RakuyoKit. All rights reserved.
//

import UIKit

// MARK: - DecorationViewConfig

public struct DecorationViewConfig {
public let backgroundColor: UIColor?

public let cornerRadius: CGFloat?

public let maskedCorners: CACornerMask

public init(backgroundColor: UIColor? = nil, cornerRadius: CGFloat? = nil, maskedCorners: CACornerMask = []) {
self.backgroundColor = backgroundColor
self.cornerRadius = cornerRadius
self.maskedCorners = maskedCorners
}
}

extension DecorationViewConfig {
public static func topCornerRadiusDecorationView(with cornerRadius: CGFloat, backgroundColor: UIColor? = nil) -> Self {
.init(backgroundColor: backgroundColor, cornerRadius: cornerRadius, maskedCorners: [
.layerMinXMinYCorner,
.layerMaxXMinYCorner,
])
}

public static func bottomCornerRadiusDecorationView(with cornerRadius: CGFloat, backgroundColor: UIColor? = nil) -> Self {
.init(backgroundColor: backgroundColor, cornerRadius: cornerRadius, maskedCorners: [
.layerMinXMaxYCorner,
.layerMaxXMaxYCorner,
])
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// GrayBackgroundDecorationView.swift
// RakuyoKit
//
// Created by Rakuyo on 2025/2/17.
// Copyright © 2024 RakuyoKit. All rights reserved.
//

#if !os(watchOS) && !os(tvOS)
import UIKit

import RAKConfig

// MARK: - WhiteBackgroundDecorationVGray

/// Set a gray background View for UICollectionView Section
open class GrayBackgroundDecorationView: BaseDecorationView {
override open var decorationViewConfig: DecorationViewConfig {
.customDecorationViewConfig(cornerRadius: nil)
}
}

// MARK: - GrayBackgroundAndCornerRadiusDecorationView

open class GrayBackgroundAndCornerRadiusDecorationView: BaseDecorationView {
/// The default rounded corners.
/// The caller can modify this property globally to achieve the purpose of modifying the global configuration.
public static let defaultCornerRadius: CGFloat = 12

override open var decorationViewConfig: DecorationViewConfig {
.customDecorationViewConfig()
}
}

// MARK: - GrayBackgroundAndTopCornerRadiusDecorationView

open class GrayBackgroundAndTopCornerRadiusDecorationView: BaseDecorationView {
override open var decorationViewConfig: DecorationViewConfig {
.customDecorationViewConfig(maskedCorners: [
.layerMinXMinYCorner,
.layerMaxXMinYCorner,
])
}
}

// MARK: - GrayBackgroundAndBottomCornerRadiusDecorationView

open class GrayBackgroundAndBottomCornerRadiusDecorationView: BaseDecorationView {
override open var decorationViewConfig: DecorationViewConfig {
.customDecorationViewConfig(maskedCorners: [
.layerMinXMaxYCorner,
.layerMaxXMaxYCorner,
])
}
}

extension DecorationViewConfig {
fileprivate static func customDecorationViewConfig(
cornerRadius: CGFloat? = GrayBackgroundAndCornerRadiusDecorationView.defaultCornerRadius,
maskedCorners: CACornerMask = []
) -> Self {
.init(
backgroundColor: Config.color.white,
cornerRadius: cornerRadius,
maskedCorners: maskedCorners
)
}
}
#endif
Loading
Loading