Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 21 additions & 13 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,29 +1,37 @@
// 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"),
.package(url: "https://github.com/stalkermv/SwiftHelpers.git", 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",
.product(name: "SwiftHelpers", package: "SwiftHelpers")
]
),
.target(
name: "SwiftUIExtensions",
dependencies: [
.product(name: "SwiftHelpers", package: "SwiftHelpers")
]
),
.testTarget(
name: "SwiftUIHelpersTests",
dependencies: ["SwiftUIHelpers"]),
name: "SwiftUIExtensionsTests",
dependencies: ["SwiftUIExtensions"],
),
]
)
38 changes: 38 additions & 0 deletions Sources/SwiftUIExtensions/Binding+Boolean.swift
Original file line number Diff line number Diff line change
@@ -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.
@MainActor var boolean: Binding<Bool> {
Binding<Bool> {
!wrappedValue.isNone
} set: { newValue in
if newValue == false { self.wrappedValue = nil }
}
}
}

#endif
52 changes: 52 additions & 0 deletions Sources/SwiftUIExtensions/Binding+Equals.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// Binding+Equals.swift
//
// Created by Valeriy Malishevskyi on 21.08.2023.
//

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<ScopeValue: Sendable>(_ binding: Binding<ScopeValue>, 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
}
}
}
}
25 changes: 25 additions & 0 deletions Sources/SwiftUIExtensions/Binding+NilCoalescing.swift
Original file line number Diff line number Diff line change
@@ -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<Value?>` (binding to an optional value)
/// - rhs: The default value if the original `wrappedValue` is `nil`
/// - Returns: The `Binding<Value>` (where `Value` is non-optional)
static func ??(
lhs: Binding<Optional<Value>>,
rhs: @autoclosure @Sendable @escaping () -> Value
) -> Binding<Value> {
Binding(
get: { lhs.wrappedValue ?? rhs() },
set: { lhs.wrappedValue = $0 }
)
}
}
44 changes: 44 additions & 0 deletions Sources/SwiftUIExtensions/Binding.swift
Original file line number Diff line number Diff line change
@@ -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<Element, ID, C>(
_ target: Binding<Element?>,
key: KeyPath<Element, ID>,
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 }
}
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
}
28 changes: 28 additions & 0 deletions Sources/SwiftUIExtensions/EdgeInsets+Init.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading