diff --git a/BlueprintUI/Sources/Layout/LayoutAttributes.swift b/BlueprintUI/Sources/Layout/LayoutAttributes.swift index 3b42f5f29..18d7c8fff 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 = .automatic 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,7 @@ public struct LayoutAttributes { view.alpha = alpha view.isUserInteractionEnabled = isUserInteractionEnabled view.isHidden = isHidden + view.tintAdjustmentMode = tintAdjustmentMode } @@ -156,6 +162,15 @@ public struct LayoutAttributes { result.isUserInteractionEnabled = layoutAttributes.isUserInteractionEnabled && isUserInteractionEnabled result.isHidden = layoutAttributes.isHidden || isHidden + switch tintAdjustmentMode { + case .dimmed, .normal: + result.tintAdjustmentMode = tintAdjustmentMode + case .automatic: + result.tintAdjustmentMode = layoutAttributes.tintAdjustmentMode + @unknown default: + result.tintAdjustmentMode = layoutAttributes.tintAdjustmentMode + } + return result } @@ -267,6 +282,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..fa8b0c9cd 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 = .automatic } @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..9388dbeb3 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,60 @@ final class LayoutAttributesTests: XCTestCase { XCTAssertTrue(combined.isHidden) } } + + func test_concat_tintAdjustmentMode() { + do { + /// combined adopts child attribute if child is non-`.automatic` + var a = LayoutAttributes() + a.tintAdjustmentMode = .automatic + + var b = LayoutAttributes() + b.tintAdjustmentMode = .normal + + let combined = b.within(a) + + XCTAssertEqual(combined.tintAdjustmentMode, .normal) + } + + do { + /// combined adopts child attribute if both child and parent are non-`.automatic` + var a = LayoutAttributes() + a.tintAdjustmentMode = .dimmed + + var b = LayoutAttributes() + b.tintAdjustmentMode = .normal + + let combined = b.within(a) + + XCTAssertEqual(combined.tintAdjustmentMode, .normal) + } + + do { + /// combined inherits from parent if child is `.automatic` + var a = LayoutAttributes() + a.tintAdjustmentMode = .normal + + var b = LayoutAttributes() + 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) + } + } } final class LayoutAttributesTests_CGRect: XCTestCase { @@ -243,4 +304,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/CHANGELOG.md b/CHANGELOG.md index ca1ac64c6..6c6194ea8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added a `TintAdjustmentMode` element and `.tintAdjustmentMode(:)` modifier for finer control of tint color during modal presentations. + ### Removed ### Changed