diff --git a/Package.swift b/Package.swift index ddcf154..61b6186 100644 --- a/Package.swift +++ b/Package.swift @@ -20,6 +20,7 @@ let openSwiftUITarget = Target.target( ], swiftSettings: [ .enableExperimentalFeature("AccessLevelOnImport"), + .define("OPENSWIFTUI_SUPPRESS_DEPRECATED_WARNINGS") ], linkerSettings: [ .unsafeFlags( @@ -36,13 +37,15 @@ let openSwiftUITestTarget = Target.testTarget( name: "OpenSwiftUITests", dependencies: [ "OpenSwiftUI", - ] + ], + exclude: ["README.md"] ) let openSwiftUICompatibilityTestTarget = Target.testTarget( name: "OpenSwiftUICompatibilityTests", dependencies: [ "OpenSwiftUI", - ] + ], + exclude: ["README.md"] ) let package = Package( diff --git a/Sources/OpenSwiftUI/Internal/Accessibility/TODO/AccessibilityButtonShapeModifier.swift b/Sources/OpenSwiftUI/Accessibility/Modifier/internal/AccessibilityButtonShapeModifier.swift similarity index 71% rename from Sources/OpenSwiftUI/Internal/Accessibility/TODO/AccessibilityButtonShapeModifier.swift rename to Sources/OpenSwiftUI/Accessibility/Modifier/internal/AccessibilityButtonShapeModifier.swift index 4e210c5..a9179e7 100644 --- a/Sources/OpenSwiftUI/Internal/Accessibility/TODO/AccessibilityButtonShapeModifier.swift +++ b/Sources/OpenSwiftUI/Accessibility/Modifier/internal/AccessibilityButtonShapeModifier.swift @@ -7,14 +7,8 @@ // Status: WIP // ID: 0D3243EDC3DD4D641848661DCC354D4B -struct AccessibilityButtonShapeModifier: AccessibilityConfigurationModifier { - typealias Configuration = Never - typealias Content = V - - // deleted method - var configuration: Never { fatalError() } - - func body(content: V) -> some View { +struct AccessibilityButtonShapeModifier: AccessibilityConfigurationModifier { + func body(content: Content) -> some View { content.modifier(Child()) } diff --git a/Sources/OpenSwiftUI/Internal/Accessibility/AccessibilityConfigurationModifier.swift b/Sources/OpenSwiftUI/Accessibility/Modifier/internal/AccessibilityConfigurationModifier.swift similarity index 66% rename from Sources/OpenSwiftUI/Internal/Accessibility/AccessibilityConfigurationModifier.swift rename to Sources/OpenSwiftUI/Accessibility/Modifier/internal/AccessibilityConfigurationModifier.swift index f21fedf..cb909c6 100644 --- a/Sources/OpenSwiftUI/Internal/Accessibility/AccessibilityConfigurationModifier.swift +++ b/Sources/OpenSwiftUI/Accessibility/Modifier/internal/AccessibilityConfigurationModifier.swift @@ -7,10 +7,14 @@ // Status: Complete protocol AccessibilityConfigurationModifier { - associatedtype Configuration + associatedtype Configuration = Never associatedtype Body associatedtype Content var configuration: Configuration { get } func body(content: Self.Content) -> Self.Body } + +extension AccessibilityConfigurationModifier where Configuration == Never { + var configuration: Configuration { fatalError() } +} diff --git a/Sources/OpenSwiftUI/Accessibility/Modifier/internal/AccessibilityLabelModifier.swift b/Sources/OpenSwiftUI/Accessibility/Modifier/internal/AccessibilityLabelModifier.swift new file mode 100644 index 0000000..7f5863d --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/Modifier/internal/AccessibilityLabelModifier.swift @@ -0,0 +1,13 @@ +struct AccessibilityLabelModifier: AccessibilityConfigurationModifier { + func body(content: Content) -> some View { + content.modifier(ChildModifier()) + } + + private struct ChildModifier: PrimitiveViewModifier {} +} + +extension View { + func accessibilityLabel() -> some View { + AccessibilityLabelModifier().body(content: self) + } +} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityBoundedNumber.swift b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityBoundedNumber.swift new file mode 100644 index 0000000..4f08b4e --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityBoundedNumber.swift @@ -0,0 +1,64 @@ +// +// AccessibilityBoundedNumber.swift +// OpenSwiftUI +// +// Created by Kyle on 2023/12/2. +// Lastest Version: iOS 15.5 +// Status: Complete + +import Foundation + +struct AccessibilityBoundedNumber { + var number: AccessibilityNumber + var lowerBound: AccessibilityNumber? + var upperBound: AccessibilityNumber? + var stride: AccessibilityNumber? + + init?(for value: S, in range: ClosedRange?, by strideValue: S.Stride?) { + let clampedValue = range.map { value.clamped(to: $0) } + let newValue = clampedValue ?? value + guard let numericValue = newValue as? AccessibilityNumeric, + let numberValue = numericValue.asNumber() else { + return nil + } + number = numberValue + if let range { + lowerBound = range.minimumValue?.asNumber() + upperBound = range.maximumValue?.asNumber() + } + if let strideValue, + let numericStride = strideValue as? AccessibilityNumeric { + stride = numericStride.asNumber() + } + } +} + +extension AccessibilityBoundedNumber: AccessibilityValue { + // This kind of description logic is very strange + // But that's how Apple's implementation even on iOS 17 :) + // eg. + // For 1.5 and [1.0, 2.0], the accessiblity output would be 150% + // For 1.5 and [1.3, 2.3], the accessiblity output would be 1.5 + var localizedDescription: String? { + let range: Double = if let lowerBound, let upperBound { + upperBound.base.doubleValue - lowerBound.base.doubleValue + } else { + .zero + } + if abs(range - 100) >= .ulpOfOne { + let style: NumberFormatter.Style = (abs(range - 1.0) < .ulpOfOne) ? .percent : .decimal + return NumberFormatter.localizedString(from: number.base, number: style) + } else { + return NumberFormatter.localizedString(from: NSNumber(value: number.base.doubleValue / 100), number: .percent) + } + } + + var displayDescription: String? { + localizedDescription + } + + var value: NSNumber { number.value } + var minValue: NSNumber? { lowerBound?.value } + var maxValue: NSNumber? { upperBound?.value } + static var type: AnyAccessibilityValueType { .boundedNumber } +} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityNumber.swift b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityNumber.swift new file mode 100644 index 0000000..a6c9d80 --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityNumber.swift @@ -0,0 +1,50 @@ +// +// AccessibilityNumber.swift +// OpenSwiftUI +// +// Created by Kyle on 2023/12/2. +// Lastest Version: iOS 15.5 +// Status: Complete + +import Foundation + +struct AccessibilityNumber { + var base: NSNumber +} + +extension AccessibilityNumber: AccessibilityValue { + var localizedDescription: String? { + NumberFormatter.localizedString(from: value, number: .decimal) + } + var displayDescription: String? { localizedDescription } + var value: NSNumber { base } + var minValue: NSNumber? { nil } + var maxValue: NSNumber? { nil } + static var type: AnyAccessibilityValueType { .number } +} + +extension AccessibilityNumber: ExpressibleByFloatLiteral { + init(floatLiteral value: Double) { + base = NSNumber(floatLiteral: value) + } +} + +extension AccessibilityNumber: ExpressibleByIntegerLiteral { + init(integerLiteral value: Int) { + base = NSNumber(integerLiteral: value) + } +} + +extension AccessibilityNumber: Codable { + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let data = try container.decode(Data.self) + self.base = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSNumber.self, from: data)! + } + + func encode(to encoder: Encoder) throws { + let data = try NSKeyedArchiver.archivedData(withRootObject: base, requiringSecureCoding: true) + var container = encoder.singleValueContainer() + try container.encode(data) + } +} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityNumeric.swift b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityNumeric.swift new file mode 100644 index 0000000..5f5c3b7 --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityNumeric.swift @@ -0,0 +1,86 @@ +// +// AccessibilityNumber.swift +// OpenSwiftUI +// +// Created by Kyle on 2023/12/3. +// Lastest Version: iOS 15.5 +// Status: Complete + +import Foundation + +protocol AccessibilityNumeric { + var isValidMinValue: Bool { get } + var isValidMaxValue: Bool { get } + func asNumber() -> AccessibilityNumber? +} + +extension AccessibilityNumeric where Self: FixedWidthInteger { + var isValidMinValue: Bool { + // TODO: Add Unit Test and check usage + if Self.bitWidth == 8 || !Self.isSigned { + true + } else { + self != .min + } + } + + var isValidMaxValue: Bool { self != .max } +} + +extension Int: AccessibilityNumeric { + func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } +} + +extension Int8: AccessibilityNumeric { + func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } +} + +extension Int16: AccessibilityNumeric { + func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } +} + +extension Int32: AccessibilityNumeric { + func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } +} + +extension Int64: AccessibilityNumeric { + func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } +} + +extension UInt: AccessibilityNumeric { + func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } +} + +extension UInt8: AccessibilityNumeric { + func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } +} + +extension UInt16: AccessibilityNumeric { + func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } +} + +extension UInt32: AccessibilityNumeric { + func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } +} + +extension UInt64: AccessibilityNumeric { + func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } +} + +extension AccessibilityNumeric where Self: BinaryFloatingPoint { + var isValidMinValue: Bool { + isFinite && self > -Self.greatestFiniteMagnitude + } + + var isValidMaxValue: Bool { + isFinite && self < Self.greatestFiniteMagnitude + } +} + +extension Float: AccessibilityNumeric { + func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } +} + +extension Double: AccessibilityNumeric { + func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } +} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityPlatformSafe.swift b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityPlatformSafe.swift new file mode 100644 index 0000000..2d47d41 --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityPlatformSafe.swift @@ -0,0 +1,20 @@ +// +// AccessibilityPlatformSafe.swift +// OpenSwiftUI +// +// Created by Kyle on 2023/12/2. +// Lastest Version: iOS 15.5 +// Status: Complete + +import Foundation + +protocol AccessibilityPlatformSafe {} + +extension String: AccessibilityPlatformSafe {} +extension Double: AccessibilityPlatformSafe {} +extension Int: AccessibilityPlatformSafe {} +extension UInt: AccessibilityPlatformSafe {} +extension UInt8: AccessibilityPlatformSafe {} +extension Bool: AccessibilityPlatformSafe {} +extension NSNumber: AccessibilityPlatformSafe {} +extension Optional: AccessibilityPlatformSafe where Wrapped: AccessibilityPlatformSafe {} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilitySliderValue.swift b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilitySliderValue.swift new file mode 100644 index 0000000..328c2b6 --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilitySliderValue.swift @@ -0,0 +1,12 @@ +// +// AccessibilitySliderValue.swift +// OpenSwiftUI +// +// Created by Kyle on 2023/12/16. +// Lastest Version: iOS 15.5 +// Status: Complete + +struct AccessibilitySliderValue: AccessibilityValueByProxy { + var base: AccessibilityBoundedNumber + static var type: AnyAccessibilityValueType { .slider } +} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityValue.swift b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityValue.swift new file mode 100644 index 0000000..27dc9bf --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityValue.swift @@ -0,0 +1,55 @@ +// +// AccessibilityValue.swift +// OpenSwiftUI +// +// Created by Kyle on 2023/12/2. +// Lastest Version: iOS 15.5 +// Status: Complete + +import Foundation + +protocol AccessibilityValue: Equatable { + associatedtype PlatformValue: AccessibilityPlatformSafe + var localizedDescription: String? { get } + var displayDescription: String? { get } + var value: PlatformValue { get } + var minValue: PlatformValue? { get } + var maxValue: PlatformValue? { get } + var step: PlatformValue? { get } + static var type: AnyAccessibilityValueType { get } +} + +extension AccessibilityValue { + var minValue: PlatformValue? { nil } + var maxValue: PlatformValue? { nil } + var step: PlatformValue? { nil } +} + +extension AccessibilityValue where PlatformValue: CustomStringConvertible { + var localizedDescription: String? { value.description } + var displayDescription: String? { value.description } +} + +extension AccessibilityValue where Self == Self.PlatformValue { + var value: PlatformValue { self } +} + +extension Int: AccessibilityValue { + typealias PlatformValue = Int + static var type: AnyAccessibilityValueType { .int } +} + +extension Double: AccessibilityValue { + typealias PlatformValue = Double + static var type: AnyAccessibilityValueType { .number } +} + +extension Bool: AccessibilityValue { + typealias PlatformValue = Bool + static var type: AnyAccessibilityValueType { .bool } +} + +extension String: AccessibilityValue { + typealias PlatformValue = String + static var type: AnyAccessibilityValueType { .string } +} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityValueByProxy.swift b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityValueByProxy.swift new file mode 100644 index 0000000..835f555 --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityValueByProxy.swift @@ -0,0 +1,21 @@ +// +// AccessibilityValueByProxy.swift +// OpenSwiftUI +// +// Created by Kyle on 2023/12/16. +// Lastest Version: iOS 15.5 +// Status: Complete + +protocol AccessibilityValueByProxy: AccessibilityValue { + associatedtype Base: AccessibilityValue + var base: Base { get } +} + +extension AccessibilityValueByProxy { + var localizedDescription: String? { base.localizedDescription } + var displayDescription: String? { base.displayDescription } + var value: Base.PlatformValue { base.value } + var minValue: Base.PlatformValue? { base.minValue } + var maxValue: Base.PlatformValue? { base.maxValue } + var step: Base.PlatformValue? { base.step } +} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AnyAccessibilityValue.swift b/Sources/OpenSwiftUI/Accessibility/internal/AnyAccessibilityValue.swift new file mode 100644 index 0000000..8a3f65b --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/internal/AnyAccessibilityValue.swift @@ -0,0 +1,154 @@ +// +// AnyAccessibilityValue.swift +// OpenSwiftUI +// +// Created by Kyle on 2023/12/2. +// Lastest Version: iOS 15.5 +// Status: WIP +// ID: 333660CD735494DA92CEC2878E6C8CC5 + +// MARK: - AbstractAnyAccessibilityValue + +private protocol AbstractAnyAccessibilityValue: Codable { + var localizedDescription: String? { get } + var displayDescription: String? { get } + var value: Any { get } + var minValue: Any? { get } + var maxValue: Any? { get } + var step: Any? { get } + var type: AnyAccessibilityValueType { get } + func `as`(_ type: Value.Type) -> Value? + func isEqual(to value: AbstractAnyAccessibilityValue) -> Bool +} + +// MARK: - AnyAccessibilityValue + +struct AnyAccessibilityValue { + private var base: AbstractAnyAccessibilityValue + + init(_ base: some Codable & AccessibilityValue) { + self.base = ConcreteBase(base: base) + } +} + +extension AnyAccessibilityValue: Equatable { + static func == (lhs: AnyAccessibilityValue, rhs: AnyAccessibilityValue) -> Bool { + lhs.base.isEqual(to: rhs.base) + } +} + +extension AnyAccessibilityValue: Codable { + private enum Keys: CodingKey { + case type + case value + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: Keys.self) + let type = try container.decode(AnyAccessibilityValueType.self, forKey: .type) + switch type { + case .int: base = try container.decode(ConcreteBase.self, forKey: .value) + case .double: base = try container.decode(ConcreteBase.self, forKey: .value) + case .bool: base = try container.decode(ConcreteBase.self, forKey: .value) + case .string: base = try container.decode(ConcreteBase.self, forKey: .value) + case .slider: base = try container.decode(ConcreteBase.self, forKey: .value) + case .boundedNumber: base = try container.decode(ConcreteBase.self, forKey: .value) + case .number: base = try container.decode(ConcreteBase.self, forKey: .value) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: Keys.self) + try container.encode(base.type, forKey: .type) + try base.encode(to: container.superEncoder(forKey: .value)) + } +} + +extension AnyAccessibilityValue: AbstractAnyAccessibilityValue { + var localizedDescription: String? { base.localizedDescription } + var displayDescription: String? { base.displayDescription } + var value: Any { base.value } + var minValue: Any? { base.minValue } + var maxValue: Any? { base.maxValue } + var step: Any? { base.step } + var type: AnyAccessibilityValueType { base.type } + func `as`(_ type: Value.Type) -> Value? where Value: AccessibilityValue { + base.as(type) + } + + fileprivate func isEqual(to value: AbstractAnyAccessibilityValue) -> Bool { + base.isEqual(to: value) + } +} + +// MARK: AnyAccessibilityValue.ConcreateBase + +extension AnyAccessibilityValue { + fileprivate struct ConcreteBase where Base: Codable, Base: AccessibilityValue { + var base: Base + } +} + +extension AnyAccessibilityValue.ConcreteBase: Codable {} +extension AnyAccessibilityValue.ConcreteBase: Equatable {} +extension AnyAccessibilityValue.ConcreteBase: AbstractAnyAccessibilityValue { + var localizedDescription: String? { base.localizedDescription } + var displayDescription: String? { base.displayDescription } + var value: Any { base.value } + var minValue: Any? { base.minValue } + var maxValue: Any? { base.maxValue } + var step: Any? { base.step } + var type: AnyAccessibilityValueType { Base.type } + func `as`(_: Value.Type) -> Value? where Value: AccessibilityValue { + base as? Value + } + + func isEqual(to value: AbstractAnyAccessibilityValue) -> Bool { + base == (value as? Self)?.base + } +} + +// MARK: AccessibilityBoundedNumber + Codable + +extension AccessibilityBoundedNumber: Codable { + private enum CodingKeys: CodingKey { + case number + case lowerBound + case upperBound + case stride + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + number = try container.decode(AccessibilityNumber.self, forKey: .number) + lowerBound = try container.decodeIfPresent(AccessibilityNumber.self, forKey: .lowerBound) + upperBound = try container.decodeIfPresent(AccessibilityNumber.self, forKey: .upperBound) + stride = try container.decodeIfPresent(AccessibilityNumber.self, forKey: .lowerBound) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(number, forKey: .number) + try container.encodeIfPresent(lowerBound, forKey: .lowerBound) + try container.encodeIfPresent(upperBound, forKey: .upperBound) + try container.encodeIfPresent(stride, forKey: .stride) + } +} + +// MARK: AccessibilitySliderValue + Codable + +extension AccessibilitySliderValue: Codable { + private enum CodingKeys: CodingKey { + case base + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + base = try container.decode(AccessibilityBoundedNumber.self, forKey: .base) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(base, forKey: .base) + } +} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AnyAccessibilityValueType.swift b/Sources/OpenSwiftUI/Accessibility/internal/AnyAccessibilityValueType.swift new file mode 100644 index 0000000..d118a5e --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/internal/AnyAccessibilityValueType.swift @@ -0,0 +1,22 @@ +// +// AnyAccessibilityValueType.swift +// OpenSwiftUI +// +// Created by Kyle on 2023/12/2. +// Lastest Version: iOS 15.5 +// Status: Complete + +enum AnyAccessibilityValueType: UInt, Codable { + case int + case double + case bool + case string +// case disclosure +// case toggle + case slider +// case stepper +// case progress + case boundedNumber +// case headingLevel + case number +} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/ClosedRange+AccessibilityNumeric.swift b/Sources/OpenSwiftUI/Accessibility/internal/ClosedRange+AccessibilityNumeric.swift new file mode 100644 index 0000000..0d5f319 --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/internal/ClosedRange+AccessibilityNumeric.swift @@ -0,0 +1,17 @@ +extension ClosedRange where Bound: Strideable { + var minimumValue: AccessibilityNumeric? { + guard let value = lowerBound as? AccessibilityNumeric, + value.isValidMinValue else { + return nil + } + return value + } + + var maximumValue: AccessibilityNumeric? { + guard let value = upperBound as? AccessibilityNumeric, + value.isValidMaxValue else { + return nil + } + return value + } +} diff --git a/Sources/OpenSwiftUI/DataAndStorage/EnvironmentValues/Internal/EnvironmentValues+sliderStyle.swift b/Sources/OpenSwiftUI/DataAndStorage/EnvironmentValues/Internal/EnvironmentValues+sliderStyle.swift new file mode 100644 index 0000000..16e5f90 --- /dev/null +++ b/Sources/OpenSwiftUI/DataAndStorage/EnvironmentValues/Internal/EnvironmentValues+sliderStyle.swift @@ -0,0 +1,19 @@ +// +// EnvironmentValues+sliderStyle.swift +// OpenSwiftUI +// +// Created by Kyle on 2023/12/2. +// Lastest Version: iOS 15.5 +// Status: Complete + +private struct SliderStyleKey: EnvironmentKey { + static let defaultValue = AnySliderStyle.default +} + +extension EnvironmentValues { + @inline(__always) + var sliderStyle: AnySliderStyle { + get { self[SliderStyleKey.self] } + set { self[SliderStyleKey.self] = newValue } + } +} diff --git a/Sources/OpenSwiftUI/DataAndStorage/EnvironmentValues/Internal/TODO/_EnvironmentKeyTransformModifier.swift b/Sources/OpenSwiftUI/DataAndStorage/EnvironmentValues/Internal/TODO/_EnvironmentKeyTransformModifier.swift index e407918..5be86a1 100644 --- a/Sources/OpenSwiftUI/DataAndStorage/EnvironmentValues/Internal/TODO/_EnvironmentKeyTransformModifier.swift +++ b/Sources/OpenSwiftUI/DataAndStorage/EnvironmentValues/Internal/TODO/_EnvironmentKeyTransformModifier.swift @@ -1,19 +1,26 @@ -protocol _GraphInputsModifier {} +// +// _EnvironmentKeyTransformModifier.swift +// OpenSwiftUI +// +// Created by Kyle on 2023/12/5. +// Lastest Version: iOS 15.5 +// Status: WIP +// ID: 1DBD4F024EFF0E73A70DB6DD05D5B548 @frozen -public struct _EnvironmentKeyTransformModifier : ViewModifier, _GraphInputsModifier { - public var keyPath: Swift.WritableKeyPath - public var transform: (inout Value) -> Swift.Void +public struct _EnvironmentKeyTransformModifier: PrimitiveViewModifier, _GraphInputsModifier { + public var keyPath: WritableKeyPath + public var transform: (inout Value) -> Void @inlinable - public init(keyPath: Swift.WritableKeyPath, transform: @escaping (inout Value) -> Swift.Void) { + public init(keyPath: WritableKeyPath, transform: @escaping (inout Value) -> Void) { self.keyPath = keyPath self.transform = transform } + public static func _makeInputs(modifier: _GraphValue<_EnvironmentKeyTransformModifier>, inputs: inout _GraphInputs) { } - public typealias Body = Never } extension View { @@ -28,3 +35,7 @@ extension View { )) } } + +private struct ChildEnvironment { + +} diff --git a/Sources/OpenSwiftUI/DataAndStorage/EnvironmentValues/TODO/EnvironmentValues.swift b/Sources/OpenSwiftUI/DataAndStorage/EnvironmentValues/TODO/EnvironmentValues.swift index ee89d44..39af302 100644 --- a/Sources/OpenSwiftUI/DataAndStorage/EnvironmentValues/TODO/EnvironmentValues.swift +++ b/Sources/OpenSwiftUI/DataAndStorage/EnvironmentValues/TODO/EnvironmentValues.swift @@ -43,6 +43,7 @@ private struct EffectiveFontKey: DerivedEnvironmentKey { } } +// TODO extension EnvironmentValues { @inline(__always) public var font: Font? { diff --git a/Sources/OpenSwiftUI/Internal/Graph/_GraphInputsModifier.swift b/Sources/OpenSwiftUI/Internal/Graph/_GraphInputsModifier.swift new file mode 100644 index 0000000..af3a79c --- /dev/null +++ b/Sources/OpenSwiftUI/Internal/Graph/_GraphInputsModifier.swift @@ -0,0 +1,11 @@ +// +// _GraphInputsModifier.swift +// OpenSwiftUI +// +// Created by Kyle on 2023/12/5. +// Lastest Version: iOS 15.5 +// Status: Complete + +protocol _GraphInputsModifier { + static func _makeInputs(modifier: _GraphValue, inputs: inout _GraphInputs) +} diff --git a/Sources/OpenSwiftUI/Internal/Other/Comparable+Extension.swift b/Sources/OpenSwiftUI/Internal/Other/Comparable+Extension.swift new file mode 100644 index 0000000..d9d8db1 --- /dev/null +++ b/Sources/OpenSwiftUI/Internal/Other/Comparable+Extension.swift @@ -0,0 +1,21 @@ +extension Comparable { + func clamped(to range: ClosedRange) -> Self { + var value = self + value.clamp(to: range) + return value + } + + mutating func clamp(to range: ClosedRange) { + self = OpenSwiftUI.clamp(self, min: range.lowerBound, max: range.upperBound) + } +} + +func clamp(_ value: Value, min minValue: Value, max maxValue: Value) -> Value { + if value < minValue { + minValue + } else if value > maxValue { + maxValue + } else { + value + } +} diff --git a/Sources/OpenSwiftUI/OpenSwiftUI.docc/Views/Controls.md b/Sources/OpenSwiftUI/OpenSwiftUI.docc/Views/Controls.md index fb8b0f6..731a287 100644 --- a/Sources/OpenSwiftUI/OpenSwiftUI.docc/Views/Controls.md +++ b/Sources/OpenSwiftUI/OpenSwiftUI.docc/Views/Controls.md @@ -14,6 +14,10 @@ Use these built-in controls and indicators when composing custom views, and styl - ``Link`` +### Getting numeric inputs + +- ``Slider`` + ### Sizing controls - ``View/controlSize(_:)`` diff --git a/Sources/OpenSwiftUI/OpenSwiftUI.docc/Views/Controls/Slider.md b/Sources/OpenSwiftUI/OpenSwiftUI.docc/Views/Controls/Slider.md new file mode 100644 index 0000000..e04181d --- /dev/null +++ b/Sources/OpenSwiftUI/OpenSwiftUI.docc/Views/Controls/Slider.md @@ -0,0 +1,29 @@ +# ``Slider`` + +## Topics + +### Creating a slider + +- ``init(value:in:onEditingChanged:)`` + +- ``init(value:in:step:onEditingChanged:)`` + +### Creating a slider with labels + +- ``init(value:in:label:onEditingChanged:)`` + +- ``init(value:in:step:label:onEditingChanged:)`` + +- ``init(value:in:label:minimumValueLabel:maximumValueLabel:onEditingChanged:)`` + +- ``init(value:in:step:label:minimumValueLabel:maximumValueLabel:onEditingChanged:)`` + +### Deprecated initializers + +- ``init(value:in:onEditingChanged:label:)`` + +- ``init(value:in:step:onEditingChanged:label:)`` + +- ``init(value:in:onEditingChanged:minimumValueLabel:maximumValueLabel:label:)`` + +- ``init(value:in:step:onEditingChanged:minimumValueLabel:maximumValueLabel:label:)`` diff --git a/Sources/OpenSwiftUI/Views/Controls/Link/Link.swift b/Sources/OpenSwiftUI/Views/Controls/Link/Link.swift index d1b7918..78807b1 100644 --- a/Sources/OpenSwiftUI/Views/Controls/Link/Link.swift +++ b/Sources/OpenSwiftUI/Views/Controls/Link/Link.swift @@ -4,7 +4,7 @@ // // Created by Kyle on 2023/11/28. // Lastest Version: iOS 15.5 -// Status: Blocked by Text +// Status: Blocked by Text and Accessibility import Foundation diff --git a/Sources/OpenSwiftUI/Views/Controls/Slider/Slider.swift b/Sources/OpenSwiftUI/Views/Controls/Slider/Slider.swift new file mode 100644 index 0000000..0f2e1ba --- /dev/null +++ b/Sources/OpenSwiftUI/Views/Controls/Slider/Slider.swift @@ -0,0 +1,694 @@ +// +// Slider.swift +// OpenSwiftUI +// +// Created by Kyle on 2023/12/14. +// Lastest Version: iOS 15.5 +// Status: Blocked by Accessibility +// ID: F045F16106E380A820CC0B639278A953 + +/// A control for selecting a value from a bounded linear range of values. +/// +/// A slider consists of a "thumb" image that the user moves between two +/// extremes of a linear "track". The ends of the track represent the minimum +/// and maximum possible values. As the user moves the thumb, the slider +/// updates its bound value. +/// +/// The following example shows a slider bound to the value `speed`. As the +/// slider updates this value, a bound ``Text`` view shows the value updating. +/// The `onEditingChanged` closure passed to the slider receives callbacks when +/// the user drags the slider. The example uses this to change the +/// color of the value text. +/// +/// @State private var speed = 50.0 +/// @State private var isEditing = false +/// +/// var body: some View { +/// VStack { +/// Slider( +/// value: $speed, +/// in: 0...100, +/// onEditingChanged: { editing in +/// isEditing = editing +/// } +/// ) +/// Text("\(speed)") +/// .foregroundColor(isEditing ? .red : .blue) +/// } +/// } +/// +/// ![An unlabeled slider, with its thumb about one third of the way from the +/// minimum extreme. Below, a blue label displays the value +/// 33.045977.](SwiftUI-Slider-simple.png) +/// +/// You can also use a `step` parameter to provide incremental steps along the +/// path of the slider. For example, if you have a slider with a range of `0` to +/// `100`, and you set the `step` value to `5`, the slider's increments would be +/// `0`, `5`, `10`, and so on. The following example shows this approach, and +/// also adds optional minimum and maximum value labels. +/// +/// @State private var speed = 50.0 +/// @State private var isEditing = false +/// +/// var body: some View { +/// Slider( +/// value: $speed, +/// in: 0...100, +/// step: 5 +/// ) { +/// Text("Speed") +/// } minimumValueLabel: { +/// Text("0") +/// } maximumValueLabel: { +/// Text("100") +/// } onEditingChanged: { editing in +/// isEditing = editing +/// } +/// Text("\(speed)") +/// .foregroundColor(isEditing ? .red : .blue) +/// } +/// +/// ![A slider with labels show minimum and maximum values of 0 and 100, +/// respectively, with its thumb most of the way to the maximum extreme. Below, +/// a blue label displays the value +/// 85.000000.](SwiftUI-Slider-withStepAndLabels.png) +/// +/// The slider also uses the `step` to increase or decrease the value when a +/// VoiceOver user adjusts the slider with voice commands. +@available(tvOS, unavailable) +public struct Slider: View where Label: View, ValueLabel: View { + public var body: some View { + style.body(configuration: .init(self)) + .viewAlias(SliderStyleLabel.self) { + label.accessibilityLabel() + } + .viewAlias(SliderMinimumValueLabel.self) { + _minimumValueLabel.accessibilityLabel() + } + .viewAlias(SliderMaximumValueLabel.self) { + _maximumValueLabel.accessibilityLabel() + } + // TODO: Accessibility + } + + @Binding var value: Double + var onEditingChanged: (Bool) -> Void + let skipDistance: Double + let discreteValueCount: Int + var _minimumValueLabel: ValueLabel + var _maximumValueLabel: ValueLabel + var hasCustomMinMaxValueLabels: Bool + var label: Label + var accessibilityValue: AccessibilityBoundedNumber? + + @Environment(\.sliderStyle) + var style: AnySliderStyle +} + +// MARK: - Creating a slider with labels + +@available(tvOS, unavailable) +extension Slider { + /// Creates a slider to select a value from a given range, which displays + /// the provided labels. + /// + /// - Parameters: + /// - value: The selected value within `bounds`. + /// - bounds: The range of the valid values. Defaults to `0...1`. + /// - label: A `View` that describes the purpose of the instance. Not all + /// slider styles show the label, but even in those cases, OpenSwiftUI + /// uses the label for accessibility. For example, VoiceOver uses the + /// label to identify the purpose of the slider. + /// - minimumValueLabel: A view that describes `bounds.lowerBound`. + /// - maximumValueLabel: A view that describes `bounds.upperBound`. + /// - onEditingChanged: A callback for when editing begins and ends. + /// + /// The `value` of the created instance is equal to the position of + /// the given value within `bounds`, mapped into `0...1`. + /// + /// The slider calls `onEditingChanged` when editing begins and ends. For + /// example, on iOS, editing begins when the user starts to drag the thumb + /// along the slider's track. + #if !OPENSWIFTUI_SUPPRESS_DEPRECATED_WARNINGS + @_alwaysEmitIntoClient + #endif + public init( + value: Binding, + in bounds: ClosedRange = 0 ... 1, + @ViewBuilder label: () -> Label, + @ViewBuilder minimumValueLabel: () -> ValueLabel, + @ViewBuilder maximumValueLabel: () -> ValueLabel, + onEditingChanged: @escaping (Bool) -> Void = { _ in } + ) where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint { + #if OPENSWIFTUI_SUPPRESS_DEPRECATED_WARNINGS + self.init( + value: value, + in: bounds, + step: nil, + onEditingChanged: onEditingChanged, + minimumValueLabel: minimumValueLabel(), + maximumValueLabel: maximumValueLabel(), + customMinMaxValueLabels: true, + label: label() + ) + #else + self.init( + value: value, + in: bounds, + onEditingChanged: onEditingChanged, + minimumValueLabel: minimumValueLabel(), + maximumValueLabel: maximumValueLabel(), + label: label + ) + #endif + } + + /// Creates a slider to select a value from a given range, subject to a + /// step increment, which displays the provided labels. + /// + /// - Parameters: + /// - value: The selected value within `bounds`. + /// - bounds: The range of the valid values. Defaults to `0...1`. + /// - step: The distance between each valid value. + /// - label: A `View` that describes the purpose of the instance. Not all + /// slider styles show the label, but even in those cases, OpenSwiftUI + /// uses the label for accessibility. For example, VoiceOver uses the + /// label to identify the purpose of the slider. + /// - minimumValueLabel: A view that describes `bounds.lowerBound`. + /// - maximumValueLabel: A view that describes `bounds.upperBound`. + /// - onEditingChanged: A callback for when editing begins and ends. + /// + /// The `value` of the created instance is equal to the position of + /// the given value within `bounds`, mapped into `0...1`. + /// + /// The slider calls `onEditingChanged` when editing begins and ends. For + /// example, on iOS, editing begins when the user starts to drag the thumb + /// along the slider's track. + #if !OPENSWIFTUI_SUPPRESS_DEPRECATED_WARNINGS + @_alwaysEmitIntoClient + #endif + public init( + value: Binding, + in bounds: ClosedRange, + step: V.Stride = 1, + @ViewBuilder label: () -> Label, + @ViewBuilder minimumValueLabel: () -> ValueLabel, + @ViewBuilder maximumValueLabel: () -> ValueLabel, + onEditingChanged: @escaping (Bool) -> Void = { _ in } + ) where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint { + #if OPENSWIFTUI_SUPPRESS_DEPRECATED_WARNINGS + self.init( + value: value, + in: bounds, + step: step, + onEditingChanged: onEditingChanged, + minimumValueLabel: minimumValueLabel(), + maximumValueLabel: maximumValueLabel(), + customMinMaxValueLabels: true, + label: label() + ) + #else + self.init( + value: value, + in: bounds, + step: step, + onEditingChanged: onEditingChanged, + minimumValueLabel: minimumValueLabel(), + maximumValueLabel: maximumValueLabel(), + label: label + ) + #endif + } +} + +@available(tvOS, unavailable) +extension Slider where ValueLabel == EmptyView { + /// Creates a slider to select a value from a given range, which displays + /// the provided label. + /// + /// - Parameters: + /// - value: The selected value within `bounds`. + /// - bounds: The range of the valid values. Defaults to `0...1`. + /// - label: A `View` that describes the purpose of the instance. Not all + /// slider styles show the label, but even in those cases, OpenSwiftUI + /// uses the label for accessibility. For example, VoiceOver uses the + /// label to identify the purpose of the slider. + /// - onEditingChanged: A callback for when editing begins and ends. + /// + /// The `value` of the created instance is equal to the position of + /// the given value within `bounds`, mapped into `0...1`. + /// + /// The slider calls `onEditingChanged` when editing begins and ends. For + /// example, on iOS, editing begins when the user starts to drag the thumb + /// along the slider's track. + #if !OPENSWIFTUI_SUPPRESS_DEPRECATED_WARNINGS + @_alwaysEmitIntoClient + #endif + public init( + value: Binding, + in bounds: ClosedRange = 0 ... 1, + @ViewBuilder label: () -> Label, + onEditingChanged: @escaping (Bool) -> Void = { _ in } + ) where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint { + #if OPENSWIFTUI_SUPPRESS_DEPRECATED_WARNINGS + self.init( + value: value, + in: bounds, + step: nil, + onEditingChanged: onEditingChanged, + minimumValueLabel: EmptyView(), + maximumValueLabel: EmptyView(), + customMinMaxValueLabels: false, + label: label() + ) + #else + self.init( + value: value, + in: bounds, + onEditingChanged: onEditingChanged, + label: label + ) + #endif + } + + /// Creates a slider to select a value from a given range, subject to a + /// step increment, which displays the provided label. + /// + /// - Parameters: + /// - value: The selected value within `bounds`. + /// - bounds: The range of the valid values. Defaults to `0...1`. + /// - step: The distance between each valid value. + /// - label: A `View` that describes the purpose of the instance. Not all + /// slider styles show the label, but even in those cases, OpenSwiftUI + /// uses the label for accessibility. For example, VoiceOver uses the + /// label to identify the purpose of the slider. + /// - onEditingChanged: A callback for when editing begins and ends. + /// + /// The `value` of the created instance is equal to the position of + /// the given value within `bounds`, mapped into `0...1`. + /// + /// The slider calls `onEditingChanged` when editing begins and ends. For + /// example, on iOS, editing begins when the user starts to drag the thumb + /// along the slider's track. + #if !OPENSWIFTUI_SUPPRESS_DEPRECATED_WARNINGS + @_alwaysEmitIntoClient + #endif + public init( + value: Binding, + in bounds: ClosedRange, + step: V.Stride = 1, + @ViewBuilder label: () -> Label, + onEditingChanged: @escaping (Bool) -> Void = { _ in } + ) where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint { + #if OPENSWIFTUI_SUPPRESS_DEPRECATED_WARNINGS + self.init( + value: value, + in: bounds, + step: step, + onEditingChanged: onEditingChanged, + minimumValueLabel: EmptyView(), + maximumValueLabel: EmptyView(), + customMinMaxValueLabels: false, + label: label() + ) + #else + self.init( + value: value, + in: bounds, + step: step, + onEditingChanged: onEditingChanged, + label: label + ) + #endif + } +} + +// MARK: - Creating a slider + +@available(tvOS, unavailable) +extension Slider where Label == EmptyView, ValueLabel == EmptyView { + /// Creates a slider to select a value from a given range. + /// + /// - Parameters: + /// - value: The selected value within `bounds`. + /// - bounds: The range of the valid values. Defaults to `0...1`. + /// - onEditingChanged: A callback for when editing begins and ends. + /// + /// The `value` of the created instance is equal to the position of + /// the given value within `bounds`, mapped into `0...1`. + /// + /// The slider calls `onEditingChanged` when editing begins and ends. For + /// example, on iOS, editing begins when the user starts to drag the thumb + /// along the slider's track. + public init( + value: Binding, + in bounds: ClosedRange = 0 ... 1, + onEditingChanged: @escaping (Bool) -> Void = { _ in } + ) where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint { + self.init( + value: value, + in: bounds, + step: nil, + onEditingChanged: onEditingChanged, + minimumValueLabel: EmptyView(), + maximumValueLabel: EmptyView(), + customMinMaxValueLabels: false, + label: EmptyView() + ) + } + + /// Creates a slider to select a value from a given range, subject to a + /// step increment. + /// + /// - Parameters: + /// - value: The selected value within `bounds`. + /// - bounds: The range of the valid values. Defaults to `0...1`. + /// - step: The distance between each valid value. + /// - onEditingChanged: A callback for when editing begins and ends. + /// + /// The `value` of the created instance is equal to the position of + /// the given value within `bounds`, mapped into `0...1`. + /// + /// The slider calls `onEditingChanged` when editing begins and ends. For + /// example, on iOS, editing begins when the user starts to drag the thumb + /// along the slider's track. + public init( + value: Binding, + in bounds: ClosedRange, + step: V.Stride = 1, + onEditingChanged: @escaping (Bool) -> Void = { _ in } + ) where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint { + self.init( + value: value, + in: bounds, + step: step, + onEditingChanged: onEditingChanged, + minimumValueLabel: EmptyView(), + maximumValueLabel: EmptyView(), + customMinMaxValueLabels: false, + label: EmptyView() + ) + } +} + +// MARK: - Deprecated initializers + +@available(tvOS, unavailable) +extension Slider { + /// Creates a slider to select a value from a given range, which displays + /// the provided labels. + /// + /// - Parameters: + /// - value: The selected value within `bounds`. + /// - bounds: The range of the valid values. Defaults to `0...1`. + /// - onEditingChanged: A callback for when editing begins and ends. + /// - minimumValueLabel: A view that describes `bounds.lowerBound`. + /// - maximumValueLabel: A view that describes `bounds.upperBound`. + /// - label: A `View` that describes the purpose of the instance. Not all + /// slider styles show the label, but even in those cases, OpenSwiftUI + /// uses the label for accessibility. For example, VoiceOver uses the + /// label to identify the purpose of the slider. + /// + /// The `value` of the created instance is equal to the position of + /// the given value within `bounds`, mapped into `0...1`. + /// + /// The slider calls `onEditingChanged` when editing begins and ends. For + /// example, on iOS, editing begins when the user starts to drag the thumb + /// along the slider's track. + @available(*, deprecated, renamed: "Slider(value:in:label:minimumValueLabel:maximumValueLabel:onEditingChanged:)") + public init( + value: Binding, + in bounds: ClosedRange = 0 ... 1, + onEditingChanged: @escaping (Bool) -> Void = { _ in }, + minimumValueLabel: ValueLabel, + maximumValueLabel: ValueLabel, + @ViewBuilder label: () -> Label + ) where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint { + self.init( + value: value, + in: bounds, + step: nil, + onEditingChanged: onEditingChanged, + minimumValueLabel: minimumValueLabel, + maximumValueLabel: maximumValueLabel, + customMinMaxValueLabels: true, + label: label() + ) + } + + /// Creates a slider to select a value from a given range, subject to a + /// step increment, which displays the provided labels. + /// + /// - Parameters: + /// - value: The selected value within `bounds`. + /// - bounds: The range of the valid values. Defaults to `0...1`. + /// - step: The distance between each valid value. + /// - onEditingChanged: A callback for when editing begins and ends. + /// - minimumValueLabel: A view that describes `bounds.lowerBound`. + /// - maximumValueLabel: A view that describes `bounds.upperBound`. + /// - label: A `View` that describes the purpose of the instance. Not all + /// slider styles show the label, but even in those cases, OpenSwiftUI + /// uses the label for accessibility. For example, VoiceOver uses the + /// label to identify the purpose of the slider. + /// + /// The `value` of the created instance is equal to the position of + /// the given value within `bounds`, mapped into `0...1`. + /// + /// The slider calls `onEditingChanged` when editing begins and ends. For + /// example, on iOS, editing begins when the user starts to drag the thumb + /// along the slider's track. + @available(*, deprecated, renamed: "Slider(value:in:step:label:minimumValueLabel:maximumValueLabel:onEditingChanged:)") + public init( + value: Binding, + in bounds: ClosedRange, + step: V.Stride = 1, + onEditingChanged: @escaping (Bool) -> Void = { _ in }, + minimumValueLabel: ValueLabel, + maximumValueLabel: ValueLabel, + @ViewBuilder label: () -> Label + ) where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint { + self.init( + value: value, + in: bounds, + step: step, + onEditingChanged: onEditingChanged, + minimumValueLabel: minimumValueLabel, + maximumValueLabel: maximumValueLabel, + customMinMaxValueLabels: true, + label: label() + ) + } +} + +@available(tvOS, unavailable) +extension Slider where ValueLabel == EmptyView { + /// Creates a slider to select a value from a given range, which displays + /// the provided label. + /// + /// - Parameters: + /// - value: The selected value within `bounds`. + /// - bounds: The range of the valid values. Defaults to `0...1`. + /// - onEditingChanged: A callback for when editing begins and ends. + /// - label: A `View` that describes the purpose of the instance. Not all + /// slider styles show the label, but even in those cases, OpenSwiftUI + /// uses the label for accessibility. For example, VoiceOver uses the + /// label to identify the purpose of the slider. + /// + /// The `value` of the created instance is equal to the position of + /// the given value within `bounds`, mapped into `0...1`. + /// + /// The slider calls `onEditingChanged` when editing begins and ends. For + /// example, on iOS, editing begins when the user starts to drag the thumb + /// along the slider's track. + @available(*, deprecated, renamed: "Slider(value:in:label:onEditingChanged:)") + @_disfavoredOverload + public init( + value: Binding, + in bounds: ClosedRange = 0 ... 1, + onEditingChanged: @escaping (Bool) -> Void = { _ in }, + @ViewBuilder label: () -> Label + ) where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint { + self.init( + value: value, + in: bounds, + step: nil, + onEditingChanged: onEditingChanged, + minimumValueLabel: EmptyView(), + maximumValueLabel: EmptyView(), + customMinMaxValueLabels: false, + label: label() + ) + } + + /// Creates a slider to select a value from a given range, subject to a + /// step increment, which displays the provided label. + /// + /// - Parameters: + /// - value: The selected value within `bounds`. + /// - bounds: The range of the valid values. Defaults to `0...1`. + /// - step: The distance between each valid value. + /// - onEditingChanged: A callback for when editing begins and ends. + /// - label: A `View` that describes the purpose of the instance. Not all + /// slider styles show the label, but even in those cases, OpenSwiftUI + /// uses the label for accessibility. For example, VoiceOver uses the + /// label to identify the purpose of the slider. + /// + /// The `value` of the created instance is equal to the position of + /// the given value within `bounds`, mapped into `0...1`. + /// + /// The slider calls `onEditingChanged` when editing begins and ends. For + /// example, on iOS, editing begins when the user starts to drag the thumb + /// along the slider's track. + @available(*, deprecated, renamed: "Slider(value:in:step:label:onEditingChanged:)") + @_disfavoredOverload + public init( + value: Binding, + in bounds: ClosedRange, + step: V.Stride = 1, + onEditingChanged: @escaping (Bool) -> Void = { _ in }, + @ViewBuilder label: () -> Label + ) where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint { + self.init( + value: value, + in: bounds, + step: step, + onEditingChanged: onEditingChanged, + minimumValueLabel: EmptyView(), + maximumValueLabel: EmptyView(), + customMinMaxValueLabels: false, + label: label() + ) + } +} + +// MARK: Internal + +struct SliderStyleLabel: ViewAlias {} +struct SliderStyleValueLabel: ViewAlias {} +struct SliderMinimumValueLabel: ViewAlias {} +struct SliderMaximumValueLabel: ViewAlias {} + +extension Slider { + init(_ slider: Slider) where Label == SliderStyleLabel, ValueLabel == SliderStyleValueLabel { + _value = slider.$value + onEditingChanged = slider.onEditingChanged + skipDistance = slider.skipDistance + discreteValueCount = slider.discreteValueCount + _minimumValueLabel = SliderStyleValueLabel() + _maximumValueLabel = SliderStyleValueLabel() + hasCustomMinMaxValueLabels = slider.hasCustomMinMaxValueLabels + label = SliderStyleLabel() + accessibilityValue = slider.accessibilityValue + } +} + +extension Slider { + init( + value: Binding, + in bounds: ClosedRange, + step: Value.Stride?, + onEditingChanged: @escaping (Bool) -> Void, + minimumValueLabel: ValueLabel, + maximumValueLabel: ValueLabel, + customMinMaxValueLabels: Bool, + label: Label + ) where Value: BinaryFloatingPoint, Value.Stride: BinaryFloatingPoint { + let normalizing = Normalizing( + min: bounds.lowerBound, + max: bounds.upperBound, + stride: step + ) + self.init( + value: value.projecting(normalizing), + skipDistance: step.map { $0 / normalizing.length }, + onEditingChanged: onEditingChanged, + minimumValueLabel: minimumValueLabel, + maximumValueLabel: maximumValueLabel, + customMinMaxValueLabels: customMinMaxValueLabels, + accessibilityValue: AccessibilityBoundedNumber( + for: value.wrappedValue, + in: bounds, + by: step + ), + label: label + ) + } + + init( + value: Binding, + skipDistance: Value?, + onEditingChanged: @escaping (Bool) -> Void, + minimumValueLabel: ValueLabel, + maximumValueLabel: ValueLabel, + customMinMaxValueLabels: Bool, + accessibilityValue: AccessibilityBoundedNumber?, + label: Label + ) { + _value = value.projecting(Clamping()) + self.onEditingChanged = onEditingChanged + self.skipDistance = skipDistance.map { Double($0) } ?? 0.1 + discreteValueCount = if let skipDistance { + Int(1.0 / Double(skipDistance)) + 1 + } else { + 0 + } + _minimumValueLabel = minimumValueLabel + _maximumValueLabel = maximumValueLabel + hasCustomMinMaxValueLabels = customMinMaxValueLabels + self.label = label + let accessibilitySliderValue = accessibilityValue.map { AccessibilitySliderValue(base: $0) } + self.accessibilityValue = accessibilitySliderValue?.base + } +} + +// MARK: Private + +private struct Clamping: Projection where Value: BinaryFloatingPoint { + func get(base: Value) -> Double { + Double(base).clamped(to: 0 ... 1) + } + + func set(base: inout Value, newValue: Double) { + base = clamp(Value(newValue), min: 0, max: 1) + } +} + +private struct Normalizing: Projection where Value: Strideable, Value: Hashable, Value.Stride: BinaryFloatingPoint { + let min: Value + let max: Value + let stride: Value.Stride? + let maxStrides: Value.Stride? + let length: Value.Stride + + init(min: Value, max: Value, stride: Value.Stride?) { + self.min = min + self.max = max + self.stride = stride + + if let stride { + let result = (min.distance(to: max) / stride).rounded(.down) + guard result > 0 else { + fatalError("max stride must be positive") + } + length = stride * result + maxStrides = result + } else { + length = min.distance(to: max) + maxStrides = nil + } + } + + func get(base: Value) -> Value.Stride { + min.distance(to: base) / length + } + + func set(base: inout Value, newValue: Value.Stride) { + let newStride: Value.Stride + if let stride, let maxStrides { + newStride = stride * (newValue * maxStrides).rounded(.toNearestOrAwayFromZero) + } else { + newStride = newValue * length + } + base = min.advanced(by: newStride) + } +} diff --git a/Sources/OpenSwiftUI/Views/Controls/Slider/internal/AnySliderStyle.swift b/Sources/OpenSwiftUI/Views/Controls/Slider/internal/AnySliderStyle.swift new file mode 100644 index 0000000..cf9d14b --- /dev/null +++ b/Sources/OpenSwiftUI/Views/Controls/Slider/internal/AnySliderStyle.swift @@ -0,0 +1,47 @@ +// +// AnySliderStyle.swift +// OpenSwiftUI +// +// Created by Kyle on 2023/11/28. +// Lastest Version: iOS 15.5 +// Status: Complete +// ID: 22A1D162CC670E67558243600080F90E + +protocol SliderStyle { + associatedtype Body: View + func body(configuration: Slider) -> Self.Body +} + +private class AnyStyleBox { + func body(configuration _: Slider) -> AnyView { + fatalError("") + } +} + +private class StyleBox: AnyStyleBox { + let base: Base + + init(base: Base) { + self.base = base + } + + override func body(configuration: Slider) -> AnyView { + AnyView(base.body(configuration: configuration)) + } +} + +struct AnySliderStyle: SliderStyle { + private let box: AnyStyleBox + + func body(configuration: Slider) -> AnyView { + box.body(configuration: configuration) + } + + private init(box: AnyStyleBox) { + self.box = box + } + + init(style: some SliderStyle) { + self.box = StyleBox(base: style) + } +} diff --git a/Sources/OpenSwiftUI/Views/Controls/Slider/internal/SystemSliderStyle.swift b/Sources/OpenSwiftUI/Views/Controls/Slider/internal/SystemSliderStyle.swift new file mode 100644 index 0000000..c200a1b --- /dev/null +++ b/Sources/OpenSwiftUI/Views/Controls/Slider/internal/SystemSliderStyle.swift @@ -0,0 +1,19 @@ +// +// SystemSliderStyle.swift +// OpenSwiftUI +// +// Created by Kyle on 2023/12/2. +// Lastest Version: iOS 15.5 +// Status: WIP +// ID: 8AA246B2E0E916EFA5AD706DCC8A0FE8 + +// TODO +private struct SystemSliderStyle: SliderStyle { + func body(configuration: Slider) -> some View { + EmptyView() + } +} + +extension AnySliderStyle { + static let `default` = AnySliderStyle(style: SystemSliderStyle()) +} diff --git a/Sources/OpenSwiftUI/Views/View/TODO/View.swift b/Sources/OpenSwiftUI/Views/View/TODO/View.swift index 71c99ce..849bfcb 100644 --- a/Sources/OpenSwiftUI/Views/View/TODO/View.swift +++ b/Sources/OpenSwiftUI/Views/View/TODO/View.swift @@ -6,14 +6,71 @@ // Lastest Version: iOS 15.5 // Status: WIP +/// A type that represents part of your app's user interface and provides +/// modifiers that you use to configure views. +/// +/// You create custom views by declaring types that conform to the `View` +/// protocol. Implement the required ``View/body-swift.property`` computed +/// property to provide the content for your custom view. +/// +/// struct MyView: View { +/// var body: some View { +/// Text("Hello, World!") +/// } +/// } +/// +/// Assemble the view's body by combining one or more of the built-in views +/// provided by SwiftUI, like the ``Text`` instance in the example above, plus +/// other custom views that you define, into a hierarchy of views. For more +/// information about creating custom views, see . +/// +/// The `View` protocol provides a set of modifiers — protocol +/// methods with default implementations — that you use to configure +/// views in the layout of your app. Modifiers work by wrapping the +/// view instance on which you call them in another view with the specified +/// characteristics, as described in . +/// For example, adding the ``View/opacity(_:)`` modifier to a +/// text view returns a new view with some amount of transparency: +/// +/// Text("Hello, World!") +/// .opacity(0.5) // Display partially transparent text. +/// +/// The complete list of default modifiers provides a large set of controls +/// for managing views. +/// For example, you can fine tune , +/// add information, +/// and respond to . +/// You can also collect groups of default modifiers into new, +/// custom view modifiers for easy reuse. @_typeEraser(AnyView) public protocol View { static func _makeView(view: _GraphValue, inputs: _ViewInputs) -> _ViewOutputs static func _makeViewList(view: _GraphValue, inputs: _ViewListInputs) -> _ViewListOutputs static func _viewListCount(inputs: _ViewListCountInputs) -> Int? + + /// The type of view representing the body of this view. + /// + /// When you create a custom view, Swift infers this type from your + /// implementation of the required ``View/body-swift.property`` property. associatedtype Body: View + /// The content and behavior of the view. + /// + /// When you implement a custom view, you must implement a computed + /// `body` property to provide the content for your view. Return a view + /// that's composed of built-in views that SwiftUI provides, plus other + /// composite views that you've already defined: + /// + /// struct MyView: View { + /// var body: some View { + /// Text("Hello, World!") + /// } + /// } + /// + /// For more information about composing views and a view hierarchy, + /// see . @ViewBuilder + @MainActor var body: Self.Body { get } } diff --git a/Sources/OpenSwiftUI/Views/View/ViewAlias.swift b/Sources/OpenSwiftUI/Views/View/ViewAlias.swift deleted file mode 100644 index b0d0570..0000000 --- a/Sources/OpenSwiftUI/Views/View/ViewAlias.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// ViewAlias.swift -// OpenSwiftUI -// -// Created by Kyle on 2023/9/21. -// Lastest Version: iOS 15.5 -// Status: Complete - -protocol ViewAlias: PrimitiveView { - init() -} diff --git a/Sources/OpenSwiftUI/Views/View/Debug/TODO/CustomViewDebugValueConvertible.swift b/Sources/OpenSwiftUI/Views/View/internal/Debug/TODO/CustomViewDebugValueConvertible.swift similarity index 100% rename from Sources/OpenSwiftUI/Views/View/Debug/TODO/CustomViewDebugValueConvertible.swift rename to Sources/OpenSwiftUI/Views/View/internal/Debug/TODO/CustomViewDebugValueConvertible.swift diff --git a/Sources/OpenSwiftUI/Views/View/Debug/TODO/_ViewDebug.swift b/Sources/OpenSwiftUI/Views/View/internal/Debug/TODO/_ViewDebug.swift similarity index 100% rename from Sources/OpenSwiftUI/Views/View/Debug/TODO/_ViewDebug.swift rename to Sources/OpenSwiftUI/Views/View/internal/Debug/TODO/_ViewDebug.swift diff --git a/Sources/OpenSwiftUI/Views/View/Debug/TODO/_printChange.swift b/Sources/OpenSwiftUI/Views/View/internal/Debug/TODO/_printChange.swift similarity index 100% rename from Sources/OpenSwiftUI/Views/View/Debug/TODO/_printChange.swift rename to Sources/OpenSwiftUI/Views/View/internal/Debug/TODO/_printChange.swift diff --git a/Sources/OpenSwiftUI/Views/View/internal/ViewAlias.swift b/Sources/OpenSwiftUI/Views/View/internal/ViewAlias.swift new file mode 100644 index 0000000..2d0d10e --- /dev/null +++ b/Sources/OpenSwiftUI/Views/View/internal/ViewAlias.swift @@ -0,0 +1,44 @@ +// +// ViewAlias.swift +// OpenSwiftUI +// +// Created by Kyle on 2023/9/21. +// Lastest Version: iOS 15.5 +// Status: TODO +// ID: D9F7AF928092578A4B8FA861B49E2161 + +protocol ViewAlias: PrimitiveView { + init() +} + +extension View { + func viewAlias( + _ alias: Alias.Type, + _ source: () -> Source + ) -> some View { + modifier(StaticSourceWriter(source: source())) + } + + func viewAlias( + _ alias: Alias.Type, + _ source: () -> Source? + ) -> some View { + modifier(OptionalSourceWriter(source: source())) + } +} + +private struct StaticSourceWriter: PrimitiveViewModifier, _GraphInputsModifier { + var source: Source + + static func _makeInputs(modifier: _GraphValue>, inputs: inout _GraphInputs) { + fatalError("TODO") + } +} + +private struct OptionalSourceWriter: PrimitiveViewModifier, _GraphInputsModifier { + var source: Source? + + static func _makeInputs(modifier: _GraphValue>, inputs: inout _GraphInputs) { + fatalError("TODO") + } +} diff --git a/Tests/OpenSwiftUITests/Accessibility/internal/AccessibilityBoundedNumberTests.swift b/Tests/OpenSwiftUITests/Accessibility/internal/AccessibilityBoundedNumberTests.swift new file mode 100644 index 0000000..ca315cb --- /dev/null +++ b/Tests/OpenSwiftUITests/Accessibility/internal/AccessibilityBoundedNumberTests.swift @@ -0,0 +1,34 @@ +// +// AccessibilityBoundedNumberTests.swift +// +// +// Created by Kyle on 2023/12/3. +// + +@testable import OpenSwiftUI +import XCTest + +final class AccessibilityBoundedNumberTests: XCTestCase { + func testBoundedNumberLocalizedDescription() throws { + if let boundedNumber = AccessibilityBoundedNumber(for: 4.5, in: 3.0...16.0, by: 0.1) { + XCTAssertEqual(boundedNumber.localizedDescription, "4.5") //decimal case + } else { + XCTFail("Failed to init bounded number") + } + if let boundedNumber = AccessibilityBoundedNumber(for: 4.5, in: 1.0...101.0, by: 0.1) { + XCTAssertEqual(boundedNumber.localizedDescription, "4%") // .percent case + } else { + XCTFail("Failed to init bounded number") + } + if let boundedNumber = AccessibilityBoundedNumber(for: 1.5, in: 1.3...2.3, by: 0.1) { + XCTAssertEqual(boundedNumber.localizedDescription, "1.5") // .decimal case + } else { + XCTFail("Failed to init bounded number") + } + if let boundedNumber = AccessibilityBoundedNumber(for: 1.5, in: 1.0...2.0, by: 0.1) { + XCTAssertEqual(boundedNumber.localizedDescription, "150%") // .percent case + } else { + XCTFail("Failed to init bounded number") + } + } +} diff --git a/Tests/OpenSwiftUITests/Views/Controls/Slider/SliderTests.swift b/Tests/OpenSwiftUITests/Views/Controls/Slider/SliderTests.swift new file mode 100644 index 0000000..4dfaac5 --- /dev/null +++ b/Tests/OpenSwiftUITests/Views/Controls/Slider/SliderTests.swift @@ -0,0 +1,17 @@ +// +// SliderTests.swift +// +// +// Created by Kyle on 2023/12/16. +// + +import XCTest +@testable import OpenSwiftUI + +final class SliderTests: XCTestCase { + func testExample() throws { + let s = Slider(value: .constant(233), in: 200.0 ... 300.0, step: 28.0) + XCTAssertEqual(s.skipDistance, 0.333, accuracy: 0.001) + XCTAssertEqual(s.discreteValueCount, 4) + } +}