From 8ae3a9989f764bfb3bf679d048c2314ebadd4fe1 Mon Sep 17 00:00:00 2001 From: Valeriy Malishevskyi Date: Wed, 7 May 2025 18:48:20 +0300 Subject: [PATCH 1/6] major updates and rework --- Package.swift | 30 ++-- Sources/SwiftUIExtensions/Binding+??.swift | 25 ++++ .../SwiftUIExtensions/Binding+Boolean.swift | 38 +++++ .../SwiftUIExtensions/Binding+Equals.swift | 56 ++++++++ Sources/SwiftUIExtensions/Binding.swift | 44 ++++++ .../Color+AccessibleFontColor.swift | 0 .../Color+Random.swift | 2 +- .../DynamicTypeSize+ContentSizeCategory.swift | 54 +++++++ .../SwiftUIExtensions/EdgeInsets+Init.swift | 28 ++++ .../Environment+IsPreview.swift | 49 +++++++ .../UIFont.TextStyle+Font.TextStyle.swift | 39 +++++ .../Font/UIFont.Weight+Font.Weight.swift | 43 ++++++ ...tDescriptor.SystemDesign+Font.Design.swift | 25 ++++ .../SwiftUIExtensions/Image+Resizable.swift | 29 ++++ .../UIApplication.swift | 0 .../UIColor+ColorScheme.swift | 40 ++++++ .../View+EmbedInNavigation.swift | 17 +++ Sources/SwiftUIExtensions/View+Hidden.swift | 70 +++++++++ .../View+HitTestAreaPadding.swift | 45 ++++++ .../SwiftUIHelpers/EnvironmentOrValue.swift | 90 ++++++++++++ Sources/SwiftUIHelpers/ExportedSwiftUI.swift | 14 -- .../SwiftUIHelpers/Extensions/Binding.swift | 102 -------------- Sources/SwiftUIHelpers/Extensions/Color.swift | 75 ---------- .../SwiftUIHelpers/Extensions/Hidden.swift | 70 --------- .../Extensions/NavigationLink.swift | 26 ---- .../Extensions/PreviewDevice.swift | 67 --------- .../SwiftUIHelpers/Extensions/UIColor.swift | 85 ----------- .../Extensions/UIImage+QRCode.swift | 78 ---------- .../Extensions/View+Embed.swift | 14 -- .../FontModifier/FontModifier.swift | 12 ++ .../StaticFontModifier/BoldModifier.swift | 16 +++ .../StaticFontModifier/ItalicModifier.swift | 16 +++ .../StaticFontModifier.swift | 10 ++ .../FontModifier/WeightModifier.swift | 16 +++ .../FontProvider/FontProvider.swift | 18 +++ .../Providers/ModifierProvider.swift | 21 +++ .../Providers/NamedProvider.swift | 32 +++++ .../Providers/StaticModifierProvider.swift | 20 +++ .../Providers/SystemProvider.swift | 26 ++++ .../Providers/TextStyleProvider.swift | 28 ++++ .../FontProvider/UIFont+Font.swift | 101 +++++++++++++ .../GeometryEffect/GeometryEffectView.swift | 128 +++++++++++++++++ .../GeometryEffect/View+GeometryEffect.swift | 19 +++ .../Helpers/RoundedCorner.swift | 56 -------- .../SwiftUIHelpers/Helpers/SizeReader.swift | 57 -------- .../InternalAPI/DetachedPlaceholder.swift | 21 +++ .../InternalAPI/DetachedView.swift | 24 ++++ ...EnvironmentValues+LineHeightMultiple.swift | 16 +++ .../InternalAPI/PreferenceKey+Delay.swift | 11 ++ .../InternalAPI/PreferenceValue+Force.swift | 11 ++ .../Text+StylisticAlternative.swift | 11 ++ .../InternalAPI/View+AddingBackground+.swift | 27 ++++ .../InternalAPI/View+AutomaticPadding.swift | 31 ++++ .../InternalAPI/View+ColorMonochrome.swift | 15 ++ .../InternalAPI/View+Identified.swift | 11 ++ .../View+IgnoresAutomaticPadding.swift | 11 ++ .../InternalAPI/View+SafeAreaInsets.swift | 21 +++ .../InternalAPI/View+Scrollable.swift | 19 +++ .../InternalAPI/View+TightPadding.swift | 11 ++ .../NavigationLinkStyle.swift | 16 +++ .../Placeholders/String+Lorem.swift | 133 ------------------ .../Placeholders/URL+PlaceholderImage.swift | 26 ---- Sources/SwiftUIHelpers/Previewable.swift | 31 ++++ .../SceneExtensions/Scene+Modifier.swift | 14 ++ .../SceneExtensions/Scene+OnChange.swift | 65 +++++++++ .../SceneExtensions/Scene+OnReceive.swift | 53 +++++++ .../ScrollView/SelectableArray.swift | 87 ++++++++++++ .../SelectableArrayPageMetadata.swift | 36 +++++ .../ScrollView/View+DefaultScrollAnchor.swift | 34 +++++ .../View+ScrollPositionSelectable.swift | 47 +++++++ .../ShapeStyleBuilder/ShapeStyleBuilder.swift | 26 ++++ Sources/SwiftUIHelpers/StateOrBinding.swift | 52 +++++++ .../SwiftUIHelpers/StatefulContainer.swift | 27 ++++ .../SwiftUIHelpers/UIColor+HexContainer.swift | 15 ++ .../View+DynamicTypeSizeHidden.swift | 27 ++++ .../View+OnTouchDownGesture.swift | 36 +++++ Sources/SwiftUIHelpers/View+Position.swift | 43 ++++++ .../View+SoftwareKeyboard.swift | 37 +++++ .../SwiftUIExtensionsTests/BindingTests.swift | 65 +++++++++ .../ColorAccessibleFontColorTests.swift | 21 +++ 80 files changed, 2145 insertions(+), 817 deletions(-) create mode 100644 Sources/SwiftUIExtensions/Binding+??.swift create mode 100644 Sources/SwiftUIExtensions/Binding+Boolean.swift create mode 100644 Sources/SwiftUIExtensions/Binding+Equals.swift create mode 100644 Sources/SwiftUIExtensions/Binding.swift rename Sources/{SwiftUIHelpers/Extensions => SwiftUIExtensions}/Color+AccessibleFontColor.swift (100%) rename Sources/{SwiftUIHelpers/Extensions => SwiftUIExtensions}/Color+Random.swift (90%) create mode 100644 Sources/SwiftUIExtensions/DynamicTypeSize+ContentSizeCategory.swift create mode 100644 Sources/SwiftUIExtensions/EdgeInsets+Init.swift create mode 100644 Sources/SwiftUIExtensions/Environment+IsPreview.swift create mode 100644 Sources/SwiftUIExtensions/Font/UIFont.TextStyle+Font.TextStyle.swift create mode 100644 Sources/SwiftUIExtensions/Font/UIFont.Weight+Font.Weight.swift create mode 100644 Sources/SwiftUIExtensions/Font/UIFontDescriptor.SystemDesign+Font.Design.swift create mode 100644 Sources/SwiftUIExtensions/Image+Resizable.swift rename Sources/{SwiftUIHelpers/Extensions => SwiftUIExtensions}/UIApplication.swift (100%) create mode 100644 Sources/SwiftUIExtensions/UIColor+ColorScheme.swift create mode 100644 Sources/SwiftUIExtensions/View+EmbedInNavigation.swift create mode 100644 Sources/SwiftUIExtensions/View+Hidden.swift create mode 100644 Sources/SwiftUIExtensions/View+HitTestAreaPadding.swift create mode 100644 Sources/SwiftUIHelpers/EnvironmentOrValue.swift delete mode 100644 Sources/SwiftUIHelpers/ExportedSwiftUI.swift delete mode 100644 Sources/SwiftUIHelpers/Extensions/Binding.swift delete mode 100644 Sources/SwiftUIHelpers/Extensions/Color.swift delete mode 100644 Sources/SwiftUIHelpers/Extensions/Hidden.swift delete mode 100644 Sources/SwiftUIHelpers/Extensions/NavigationLink.swift delete mode 100644 Sources/SwiftUIHelpers/Extensions/PreviewDevice.swift delete mode 100644 Sources/SwiftUIHelpers/Extensions/UIColor.swift delete mode 100644 Sources/SwiftUIHelpers/Extensions/UIImage+QRCode.swift delete mode 100644 Sources/SwiftUIHelpers/Extensions/View+Embed.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/FontModifier/FontModifier.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/BoldModifier.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/ItalicModifier.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/StaticFontModifier.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/FontModifier/WeightModifier.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/FontProvider.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/Providers/ModifierProvider.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/Providers/NamedProvider.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/Providers/StaticModifierProvider.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/Providers/SystemProvider.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/Providers/TextStyleProvider.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/UIFont+Font.swift create mode 100644 Sources/SwiftUIHelpers/GeometryEffect/GeometryEffectView.swift create mode 100644 Sources/SwiftUIHelpers/GeometryEffect/View+GeometryEffect.swift delete mode 100644 Sources/SwiftUIHelpers/Helpers/RoundedCorner.swift delete mode 100644 Sources/SwiftUIHelpers/Helpers/SizeReader.swift create mode 100644 Sources/SwiftUIHelpers/InternalAPI/DetachedPlaceholder.swift create mode 100644 Sources/SwiftUIHelpers/InternalAPI/DetachedView.swift create mode 100644 Sources/SwiftUIHelpers/InternalAPI/EnvironmentValues+LineHeightMultiple.swift create mode 100644 Sources/SwiftUIHelpers/InternalAPI/PreferenceKey+Delay.swift create mode 100644 Sources/SwiftUIHelpers/InternalAPI/PreferenceValue+Force.swift create mode 100644 Sources/SwiftUIHelpers/InternalAPI/Text+StylisticAlternative.swift create mode 100644 Sources/SwiftUIHelpers/InternalAPI/View+AddingBackground+.swift create mode 100644 Sources/SwiftUIHelpers/InternalAPI/View+AutomaticPadding.swift create mode 100644 Sources/SwiftUIHelpers/InternalAPI/View+ColorMonochrome.swift create mode 100644 Sources/SwiftUIHelpers/InternalAPI/View+Identified.swift create mode 100644 Sources/SwiftUIHelpers/InternalAPI/View+IgnoresAutomaticPadding.swift create mode 100644 Sources/SwiftUIHelpers/InternalAPI/View+SafeAreaInsets.swift create mode 100644 Sources/SwiftUIHelpers/InternalAPI/View+Scrollable.swift create mode 100644 Sources/SwiftUIHelpers/InternalAPI/View+TightPadding.swift create mode 100644 Sources/SwiftUIHelpers/NavigationLinkStyle/NavigationLinkStyle.swift delete mode 100644 Sources/SwiftUIHelpers/Placeholders/String+Lorem.swift delete mode 100644 Sources/SwiftUIHelpers/Placeholders/URL+PlaceholderImage.swift create mode 100644 Sources/SwiftUIHelpers/Previewable.swift create mode 100644 Sources/SwiftUIHelpers/SceneExtensions/Scene+Modifier.swift create mode 100644 Sources/SwiftUIHelpers/SceneExtensions/Scene+OnChange.swift create mode 100644 Sources/SwiftUIHelpers/SceneExtensions/Scene+OnReceive.swift create mode 100644 Sources/SwiftUIHelpers/ScrollView/SelectableArray.swift create mode 100644 Sources/SwiftUIHelpers/ScrollView/SelectableArrayPageMetadata.swift create mode 100644 Sources/SwiftUIHelpers/ScrollView/View+DefaultScrollAnchor.swift create mode 100644 Sources/SwiftUIHelpers/ScrollView/View+ScrollPositionSelectable.swift create mode 100644 Sources/SwiftUIHelpers/ShapeStyleBuilder/ShapeStyleBuilder.swift create mode 100644 Sources/SwiftUIHelpers/StateOrBinding.swift create mode 100644 Sources/SwiftUIHelpers/StatefulContainer.swift create mode 100644 Sources/SwiftUIHelpers/UIColor+HexContainer.swift create mode 100644 Sources/SwiftUIHelpers/View+DynamicTypeSizeHidden.swift create mode 100644 Sources/SwiftUIHelpers/View+OnTouchDownGesture.swift create mode 100644 Sources/SwiftUIHelpers/View+Position.swift create mode 100644 Sources/SwiftUIHelpers/View+SoftwareKeyboard.swift create mode 100644 Tests/SwiftUIExtensionsTests/BindingTests.swift create mode 100644 Tests/SwiftUIExtensionsTests/ColorAccessibleFontColorTests.swift diff --git a/Package.swift b/Package.swift index 146a768..2f99792 100644 --- a/Package.swift +++ b/Package.swift @@ -1,29 +1,33 @@ -// swift-tools-version: 5.8 +// swift-tools-version: 6.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "SwiftUIHelpers", - platforms: [.iOS(.v14)], + platforms: [.iOS(.v15), .macOS(.v13), .tvOS(.v15), .watchOS(.v8)], products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. - .library( - name: "SwiftUIHelpers", - targets: ["SwiftUIHelpers"]), + // Products define the executables and libraries a package produces, making them visible to other packages. + .library(name: "SwiftUIHelpers", targets: ["SwiftUIHelpers"]), ], dependencies: [ - // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. .target( name: "SwiftUIHelpers", - dependencies: []), + dependencies: [ + "SwiftUIExtensions", + ] + ), + .target( + name: "SwiftUIExtensions", + dependencies: [] + ), .testTarget( - name: "SwiftUIHelpersTests", - dependencies: ["SwiftUIHelpers"]), + name: "SwiftUIExtensionsTests", + dependencies: ["SwiftUIExtensions"], + ), ] ) diff --git a/Sources/SwiftUIExtensions/Binding+??.swift b/Sources/SwiftUIExtensions/Binding+??.swift new file mode 100644 index 0000000..9148357 --- /dev/null +++ b/Sources/SwiftUIExtensions/Binding+??.swift @@ -0,0 +1,25 @@ +// +// Binding+??.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 14.07.2024. +// + +import SwiftUI + +public extension Binding where Value: Sendable { + /// Create a non-optional version of an optional `Binding` with a default value + /// - Parameters: + /// - lhs: The original `Binding` (binding to an optional value) + /// - rhs: The default value if the original `wrappedValue` is `nil` + /// - Returns: The `Binding` (where `Value` is non-optional) + static func ??( + lhs: Binding>, + rhs: @autoclosure @Sendable @escaping () -> Value + ) -> Binding { + Binding( + get: { lhs.wrappedValue ?? rhs() }, + set: { lhs.wrappedValue = $0 } + ) + } +} diff --git a/Sources/SwiftUIExtensions/Binding+Boolean.swift b/Sources/SwiftUIExtensions/Binding+Boolean.swift new file mode 100644 index 0000000..8914689 --- /dev/null +++ b/Sources/SwiftUIExtensions/Binding+Boolean.swift @@ -0,0 +1,38 @@ +// +// Binding+Boolean.swift +// +// Created by Valeriy Malishevskyi on 21.08.2023. +// + +#if canImport(FoundationExtensions) + +import SwiftUI +import FoundationExtensions + +public extension Binding where Value : OptionalProtocol { + /// A boolean binding representation of the current binding. + /// + /// This computed property provides a boolean binding that's `true` when the wrapped value is not `nil` and `false` otherwise. It's useful when you want to create conditions or UI elements based on the presence or absence of a value. + /// + /// When setting the binding to `false`, it will set the original binding to `nil`. Setting it to `true` will not modify the original binding. + /// + /// Example: + /// ```swift + /// @State var text: String? = "Hello" + /// + /// var body: some View { + /// ToggleClearView("Is not clean?", contentPresented: $text.boolean) + /// } + /// ``` + /// + /// - Note: Setting the boolean binding to `true` will not modify the original binding. You should handle such cases separately. + var boolean: Binding { + Binding { + !wrappedValue.isNone + } set: { newValue in + if newValue == false { self.wrappedValue = nil } + } + } +} + +#endif \ No newline at end of file diff --git a/Sources/SwiftUIExtensions/Binding+Equals.swift b/Sources/SwiftUIExtensions/Binding+Equals.swift new file mode 100644 index 0000000..da1c99e --- /dev/null +++ b/Sources/SwiftUIExtensions/Binding+Equals.swift @@ -0,0 +1,56 @@ +// +// Binding+Equals.swift +// +// Created by Valeriy Malishevskyi on 21.08.2023. +// + +#if canImport(FoundationExtensions) + +import SwiftUI +import FoundationExtensions + +public extension Binding { + /// Initializes a new boolean binding based on the equality of a hashable value. + /// + /// - Note: When setting the binding to `false`, it will set the original binding to `nil` + /// if the current value matches the specified value; otherwise, it will do nothing. + /// + /// This initializer allows you to create a boolean binding that's `true` when the provided hashable value matches + /// the specified value and `false` otherwise. + /// It's especially useful when working with enums or other hashable types + /// where you want to create conditions based on the current value. + /// + /// Example: + /// ```swift + /// enum SelectedArea { + /// case top + /// case bottom + /// } + /// @State var selected: SelectedArea? = SelectedArea.top + /// + /// var body: some View { + /// Text("Hello, World!") + /// .sheet(isPresented: Binding($selected, equals: .bottom), content: { + /// Text("Sheet Content") + /// }) + /// } + /// ``` + /// + /// - Parameters: + /// - binding: A binding to a hashable value. + /// - value: The value to compare against the hashable value. + init(_ binding: Binding, equals value: ScopeValue) + where ScopeValue : Hashable, ScopeValue : OptionalProtocol, Value == Bool { + self.init { + return binding.wrappedValue == value + } set: { newValue in + if binding.wrappedValue == value, !newValue { + binding.wrappedValue = nil + } else if newValue { + binding.wrappedValue = value + } + } + } +} + +#endif diff --git a/Sources/SwiftUIExtensions/Binding.swift b/Sources/SwiftUIExtensions/Binding.swift new file mode 100644 index 0000000..25089be --- /dev/null +++ b/Sources/SwiftUIExtensions/Binding.swift @@ -0,0 +1,44 @@ +// +// Created by Valeriy Malishevskyi on 19.03.2024. +// + +import SwiftUI + +extension Binding { + /// Creates a new `Binding` to an `Element` of a collection that can be identified by a specific key. + /// + /// This initializer creates a binding to a specific element within a collection. The element is identified using + /// a `KeyPath` of the element to its identifier. This is useful in scenarios where you need a `Binding` to + /// an optional element of a collection, allowing you to work with the selection state in a SwiftUI view. + /// + /// - Parameters: + /// - target: A `Binding` to an optional `Element`, typically representing the currently selected item. + /// - key: A `KeyPath` from the `Element` type to its identifier (`ID`), used to uniquely identify elements in the collection. + /// - collection: A collection of `Element` objects, from which the target element is selected. + /// + /// - Precondition: + /// - `Element` must conform to `Identifiable` protocol, and `ID` must match `Element`'s associated identifier type. + /// - The `Value` type of this binding extension must be compatible with `ID?`, representing an optional identifier. + /// - `C` must be a `Collection` where its elements are of type `Element`. + /// + /// This initializer enhances the `Binding` type with the ability to directly bind to an identifiable element within + /// a collection, simplifying state management in SwiftUI views, especially when dealing with selections in lists + /// or grids. The binding will automatically update when the selected `ID` changes, ensuring the UI stays in sync + /// with the underlying data model. + @MainActor public init( + _ target: Binding, + key: KeyPath, + in collection: C + ) where Element: Identifiable, ID == Element.ID, Value == ID?, C: Collection, C.Element == Element{ + self.init( + get: { + target.wrappedValue?[keyPath: key] + }, + set: { id in + if let newId = id, target.wrappedValue?[keyPath: key] != newId { + target.wrappedValue = collection.first { $0[keyPath: key] == newId } + } + } + ) + } +} diff --git a/Sources/SwiftUIHelpers/Extensions/Color+AccessibleFontColor.swift b/Sources/SwiftUIExtensions/Color+AccessibleFontColor.swift similarity index 100% rename from Sources/SwiftUIHelpers/Extensions/Color+AccessibleFontColor.swift rename to Sources/SwiftUIExtensions/Color+AccessibleFontColor.swift diff --git a/Sources/SwiftUIHelpers/Extensions/Color+Random.swift b/Sources/SwiftUIExtensions/Color+Random.swift similarity index 90% rename from Sources/SwiftUIHelpers/Extensions/Color+Random.swift rename to Sources/SwiftUIExtensions/Color+Random.swift index d01dfd3..9339b04 100644 --- a/Sources/SwiftUIHelpers/Extensions/Color+Random.swift +++ b/Sources/SwiftUIExtensions/Color+Random.swift @@ -11,7 +11,7 @@ extension Color { /// Returns a random color with RGB values between 0 and 1. /// /// - Returns: A random color. - static var random: Color { + public static var random: Color { return Color( red: .random(in: 0...1), green: .random(in: 0...1), diff --git a/Sources/SwiftUIExtensions/DynamicTypeSize+ContentSizeCategory.swift b/Sources/SwiftUIExtensions/DynamicTypeSize+ContentSizeCategory.swift new file mode 100644 index 0000000..c9c6e1b --- /dev/null +++ b/Sources/SwiftUIExtensions/DynamicTypeSize+ContentSizeCategory.swift @@ -0,0 +1,54 @@ +// +// DynamicTypeSize+ContentSizeCategory.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import SwiftUI + +extension ContentSizeCategory { + + /// Initializes a `ContentSizeCategory` from a corresponding `DynamicTypeSize` value. + /// + /// This initializer provides a mapping between SwiftUI's `DynamicTypeSize` and UIKit's + /// `UIContentSizeCategory`, allowing interoperability between the two APIs. + /// It covers all standard and accessibility sizes, and defaults to `.medium` for any unknown cases. + /// + /// ```swift + /// let contentSize = ContentSizeCategory(.large) + /// print(contentSize) // .large + /// ``` + /// + /// - Parameter dynamicTypeSize: The `DynamicTypeSize` to convert. + public init(_ dynamicTypeSize: DynamicTypeSize) { + switch dynamicTypeSize { + case .xSmall: + self = .extraSmall + case .small: + self = .small + case .medium: + self = .medium + case .large: + self = .large + case .xLarge: + self = .extraLarge + case .xxLarge: + self = .extraExtraLarge + case .xxxLarge: + self = .extraExtraExtraLarge + case .accessibility1: + self = .accessibilityMedium + case .accessibility2: + self = .accessibilityLarge + case .accessibility3: + self = .accessibilityExtraLarge + case .accessibility4: + self = .accessibilityExtraExtraLarge + case .accessibility5: + self = .accessibilityExtraExtraExtraLarge + default: + self = .medium + } + } +} diff --git a/Sources/SwiftUIExtensions/EdgeInsets+Init.swift b/Sources/SwiftUIExtensions/EdgeInsets+Init.swift new file mode 100644 index 0000000..1aafd08 --- /dev/null +++ b/Sources/SwiftUIExtensions/EdgeInsets+Init.swift @@ -0,0 +1,28 @@ +// +// EdgeInsets+Init.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 01.08.2024. +// + +import SwiftUI + +extension EdgeInsets { + + /// Initializes `EdgeInsets` with equal horizontal and vertical padding. + /// + /// This initializer simplifies creating symmetrical insets by applying the same horizontal + /// value to both `leading` and `trailing`, and the same vertical value to both `top` and `bottom`. + /// + /// ```swift + /// let insets = EdgeInsets(horizontal: 16, vertical: 8) + /// // Equivalent to: EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16) + /// ``` + /// + /// - Parameters: + /// - horizontal: The inset value for both `leading` and `trailing`. Defaults to `0`. + /// - vertical: The inset value for both `top` and `bottom`. Defaults to `0`. + public init(horizontal: CGFloat = 0, vertical: CGFloat = 0) { + self.init(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) + } +} diff --git a/Sources/SwiftUIExtensions/Environment+IsPreview.swift b/Sources/SwiftUIExtensions/Environment+IsPreview.swift new file mode 100644 index 0000000..4e5870c --- /dev/null +++ b/Sources/SwiftUIExtensions/Environment+IsPreview.swift @@ -0,0 +1,49 @@ +// +// Environment+IsPreview.swift +// +// +// Created by Valeriy Malishevskyi on 17.08.2023. +// + +import SwiftUI +import FoundationExtensions + +// MARK: - Environment Key for SwiftUI Preview Detection + +public extension EnvironmentValues { + /// Indicates if the current view is being rendered in a SwiftUI preview. + var isPreview: Bool { + get { self[IsPreviewEnvironmentKey.self] } + } +} + +private struct IsPreviewEnvironmentKey: EnvironmentKey { + /// The default value is determined by `ProcessInfo.isPreview`. + static let defaultValue: Bool = ProcessInfo.isPreview +} + +// MARK: - View Extensions for Preview Detection + +public extension View { + /// Indicates if the view is being rendered in a SwiftUI preview. + static var isPreview: Bool { + ProcessInfo.isPreview + } +} + +// MARK: - Sample View for Testing Preview Detection + +/// A view that displays whether it is currently in a SwiftUI preview or not. +struct Environment_IsPreview: View { + var body: some View { + VStack { + Text("Is preview?") + .font(.title) + Text("\(Self.isPreview ? "Yes" : "No")") + } + } +} + +#Preview { + Environment_IsPreview() +} diff --git a/Sources/SwiftUIExtensions/Font/UIFont.TextStyle+Font.TextStyle.swift b/Sources/SwiftUIExtensions/Font/UIFont.TextStyle+Font.TextStyle.swift new file mode 100644 index 0000000..2052668 --- /dev/null +++ b/Sources/SwiftUIExtensions/Font/UIFont.TextStyle+Font.TextStyle.swift @@ -0,0 +1,39 @@ +// +// UIFont.TextStyle+Font.TextStyle.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import SwiftUI + +extension UIFont.TextStyle { + public init(_ style: Font.TextStyle) { + switch style { + case .largeTitle: + self = .largeTitle + case .title: + self = .title1 + case .title2: + self = .title2 + case .title3: + self = .title3 + case .headline: + self = .headline + case .subheadline: + self = .subheadline + case .body: + self = .body + case .callout: + self = .callout + case .footnote: + self = .footnote + case .caption: + self = .caption1 + case .caption2: + self = .caption2 + default: + self = .body + } + } +} diff --git a/Sources/SwiftUIExtensions/Font/UIFont.Weight+Font.Weight.swift b/Sources/SwiftUIExtensions/Font/UIFont.Weight+Font.Weight.swift new file mode 100644 index 0000000..87bd797 --- /dev/null +++ b/Sources/SwiftUIExtensions/Font/UIFont.Weight+Font.Weight.swift @@ -0,0 +1,43 @@ +// +// UIFont.Weight+Font.Weight.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import SwiftUI + +extension UIFont.Weight { + public init(_ weight: Font.Weight) { + switch weight { + case .ultraLight: + self = .ultraLight + case .thin: + self = .thin + case .light: + self = .light + case .regular: + self = .regular + case .medium: + self = .medium + case .semibold: + self = .semibold + case .bold: + self = .bold + case .heavy: + self = .heavy + case .black: + self = .black + default: + self = weight.rawValue.map(UIFont.Weight.init(rawValue:)) ?? .regular + } + } +} + +extension Font.Weight { + var rawValue: CGFloat? { + let mirror = Mirror(reflecting: self) + let value = mirror.children.first?.value as? CGFloat + return value + } +} diff --git a/Sources/SwiftUIExtensions/Font/UIFontDescriptor.SystemDesign+Font.Design.swift b/Sources/SwiftUIExtensions/Font/UIFontDescriptor.SystemDesign+Font.Design.swift new file mode 100644 index 0000000..e7d2860 --- /dev/null +++ b/Sources/SwiftUIExtensions/Font/UIFontDescriptor.SystemDesign+Font.Design.swift @@ -0,0 +1,25 @@ +// +// UIFontDescriptor.SystemDesign+Font.Design.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import SwiftUI + +extension UIFontDescriptor.SystemDesign { + public init(_ design: Font.Design) { + switch design { + case .default: + self = .default + case .serif: + self = .serif + case .rounded: + self = .rounded + case .monospaced: + self = .monospaced + default: + self = .default + } + } +} diff --git a/Sources/SwiftUIExtensions/Image+Resizable.swift b/Sources/SwiftUIExtensions/Image+Resizable.swift new file mode 100644 index 0000000..c371388 --- /dev/null +++ b/Sources/SwiftUIExtensions/Image+Resizable.swift @@ -0,0 +1,29 @@ +// +// Image+Resizable.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 11.07.2024. +// + +import SwiftUI + +extension Image { + + /// Returns a resizable image with the specified content mode. + /// + /// This method applies `.resizable()` to the image and then adjusts its aspect ratio + /// according to the given `ContentMode` (`.fit` or `.fill`). + /// + /// ```swift + /// Image("example") + /// .resizable(contentMode: .fit) + /// .frame(width: 100, height: 100) + /// ``` + /// + /// - Parameter contentMode: The desired scaling behavior for the image's aspect ratio. + /// - Returns: A view that displays the image with the specified content mode and is resizable. + public func resizable(contentMode: ContentMode) -> some View { + self.resizable() + .aspectRatio(contentMode: contentMode) + } +} diff --git a/Sources/SwiftUIHelpers/Extensions/UIApplication.swift b/Sources/SwiftUIExtensions/UIApplication.swift similarity index 100% rename from Sources/SwiftUIHelpers/Extensions/UIApplication.swift rename to Sources/SwiftUIExtensions/UIApplication.swift diff --git a/Sources/SwiftUIExtensions/UIColor+ColorScheme.swift b/Sources/SwiftUIExtensions/UIColor+ColorScheme.swift new file mode 100644 index 0000000..f5ce0b9 --- /dev/null +++ b/Sources/SwiftUIExtensions/UIColor+ColorScheme.swift @@ -0,0 +1,40 @@ +// +// UIColor+ColorScheme.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 05.08.2024. +// + +import SwiftUI + +extension UIColor { + + /// Initializes a dynamic color that adapts to the current user interface style (light or dark mode). + /// + /// This initializer allows you to provide separate color values for light and dark appearances. + /// The system automatically chooses the appropriate color at runtime based on the current + /// `UITraitCollection`. + /// + /// ```swift + /// let backgroundColor = UIColor( + /// light: .white, + /// dark: .black + /// ) + /// ``` + /// + /// - Parameters: + /// - light: The color to use in light mode or when the interface style is unspecified. + /// - dark: The color to use in dark mode. + public convenience init(light: UIColor, dark: UIColor) { + self.init { traitCollection in + switch traitCollection.userInterfaceStyle { + case .light, .unspecified: + return light + case .dark: + return dark + @unknown default: + return light + } + } + } +} diff --git a/Sources/SwiftUIExtensions/View+EmbedInNavigation.swift b/Sources/SwiftUIExtensions/View+EmbedInNavigation.swift new file mode 100644 index 0000000..09faaac --- /dev/null +++ b/Sources/SwiftUIExtensions/View+EmbedInNavigation.swift @@ -0,0 +1,17 @@ +// +// Created by Valeriy Malishevskyi on 06.05.2023. +// + +import SwiftUI + +public extension View { + + /// Embeds the view in a `NavigationView`. + /// + /// - Returns: A view embedded in a `NavigationView`. + @available(iOS 16.0, *) + @ViewBuilder func embedInNavigation(titleDisplayMode: NavigationBarItem.TitleDisplayMode = .automatic) -> some View { + NavigationStack { self.navigationBarTitleDisplayMode(titleDisplayMode) } + } +} + diff --git a/Sources/SwiftUIExtensions/View+Hidden.swift b/Sources/SwiftUIExtensions/View+Hidden.swift new file mode 100644 index 0000000..7f43a44 --- /dev/null +++ b/Sources/SwiftUIExtensions/View+Hidden.swift @@ -0,0 +1,70 @@ +// +// View+Hidden.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 18.08.2024. +// + +import SwiftUI + +extension View { + + /// Hide or show the view based on a boolean value. + /// + /// Example for visibility: + /// + /// Text("Label") + /// .hidden(true) + /// + /// Example for complete removal: + /// + /// Text("Label") + /// .hidden(true, remove: true) + /// + /// - Parameters: + /// - hidden: Set to `false` to show the view. Set to `true` to hide the view. + /// - remove: Boolean value indicating whether or not to remove the view. + @inlinable + public func hidden( + _ isHidden: Bool, + transition: AnyTransition = .identity, + remove: Bool = false + ) -> some View { + modifier( + HiddenModifier( + isHidden: isHidden, + transition: transition, + remove: remove + ) + ) + } +} + +public struct HiddenModifier: ViewModifier { + + public var isHidden: Bool + public var remove: Bool + public var transition: AnyTransition + + @inlinable + public init( + isHidden: Bool, + transition: AnyTransition = .opacity, + remove: Bool = false + ) { + self.isHidden = isHidden + self.transition = transition + self.remove = remove + } + + public func body(content: Content) -> some View { + if isHidden { + if !remove { + content.hidden() + } + } else { + content + .transition(transition) + } + } +} diff --git a/Sources/SwiftUIExtensions/View+HitTestAreaPadding.swift b/Sources/SwiftUIExtensions/View+HitTestAreaPadding.swift new file mode 100644 index 0000000..373cf02 --- /dev/null +++ b/Sources/SwiftUIExtensions/View+HitTestAreaPadding.swift @@ -0,0 +1,45 @@ +// +// View+HitTestAreaPadding.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 01.08.2024. +// + +import SwiftUI + +public enum PaddingAreaScope { + case hitTestArea +} + +extension View { + public func padding(_ insets: EdgeInsets, for scope: PaddingAreaScope) -> some View { + modifier(HitTestAreaPaddingModifier(insets: insets)) + } + + public func padding(_ edges: Edge.Set = .all, _ length: CGFloat, for scope: PaddingAreaScope) -> some View { + let edgeInsets = EdgeInsets( + top: edges.contains(.top) ? length : 0, + leading: edges.contains(.leading) ? length : 0, + bottom: edges.contains(.bottom) ? length : 0, + trailing: edges.contains(.trailing) ? length : 0 + ) + + return padding(edgeInsets, for: .hitTestArea) + } + + public func padding(_ length: CGFloat, for scope: PaddingAreaScope) -> some View { + padding(.all, length, for: .hitTestArea) + } +} + +private struct HitTestAreaPaddingModifier: ViewModifier { + let insets: EdgeInsets + + func body(content: Content) -> some View { + content + .padding(insets) + .contentShape(.rect) + .padding(-insets) + } +} + diff --git a/Sources/SwiftUIHelpers/EnvironmentOrValue.swift b/Sources/SwiftUIHelpers/EnvironmentOrValue.swift new file mode 100644 index 0000000..3362b90 --- /dev/null +++ b/Sources/SwiftUIHelpers/EnvironmentOrValue.swift @@ -0,0 +1,90 @@ +// +// EnvironmentOrValue.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 13.09.2024. +// + +import SwiftUI + +/// A property wrapper that provides a value from the environment if available, or falls back to a manually-supplied value. +/// +/// `EnvironmentOrValue` allows you to write components that use a value from the SwiftUI `Environment` +/// when injected, or fall back to a specified default value otherwise. You can both read and write the value; +/// writing will override the environment-derived value. +/// +/// This is useful for making views configurable while still respecting global settings. +/// +/// ```swift +/// struct ExampleView: View { +/// @EnvironmentOrValue(\.colorScheme) var colorScheme +/// +/// var body: some View { +/// Text("Current scheme: \(colorScheme == .dark ? "Dark" : "Light")") +/// } +/// } +/// ``` +/// +/// You can also pass an explicit value: +/// +/// ```swift +/// ExampleView(colorScheme: .dark) +/// ``` +@frozen +@propertyWrapper +public struct EnvironmentOrValue: DynamicProperty { + + @usableFromInline + enum Storage: DynamicProperty { + /// Uses a value from the SwiftUI environment. + case environment(Environment) + /// Uses a manually provided value. + case value(Value) + } + + @usableFromInline + var storage: Storage + + /// Initializes the wrapper with a direct value. + /// + /// - Parameter value: A constant value to use in place of one from the environment. + @inlinable + public init(_ value: Value) { + self.storage = .value(value) + } + + /// Initializes the wrapper with a key path into the SwiftUI `EnvironmentValues`. + /// + /// - Parameter keyPath: A key path referencing a specific environment value. + @inlinable + public init(_ keyPath: KeyPath) { + self.storage = .environment(.init(keyPath)) + } + + /// The current value, either from the environment or the assigned value. + /// + /// Writing to this property overrides the current value, switching it from environment-backed to value-backed. + public var wrappedValue: Value { + get { + switch storage { + case .environment(let environment): + return environment.wrappedValue + case .value(let value): + return value + } + } + set { + storage = .value(newValue) + } + } + + /// Indicates whether the current value was manually set, rather than derived from the environment. + public var isValue: Bool { + switch storage { + case .environment: + return false + case .value: + return true + } + } +} diff --git a/Sources/SwiftUIHelpers/ExportedSwiftUI.swift b/Sources/SwiftUIHelpers/ExportedSwiftUI.swift deleted file mode 100644 index 9b82f65..0000000 --- a/Sources/SwiftUIHelpers/ExportedSwiftUI.swift +++ /dev/null @@ -1,14 +0,0 @@ -// Why do I need to re-export the SwiftUI module? -// -// Some code in this package relies on certain SwiftUI structures or types. -// In order to use these types in your code without having to explicitly import the original module, -// I've defined a module re-export. -// -// Re-exporting a module essentially makes all of its public types and functions available in the module -// that imports it, and at the same time in the project that imports the module. -// -// By re-exporting the SwiftUI module here, you don't have to import SwiftUI -// when you imported this package. - - -@_exported import SwiftUI diff --git a/Sources/SwiftUIHelpers/Extensions/Binding.swift b/Sources/SwiftUIHelpers/Extensions/Binding.swift deleted file mode 100644 index 534c852..0000000 --- a/Sources/SwiftUIHelpers/Extensions/Binding.swift +++ /dev/null @@ -1,102 +0,0 @@ -// MIT License -// -// Copyright (c) 2023 Maksym Kuznietsov -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// Created by Valeriy Malishevskyi on 06.05.2023. -// - -import SwiftUI - -public extension Binding where Value == Bool { - /// Returns a closure that toggles the wrapped value when executed. - /// - /// - Returns: A closure that toggles the wrapped value. - func toggleAction() -> () -> Void { - return { - self.wrappedValue.toggle() - } - } - - /// Returns a closure that toggles the wrapped value with the specified animation when executed. - /// - /// - Parameter animation: The animation to use when toggling the wrapped value. - /// - Returns: A closure that toggles the wrapped value with the specified animation. - func toggleAction(_ animation: Animation = .easeInOut) -> () -> Void { - return { - withAnimation(animation) { - self.wrappedValue.toggle() - } - } - } -} - -public extension Binding { - /// Creates a `Binding` with the specified value and action closure. - /// - /// - Parameters: - /// - value: The value to bind to. - /// - action: The closure to execute when the value is set. - init(value: Value, action: @escaping () -> Void) { - self.init(get: { value }, set: { _ in action() } ) - } - - /// Creates a `Binding` with the specified value and action closure. - /// - /// - Parameters: - /// - value: The value to bind to. - /// - action: The closure to execute when the value is set. - init(value: Value, action: @escaping (Value) -> Void) { - self.init(get: { value }, set: { action($0) } ) - } - - /// Creates a `Binding` with the specified getter and action closure. - /// - /// - Parameters: - /// - get: The closure to execute when getting the value. - /// - action: The closure to execute when the value is set. - init(get: @escaping () -> Value, action: @escaping () -> Void) { - self.init(get: get, set: { _ in action() }) - } - - /// Creates a `Binding` with the specified getter and action closure. - /// - /// - Parameters: - /// - get: The closure to execute when getting the value. - /// - action: The closure to execute when the value is set. - init(get: @escaping () -> Value, action: @escaping (Value) -> Void) { - self.init(get: get, set: { action($0) }) - } - - /// Creates a `Binding` with the specified read-only getter. - /// - /// - Parameters: - /// - get: The closure to execute when getting the value. - init(readOnly get: @escaping () -> Value) { - self.init(get: get, set: { _ in }) - } - - /// Creates a `Binding` with the specified value. - /// - /// - Parameter value: The value to bind to. - init(value: Value) { - self.init(get: { value }, set: { _ in }) - } -} diff --git a/Sources/SwiftUIHelpers/Extensions/Color.swift b/Sources/SwiftUIHelpers/Extensions/Color.swift deleted file mode 100644 index a939a7c..0000000 --- a/Sources/SwiftUIHelpers/Extensions/Color.swift +++ /dev/null @@ -1,75 +0,0 @@ -// MIT License -// -// Copyright (c) 2023 Maksym Kuznietsov -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// Created by Valeriy Malishevskyi on 06.05.2023. -// - -import SwiftUI - -public extension Color { - func uiColor() -> UIColor { - let components = self.components() - return UIColor(red: components.r, green: components.g, blue: components.b, alpha: components.a) - } - - private func components() -> (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) { - let scanner = Scanner(string: self.description.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)) - var hexNumber: UInt64 = 0 - var r: CGFloat = 0.0, g: CGFloat = 0.0, b: CGFloat = 0.0, a: CGFloat = 0.0 - - let result = scanner.scanHexInt64(&hexNumber) - if result { - r = CGFloat((hexNumber & 0xff000000) >> 24) / 255 - g = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255 - b = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255 - a = CGFloat(hexNumber & 0x000000ff) / 255 - } - return (r, g, b, a) - } -} - -public extension Color { - init(hex: String) { - let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) - var int: UInt64 = 0 - Scanner(string: hex).scanHexInt64(&int) - let a, r, g, b: UInt64 - switch hex.count { - case 3: // RGB (12-bit) - (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) - case 6: // RGB (24-bit) - (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) - case 8: // ARGB (32-bit) - (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) - default: - (a, r, g, b) = (1, 1, 1, 0) - } - - self.init( - .sRGB, - red: Double(r) / 255, - green: Double(g) / 255, - blue: Double(b) / 255, - opacity: Double(a) / 255 - ) - } -} diff --git a/Sources/SwiftUIHelpers/Extensions/Hidden.swift b/Sources/SwiftUIHelpers/Extensions/Hidden.swift deleted file mode 100644 index efc08a6..0000000 --- a/Sources/SwiftUIHelpers/Extensions/Hidden.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// Created by Valeriy Malishevskyi on 06.05.2023. -// - -import SwiftUI - -public extension View { - - /// Hide or show the view based on a boolean value. - /// - /// Example for visibility: - /// - /// Text("Label") - /// .hidden(true) - /// - /// Example for complete removal: - /// - /// Text("Label") - /// .hidden(true, remove: true) - /// - /// - Parameters: - /// - hidden: Set to `false` to show the view. Set to `true` to hide the view. - /// - remove: Boolean value indicating whether or not to remove the view. - @ViewBuilder func hidden(_ hidden: Bool, remove: Bool = false) -> some View { - if hidden { - if !remove { - self.hidden() - } - } else { - self - } - } - - /// Hides or shows the view based on the specified boolean value, and displays a placeholder view when hidden. - /// - /// - Parameters: - /// - hidden: A boolean value indicating whether to hide (`true`) or show (`false`) the view. - /// - remove: A boolean value indicating whether or not to remove the view. - /// - placeholder: A closure returning a `View` to display when the main view is hidden. - /// - /// - Returns: A `ViewBuilder` containing the modified `View`. - @ViewBuilder func hidden(_ hidden: Bool, remove: Bool = false, placeholder: () -> V) -> some View { - if hidden { - if !remove { - self.hidden() - .overlay(placeholder()) - } else { - placeholder() - } - } else { - self - } - } - - /// Hides or shows the view based on the specified optional boolean value, and displays a placeholder view when hidden. - /// - /// - Parameters: - /// - hidden: An optional boolean value indicating whether to hide (`true`) or show (`false`) the view. - /// - remove: A boolean value indicating whether or not to remove the view. - /// - placeholder: A closure returning a `View` to display when the main view is hidden. - /// - /// - Returns: A `ViewBuilder` containing the modified `View`. - @ViewBuilder func hidden(_ hidden: Bool?, remove: Bool = false, placeholder: () -> V) -> some View { - if let hidden { - self.hidden(hidden, remove: remove, placeholder: placeholder) - } else { - self - } - } -} diff --git a/Sources/SwiftUIHelpers/Extensions/NavigationLink.swift b/Sources/SwiftUIHelpers/Extensions/NavigationLink.swift deleted file mode 100644 index b0c4a56..0000000 --- a/Sources/SwiftUIHelpers/Extensions/NavigationLink.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// Created by Valeriy Malishevskyi on 06.05.2023. -// - -import SwiftUI - -public extension NavigationLink where Label == EmptyView { - /// Creates a `NavigationLink` with an empty label and the specified destination and binding to its `isActive` state. - /// - /// - Parameters: - /// - destination: The destination view of the link. - /// - isActive: A binding to the `isActive` state of the link. - init(destination: Destination, isActive: Binding) { - self.init(destination: destination, isActive: isActive) { EmptyView() } - } - - /// Creates a `NavigationLink` with an empty label and the specified closure to construct its destination - /// and binding to its `isActive` state. - /// - /// - Parameters: - /// - isActive: A binding to the `isActive` state of the link. - /// - destination: A closure returning the destination view of the link. - init(isActive: Binding, @ViewBuilder destination: @escaping () -> Destination) { - self.init(destination: destination(), isActive: isActive) { EmptyView() } - } -} diff --git a/Sources/SwiftUIHelpers/Extensions/PreviewDevice.swift b/Sources/SwiftUIHelpers/Extensions/PreviewDevice.swift deleted file mode 100644 index ae0efc1..0000000 --- a/Sources/SwiftUIHelpers/Extensions/PreviewDevice.swift +++ /dev/null @@ -1,67 +0,0 @@ -// MIT License -// -// Copyright (c) 2023 Maksym Kuznietsov -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// Created by Valeriy Malishevskyi on 06.05.2023. -// - -import SwiftUI - -public extension PreviewDevice { - static let iPhoneSE: PreviewDevice = .init(rawValue: "iPhone SE (1st generation)") - static let iPhoneSE2: PreviewDevice = .init(rawValue: "iPhone SE (2nd generation)") - static let iPhone8: PreviewDevice = .init(rawValue: "iPhone 8") - static let iPhone8Plus: PreviewDevice = .init(rawValue: "iPhone 8 Plus") - static let iPhone11: PreviewDevice = .init(rawValue: "iPhone 11") - static let iPhone11Pro: PreviewDevice = .init(rawValue: "iPhone 11 Pro") - static let iPhone11ProMax: PreviewDevice = .init(rawValue: "iPhone 11 Pro Max") -} - -// MARK: - PreviewOnDevices - -public extension PreviewDevice { - /// Returns a `Group` of views with the specified view wrapped in a `ForEach` loop for each device in the specified array, previewing the view on each device. - /// - /// - Parameters: - /// - devices: An array of `PreviewDevice` values to preview the view on. Defaults to all available devices. - /// - contentBuilder: A closure returning the view to preview on each device. - /// - /// - Returns: A `Group` of views previewing the specified view on each device. - static func previewOnDevices(_ devices: [PreviewDevice] = PreviewDevice.allCases, @ViewBuilder contentBuilder: @escaping () -> V) -> some View { - Group { - ForEach(devices) { - contentBuilder() - .previewDevice($0) - .previewDisplayName($0.rawValue) - } - } - } -} - -// MARK: - Identifiable -extension PreviewDevice: Identifiable { - public var id: String { rawValue } -} - -// MARK: - CaseIterable -extension PreviewDevice: CaseIterable { - public static var allCases: [PreviewDevice] = [.iPhone11ProMax, .iPhone11Pro, .iPhone11, .iPhone8Plus, .iPhone8, .iPhoneSE] -} diff --git a/Sources/SwiftUIHelpers/Extensions/UIColor.swift b/Sources/SwiftUIHelpers/Extensions/UIColor.swift deleted file mode 100644 index d2546de..0000000 --- a/Sources/SwiftUIHelpers/Extensions/UIColor.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// Created by Valeriy Malishevskyi on 06.05.2023. -// - -import SwiftUI - -public extension UIColor { - /// Creates a `UIColor` instance with the specified red, green, and blue values. - /// - /// - Parameters: - /// - red: The red component of the color, from 0 to 255. - /// - green: The green component of the color, from 0 to 255. - /// - blue: The blue component of the color, from 0 to 255. - /// - /// - Returns: A `UIColor` instance with the specified RGB values. - convenience init(red: Int, green: Int, blue: Int) { - assert(red >= 0 && red <= 255, "Invalid red component") - assert(green >= 0 && green <= 255, "Invalid green component") - assert(blue >= 0 && blue <= 255, "Invalid blue component") - - self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0) - } - - /// Creates a `UIColor` instance with the specified RGB value. - /// - /// - Parameter rgb: The RGB value of the color, in hexadecimal format. - /// - /// - Returns: A `UIColor` instance with the specified RGB value. - convenience init(rgb: Int) { - self.init( - red: (rgb >> 16) & 0xFF, - green: (rgb >> 8) & 0xFF, - blue: rgb & 0xFF - ) - } - - /// Creates a `UIColor` instance with the specified hexadecimal value. - /// - /// - Parameter hex: The hexadecimal value of the color. - /// - /// - Returns: A `UIColor` instance with the specified hexadecimal value. - convenience init?(hex: String) { - var cString:String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() - - if (cString.hasPrefix("#")) { - cString.remove(at: cString.startIndex) - } - - if ((cString.count) != 6) { - return nil - } - - var rgbValue:UInt64 = 0 - Scanner(string: cString).scanHexInt64(&rgbValue) - - self.init( - red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0, - green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0, - blue: CGFloat(rgbValue & 0x0000FF) / 255.0, - alpha: CGFloat(1.0) - ) - } -} - -public extension UIColor { - /// Creates a `UIColor` instance that adapts to the current interface style (light or dark) by returning the appropriate color. - /// - /// - Parameters: - /// - light: The color to use in light mode. - /// - dark: The color to use in dark mode. - /// - /// - Returns: A `UIColor` instance that adapts to the current interface style. - convenience init(light: UIColor, dark: UIColor) { - self.init { traitCollection in - switch traitCollection.userInterfaceStyle { - case .light, .unspecified: - return light - case .dark: - return dark - @unknown default: - return light - } - } - } -} diff --git a/Sources/SwiftUIHelpers/Extensions/UIImage+QRCode.swift b/Sources/SwiftUIHelpers/Extensions/UIImage+QRCode.swift deleted file mode 100644 index 7e17da2..0000000 --- a/Sources/SwiftUIHelpers/Extensions/UIImage+QRCode.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// Created by Valeriy Malishevskyi on 06.05.2023. -// - -import SwiftUI - -public extension UIImage { - /// QR Code correction levels as defined by the ISO/IEC 18004:2015 standard. - /// - /// Note: Increasing the error correction level of a QR Code image also increases the amount of data it encodes, - /// thus producing a larger (denser) image. - /// - /// - low: 7% of codewords can be restored. - /// - medium: 15% of codewords can be restored. - /// - quartile: 25% of codewords can be restored. - /// - high: 30% of codewords can be restored. - enum QRCodeCorrectionLevel: String { - case low = "L" - case medium = "M" - case quartile = "Q" - case high = "H" - } - - /// Create an UIImage by rendering a QR Code that contains the data of the provided string. - /// - /// - Parameters: - /// - qrString: The data to embed in the QR Code. - /// - size: The final size of the QR Code image. - /// - scale: The scale of the QR Code image. Ideally this matches the target screen's scale. - /// - errorCorrectionLevel: The error collection level to use when generating the QR Code image. - convenience init?( - qrString: String, - size: CGSize = CGSize(width: 128, height: 128), - scale: CGFloat = 1.0, - errorCorrectionLevel: QRCodeCorrectionLevel = .medium - ) { - let filterParameters: [String : Any] = [ - "inputMessage": qrString.data(using: .utf8)!, - "inputCorrectionLevel": errorCorrectionLevel.rawValue - ] - - // Create filter and generate qr code at default size - guard let ciImage = CIFilter(name: "CIQRCodeGenerator", parameters: filterParameters)?.outputImage else { - return nil - } - - // Convert the CIImage object to a CGImage object - guard let cgImage = CIContext(options: nil).createCGImage(ciImage, from: ciImage.extent) else { - return nil - } - - // Begin graphics context so that we can resize the QR code - UIGraphicsBeginImageContextWithOptions(size, false, scale) - - // Get the graphics context - guard let context = UIGraphicsGetCurrentContext() else { - return nil - } - - // Disable interpolation so we get a crisp image (best for QR code upscaling) - context.interpolationQuality = .none - - // Draw the CGImage in the graphics context - context.draw(cgImage, in: context.boundingBoxOfClipPath) - - // Get the upscaled image from the graphics context - guard let upscaledCgImage = UIGraphicsGetImageFromCurrentImageContext()?.cgImage else { - return nil - } - - // End the graphics context - UIGraphicsEndImageContext() - - // Initialize self with the final updscaled image by remapping it to the desired scale, and by flipping it - // vertically to match the iOS graphics orientation. - self.init(cgImage: upscaledCgImage, scale: scale, orientation: .downMirrored) - } -} diff --git a/Sources/SwiftUIHelpers/Extensions/View+Embed.swift b/Sources/SwiftUIHelpers/Extensions/View+Embed.swift deleted file mode 100644 index 7214425..0000000 --- a/Sources/SwiftUIHelpers/Extensions/View+Embed.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Created by Valeriy Malishevskyi on 06.05.2023. -// - -import SwiftUI - -public extension View { - /// Embeds the view in a `NavigationView`. - /// - /// - Returns: A view embedded in a `NavigationView`. - func embedInNavigationView() -> some View { - NavigationView { self } - } -} diff --git a/Sources/SwiftUIHelpers/FontProvider/FontModifier/FontModifier.swift b/Sources/SwiftUIHelpers/FontProvider/FontModifier/FontModifier.swift new file mode 100644 index 0000000..40a5b85 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/FontModifier/FontModifier.swift @@ -0,0 +1,12 @@ +// +// FontModifier.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import UIKit + +protocol FontModifier { + func modify(_ fontDescriptor: inout UIFontDescriptor) +} diff --git a/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/BoldModifier.swift b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/BoldModifier.swift new file mode 100644 index 0000000..c358312 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/BoldModifier.swift @@ -0,0 +1,16 @@ +// +// BoldModifier.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import UIKit + +struct BoldModifier: StaticFontModifier { + init() {} + + func modify(_ fontDescriptor: inout UIFontDescriptor) { + fontDescriptor = fontDescriptor.withSymbolicTraits(.traitBold) ?? fontDescriptor + } +} diff --git a/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/ItalicModifier.swift b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/ItalicModifier.swift new file mode 100644 index 0000000..f67c6dc --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/ItalicModifier.swift @@ -0,0 +1,16 @@ +// +// ItalicModifier.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import UIKit + +struct ItalicModifier: StaticFontModifier { + init() {} + + func modify(_ fontDescriptor: inout UIFontDescriptor) { + fontDescriptor = fontDescriptor.withSymbolicTraits(.traitItalic) ?? fontDescriptor + } +} diff --git a/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/StaticFontModifier.swift b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/StaticFontModifier.swift new file mode 100644 index 0000000..3e32122 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/StaticFontModifier.swift @@ -0,0 +1,10 @@ +// +// StaticFontModifier.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +protocol StaticFontModifier: FontModifier { + init() +} diff --git a/Sources/SwiftUIHelpers/FontProvider/FontModifier/WeightModifier.swift b/Sources/SwiftUIHelpers/FontProvider/FontModifier/WeightModifier.swift new file mode 100644 index 0000000..fbac8b0 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/FontModifier/WeightModifier.swift @@ -0,0 +1,16 @@ +// +// WeightModifier.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import UIKit + +struct WeightModifier: FontModifier { + let weight: UIFont.Weight + + func modify(_ fontDescriptor: inout UIFontDescriptor) { + fontDescriptor = fontDescriptor.addingAttributes([.traits: [UIFontDescriptor.TraitKey.weight: weight]]) + } +} diff --git a/Sources/SwiftUIHelpers/FontProvider/FontProvider.swift b/Sources/SwiftUIHelpers/FontProvider/FontProvider.swift new file mode 100644 index 0000000..7f9049f --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/FontProvider.swift @@ -0,0 +1,18 @@ +// +// FontProvider.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import UIKit + +protocol FontProvider { + func fontDescriptor(with traitCollection: UITraitCollection?) -> UIFontDescriptor +} + +extension FontProvider { + func font(with traitCollection: UITraitCollection?) -> UIFont { + UIFont(descriptor: fontDescriptor(with: traitCollection), size: 0) + } +} diff --git a/Sources/SwiftUIHelpers/FontProvider/Providers/ModifierProvider.swift b/Sources/SwiftUIHelpers/FontProvider/Providers/ModifierProvider.swift new file mode 100644 index 0000000..4dd6cc9 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/Providers/ModifierProvider.swift @@ -0,0 +1,21 @@ +// +// ModifierProvider.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import SwiftUI + +struct ModifierProvider: FontProvider { + let base: FontProvider + let modifier: FontModifier + + func fontDescriptor(with traitCollection: UITraitCollection?) -> UIFontDescriptor { + var descriptor = base.fontDescriptor(with: traitCollection) + + modifier.modify(&descriptor) + + return descriptor + } +} diff --git a/Sources/SwiftUIHelpers/FontProvider/Providers/NamedProvider.swift b/Sources/SwiftUIHelpers/FontProvider/Providers/NamedProvider.swift new file mode 100644 index 0000000..759d789 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/Providers/NamedProvider.swift @@ -0,0 +1,32 @@ +// +// NamedProvider.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import UIKit + +struct NamedProvider: FontProvider { + var name: String + + var size: CGFloat + + var textStyle: UIFont.TextStyle? + + func fontDescriptor(with traitCollection: UITraitCollection?) -> UIFontDescriptor { + if let textStyle = textStyle { + let metrics = UIFontMetrics(forTextStyle: textStyle) + + return UIFontDescriptor(fontAttributes: [ + .family: name, + .size: metrics.scaledValue(for: size, compatibleWith: traitCollection) + ]) + } else { + return UIFontDescriptor(fontAttributes: [ + .family: name, + .size: size + ]) + } + } +} diff --git a/Sources/SwiftUIHelpers/FontProvider/Providers/StaticModifierProvider.swift b/Sources/SwiftUIHelpers/FontProvider/Providers/StaticModifierProvider.swift new file mode 100644 index 0000000..93e1503 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/Providers/StaticModifierProvider.swift @@ -0,0 +1,20 @@ +// +// StaticModifierProvider.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import SwiftUI + +struct StaticModifierProvider: FontProvider { + var base: FontProvider + + func fontDescriptor(with traitCollection: UITraitCollection?) -> UIFontDescriptor { + var descriptor = base.fontDescriptor(with: traitCollection) + + M().modify(&descriptor) + + return descriptor + } +} diff --git a/Sources/SwiftUIHelpers/FontProvider/Providers/SystemProvider.swift b/Sources/SwiftUIHelpers/FontProvider/Providers/SystemProvider.swift new file mode 100644 index 0000000..322497f --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/Providers/SystemProvider.swift @@ -0,0 +1,26 @@ +// +// SystemProvider.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import SwiftUI + +struct SystemProvider: FontProvider { + var size: CGFloat + + var design: UIFontDescriptor.SystemDesign + + var weight: UIFont.Weight? + + func fontDescriptor(with traitCollection: UITraitCollection?) -> UIFontDescriptor { + UIFont + .preferredFont(forTextStyle: .body, compatibleWith: traitCollection) + .fontDescriptor + .withDesign(design)! + .addingAttributes([ + .size: size + ]) + } +} diff --git a/Sources/SwiftUIHelpers/FontProvider/Providers/TextStyleProvider.swift b/Sources/SwiftUIHelpers/FontProvider/Providers/TextStyleProvider.swift new file mode 100644 index 0000000..e6312f6 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/Providers/TextStyleProvider.swift @@ -0,0 +1,28 @@ +// +// TextStyleProvider.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import UIKit + +struct TextStyleProvider: FontProvider { + func fontDescriptor(with traitCollection: UITraitCollection?) -> UIFontDescriptor { + let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style) + + if let design = design { + descriptor.withDesign(design) + } + + if let weight = weight { + descriptor.withSymbolicTraits(.traitBold) + } + + return descriptor + } + + let style: UIFont.TextStyle + let design: UIFontDescriptor.SystemDesign? + let weight: UIFont.Weight? +} diff --git a/Sources/SwiftUIHelpers/FontProvider/UIFont+Font.swift b/Sources/SwiftUIHelpers/FontProvider/UIFont+Font.swift new file mode 100644 index 0000000..2e14620 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/UIFont+Font.swift @@ -0,0 +1,101 @@ +// +// UIFont+Font.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import SwiftUI +import SwiftUIExtensions +import UIKit + +extension UIFont { + public static func resolving(font: Font, dynamicTypeSize: DynamicTypeSize = .medium) -> UIFont { + let traits = UITraitCollection(traitsFrom: [ + .init(preferredContentSizeCategory: UIContentSizeCategory(dynamicTypeSize)) + ]) + + if let provider = font.getFontProvider() { + return provider.font(with: traits) + } + + return .preferredFont(forTextStyle: .body) + } +} + +extension Font { + func getFontProvider() -> FontProvider? { + let mirror = Mirror(reflecting: self) + + guard let provider = mirror.descendant("provider", "base") else { + return nil + } + + return resolveFontProvider(provider) + } + + private func resolveFontProvider(_ provider: Any) -> FontProvider? { + let mirror = Mirror(reflecting: provider) + + switch String(describing: type(of: provider)) { + case "StaticModifierProvider": + guard let base = mirror.descendant("base", "provider", "base") else { + return nil + } + + return resolveFontProvider(base).map(StaticModifierProvider.init) + case "StaticModifierProvider": + guard let base = mirror.descendant("base", "provider", "base") else { + return nil + } + + return resolveFontProvider(base).map(StaticModifierProvider.init) + case "SystemProvider": + guard let size = mirror.descendant("size") as? CGFloat, + let design = mirror.descendant("design") as? Font.Design else { + return nil + } + + let weight = mirror.descendant("weight") as? Font.Weight + + return SystemProvider(size: size, design: .init(design), weight: weight.map(UIFont.Weight.init)) + case "NamedProvider": + guard let name = mirror.descendant("name") as? String, + let size = mirror.descendant("size") as? CGFloat else { + return nil + } + + let textStyle = mirror.descendant("textStyle") as? Font.TextStyle + + return NamedProvider(name: name, size: size, textStyle: textStyle.map(UIFont.TextStyle.init)) + case "TextStyleProvider": + guard let style = mirror.descendant("style") as? Font.TextStyle else { + return nil + } + + let design = mirror.descendant("design") as? Font.Design + let weight = mirror.descendant("weight") as? Font.Weight + + return TextStyleProvider( + style: .init(style), + design: design.map(UIFontDescriptor.SystemDesign.init), + weight: weight.map(UIFont.Weight.init) + ) + case "ModifierProvider": + guard let base = mirror.descendant("base") as? Font, + let provider = base.getFontProvider() else { + return nil + } + + let weight = mirror.descendant("modifier", "weight") as! Font.Weight + let weightModifier = WeightModifier(weight: .init(weight)) + + return ModifierProvider.init(base: provider, modifier: weightModifier) + default: + // Not exhaustive, more providers need to be handled here. + + print("Unknown font provider: \(provider)") + return nil + } + } +} diff --git a/Sources/SwiftUIHelpers/GeometryEffect/GeometryEffectView.swift b/Sources/SwiftUIHelpers/GeometryEffect/GeometryEffectView.swift new file mode 100644 index 0000000..7c857e0 --- /dev/null +++ b/Sources/SwiftUIHelpers/GeometryEffect/GeometryEffectView.swift @@ -0,0 +1,128 @@ +// +// Created by Valeriy Malishevskyi on 22.02.2024. +// + +import SwiftUI + +/// A modifier that applies a custom effect to a view based on its geometry. +/// +/// This struct captures the essence of a geometry-based visual effect, allowing you to encapsulate complex view transformations that depend on the view's geometric properties. +/// It's the backbone of the `geometryEffect` modifier, providing the functionality to apply the effect. +/// +/// - Parameter effect: The closure that encapsulates the custom effect, taking a snapshot of the view and its geometry as inputs, and producing a transformed view. +public struct GeometryEffectView: View { + var root: Root + var content: @MainActor (Root, GeometryEffectProxy) -> Content + + public init( + root: Root, + @ViewBuilder content: @MainActor @escaping (Root, GeometryEffectProxy) -> Content + ) { + self.root = root + self.content = content + } + + public var body: some View { + root + .hidden() + .modifier(GeometryProxyWrapper()) + .overlayPreferenceValue(GeometryEffectProxyPreferenceKey.self) { proxy in + content(root, proxy) + } + } +} + +struct GeometryProxyWrapper: ViewModifier { + func body(content: Content) -> some View { + content + .overlay( + GeometryReader { geometry in + Color.clear.preference( + key: GeometryEffectProxyPreferenceKey.self, + value: GeometryEffectProxy(geometry) + ) + } + ) + } +} + +@MainActor public struct GeometryEffectProxy: Equatable, Sendable { + private let localFrame: CGRect? + private let globalFrame: CGRect? + private let proxy: EquatableGeometryProxy? + + public let safeAreaInsets: EdgeInsets + + public var size: CGSize { + localFrame?.size ?? .zero + } + + static nonisolated var empty: GeometryEffectProxy { + .init(nil) + } + + public nonisolated init(_ geometry: GeometryProxy?) { + localFrame = geometry?.frame(in: .local) + globalFrame = geometry?.frame(in: .global) + + safeAreaInsets = geometry?.safeAreaInsets ?? .init() + + proxy = geometry.map(EquatableGeometryProxy.init) + } + + public func _frame(in coordinateSpace: CoordinateSpace) -> CGRect? { + switch coordinateSpace { + case .local: + guard let result = localFrame else { + return nil + } + + return result + case .global: + guard let result = globalFrame else { + return nil + } + + return result + case .named(let name): + guard let result = proxy?.proxy.frame(in: coordinateSpace) else { + return nil + } + + return result + default: + return nil + } + } + + public func frame( + in coordinateSpace: CoordinateSpace + ) -> CGRect { + _frame(in: coordinateSpace) ?? .zero + } +} + +private struct EquatableGeometryProxy: Equatable, @unchecked Sendable { + let proxy: GeometryProxy + + init(_ proxy: GeometryProxy) { + self.proxy = proxy + } + + static func == (lhs: EquatableGeometryProxy, rhs: EquatableGeometryProxy) -> Bool { + true + } +} + +struct GeometryEffectProxyPreferenceKey: PreferenceKey { + static let defaultValue: GeometryEffectProxy = .empty + + static func reduce(value: inout GeometryEffectProxy, nextValue: () -> GeometryEffectProxy) { + let isChanged = value != nextValue() + let isNotEmpty = value != .empty + if isChanged && isNotEmpty { + value = nextValue() + } + print("GeometryEffectProxyPreferenceKey reduce called with \(value)") + } +} diff --git a/Sources/SwiftUIHelpers/GeometryEffect/View+GeometryEffect.swift b/Sources/SwiftUIHelpers/GeometryEffect/View+GeometryEffect.swift new file mode 100644 index 0000000..7bda40a --- /dev/null +++ b/Sources/SwiftUIHelpers/GeometryEffect/View+GeometryEffect.swift @@ -0,0 +1,19 @@ +// +// Created by Valeriy Malishevskyi on 22.02.2024. +// + +import SwiftUI + +public extension View { + /// Provides a modifier for applying custom geometry effects to a view. + /// + /// Use this modifier to add dynamic effects to a view based on its geometry, such as position and size. + /// Ideal for creating animations or visual transformations that respond to changes in the view's environment. + /// + /// - Parameter effect: A closure that defines the custom effect, taking the current view and its geometry proxy as arguments, and returning a modified view. + /// - Returns: A view modified by the specified geometry effect. + @_disfavoredOverload + func geometryEffect(@ViewBuilder _ effect: @escaping @MainActor (Self, GeometryEffectProxy) -> some View) -> some View { + GeometryEffectView(root: self, content: effect) + } +} diff --git a/Sources/SwiftUIHelpers/Helpers/RoundedCorner.swift b/Sources/SwiftUIHelpers/Helpers/RoundedCorner.swift deleted file mode 100644 index 6e94d7e..0000000 --- a/Sources/SwiftUIHelpers/Helpers/RoundedCorner.swift +++ /dev/null @@ -1,56 +0,0 @@ -// MIT License -// -// Copyright (c) 2023 Maksym Kuznietsov -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// Created by Valeriy Malishevskyi on 06.05.2023. -// - -#if canImport(UIKit) -import SwiftUI - -public extension View { - /// Rounds the specified corners of the view. - /// - /// - Parameters: - /// - radius: The radius of the rounded corners. - /// - corners: The corners to round. - /// - /// - Returns: A view with the specified corners rounded. - func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View { - clipShape(RoundedCorner(radius: radius, corners: corners) ) - } -} - -private struct RoundedCorner: Shape { - public var radius: CGFloat - public var corners: UIRectCorner - - init(radius: CGFloat = .infinity, corners: UIRectCorner = .allCorners) { - self.radius = radius - self.corners = corners - } - - func path(in rect: CGRect) -> Path { - let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) - return Path(path.cgPath) - } -} -#endif diff --git a/Sources/SwiftUIHelpers/Helpers/SizeReader.swift b/Sources/SwiftUIHelpers/Helpers/SizeReader.swift deleted file mode 100644 index 0296fa0..0000000 --- a/Sources/SwiftUIHelpers/Helpers/SizeReader.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// Created by Valeriy Malishevskyi on 06.05.2023. -// - -import SwiftUI - -fileprivate struct SizePreferenceKey: PreferenceKey { - static var defaultValue: CGSize = .zero - - static func reduce(value: inout CGSize, nextValue: () -> CGSize) { - value = nextValue() - } -} - -public extension View { - /// Adds a hidden view with a `GeometryReader` to read the size of the view, then calls the specified closure with the size whenever it changes. - /// - /// - Parameter onChange: A closure called with the size of the view whenever it changes. - /// - /// - Returns: A view with a hidden `GeometryReader`. - func readSize(onChange: @escaping (CGSize) -> Void) -> some View { - overlay( - GeometryReader { geometryProxy in - Color.clear - .preference(key: SizePreferenceKey.self, value: geometryProxy.size) - } - ) - .onPreferenceChange(SizePreferenceKey.self, perform: onChange) - } -} - -public extension View { - /// Adds a hidden view with a `GeometryReader` to read the width of the view, then sets the specified binding to the width - /// whenever it changes. - /// - /// - Parameter width: A binding to the width of the view. - /// - /// - Returns: A view with a hidden `GeometryReader`. - func readFrameWidth(_ width: Binding) -> some View { - readSize { newSize in - width.wrappedValue = newSize.width - } - } - - /// Adds a hidden view with a `GeometryReader` to read the height of the view, then sets the specified binding to the height - /// whenever it changes. - /// - /// - Parameter height: A binding to the height of the view. - /// - /// - Returns: A view with a hidden `GeometryReader`. - func readFrameHeight(_ height: Binding) -> some View { - readSize { newSize in - height.wrappedValue = newSize.height - } - } -} - diff --git a/Sources/SwiftUIHelpers/InternalAPI/DetachedPlaceholder.swift b/Sources/SwiftUIHelpers/InternalAPI/DetachedPlaceholder.swift new file mode 100644 index 0000000..b876569 --- /dev/null +++ b/Sources/SwiftUIHelpers/InternalAPI/DetachedPlaceholder.swift @@ -0,0 +1,21 @@ +// +// Created by Valeriy Malishevskyi on 06.05.2024. +// + +import SwiftUI + +public typealias EmptyDetached = _EmptyDetached + +/// An opaque placeholder for a detached view, the placeholder may be used as a child of one other view. +public struct DetachedPlaceholder where Detached : _Detachable { + let detached: Detached +} + +extension View { + public func detached( + with type: T.Type = T.self, + _ transform: @escaping (_DetachedPlaceholder) -> U + ) -> some View where T : _Detachable, U : View { + _detached(with: type, transform) + } +} diff --git a/Sources/SwiftUIHelpers/InternalAPI/DetachedView.swift b/Sources/SwiftUIHelpers/InternalAPI/DetachedView.swift new file mode 100644 index 0000000..8c35b71 --- /dev/null +++ b/Sources/SwiftUIHelpers/InternalAPI/DetachedView.swift @@ -0,0 +1,24 @@ +// +// Created by Valeriy Malishevskyi on 06.05.2024. +// + +import SwiftUI + +/// A view that instantiates a child content view, “detaching” some of its inherited attributes, +/// then constructs a new child view hierarchy as a function of a placeholder representing the detached view. +/// Typically used to restrict where preference values are read from to avoid evaluation cycles. +public struct DetachedView where Detached : _Detachable, Content : View, Child : View { + let content: Content + let transform: (_DetachedPlaceholder) -> Child + + public init(content: Content, transform: @escaping (_DetachedPlaceholder) -> Child) { + self.content = content + self.transform = transform + } +} + +extension DetachedView: View { + public var body: some View { + _DetachedView(content: content, transform: transform) + } +} diff --git a/Sources/SwiftUIHelpers/InternalAPI/EnvironmentValues+LineHeightMultiple.swift b/Sources/SwiftUIHelpers/InternalAPI/EnvironmentValues+LineHeightMultiple.swift new file mode 100644 index 0000000..8b0f29c --- /dev/null +++ b/Sources/SwiftUIHelpers/InternalAPI/EnvironmentValues+LineHeightMultiple.swift @@ -0,0 +1,16 @@ +// +// Created by Valeriy Malishevskyi on 06.05.2024. +// + +import SwiftUI + +extension EnvironmentValues { + public var lineHeightMultiple: CGFloat { + get { + self._lineHeightMultiple + } + set { + self._lineHeightMultiple = newValue + } + } +} diff --git a/Sources/SwiftUIHelpers/InternalAPI/PreferenceKey+Delay.swift b/Sources/SwiftUIHelpers/InternalAPI/PreferenceKey+Delay.swift new file mode 100644 index 0000000..2c3661c --- /dev/null +++ b/Sources/SwiftUIHelpers/InternalAPI/PreferenceKey+Delay.swift @@ -0,0 +1,11 @@ +// +// Created by Valeriy Malishevskyi on 06.05.2024. +// + +import SwiftUI + +extension PreferenceKey { + public static func delay(_ transform: @escaping (_PreferenceValue) -> T) -> some View where T : View { + Self._delay(transform) + } +} diff --git a/Sources/SwiftUIHelpers/InternalAPI/PreferenceValue+Force.swift b/Sources/SwiftUIHelpers/InternalAPI/PreferenceValue+Force.swift new file mode 100644 index 0000000..e6edf18 --- /dev/null +++ b/Sources/SwiftUIHelpers/InternalAPI/PreferenceValue+Force.swift @@ -0,0 +1,11 @@ +// +// Created by Valeriy Malishevskyi on 06.05.2024. +// + +import SwiftUI + +extension _PreferenceValue where Key: PreferenceKey { + public func force(_ transform: @escaping (Key.Value) -> T) -> _PreferenceReadingView where T : View { + self._force(transform) + } +} diff --git a/Sources/SwiftUIHelpers/InternalAPI/Text+StylisticAlternative.swift b/Sources/SwiftUIHelpers/InternalAPI/Text+StylisticAlternative.swift new file mode 100644 index 0000000..115506e --- /dev/null +++ b/Sources/SwiftUIHelpers/InternalAPI/Text+StylisticAlternative.swift @@ -0,0 +1,11 @@ +// +// Created by Valeriy Malishevskyi on 06.05.2024. +// + +import SwiftUI + +extension Text { + public func stylisticAlternative(_ alternative: Font._StylisticAlternative) -> some View { + self._stylisticAlternative(alternative) + } +} diff --git a/Sources/SwiftUIHelpers/InternalAPI/View+AddingBackground+.swift b/Sources/SwiftUIHelpers/InternalAPI/View+AddingBackground+.swift new file mode 100644 index 0000000..b917f7f --- /dev/null +++ b/Sources/SwiftUIHelpers/InternalAPI/View+AddingBackground+.swift @@ -0,0 +1,27 @@ +// +// Created by Valeriy Malishevskyi on 06.05.2024. +// + +import SwiftUI + +extension View { + /// Add a background group, affecting the default background color. + public func addingBackgroundGroup() -> some View { + _addingBackgroundGroup() + } +} + + +extension View { + /// Add a background layer, affecting the default background color. + public func addingBackgroundLayer() -> some View { + _addingBackgroundLayer() + } +} + +extension View { + /// Sets the style context of self to the default context. + public func defaultContext() -> some View { + _defaultContext() + } +} diff --git a/Sources/SwiftUIHelpers/InternalAPI/View+AutomaticPadding.swift b/Sources/SwiftUIHelpers/InternalAPI/View+AutomaticPadding.swift new file mode 100644 index 0000000..d42766c --- /dev/null +++ b/Sources/SwiftUIHelpers/InternalAPI/View+AutomaticPadding.swift @@ -0,0 +1,31 @@ +// +// Created by Valeriy Malishevskyi on 06.05.2024. +// + +import SwiftUI + +extension View { + + /// Applies explicit padding to a view that allows being disabled by that view using `ignoresAutomaticPadding`. + /// - Parameter insets: The amount to inset this view on each edge. + /// - Returns: A view that insets this view by the amount specified in + public func automaticPadding(_ insets: EdgeInsets) -> some View { + self._automaticPadding(insets) + } + + /// Applies explicit padding to a view that allows being disabled by that view using `ignoresAutomaticPadding`. + /// - Parameters: + /// - edge: The edge to apply padding to. + /// - length: The amount to inset this view on the specified edge. + /// - Returns: A view that insets this view by the amount specified + public func automaticPadding(_ edge: Edge.Set, _ length: CGFloat = 16) -> some View { + let edgeInsets = EdgeInsets( + top: edge.contains(.top) ? length : 0, + leading: edge.contains(.leading) ? length : 0, + bottom: edge.contains(.bottom) ? length : 0, + trailing: edge.contains(.trailing) ? length : 0 + ) + + return self.automaticPadding(edgeInsets) + } +} diff --git a/Sources/SwiftUIHelpers/InternalAPI/View+ColorMonochrome.swift b/Sources/SwiftUIHelpers/InternalAPI/View+ColorMonochrome.swift new file mode 100644 index 0000000..709fb6b --- /dev/null +++ b/Sources/SwiftUIHelpers/InternalAPI/View+ColorMonochrome.swift @@ -0,0 +1,15 @@ +// +// Created by Valeriy Malishevskyi on 06.05.2024. +// + +import SwiftUI + +extension View { + public func colorMonochrome( + _ color: Color, + amount: Double = 1, + bias: Double = 0 + ) -> some View { + self._colorMonochrome(color, amount: amount, bias: bias) + } +} diff --git a/Sources/SwiftUIHelpers/InternalAPI/View+Identified.swift b/Sources/SwiftUIHelpers/InternalAPI/View+Identified.swift new file mode 100644 index 0000000..1152e7f --- /dev/null +++ b/Sources/SwiftUIHelpers/InternalAPI/View+Identified.swift @@ -0,0 +1,11 @@ +// +// Created by Valeriy Malishevskyi on 06.05.2024. +// + +import SwiftUI + +extension View { + public func identified(by identifier: I) -> some View where I : Hashable { + self._identified(by: identifier) + } +} diff --git a/Sources/SwiftUIHelpers/InternalAPI/View+IgnoresAutomaticPadding.swift b/Sources/SwiftUIHelpers/InternalAPI/View+IgnoresAutomaticPadding.swift new file mode 100644 index 0000000..d064bd3 --- /dev/null +++ b/Sources/SwiftUIHelpers/InternalAPI/View+IgnoresAutomaticPadding.swift @@ -0,0 +1,11 @@ +// +// Created by Valeriy Malishevskyi on 06.05.2024. +// + +import SwiftUI + +extension View { + public func ignoresAutomaticPadding(_ flag: Bool = true) -> some View { + self._ignoresAutomaticPadding(flag) + } +} diff --git a/Sources/SwiftUIHelpers/InternalAPI/View+SafeAreaInsets.swift b/Sources/SwiftUIHelpers/InternalAPI/View+SafeAreaInsets.swift new file mode 100644 index 0000000..3d67c33 --- /dev/null +++ b/Sources/SwiftUIHelpers/InternalAPI/View+SafeAreaInsets.swift @@ -0,0 +1,21 @@ +// +// Created by Valeriy Malishevskyi on 06.05.2024. +// + +import SwiftUI + +extension View { + public func safeAreaInsets(_ insets: EdgeInsets) -> some View { + _safeAreaInsets(insets) + } + + public func safeAreaInsets(_ edges: Edge.Set = .all, _ length: CGFloat) -> some View { + let edgeInsets = EdgeInsets( + top: edges.contains(.top) ? length : 0, + leading: edges.contains(.leading) ? length : 0, + bottom: edges.contains(.bottom) ? length : 0, + trailing: edges.contains(.trailing) ? length : 0 + ) + return safeAreaInsets(edgeInsets) + } +} diff --git a/Sources/SwiftUIHelpers/InternalAPI/View+Scrollable.swift b/Sources/SwiftUIHelpers/InternalAPI/View+Scrollable.swift new file mode 100644 index 0000000..eaa651f --- /dev/null +++ b/Sources/SwiftUIHelpers/InternalAPI/View+Scrollable.swift @@ -0,0 +1,19 @@ +// +// Created by Valeriy Malishevskyi on 06.05.2024. +// + +import SwiftUI + +extension View { + public func scrollable( + stretchChildrenToMaxHeight: Bool = false, + horizontal: TextAlignment? = .center, + vertical: _VAlignment? = .center + ) -> _ScrollView<_AligningContentProvider> { + _scrollable( + stretchChildrenToMaxHeight: stretchChildrenToMaxHeight, + horizontal: horizontal, + vertical: vertical + ) + } +} diff --git a/Sources/SwiftUIHelpers/InternalAPI/View+TightPadding.swift b/Sources/SwiftUIHelpers/InternalAPI/View+TightPadding.swift new file mode 100644 index 0000000..664afea --- /dev/null +++ b/Sources/SwiftUIHelpers/InternalAPI/View+TightPadding.swift @@ -0,0 +1,11 @@ +// +// Created by Valeriy Malishevskyi on 06.05.2024. +// + +import SwiftUI + +extension View { + @inlinable public func tightPadding() -> some View { + _tightPadding() + } +} diff --git a/Sources/SwiftUIHelpers/NavigationLinkStyle/NavigationLinkStyle.swift b/Sources/SwiftUIHelpers/NavigationLinkStyle/NavigationLinkStyle.swift new file mode 100644 index 0000000..685710a --- /dev/null +++ b/Sources/SwiftUIHelpers/NavigationLinkStyle/NavigationLinkStyle.swift @@ -0,0 +1,16 @@ +// +// NavigationLinkStyle.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 06.08.2024. +// + +import SwiftUI + +public protocol NavigationLinkStyle: ButtonStyle { } + +extension View { + public func navigationLinkStyle(_ style: some NavigationLinkStyle) -> some View { + buttonStyle(style) + } +} diff --git a/Sources/SwiftUIHelpers/Placeholders/String+Lorem.swift b/Sources/SwiftUIHelpers/Placeholders/String+Lorem.swift deleted file mode 100644 index 7f5b720..0000000 --- a/Sources/SwiftUIHelpers/Placeholders/String+Lorem.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// Created by Valeriy Malishevskyi on 06.05.2023. -// - -import Foundation - -public extension String { - enum Lorem { } -} - -public extension String.Lorem { - - /// Generates a single word. - static var word: String { - words.randomElement()! - } - - /// Generates multiple words whose count is defined by the given value. - /// - /// - Parameter count: The number of words to generate. - /// - Returns: The generated words joined by a space character. - static func words(_ count: Int) -> String { - _compose( - word, - count: count, - join: .space - ) - } - - /// Generates a single sentence. - static var sentence: String { - let numberOfWords = Int.random( - in: minWordsCountInSentence...maxWordsCountInSentence - ) - - return _compose( - word, - count: numberOfWords, - join: .space, - endWith: .dot, - decorate: { $0.firstLetterCapitalized } - ) - } - - /// Generates multiple sentences whose count is defined by the given value. - /// - /// - Parameter count: The number of sentences to generate. - /// - Returns: The generated sentences joined by a space character. - static func sentences(_ count: Int = 1) -> String { - return _compose( - sentence, - count: count, - join: .space - ) - } - - // MARK: - Private Helpers - - fileprivate static let minWordsCountInSentence = 4 - fileprivate static let maxWordsCountInSentence = 16 - - private static let words: [String] = [ - "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit", - "curabitur", "vel", "accumsan", "hendrerit", "libero", "aliquam", "eleifend", "blandit", - "nunc", "ornare", "odio", "auctor", "ut", "eros", "blandit", - "class", "commodo", "condimentum", "congue", "consequat", "conubia", - "convallis", "cras", "cubilia", "curabitur", "curae", "cursus", - "dapibus", "diam", "dictum", "dictumst", "dignissim", "dis", "donec", - "dui", "duis", "efficitur", "egestas", "eget", "eleifend", "elementum", - "enim", "erat", "eros", "est", "et", "etiam", "eu", "euismod", "ex", - "facilisi", "facilisis", "fames", "faucibus", "felis", "fermentum", - "feugiat", "finibus", "fringilla", "fusce", "gravida", "habitant", - "habitasse", "hac", "hendrerit", "himenaeos", "iaculis", "id", - "imperdiet", "in", "inceptos", "integer", "interdum", "justo", - "lacinia", "lacus", "laoreet", "lectus", "leo", "libero", "ligula", - "litora", "lobortis", "luctus", "maecenas", "magna", "magnis", - "malesuada", "massa", "mattis", "mauris", "maximus", "metus", "mi", - "molestie", "mollis", "montes", "morbi", "mus", "nam", "nascetur", - "natoque", "nec", "neque", "netus", "nibh", "nisi", "nisl", "non", - "nostra", "nulla", "nullam", "nunc", "odio", "orci", "ornare", - "parturient", "pellentesque", "penatibus", "per", "pharetra", - "phasellus", "placerat", "platea", "porta", "porttitor", "posuere", - "potenti", "praesent", "pretium", "primis", "proin", "pulvinar", - "purus", "quam", "quis", "quisque", "rhoncus", "ridiculus", "risus", - "rutrum", "sagittis", "sapien", "scelerisque", "sed", "sem", "semper", - "senectus", "sociosqu", "sodales", "sollicitudin", "suscipit", - "suspendisse", "taciti", "tellus", "tempor", "tempus", "tincidunt", - "torquent", "tortor", "tristique", "turpis", "ullamcorper", "ultrices", - "ultricies", "urna", "ut", "varius", "vehicula", "vel", "velit", - "venenatis", "vestibulum", "vitae", "vivamus", "viverra", "volutpat", - "vulputate", - ] - - fileprivate enum Separator: String { - case none = "" - case space = " " - case dot = "." - case newLine = "\n" - } - - fileprivate static func _compose( - _ provider: @autoclosure () -> String, - count: Int, - join middleSeparator: Separator, - endWith endSeparator: Separator = .none, - decorate decorator: ((String) -> String)? = nil - ) -> String { - var string = "" - - for index in 0.. URL { - return URL(string: "http://placebeard.it/\(width)/\(height)")! - } - - /// Generates a image URL with kitten - static func kitten(width: Int = 640, height: Int = 360) -> URL { - return URL(string: "http://placekitten.com/\(width)/\(height)")! - } - - /// Generates a image URL with gray placeholder - static func placehodler(width: Int = 640, height: Int = 360) -> URL { - return URL(string: "http://fakeimg.pl/\(width)x\(height)")! - } -} diff --git a/Sources/SwiftUIHelpers/Previewable.swift b/Sources/SwiftUIHelpers/Previewable.swift new file mode 100644 index 0000000..0c2f0bd --- /dev/null +++ b/Sources/SwiftUIHelpers/Previewable.swift @@ -0,0 +1,31 @@ +// +// Created by Valeriy Malishevskyi on 15.03.2024. +// + +import Foundation + +public protocol Previewable { + static var preview: Self { get } + static func preview(count: Int) -> [Self] + static func makePreview(idx: Int) -> Self +} + +extension Previewable { + public static var preview: Self { + makePreview(idx: 0) + } + + public static func preview(count: Int) -> [Self] { + (0.. [Element] { + Element.preview(count: count) + } +} diff --git a/Sources/SwiftUIHelpers/SceneExtensions/Scene+Modifier.swift b/Sources/SwiftUIHelpers/SceneExtensions/Scene+Modifier.swift new file mode 100644 index 0000000..ee63c67 --- /dev/null +++ b/Sources/SwiftUIHelpers/SceneExtensions/Scene+Modifier.swift @@ -0,0 +1,14 @@ +// +// Scene+Modifier.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 13.12.2024. +// + +import SwiftUI + +extension Scene { + public func modifier(_ modifier: M) -> some Scene { + ModifiedContent(content: self, modifier: modifier) + } +} diff --git a/Sources/SwiftUIHelpers/SceneExtensions/Scene+OnChange.swift b/Sources/SwiftUIHelpers/SceneExtensions/Scene+OnChange.swift new file mode 100644 index 0000000..3755120 --- /dev/null +++ b/Sources/SwiftUIHelpers/SceneExtensions/Scene+OnChange.swift @@ -0,0 +1,65 @@ +// +// Scene+OnChange.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 13.12.2024. +// + +#if canImport(SwiftHelpers) + +import SwiftHelpers +import SwiftUI + +extension Scene { + public func onChange(of value: V, initial: Bool = false, _ action: @escaping (_ oldValue: V, _ newValue: V) -> Void) -> some Scene + where V : Equatable { + modifier(_ValueActionModifier(value: value, initial: initial, action: action)) + } +} + +struct _ValueActionModifier : _SceneModifier where Value : Equatable { + let property: ValueActionProperty + + init(value: Value, initial: Bool, action: @escaping (Value, Value) -> Void) { + self.property = ValueActionProperty(value: value, initial: initial, action: action) + } + + func body(content: SceneContent) -> some Scene { + content + .onChange(of: "") { _ in } // Keep it to trigger the action + } +} + +struct ValueActionProperty: DynamicProperty { + + @StoredValue private var value: Value! + + private var initialValue: Value + private var action: (Value, Value) -> Void + private var useInitial: Bool + + init(value: Value, initial: Bool, action: @escaping (Value, Value) -> Void) { + self.initialValue = value + + self.action = action + self.useInitial = initial + } + + var wrappedValue: Value { + get { value ?? initialValue } + set { value = newValue } + } + + func update() { + guard value != initialValue else { return } + if useInitial { + action(initialValue, initialValue) + } else if let value = value { + action(initialValue, value) + } + + value = initialValue + } +} + +#endif diff --git a/Sources/SwiftUIHelpers/SceneExtensions/Scene+OnReceive.swift b/Sources/SwiftUIHelpers/SceneExtensions/Scene+OnReceive.swift new file mode 100644 index 0000000..654472d --- /dev/null +++ b/Sources/SwiftUIHelpers/SceneExtensions/Scene+OnReceive.swift @@ -0,0 +1,53 @@ +// +// Scene+OnReceive.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 13.12.2024. +// + +#if canImport(SwiftHelpers) + +import SwiftHelpers +import Combine +import SwiftUI + +extension Scene { + public func onReceive

(_ publisher: P, perform action: @escaping (P.Output) -> Void) -> some Scene + where P : Publisher, P.Failure == Never { + modifier(_PublisherActionModifier(publisher: publisher, action: action)) + } +} + +struct _PublisherActionProperty

: DynamicProperty +where P : Publisher, P.Failure == Never { + @StoredValue private var cancelable: AnyCancellable? + + private let publisher: P + private let action: (P.Output) -> Void + + let nonChangingValue = 1 + + init(publisher: P, action: @escaping (P.Output) -> Void) { + self.publisher = publisher + self.action = action + } + + nonisolated func update() { + guard cancelable == nil else { return } + cancelable = publisher.sink(receiveValue: action) + } +} + +struct _PublisherActionModifier

: _SceneModifier where P : Publisher, P.Failure == Never { + let property: _PublisherActionProperty

+ + public init(publisher: P, action: @escaping (P.Output) -> Void) { + self.property = _PublisherActionProperty(publisher: publisher, action: action) + } + + func body(content: SceneContent) -> some Scene { + content.onChange(of: "") { _ in } + } +} + +#endif diff --git a/Sources/SwiftUIHelpers/ScrollView/SelectableArray.swift b/Sources/SwiftUIHelpers/ScrollView/SelectableArray.swift new file mode 100644 index 0000000..2c681c3 --- /dev/null +++ b/Sources/SwiftUIHelpers/ScrollView/SelectableArray.swift @@ -0,0 +1,87 @@ +// +// SelectableArray.swift +// SwiftHelpers +// +// Created by Valeriy Malishevskyi on 28.09.2024. +// + +import SwiftUI + +/// A collection that stores elements and allows for a single element to be selected. +/// +/// `SelectableArray` is a custom data structure that behaves like a standard array with additional functionality +/// to manage a selection state. It conforms to `RandomAccessCollection`, `MutableCollection`, and `BidirectionalCollection`, +/// which allows it to be used similarly to an array. Additionally, it conforms to `ExpressibleByArrayLiteral` and +/// `DynamicProperty`, making it suitable for use in SwiftUI environments. +/// +/// The selection is managed as a state using SwiftUI's `@State` property wrapper, enabling the `SelectableArray` to be +/// used within SwiftUI views and have its state managed automatically by the SwiftUI framework. +public struct SelectableArray: RandomAccessCollection, MutableCollection, ExpressibleByArrayLiteral, BidirectionalCollection, DynamicProperty { + + /// The current selected element in the array, if any. + @State @MainActor public var selection: Element? + + public var startIndex: Int { elements.startIndex } + public var endIndex: Int { elements.endIndex } + + public subscript(position: Int) -> Element { + get { elements[position] } + set { elements[position] = newValue } + } + + /// Creates an instance initialized with the given elements. + /// + /// - Parameter elements: A variadic list of elements to initialize the collection. + public init(arrayLiteral elements: Element...) { + self.elements = elements + _selection = State(initialValue: elements.first) + } + + /// Creates an instance initialized with the given array of elements. + /// + /// - Parameter elements: An array of elements to initialize the collection. + public init(_ elements: [Element], selection: Element? = nil) { + self.elements = elements + _selection = State(initialValue: selection ?? elements.first) + } + + public init(_ elements: [Element], selectionIndex: Int?) { + self.elements = elements + + _selection = State(initialValue: selectionIndex.flatMap { elements[safe: $0] }) + } + + /// The underlying array of elements in the collection. + public var elements: [Element] +} + +extension SelectableArray where Element : Identifiable { + @MainActor public var selectionIndex: Int? { + elements.firstIndex { $0.id == selection?.id } + } + + @MainActor public var selectionIndexBinding: Binding { + .init(get: { selection }, set: { selection = $0 }) + } + + public init(_ elements: [Element], selection: Element.ID) { + self.elements = elements + _selection = State(initialValue: elements.first { $0.id == selection }) + } +} + +extension SelectableArray: Equatable where Element: Equatable { + public static func == (lhs: SelectableArray, rhs: SelectableArray) -> Bool { + lhs.elements == rhs.elements + } +} + +extension SelectableArray { + public var collection: [Element] { elements } +} + +private extension Collection { + subscript(safe index: Index) -> Element? { + indices.contains(index) ? self[index] : nil + } +} diff --git a/Sources/SwiftUIHelpers/ScrollView/SelectableArrayPageMetadata.swift b/Sources/SwiftUIHelpers/ScrollView/SelectableArrayPageMetadata.swift new file mode 100644 index 0000000..9407522 --- /dev/null +++ b/Sources/SwiftUIHelpers/ScrollView/SelectableArrayPageMetadata.swift @@ -0,0 +1,36 @@ +// +// SelectableArrayPageMetadata.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 28.09.2024. +// + +import SwiftUI + +public struct SelectableArrayPageMetadata: Equatable { + public var index: Int + public var count: Int +} + +extension FormatStyle where Self == SelectableArrayPageMetadata.FormatStyle { + public static var pageMetadata: SelectableArrayPageMetadata.FormatStyle { + .init() + } +} + +extension SelectableArray where Element: Identifiable { + @MainActor public var pageMetadata: SelectableArrayPageMetadata { + .init(index: selectionIndex ?? 0, count: count) + } +} + +extension SelectableArrayPageMetadata { + public struct FormatStyle: SwiftUI.FormatStyle { + public typealias FormatInput = SelectableArrayPageMetadata + public typealias FormatOutput = String + + public func format(_ value: FormatInput) -> FormatOutput { + "\(value.index + 1) / \(value.count)" + } + } +} diff --git a/Sources/SwiftUIHelpers/ScrollView/View+DefaultScrollAnchor.swift b/Sources/SwiftUIHelpers/ScrollView/View+DefaultScrollAnchor.swift new file mode 100644 index 0000000..0adec65 --- /dev/null +++ b/Sources/SwiftUIHelpers/ScrollView/View+DefaultScrollAnchor.swift @@ -0,0 +1,34 @@ +// +// View+defaultScrollAnchor.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 28.09.2024. +// + +import SwiftUI + +extension View { + public func defaultScrollAnchor(index: Int, count: Int) -> some View { + modifier(DefaultScrollAnchorModifier(index: index, count: count)) + } +} + +struct DefaultScrollAnchorModifier: ViewModifier { + var index: Int + var count: Int + + private var defaultScrollAnchor: UnitPoint? { + let index = CGFloat(self.index) + let count = CGFloat(self.count) + let offset: CGFloat = index / (count - 1) + return .init(x: offset, y: 0.5) + } + + func body(content: Content) -> some View { + if #available(iOS 17, macOS 14.4, tvOS 17.4, watchOS 10.4, *) { + content.defaultScrollAnchor(defaultScrollAnchor) + } else { + content + } + } +} diff --git a/Sources/SwiftUIHelpers/ScrollView/View+ScrollPositionSelectable.swift b/Sources/SwiftUIHelpers/ScrollView/View+ScrollPositionSelectable.swift new file mode 100644 index 0000000..5b9c98b --- /dev/null +++ b/Sources/SwiftUIHelpers/ScrollView/View+ScrollPositionSelectable.swift @@ -0,0 +1,47 @@ +// +// View+ScrollPositionSelectable.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 28.09.2024. +// + +import SwiftUI + +extension View { + public func scrollPosition(selectable array: SelectableArray) -> some View where Element: Identifiable { + modifier( + ScrollPositionModifier( + selectable: array, + selection: array.$selection) + ) + } +} + +struct ScrollPositionModifier: ViewModifier { + var selectable: SelectableArray + @Binding var selection: Element? + + @MainActor private var defaultScrollAnchor: UnitPoint? { + let index = CGFloat(selectable.pageMetadata.index) + let count = CGFloat(selectable.pageMetadata.count) + guard count > 1 else { return nil } + + let offset: CGFloat = index / (count - 1) + return .init(x: offset, y: 0.5) + } + + init(selectable: SelectableArray, selection: Binding) { + self.selectable = selectable + self._selection = selection + } + + func body(content: Content) -> some View { + if #available(iOS 17, macOS 14.4, tvOS 17.4, watchOS 10.4, *) { + content + .defaultScrollAnchor(defaultScrollAnchor) + .scrollPosition(id: Binding($selection, key: \.id, in: selectable)) + } else { + content + } + } +} diff --git a/Sources/SwiftUIHelpers/ShapeStyleBuilder/ShapeStyleBuilder.swift b/Sources/SwiftUIHelpers/ShapeStyleBuilder/ShapeStyleBuilder.swift new file mode 100644 index 0000000..cca1654 --- /dev/null +++ b/Sources/SwiftUIHelpers/ShapeStyleBuilder/ShapeStyleBuilder.swift @@ -0,0 +1,26 @@ +// +// Created by Valeriy Malishevskyi on 28.08.2023. +// + +import SwiftUI + +@available(iOS 15.0, *) +@resultBuilder +public struct ShapeStyleBuilder { + + public static func buildBlock(_ components: S...) -> some ShapeStyle { + if let first = components.first { + AnyShapeStyle(first) + } else { + AnyShapeStyle(BackgroundStyle()) + } + } + + public static func buildEither(first component: T) -> AnyShapeStyle { + AnyShapeStyle(component) + } + + public static func buildEither(second component: F) -> AnyShapeStyle { + AnyShapeStyle(component) + } +} diff --git a/Sources/SwiftUIHelpers/StateOrBinding.swift b/Sources/SwiftUIHelpers/StateOrBinding.swift new file mode 100644 index 0000000..fecafd7 --- /dev/null +++ b/Sources/SwiftUIHelpers/StateOrBinding.swift @@ -0,0 +1,52 @@ +// +// Created by Valeriy Malishevskyi on 08.11.2023. +// + +import SwiftUI + +/// A property wrapper that can read and write a value from a wrapped `State` or `Binding`. +@propertyWrapper +@frozen +public enum StateOrBinding: DynamicProperty { + + case state(State) + case binding(Binding) + + public var wrappedValue: Value { + get { + switch self { + case .state(let state): + return state.wrappedValue + case .binding(let binding): + return binding.wrappedValue + } + } + nonmutating set { + switch self { + case .state(let state): + state.wrappedValue = newValue + case .binding(let binding): + binding.wrappedValue = newValue + } + } + } + + public var projectedValue: Binding { + switch self { + case .state(let state): + return state.projectedValue + case .binding(let binding): + return binding + } + } + + @inlinable + public init(_ value: Value) { + self = .state(State(initialValue: value)) + } + + @inlinable + public init(_ binding: Binding) { + self = .binding(binding) + } +} diff --git a/Sources/SwiftUIHelpers/StatefulContainer.swift b/Sources/SwiftUIHelpers/StatefulContainer.swift new file mode 100644 index 0000000..948f61a --- /dev/null +++ b/Sources/SwiftUIHelpers/StatefulContainer.swift @@ -0,0 +1,27 @@ +// +// StatefulContainer.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 31.07.2024. +// + +import SwiftUI + +public struct StatefulContainer: View { + @State var value: Value + var content: (Binding) -> Content + + public var body: some View { + content($value) + } + + public init(_ value: Value, @ViewBuilder content: @escaping (Binding) -> Content) { + self._value = State(wrappedValue: value) + self.content = content + } + + public init(_ value: () -> Value, @ViewBuilder content: @escaping (Binding) -> Content) { + self._value = State(wrappedValue: value()) + self.content = content + } +} diff --git a/Sources/SwiftUIHelpers/UIColor+HexContainer.swift b/Sources/SwiftUIHelpers/UIColor+HexContainer.swift new file mode 100644 index 0000000..c7a2c6d --- /dev/null +++ b/Sources/SwiftUIHelpers/UIColor+HexContainer.swift @@ -0,0 +1,15 @@ +// +// UIColor+HexContainer.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 05.08.2024. +// + +import SwiftUI +import SwiftHelpers + +extension UIColor { + public convenience init(_ container: HexColorContainer) { + self.init(red: container.red, green: container.green, blue: container.blue, alpha: CGFloat(1.0)) + } +} diff --git a/Sources/SwiftUIHelpers/View+DynamicTypeSizeHidden.swift b/Sources/SwiftUIHelpers/View+DynamicTypeSizeHidden.swift new file mode 100644 index 0000000..8cc0ce8 --- /dev/null +++ b/Sources/SwiftUIHelpers/View+DynamicTypeSizeHidden.swift @@ -0,0 +1,27 @@ +// +// View+DynamicTypeSizeHidden.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 10.04.2025. +// + +import SwiftUI + +extension View { + public func hidden(for dynamicTypeSizeRange: T) -> some View + where T : RangeExpression, T.Bound == DynamicTypeSize { + let modifier = DynamicTypeSizeHiddenModifier(categories: dynamicTypeSizeRange) + return self.modifier(modifier) + } +} + +struct DynamicTypeSizeHiddenModifier: ViewModifier { + @Environment(\.dynamicTypeSize) private var dynamicTypeSize + + let categories: any RangeExpression + + func body(content: Content) -> some View { + content + .hidden(categories.contains(dynamicTypeSize), remove: true) + } +} diff --git a/Sources/SwiftUIHelpers/View+OnTouchDownGesture.swift b/Sources/SwiftUIHelpers/View+OnTouchDownGesture.swift new file mode 100644 index 0000000..ed788d5 --- /dev/null +++ b/Sources/SwiftUIHelpers/View+OnTouchDownGesture.swift @@ -0,0 +1,36 @@ +// +// View+OnTouchDownGesture.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 02.08.2024. +// + +import SwiftUI + +extension View { + public func onTouchDownGesture(callback: @escaping (Bool) -> Void) -> some View { + modifier(OnTouchDownGestureModifier(callback: callback)) + } +} + +private struct OnTouchDownGestureModifier: ViewModifier { + @State private var tapped = false + let callback: (Bool) -> Void + + func body(content: Content) -> some View { + content + .simultaneousGesture(DragGesture(minimumDistance: 0) + .onChanged { _ in + if !self.tapped { + self.tapped = true + self.callback(true) + } + } + .onEnded { _ in + if self.tapped { + self.callback(false) + self.tapped = false + } + }) + } +} diff --git a/Sources/SwiftUIHelpers/View+Position.swift b/Sources/SwiftUIHelpers/View+Position.swift new file mode 100644 index 0000000..7ba8139 --- /dev/null +++ b/Sources/SwiftUIHelpers/View+Position.swift @@ -0,0 +1,43 @@ +// +// View+Position.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 31.07.2024. +// + +import SwiftUI + +extension View { + public func position( + point: UnitPoint, + in coordinateSpace: CoordinateSpace + ) -> some View { + let modifier = AnchorPointPositionModifier( + unitPoint: point, + coordinateSpace: coordinateSpace + ) + + return self.modifier(modifier) + } +} + +private struct AnchorPointPositionModifier: ViewModifier { + let unitPoint: UnitPoint + let coordinateSpace: CoordinateSpace + + func body(content: Content) -> some View { + content.geometryEffect { content, proxy in + let size = proxy.frame(in: coordinateSpace) + + let xOffset = (unitPoint.x - 0.5) * size.origin.x * 2 + let yOffset = (unitPoint.y - 0.5) * size.origin.y * 2 + + let projectedPoint = CGPoint( + x: xOffset + (size.width / 2), + y: yOffset + (size.height / 2) + ) + + content.position(projectedPoint) + } + } +} diff --git a/Sources/SwiftUIHelpers/View+SoftwareKeyboard.swift b/Sources/SwiftUIHelpers/View+SoftwareKeyboard.swift new file mode 100644 index 0000000..2eecb9e --- /dev/null +++ b/Sources/SwiftUIHelpers/View+SoftwareKeyboard.swift @@ -0,0 +1,37 @@ +// +// Created by Valeriy Malishevskyi on 20.01.2024. +// + +import SwiftUI + +extension View { + /// Modifies the view to trigger a software keyboard on appearance. + /// + /// This method is used to enforce the usage of the software keyboard instead of a hardware keyboard. + /// It's particularly useful in scenarios where the app is running in an environment where both hardware + /// and software keyboards are available, such as on iPad with an external keyboard connected. + /// + /// The method works by iterating over all active input modes (like different language keyboards) + /// and sending a `setHardwareLayout:` message to each, instructing them to use the software layout. + /// + /// - Returns: A view that triggers the switch to a software keyboard when it appears. + /// + /// Usage: + /// ``` + /// TextField("Hello, World!") + /// .softwareKeyboard() // Enforces software keyboard on this TextField view when it appears + /// ``` + /// + /// - Warning: This method uses private APIs (`NSSelectorFromString` and `setHardwareLayout:`) which + /// can lead to app rejection if used in apps submitted to the App Store. It's recommended to use this + /// for internal or testing purposes only. + public func softwareKeyboard() -> some View { + onAppear { + let setHardwareLayout = NSSelectorFromString("setHardwareLayout:") + + UITextInputMode.activeInputModes + .filter({ $0.responds(to: setHardwareLayout) }) + .forEach { $0.perform(setHardwareLayout, with: nil) } + } + } +} diff --git a/Tests/SwiftUIExtensionsTests/BindingTests.swift b/Tests/SwiftUIExtensionsTests/BindingTests.swift new file mode 100644 index 0000000..5c285de --- /dev/null +++ b/Tests/SwiftUIExtensionsTests/BindingTests.swift @@ -0,0 +1,65 @@ +// +// BindingTests.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 03.05.2025. +// + +import SwiftUI +import Testing +@testable import SwiftUIExtensions + +@MainActor struct BindingTests { + @Test func bindingKeyPath() { + struct Item: Identifiable, Equatable { + let id: Int + let name: String + } + + let items = [ + Item(id: 1, name: "One"), + Item(id: 2, name: "Two"), + Item(id: 3, name: "Three") + ] + + var selected: Item? = items[0] + let selectedBinding = Binding( + get: { selected }, + set: { selected = $0 } + ) + + let idBinding = Binding( + selectedBinding, + key: \.id, + in: items + ) + #expect(idBinding.wrappedValue == 1) + + idBinding.wrappedValue = 2 + #expect(selected == items[1]) + + selected = nil + #expect(idBinding.wrappedValue == nil) + } + + @Test func testBindingNilCoalescingOperator() { + var value: Int? = nil + let optionalBinding = Binding( + get: { value }, + set: { value = $0 } + ) + let nonOptionalBinding = optionalBinding ?? 42 + + // Should return the default value when nil + #expect(nonOptionalBinding.wrappedValue == 42) + + // Setting the non-optional binding updates the original value + nonOptionalBinding.wrappedValue = 100 + #expect(value == 100) + #expect(nonOptionalBinding.wrappedValue == 100) + + // Setting the original value to nil again + value = nil + #expect(nonOptionalBinding.wrappedValue == 42) + } +} diff --git a/Tests/SwiftUIExtensionsTests/ColorAccessibleFontColorTests.swift b/Tests/SwiftUIExtensionsTests/ColorAccessibleFontColorTests.swift new file mode 100644 index 0000000..1bad39f --- /dev/null +++ b/Tests/SwiftUIExtensionsTests/ColorAccessibleFontColorTests.swift @@ -0,0 +1,21 @@ +import SwiftUI +import Testing +@testable import SwiftUIExtensions + +struct ColorAccessibleFontColorTests { + @Test func testAccessibleFontColorReturnsBlackForLightColors() { + let lightColor = Color(red: 0.9, green: 0.9, blue: 0.9) + #expect(lightColor.accessibleFontColor == .black) + } + + @Test func testAccessibleFontColorReturnsWhiteForDarkColors() { + let darkColor = Color(red: 0.1, green: 0.1, blue: 0.1) + #expect(darkColor.accessibleFontColor == .white) + } + + @Test func testAccessibleFontColorEdgeCase() { + let midColor = Color(red: 0.7, green: 0.7, blue: 0.6) + // According to the isLightColor logic, this should be black + #expect(midColor.accessibleFontColor == .black) + } +} From b2fa99e69faff1f809b14d10bfd7c48525d0553f Mon Sep 17 00:00:00 2001 From: Valeriy Malishevskyi Date: Thu, 8 May 2025 21:07:11 +0300 Subject: [PATCH 2/6] Create tests.yml --- .github/workflows/tests.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..37f769c --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,24 @@ +# This workflow will build a Swift project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift + +name: Test + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: macos-15 + + steps: + - name: Select Xcode 16.3 + run: sudo xcode-select -s /Applications/Xcode_16.3.app + - uses: actions/checkout@v4 + - name: Build + run: swift build -v + - name: Run tests + run: swift test -v From 3f6b634e34440bc4c238373a87972fac07070984 Mon Sep 17 00:00:00 2001 From: Valeriy Malishevskyi Date: Thu, 8 May 2025 21:22:50 +0300 Subject: [PATCH 3/6] update for macOS --- Package.resolved | 15 +++ Package.swift | 6 +- .../SwiftUIExtensions/Binding+Equals.swift | 6 +- .../Color+AccessibleFontColor.swift | 8 +- .../UIFont.TextStyle+Font.TextStyle.swift | 2 + .../Font/UIFont.Weight+Font.Weight.swift | 2 + ...tDescriptor.SystemDesign+Font.Design.swift | 2 + .../UIColor+ColorScheme.swift | 2 + .../View+EmbedInNavigation.swift | 3 +- .../FontModifier/FontModifier.swift | 12 --- .../StaticFontModifier/BoldModifier.swift | 16 --- .../StaticFontModifier/ItalicModifier.swift | 16 --- .../StaticFontModifier.swift | 10 -- .../FontModifier/WeightModifier.swift | 16 --- .../FontProvider/FontProvider.swift | 18 ---- .../Providers/ModifierProvider.swift | 21 ---- .../Providers/NamedProvider.swift | 32 ------ .../Providers/StaticModifierProvider.swift | 20 ---- .../Providers/SystemProvider.swift | 26 ----- .../Providers/TextStyleProvider.swift | 28 ----- .../FontProvider/UIFont+Font.swift | 101 ------------------ .../View+ScrollPositionSelectable.swift | 1 + .../SwiftUIHelpers/UIColor+HexContainer.swift | 2 + .../View+DynamicTypeSizeHidden.swift | 2 + .../View+SoftwareKeyboard.swift | 3 +- 25 files changed, 45 insertions(+), 325 deletions(-) create mode 100644 Package.resolved delete mode 100644 Sources/SwiftUIHelpers/FontProvider/FontModifier/FontModifier.swift delete mode 100644 Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/BoldModifier.swift delete mode 100644 Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/ItalicModifier.swift delete mode 100644 Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/StaticFontModifier.swift delete mode 100644 Sources/SwiftUIHelpers/FontProvider/FontModifier/WeightModifier.swift delete mode 100644 Sources/SwiftUIHelpers/FontProvider/FontProvider.swift delete mode 100644 Sources/SwiftUIHelpers/FontProvider/Providers/ModifierProvider.swift delete mode 100644 Sources/SwiftUIHelpers/FontProvider/Providers/NamedProvider.swift delete mode 100644 Sources/SwiftUIHelpers/FontProvider/Providers/StaticModifierProvider.swift delete mode 100644 Sources/SwiftUIHelpers/FontProvider/Providers/SystemProvider.swift delete mode 100644 Sources/SwiftUIHelpers/FontProvider/Providers/TextStyleProvider.swift delete mode 100644 Sources/SwiftUIHelpers/FontProvider/UIFont+Font.swift diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..be494c6 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "c5fb95fdff90437c4333c655d0426a5868671837302528fd1d4aa634d3d270f7", + "pins" : [ + { + "identity" : "swifthelpers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stalkermv/SwiftHelpers.git", + "state" : { + "revision" : "b66dc865eae22bdf7149cfb0e59fbcfd059b719e", + "version" : "1.0.2" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift index 2f99792..0a81263 100644 --- a/Package.swift +++ b/Package.swift @@ -11,6 +11,7 @@ let package = Package( .library(name: "SwiftUIHelpers", targets: ["SwiftUIHelpers"]), ], dependencies: [ + .package(url: "https://github.com/stalkermv/SwiftHelpers.git", from: "1.0.0") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. @@ -19,11 +20,14 @@ let package = Package( name: "SwiftUIHelpers", dependencies: [ "SwiftUIExtensions", + .product(name: "SwiftHelpers", package: "SwiftHelpers") ] ), .target( name: "SwiftUIExtensions", - dependencies: [] + dependencies: [ + .product(name: "SwiftHelpers", package: "SwiftHelpers") + ] ), .testTarget( name: "SwiftUIExtensionsTests", diff --git a/Sources/SwiftUIExtensions/Binding+Equals.swift b/Sources/SwiftUIExtensions/Binding+Equals.swift index da1c99e..7efe26c 100644 --- a/Sources/SwiftUIExtensions/Binding+Equals.swift +++ b/Sources/SwiftUIExtensions/Binding+Equals.swift @@ -4,8 +4,6 @@ // Created by Valeriy Malishevskyi on 21.08.2023. // -#if canImport(FoundationExtensions) - import SwiftUI import FoundationExtensions @@ -39,7 +37,7 @@ public extension Binding { /// - Parameters: /// - binding: A binding to a hashable value. /// - value: The value to compare against the hashable value. - init(_ binding: Binding, equals value: ScopeValue) + init(_ binding: Binding, equals value: ScopeValue) where ScopeValue : Hashable, ScopeValue : OptionalProtocol, Value == Bool { self.init { return binding.wrappedValue == value @@ -52,5 +50,3 @@ public extension Binding { } } } - -#endif diff --git a/Sources/SwiftUIExtensions/Color+AccessibleFontColor.swift b/Sources/SwiftUIExtensions/Color+AccessibleFontColor.swift index ccded41..1bc7acf 100644 --- a/Sources/SwiftUIExtensions/Color+AccessibleFontColor.swift +++ b/Sources/SwiftUIExtensions/Color+AccessibleFontColor.swift @@ -4,13 +4,19 @@ import SwiftUI +#if canImport(UIKit) +typealias PlatformColor = UIColor +#else +typealias PlatformColor = NSColor +#endif + public extension Color { /// This color is either black or white, whichever is more accessible when viewed against the scrum color. var accessibleFontColor: Color { var red: CGFloat = 0 var green: CGFloat = 0 var blue: CGFloat = 0 - UIColor(self).getRed(&red, green: &green, blue: &blue, alpha: nil) + PlatformColor(self).getRed(&red, green: &green, blue: &blue, alpha: nil) return isLightColor(red: red, green: green, blue: blue) ? .black : .white } diff --git a/Sources/SwiftUIExtensions/Font/UIFont.TextStyle+Font.TextStyle.swift b/Sources/SwiftUIExtensions/Font/UIFont.TextStyle+Font.TextStyle.swift index 2052668..7576f76 100644 --- a/Sources/SwiftUIExtensions/Font/UIFont.TextStyle+Font.TextStyle.swift +++ b/Sources/SwiftUIExtensions/Font/UIFont.TextStyle+Font.TextStyle.swift @@ -5,6 +5,7 @@ // Created by Valeriy Malishevskyi on 12.07.2024. // +#if canImport(UIKit) import SwiftUI extension UIFont.TextStyle { @@ -37,3 +38,4 @@ extension UIFont.TextStyle { } } } +#endif diff --git a/Sources/SwiftUIExtensions/Font/UIFont.Weight+Font.Weight.swift b/Sources/SwiftUIExtensions/Font/UIFont.Weight+Font.Weight.swift index 87bd797..53e4d4a 100644 --- a/Sources/SwiftUIExtensions/Font/UIFont.Weight+Font.Weight.swift +++ b/Sources/SwiftUIExtensions/Font/UIFont.Weight+Font.Weight.swift @@ -5,6 +5,7 @@ // Created by Valeriy Malishevskyi on 12.07.2024. // +#if canImport(UIKit) import SwiftUI extension UIFont.Weight { @@ -41,3 +42,4 @@ extension Font.Weight { return value } } +#endif diff --git a/Sources/SwiftUIExtensions/Font/UIFontDescriptor.SystemDesign+Font.Design.swift b/Sources/SwiftUIExtensions/Font/UIFontDescriptor.SystemDesign+Font.Design.swift index e7d2860..27c65a9 100644 --- a/Sources/SwiftUIExtensions/Font/UIFontDescriptor.SystemDesign+Font.Design.swift +++ b/Sources/SwiftUIExtensions/Font/UIFontDescriptor.SystemDesign+Font.Design.swift @@ -5,6 +5,7 @@ // Created by Valeriy Malishevskyi on 12.07.2024. // +#if canImport(UIKit) import SwiftUI extension UIFontDescriptor.SystemDesign { @@ -23,3 +24,4 @@ extension UIFontDescriptor.SystemDesign { } } } +#endif diff --git a/Sources/SwiftUIExtensions/UIColor+ColorScheme.swift b/Sources/SwiftUIExtensions/UIColor+ColorScheme.swift index f5ce0b9..b315bd1 100644 --- a/Sources/SwiftUIExtensions/UIColor+ColorScheme.swift +++ b/Sources/SwiftUIExtensions/UIColor+ColorScheme.swift @@ -5,6 +5,7 @@ // Created by Valeriy Malishevskyi on 05.08.2024. // +#if canImport(UIKit) import SwiftUI extension UIColor { @@ -38,3 +39,4 @@ extension UIColor { } } } +#endif diff --git a/Sources/SwiftUIExtensions/View+EmbedInNavigation.swift b/Sources/SwiftUIExtensions/View+EmbedInNavigation.swift index 09faaac..eca27f7 100644 --- a/Sources/SwiftUIExtensions/View+EmbedInNavigation.swift +++ b/Sources/SwiftUIExtensions/View+EmbedInNavigation.swift @@ -2,6 +2,7 @@ // Created by Valeriy Malishevskyi on 06.05.2023. // +#if canImport(UIKit) import SwiftUI public extension View { @@ -14,4 +15,4 @@ public extension View { NavigationStack { self.navigationBarTitleDisplayMode(titleDisplayMode) } } } - +#endif diff --git a/Sources/SwiftUIHelpers/FontProvider/FontModifier/FontModifier.swift b/Sources/SwiftUIHelpers/FontProvider/FontModifier/FontModifier.swift deleted file mode 100644 index 40a5b85..0000000 --- a/Sources/SwiftUIHelpers/FontProvider/FontModifier/FontModifier.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// FontModifier.swift -// SwiftUIHelpers -// -// Created by Valeriy Malishevskyi on 12.07.2024. -// - -import UIKit - -protocol FontModifier { - func modify(_ fontDescriptor: inout UIFontDescriptor) -} diff --git a/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/BoldModifier.swift b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/BoldModifier.swift deleted file mode 100644 index c358312..0000000 --- a/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/BoldModifier.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// BoldModifier.swift -// SwiftUIHelpers -// -// Created by Valeriy Malishevskyi on 12.07.2024. -// - -import UIKit - -struct BoldModifier: StaticFontModifier { - init() {} - - func modify(_ fontDescriptor: inout UIFontDescriptor) { - fontDescriptor = fontDescriptor.withSymbolicTraits(.traitBold) ?? fontDescriptor - } -} diff --git a/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/ItalicModifier.swift b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/ItalicModifier.swift deleted file mode 100644 index f67c6dc..0000000 --- a/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/ItalicModifier.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// ItalicModifier.swift -// SwiftUIHelpers -// -// Created by Valeriy Malishevskyi on 12.07.2024. -// - -import UIKit - -struct ItalicModifier: StaticFontModifier { - init() {} - - func modify(_ fontDescriptor: inout UIFontDescriptor) { - fontDescriptor = fontDescriptor.withSymbolicTraits(.traitItalic) ?? fontDescriptor - } -} diff --git a/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/StaticFontModifier.swift b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/StaticFontModifier.swift deleted file mode 100644 index 3e32122..0000000 --- a/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/StaticFontModifier.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// StaticFontModifier.swift -// SwiftUIHelpers -// -// Created by Valeriy Malishevskyi on 12.07.2024. -// - -protocol StaticFontModifier: FontModifier { - init() -} diff --git a/Sources/SwiftUIHelpers/FontProvider/FontModifier/WeightModifier.swift b/Sources/SwiftUIHelpers/FontProvider/FontModifier/WeightModifier.swift deleted file mode 100644 index fbac8b0..0000000 --- a/Sources/SwiftUIHelpers/FontProvider/FontModifier/WeightModifier.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// WeightModifier.swift -// SwiftUIHelpers -// -// Created by Valeriy Malishevskyi on 12.07.2024. -// - -import UIKit - -struct WeightModifier: FontModifier { - let weight: UIFont.Weight - - func modify(_ fontDescriptor: inout UIFontDescriptor) { - fontDescriptor = fontDescriptor.addingAttributes([.traits: [UIFontDescriptor.TraitKey.weight: weight]]) - } -} diff --git a/Sources/SwiftUIHelpers/FontProvider/FontProvider.swift b/Sources/SwiftUIHelpers/FontProvider/FontProvider.swift deleted file mode 100644 index 7f9049f..0000000 --- a/Sources/SwiftUIHelpers/FontProvider/FontProvider.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// FontProvider.swift -// SwiftUIHelpers -// -// Created by Valeriy Malishevskyi on 12.07.2024. -// - -import UIKit - -protocol FontProvider { - func fontDescriptor(with traitCollection: UITraitCollection?) -> UIFontDescriptor -} - -extension FontProvider { - func font(with traitCollection: UITraitCollection?) -> UIFont { - UIFont(descriptor: fontDescriptor(with: traitCollection), size: 0) - } -} diff --git a/Sources/SwiftUIHelpers/FontProvider/Providers/ModifierProvider.swift b/Sources/SwiftUIHelpers/FontProvider/Providers/ModifierProvider.swift deleted file mode 100644 index 4dd6cc9..0000000 --- a/Sources/SwiftUIHelpers/FontProvider/Providers/ModifierProvider.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// ModifierProvider.swift -// SwiftUIHelpers -// -// Created by Valeriy Malishevskyi on 12.07.2024. -// - -import SwiftUI - -struct ModifierProvider: FontProvider { - let base: FontProvider - let modifier: FontModifier - - func fontDescriptor(with traitCollection: UITraitCollection?) -> UIFontDescriptor { - var descriptor = base.fontDescriptor(with: traitCollection) - - modifier.modify(&descriptor) - - return descriptor - } -} diff --git a/Sources/SwiftUIHelpers/FontProvider/Providers/NamedProvider.swift b/Sources/SwiftUIHelpers/FontProvider/Providers/NamedProvider.swift deleted file mode 100644 index 759d789..0000000 --- a/Sources/SwiftUIHelpers/FontProvider/Providers/NamedProvider.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// NamedProvider.swift -// SwiftUIHelpers -// -// Created by Valeriy Malishevskyi on 12.07.2024. -// - -import UIKit - -struct NamedProvider: FontProvider { - var name: String - - var size: CGFloat - - var textStyle: UIFont.TextStyle? - - func fontDescriptor(with traitCollection: UITraitCollection?) -> UIFontDescriptor { - if let textStyle = textStyle { - let metrics = UIFontMetrics(forTextStyle: textStyle) - - return UIFontDescriptor(fontAttributes: [ - .family: name, - .size: metrics.scaledValue(for: size, compatibleWith: traitCollection) - ]) - } else { - return UIFontDescriptor(fontAttributes: [ - .family: name, - .size: size - ]) - } - } -} diff --git a/Sources/SwiftUIHelpers/FontProvider/Providers/StaticModifierProvider.swift b/Sources/SwiftUIHelpers/FontProvider/Providers/StaticModifierProvider.swift deleted file mode 100644 index 93e1503..0000000 --- a/Sources/SwiftUIHelpers/FontProvider/Providers/StaticModifierProvider.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// StaticModifierProvider.swift -// SwiftUIHelpers -// -// Created by Valeriy Malishevskyi on 12.07.2024. -// - -import SwiftUI - -struct StaticModifierProvider: FontProvider { - var base: FontProvider - - func fontDescriptor(with traitCollection: UITraitCollection?) -> UIFontDescriptor { - var descriptor = base.fontDescriptor(with: traitCollection) - - M().modify(&descriptor) - - return descriptor - } -} diff --git a/Sources/SwiftUIHelpers/FontProvider/Providers/SystemProvider.swift b/Sources/SwiftUIHelpers/FontProvider/Providers/SystemProvider.swift deleted file mode 100644 index 322497f..0000000 --- a/Sources/SwiftUIHelpers/FontProvider/Providers/SystemProvider.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// SystemProvider.swift -// SwiftUIHelpers -// -// Created by Valeriy Malishevskyi on 12.07.2024. -// - -import SwiftUI - -struct SystemProvider: FontProvider { - var size: CGFloat - - var design: UIFontDescriptor.SystemDesign - - var weight: UIFont.Weight? - - func fontDescriptor(with traitCollection: UITraitCollection?) -> UIFontDescriptor { - UIFont - .preferredFont(forTextStyle: .body, compatibleWith: traitCollection) - .fontDescriptor - .withDesign(design)! - .addingAttributes([ - .size: size - ]) - } -} diff --git a/Sources/SwiftUIHelpers/FontProvider/Providers/TextStyleProvider.swift b/Sources/SwiftUIHelpers/FontProvider/Providers/TextStyleProvider.swift deleted file mode 100644 index e6312f6..0000000 --- a/Sources/SwiftUIHelpers/FontProvider/Providers/TextStyleProvider.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// TextStyleProvider.swift -// SwiftUIHelpers -// -// Created by Valeriy Malishevskyi on 12.07.2024. -// - -import UIKit - -struct TextStyleProvider: FontProvider { - func fontDescriptor(with traitCollection: UITraitCollection?) -> UIFontDescriptor { - let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style) - - if let design = design { - descriptor.withDesign(design) - } - - if let weight = weight { - descriptor.withSymbolicTraits(.traitBold) - } - - return descriptor - } - - let style: UIFont.TextStyle - let design: UIFontDescriptor.SystemDesign? - let weight: UIFont.Weight? -} diff --git a/Sources/SwiftUIHelpers/FontProvider/UIFont+Font.swift b/Sources/SwiftUIHelpers/FontProvider/UIFont+Font.swift deleted file mode 100644 index 2e14620..0000000 --- a/Sources/SwiftUIHelpers/FontProvider/UIFont+Font.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// UIFont+Font.swift -// SwiftUIHelpers -// -// Created by Valeriy Malishevskyi on 12.07.2024. -// - -import SwiftUI -import SwiftUIExtensions -import UIKit - -extension UIFont { - public static func resolving(font: Font, dynamicTypeSize: DynamicTypeSize = .medium) -> UIFont { - let traits = UITraitCollection(traitsFrom: [ - .init(preferredContentSizeCategory: UIContentSizeCategory(dynamicTypeSize)) - ]) - - if let provider = font.getFontProvider() { - return provider.font(with: traits) - } - - return .preferredFont(forTextStyle: .body) - } -} - -extension Font { - func getFontProvider() -> FontProvider? { - let mirror = Mirror(reflecting: self) - - guard let provider = mirror.descendant("provider", "base") else { - return nil - } - - return resolveFontProvider(provider) - } - - private func resolveFontProvider(_ provider: Any) -> FontProvider? { - let mirror = Mirror(reflecting: provider) - - switch String(describing: type(of: provider)) { - case "StaticModifierProvider": - guard let base = mirror.descendant("base", "provider", "base") else { - return nil - } - - return resolveFontProvider(base).map(StaticModifierProvider.init) - case "StaticModifierProvider": - guard let base = mirror.descendant("base", "provider", "base") else { - return nil - } - - return resolveFontProvider(base).map(StaticModifierProvider.init) - case "SystemProvider": - guard let size = mirror.descendant("size") as? CGFloat, - let design = mirror.descendant("design") as? Font.Design else { - return nil - } - - let weight = mirror.descendant("weight") as? Font.Weight - - return SystemProvider(size: size, design: .init(design), weight: weight.map(UIFont.Weight.init)) - case "NamedProvider": - guard let name = mirror.descendant("name") as? String, - let size = mirror.descendant("size") as? CGFloat else { - return nil - } - - let textStyle = mirror.descendant("textStyle") as? Font.TextStyle - - return NamedProvider(name: name, size: size, textStyle: textStyle.map(UIFont.TextStyle.init)) - case "TextStyleProvider": - guard let style = mirror.descendant("style") as? Font.TextStyle else { - return nil - } - - let design = mirror.descendant("design") as? Font.Design - let weight = mirror.descendant("weight") as? Font.Weight - - return TextStyleProvider( - style: .init(style), - design: design.map(UIFontDescriptor.SystemDesign.init), - weight: weight.map(UIFont.Weight.init) - ) - case "ModifierProvider": - guard let base = mirror.descendant("base") as? Font, - let provider = base.getFontProvider() else { - return nil - } - - let weight = mirror.descendant("modifier", "weight") as! Font.Weight - let weightModifier = WeightModifier(weight: .init(weight)) - - return ModifierProvider.init(base: provider, modifier: weightModifier) - default: - // Not exhaustive, more providers need to be handled here. - - print("Unknown font provider: \(provider)") - return nil - } - } -} diff --git a/Sources/SwiftUIHelpers/ScrollView/View+ScrollPositionSelectable.swift b/Sources/SwiftUIHelpers/ScrollView/View+ScrollPositionSelectable.swift index 5b9c98b..1ddd39f 100644 --- a/Sources/SwiftUIHelpers/ScrollView/View+ScrollPositionSelectable.swift +++ b/Sources/SwiftUIHelpers/ScrollView/View+ScrollPositionSelectable.swift @@ -6,6 +6,7 @@ // import SwiftUI +import SwiftUIExtensions extension View { public func scrollPosition(selectable array: SelectableArray) -> some View where Element: Identifiable { diff --git a/Sources/SwiftUIHelpers/UIColor+HexContainer.swift b/Sources/SwiftUIHelpers/UIColor+HexContainer.swift index c7a2c6d..3079b25 100644 --- a/Sources/SwiftUIHelpers/UIColor+HexContainer.swift +++ b/Sources/SwiftUIHelpers/UIColor+HexContainer.swift @@ -5,6 +5,7 @@ // Created by Valeriy Malishevskyi on 05.08.2024. // +#if canImport(UIKit) import SwiftUI import SwiftHelpers @@ -13,3 +14,4 @@ extension UIColor { self.init(red: container.red, green: container.green, blue: container.blue, alpha: CGFloat(1.0)) } } +#endif diff --git a/Sources/SwiftUIHelpers/View+DynamicTypeSizeHidden.swift b/Sources/SwiftUIHelpers/View+DynamicTypeSizeHidden.swift index 8cc0ce8..0ab7f7e 100644 --- a/Sources/SwiftUIHelpers/View+DynamicTypeSizeHidden.swift +++ b/Sources/SwiftUIHelpers/View+DynamicTypeSizeHidden.swift @@ -5,6 +5,7 @@ // Created by Valeriy Malishevskyi on 10.04.2025. // +#if canImport(UIKit) import SwiftUI extension View { @@ -25,3 +26,4 @@ struct DynamicTypeSizeHiddenModifier: ViewModifier { .hidden(categories.contains(dynamicTypeSize), remove: true) } } +#endif diff --git a/Sources/SwiftUIHelpers/View+SoftwareKeyboard.swift b/Sources/SwiftUIHelpers/View+SoftwareKeyboard.swift index 2eecb9e..2299605 100644 --- a/Sources/SwiftUIHelpers/View+SoftwareKeyboard.swift +++ b/Sources/SwiftUIHelpers/View+SoftwareKeyboard.swift @@ -1,7 +1,7 @@ // // Created by Valeriy Malishevskyi on 20.01.2024. // - +#if canImport(UIKit) import SwiftUI extension View { @@ -35,3 +35,4 @@ extension View { } } } +#endif From cd6b7c1d7333fe312e3dfe99fdaa357c0646680c Mon Sep 17 00:00:00 2001 From: Valeriy Malishevskyi Date: Thu, 8 May 2025 21:24:29 +0300 Subject: [PATCH 4/6] remove warnings --- Sources/SwiftUIExtensions/Binding+Boolean.swift | 4 ++-- .../SwiftUIHelpers/GeometryEffect/GeometryEffectView.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftUIExtensions/Binding+Boolean.swift b/Sources/SwiftUIExtensions/Binding+Boolean.swift index 8914689..a7001b5 100644 --- a/Sources/SwiftUIExtensions/Binding+Boolean.swift +++ b/Sources/SwiftUIExtensions/Binding+Boolean.swift @@ -26,7 +26,7 @@ public extension Binding where Value : OptionalProtocol { /// ``` /// /// - Note: Setting the boolean binding to `true` will not modify the original binding. You should handle such cases separately. - var boolean: Binding { + @MainActor var boolean: Binding { Binding { !wrappedValue.isNone } set: { newValue in @@ -35,4 +35,4 @@ public extension Binding where Value : OptionalProtocol { } } -#endif \ No newline at end of file +#endif diff --git a/Sources/SwiftUIHelpers/GeometryEffect/GeometryEffectView.swift b/Sources/SwiftUIHelpers/GeometryEffect/GeometryEffectView.swift index 7c857e0..7fba9ed 100644 --- a/Sources/SwiftUIHelpers/GeometryEffect/GeometryEffectView.swift +++ b/Sources/SwiftUIHelpers/GeometryEffect/GeometryEffectView.swift @@ -84,7 +84,7 @@ struct GeometryProxyWrapper: ViewModifier { } return result - case .named(let name): + case .named(_): guard let result = proxy?.proxy.frame(in: coordinateSpace) else { return nil } From a4cefd05a9e207b41e61ac72f114ec89aa569ac9 Mon Sep 17 00:00:00 2001 From: Valeriy Malishevskyi Date: Mon, 12 May 2025 18:36:11 +0300 Subject: [PATCH 5/6] add font modifier --- .../FontModifier/FontModifier.swift | 12 ++ .../StaticFontModifier/BoldModifier.swift | 26 ++++ .../StaticFontModifier/DesignModifier.swift | 19 +++ .../StaticFontModifier/ItalicModifier.swift | 26 ++++ .../MonospacedModifier.swift | 16 +++ .../StaticFontModifier/SerifModifier.swift | 16 +++ .../StaticFontModifier.swift | 10 ++ .../FontModifier/WeightModifier.swift | 27 ++++ .../FontProvider/FontProvider.swift | 22 +++ .../FontProvider/PlatformFont+Font.swift | 100 ++++++++++++++ .../FontProvider/PlatformTypes.swift | 130 ++++++++++++++++++ .../Providers/ModifierProvider.swift | 21 +++ .../Providers/NamedProvider.swift | 39 ++++++ .../Providers/StaticModifierProvider.swift | 24 ++++ .../Providers/SystemProvider.swift | 42 ++++++ .../Providers/TextStyleProvider.swift | 51 +++++++ .../SceneExtensions/Scene+OnChange.swift | 8 +- .../SceneExtensions/Scene+OnReceive.swift | 8 +- 18 files changed, 585 insertions(+), 12 deletions(-) create mode 100644 Sources/SwiftUIHelpers/FontProvider/FontModifier/FontModifier.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/BoldModifier.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/DesignModifier.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/ItalicModifier.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/MonospacedModifier.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/SerifModifier.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/StaticFontModifier.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/FontModifier/WeightModifier.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/FontProvider.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/PlatformFont+Font.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/PlatformTypes.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/Providers/ModifierProvider.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/Providers/NamedProvider.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/Providers/StaticModifierProvider.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/Providers/SystemProvider.swift create mode 100644 Sources/SwiftUIHelpers/FontProvider/Providers/TextStyleProvider.swift diff --git a/Sources/SwiftUIHelpers/FontProvider/FontModifier/FontModifier.swift b/Sources/SwiftUIHelpers/FontProvider/FontModifier/FontModifier.swift new file mode 100644 index 0000000..a4e49de --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/FontModifier/FontModifier.swift @@ -0,0 +1,12 @@ +// +// FontModifier.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import SwiftUI + +protocol FontModifier { + func modify(_ fontDescriptor: inout PlatformFontDescriptor) +} diff --git a/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/BoldModifier.swift b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/BoldModifier.swift new file mode 100644 index 0000000..4259202 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/BoldModifier.swift @@ -0,0 +1,26 @@ +// +// BoldModifier.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import SwiftUI + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +struct BoldModifier: StaticFontModifier { + init() {} + + func modify(_ fontDescriptor: inout PlatformFontDescriptor) { + #if canImport(UIKit) + fontDescriptor = fontDescriptor.withSymbolicTraits(.traitBold) ?? fontDescriptor + #else + fontDescriptor = fontDescriptor.withSymbolicTraits(.bold) + #endif + } +} diff --git a/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/DesignModifier.swift b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/DesignModifier.swift new file mode 100644 index 0000000..5a6f851 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/DesignModifier.swift @@ -0,0 +1,19 @@ +import SwiftUI + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +struct DesignModifier: FontModifier { + let design: PlatformSystemDesign + + init(_ design: Font.Design) { + self.design = .init(design) + } + + func modify(_ fontDescriptor: inout PlatformFontDescriptor) { + fontDescriptor = fontDescriptor.withDesign(design) ?? fontDescriptor + } +} \ No newline at end of file diff --git a/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/ItalicModifier.swift b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/ItalicModifier.swift new file mode 100644 index 0000000..770320e --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/ItalicModifier.swift @@ -0,0 +1,26 @@ +// +// ItalicModifier.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import SwiftUI + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +struct ItalicModifier: StaticFontModifier { + init() {} + + func modify(_ fontDescriptor: inout PlatformFontDescriptor) { + #if canImport(UIKit) + fontDescriptor = fontDescriptor.withSymbolicTraits(.traitItalic) ?? fontDescriptor + #else + fontDescriptor = fontDescriptor.withSymbolicTraits(.italic) + #endif + } +} diff --git a/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/MonospacedModifier.swift b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/MonospacedModifier.swift new file mode 100644 index 0000000..528bcf3 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/MonospacedModifier.swift @@ -0,0 +1,16 @@ +import SwiftUI + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +struct MonospacedModifier: StaticFontModifier { + init() {} + + func modify(_ fontDescriptor: inout PlatformFontDescriptor) { + let designModifier = DesignModifier(.monospaced) + designModifier.modify(&fontDescriptor) + } +} \ No newline at end of file diff --git a/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/SerifModifier.swift b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/SerifModifier.swift new file mode 100644 index 0000000..c529cb6 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/SerifModifier.swift @@ -0,0 +1,16 @@ +import SwiftUI + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +struct SerifModifier: StaticFontModifier { + init() {} + + func modify(_ fontDescriptor: inout PlatformFontDescriptor) { + let designModifier = DesignModifier(.serif) + designModifier.modify(&fontDescriptor) + } +} \ No newline at end of file diff --git a/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/StaticFontModifier.swift b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/StaticFontModifier.swift new file mode 100644 index 0000000..3e32122 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/FontModifier/StaticFontModifier/StaticFontModifier.swift @@ -0,0 +1,10 @@ +// +// StaticFontModifier.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +protocol StaticFontModifier: FontModifier { + init() +} diff --git a/Sources/SwiftUIHelpers/FontProvider/FontModifier/WeightModifier.swift b/Sources/SwiftUIHelpers/FontProvider/FontModifier/WeightModifier.swift new file mode 100644 index 0000000..4581cda --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/FontModifier/WeightModifier.swift @@ -0,0 +1,27 @@ +// +// WeightModifier.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import SwiftUI + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +struct WeightModifier: FontModifier { + let weight: PlatformFontWeight + + func modify(_ fontDescriptor: inout PlatformFontDescriptor) { + #if canImport(UIKit) + fontDescriptor = fontDescriptor.addingAttributes([.traits: [UIFontDescriptor.TraitKey.weight: weight]]) + #else + fontDescriptor = fontDescriptor.addingAttributes([.traits: [NSFontDescriptor.TraitKey.weight: weight]]) + #endif + } +} + diff --git a/Sources/SwiftUIHelpers/FontProvider/FontProvider.swift b/Sources/SwiftUIHelpers/FontProvider/FontProvider.swift new file mode 100644 index 0000000..c9dadc9 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/FontProvider.swift @@ -0,0 +1,22 @@ +import SwiftUI + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +protocol FontProvider { + func fontDescriptor(with traitCollection: PlatformTraitCollection?) -> PlatformFontDescriptor + func font(with traitCollection: PlatformTraitCollection?) -> PlatformFont +} + +extension FontProvider { + func font(with traitCollection: PlatformTraitCollection?) -> PlatformFont { + #if canImport(UIKit) + return UIFont(descriptor: fontDescriptor(with: traitCollection), size: 0) + #else + return NSFont(descriptor: fontDescriptor(with: traitCollection), size: 0) ?? .systemFont(ofSize: NSFont.systemFontSize) + #endif + } +} diff --git a/Sources/SwiftUIHelpers/FontProvider/PlatformFont+Font.swift b/Sources/SwiftUIHelpers/FontProvider/PlatformFont+Font.swift new file mode 100644 index 0000000..58c2e15 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/PlatformFont+Font.swift @@ -0,0 +1,100 @@ +import SwiftUI +import SwiftUIExtensions + +extension PlatformFont { + public static func resolving(font: Font, dynamicTypeSize: DynamicTypeSize = .medium) -> PlatformFont { + #if canImport(UIKit) + let traits = PlatformTraitCollection( + traitsFrom: [ + .init(preferredContentSizeCategory: UIContentSizeCategory(dynamicTypeSize)) + + ] + ) + #else + let traits = PlatformTraitCollection(preferredContentSizeCategory: dynamicTypeSize) + #endif + + if let provider = font.getFontProvider() { + return provider.font(with: traits) + } + + #if canImport(UIKit) + return .preferredFont(forTextStyle: .body) + #else + return .systemFont(ofSize: NSFont.systemFontSize) + #endif + } +} + +extension Font { + func getFontProvider() -> FontProvider? { + let mirror = Mirror(reflecting: self) + + guard let provider = mirror.descendant("provider", "base") else { + return nil + } + + return resolveFontProvider(provider) + } + + private func resolveFontProvider(_ provider: Any) -> FontProvider? { + let mirror = Mirror(reflecting: provider) + + switch String(describing: type(of: provider)) { + case "StaticModifierProvider": + guard let base = mirror.descendant("base", "provider", "base") else { + return nil + } + + return resolveFontProvider(base).map(StaticModifierProvider.init) + case "StaticModifierProvider": + guard let base = mirror.descendant("base", "provider", "base") else { + return nil + } + + return resolveFontProvider(base).map(StaticModifierProvider.init) + case "SystemProvider": + guard let size = mirror.descendant("size") as? CGFloat, + let design = mirror.descendant("design") as? Font.Design else { + return nil + } + + let weight = mirror.descendant("weight") as? Font.Weight + return SystemProvider(size: size, design: .init(design), weight: weight.map(PlatformFontWeight.init)) + case "NamedProvider": + guard let name = mirror.descendant("name") as? String, + let size = mirror.descendant("size") as? CGFloat else { + return nil + } + + let textStyle = mirror.descendant("textStyle") as? Font.TextStyle + return NamedProvider(name: name, size: size, textStyle: textStyle.map(PlatformTextStyle.init)) + case "TextStyleProvider": + guard let style = mirror.descendant("style") as? Font.TextStyle else { + return nil + } + + let design = mirror.descendant("design") as? Font.Design + let weight = mirror.descendant("weight") as? Font.Weight + + return TextStyleProvider( + style: .init(style), + design: design.map(PlatformSystemDesign.init), + weight: weight.map(PlatformFontWeight.init) + ) + case "ModifierProvider": + guard let base = mirror.descendant("base") as? Font, + let provider = base.getFontProvider() else { + return nil + } + + let weight = mirror.descendant("modifier", "weight") as! Font.Weight + let weightModifier = WeightModifier(weight: PlatformFontWeight(weight)) + + return ModifierProvider.init(base: provider, modifier: weightModifier) + default: + print("Unknown font provider: \(provider)") + return nil + } + } +} diff --git a/Sources/SwiftUIHelpers/FontProvider/PlatformTypes.swift b/Sources/SwiftUIHelpers/FontProvider/PlatformTypes.swift new file mode 100644 index 0000000..8998a00 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/PlatformTypes.swift @@ -0,0 +1,130 @@ +import SwiftUI + +#if canImport(UIKit) +import UIKit + +public typealias PlatformFont = UIFont +public typealias PlatformFontDescriptor = UIFontDescriptor +public typealias PlatformTextStyle = UIFont.TextStyle +public typealias PlatformSystemDesign = UIFontDescriptor.SystemDesign +public typealias PlatformFontWeight = UIFont.Weight +public typealias PlatformTraitCollection = UITraitCollection +#else +import AppKit + +public typealias PlatformFont = NSFont +public typealias PlatformFontDescriptor = NSFontDescriptor +public typealias PlatformTextStyle = NSFont.TextStyle +public typealias PlatformSystemDesign = NSFontDescriptor.SystemDesign +public typealias PlatformFontWeight = NSFont.Weight + +// A simple trait collection for macOS to maintain API compatibility +public struct MacOSTraitCollection { + let preferredContentSizeCategory: DynamicTypeSize + + init(preferredContentSizeCategory: DynamicTypeSize) { + self.preferredContentSizeCategory = preferredContentSizeCategory + } +} +public typealias PlatformTraitCollection = MacOSTraitCollection +#endif + +extension PlatformTextStyle { + init(_ style: Font.TextStyle) { + #if canImport(UIKit) + switch style { + case .largeTitle: self = .largeTitle + case .title: self = .title1 + case .title2: self = .title2 + case .title3: self = .title3 + case .headline: self = .headline + case .body: self = .body + case .callout: self = .callout + case .subheadline: self = .subheadline + case .footnote: self = .footnote + case .caption: self = .caption1 + case .caption2: self = .caption2 + @unknown default: self = .body + } + #else + switch style { + case .largeTitle: self = .title1 + case .title: self = .title1 + case .title2: self = .title2 + case .title3: self = .title3 + case .headline: self = .headline + case .body: self = .body + case .callout: self = .body + case .subheadline: self = .subheadline + case .footnote: self = .footnote + case .caption: self = .caption1 + case .caption2: self = .caption2 + @unknown default: self = .body + } + #endif + } +} + +extension PlatformFontWeight { + init(_ weight: Font.Weight) { + #if canImport(UIKit) + switch weight { + case .ultraLight: self = .ultraLight + case .thin: self = .thin + case .light: self = .light + case .regular: self = .regular + case .medium: self = .medium + case .semibold: self = .semibold + case .bold: self = .bold + case .heavy: self = .heavy + case .black: self = .black + default: self = weight.rawValue + .map(PlatformFont.Weight.init(rawValue:)) ?? .regular + } + #else + switch weight { + case .ultraLight: self = .ultraLight + case .thin: self = .thin + case .light: self = .light + case .regular: self = .regular + case .medium: self = .medium + case .semibold: self = .semibold + case .bold: self = .bold + case .heavy: self = .heavy + case .black: self = .black + default: self = weight.rawValue + .map(PlatformFont.Weight.init(rawValue:)) ?? .regular + } + #endif + } +} + +extension Font.Weight { + var rawValue: CGFloat? { + let mirror = Mirror(reflecting: self) + let value = mirror.children.first?.value as? CGFloat + return value + } +} + +extension PlatformSystemDesign { + init(_ design: Font.Design) { + #if canImport(UIKit) + switch design { + case .default: self = .default + case .serif: self = .serif + case .rounded: self = .rounded + case .monospaced: self = .monospaced + @unknown default: self = .default + } + #else + switch design { + case .default: self = .default + case .serif: self = .serif + case .rounded: self = .rounded + case .monospaced: self = .monospaced + @unknown default: self = .default + } + #endif + } +} diff --git a/Sources/SwiftUIHelpers/FontProvider/Providers/ModifierProvider.swift b/Sources/SwiftUIHelpers/FontProvider/Providers/ModifierProvider.swift new file mode 100644 index 0000000..d8c5b60 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/Providers/ModifierProvider.swift @@ -0,0 +1,21 @@ +// +// ModifierProvider.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import SwiftUI + +struct ModifierProvider: FontProvider { + let base: FontProvider + let modifier: FontModifier + + func fontDescriptor(with traitCollection: PlatformTraitCollection?) -> PlatformFontDescriptor { + var descriptor = base.fontDescriptor(with: traitCollection) + + modifier.modify(&descriptor) + + return descriptor + } +} diff --git a/Sources/SwiftUIHelpers/FontProvider/Providers/NamedProvider.swift b/Sources/SwiftUIHelpers/FontProvider/Providers/NamedProvider.swift new file mode 100644 index 0000000..1b5c361 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/Providers/NamedProvider.swift @@ -0,0 +1,39 @@ +// +// NamedProvider.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import SwiftUI + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +struct NamedProvider: FontProvider { + var name: String + var size: CGFloat + var textStyle: PlatformTextStyle? + + func fontDescriptor(with traitCollection: PlatformTraitCollection?) -> PlatformFontDescriptor { + let scaledSize: CGFloat + if let textStyle = textStyle { + #if canImport(UIKit) + let metrics = UIFontMetrics(forTextStyle: textStyle) + scaledSize = metrics.scaledValue(for: size, compatibleWith: traitCollection) + #else + scaledSize = size + #endif + } else { + scaledSize = size + } + + return PlatformFontDescriptor(fontAttributes: [ + .family: name, + .size: scaledSize + ]) + } +} diff --git a/Sources/SwiftUIHelpers/FontProvider/Providers/StaticModifierProvider.swift b/Sources/SwiftUIHelpers/FontProvider/Providers/StaticModifierProvider.swift new file mode 100644 index 0000000..e4408c8 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/Providers/StaticModifierProvider.swift @@ -0,0 +1,24 @@ +// +// StaticModifierProvider.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import SwiftUI + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +struct StaticModifierProvider: FontProvider { + var base: FontProvider + + func fontDescriptor(with traitCollection: PlatformTraitCollection?) -> PlatformFontDescriptor { + var descriptor = base.fontDescriptor(with: traitCollection) + M().modify(&descriptor) + return descriptor + } +} diff --git a/Sources/SwiftUIHelpers/FontProvider/Providers/SystemProvider.swift b/Sources/SwiftUIHelpers/FontProvider/Providers/SystemProvider.swift new file mode 100644 index 0000000..ede7e56 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/Providers/SystemProvider.swift @@ -0,0 +1,42 @@ +// +// SystemProvider.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import SwiftUI + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +struct SystemProvider: FontProvider { + var size: CGFloat + var design: PlatformSystemDesign + var weight: PlatformFontWeight? + + func fontDescriptor(with traitCollection: PlatformTraitCollection?) -> PlatformFontDescriptor { + #if canImport(UIKit) + let descriptor = UIFont.preferredFont(forTextStyle: .body, compatibleWith: traitCollection).fontDescriptor + #else + let descriptor = NSFont.systemFont(ofSize: NSFont.systemFontSize).fontDescriptor + #endif + + var attributes: [PlatformFontDescriptor.AttributeName: Any] = [.size: size] + + if let weight = weight { + #if canImport(UIKit) + attributes[.traits] = [UIFontDescriptor.TraitKey.weight: weight] + #else + attributes[.traits] = [NSFontDescriptor.TraitKey.weight: weight] + #endif + } + + return descriptor + .withDesign(design)? + .addingAttributes(attributes) ?? descriptor + } +} diff --git a/Sources/SwiftUIHelpers/FontProvider/Providers/TextStyleProvider.swift b/Sources/SwiftUIHelpers/FontProvider/Providers/TextStyleProvider.swift new file mode 100644 index 0000000..ec54dc7 --- /dev/null +++ b/Sources/SwiftUIHelpers/FontProvider/Providers/TextStyleProvider.swift @@ -0,0 +1,51 @@ +// +// TextStyleProvider.swift +// SwiftUIHelpers +// +// Created by Valeriy Malishevskyi on 12.07.2024. +// + +import SwiftUI + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +struct TextStyleProvider: FontProvider { + let style: PlatformTextStyle + let design: PlatformSystemDesign? + let weight: PlatformFontWeight? + + func fontDescriptor(with traitCollection: PlatformTraitCollection?) -> PlatformFontDescriptor { + #if canImport(UIKit) + var descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style, compatibleWith: traitCollection) + #else + var descriptor = NSFontDescriptor.preferredFontDescriptor(forTextStyle: style) + #endif + + if let design = design { + descriptor = descriptor.withDesign(design) ?? descriptor + } + + if let weight = weight { + var traits: [PlatformFontDescriptor.TraitKey: Any] = [:] + #if canImport(UIKit) + if let existingTraits = descriptor.object(forKey: .traits) as? [UIFontDescriptor.TraitKey: Any] { + traits = existingTraits + } + traits[.weight] = weight + descriptor = descriptor.addingAttributes([.traits: traits]) + #else + if let existingTraits = descriptor.object(forKey: .traits) as? [NSFontDescriptor.TraitKey: Any] { + traits = existingTraits + } + traits[.weight] = weight + descriptor = descriptor.addingAttributes([.traits: traits]) + #endif + } + + return descriptor + } +} diff --git a/Sources/SwiftUIHelpers/SceneExtensions/Scene+OnChange.swift b/Sources/SwiftUIHelpers/SceneExtensions/Scene+OnChange.swift index 3755120..6d44423 100644 --- a/Sources/SwiftUIHelpers/SceneExtensions/Scene+OnChange.swift +++ b/Sources/SwiftUIHelpers/SceneExtensions/Scene+OnChange.swift @@ -5,8 +5,6 @@ // Created by Valeriy Malishevskyi on 13.12.2024. // -#if canImport(SwiftHelpers) - import SwiftHelpers import SwiftUI @@ -32,7 +30,7 @@ struct _ValueActionModifier : _SceneModifier where Value : Equatable { struct ValueActionProperty: DynamicProperty { - @StoredValue private var value: Value! + private var value: Value! private var initialValue: Value private var action: (Value, Value) -> Void @@ -50,7 +48,7 @@ struct ValueActionProperty: DynamicProperty { set { value = newValue } } - func update() { + nonisolated mutating func update() { guard value != initialValue else { return } if useInitial { action(initialValue, initialValue) @@ -61,5 +59,3 @@ struct ValueActionProperty: DynamicProperty { value = initialValue } } - -#endif diff --git a/Sources/SwiftUIHelpers/SceneExtensions/Scene+OnReceive.swift b/Sources/SwiftUIHelpers/SceneExtensions/Scene+OnReceive.swift index 654472d..670e1cc 100644 --- a/Sources/SwiftUIHelpers/SceneExtensions/Scene+OnReceive.swift +++ b/Sources/SwiftUIHelpers/SceneExtensions/Scene+OnReceive.swift @@ -5,8 +5,6 @@ // Created by Valeriy Malishevskyi on 13.12.2024. // -#if canImport(SwiftHelpers) - import SwiftHelpers import Combine import SwiftUI @@ -20,7 +18,7 @@ extension Scene { struct _PublisherActionProperty

