diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..2e65cd8 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,8 @@ +--swiftversion 6.1 +--exclude Templates,.build +--header ignore +--extensionacl on-declarations +--trailingcommas never +--decimalgrouping 3,4 +--modifierorder isolated,nonisolated,private,fileprivate,internal,package,public,open,private(set),fileprivate(set),internal(set),package(set),public(set),final,convenience +--disable swiftTestingTestCaseNames,redundantSendable,wrapMultilineStatementBraces diff --git a/Sources/FuturedArchitecture/Architecture/Alert/AlertModifier.swift b/Sources/FuturedArchitecture/Architecture/Alert/AlertModifier.swift index eed0c04..16ff120 100644 --- a/Sources/FuturedArchitecture/Architecture/Alert/AlertModifier.swift +++ b/Sources/FuturedArchitecture/Architecture/Alert/AlertModifier.swift @@ -4,7 +4,7 @@ private struct AlertModifier: ViewModifier { @Binding var model: AlertModel? init(_ model: Binding) { - self._model = model + _model = model } func body(content: Content) -> some View { diff --git a/Sources/FuturedArchitecture/Architecture/ComponentModel.swift b/Sources/FuturedArchitecture/Architecture/ComponentModel.swift index bdc7138..bf3b7b0 100644 --- a/Sources/FuturedArchitecture/Architecture/ComponentModel.swift +++ b/Sources/FuturedArchitecture/Architecture/ComponentModel.swift @@ -21,7 +21,6 @@ import Foundation /// compile but SwiftUI views will not react to state changes. @MainActor public protocol ComponentModel: AnyObject { - /// Type used to pass events to the *coordinator*. `enum` is used in most cases, but not required. associatedtype Event diff --git a/Sources/FuturedArchitecture/Architecture/Coordinator.swift b/Sources/FuturedArchitecture/Architecture/Coordinator.swift index 078472c..19e3c67 100644 --- a/Sources/FuturedArchitecture/Architecture/Coordinator.swift +++ b/Sources/FuturedArchitecture/Architecture/Coordinator.swift @@ -49,17 +49,17 @@ extension Coordinator { public func present(modal destination: Destination, type: ModalCoverModelStyle) { switch type { case .sheet: - self.modalCover = .init(destination: destination, style: .sheet) + modalCover = .init(destination: destination, style: .sheet) #if !os(macOS) - case .fullscreenCover: - self.modalCover = .init(destination: destination, style: .fullscreenCover) + case .fullscreenCover: + modalCover = .init(destination: destination, style: .fullscreenCover) #endif } } /// Convenience method for dismissing a modal. public func dismissModal() { - self.modalCover = nil + modalCover = nil } public func onModalDismiss() {} @@ -88,12 +88,12 @@ public protocol NavigationStackCoordinator: Coordinator { extension NavigationStackCoordinator { /// Convenience function used to add new view to the navigation stack. public func navigate(to destination: Destination) { - self.path.append(destination) + path.append(destination) } /// Convenience function used to remove topmost view from the navigation stack. public func pop() { - self.path.removeLast() + path.removeLast() } /// Convenience function used to remove all views from the stack, until the provided destination. @@ -101,11 +101,11 @@ extension NavigationStackCoordinator { /// is not currently on the stack, all views are removed. /// - Experiment: This API is in preview and subject to change. public func pop(to destination: Destination) { - guard let index = self.path.lastIndex(of: destination) else { + guard let index = path.lastIndex(of: destination) else { assertionFailure("Destination not found on the stack") return } - self.path = Array(path[path.startIndex...index]) + path = Array(path[path.startIndex ... index]) } public func popToRoot() { diff --git a/Sources/FuturedArchitecture/Architecture/DataCache.swift b/Sources/FuturedArchitecture/Architecture/DataCache.swift index ea8645d..592d62f 100644 --- a/Sources/FuturedArchitecture/Architecture/DataCache.swift +++ b/Sources/FuturedArchitecture/Architecture/DataCache.swift @@ -18,7 +18,6 @@ import Foundation @Observable @MainActor public final class DataCache { - /// The data held by this data cache. public private(set) var value: Model @@ -50,10 +49,10 @@ public final class DataCache { with newItems: T ) where T: RangeReplaceableCollection & MutableCollection, T.Element: Identifiable & Equatable { guard !newItems.isEmpty else { return } - let original = self.value[keyPath: keyPath] + let original = value[keyPath: keyPath] let merged = merging(original, with: newItems) guard !merged.elementsEqual(original) else { return } - self.value[keyPath: keyPath] = merged + value[keyPath: keyPath] = merged } /// Optional-collection variant of `populate(_:with:)`. @@ -62,10 +61,10 @@ public final class DataCache { with newItems: T ) where T: RangeReplaceableCollection & MutableCollection, T.Element: Identifiable & Equatable { guard !newItems.isEmpty else { return } - let original = self.value[keyPath: keyPath] ?? T() + let original = value[keyPath: keyPath] ?? T() let merged = merging(original, with: newItems) guard !merged.elementsEqual(original) else { return } - self.value[keyPath: keyPath] = merged + value[keyPath: keyPath] = merged } private func merging( @@ -84,7 +83,7 @@ public final class DataCache { var appendedIds = existingIds for item in newItems where appendedIds.insert(item.id).inserted { - result.append(newItemsDict[item.id]!) + result.append(newItemsDict[item.id] ?? item) } return result diff --git a/Sources/FuturedArchitecture/Architecture/ModalCoverModel.swift b/Sources/FuturedArchitecture/Architecture/ModalCoverModel.swift index 72d984f..6440217 100644 --- a/Sources/FuturedArchitecture/Architecture/ModalCoverModel.swift +++ b/Sources/FuturedArchitecture/Architecture/ModalCoverModel.swift @@ -16,11 +16,11 @@ public enum SheetDetent: Hashable { func detent() -> PresentationDetent { switch self { case .medium: - return .medium + .medium case .large: - return .large + .large case let .fraction(fraction): - return .fraction(fraction) + .fraction(fraction) } } } @@ -32,7 +32,7 @@ public enum SheetDetent: Hashable { public enum ModalCoverModelStyle { case sheet #if !os(macOS) - case fullscreenCover + case fullscreenCover #endif } diff --git a/Sources/FuturedArchitecture/Architecture/ModalCoverModifier.swift b/Sources/FuturedArchitecture/Architecture/ModalCoverModifier.swift index 2d8a491..0a71b07 100644 --- a/Sources/FuturedArchitecture/Architecture/ModalCoverModifier.swift +++ b/Sources/FuturedArchitecture/Architecture/ModalCoverModifier.swift @@ -17,23 +17,23 @@ struct ModalCoverModifier: ViewModifier { } #if os(macOS) - func body(content: Content) -> some View { - content - .sheet(item: sheetBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:)) - } + func body(content: Content) -> some View { + content + .sheet(item: sheetBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:)) + } #else - private var fullscreenCoverBinding: Binding { - .init { - coordinator.modalCover?.style == .fullscreenCover ? coordinator.modalCover?.destination : nil - } set: { destination in - coordinator.modalCover = destination.map { .init(destination: $0, style: .fullscreenCover) } + private var fullscreenCoverBinding: Binding { + .init { + coordinator.modalCover?.style == .fullscreenCover ? coordinator.modalCover?.destination : nil + } set: { destination in + coordinator.modalCover = destination.map { .init(destination: $0, style: .fullscreenCover) } + } } - } - func body(content: Content) -> some View { - content - .sheet(item: sheetBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:)) - .fullScreenCover(item: fullscreenCoverBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:)) - } + func body(content: Content) -> some View { + content + .sheet(item: sheetBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:)) + .fullScreenCover(item: fullscreenCoverBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:)) + } #endif } diff --git a/Sources/FuturedArchitecture/Architecture/NavigationStackFlow.swift b/Sources/FuturedArchitecture/Architecture/NavigationStackFlow.swift index 0bdc28e..5f77407 100644 --- a/Sources/FuturedArchitecture/Architecture/NavigationStackFlow.swift +++ b/Sources/FuturedArchitecture/Architecture/NavigationStackFlow.swift @@ -19,8 +19,8 @@ public struct NavigationStackFlow Content ) { - self.navigationDetents = detents.map { Set($0.map { $0.detent() }) } - self._coordinator = State(wrappedValue: coordinator) + navigationDetents = detents.map { Set($0.map { $0.detent() }) } + _coordinator = State(wrappedValue: coordinator) self.content = content } diff --git a/Sources/FuturedArchitecture/Architecture/TabViewFlow.swift b/Sources/FuturedArchitecture/Architecture/TabViewFlow.swift index 6a1c7e0..91a53a2 100644 --- a/Sources/FuturedArchitecture/Architecture/TabViewFlow.swift +++ b/Sources/FuturedArchitecture/Architecture/TabViewFlow.swift @@ -19,7 +19,7 @@ public struct TabViewFlow: View { /// - content: The definition of tabs held by this TabView should be placed into this ViewBuilder. You are required to use instances of `Tab` /// type as tags of the views. For an example refer to the template. public init(coordinator: Coordinator, @ViewBuilder content: @MainActor @escaping () -> Content) { - self._coordinator = State(wrappedValue: coordinator) + _coordinator = State(wrappedValue: coordinator) self.content = content } @@ -56,7 +56,7 @@ public struct TabContentFlow content: @MainActor @escaping () -> Content) { - self._coordinator = State(wrappedValue: coordinator) + _coordinator = State(wrappedValue: coordinator) self.content = content } diff --git a/Sources/FuturedHelpers/Helpers/ConfigKey.swift b/Sources/FuturedHelpers/Helpers/ConfigKey.swift index 20332ce..e6259b3 100644 --- a/Sources/FuturedHelpers/Helpers/ConfigKey.swift +++ b/Sources/FuturedHelpers/Helpers/ConfigKey.swift @@ -7,9 +7,9 @@ public protocol ConfigKey: RawRepresentable where RawValue == String { var value: String { get throws } } -public extension ConfigKey { +extension ConfigKey { /// Default implementation that reads from Info.plist and handles Base64 decoding - var value: String { + public var value: String { get throws { guard let rawValue = Bundle.main.object(forInfoDictionaryKey: rawValue) as? String else { throw ConfigKeyError.valueNotFound(key: rawValue) diff --git a/Sources/FuturedHelpers/Helpers/Font+Extension.swift b/Sources/FuturedHelpers/Helpers/Font+Extension.swift index 38121c0..749c567 100644 --- a/Sources/FuturedHelpers/Helpers/Font+Extension.swift +++ b/Sources/FuturedHelpers/Helpers/Font+Extension.swift @@ -1,104 +1,103 @@ #if canImport(UIKit) -import SwiftUI + import SwiftUI -extension Font.TextStyle { - - /// The UIKit dynamic text style to use for fonts. - public var uiFontTextStyle: UIFont.TextStyle { - switch self { - case .largeTitle: - .largeTitle - case .title: - .title1 - case .title2: - .title2 - case .title3: - .title3 - case .headline: - .headline - case .subheadline: - .subheadline - case .body: - .body - case .callout: - .callout - case .footnote: - .footnote - case .caption: - .caption1 - case .caption2: - .caption2 - case .extraLargeTitle: - .extraLargeTitle - case .extraLargeTitle2: - .extraLargeTitle2 - @unknown default: - .body + extension Font.TextStyle { + /// The UIKit dynamic text style to use for fonts. + public var uiFontTextStyle: UIFont.TextStyle { + switch self { + case .largeTitle: + .largeTitle + case .title: + .title1 + case .title2: + .title2 + case .title3: + .title3 + case .headline: + .headline + case .subheadline: + .subheadline + case .body: + .body + case .callout: + .callout + case .footnote: + .footnote + case .caption: + .caption1 + case .caption2: + .caption2 + case .extraLargeTitle: + .extraLargeTitle + case .extraLargeTitle2: + .extraLargeTitle2 + @unknown default: + .body + } } } -} -extension Font.Weight { - /// The UIKit weight that represents standard typeface style. - public var uiFontWeight: UIFont.Weight { - switch self { - case .ultraLight: - .ultraLight - case .thin: - .thin - case .regular: - .regular - case .medium: - .medium - case .semibold: - .semibold - case .bold: - .bold - case .heavy: - .heavy - case .black: - .black - default: - .regular + extension Font.Weight { + /// The UIKit weight that represents standard typeface style. + public var uiFontWeight: UIFont.Weight { + switch self { + case .ultraLight: + .ultraLight + case .thin: + .thin + case .regular: + .regular + case .medium: + .medium + case .semibold: + .semibold + case .bold: + .bold + case .heavy: + .heavy + case .black: + .black + default: + .regular + } } } -} -extension Font.Width { - /// AThe UIKit width to use for fonts that have multiple widths. - public var uiFontWidth: UIFont.Width { - switch self { - case .compressed: - .compressed - case .condensed: - .condensed - case .expanded: - .expanded - case .standard: - .standard - default: - .standard + extension Font.Width { + /// AThe UIKit width to use for fonts that have multiple widths. + public var uiFontWidth: UIFont.Width { + switch self { + case .compressed: + .compressed + case .condensed: + .condensed + case .expanded: + .expanded + case .standard: + .standard + default: + .standard + } } } -} -extension Font.Design { - /// The UIKit design that describes the system-defined typeface designs. - public var uiDesign: UIFontDescriptor.SystemDesign { - switch self { - case .default: - .default - case .monospaced: - .monospaced - case .rounded: - .rounded - case .serif: - .serif - @unknown default: - .default + extension Font.Design { + /// The UIKit design that describes the system-defined typeface designs. + public var uiDesign: UIFontDescriptor.SystemDesign { + switch self { + case .default: + .default + case .monospaced: + .monospaced + case .rounded: + .rounded + case .serif: + .serif + @unknown default: + .default + } } } -} #endif diff --git a/Sources/FuturedHelpers/Helpers/SceneDelegate.swift b/Sources/FuturedHelpers/Helpers/SceneDelegate.swift index 878cf46..65a5b31 100644 --- a/Sources/FuturedHelpers/Helpers/SceneDelegate.swift +++ b/Sources/FuturedHelpers/Helpers/SceneDelegate.swift @@ -8,74 +8,74 @@ import SwiftUI #if !os(macOS) -/// Protocol for the app's scene delegate that can be observed via SwiftUI's environment. -/// -/// Conform your `UIWindowSceneDelegate` to this protocol and annotate it with `@Observable`. -/// Then inject it into the SwiftUI environment from your root view using `.environment()`. -/// -/// ```swift -/// // 1. Declare the delegate -/// @Observable -/// final class MySceneDelegate: NSObject, AppSceneDelegate { -/// var delegate: SceneDelegate? -/// // ... UIWindowSceneDelegate implementation -/// } -/// -/// // 2. Inject at the root -/// @main -/// struct MyApp: App { -/// @State private var sceneDelegate = MySceneDelegate() -/// -/// var body: some Scene { -/// WindowGroup { -/// RootView() -/// .environment(sceneDelegate) -/// } -/// } -/// } -/// -/// // 3. Use in a view -/// struct RootView: View { -/// var body: some View { -/// ContentView() -/// .set(appSceneDelegateClass: MySceneDelegate.self, sceneDelegate: mySceneDelegate) -/// } -/// } -/// ``` -/// -/// - Important: Forgetting to inject the delegate via `.environment()` will cause a runtime crash. -/// Unlike `@EnvironmentObject`, the compiler cannot warn you about a missing injection. -public protocol AppSceneDelegate: AnyObject, UIWindowSceneDelegate, Observable { - var delegate: SceneDelegate? { get set } -} + /// Protocol for the app's scene delegate that can be observed via SwiftUI's environment. + /// + /// Conform your `UIWindowSceneDelegate` to this protocol and annotate it with `@Observable`. + /// Then inject it into the SwiftUI environment from your root view using `.environment()`. + /// + /// ```swift + /// // 1. Declare the delegate + /// @Observable + /// final class MySceneDelegate: NSObject, AppSceneDelegate { + /// var delegate: SceneDelegate? + /// // ... UIWindowSceneDelegate implementation + /// } + /// + /// // 2. Inject at the root + /// @main + /// struct MyApp: App { + /// @State private var sceneDelegate = MySceneDelegate() + /// + /// var body: some Scene { + /// WindowGroup { + /// RootView() + /// .environment(sceneDelegate) + /// } + /// } + /// } + /// + /// // 3. Use in a view + /// struct RootView: View { + /// var body: some View { + /// ContentView() + /// .set(appSceneDelegateClass: MySceneDelegate.self, sceneDelegate: mySceneDelegate) + /// } + /// } + /// ``` + /// + /// - Important: Forgetting to inject the delegate via `.environment()` will cause a runtime crash. + /// Unlike `@EnvironmentObject`, the compiler cannot warn you about a missing injection. + public protocol AppSceneDelegate: AnyObject, UIWindowSceneDelegate, Observable { + var delegate: SceneDelegate? { get set } + } -public protocol SceneDelegate: AnyObject { - func sceneDidEnterBackground(_ scene: UIScene) - func sceneWillEnterForeground(_ scene: UIScene) -} + public protocol SceneDelegate: AnyObject { + func sceneDidEnterBackground(_ scene: UIScene) + func sceneWillEnterForeground(_ scene: UIScene) + } -private struct SceneDelegateWrapperViewModifier: ViewModifier { - @Environment(Delegate.self) private var appSceneDelegate: Delegate? + private struct SceneDelegateWrapperViewModifier: ViewModifier { + @Environment(Delegate.self) private var appSceneDelegate: Delegate? - let sceneDelegate: SceneDelegate? + let sceneDelegate: SceneDelegate? - func body(content: Content) -> some View { - content - .onAppear { - appSceneDelegate?.delegate = sceneDelegate - } + func body(content: Content) -> some View { + content + .onAppear { + appSceneDelegate?.delegate = sceneDelegate + } + } } -} -extension View { - /// Sets the SceneDelegate for the application. - /// - Parameter appSceneDelegateClass: The class which conforms to the UIWindowSceneDelegate. - /// - Parameter sceneDelegate: The SceneDelegate to set. - /// - Description: - /// In the main app root view call this modifier and pass the SceneDelegate. You need to specify the AppSceneDelegate which conforms to the UIWindowSceneDelegate. - /// The AppSceneDelegate must be injected into the environment using `.environment()` modifier. - public func set(appSceneDelegateClass: T.Type, sceneDelegate: SceneDelegate) -> some View { - modifier(SceneDelegateWrapperViewModifier(sceneDelegate: sceneDelegate)) + extension View { + /// Sets the SceneDelegate for the application. + /// - Parameter appSceneDelegateClass: The class which conforms to the UIWindowSceneDelegate. + /// - Parameter sceneDelegate: The SceneDelegate to set. + /// - Description: + /// In the main app root view call this modifier and pass the SceneDelegate. You need to specify the AppSceneDelegate which conforms to the UIWindowSceneDelegate. + /// The AppSceneDelegate must be injected into the environment using `.environment()` modifier. + public func set(appSceneDelegateClass _: T.Type, sceneDelegate: SceneDelegate) -> some View { + modifier(SceneDelegateWrapperViewModifier(sceneDelegate: sceneDelegate)) + } } -} #endif diff --git a/Sources/FuturedHelpers/Helpers/TextStyle.swift b/Sources/FuturedHelpers/Helpers/TextStyle.swift index 3f88e2d..fe281c0 100644 --- a/Sources/FuturedHelpers/Helpers/TextStyle.swift +++ b/Sources/FuturedHelpers/Helpers/TextStyle.swift @@ -1,291 +1,285 @@ #if canImport(UIKit) -import SwiftUI - -// MARK: - TextStyle - -/// A structure that represents a text style. -/// Use this structure to define a text style with a specific font, size, line height, font scaling, letter spacing, text case, and text decoration. -/// You can apply the text style to a view using the `textStyle(_:)` modifier or to a text using the `textStyleText(_:)` modifier if you need to apply a text style to a `Text` specifically. -/// Additionally, you can apply the text style to an attributed string using the `stylize(with:)` method or initialize an attributed string with the text style using the `NSAttributedString` initializer. -/// Example: -/// ```swift -/// extension TextStyle { -/// static let title = TextStyle( -/// fontType: .system(weight: .bold), -/// size: 24, -/// lineHeight: 32, -/// scaling: .relativeTo(textStyle: .title), -/// letter: .relative(percent: 5), -/// textCase: .uppercase, -/// textDecoration: .underline -/// ) -/// } -/// -/// struct ExampleView: View { -/// var body: some View { -/// Text("Title") -/// .textStyle(.title) -/// } -/// } -/// ``` -/// - Note: The `textStyleText(_:)` modifier does not apply line spacing, text case, or vertical padding based on line height. -/// - Note: The `stylize(with:)` method does not apply line spacing, text case, or vertical padding based on line height. -/// - Note: The `NSAttributedString` initializer does not apply line spacing, text case, or vertical padding based on line height. -public struct TextStyle { - - /// An enumeration that represents a type of font for a text style. - public enum FontType { - - /// A custom font specified by its name. - /// - Parameter name: The name of the font to be used - case custom(name: String) - - /// A system font specified by its weight and width. - /// - Parameters: - /// - weight: The weight of the system font. - /// - width: The width of the system font. Default is `.standard`. - /// - design: The design of the system font. Default is `.default`. - case system(weight: Font.Weight, width: Font.Width = .standard, design: Font.Design = .default) } - - /// An enumeration that represents a type of font scaling for a text style. - public enum FontScaling { - - /// The default font scaling using default font metrics object. - case `default` - - /// The fixed size font scaling without any scaling. - case fixedSize - - /// The font scaling relative to the specified text style. - /// - Parameter textStyle: The text style to which the font scaling is relative. - case relativeTo(textStyle: Font.TextStyle) - - /// The font metrics object for the specified font scaling. - public var fontMetrics: UIFontMetrics? { - switch self { - case .default: - .default - case .fixedSize: - nil - case .relativeTo(let textStyle): - .init(forTextStyle: textStyle.uiFontTextStyle) - } + import SwiftUI + + // MARK: - TextStyle + + /// A structure that represents a text style. + /// Use this structure to define a text style with a specific font, size, line height, font scaling, letter spacing, text case, and text decoration. + /// You can apply the text style to a view using the `textStyle(_:)` modifier or to a text using the `textStyleText(_:)` modifier if you need to apply a text style to a `Text` specifically. + /// Additionally, you can apply the text style to an attributed string using the `stylize(with:)` method or initialize an attributed string with the text style using the `NSAttributedString` initializer. + /// Example: + /// ```swift + /// extension TextStyle { + /// static let title = TextStyle( + /// fontType: .system(weight: .bold), + /// size: 24, + /// lineHeight: 32, + /// scaling: .relativeTo(textStyle: .title), + /// letter: .relative(percent: 5), + /// textCase: .uppercase, + /// textDecoration: .underline + /// ) + /// } + /// + /// struct ExampleView: View { + /// var body: some View { + /// Text("Title") + /// .textStyle(.title) + /// } + /// } + /// ``` + /// - Note: The `textStyleText(_:)` modifier does not apply line spacing, text case, or vertical padding based on line height. + /// - Note: The `stylize(with:)` method does not apply line spacing, text case, or vertical padding based on line height. + /// - Note: The `NSAttributedString` initializer does not apply line spacing, text case, or vertical padding based on line height. + public struct TextStyle { + /// An enumeration that represents a type of font for a text style. + public enum FontType { + /// A custom font specified by its name. + /// - Parameter name: The name of the font to be used + case custom(name: String) + + /// A system font specified by its weight and width. + /// - Parameters: + /// - weight: The weight of the system font. + /// - width: The width of the system font. Default is `.standard`. + /// - design: The design of the system font. Default is `.default`. + case system(weight: Font.Weight, width: Font.Width = .standard, design: Font.Design = .default) } - } - /// An enumeration that represents a type of letter spacing for a text style. - public enum Letter { + /// An enumeration that represents a type of font scaling for a text style. + public enum FontScaling { + /// The default font scaling using default font metrics object. + case `default` + + /// The fixed size font scaling without any scaling. + case fixedSize + + /// The font scaling relative to the specified text style. + /// - Parameter textStyle: The text style to which the font scaling is relative. + case relativeTo(textStyle: Font.TextStyle) + + /// The font metrics object for the specified font scaling. + public var fontMetrics: UIFontMetrics? { + switch self { + case .default: + .default + case .fixedSize: + nil + case let .relativeTo(textStyle): + .init(forTextStyle: textStyle.uiFontTextStyle) + } + } + } - /// The relative letter spacing expressed as a percentage of the font size. - /// - Parameter percent: The percentage of the font size for the relative letter spacing. - case relative(percent: CGFloat) + /// An enumeration that represents a type of letter spacing for a text style. + public enum Letter { + /// The relative letter spacing expressed as a percentage of the font size. + /// - Parameter percent: The percentage of the font size for the relative letter spacing. + case relative(percent: CGFloat) - /// The absolute letter spacing expressed in points. - /// - Parameter points: The points for the absolute letter spacing. - case absolute(points: CGFloat) - } + /// The absolute letter spacing expressed in points. + /// - Parameter points: The points for the absolute letter spacing. + case absolute(points: CGFloat) + } - /// An enumeration that represents a type of text decoration for a text style. - public enum TextDecoration: String { + /// An enumeration that represents a type of text decoration for a text style. + public enum TextDecoration: String { + /// The underline text decoration. + case underline - /// The underline text decoration. - case underline + /// The strikethrough text decoration. + case strikethrough + } - /// The strikethrough text decoration. - case strikethrough - } + /// The type of font for the text style. + public let fontType: FontType - /// The type of font for the text style. - public let fontType: FontType + /// The size of the font for the text style. + public let size: CGFloat - /// The size of the font for the text style. - public let size: CGFloat + /// The line height of the font for the text style. + public let lineHeight: CGFloat - /// The line height of the font for the text style. - public let lineHeight: CGFloat + /// The font scaling for the text style. + public let scaling: FontScaling - /// The font scaling for the text style. - public let scaling: FontScaling + /// The letter spacing for the text style. + public let letter: Letter - /// The letter spacing for the text style. - public let letter: Letter + /// The text case for the text style. + public let textCase: Text.Case? - /// The text case for the text style. - public let textCase: Text.Case? + /// The text decoration for the text style. + public let textDecoration: TextDecoration? - /// The text decoration for the text style. - public let textDecoration: TextDecoration? + /// The line spacing of the font for the text style. + public var lineSpacing: CGFloat { + (scaling.fontMetrics?.scaledValue(for: lineHeight) ?? lineHeight) - uiFont.lineHeight + } - /// The line spacing of the font for the text style. - public var lineSpacing: CGFloat { - (scaling.fontMetrics?.scaledValue(for: lineHeight) ?? lineHeight) - uiFont.lineHeight - } + /// The font for the text style. + public var font: Font { + switch fontType { + case let .custom(name): + switch scaling { + case .default: + .custom(name, size: size) + case .fixedSize: + .custom(name, fixedSize: size) + case let .relativeTo(textStyle): + .custom(name, size: size, relativeTo: textStyle) + } + case let .system(weight, width, design): + .system(size: scaling.fontMetrics?.scaledValue(for: size) ?? size, weight: weight, design: design).width(width) + } + } - /// The font for the text style. - public var font: Font { - switch fontType { - case let .custom(name): - switch scaling { - case .default: - .custom(name, size: size) - case .fixedSize: - .custom(name, fixedSize: size) - case let .relativeTo(textStyle): - .custom(name, size: size, relativeTo: textStyle) + /// The `UIFont` for the text style. + public var uiFont: UIFont { + switch fontType { + case let .custom(name): + let font = UIFont(name: name, size: size) ?? .systemFont(ofSize: size) + return scaling.fontMetrics?.scaledFont(for: font) ?? font + case let .system(weight, width, design): + let font = UIFont.systemFont( + ofSize: scaling.fontMetrics?.scaledValue(for: size) ?? size, + weight: weight.uiFontWeight, + width: width.uiFontWidth + ) + if let descriptor = font.fontDescriptor.withDesign(design.uiDesign) { + return .init(descriptor: descriptor, size: size) + } else { + return font + } } - case let .system(weight, width, design): - .system(size: scaling.fontMetrics?.scaledValue(for: size) ?? size, weight: weight, design: design).width(width) } - } - /// The `UIFont` for the text style. - public var uiFont: UIFont { - switch fontType { - case let .custom(name): - let font = UIFont(name: name, size: size) ?? .systemFont(ofSize: size) - return scaling.fontMetrics?.scaledFont(for: font) ?? font - case let .system(weight, width, design): - let font = UIFont.systemFont( - ofSize: scaling.fontMetrics?.scaledValue(for: size) ?? size, - weight: weight.uiFontWeight, - width: width.uiFontWidth - ) - if let descriptor = font.fontDescriptor.withDesign(design.uiDesign) { - return .init(descriptor: descriptor, size: size) - } else { - return font + /// The kerning of the font for the text style. + public var kerning: CGFloat { + switch letter { + case let .relative(percent): + (scaling.fontMetrics?.scaledValue(for: lineHeight) ?? lineHeight) * (percent / 100.0) + case let .absolute(pixels): + scaling.fontMetrics?.scaledValue(for: pixels) ?? pixels } } - } - /// The kerning of the font for the text style. - public var kerning: CGFloat { - switch letter { - case .relative(let percent): - (scaling.fontMetrics?.scaledValue(for: lineHeight) ?? lineHeight) * (percent / 100.0) - case .absolute(let pixels): - scaling.fontMetrics?.scaledValue(for: pixels) ?? pixels + /// Initializes a text style with the specified properties. + /// - Parameters: + /// - fontType: The type of font for the text style. + /// - size: The size of the font for the text style. + /// - lineHeight: The line height of the font for the text style. + /// - scaling: The font scaling for the text style. Default is `.default`. + /// - letter: The letter spacing for the text style. Default is `.absolute(points: 0)`. + /// - textCase: The text case for the text style. Default is `nil`. + /// - textDecoration: The text decoration for the text style. Default is `nil`. + public init( + fontType: FontType, + size: CGFloat, + lineHeight: CGFloat, + scaling: FontScaling = .default, + letter: Letter = .absolute(points: 0), + textCase: Text.Case? = nil, + textDecoration: TextDecoration? = nil + ) { + self.fontType = fontType + self.size = size + self.lineHeight = lineHeight + self.scaling = scaling + self.letter = letter + self.textCase = textCase + self.textDecoration = textDecoration } } - /// Initializes a text style with the specified properties. - /// - Parameters: - /// - fontType: The type of font for the text style. - /// - size: The size of the font for the text style. - /// - lineHeight: The line height of the font for the text style. - /// - scaling: The font scaling for the text style. Default is `.default`. - /// - letter: The letter spacing for the text style. Default is `.absolute(points: 0)`. - /// - textCase: The text case for the text style. Default is `nil`. - /// - textDecoration: The text decoration for the text style. Default is `nil`. - public init( - fontType: FontType, - size: CGFloat, - lineHeight: CGFloat, - scaling: FontScaling = .default, - letter: Letter = .absolute(points: 0), - textCase: Text.Case? = nil, - textDecoration: TextDecoration? = nil - ) { - self.fontType = fontType - self.size = size - self.lineHeight = lineHeight - self.scaling = scaling - self.letter = letter - self.textCase = textCase - self.textDecoration = textDecoration - } -} - -// MARK: - Extensions - -extension View { - /// Applies the specified text style to the view. - /// - Parameter style: The text style to apply to the view. - /// - Returns: A view that applies the specified text style. - public func textStyle(_ style: TextStyle) -> some View { - self - .font(style.font) - .lineSpacing(style.lineSpacing) - .kerning(style.kerning) - .underline(style.textDecoration == .underline) - .strikethrough(style.textDecoration == .strikethrough) - .textCase(style.textCase) - .padding(.vertical, style.lineSpacing / 2) - } + // MARK: - Extensions + + extension View { + /// Applies the specified text style to the view. + /// - Parameter style: The text style to apply to the view. + /// - Returns: A view that applies the specified text style. + public func textStyle(_ style: TextStyle) -> some View { + font(style.font) + .lineSpacing(style.lineSpacing) + .kerning(style.kerning) + .underline(style.textDecoration == .underline) + .strikethrough(style.textDecoration == .strikethrough) + .textCase(style.textCase) + .padding(.vertical, style.lineSpacing / 2) + } - /// Applies the specified text style to the view with a custom foreground color. - /// - Parameters: - /// - style: The text style to apply to the view. - /// - foregroundStyle: The foreground color to apply to the view. - /// - Returns: A view that applies the specified text style with the custom foreground color. - public func textStyle(_ style: TextStyle, foregroundStyle: Color) -> some View { - self - .textStyle(style) - .foregroundStyle(foregroundStyle) - } -} - -extension Text { - /// Applies the specified text style to the text. - /// Use this modifier when you need to apply a text style to a `Text`, rather than to a generic View. - /// - Parameter style: The text style to apply to the text. - /// - Returns: A text that applies the specified text style. - /// - Note: This modifier does not apply line spacing, text case, or vertical padding based on line height. - public func textStyleText(_ style: TextStyle) -> Text { - self - .font(style.font) - .kerning(style.kerning) - .underline(style.textDecoration == .underline) - .strikethrough(style.textDecoration == .strikethrough) + /// Applies the specified text style to the view with a custom foreground color. + /// - Parameters: + /// - style: The text style to apply to the view. + /// - foregroundStyle: The foreground color to apply to the view. + /// - Returns: A view that applies the specified text style with the custom foreground color. + public func textStyle(_ style: TextStyle, foregroundStyle: Color) -> some View { + textStyle(style) + .foregroundStyle(foregroundStyle) + } } - /// Applies the specified text style to the text with a custom foreground color. - /// Use this modifier when you need to apply a text style to a `Text`, rather than to a generic View. - /// - Parameters: - /// - style: The text style to apply to the text. - /// - foregroundStyle: The foreground color to apply to the text - /// - Returns: A text that applies the specified text style with the custom foreground color. - /// - Note: This modifier does not apply line spacing, text case, or vertical padding based on line height. - public func textStyleText(_ style: TextStyle, foregroundStyle: Color) -> Text { - self - .textStyleText(style) - .foregroundStyle(foregroundStyle) + extension Text { + /// Applies the specified text style to the text. + /// Use this modifier when you need to apply a text style to a `Text`, rather than to a generic View. + /// - Parameter style: The text style to apply to the text. + /// - Returns: A text that applies the specified text style. + /// - Note: This modifier does not apply line spacing, text case, or vertical padding based on line height. + public func textStyleText(_ style: TextStyle) -> Text { + font(style.font) + .kerning(style.kerning) + .underline(style.textDecoration == .underline) + .strikethrough(style.textDecoration == .strikethrough) + } + + /// Applies the specified text style to the text with a custom foreground color. + /// Use this modifier when you need to apply a text style to a `Text`, rather than to a generic View. + /// - Parameters: + /// - style: The text style to apply to the text. + /// - foregroundStyle: The foreground color to apply to the text + /// - Returns: A text that applies the specified text style with the custom foreground color. + /// - Note: This modifier does not apply line spacing, text case, or vertical padding based on line height. + public func textStyleText(_ style: TextStyle, foregroundStyle: Color) -> Text { + textStyleText(style) + .foregroundStyle(foregroundStyle) + } } -} - -extension AttributedStringProtocol { - /// Applies the specified text style to the attributed string. - /// - Parameter style: The text style to apply to the attributed string. - /// - Returns: An attributed string that applies the specified text style. - /// - Note: This method does not apply line spacing, text case, or vertical padding based on line height. - public mutating func stylize(with style: TextStyle) { - self.font = style.font - self.kern = style.kerning - self.underlineStyle = style.textDecoration == .underline ? .single : nil - self.strikethroughStyle = style.textDecoration == .strikethrough ? .single : nil + + extension AttributedStringProtocol { + /// Applies the specified text style to the attributed string. + /// - Parameter style: The text style to apply to the attributed string. + /// - Returns: An attributed string that applies the specified text style. + /// - Note: This method does not apply line spacing, text case, or vertical padding based on line height. + public mutating func stylize(with style: TextStyle) { + // swiftformat:disable redundantSelf -- AttributedStringProtocol attributes are dynamic members + self.font = style.font + self.kern = style.kerning + self.underlineStyle = style.textDecoration == .underline ? .single : nil + self.strikethroughStyle = style.textDecoration == .strikethrough ? .single : nil + // swiftformat:enable redundantSelf + } } -} - -extension NSAttributedString { - /// Initializes an attributed string with the specified properties. - /// - Parameters: - /// - string: The string for the attributed string. - /// - textStyle: The text style for the attributed string. - /// - additionalAttributes: The additional attributes for the attributed string. Default is `nil`. - public convenience init( - string: String, - textStyle: TextStyle, - additionalAttributes: [NSAttributedString.Key: Any]? = nil - ) { - self.init( - string: string, - attributes: [ - .font: textStyle.uiFont, - .kern: textStyle.kerning - ] - ) + + extension NSAttributedString { + /// Initializes an attributed string with the specified properties. + /// - Parameters: + /// - string: The string for the attributed string. + /// - textStyle: The text style for the attributed string. + /// - additionalAttributes: The additional attributes for the attributed string. Default is `nil`. + public convenience init( + string: String, + textStyle: TextStyle, + additionalAttributes _: [NSAttributedString.Key: Any]? = nil + ) { + self.init( + string: string, + attributes: [ + .font: textStyle.uiFont, + .kern: textStyle.kerning + ] + ) + } } -} #endif diff --git a/Tests/FuturedKitTests/DataCacheTests.swift b/Tests/FuturedKitTests/DataCacheTests.swift index 7b06a58..82ee5f0 100644 --- a/Tests/FuturedKitTests/DataCacheTests.swift +++ b/Tests/FuturedKitTests/DataCacheTests.swift @@ -5,7 +5,6 @@ import Testing @Suite("DataCache") @MainActor struct DataCacheTests { - private struct Item: Identifiable, Equatable, Sendable { let id: Int var name: String @@ -169,8 +168,8 @@ struct DataCacheTests { var didChange = false } - private func assertNoChange( - on cache: DataCache, + private func assertNoChange( + on cache: DataCache, during action: () -> Void ) { let detector = ChangeDetector()