diff --git a/.github/workflows/Build.yml b/.github/workflows/Build.yml index 3e287e11b..475ec22a2 100644 --- a/.github/workflows/Build.yml +++ b/.github/workflows/Build.yml @@ -10,7 +10,7 @@ on: jobs: build_iOS: - runs-on: macOS-12 + runs-on: macOS-13 steps: - uses: actions/checkout@v1 - name: Generate projects @@ -19,6 +19,8 @@ jobs: run: make build_lib_iOS - name: Run tests run: make test_lib_iOS + - name: Check Log + run: make check_test_log - name: Prepare Report run: make prepare_report - name: Upload Coverage @@ -34,7 +36,7 @@ jobs: build_Example: - runs-on: macOS-12 + runs-on: macOS-13 steps: - uses: actions/checkout@v1 - name: Generate projects @@ -43,6 +45,8 @@ jobs: run: make build_example_iOS - name: Run tests run: make test_example_iOS + - name: Check Log + run: make check_test_log - name: Prepare Report run: make prepare_example_report - name: Upload Coverage @@ -57,7 +61,7 @@ jobs: verbose: true build_tvOS: - runs-on: macOS-12 + runs-on: macOS-13 steps: - uses: actions/checkout@v1 - name: Generate projects diff --git a/.swiftlint.yml b/.swiftlint.yml index c3c3e366d..2f846ff8a 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -86,12 +86,6 @@ custom_rules: message: "Every MARK should be surrounded with 1 newline before and 1 after it" severity: warning - redundant_default_type: - included: ".*.swift" - name: "Redundant explicit type" - regex: "(var|let) [0-9a-zA-Z_]*: (Int|String|Double|Bool)[ ]*= " - message: "Redundant explicit declaration of default types should be avoided" - class_modificators_order: include: ".*.swift" name: "Class modificators order" diff --git a/Components/Sources/Collection/CollectionWrappedCell.swift b/Components/Sources/Collection/CollectionWrappedCell.swift index a8c8b89aa..2d56ceeec 100644 --- a/Components/Sources/Collection/CollectionWrappedCell.swift +++ b/Components/Sources/Collection/CollectionWrappedCell.swift @@ -28,7 +28,15 @@ public final class CollectionWrappedCell: UICollectionVi extension CollectionWrappedCell: CalculatableHeightItem where View: CalculatableHeightItem { public static func getHeight(forWidth width: CGFloat, with model: Model) -> CGFloat { - return View.getHeight(forWidth: width, with: model) + let nestedViewHeight = View.getHeight(forWidth: width, with: model) + if let alignment = (model as? AlignmentProvider)?.alignment { + switch alignment { + case .leading(let insets), .trailing(let insets), .all(let insets): + return nestedViewHeight + insets.top + insets.bottom + } + } else { + return nestedViewHeight + } } } @@ -38,7 +46,15 @@ extension CollectionWrappedCell: CalculatableHeightItem where View: Calculatable extension CollectionWrappedCell: CalculatableWidthItem where View: CalculatableWidthItem { public static func getWidth(forHeight height: CGFloat, with model: View.Model) -> CGFloat { - return View.getWidth(forHeight: height, with: model) + let nestedViewWidth = View.getWidth(forHeight: height, with: model) + if let alignment = (model as? AlignmentProvider)?.alignment { + switch alignment { + case .leading(let insets), .trailing(let insets), .all(let insets): + return nestedViewWidth + insets.left + insets.right + } + } else { + return nestedViewWidth + } } } diff --git a/Components/Sources/Common/Models/Styles/TextStyle.swift b/Components/Sources/Common/Models/Styles/TextStyle.swift index 192d9e493..975852698 100644 --- a/Components/Sources/Common/Models/Styles/TextStyle.swift +++ b/Components/Sources/Common/Models/Styles/TextStyle.swift @@ -12,7 +12,7 @@ public struct TextStyle: Equatable { public let color: UIColor public let font: UIFont - public init(color: UIColor = .black, font: UIFont = .systemFont(ofSize: 16)) { + public init(color: UIColor = .black, font: UIFont = .preferredFont(forTextStyle: .body)) { self.color = color self.font = font } diff --git a/Components/Sources/Table/TableWrappedCell.swift b/Components/Sources/Table/TableWrappedCell.swift index f456c3e21..d77a7c768 100644 --- a/Components/Sources/Table/TableWrappedCell.swift +++ b/Components/Sources/Table/TableWrappedCell.swift @@ -28,7 +28,15 @@ public final class TableWrappedCell: UITableViewCell, Vi extension TableWrappedCell: CalculatableHeightItem where View: CalculatableHeightItem { public static func getHeight(forWidth width: CGFloat, with model: Model) -> CGFloat { - return View.getHeight(forWidth: width, with: model) + let nestedViewHeight = View.getHeight(forWidth: width, with: model) + if let alignment = (model as? AlignmentProvider)?.alignment { + switch alignment { + case .leading(let insets), .trailing(let insets), .all(let insets): + return nestedViewHeight + insets.top + insets.bottom + } + } else { + return nestedViewHeight + } } } diff --git a/Documentation/Accessibility/Overview.md b/Documentation/Accessibility/Overview.md new file mode 100644 index 000000000..0358973f1 --- /dev/null +++ b/Documentation/Accessibility/Overview.md @@ -0,0 +1,7 @@ +# Overview + +- [UIAccessibility Basics](Pages/UIAccessibility%20Basics.md) +- [Usage](Pages/Usage.md) +- [Invalidation](Pages/Invalidation.md) +- [Debugging](Pages/Debugging.md) +- [Advanced Usage](Pages/Advanced%20Usage.md) \ No newline at end of file diff --git a/Documentation/Accessibility/Pages/Advanced Usage.md b/Documentation/Accessibility/Pages/Advanced Usage.md new file mode 100644 index 000000000..a8da3c21e --- /dev/null +++ b/Documentation/Accessibility/Pages/Advanced Usage.md @@ -0,0 +1,89 @@ +[⏎ Overview](../Overview.md) + +# Advanced Usage + +## How it works + +The main logic is located in `TableAccessibilityPlugin` and `CollectionAccessibilityPlugin`. It automatically added to all builders to be ready work with `AccessibilityItems`. It's a common plugin that works with delgate events. + +**Only each `AccessibilityItem` will be processed by these plugins.** + +On `willDisplay` event every time 2 steps are happens: + +1. Element will be passed to its defined modifier through corresponding methods `modify(item: accessibilityItem)` or `modify(item: accessibilityItem, generator: generator)` if generator implements `AccessibilityStrategyProvider`. +2. A new invalidator instance is setted with provided `indexPath` if supports invalidation mechanism. Its invalidate method calls delegate to pass the new event `invalidatedAccessibility` which will execute step 1. + +On `didEndDisplay` event the plugin will remove invalidator if it exists. + +Plugin Example + +
+ +This is the essence how accessibility works in **RDDM**, so you don't have access to its functionality. Instead, you may have full control under modify and invalidate processes. + +*** + +
+ +## Modify custom accessibility parameters + +If you need to set a parameter which is not presented or change the way how they setting, you need to define your own strategy provider (or custom accessibility item) and modifier. You can combine your custom logic with base functionality. + +*Example:* +```swift +enum CustomAccessibilityModifier: AccessibilityModifier { + static func modify(item: AccessibilityItem) { + // set base parameters + BaseAccessibilityModifier.modify(item) + + // additional logic for custom item + guard let item = item as? CustomAccessibilityItem else { return } + item.accessibilityIdentifier = item.identifier.value + item.accessibilityHint = item.hintStrategy.value + } + + // corresponding method implementation with generator +} + +protocol CustomAccessibilityItem: AccessibilityItem { + var modifierType: AccessibilityModifierType { CustomAccessibilityModifier.self } + + // expand base accessibility item with new parameters using string strategy + var identifier: AccessibilityStringStrategy { get } + var hintStrategy: AccessibilityStringStrategy { get } +} +``` + +By this way you can add any other functionality to accessibility items. + +*** + +
+ +## Customize invalidation + + +### in table or collection +If you want to perform additional logic on invalidate, you can implement your own `AccessibilityItemInvalidator` and set `AccessibilityInvalidatorCreationBlock` + +**For example** +```swift + + private lazy var adapter = collectionView.rddm.baseBuilder + .add(plugin: .accessibility({ item, kind, delegate in + return CustomInvalidator(item, kind, delegate) + })) + .build() +``` + +*Note that we have `DelegatedAccessibilityItemInvalidator` which could be used as base for your custom invalidator. This class is needed to combine strategies from item and generator.* + +### in other view + +Any view can implement `AccessibilityItem`, but you need adittionaly call `AccessibilityItem.modifySelf()` to setup accessibility-properties for real. + +`AcessibilityInvalidatable.setBasicInvalidator` is also available for invalidation. + +*Note that is recomended to call* +- `setBasicInvalidator` when view become **visible** +- `removeInvalidator` when view become **invisible** diff --git a/Documentation/Accessibility/Pages/Debugging.md b/Documentation/Accessibility/Pages/Debugging.md new file mode 100644 index 000000000..629dd3759 --- /dev/null +++ b/Documentation/Accessibility/Pages/Debugging.md @@ -0,0 +1,77 @@ +[⏎ Overview](../Overview.md) + +# Debugging + +## Accessibility Inspector + +Accessibility Inspector is an instrument provided with Xcode, so this is most common insturment to see accessibility parameters. It can be opened in Menu Bar: `Xcode -> Open Developer Tool -> Accessibility Inspector`. + +Accessibility Inspector + +Here you can see common acceessibility parameters and perform accessibility actions. To select an element you need fisrt select a running simulator. Then you click target button on the right and select an element or click arrorws "<" and ">" to navigate through all elements. Optionally, you can enable reading for selected elements. + +Be aware that not all accessibility parameters are displayed as they VoiceOver is reading. For example, if the element is `UISwitch`, in inspector you can see values 0 or 1, but VoiceOver will localize these values into "enabled" and "disabled". So it's better to also check elements with VoiceOver. + +*Note: Accessibility parameters also can be seen in a view debugger, but it's non interactive.* +View Debugger + +*** + +
+ +## Accessibility Audit + +Accessibility Inspector allows you to run audit of current screen to find and different accessibility issues if they exists. The second tab in right corner navigates you to audit window. + +Accessibility Audit + +*** + +
+ +## VoiceOver + +If you will test your app on a device with VoiceOver you can enable some useful options. + +Caption Panel +Accessibility Shortcut + +Caption panel allows you to read instead of listen to VoiceOver. And Accessibility Shortcut allows you to fast enable or disable VoiceOver by triple-click the lock button. + +It also useful to read an article how to use VoiceOver gestures https://support.apple.com/guide/iphone/iph3e2e2281/ios + +*** + +
+ +## UI Tests + +Accessibility elements are also used for UI tests. So changing traits affects on `XCUIElement` type (any element with trait `.button` becomes a button `XCUIElement`). + +If you manually change collection or table classes to become an accessibility elements your UI tests may fail, if they will not contains required accessibility. + +UI test failure + +To fix this issue **RDDM** provides a built-in modifier for UI tests. You only need to provide the command line argument `"-rddm.XCUITestsCompatible"` + +UI tests modifier + +This modifier will add traits `.button` to all cells and `.staticText` to all headers and footers to prevent UI tests failures. + +*** + +
+ +## UI Tests Accessibility Audit + +In Xcode 15 you can run automatic accessibility audit with just a line of code: + +```swift +try app.performAccessibilityAudit() +``` + +It's the same audit from Accessibility Inspector. If there will be any issues test will fail and you can see these issues in logs. See more at https://developer.apple.com/videos/play/wwdc2023/10035/. + +
+ +Next [Advanced Usage →](./Advanced%20Usage.md) \ No newline at end of file diff --git a/Documentation/Accessibility/Pages/Invalidation.md b/Documentation/Accessibility/Pages/Invalidation.md new file mode 100644 index 000000000..60cb32a50 --- /dev/null +++ b/Documentation/Accessibility/Pages/Invalidation.md @@ -0,0 +1,44 @@ +[⏎ Overview](../Overview.md) + +# Invalidation + +**RDDM**'s accessibility plugin allows you to use your dynamic properties in accessibility strategies and so on. But if some properties will changed while the cell is on screen, you need to invalidate current accessibility parameters. For these needs you need implement `AccessibilityInvalidatable` protocol. + +```swift +protocol AccessibilityInvalidatable: AccessibilityItem { + var accessibilityInvalidator: AccessibilityItemInvalidator? { get set } +} +``` + +This protocol required you to define an invalidatior property. Through this invalidator you can call `invalidateParameters()` method which triggers re-set of all accessibility parameters. + +*Important: this property is fully managed by accessibility plugin, so you don't need to set it. But if you want to add your own funtionality, see [Advanced Usage](./Advanced%20Usage.md). Invalidator may be `nil` if cell isn't on screen, so the latest parameters will be applied on `willDisplay` after the `configure()` method.* + +*Example:* + +```swift +func updateState(state: String, isSelected: Bool) { + valueStrategy = .just(state) + isSelected ? traitsStrategy.insert(.selected) : traitsStrategy.remove(.selected) + accessibilityInvalidator?.invalidateParameters() +} +``` + +
+ +If you manage cell states like `isSelected`, `isEnabled`, you may use corresponding accessibility traits. But in a default `AccessibilityItem` these states wouldn't be changes because of `shouldOverrideStateTraits` property which by default is `false`. So you need to define this property as `true`. + +*Example:* + +```swift +var labelStrategy: AccessibilityStringStrategy { .from(object: titleLabel) } +var valueStrategy: AccessibilityStringStrategy = .just(nil) +lazy var traitsStrategy: AccessibilityTraitsStrategy = .from(object: titleLabel) +var shouldOverrideStateTraits: Bool { true } + +var accessibilityInvalidator: AccessibilityItemInvalidator? +``` + +
+ +Next [Debugging →](./Debugging.md) \ No newline at end of file diff --git a/Documentation/Accessibility/Pages/UIAccessibility Basics.md b/Documentation/Accessibility/Pages/UIAccessibility Basics.md new file mode 100644 index 000000000..905bd960c --- /dev/null +++ b/Documentation/Accessibility/Pages/UIAccessibility Basics.md @@ -0,0 +1,147 @@ +[⏎ Overview](../Overview.md) + +# `UIAccessibility` Basics +- [isAccessibilityElement](#isaccessibilityelement) +- [accessibilityElements](#accessibilityelements) +- [accessibilityLabel](#accessibilitylabel) +- [accessibilityValue](#accessibilityvalue) +- [accessibilityTraits](#accessibilitytraits) +- [accessibilityCustomActions](#accessibilitycustomcctions) +- [accessibilityActivationPoint](#accessibilityactivationPoint) +- [accessibilityViewIsModal](#accessibilityviewismodal) +- [func accessibilityActivate()](#func-accessibilityactivate) +- [func accessibilityIncrement()](#func-accessibilityincrement) +- [func accessibilityDecrement()](#func-accessibilitydecrement) +- [func accessibilityPerformEscape()](#func-accessibilityperformescape) +- [UIAccessibility.isVoiceOverRunning](#uiaccessibilityisvoiceoverrunning) +- [UIAccessibility.post(notification:, argument:)](#uiaccessibilitypostnotification-argument) + +*** +
+ +## `isAccessibilityElement` + +```swift +var isAccessibilityElement: Bool +``` +Defines accessibility view, which can be focused by VoiceOver. No other of its subviews can be focused + +
+ +## `accessibilityElements` + +```swift +var accessibilityElements: [Any]? +``` +Defines an array and order of container's accessibility elements. +Can be used instead of dynamic methods `accessibilityElementsCount()` and `accessibilityElement(at index:)` + +
+ +## `accessibilityLabel` +```swift +var accessibilityLabel: String? +``` +The "name" of accessibility element. Always read by VoiceOver. For labels and buttons it's equals to title. + +
+ +## `accessibilityValue` +```swift +var accessibilityValue: String? +``` +Contains the value of element, can be read multiple times when it updates and has a trait `.updatesFrequently`. + +
+ +## `accessibilityTraits` +```swift +var accessibilityTraits: UIAccessiblityTraits +``` +Describes how an accessibility element behaves or how to treat it. + +- `.button` - element is a button and has an action. This is default trait for all buttons. Can be activated by double tap. See [activate method](#func-accessibilityactivate). +- `.staticText` - element is a static text, that wouldn't change. This is default trait for all labels. +- `.header` - marks and pronounced by VoiceOver that current element is header of some view structure. +- `.selected` - marks and pronounced by VoiceOver that current element is in selected state. Automatically added for selected cells. +- `.notEnabled` - marks and pronounced by VoiceOver that current element is in disabled state. Automatically added for disables controls. +- `.adjustable` - element is a contol that can be manipulated by swipes, for example slider or segmented controll. By default only slider owns this trait. See [increment](#func-accessibilityincrement) and [decrement](#func-accessibilitydecrement) methods. +- `.updatesFrequently` - marks that current element frequently updates its label or value and schedule notifications for it. + +Resources: +- https://mobilea11y.com/blog/traits/ + +
+ +## `accessibilityCustomActions` +```swift +var accessibilityCustomActions: [UIAccessibilityCustomAction]? +``` +Provides custom actions for element that can be activated by user. + +
+ +## `accessibilityActivationPoint` +```swift +var accessibilityActivationPoint: CGPoint +``` +A point for `.touchUpInside` event. Measured in screen coordinates. + +
+ +## `accessibilityViewIsModal` +```swift +var accessibilityViewIsModal: Bool +``` +Defines a view is modal to ignore other views in a widnow. + +
+ +## `func accessibilityActivate()` +```swift +func accessibilityActivate() -> Bool +``` +Called when accessibility element wtih `.button` trait is activated. Can be used when element has no `.touchUpInside` handlers. + +
+ +## `func accessibilityIncrement()` +```swift +func accessibilityIncrement() -> Bool +``` +Called when accessibility element has trait `.adjustable` and user performs increment action. + +
+ +## `func accessibilityDecrement()` +```swift +func accessibilityDecrement() -> Bool +``` +Called when accessibility element has trait `.adjustable` and user performs decrement action. + +
+ +## `func accessibilityPerformEscape()` +```swift +func accessibilityPerformEscape() -> Bool +``` +Called when user performs the special gesture to dismiss the modal view. + +
+ +## `UIAccessibility.isVoiceOverRunning` +```swift +static var isVoiceOverRunning: Bool { get } +``` +Defines VoiceOver running state. + +
+ +## `UIAccessibility.post(notification:, argument:)` +```swift + UIAccessibility.post(notification: UIAccessibility.Notification, argument: Any?) +``` +Method that can manipulate VoiceOver's focus or make announcements for user. There are different types of notifications: +- `.announcement` - a simple notification with argument `String`, that be announced +- `.layoutChanged` - posted when big layout changes occures, updates accessibility elements hierarchy. Can be specified with argument which is accessibility element to move focus on it. +- `.screenChanged` - posted when screen is changed, updates accessibility elements hierarchy. Can be specified with argument which is accessibility element to move focus on it. diff --git a/Documentation/Accessibility/Pages/Usage.md b/Documentation/Accessibility/Pages/Usage.md new file mode 100644 index 000000000..55c569932 --- /dev/null +++ b/Documentation/Accessibility/Pages/Usage.md @@ -0,0 +1,203 @@ +[⏎ Overview](../Overview.md) + +# Usage + +To make your App more friendly for disabled users **RDDM** provides a set of instruments for accessibility optimizations. + +By default, all cells inside tables and collections are not accessibility elements, that can add difficult to users to navigate and read them. Despite this fact, many accessibility consultants recommend to make cells a single accessibility element. + +**RDDM** in initial will not make any changes to accessibility elements hierarchy, so you can manage your own optimization. + +To enable accessibility plugin you need to add it to builder + +*like below* +```swift + + private lazy var adapter = collectionView.rddm.baseBuilder + .add(plugin: .accessibility()) + .build() +``` + +You can see how the accessibility plugin works in the Example project. It completely includes all basic usage. + +
+ +## AccessibilityItem + +```swift +protocol AccessibilityItem: UIView, ... {} +``` + +Main protocol that need to be adopted by cells, headers and footers. It's also inherited from some accessibility parameters providers which will be setted by internal modifier. + +*To use **RDDM**'s accessibility optimizations you only need to add this protocol to your cell's classes and fill required parameters.* + +*Example:* +```swift +// MARK: - AccessibilityItem + +extension TitleCollectionListCell: AccessibilityItem { + + var labelStrategy: AccessibilityStringStrategy { .from(object: titleLabel) } + var traitsStrategy: AccessibilityTraitsStrategy { .from(object: titleLabel) } + +} +``` + +After that, a cell, header or footer which is `AccessibilityItem`, becomes an [accessibility element](./UIAccessibility%20Basics.md#isaccessibilityelement). + +before after + +*** +
+ +## Strategies + +To provide accessibility parameters you can use **RDDM**'s strategies `AccessibilityStringStrategy` and `AccessibilityTraitsStrategy`. With these strategies you can easy provide default accessibility parameters such as `accessibilityLabel, accessibilityValue, accessibilityTraits`. + +*If you need other parameters, you can define your own item and modifier for it, see [Advanced Usage](./Advanced%20Usage.md). Also you can provide `self` propetries and use [invalidation mechanism](./Invalidation.md) to update them.* + +
+ +#### AccessibilityStringStrategy +This strategy is used for string parameters [`accessibilityLabel`](./UIAccessibility%20Basics.md#accessibilitylabel) and [`AccessibilityValue`](./UIAccessibility%20Basics.md#Accessibilityvalue). +Strategy for label is required for item. + +```swift +public enum AccessibilityStringStrategy { + case ignored + case just(String?) + case from(object: NSObject, keyPath: KeyPath = \.accessibilityLabel) + indirect case joined([AccessibilityStringStrategy]) +} +``` + +- `.ignored` - value will not be changed by this strategy. +- `.just(String?)` - simple string value, can be nil. +- `.from(object: NSObject, keyPath: KeyPath)` - a reference value from object with specified keypath from value would be taken. By default it's `accessibilityLabel`. + +*Note: `NSObject` is used because all accessibility parameters defined in it. Default parameter is `accessibilityLabel` because labels and buttons have this parameter is equal to their title. If you want to use `text` from label or `title(for:)` from button then provide `.just` case with this value, it also will work.* + +- `.joined([AccessibilityStringStrategy])` - a combination of strategies, joined without spaces. + +*Example: a cell with `UIButton` and `UISwitch`* +```swift +var labelStrategy: AccessibilityStringStrategy { .from(object: button) } + +var valueStrategy: AccessibilityStringStrategy { + .joined([ + .just(isSmall ? "collapsed" : "expanded"), + .just(", is animated: "), + .from(object: switcher, keyPath: \.accessibilityValue) + ]) +} +``` + +
+ +#### AccessibilityTraitsStrategy + +This strategy is used for [`accessibilityTraits`](./UIAccessibility%20Basics.md#accessibilitytraits) parameter. Is required for item. + +```swift +public enum AccessibilityTraitsStrategy { + case ignored + case just(UIAccessibilityTraits) + case from(object: NSObject) + case merge([NSObject]) +} +``` + +- `.ignored` - value will not be changed by this strategy. +- `.just(UIAccessibilityTraits)` - simple accessibility traits. +- `.from(object: NSObject)` - a reference traits from another object +- `.merge([NSObject])` - a reference traits merged from specified objects + +*Example: the same cell with `UIButton` and `UISwitch`* +```swift +var traitsStrategy: AccessibilityTraitsStrategy { .from(object: button) } +``` + +*Note: `UISwitch` has a unique hidden trait, that only reads values 0 or 1 and localize them. If you want to use this trait, you need use `accessibilityValue` only from switcher and do not change it.* + +*** +

+ +#### AccessibilityActionsStrategy + +This strategy is used for [custom actions](./UIAccessibility%20Basics.md#accessibilitycustomactions). + +*Note: all system actions such as swipes, editing, moving, drag and drop are already provided by UIKit in another way. Use this actions provider to add custom interaction with the cell content.* + +*Static values* +- `.ignored` - value will not be changed by this strategy. +- `.just([AccessibilityAction])` - array of actions. + +*Mutation functions* +- `.append(AccessibilityAction)` - appending existing array of actions +- `.remove(AccessibilityAction)` - removing action from array +- `.removeAll()` - removing all actions from current array + +*Example:* +```swift + actionsStrategy = .just([]) + + if isEditable { + + actionsStrategy.append(.selector(name: "Buy", + target: nil, + selector: #selector(buyMethod))) + actionsStrategy.append(.closure(name: "Add to favorite", handler: { + // do something + })) + actionsStrategy.append(.closure(name: "Delete", handler: onDeleteTapped)) + + + } else { + actionsStrategy.removeAll() + } +``` + +*Note that closure based init is available only from iOS 13* + +
+ +## Providers + +Providers in **RDDM** is a separate parameters containers which are included in `AccessibilityItem`. But these providers can be used for generators to combine them with cell's parameters. + +*Note: only `AccessibilityItem` defined for a cell modifies it and apply provided parameters.* + +
+ +#### AccessibilityStrategyProvider + +This is common provider for `accessibilityLabel, accessibilityValue` and `accessibilityTraits` strategies. + +```swift +public protocol AccessibilityStrategyProvider { + var labelStrategy: AccessibilityStringStrategy { get } + var valueStrategy: AccessibilityStringStrategy { get } + var traitsStrategy: AccessibilityTraitsStrategy { get } + var actionsStrategy: AccessibilityActionsStrategy { get } + var isAccessibilityIgnored: Bool { get } +} +``` + +The last parameter `isAccessibilityIgnored` idicates that `AccessibilityItem` should become an accessibility element. Equals `true` if all strategies is in state `.ignored`. Can be overrided to make cell a container for other accessibility elements. + +Some **RDDM** generators already includes this provider to provide nesssecary traits: `.header` for header generators and `.button` for selectable and foldable generators. + +*Example:* +```swift +extension SelectableItem: AccessibilityStrategyProvider { + public var labelStrategy: AccessibilityStringStrategy { .ignored } + public var traitsStrategy: AccessibilityTraitsStrategy { didSelectEvent.isEmpty ? .ignored : .just(.button) } +} +``` + +`labelStrategy` and `traitsStrategy` is required parameters but `valueStrategy` and `actionsStrategy` is `.ignored` by default. + +
+ +Next [Invalidation →](./Invalidation.md) \ No newline at end of file diff --git a/Example/Common/UIViewController+Delay.swift b/Example/Common/UIViewController+Delay.swift index b9dcb311c..c548eec1c 100644 --- a/Example/Common/UIViewController+Delay.swift +++ b/Example/Common/UIViewController+Delay.swift @@ -11,7 +11,15 @@ import UIKit extension UIResponder { func delay(_ deadline: DispatchTime, completion: @escaping () -> Void) { - DispatchQueue.global(qos: .utility).asyncAfter(deadline: deadline) { + let normalizedDeadline: DispatchTime + + if CommandLine.arguments.contains("-decreaseDelay") { + // to improve UI tests performance we are using one delay for any values + normalizedDeadline = .now() + .nanoseconds(100) + } else { + normalizedDeadline = deadline + } + DispatchQueue.global(qos: .utility).asyncAfter(deadline: normalizedDeadline) { DispatchQueue.main.async { completion() } diff --git a/Example/ReactiveChat/Library/Styles/UIStyle+UITextView.swift b/Example/ReactiveChat/Library/Styles/UIStyle+UITextView.swift index d09207171..2189ae8ed 100644 --- a/Example/ReactiveChat/Library/Styles/UIStyle+UITextView.swift +++ b/Example/ReactiveChat/Library/Styles/UIStyle+UITextView.swift @@ -21,7 +21,7 @@ extension UITextView { extension UIStyle { static var chatInput: UIStyle { - return TextViewStyle(font: UIFont.systemFont(ofSize: 14), + return TextViewStyle(font: UIFont.preferredFont(forTextStyle: .subheadline), textColor: .black, lineHeight: 20, kern: -0.24, @@ -31,7 +31,7 @@ extension UIStyle { } static var chatInputError: UIStyle { - return TextViewStyle(font: UIFont.systemFont(ofSize: 14), + return TextViewStyle(font: UIFont.preferredFont(forTextStyle: .subheadline), textColor: .red, lineHeight: 20, kern: -0.24, @@ -41,7 +41,7 @@ extension UIStyle { } static var chatPlaceholder: UIStyle { - return TextViewStyle(font: UIFont.systemFont(ofSize: 14), + return TextViewStyle(font: UIFont.preferredFont(forTextStyle: .subheadline), textColor: .lightGray, lineHeight: 20, kern: -0.24, diff --git a/Example/ReactiveDataDisplayManager/Application/AppDelegate.swift b/Example/ReactiveDataDisplayManager/Application/AppDelegate.swift index eadca2071..e43039042 100644 --- a/Example/ReactiveDataDisplayManager/Application/AppDelegate.swift +++ b/Example/ReactiveDataDisplayManager/Application/AppDelegate.swift @@ -19,6 +19,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { UITabBar.appearance().tintColor = .rddm UITableView.appearance().sectionIndexColor = .rddm UICollectionView.appearance().tintColor = .rddm + UITextView.appearance().adjustsFontForContentSizeCategory = true + UILabel.appearance().adjustsFontForContentSizeCategory = true + UIButton.appearance().adjustsImageSizeForAccessibilityContentSizeCategory = true #if DEBUG if CommandLine.arguments.contains("-disableAnimations") { diff --git a/Example/ReactiveDataDisplayManager/Collection.storyboard b/Example/ReactiveDataDisplayManager/Collection.storyboard index b30d2f5a2..aacce485c 100644 --- a/Example/ReactiveDataDisplayManager/Collection.storyboard +++ b/Example/ReactiveDataDisplayManager/Collection.storyboard @@ -1,9 +1,9 @@ - + - + @@ -678,8 +678,8 @@ - - + + diff --git a/Example/ReactiveDataDisplayManager/Collection/AlignedCollectionViewController/AlignedCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/AlignedCollectionViewController/AlignedCollectionViewController.swift index 9e8bedfb5..641f25e15 100644 --- a/Example/ReactiveDataDisplayManager/Collection/AlignedCollectionViewController/AlignedCollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/AlignedCollectionViewController/AlignedCollectionViewController.swift @@ -23,6 +23,7 @@ final class AlignedCollectionViewController: UIViewController { // MARK: - Private Properties private lazy var adapter = collectionView.rddm.baseBuilder + .add(plugin: .accessibility()) .build() // MARK: - UIViewController diff --git a/Example/ReactiveDataDisplayManager/Collection/CarouselCollectionViewLayout/CarouselCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/CarouselCollectionViewLayout/CarouselCollectionViewController.swift index a84a322ef..2cad34270 100644 --- a/Example/ReactiveDataDisplayManager/Collection/CarouselCollectionViewLayout/CarouselCollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/CarouselCollectionViewLayout/CarouselCollectionViewController.swift @@ -26,6 +26,7 @@ final class CarouselCollectionViewController: UIViewController { // MARK: - Private Properties private lazy var adapter = collectionView.rddm.baseBuilder + .add(plugin: .accessibility()) .build() // MARK: - UIViewController diff --git a/Example/ReactiveDataDisplayManager/Collection/CollectionCompositionalViewController/CollectionCompositionalViewController.swift b/Example/ReactiveDataDisplayManager/Collection/CollectionCompositionalViewController/CollectionCompositionalViewController.swift index 4439fcb5a..e8927eef5 100644 --- a/Example/ReactiveDataDisplayManager/Collection/CollectionCompositionalViewController/CollectionCompositionalViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/CollectionCompositionalViewController/CollectionCompositionalViewController.swift @@ -19,7 +19,12 @@ final class CollectionCompositionalViewController: UIViewController { // MARK: - Constants private enum Constants { - static let boundaryItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(50.0)) + static let boundaryItemSize: NSCollectionLayoutSize = { + let estimatedHeight = TitleCollectionReusableView.getHeight(forWidth: UIScreen.main.bounds.width, + with: "Some section") + return NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(estimatedHeight)) + }() static let edgeInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10) static let fraction: CGFloat = 1.0 / 2 static let minScale: CGFloat = 0.8 @@ -40,6 +45,7 @@ final class CollectionCompositionalViewController: UIViewController { .add(plugin: prefetcherablePlugin) .add(plugin: scrrollPlugin) .add(plugin: .scrollOnSelect(to: .centeredHorizontally)) + .add(plugin: .accessibility()) .build() // MARK: - UIViewController @@ -96,7 +102,9 @@ private extension CollectionCompositionalViewController { for index in 0...11 { // Create generator let needIndexTitle = index % 2 == 0 ? true : false - let generator = TitleCollectionGenerator(model: "Item \(index)", needIndexTitle: needIndexTitle) + let generator = TitleCollectionGenerator(model: "Item \(index)", + referencedWidth: 128, + needIndexTitle: needIndexTitle) // Add generator to adapter adapter += generator @@ -184,7 +192,24 @@ private extension CollectionCompositionalViewController { let footer = makeSectionFooter() // Item - let item = makeItem(with: .init(width: 0.33, height: 1.0)) + let item: NSCollectionLayoutItem = { + + #if swift(>=5.9) + if #available(iOS 17.0, *) { + let item = makeItem(with: makeAutoLayoutSize(for: .init(width: 128, height: 128))) + item.edgeSpacing = NSCollectionLayoutEdgeSpacing(leading: .fixed(12), + top: .fixed(12), + trailing: .fixed(12), + bottom: .fixed(12)) + return item + } else { + return makeItem(with: makeLayoutSize(for: .init(width: 0.33, height: 1))) + } + #else + return makeItem(with: makeLayoutSize(for: .init(width: 0.33, height: 1))) + #endif + + }() // Group let group = NSCollectionLayoutGroup.horizontal(layoutSize: makeLayoutSize(for: .init(width: 1.0, height: 0.2)), subitems: [item]) @@ -204,10 +229,10 @@ private extension CollectionCompositionalViewController { let footer = makeSectionFooter() // Item medium image - let leadingItem = makeItem(with: .init(width: 0.7, height: 1.0)) + let leadingItem = makeItem(with: makeLayoutSize(for: .init(width: 0.7, height: 1.0))) // Item small image - let trailingItem = makeItem(with: .init(width: 1.0, height: 0.3)) + let trailingItem = makeItem(with: makeLayoutSize(for: .init(width: 1.0, height: 0.3))) // Group combine 2 small image let trailingGroup = NSCollectionLayoutGroup.vertical(layoutSize: makeLayoutSize(for: .init(width: 0.3, height: 1.0)), @@ -219,7 +244,7 @@ private extension CollectionCompositionalViewController { subitems: [leadingItem, trailingGroup]) // Item long image) - let topItem = makeItem(with: .init(width: 1.0, height: 0.3)) + let topItem = makeItem(with: makeLayoutSize(for: .init(width: 1.0, height: 0.3))) // Main Group long image / medium image | 2 small image let nestedGroup = NSCollectionLayoutGroup.vertical(layoutSize: makeLayoutSize(for: .init(width: 1.0, height: 0.4)), @@ -244,8 +269,8 @@ private extension CollectionCompositionalViewController { alignment: .bottom) } - func makeItem(with size: CGSize, contentInsets: NSDirectionalEdgeInsets = Constants.edgeInsets) -> NSCollectionLayoutItem { - let layoutSize = makeLayoutSize(for: size) + func makeItem(with layoutSize: NSCollectionLayoutSize, + contentInsets: NSDirectionalEdgeInsets = Constants.edgeInsets) -> NSCollectionLayoutItem { let item = NSCollectionLayoutItem(layoutSize: layoutSize) item.contentInsets = contentInsets return item @@ -256,4 +281,12 @@ private extension CollectionCompositionalViewController { heightDimension: .fractionalHeight(size.height)) } + #if swift(>=5.9) + @available(iOS 17.0, *) + func makeAutoLayoutSize(for estimatedSize: CGSize) -> NSCollectionLayoutSize { + return NSCollectionLayoutSize(widthDimension: .uniformAcrossSiblings(estimate: estimatedSize.width), + heightDimension: .estimated(estimatedSize.height)) + } + #endif + } diff --git a/Example/ReactiveDataDisplayManager/Collection/CollectionListViewController/CollectionListViewController.swift b/Example/ReactiveDataDisplayManager/Collection/CollectionListViewController/CollectionListViewController.swift index d64a00a29..4d74b7e0d 100644 --- a/Example/ReactiveDataDisplayManager/Collection/CollectionListViewController/CollectionListViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/CollectionListViewController/CollectionListViewController.swift @@ -20,6 +20,7 @@ class CollectionListViewController: UIViewController { private lazy var adapter = collectionView.rddm.baseBuilder .add(plugin: .selectable()) + .add(plugin: .accessibility()) .build() private var titles = ["Item 1", "Item 2", "Item 3", "Item 4"] diff --git a/Example/ReactiveDataDisplayManager/Collection/CollectionViewController/CollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/CollectionViewController/CollectionViewController.swift index 1130b0f87..bbc6b5e7a 100644 --- a/Example/ReactiveDataDisplayManager/Collection/CollectionViewController/CollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/CollectionViewController/CollectionViewController.swift @@ -23,8 +23,10 @@ class CollectionViewController: UIViewController { // MARK: - Private Properties private lazy var adapter = collectionView.rddm.baseBuilder + .set(delegate: FlowCollectionDelegate()) .add(plugin: .selectable()) .add(plugin: .highlightable()) + .add(plugin: .accessibility()) .build() private lazy var titles: [String] = ["One", "Two", "Three", "Four"] @@ -62,7 +64,7 @@ private extension CollectionViewController { CollectionGenerators { titles.map { title -> CollectionCellGenerator in // Create generator - let generator = TitleCollectionViewCell.rddm.baseGenerator(with: title) + let generator = TitleCollectionViewCell.rddm.calculatableHeightGenerator(with: title, referencedWidth: 100) generator.didSelectEvent += { debugPrint("\(title) selected") } diff --git a/Example/ReactiveDataDisplayManager/Collection/DiffableCollectionViewController/DiffableCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/DiffableCollectionViewController/DiffableCollectionViewController.swift index eee631ecb..f4ec6bab8 100644 --- a/Example/ReactiveDataDisplayManager/Collection/DiffableCollectionViewController/DiffableCollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/DiffableCollectionViewController/DiffableCollectionViewController.swift @@ -46,6 +46,7 @@ final class DiffableCollectionViewController: UIViewController { private lazy var adapter = collectionView.rddm.baseBuilder .set(dataSource: { DiffableCollectionDataSource(provider: $0) }) + .add(plugin: .accessibility()) .build() private var generators: [DiffableGenerator] = [] @@ -84,6 +85,7 @@ private extension DiffableCollectionViewController { func setupSearch() { let searchBar = UISearchBar() searchBar.delegate = self + searchBar.searchTextField.accessibilityLabel = "Search field" navigationItem.titleView = searchBar } diff --git a/Example/ReactiveDataDisplayManager/Collection/DifferenceCollectionViewController/DifferenceCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/DifferenceCollectionViewController/DifferenceCollectionViewController.swift index 90fb0b612..0de1af2b4 100644 --- a/Example/ReactiveDataDisplayManager/Collection/DifferenceCollectionViewController/DifferenceCollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/DifferenceCollectionViewController/DifferenceCollectionViewController.swift @@ -42,7 +42,9 @@ final class DifferenceCollectionViewController: UIViewController { // MARK: - Private Properties - private lazy var adapter = collectionView.rddm.baseBuilder.build() + private lazy var adapter = collectionView.rddm.baseBuilder + .add(plugin: .accessibility()) + .build() private var generators: [DiffableGenerator] = [] @@ -86,6 +88,11 @@ private extension DifferenceCollectionViewController { func setupSearch() { let searchBar = UISearchBar() searchBar.delegate = self + if #available(iOS 13.0, *) { + searchBar.searchTextField.accessibilityLabel = "Search field" + } else { + // Fallback on earlier versions + } navigationItem.titleView = searchBar } diff --git a/Example/ReactiveDataDisplayManager/Collection/DragAndDroppableCollectionViewController/DragAndDroppableCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/DragAndDroppableCollectionViewController/DragAndDroppableCollectionViewController.swift index 5a951fdaf..28d98bf36 100644 --- a/Example/ReactiveDataDisplayManager/Collection/DragAndDroppableCollectionViewController/DragAndDroppableCollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/DragAndDroppableCollectionViewController/DragAndDroppableCollectionViewController.swift @@ -37,13 +37,14 @@ final class DragAndDroppableCollectionViewController: UIViewController { .add(featurePlugin: .dragAndDroppable(by: .current, draggableParameters: draggableParameters, positionChanged: { print($0.id ?? "") })) + .add(plugin: .accessibility()) .build() // MARK: - UIViewController override func viewDidLoad() { super.viewDidLoad() - title = "Collection with drag'n'drop items" + title = "drag'n'drop items" configureLayoutFlow() collectionView.accessibilityIdentifier = "Collection_with_drag_n_drop_items" @@ -86,7 +87,8 @@ private extension DragAndDroppableCollectionViewController { var generators = [TitleCollectionGenerator]() for index in range { - let generator = TitleCollectionGenerator(model: "Cell: \(index)") + let generator = TitleCollectionGenerator(model: "Cell: \(index)", + referencedWidth: 128) generators.append(generator) } diff --git a/Example/ReactiveDataDisplayManager/Collection/DynamicHeightViewController/DynamicHeightViewController.swift b/Example/ReactiveDataDisplayManager/Collection/DynamicHeightViewController/DynamicHeightViewController.swift index d71e72962..60be8f579 100644 --- a/Example/ReactiveDataDisplayManager/Collection/DynamicHeightViewController/DynamicHeightViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/DynamicHeightViewController/DynamicHeightViewController.swift @@ -16,7 +16,9 @@ final class DynamicHeightViewController: UIViewController { // MARK: - Private Properties - private lazy var adapter = tableView.rddm.manualBuilder.build() + private lazy var adapter = tableView.rddm.manualBuilder + .add(plugin: .accessibility()) + .build() // MARK: - UIViewController diff --git a/Example/ReactiveDataDisplayManager/Collection/FoldableCollectionViewController/FoldableCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/FoldableCollectionViewController/FoldableCollectionViewController.swift index 732e9c519..af329e46b 100644 --- a/Example/ReactiveDataDisplayManager/Collection/FoldableCollectionViewController/FoldableCollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/FoldableCollectionViewController/FoldableCollectionViewController.swift @@ -26,6 +26,7 @@ final class FoldableCollectionViewController: UIViewController { private lazy var adapter = collectionView.rddm.baseBuilder .add(plugin: .foldable()) + .add(plugin: .accessibility()) .build() private var appearance: Appearance = .grid diff --git a/Example/ReactiveDataDisplayManager/Collection/ImageCollectionViewController/ImageCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/ImageCollectionViewController/ImageCollectionViewController.swift index acf4de162..11af7891f 100644 --- a/Example/ReactiveDataDisplayManager/Collection/ImageCollectionViewController/ImageCollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/ImageCollectionViewController/ImageCollectionViewController.swift @@ -28,6 +28,7 @@ final class ImageCollectionViewController: UIViewController { // MARK: - Private Properties private lazy var adapter = collectionView.rddm.baseBuilder + .add(plugin: .accessibility()) .build() // MARK: - UIViewController diff --git a/Example/ReactiveDataDisplayManager/Collection/ImageHorizontalCollectionViewController/ImageHorizontalCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/ImageHorizontalCollectionViewController/ImageHorizontalCollectionViewController.swift index 2ba77ff50..405e22a99 100644 --- a/Example/ReactiveDataDisplayManager/Collection/ImageHorizontalCollectionViewController/ImageHorizontalCollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/ImageHorizontalCollectionViewController/ImageHorizontalCollectionViewController.swift @@ -36,6 +36,7 @@ final class ImageHorizontalCollectionViewController: UIViewController { private lazy var adapter = collectionView.rddm.baseBuilder .add(plugin: .scrollableBehaviour(scrollProvider: scrollManager)) .add(plugin: .scrollOnSelect(to: .centeredHorizontally)) + .add(plugin: .accessibility()) .build() // MARK: - UIViewController diff --git a/Example/ReactiveDataDisplayManager/Collection/ItemTitleCollectionViewController/ItemTitleCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/ItemTitleCollectionViewController/ItemTitleCollectionViewController.swift index 71593c13a..fb07710d2 100644 --- a/Example/ReactiveDataDisplayManager/Collection/ItemTitleCollectionViewController/ItemTitleCollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/ItemTitleCollectionViewController/ItemTitleCollectionViewController.swift @@ -26,14 +26,16 @@ class ItemTitleCollectionViewController: UIViewController { private var appearance: Appearance = .grid private lazy var adapter = collectionView.rddm.baseBuilder + .set(delegate: FlowCollectionDelegate()) .add(featurePlugin: .sectionTitleDisplayable()) + .add(plugin: .accessibility()) .build() // MARK: - UIViewController override func viewDidLoad() { super.viewDidLoad() - title = "Collection with item index titles" + title = "item index titles" configureLayoutFlow(with: appearance) updateBarButtonItem(with: appearance.title) @@ -70,7 +72,9 @@ private extension ItemTitleCollectionViewController { for index in 0...50 { // Create generator - let generator = TitleCollectionGenerator(model: "Item \(index)", needIndexTitle: index % 2 == 0 ? true : false) + let generator = TitleCollectionGenerator(model: "Item \(index)", + referencedWidth: 128, + needIndexTitle: index % 2 == 0 ? true : false) // Add generator to adapter adapter += generator diff --git a/Example/ReactiveDataDisplayManager/Collection/MainCollectionViewController/MainCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/MainCollectionViewController/MainCollectionViewController.swift index 09a50da57..226690d91 100644 --- a/Example/ReactiveDataDisplayManager/Collection/MainCollectionViewController/MainCollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/MainCollectionViewController/MainCollectionViewController.swift @@ -49,16 +49,16 @@ final class MainCollectionViewController: UIViewController { ("Horizontal image collection", .imageHorizontalCollection), ("Sizable collection", .sizableCollection), ("Foldable collection", .foldableCollection), - ("Collection with item index titles", .itemTitleCollection), - ("Collection with diffableDataSource", .diffableCollection), - ("Collection with pagination", .paginatableCollection), - ("Collection with two direction pagination", .twoDirectionPaginatableCollection), - ("Collection with compositional layout", .compositionalCollection), - ("Collection with DifferenceKit", .differenceCollection), - ("List Appearances with swipeable items", .swipeableListAppearances), - ("Collection with movable items", .movableCollection), - ("Collection with drag and drop item", .dragAndDroppableCollection), - ("Collection with stack cell", .stackCellCollectionViewController), + ("item index titles", .itemTitleCollection), + ("diffableDataSource", .diffableCollection), + ("pagination", .paginatableCollection), + ("back/forward pagination", .twoDirectionPaginatableCollection), + ("compositional layout", .compositionalCollection), + ("DifferenceKit", .differenceCollection), + ("list with swipes", .swipeableListAppearances), + ("movable items", .movableCollection), + ("drag and drop item", .dragAndDroppableCollection), + ("stack cell inside", .stackCellCollectionViewController), ("Carousel collection view layout", .carouselCollection), ("Aligned collection layout", .alignedCollection), ("Dynamic height ViewController", .dynamicHeightViewController) @@ -73,6 +73,7 @@ final class MainCollectionViewController: UIViewController { private lazy var adapter = tableView.rddm.manualBuilder .add(plugin: .selectable()) + .add(plugin: .accessibility()) .build() // MARK: - UIViewController diff --git a/Example/ReactiveDataDisplayManager/Collection/MovableCollectionViewController/MovableCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/MovableCollectionViewController/MovableCollectionViewController.swift index 183bca08a..e858f30a9 100644 --- a/Example/ReactiveDataDisplayManager/Collection/MovableCollectionViewController/MovableCollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/MovableCollectionViewController/MovableCollectionViewController.swift @@ -30,7 +30,9 @@ final class MovableCollectionViewController: UIViewController { private let movablePlugin: CollectionMovableItemPlugin = .movable() private lazy var adapter = collectionView.rddm.baseBuilder + .set(delegate: FlowCollectionDelegate()) .add(featurePlugin: .movable(cellDidChangePosition: { print($0.id ?? "") })) + .add(plugin: .accessibility()) .build() // MARK: - UIViewController @@ -47,7 +49,7 @@ final class MovableCollectionViewController: UIViewController { private extension MovableCollectionViewController { func setupInitialState() { - title = "Collection with movable cell" + title = "movable cell" configureCollectionView() fillAdapter() @@ -78,7 +80,11 @@ private extension MovableCollectionViewController { // Create cells generators func makeMovableCellGenerators() -> [MovableCollectionCellGenerator] { - return Constants.models.enumerated().map { MovableCollectionCellGenerator(id: $0.offset, model: "\($0.element) \($0.offset)") } + Constants.models.enumerated() + .map { MovableCollectionCellGenerator(id: $0.offset, + model: "\($0.element) \($0.offset)", + referencedWidth: 128) + } } } diff --git a/Example/ReactiveDataDisplayManager/Collection/PaginatableCollectionViewController/PaginatableCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/PaginatableCollectionViewController/PaginatableCollectionViewController.swift index 3471153f1..cb9974226 100644 --- a/Example/ReactiveDataDisplayManager/Collection/PaginatableCollectionViewController/PaginatableCollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/PaginatableCollectionViewController/PaginatableCollectionViewController.swift @@ -29,6 +29,7 @@ final class PaginatableCollectionViewController: UIViewController { private lazy var adapter = collectionView.rddm.baseBuilder .add(plugin: .paginatable(progressView: progressView, output: self)) + .add(plugin: .accessibility()) .build() private weak var paginatableInput: PaginatableInput? @@ -40,7 +41,7 @@ final class PaginatableCollectionViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - title = "Collection with pagination" + title = "pagination" configureActivityIndicatorIfNeeded() loadFirstPage() @@ -100,7 +101,8 @@ private extension PaginatableCollectionViewController { func makeGenerator() -> CollectionCellGenerator { let title = "Random cell \(Int.random(in: 0...1000)) from page \(currentPage)" - return TitleCollectionViewCell.rddm.baseGenerator(with: title) + return TitleCollectionViewCell.rddm.calculatableHeightGenerator(with: title, + referencedWidth: 200) } func canFillNext() -> Bool { diff --git a/Example/ReactiveDataDisplayManager/Collection/PrefetchingCollectionViewController/PrefetchingCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/PrefetchingCollectionViewController/PrefetchingCollectionViewController.swift index 6a6ccddae..a35f828cb 100644 --- a/Example/ReactiveDataDisplayManager/Collection/PrefetchingCollectionViewController/PrefetchingCollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/PrefetchingCollectionViewController/PrefetchingCollectionViewController.swift @@ -32,6 +32,7 @@ final class PrefetchingCollectionViewController: UIViewController { private lazy var adapter = collectionView.rddm.baseBuilder .add(plugin: prefetcherablePlugin) + .add(plugin: .accessibility()) .build() // MARK: - UIViewController diff --git a/Example/ReactiveDataDisplayManager/Collection/RefreshableCollectionViewController/RefreshableCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/RefreshableCollectionViewController/RefreshableCollectionViewController.swift index c7b7ae60a..91a5e7169 100644 --- a/Example/ReactiveDataDisplayManager/Collection/RefreshableCollectionViewController/RefreshableCollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/RefreshableCollectionViewController/RefreshableCollectionViewController.swift @@ -33,6 +33,7 @@ final class RefreshableCollectionViewController: UIViewController { private lazy var adapter = collectionView.rddm.baseBuilder .add(plugin: .refreshable(refreshControl: refreshControl, output: self)) + .add(plugin: .accessibility()) .build() // MARK: - UIViewController diff --git a/Example/ReactiveDataDisplayManager/Collection/SizableCollectionViewController/SizableCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/SizableCollectionViewController/SizableCollectionViewController.swift index ad5637f45..2f0f86888 100644 --- a/Example/ReactiveDataDisplayManager/Collection/SizableCollectionViewController/SizableCollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/SizableCollectionViewController/SizableCollectionViewController.swift @@ -26,6 +26,7 @@ final class SizableCollectionViewController: UIViewController { private lazy var adapter = collectionView.rddm.baseBuilder .set(delegate: FlowCollectionDelegate()) + .add(plugin: .accessibility()) .build() // MARK: - UIViewController diff --git a/Example/ReactiveDataDisplayManager/Collection/StackCellExampleCollectionViewController/StackCellExampleCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/StackCellExampleCollectionViewController/StackCellExampleCollectionViewController.swift index 1e65704d6..3d7aa094b 100644 --- a/Example/ReactiveDataDisplayManager/Collection/StackCellExampleCollectionViewController/StackCellExampleCollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/StackCellExampleCollectionViewController/StackCellExampleCollectionViewController.swift @@ -32,6 +32,7 @@ final class StackCellExampleCollectionViewController: UIViewController { .set(delegate: FlowCollectionDelegate()) .add(plugin: .highlightable()) .add(plugin: .selectable()) + .add(plugin: .accessibility()) .build() lazy var horizontalNestedStackCell = HorizontalCollectionStack { @@ -52,7 +53,7 @@ final class StackCellExampleCollectionViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - title = "Stack cell collection" + title = "Stack incide collection" let flowLayout = makeFlowLayout() collectionView.setCollectionViewLayout(flowLayout, animated: false) diff --git a/Example/ReactiveDataDisplayManager/Collection/SwipeableCollectionListViewController/SwipeableCollectionListViewController.swift b/Example/ReactiveDataDisplayManager/Collection/SwipeableCollectionListViewController/SwipeableCollectionListViewController.swift index 86277425c..815583bdf 100644 --- a/Example/ReactiveDataDisplayManager/Collection/SwipeableCollectionListViewController/SwipeableCollectionListViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/SwipeableCollectionListViewController/SwipeableCollectionListViewController.swift @@ -28,6 +28,7 @@ final class SwipeableCollectionListViewController: UIViewController { private lazy var plugin = CollectionSwipeActionsConfigurationPlugin(swipeProvider: swipeProvider) private lazy var adapter = collectionView.rddm.baseBuilder .add(featurePlugin: plugin) + .add(plugin: .accessibility()) .build() private var appearance = UICollectionLayoutListConfiguration.Appearance.plain @@ -36,7 +37,7 @@ final class SwipeableCollectionListViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - title = "List Appearances with swipeable items" + title = "list with swipes" fillAdapter() configureLayoutFlow(with: appearance) diff --git a/Example/ReactiveDataDisplayManager/Collection/TwoDirectionPaginatableCollectionViewController/TwoDirectionPaginatableCollectionViewController.swift b/Example/ReactiveDataDisplayManager/Collection/TwoDirectionPaginatableCollectionViewController/TwoDirectionPaginatableCollectionViewController.swift index aa8d35d8d..f65514bb1 100644 --- a/Example/ReactiveDataDisplayManager/Collection/TwoDirectionPaginatableCollectionViewController/TwoDirectionPaginatableCollectionViewController.swift +++ b/Example/ReactiveDataDisplayManager/Collection/TwoDirectionPaginatableCollectionViewController/TwoDirectionPaginatableCollectionViewController.swift @@ -63,7 +63,7 @@ final class TwoDirectionPaginatableCollectionViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - title = "Collection with two direction pagination" + title = "two directional pagination" configureActivityIndicatorIfNeeded() loadFirstPage() diff --git a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/DynamicHeightViewCell/DynamicHeightTableViewCell.swift b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/DynamicHeightViewCell/DynamicHeightTableViewCell.swift index 954801ce6..aa3116ce6 100644 --- a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/DynamicHeightViewCell/DynamicHeightTableViewCell.swift +++ b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/DynamicHeightViewCell/DynamicHeightTableViewCell.swift @@ -28,7 +28,9 @@ final class DynamicHeightTableViewCell: UITableViewCell, ConfigurableItem, Const // MARK: - Private Properties - private lazy var adapter = collectionView.rddm.baseBuilder.build() + private lazy var adapter = collectionView.rddm.baseBuilder + .add(plugin: .accessibility()) + .build() // MARK: - UITableViewCell diff --git a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/DynamicHeightViewCell/DynamicHeightTableViewCell.xib b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/DynamicHeightViewCell/DynamicHeightTableViewCell.xib index 18eb627f8..d84bc3405 100644 --- a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/DynamicHeightViewCell/DynamicHeightTableViewCell.xib +++ b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/DynamicHeightViewCell/DynamicHeightTableViewCell.xib @@ -1,9 +1,9 @@ - + - + @@ -30,13 +30,13 @@ diff --git a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/DynamicHeightViewCell/Subviews/RectangleColorCollectionViewCell.swift b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/DynamicHeightViewCell/Subviews/RectangleColorCollectionViewCell.swift index 0f882a602..8f12a8050 100644 --- a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/DynamicHeightViewCell/Subviews/RectangleColorCollectionViewCell.swift +++ b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/DynamicHeightViewCell/Subviews/RectangleColorCollectionViewCell.swift @@ -8,16 +8,26 @@ import UIKit import ReactiveDataDisplayManager -final class RectangleColorCollectionViewCell: UICollectionViewCell, ConfigurableItem { +final class RectangleColorCollectionViewCell: UICollectionViewCell, ConfigurableItem, AccessibilityItem { // MARK: - @IBOutlets @IBOutlet private weak var colorView: UIView! + // MARK: - AccessibilityItem + + var labelStrategy: AccessibilityStringStrategy = .ignored + var traitsStrategy: AccessibilityTraitsStrategy { .just(.none) } + // MARK: - ConfigurableItem func configure(with model: UIColor) { colorView.backgroundColor = model + if #available(iOS 14.0, *) { + labelStrategy = .just(model.accessibilityName) + } else { + labelStrategy = .just("some color") + } colorView.layer.cornerRadius = 20 } diff --git a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/FittingCompressedSizeCollectionViewCell/FittingCompressedSizeCollectionViewCell.swift b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/FittingCompressedSizeCollectionViewCell/FittingCompressedSizeCollectionViewCell.swift index 904b687e4..128251146 100644 --- a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/FittingCompressedSizeCollectionViewCell/FittingCompressedSizeCollectionViewCell.swift +++ b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/FittingCompressedSizeCollectionViewCell/FittingCompressedSizeCollectionViewCell.swift @@ -50,6 +50,13 @@ extension FittingCompressedSizeCollectionViewCell: ConfigurableItem { } +extension FittingCompressedSizeCollectionViewCell: AccessibilityItem { + + var labelStrategy: AccessibilityStringStrategy { .from(titleLabel) } + var traitsStrategy: AccessibilityTraitsStrategy { .from(titleLabel) } + +} + // MARK: - Configuration private extension FittingCompressedSizeCollectionViewCell { diff --git a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/FittingCompressedSizeCollectionViewCell/FittingCompressedSizeCollectionViewCell.xib b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/FittingCompressedSizeCollectionViewCell/FittingCompressedSizeCollectionViewCell.xib index 63fc16654..5b000cb6c 100644 --- a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/FittingCompressedSizeCollectionViewCell/FittingCompressedSizeCollectionViewCell.xib +++ b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/FittingCompressedSizeCollectionViewCell/FittingCompressedSizeCollectionViewCell.xib @@ -1,9 +1,9 @@ - + - + @@ -19,7 +19,7 @@ diff --git a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/FoldableCollectionViewCell/FoldableCollectionViewCell.swift b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/FoldableCollectionViewCell/FoldableCollectionViewCell.swift index f5fc46bd0..68904ee52 100644 --- a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/FoldableCollectionViewCell/FoldableCollectionViewCell.swift +++ b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/FoldableCollectionViewCell/FoldableCollectionViewCell.swift @@ -46,7 +46,7 @@ final class FoldableCollectionViewCell: UICollectionViewCell, FoldableStateHolde } -// MARK: - Configurable +// MARK: - ConfigurableItem extension FoldableCollectionViewCell: ConfigurableItem { @@ -56,6 +56,15 @@ extension FoldableCollectionViewCell: ConfigurableItem { } +// MARK: - AccessibilityItem + +extension FoldableCollectionViewCell: AccessibilityItem { + + var labelStrategy: AccessibilityStringStrategy { .just("Expandable item") } + var traitsStrategy: AccessibilityTraitsStrategy { .just(Optional.none) } + +} + // MARK: - Configuration private extension FoldableCollectionViewCell { diff --git a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/Generators/MovableCollectionCellGenerator.swift b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/Generators/MovableCollectionCellGenerator.swift index c69b95c8b..53f6bb950 100644 --- a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/Generators/MovableCollectionCellGenerator.swift +++ b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/Generators/MovableCollectionCellGenerator.swift @@ -14,11 +14,24 @@ final class MovableCollectionCellGenerator: BaseCollectionCellGenerator CGSize { + .init(width: referencedWidth, + height: TitleCollectionListCell.getHeight(forWidth: referencedWidth, with: model) + ) + } + +} diff --git a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/Generators/TitleCollectionGenerator.swift b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/Generators/TitleCollectionGenerator.swift index 3c5bba2da..31ef5bfa6 100644 --- a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/Generators/TitleCollectionGenerator.swift +++ b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/Generators/TitleCollectionGenerator.swift @@ -15,6 +15,7 @@ final class TitleCollectionGenerator: BaseCollectionCellGenerator CGSize { + .init(width: referencedWidth, + height: TitleCollectionViewCell.getHeight(forWidth: referencedWidth, with: title) + ) + } + +} diff --git a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/Generators/VerticalSizableTextGenerator.swift b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/Generators/VerticalSizableTextGenerator.swift index 8fc0fae61..160b013ec 100644 --- a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/Generators/VerticalSizableTextGenerator.swift +++ b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/Generators/VerticalSizableTextGenerator.swift @@ -31,7 +31,8 @@ final class VerticalSizableTextGenerator: BaseCollectionCellGenerator CGSize { - return SizableCollectionViewCell.getCellSize(for: text, withWight: maxWight) + .init(width: maxWight, + height: SizableCollectionViewCell.getHeight(forWidth: maxWight, with: text)) } } diff --git a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/ImageCollectionViewCell/ImageCollectionViewCell.swift b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/ImageCollectionViewCell/ImageCollectionViewCell.swift index 750c42ee5..06eb7b956 100644 --- a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/ImageCollectionViewCell/ImageCollectionViewCell.swift +++ b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/ImageCollectionViewCell/ImageCollectionViewCell.swift @@ -44,7 +44,7 @@ final class ImageCollectionViewCell: UICollectionViewCell { } -// MARK: - Configurable +// MARK: - ConfigurableItem extension ImageCollectionViewCell: ConfigurableItem { @@ -54,6 +54,15 @@ extension ImageCollectionViewCell: ConfigurableItem { } +// MARK: - Configurable + +extension ImageCollectionViewCell: AccessibilityItem { + + var labelStrategy: AccessibilityStringStrategy { .just("some image") } + var traitsStrategy: AccessibilityTraitsStrategy { .just(.image) } + +} + // MARK: - Configuration private extension ImageCollectionViewCell { diff --git a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/SizableCollectionViewCell/SizableCollectionViewCell.swift b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/SizableCollectionViewCell/SizableCollectionViewCell.swift index 756c456b5..728968936 100644 --- a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/SizableCollectionViewCell/SizableCollectionViewCell.swift +++ b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/SizableCollectionViewCell/SizableCollectionViewCell.swift @@ -11,12 +11,6 @@ import ReactiveDataDisplayManager final class SizableCollectionViewCell: UICollectionViewCell { - // MARK: - Constants - - private enum Constants { - static let titleFont = UIFont.systemFont(ofSize: 15.0) - } - // MARK: - IBOutlet @IBOutlet private weak var titleLabel: UILabel! @@ -28,16 +22,9 @@ final class SizableCollectionViewCell: UICollectionViewCell { setupInitialState() } - // MARK: - Static Methods - - static func getCellSize(for viewModel: String, withWight wight: CGFloat) -> CGSize { - let height = viewModel.getHeight(withConstrainedWidth: wight, font: Constants.titleFont) - return CGSize(width: wight, height: height) - } - } -// MARK: - Configurable +// MARK: - ConfigurableItem extension SizableCollectionViewCell: ConfigurableItem { @@ -47,6 +34,26 @@ extension SizableCollectionViewCell: ConfigurableItem { } +// MARK: - CalculatableHeightItem + +extension SizableCollectionViewCell: CalculatableHeightItem { + + static func getHeight(forWidth width: CGFloat, with model: String) -> CGFloat { + model.getHeight(withConstrainedWidth: width, + font: .preferredFont(forTextStyle: .subheadline)) + } + +} + +// MARK: - AccessibilityItem + +extension SizableCollectionViewCell: AccessibilityItem { + + var labelStrategy: AccessibilityStringStrategy { .from(titleLabel) } + var traitsStrategy: AccessibilityTraitsStrategy { .from(titleLabel) } + +} + // MARK: - Configuration private extension SizableCollectionViewCell { @@ -54,7 +61,7 @@ private extension SizableCollectionViewCell { func setupInitialState() { // configure titleLabel titleLabel.numberOfLines = 0 - titleLabel.font = Constants.titleFont + titleLabel.font = .preferredFont(forTextStyle: .subheadline) } } diff --git a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/SizableCollectionViewCell/SizableCollectionViewCell.xib b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/SizableCollectionViewCell/SizableCollectionViewCell.xib index c4caf4425..28420237b 100644 --- a/Example/ReactiveDataDisplayManager/Collection/Views/Cells/SizableCollectionViewCell/SizableCollectionViewCell.xib +++ b/Example/ReactiveDataDisplayManager/Collection/Views/Cells/SizableCollectionViewCell/SizableCollectionViewCell.xib @@ -1,25 +1,25 @@ - + - + - + -