: DynamicProperty where P : Publisher, P.Failure == Never { - @StoredValue private var cancelable: AnyCancellable? + private var cancelable: AnyCancellable? private let publisher: P private let action: (P.Output) -> Void @@ -32,7 +30,7 @@ where P : Publisher, P.Failure == Never { self.action = action } - nonisolated func update() { + nonisolated mutating func update() { guard cancelable == nil else { return } cancelable = publisher.sink(receiveValue: action) } @@ -49,5 +47,3 @@ struct _PublisherActionModifier

: _SceneModifier where P : Publisher, P.Failu content.onChange(of: "") { _ in } } } - -#endif From b52feb9cd173df45f13b5f057fd7ef7dd2396e45 Mon Sep 17 00:00:00 2001 From: Valeriy Malishevskyi Date: Mon, 12 May 2025 18:38:11 +0300 Subject: [PATCH 6/6] Update Binding+NilCoalescing.swift --- .../{Binding+??.swift => Binding+NilCoalescing.swift} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Sources/SwiftUIExtensions/{Binding+??.swift => Binding+NilCoalescing.swift} (100%) diff --git a/Sources/SwiftUIExtensions/Binding+??.swift b/Sources/SwiftUIExtensions/Binding+NilCoalescing.swift similarity index 100% rename from Sources/SwiftUIExtensions/Binding+??.swift rename to Sources/SwiftUIExtensions/Binding+NilCoalescing.swift