From 00d1f31169db6bc9c9a91c1088ec7a67ca65d95f Mon Sep 17 00:00:00 2001 From: Jackson Cheek Date: Fri, 15 Sep 2023 16:39:03 -0400 Subject: [PATCH 1/4] Add tintAdjustmentMode to Image --- BlueprintUICommonControls/Sources/Image.swift | 11 +++++++++++ .../Tests/Sources/ImageTests.swift | 1 + 2 files changed, 12 insertions(+) diff --git a/BlueprintUICommonControls/Sources/Image.swift b/BlueprintUICommonControls/Sources/Image.swift index 5ec12f8e2..875bd4417 100644 --- a/BlueprintUICommonControls/Sources/Image.swift +++ b/BlueprintUICommonControls/Sources/Image.swift @@ -11,6 +11,12 @@ public struct Image: Element { /// The tint color. public var tintColor: UIColor? + /// The tint adjustment mode. When this value is `.dimmed`, the view's `tintColor` + /// property returns a desaturated, dimmed version of the view's original tint color. When + /// this value is `.normal`, the view's `tintColor` property returns the completely + /// unmodified tint color. + public var tintAdjustmentMode: UIView.TintAdjustmentMode? + /// The content mode determines the layout of the image when its size does /// not precisely match the size that the element is assigned. public var contentMode: ContentMode @@ -24,11 +30,13 @@ public struct Image: Element { image: UIImage?, contentMode: ContentMode = .aspectFill, tintColor: UIColor? = nil, + tintAdjustmentMode: UIView.TintAdjustmentMode? = nil, blockAccessibilityDescription: Bool = false ) { self.image = image self.contentMode = contentMode self.tintColor = tintColor + self.tintAdjustmentMode = tintAdjustmentMode self.blockAccessibilityDescription = blockAccessibilityDescription } @@ -45,6 +53,9 @@ public struct Image: Element { config[\.contentMode] = contentMode.uiViewContentMode config[\.layer.minificationFilter] = .trilinear config[\.tintColor] = tintColor + if let tintAdjustmentMode { + config[\.tintAdjustmentMode] = tintAdjustmentMode + } if blockAccessibilityDescription { // Seting `isAccessibilityElement = false` isn't enough here, VoiceOver is very aggressive in finding images to discribe. We need to explicitly remove the `.image` trait. config[\.accessibilityTraits] = UIAccessibilityTraits.none diff --git a/BlueprintUICommonControls/Tests/Sources/ImageTests.swift b/BlueprintUICommonControls/Tests/Sources/ImageTests.swift index eb97b6506..ea2a3a723 100644 --- a/BlueprintUICommonControls/Tests/Sources/ImageTests.swift +++ b/BlueprintUICommonControls/Tests/Sources/ImageTests.swift @@ -11,6 +11,7 @@ class ImageTests: XCTestCase { let element = Image(image: image) XCTAssertEqual(element.contentMode, .aspectFill) XCTAssertNil(element.tintColor) + XCTAssertNil(element.tintAdjustmentMode) } func test_aspectFill() { From 0305eb500c9cf05ea18fa1e193145ae4e0f49fe5 Mon Sep 17 00:00:00 2001 From: Jackson Cheek Date: Fri, 15 Sep 2023 16:53:32 -0400 Subject: [PATCH 2/4] Add to CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91f1c8028..89b452253 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added `tintAdjustmentMode` to `Image` for finer control of tint color during modal presentations. + ### Removed ### Changed From 7b08f6ea25cbd028ccebd11bc4a10f577a2878e9 Mon Sep 17 00:00:00 2001 From: Jackson Cheek Date: Mon, 18 Sep 2023 17:17:33 -0400 Subject: [PATCH 3/4] Feature: add TintAdjustmentMode, modifiers, and tests --- .../Sources/Layout/LayoutAttributes.swift | 10 +++ .../Sources/Layout/LayoutSubelement.swift | 4 ++ .../Sources/Layout/TintAdjustmentMode.swift | 65 +++++++++++++++++++ BlueprintUI/Tests/LayoutAttributesTests.swift | 57 ++++++++++++++++ .../Tests/TintAdjustmentModeTests.swift | 45 +++++++++++++ BlueprintUICommonControls/Sources/Image.swift | 11 ---- .../Tests/Sources/ImageTests.swift | 1 - CHANGELOG.md | 2 +- 8 files changed, 182 insertions(+), 13 deletions(-) create mode 100644 BlueprintUI/Sources/Layout/TintAdjustmentMode.swift create mode 100644 BlueprintUI/Tests/TintAdjustmentModeTests.swift diff --git a/BlueprintUI/Sources/Layout/LayoutAttributes.swift b/BlueprintUI/Sources/Layout/LayoutAttributes.swift index 3b42f5f29..eac5d6496 100755 --- a/BlueprintUI/Sources/Layout/LayoutAttributes.swift +++ b/BlueprintUI/Sources/Layout/LayoutAttributes.swift @@ -29,6 +29,9 @@ public struct LayoutAttributes { /// Corresponds to `UIView.isHidden`. public var isHidden: Bool + /// Corresponds to `UIView.tintAdjustmentMode`. + public var tintAdjustmentMode: UIView.TintAdjustmentMode? + public init() { self.init(center: .zero, bounds: .zero) } @@ -51,6 +54,7 @@ public struct LayoutAttributes { alpha = 1.0 isUserInteractionEnabled = true isHidden = false + tintAdjustmentMode = nil validateBounds() validateCenter() @@ -64,6 +68,7 @@ public struct LayoutAttributes { alpha = attributes.alpha isUserInteractionEnabled = attributes.isUserInteractionEnabled isHidden = attributes.isHidden + tintAdjustmentMode = attributes.tintAdjustmentMode } public var frame: CGRect { @@ -88,6 +93,9 @@ public struct LayoutAttributes { view.alpha = alpha view.isUserInteractionEnabled = isUserInteractionEnabled view.isHidden = isHidden + if let tintAdjustmentMode { + view.tintAdjustmentMode = tintAdjustmentMode + } } @@ -155,6 +163,7 @@ public struct LayoutAttributes { result.alpha = alpha * layoutAttributes.alpha result.isUserInteractionEnabled = layoutAttributes.isUserInteractionEnabled && isUserInteractionEnabled result.isHidden = layoutAttributes.isHidden || isHidden + result.tintAdjustmentMode = layoutAttributes.tintAdjustmentMode ?? tintAdjustmentMode return result } @@ -267,6 +276,7 @@ extension LayoutAttributes: Equatable { && lhs.alpha == rhs.alpha && lhs.isUserInteractionEnabled == rhs.isUserInteractionEnabled && lhs.isHidden == rhs.isHidden + && lhs.tintAdjustmentMode == rhs.tintAdjustmentMode } } diff --git a/BlueprintUI/Sources/Layout/LayoutSubelement.swift b/BlueprintUI/Sources/Layout/LayoutSubelement.swift index 10ef2af16..3e03721f5 100644 --- a/BlueprintUI/Sources/Layout/LayoutSubelement.swift +++ b/BlueprintUI/Sources/Layout/LayoutSubelement.swift @@ -1,6 +1,7 @@ import CoreGraphics import Foundation import QuartzCore +import UIKit /// A collection of proxy values that represent the child elements of a layout. public typealias LayoutSubelements = [LayoutSubelement] @@ -179,6 +180,9 @@ extension LayoutSubelement { /// Corresponds to `UIView.isHidden`. public var isHidden: Bool = false + + /// Corresponds to `UIView.tintAdjustmentMode`. + public var tintAdjustmentMode: UIView.TintAdjustmentMode? } @propertyWrapper diff --git a/BlueprintUI/Sources/Layout/TintAdjustmentMode.swift b/BlueprintUI/Sources/Layout/TintAdjustmentMode.swift new file mode 100644 index 000000000..46bfa01f7 --- /dev/null +++ b/BlueprintUI/Sources/Layout/TintAdjustmentMode.swift @@ -0,0 +1,65 @@ +import CoreGraphics +import UIKit + +/// `TintAdjustmentMode` conditionally modifies the tint adjustment mode of its wrapped element. +/// +/// - Note: When a tint adjustment mode is applied, any elements within the wrapped element will adopt the parent's tint adjustment mode. +public struct TintAdjustmentMode: Element { + public var tintAdjustmentMode: UIView.TintAdjustmentMode + + public var wrappedElement: Element + + public init(_ tintAdjustmentMode: UIView.TintAdjustmentMode, wrapping element: Element) { + self.tintAdjustmentMode = tintAdjustmentMode + wrappedElement = element + } + + public var content: ElementContent { + ElementContent(child: wrappedElement, layout: Layout(tintAdjustmentMode: tintAdjustmentMode)) + } + + public func backingViewDescription(with context: ViewDescriptionContext) -> ViewDescription? { + nil + } + + private struct Layout: SingleChildLayout { + var tintAdjustmentMode: UIView.TintAdjustmentMode + + func measure(in constraint: SizeConstraint, child: Measurable) -> CGSize { + child.measure(in: constraint) + } + + func layout(size: CGSize, child: Measurable) -> LayoutAttributes { + var attributes = LayoutAttributes(size: size) + attributes.tintAdjustmentMode = tintAdjustmentMode + return attributes + } + + func sizeThatFits( + proposal: SizeConstraint, + subelement: Subelement, + environment: Environment, + cache: inout Cache + ) -> CGSize { + subelement.sizeThatFits(proposal) + } + + func placeSubelement( + in size: CGSize, + subelement: Subelement, + environment: Environment, + cache: inout () + ) { + subelement.attributes.tintAdjustmentMode = tintAdjustmentMode + } + } +} + +extension Element { + /// Conditionally modifies the tint adjustment mode of its wrapped element. + /// + /// - Note: When a tint adjustment mode is applied, any elements within the wrapped element will adopt the parent's tint adjustment mode. + public func tintAdjustmentMode(_ tintAdjustmentMode: UIView.TintAdjustmentMode) -> TintAdjustmentMode { + TintAdjustmentMode(tintAdjustmentMode, wrapping: self) + } +} diff --git a/BlueprintUI/Tests/LayoutAttributesTests.swift b/BlueprintUI/Tests/LayoutAttributesTests.swift index 0d1a6409b..f73d7078f 100644 --- a/BlueprintUI/Tests/LayoutAttributesTests.swift +++ b/BlueprintUI/Tests/LayoutAttributesTests.swift @@ -56,6 +56,13 @@ final class LayoutAttributesTests: XCTestCase { XCTAssertNotEqual(attributes, other) } + do { + /// tintAdjustmentMode + var other = attributes + other.tintAdjustmentMode = .normal + XCTAssertNotEqual(attributes, other) + } + } func testConcatAlpha() { @@ -179,6 +186,47 @@ final class LayoutAttributesTests: XCTestCase { XCTAssertTrue(combined.isHidden) } } + + func test_concat_tintAdjustmentMode() { + do { + /// parent adopts child attribute if parent not set + var a = LayoutAttributes() + a.tintAdjustmentMode = nil + + var b = LayoutAttributes() + b.tintAdjustmentMode = .normal + + let combined = b.within(a) + + XCTAssertEqual(combined.tintAdjustmentMode, .normal) + } + + do { + /// parent overrides child + var a = LayoutAttributes() + a.tintAdjustmentMode = .normal + + var b = LayoutAttributes() + b.tintAdjustmentMode = .dimmed + + let combined = b.within(a) + + XCTAssertEqual(combined.tintAdjustmentMode, .normal) + } + + do { + /// child inherits from parent + var a = LayoutAttributes() + a.tintAdjustmentMode = .normal + + var b = LayoutAttributes() + b.tintAdjustmentMode = nil + + let combined = b.within(a) + + XCTAssertEqual(combined.tintAdjustmentMode, .normal) + } + } } final class LayoutAttributesTests_CGRect: XCTestCase { @@ -243,4 +291,13 @@ final class LayoutAttributesTests_Apply: XCTestCase { attributes.apply(to: view) XCTAssertTrue(view.isHidden) } + + func test_apply_tintAdjustmentMode() { + var attributes = LayoutAttributes() + attributes.tintAdjustmentMode = .normal + + let view = UIView() + attributes.apply(to: view) + XCTAssertEqual(view.tintAdjustmentMode, .normal) + } } diff --git a/BlueprintUI/Tests/TintAdjustmentModeTests.swift b/BlueprintUI/Tests/TintAdjustmentModeTests.swift new file mode 100644 index 000000000..5dc4a44d4 --- /dev/null +++ b/BlueprintUI/Tests/TintAdjustmentModeTests.swift @@ -0,0 +1,45 @@ +import XCTest +@testable import BlueprintUI + +final class TintAdjustmentModeTests: XCTestCase { + func test() throws { + do { + let wrapped = TintAdjustmentMode(.normal, wrapping: TestElement()) + let layout = wrapped.layout(frame: CGRect(origin: .zero, size: .init(width: 10, height: 10))) + if let child = layout.findLayout(of: TestElement.self) { + XCTAssertEqual( + child.layoutAttributes.tintAdjustmentMode, + .normal + ) + } else { + XCTFail("TestElement should be a child element") + } + } + } + + func test_convenience() throws { + do { + let wrapped = TestElement().tintAdjustmentMode(.normal) + let layout = wrapped.layout(frame: CGRect(origin: .zero, size: .init(width: 10, height: 10))) + if let child = layout.findLayout(of: TestElement.self) { + XCTAssertEqual( + child.layoutAttributes.tintAdjustmentMode, + .normal + ) + } else { + XCTFail("TestElement should be a child element") + } + } + } + + /// A view-backed box to generate a native view node + struct TestElement: Element { + var content: ElementContent { + ElementContent(intrinsicSize: .init(width: 10, height: 10)) + } + + func backingViewDescription(with context: ViewDescriptionContext) -> ViewDescription? { + UIView.describe { _ in } + } + } +} diff --git a/BlueprintUICommonControls/Sources/Image.swift b/BlueprintUICommonControls/Sources/Image.swift index 875bd4417..5ec12f8e2 100644 --- a/BlueprintUICommonControls/Sources/Image.swift +++ b/BlueprintUICommonControls/Sources/Image.swift @@ -11,12 +11,6 @@ public struct Image: Element { /// The tint color. public var tintColor: UIColor? - /// The tint adjustment mode. When this value is `.dimmed`, the view's `tintColor` - /// property returns a desaturated, dimmed version of the view's original tint color. When - /// this value is `.normal`, the view's `tintColor` property returns the completely - /// unmodified tint color. - public var tintAdjustmentMode: UIView.TintAdjustmentMode? - /// The content mode determines the layout of the image when its size does /// not precisely match the size that the element is assigned. public var contentMode: ContentMode @@ -30,13 +24,11 @@ public struct Image: Element { image: UIImage?, contentMode: ContentMode = .aspectFill, tintColor: UIColor? = nil, - tintAdjustmentMode: UIView.TintAdjustmentMode? = nil, blockAccessibilityDescription: Bool = false ) { self.image = image self.contentMode = contentMode self.tintColor = tintColor - self.tintAdjustmentMode = tintAdjustmentMode self.blockAccessibilityDescription = blockAccessibilityDescription } @@ -53,9 +45,6 @@ public struct Image: Element { config[\.contentMode] = contentMode.uiViewContentMode config[\.layer.minificationFilter] = .trilinear config[\.tintColor] = tintColor - if let tintAdjustmentMode { - config[\.tintAdjustmentMode] = tintAdjustmentMode - } if blockAccessibilityDescription { // Seting `isAccessibilityElement = false` isn't enough here, VoiceOver is very aggressive in finding images to discribe. We need to explicitly remove the `.image` trait. config[\.accessibilityTraits] = UIAccessibilityTraits.none diff --git a/BlueprintUICommonControls/Tests/Sources/ImageTests.swift b/BlueprintUICommonControls/Tests/Sources/ImageTests.swift index ea2a3a723..eb97b6506 100644 --- a/BlueprintUICommonControls/Tests/Sources/ImageTests.swift +++ b/BlueprintUICommonControls/Tests/Sources/ImageTests.swift @@ -11,7 +11,6 @@ class ImageTests: XCTestCase { let element = Image(image: image) XCTAssertEqual(element.contentMode, .aspectFill) XCTAssertNil(element.tintColor) - XCTAssertNil(element.tintAdjustmentMode) } func test_aspectFill() { diff --git a/CHANGELOG.md b/CHANGELOG.md index 89b452253..b9a7cc0dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added `tintAdjustmentMode` to `Image` for finer control of tint color during modal presentations. +- Added a `TintAdjustmentMode` element and `.tintAdjustmentMode(:)` modifier for finer control of tint color during modal presentations. ### Removed From 4afb1cfc4d52a49e8e09ded64b08aea066f7247a Mon Sep 17 00:00:00 2001 From: Jackson Cheek Date: Thu, 21 Sep 2023 14:19:34 -0400 Subject: [PATCH 4/4] Update concatenation logic and unit tests --- .../Sources/Layout/LayoutAttributes.swift | 18 ++++++++----- .../Sources/Layout/LayoutSubelement.swift | 2 +- BlueprintUI/Tests/LayoutAttributesTests.swift | 27 ++++++++++++++----- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/BlueprintUI/Sources/Layout/LayoutAttributes.swift b/BlueprintUI/Sources/Layout/LayoutAttributes.swift index eac5d6496..18d7c8fff 100755 --- a/BlueprintUI/Sources/Layout/LayoutAttributes.swift +++ b/BlueprintUI/Sources/Layout/LayoutAttributes.swift @@ -30,7 +30,7 @@ public struct LayoutAttributes { public var isHidden: Bool /// Corresponds to `UIView.tintAdjustmentMode`. - public var tintAdjustmentMode: UIView.TintAdjustmentMode? + public var tintAdjustmentMode: UIView.TintAdjustmentMode public init() { self.init(center: .zero, bounds: .zero) @@ -54,7 +54,7 @@ public struct LayoutAttributes { alpha = 1.0 isUserInteractionEnabled = true isHidden = false - tintAdjustmentMode = nil + tintAdjustmentMode = .automatic validateBounds() validateCenter() @@ -93,9 +93,7 @@ public struct LayoutAttributes { view.alpha = alpha view.isUserInteractionEnabled = isUserInteractionEnabled view.isHidden = isHidden - if let tintAdjustmentMode { - view.tintAdjustmentMode = tintAdjustmentMode - } + view.tintAdjustmentMode = tintAdjustmentMode } @@ -163,7 +161,15 @@ public struct LayoutAttributes { result.alpha = alpha * layoutAttributes.alpha result.isUserInteractionEnabled = layoutAttributes.isUserInteractionEnabled && isUserInteractionEnabled result.isHidden = layoutAttributes.isHidden || isHidden - result.tintAdjustmentMode = layoutAttributes.tintAdjustmentMode ?? tintAdjustmentMode + + switch tintAdjustmentMode { + case .dimmed, .normal: + result.tintAdjustmentMode = tintAdjustmentMode + case .automatic: + result.tintAdjustmentMode = layoutAttributes.tintAdjustmentMode + @unknown default: + result.tintAdjustmentMode = layoutAttributes.tintAdjustmentMode + } return result } diff --git a/BlueprintUI/Sources/Layout/LayoutSubelement.swift b/BlueprintUI/Sources/Layout/LayoutSubelement.swift index 3e03721f5..fa8b0c9cd 100644 --- a/BlueprintUI/Sources/Layout/LayoutSubelement.swift +++ b/BlueprintUI/Sources/Layout/LayoutSubelement.swift @@ -182,7 +182,7 @@ extension LayoutSubelement { public var isHidden: Bool = false /// Corresponds to `UIView.tintAdjustmentMode`. - public var tintAdjustmentMode: UIView.TintAdjustmentMode? + public var tintAdjustmentMode: UIView.TintAdjustmentMode = .automatic } @propertyWrapper diff --git a/BlueprintUI/Tests/LayoutAttributesTests.swift b/BlueprintUI/Tests/LayoutAttributesTests.swift index f73d7078f..9388dbeb3 100644 --- a/BlueprintUI/Tests/LayoutAttributesTests.swift +++ b/BlueprintUI/Tests/LayoutAttributesTests.swift @@ -189,9 +189,9 @@ final class LayoutAttributesTests: XCTestCase { func test_concat_tintAdjustmentMode() { do { - /// parent adopts child attribute if parent not set + /// combined adopts child attribute if child is non-`.automatic` var a = LayoutAttributes() - a.tintAdjustmentMode = nil + a.tintAdjustmentMode = .automatic var b = LayoutAttributes() b.tintAdjustmentMode = .normal @@ -202,12 +202,12 @@ final class LayoutAttributesTests: XCTestCase { } do { - /// parent overrides child + /// combined adopts child attribute if both child and parent are non-`.automatic` var a = LayoutAttributes() - a.tintAdjustmentMode = .normal + a.tintAdjustmentMode = .dimmed var b = LayoutAttributes() - b.tintAdjustmentMode = .dimmed + b.tintAdjustmentMode = .normal let combined = b.within(a) @@ -215,17 +215,30 @@ final class LayoutAttributesTests: XCTestCase { } do { - /// child inherits from parent + /// combined inherits from parent if child is `.automatic` var a = LayoutAttributes() a.tintAdjustmentMode = .normal var b = LayoutAttributes() - b.tintAdjustmentMode = nil + b.tintAdjustmentMode = .automatic let combined = b.within(a) XCTAssertEqual(combined.tintAdjustmentMode, .normal) } + + do { + /// combined is `.automatic` if both parent and child attributes are `.automatic` + var a = LayoutAttributes() + a.tintAdjustmentMode = .automatic + + var b = LayoutAttributes() + b.tintAdjustmentMode = .automatic + + let combined = b.within(a) + + XCTAssertEqual(combined.tintAdjustmentMode, .automatic) + } } }