From 5e8382c08f76c8d03c06a4f49b26bdf945ce9548 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 3 Dec 2023 02:54:05 +0800 Subject: [PATCH] Add AccessibilityBoundedNumber.swift implementation --- .../internal/AccessibilityBoundedNumber.swift | 77 +++++++++++++++++++ .../internal/AccessibilityNumber.swift | 7 ++ .../internal/AccessibilityValue.swift | 13 +--- .../AccessibilityBoundedNumberTests.swift | 34 ++++++++ 4 files changed, 119 insertions(+), 12 deletions(-) create mode 100644 Sources/OpenSwiftUI/Accessibility/internal/AccessibilityBoundedNumber.swift create mode 100644 Tests/OpenSwiftUITests/Accessibility/internal/AccessibilityBoundedNumberTests.swift diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityBoundedNumber.swift b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityBoundedNumber.swift new file mode 100644 index 0000000..3a7a849 --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityBoundedNumber.swift @@ -0,0 +1,77 @@ +// +// AccessibilityBoundedNumber.swift +// OpenSwiftUI +// +// Created by Kyle on 2023/12/2. +// Lastest Version: iOS 15.5 +// Status: WIP +// ID: 333660CD735494DA92CEC2878E6C8CC5 + +import Foundation + +struct AccessibilityBoundedNumber { + var number: AccessibilityNumber + var lowerBound: AccessibilityNumber? + var upperBound: AccessibilityNumber? + var stride: AccessibilityNumber? + + // TODO + init?(for value: S, in range: ClosedRange?, by stride: S.Stride?) { + return nil + } +} + +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 } +} + +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) + } +} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityNumber.swift b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityNumber.swift index 9e201ea..a6c9d80 100644 --- a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityNumber.swift +++ b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityNumber.swift @@ -13,7 +13,14 @@ struct AccessibilityNumber { } 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 { diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityValue.swift b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityValue.swift index 6bfbac4..1b97a1c 100644 --- a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityValue.swift +++ b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityValue.swift @@ -19,17 +19,6 @@ protocol AccessibilityValue: Equatable { static var type: AnyAccessibilityValueType { get } } -extension AccessibilityValue where PlatformValue: NSNumber { - var localizedDescription: String? { - NumberFormatter.localizedString(from: value, number: .decimal) - } - - var displayDescription: String? { - NumberFormatter.localizedString(from: value, number: .decimal) - } - - var minValue: NSNumber? { nil } - var maxValue: NSNumber? { nil } +extension AccessibilityValue { var step: NSNumber? { nil } - static var type: AnyAccessibilityValueType { .number } } diff --git a/Tests/OpenSwiftUITests/Accessibility/internal/AccessibilityBoundedNumberTests.swift b/Tests/OpenSwiftUITests/Accessibility/internal/AccessibilityBoundedNumberTests.swift new file mode 100644 index 0000000..5fd9a75 --- /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.503") //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, "5%") // .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") + } + } +}