Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into kve/move-cache-to-hie…
Browse files Browse the repository at this point in the history
…rarchy

* origin/main:
  release: Blueprint 5.1.0 (#528)
  chore(ios): Scripts for preparing release and changelog (#529)
  feat: `accessibilityIdentifier` can now be set on `AttributedLabel` (#524)
  Bump rexml from 3.3.6 to 3.3.9
  Prepare 5.0.1 release (#522)
  Update CHANGELOG.md
  Bumping versions to 5.0.0.
  Add precondition to `setNeedsViewHierarchyUpdate`. Expanded precondition message to encourage self-diagnosis.
  Renamed accessibility(...) to deprecated_accessibility(...) (#516)
  Annotate `updateViewHierarchyIfNeeded` and `update(node:context:)` with @mainactor and add `preconditionIsolated` runtime check
  Bump Github jobs to use macos-14
  Bump CI Xcode to 15.4 (from 15.1)
  Add priority to allow adjusting how extra space in a run/row in a flow should be used.
  • Loading branch information
kyleve committed Dec 12, 2024
2 parents 54f9c6d + 89f75ba commit da6e548
Show file tree
Hide file tree
Showing 26 changed files with 481 additions and 51 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
jobs:
build:
name: Generate docs with jazzy and publish to Github pages
runs-on: macos-13-xlarge
runs-on: macos-14-xlarge

steps:
- name: Checkout
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/env.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
xcode_version=15.1
xcode_version=15.4.0
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
jobs:
lint:
name: Lint Swift code with SwiftFormat
runs-on: macos-13-xlarge
runs-on: macos-14-xlarge

steps:
- name: Checkout
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
spm:
name: "iOS ${{ matrix.sdk }}"

runs-on: macos-13-xlarge
runs-on: macos-14-xlarge

strategy:
fail-fast: false # Don’t fail-fast so that we get all snapshot test changes
Expand Down Expand Up @@ -66,7 +66,7 @@ jobs:
cocoapods:
name: "CocoaPods"

runs-on: macos-13-xlarge
runs-on: macos-14-xlarge

steps:
- uses: actions/checkout@v4
Expand Down
29 changes: 29 additions & 0 deletions BlueprintUI/Sources/BlueprintView/BlueprintView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,16 @@ public final class BlueprintView: UIView {

/// Clears any sizing caches, invalidates the `intrinsicContentSize` of the
/// view, and marks the view as needing a layout.
@MainActor
private func setNeedsViewHierarchyUpdate() {
MainActor.preconditionIsolated(
"""
setNeedsViewHierarchyUpdate() must only execute on the main actor.
Examine your stack trace to determine what caused this to be called
off of the main actor.
"""
)

invalidateIntrinsicContentSize()
sizesThatFit.removeAll()
Expand All @@ -366,7 +375,17 @@ public final class BlueprintView: UIView {
setNeedsLayout()
}

@MainActor
private func updateViewHierarchyIfNeeded() {
MainActor.preconditionIsolated(
"""
updateViewHierarchyIfNeeded() must only execute on the main actor.
Examine your stack trace to determine what caused this to be called
off of the main actor.
"""
)

guard needsViewHierarchyUpdate || bounds != lastViewHierarchyUpdateBounds else { return }

precondition(
Expand Down Expand Up @@ -620,7 +639,17 @@ extension BlueprintView {
node.viewDescription.viewType == type(of: view)
}

@MainActor
fileprivate func update(node: NativeViewNode, context: UpdateContext) -> UpdateResult {
MainActor.preconditionIsolated(
"""
BlueprintView.NativeViewController.update(node:context:) must
only execute on the main actor.
Examine your stack trace to determine what caused this to be
called off of the main actor.
"""
)

assert(node.viewDescription.viewType == type(of: view))

Expand Down
152 changes: 122 additions & 30 deletions BlueprintUI/Sources/Layout/Flow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,25 @@ extension Flow {
case bottom
}

/// When there is extra space in a run, how the extra space should be used.
public enum Priority {

public static let `default` = Self.fixed

/// The item will take up only the space it asked for.
case fixed

/// The item will be stretched to fill any extra space in each run.
case grows

var scales: Bool {
switch self {
case .fixed: false
case .grows: true
}
}
}

/// A child placed within the flow layout.
public struct Child: ElementBuilderChild {

Expand All @@ -136,22 +155,33 @@ extension Flow {
}

/// Creates a new child item with the given element.
public init(_ element: Element, key: AnyHashable? = nil) {
public init(_ element: Element, key: AnyHashable? = nil, priority: Priority = .default) {
self.key = key
traits = .init()
self.element = element

traits = .init(priority: priority)
}

public struct Traits {}
public struct Traits {

public var priority: Priority

public init(priority: Flow.Priority = .default) {
self.priority = priority
}
}
}
}


extension Element {

/// Wraps the element in a `Flow.Child` to allow customizing the item in the flow layout.
public func flowChild(key: AnyHashable? = nil) -> Flow.Child {
.init(self, key: key)
public func flowChild(
priority: Flow.Priority = .default,
key: AnyHashable? = nil
) -> Flow.Child {
.init(self, key: key, priority: priority)
}
}

Expand All @@ -178,7 +208,12 @@ extension Flow {
cache: inout ()
) -> CGSize {
size(
for: subelements.map { $0.sizeThatFits(_:) },
for: subelements.map {
.init(
traits: $0.traits(forLayoutType: Self.self),
size: $0.sizeThatFits(_:)
)
},
in: proposal
)
}
Expand All @@ -191,7 +226,12 @@ extension Flow {
) {
zip(
frames(
for: subelements.map { $0.sizeThatFits(_:) },
for: subelements.map {
.init(
traits: $0.traits(forLayoutType: Self.self),
size: $0.sizeThatFits(_:)
)
},
in: .init(size)
),
subelements
Expand All @@ -200,10 +240,17 @@ extension Flow {
}
}

typealias ElementSize = (SizeConstraint) -> CGSize

/// Shim type. Once legacy layout is removed, we can remove this shim and just use `Child` directly.
private struct FlowChild {
typealias ElementSize = (SizeConstraint) -> CGSize

var traits: Traits
var size: ElementSize
}

private func frames(
for elements: [ElementSize],
for elements: [FlowChild],
in constraint: SizeConstraint
) -> [CGRect] {

Expand All @@ -217,7 +264,7 @@ extension Flow {
for element in elements {

let elementSize: CGSize = {
let size = element(constraint)
let size = element.size(constraint)

return CGSize(
width: min(size.width, constraint.width.maximum),
Expand All @@ -237,14 +284,19 @@ extension Flow {
)
}

row.addItem(of: elementSize)
row.add(
.init(
size: elementSize,
traits: element.traits
)
)
}

return frames + row.itemFrames()
}

private func size(
for elements: [ElementSize],
for elements: [FlowChild],
in constraint: SizeConstraint
) -> CGSize {
frames(
Expand All @@ -268,7 +320,12 @@ extension Flow {
)]
) -> CGSize {
size(
for: items.map { $0.content.measure(in:) },
for: items.map {
.init(
traits: $0.traits,
size: $0.content.measure(in:)
)
},
in: constraint
)
}
Expand All @@ -281,7 +338,12 @@ extension Flow {
)]
) -> [LayoutAttributes] {
frames(
for: items.map { $0.content.measure(in:) },
for: items.map {
.init(
traits: $0.traits,
size: $0.content.measure(in:)
)
},
in: .init(size)
).map(LayoutAttributes.init(frame:))
}
Expand Down Expand Up @@ -336,7 +398,7 @@ extension Flow.Layout {

struct Item {
let size: CGSize
let xOffset: CGFloat
let traits: Flow.Layout.Traits
}

/// `True` if we can fit an item of the given size in the row.
Expand All @@ -347,32 +409,58 @@ extension Flow.Layout {
}

/// Adds item of given size to the row layout.
mutating func addItem(of size: CGSize) {
items.append(
.init(
size: size,
xOffset: totalItemWidth + itemSpacing * CGFloat(items.count)
)
)
totalItemWidth += size.width
height = max(size.height, height)
mutating func add(_ item: Item) {
items.append(item)

totalItemWidth += item.size.width
height = max(item.size.height, height)
}

/// Compute frames for the items in the row layout.
func itemFrames() -> [CGRect] {

let totalSpacing = (CGFloat(items.count) - 1) * itemSpacing

let scalingConstant: CGFloat = items
.map {
switch $0.traits.priority {
case .fixed: 0.0
case .grows: 1.0
}
}
.reduce(0, +)

let scalableWidth = items
.filter(\.traits.priority.scales)
.map(\.size.width)
.reduce(0, +)

let hasScalingItems = scalingConstant > 0.0

let extraWidth = maxWidth - totalItemWidth - totalSpacing
let firstItemX: CGFloat = {

let firstItemX: CGFloat = if hasScalingItems {
0.0
} else {
switch lineAlignment {
case .center: extraWidth / 2.0
case .trailing: extraWidth
case .leading: 0.0
}
}()
}

var xOrigin: CGFloat = firstItemX

return items.map { item in
.init(
x: firstItemX + item.xOffset,
let percentOfScalableWidth = item.size.width / scalableWidth

let width = if item.traits.priority.scales {
item.size.width + (extraWidth * percentOfScalableWidth)
} else {
item.size.width
}

let frame = CGRect(
x: xOrigin,
y: {
switch itemAlignment {
case .fill: origin
Expand All @@ -381,14 +469,18 @@ extension Flow.Layout {
case .bottom: origin + (height - item.size.height)
}
}(),
width: item.size.width,
width: width,
height: {
switch itemAlignment {
case .fill: height
case .top, .center, .bottom: item.size.height
}
}()
)

xOrigin = frame.maxX + itemSpacing

return frame
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ extension Element {
deprecated,
renamed: "accessibilityElement(label:value:traits:hint:identifier:accessibilityFrameSize:)"
)
public func accessibility(
public func deprecated_accessibility(
label: String? = nil,
value: String? = nil,
hint: String? = nil,
Expand Down
4 changes: 4 additions & 0 deletions BlueprintUICommonControls/Sources/AttributedLabel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ public struct AttributedLabel: Element, Hashable {
/// A localized string that describes the result of performing an action on the element, when the result is non-obvious.
public var accessibilityHint: String?

/// A string that identifies the element.
public var accessibilityIdentifier: String?

/// An array containing one or more `AccessibilityElement.CustomAction`s, defining additional supported actions. Assistive technologies, such as VoiceOver, will display your custom actions to the user at appropriate times.
public var accessibilityCustomActions: [AccessibilityElement.CustomAction] = []

Expand Down Expand Up @@ -260,6 +263,7 @@ extension AttributedLabel {
isAccessibilityElement = model.isAccessibilityElement
accessibilityHint = model.accessibilityHint
accessibilityValue = model.accessibilityValue
accessibilityIdentifier = model.accessibilityIdentifier
updateAccessibilityTraits(with: model)
accessibilityCustomActions = model.accessibilityCustomActions.map { action in
UIAccessibilityCustomAction(name: action.name) { _ in action.onActivation() }
Expand Down
Loading

0 comments on commit da6e548

Please sign in to comment.