Skip to content

Commit

Permalink
Mark protocols with MainActor so they can be applied to UIKit types a…
Browse files Browse the repository at this point in the history
…nd adjust all call sites to match
  • Loading branch information
jszumski committed May 29, 2024
1 parent 0205655 commit da34c78
Show file tree
Hide file tree
Showing 17 changed files with 113 additions and 33 deletions.
12 changes: 12 additions & 0 deletions Example/ParalayoutSnapshotTests/ViewAlignmentSnapshotTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import SnapshotTesting

final class ViewAlignmentSnapshotTests: SnapshotTestCase {

@MainActor
func testSiblingAlignment() {
let containerView = UIView(frame: .init(x: 0, y: 0, width: 200, height: 200))
containerView.backgroundColor = .white
Expand All @@ -31,6 +32,7 @@ final class ViewAlignmentSnapshotTests: SnapshotTestCase {
secondSubview.backgroundColor = .red
containerView.addSubview(secondSubview)

@MainActor
func verifySnapshot(
receiverPosition: Position,
targetPosition: Position,
Expand Down Expand Up @@ -80,6 +82,7 @@ final class ViewAlignmentSnapshotTests: SnapshotTestCase {
verifySnapshot(receiverPosition: .center, targetPosition: .topRight, verticalOffset: -15)
}

@MainActor
func testLayoutDirection() {
let containerView = UIView(frame: .init(x: 0, y: 0, width: 100, height: 100))
containerView.backgroundColor = .white
Expand Down Expand Up @@ -140,6 +143,7 @@ final class ViewAlignmentSnapshotTests: SnapshotTestCase {
assertSnapshot(matching: containerView, as: .image, named: nameForSnapshot(with: []))
}

@MainActor
func testTransformHasNoEffect() {
let containerView = UIView(frame: .init(x: 0, y: 0, width: 100, height: 100))
containerView.backgroundColor = .white
Expand All @@ -152,6 +156,7 @@ final class ViewAlignmentSnapshotTests: SnapshotTestCase {
secondSubview.backgroundColor = .red
containerView.addSubview(secondSubview)

@MainActor
func verifySnapshot(
receiverTransform: CGAffineTransform,
targetTransform: CGAffineTransform,
Expand Down Expand Up @@ -185,6 +190,7 @@ final class ViewAlignmentSnapshotTests: SnapshotTestCase {
verifySnapshot(receiverTransform: .identity, targetTransform: .init(scaleX: 2, y: 3))
}

@MainActor
func testNonZeroBoundsOrigin() {
let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
containerView.backgroundColor = .white
Expand Down Expand Up @@ -213,6 +219,7 @@ final class ViewAlignmentSnapshotTests: SnapshotTestCase {
assertSnapshot(matching: containerView, as: .image, named: nameForSnapshot(with: []))
}

@MainActor
func testAlignmentWithLayoutMargins() {
let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
containerView.backgroundColor = .white
Expand Down Expand Up @@ -246,6 +253,7 @@ final class ViewAlignmentSnapshotTests: SnapshotTestCase {
assertSnapshot(matching: containerView, as: .image, named: nameForSnapshot(with: ["bothLayoutMargins"]))
}

@MainActor
func testAlignmentUsingCapInsets() {
let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 250, height: 100))
containerView.backgroundColor = .white
Expand All @@ -269,6 +277,7 @@ final class ViewAlignmentSnapshotTests: SnapshotTestCase {
assertSnapshot(matching: containerView, as: .image, named: nameForSnapshot(with: []))
}

@MainActor
func testAlignmentUsingFirstLine() {
let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 250, height: 100))
containerView.backgroundColor = .white
Expand Down Expand Up @@ -299,6 +308,7 @@ final class ViewAlignmentSnapshotTests: SnapshotTestCase {
assertSnapshot(matching: containerView, as: .image, named: nameForSnapshot(with: []))
}

@MainActor
func testAlignmentUsingFirstLineCapInsets() {
let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 250, height: 100))
containerView.backgroundColor = .white
Expand Down Expand Up @@ -329,6 +339,7 @@ final class ViewAlignmentSnapshotTests: SnapshotTestCase {
assertSnapshot(matching: containerView, as: .image, named: nameForSnapshot(with: []))
}

@MainActor
func testAlignmentWithRect() {
let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
containerView.backgroundColor = .white
Expand Down Expand Up @@ -365,6 +376,7 @@ final class ViewAlignmentSnapshotTests: SnapshotTestCase {
assertSnapshot(matching: containerView, as: .image, named: nameForSnapshot(with: []))
}

@MainActor
func testAlignmentWithFrame() {
let targetTransform = CGAffineTransform(translationX: -20, y: 10)
let receiverTransform = CGAffineTransform(scaleX: 0.8, y: 0.8)
Expand Down
4 changes: 4 additions & 0 deletions Paralayout/Alignable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ import UIKit
/// Describes an object that can participate in alignment. In practice, this represents a view.
public protocol Alignable {

@MainActor
var alignmentContext: AlignmentContext { get }

}

@MainActor
public struct AlignmentContext {

// MARK: - Life Cycle
Expand All @@ -47,6 +49,7 @@ public struct AlignmentContext {

extension UIView: Alignable {

@MainActor
public var alignmentContext: AlignmentContext {
return AlignmentContext(view: self, alignmentBounds: bounds)
}
Expand Down Expand Up @@ -168,6 +171,7 @@ public struct FrameAlignmentProxy: Alignable {

// MARK: - Private Methods

@MainActor
private func withViewInSuperview<T>(view: UIView, perform: (_ superview: UIView) -> T) -> T {
if let superview = view.superview {
return perform(superview)
Expand Down
21 changes: 11 additions & 10 deletions Paralayout/AspectRatio.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,15 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {
///
/// - parameter width: The desired width.
/// - parameter scaleFactor: The view/window/screen to use for pixel rounding.
public func height(forWidth width: CGFloat, in scaleFactor: ScaleFactorProviding) -> CGFloat {
@MainActor public func height(forWidth width: CGFloat, in scaleFactor: ScaleFactorProviding) -> CGFloat {
return (ratioHeight * width / ratioWidth).roundedToPixel(in: scaleFactor)
}

/// Returns the width of the aspect ratio for a given `height` rounded to the nearest pixel.
///
/// - parameter height: The desired height.
/// - parameter scaleFactor: The view/window/screen to use for pixel rounding.
public func width(forHeight height: CGFloat, in scaleFactor: ScaleFactorProviding) -> CGFloat {
@MainActor public func width(forHeight height: CGFloat, in scaleFactor: ScaleFactorProviding) -> CGFloat {
return (ratioWidth * height / ratioHeight).roundedToPixel(in: scaleFactor)
}

Expand All @@ -122,7 +122,7 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {
///
/// - parameter width: The desired width.
/// - parameter scaleFactor: The view/window/screen to use for pixel rounding.
public func size(forWidth width: CGFloat, in scaleFactor: ScaleFactorProviding) -> CGSize {
@MainActor public func size(forWidth width: CGFloat, in scaleFactor: ScaleFactorProviding) -> CGSize {
return CGSize(
width: width,
height: height(forWidth: width, in: scaleFactor)
Expand All @@ -134,7 +134,7 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {
///
/// - parameter height: The desired height.
/// - parameter scaleFactor: The view/window/screen to use for pixel rounding.
public func size(forHeight height: CGFloat, in scaleFactor: ScaleFactorProviding) -> CGSize {
@MainActor public func size(forHeight height: CGFloat, in scaleFactor: ScaleFactorProviding) -> CGSize {
return CGSize(
width: width(forHeight: height, in: scaleFactor),
height: height
Expand All @@ -147,7 +147,7 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {
/// - parameter size: The bounding size.
/// - parameter scaleFactor: The view/window/screen to use for pixel alignment.
/// - returns: A size with the receiver's aspect ratio, no larger than the bounding size.
public func size(toFit size: CGSize, in scaleFactor: ScaleFactorProviding) -> CGSize {
@MainActor public func size(toFit size: CGSize, in scaleFactor: ScaleFactorProviding) -> CGSize {
if size.aspectRatio <= self {
// Match width, narrow the height.
let fitHeight = min(size.height, height(forWidth: size.width, in: scaleFactor))
Expand All @@ -169,7 +169,7 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {
/// - parameter scaleFactor: The view/window/screen to use for pixel alignment.
/// - parameter layoutDirection: The effective layout direction of the view in which the `rect` is defined.
/// - returns: A rect with the receiver's aspect ratio, strictly within the bounding rect.
public func rect(
@MainActor public func rect(
toFit rect: CGRect,
at position: Position,
in scaleFactor: ScaleFactorProviding,
Expand All @@ -193,7 +193,7 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {
/// - parameter context: The view/window/screen that provides the scale factor and effective layout direction in
/// which the rect should be positioned.
/// - returns: A rect with the receiver's aspect ratio, strictly within the bounding rect.
public func rect(
@MainActor public func rect(
toFit rect: CGRect,
at position: Position,
in context: (ScaleFactorProviding & LayoutDirectionProviding)
Expand All @@ -212,7 +212,7 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {
/// - parameter size: The bounding size.
/// - parameter scaleFactor: The view/window/screen to use for pixel alignment.
/// - returns: A size with the receiver's aspect ratio, at least as large as the bounding size.
public func size(toFill size: CGSize, in scaleFactor: ScaleFactorProviding) -> CGSize {
@MainActor public func size(toFill size: CGSize, in scaleFactor: ScaleFactorProviding) -> CGSize {
if size.aspectRatio <= self {
// Match height, expand the width.
let fillWidth = width(forHeight: size.height, in: scaleFactor)
Expand All @@ -234,7 +234,7 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {
/// - parameter scaleFactor: The view/window/screen to use for pixel alignment.
/// - parameter layoutDirection: The effective layout direction of the view in which the `rect` is defined.
/// - returns: A rect with the receiver's aspect ratio, strictly containing the bounding rect.
public func rect(
@MainActor public func rect(
toFill rect: CGRect,
at position: Position,
in scaleFactor: ScaleFactorProviding,
Expand All @@ -257,7 +257,7 @@ public struct AspectRatio: Comparable, CustomDebugStringConvertible, Sendable {
/// - parameter context: The view/window/screen that provides the scale factor and effective layout direction in
/// which the rect should be positioned.
/// - returns: A rect with the receiver's aspect ratio, strictly containing the bounding rect.
public func rect(
@MainActor public func rect(
toFill rect: CGRect,
at position: Position,
in context: (ScaleFactorProviding & LayoutDirectionProviding)
Expand Down Expand Up @@ -294,6 +294,7 @@ extension CGRect {

// MARK: - Life Cycle

@MainActor
fileprivate init(
size newSize: CGSize,
at position: Position,
Expand Down
1 change: 1 addition & 0 deletions Paralayout/LayoutDirection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import UIKit
/// Defines an object that vends its current user interface layout direction.
public protocol LayoutDirectionProviding {

@MainActor
var effectiveUserInterfaceLayoutDirection: UIUserInterfaceLayoutDirection { get }

}
Expand Down
24 changes: 13 additions & 11 deletions Paralayout/PixelRounding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import UIKit
/// The ratio of pixels to points, either of a UIScreen, a UIView's screen, or an explicit value.
public protocol ScaleFactorProviding {

@MainActor
var pixelsPerPoint: CGFloat { get }

}
Expand Down Expand Up @@ -76,7 +77,7 @@ extension CGFloat {
/// - parameter scaleFactor: The pixel scale to use, e.g. a UIScreen, UIView, or explicit value (pass `0` to *not*
/// snap to pixel).
/// - returns: The adjusted coordinate.
public func flooredToPixel(in scaleFactor: ScaleFactorProviding) -> CGFloat {
@MainActor public func flooredToPixel(in scaleFactor: ScaleFactorProviding) -> CGFloat {
return adjustedToPixel(scaleFactor) { floor($0) }
}

Expand All @@ -85,7 +86,7 @@ extension CGFloat {
/// - parameter scaleFactor: The pixel scale to use, e.g. a UIScreen, UIView, or explicit value (pass `0` to *not*
/// snap to pixel).
/// - returns: The adjusted coordinate.
public func ceiledToPixel(in scaleFactor: ScaleFactorProviding) -> CGFloat {
@MainActor public func ceiledToPixel(in scaleFactor: ScaleFactorProviding) -> CGFloat {
return adjustedToPixel(scaleFactor) { ceil($0) }
}

Expand All @@ -94,14 +95,15 @@ extension CGFloat {
/// - parameter scaleFactor: The pixel scale to use, e.g. a UIScreen, UIView, or explicit value (pass `0` to *not*
/// snap to pixel).
/// - returns: The adjusted coordinate.
public func roundedToPixel(in scaleFactor: ScaleFactorProviding) -> CGFloat {
@MainActor public func roundedToPixel(in scaleFactor: ScaleFactorProviding) -> CGFloat {
// Invoke the namespaced Darwin.round() function since round() is ambiguous (it's also a mutating instance
// method).
return adjustedToPixel(scaleFactor) { Darwin.round($0) }
}

// MARK: - Private Methods

@MainActor
private func adjustedToPixel(_ scaleFactor: ScaleFactorProviding, _ adjustment: (CGFloat) -> CGFloat) -> CGFloat {
let scale = scaleFactor.pixelsPerPoint
return (scale > 0.0) ? (adjustment(self * scale) / scale) : self
Expand All @@ -117,7 +119,7 @@ extension CGPoint {
/// - parameter scaleFactor: The pixel scale to use, e.g. a UIScreen, UIView, or explicit value (pass `0` to *not*
/// snap to pixel).
/// - returns: The adjusted coordinate.
public func flooredToPixel(in scaleFactor: ScaleFactorProviding) -> CGPoint {
@MainActor public func flooredToPixel(in scaleFactor: ScaleFactorProviding) -> CGPoint {
return CGPoint(x: x.flooredToPixel(in: scaleFactor), y: y.flooredToPixel(in: scaleFactor))
}

Expand All @@ -127,7 +129,7 @@ extension CGPoint {
/// - parameter scaleFactor: The pixel scale to use, e.g. a UIScreen, UIView, or explicit value (pass `0` to *not*
/// snap to pixel).
/// - returns: The adjusted coordinate.
public func ceiledToPixel(in scaleFactor: ScaleFactorProviding) -> CGPoint {
@MainActor public func ceiledToPixel(in scaleFactor: ScaleFactorProviding) -> CGPoint {
return CGPoint(x: x.ceiledToPixel(in: scaleFactor), y: y.ceiledToPixel(in: scaleFactor))
}

Expand All @@ -137,7 +139,7 @@ extension CGPoint {
/// - parameter scaleFactor: The pixel scale to use, e.g. a UIScreen, UIView, or explicit value (pass `0` to *not*
/// snap to pixel).
/// - returns: The adjusted coordinate.
public func roundedToPixel(in scaleFactor: ScaleFactorProviding) -> CGPoint {
@MainActor public func roundedToPixel(in scaleFactor: ScaleFactorProviding) -> CGPoint {
return CGPoint(x: x.roundedToPixel(in: scaleFactor), y: y.roundedToPixel(in: scaleFactor))
}

Expand All @@ -150,7 +152,7 @@ extension CGSize {
/// - parameter scaleFactor: The pixel scale to use, e.g. a UIScreen, UIView, or explicit value (pass `0` to *not*
/// snap to pixel).
/// - returns: The adjusted coordinate.
public func flooredToPixel(in scaleFactor: ScaleFactorProviding) -> CGSize {
@MainActor public func flooredToPixel(in scaleFactor: ScaleFactorProviding) -> CGSize {
return CGSize(width: width.flooredToPixel(in: scaleFactor), height: height.flooredToPixel(in: scaleFactor))
}

Expand All @@ -159,7 +161,7 @@ extension CGSize {
/// - parameter scaleFactor: The pixel scale to use, e.g. a UIScreen, UIView, or explicit value (pass `0` to *not*
/// snap to pixel).
/// - returns: The adjusted coordinate.
public func ceiledToPixel(in scaleFactor: ScaleFactorProviding) -> CGSize {
@MainActor public func ceiledToPixel(in scaleFactor: ScaleFactorProviding) -> CGSize {
return CGSize(width: width.ceiledToPixel(in: scaleFactor), height: height.ceiledToPixel(in: scaleFactor))
}

Expand All @@ -168,7 +170,7 @@ extension CGSize {
/// - parameter scaleFactor: The pixel scale to use, e.g. a UIScreen, UIView, or explicit value (pass `0` to *not*
/// snap to pixel).
/// - returns: The adjusted coordinate.
public func roundedToPixel(in scaleFactor: ScaleFactorProviding) -> CGSize {
@MainActor public func roundedToPixel(in scaleFactor: ScaleFactorProviding) -> CGSize {
return CGSize(width: width.roundedToPixel(in: scaleFactor), height: height.roundedToPixel(in: scaleFactor))
}

Expand All @@ -181,7 +183,7 @@ extension CGRect {
/// - parameter scaleFactor: The pixel scale to use, e.g. a UIScreen, UIView, or explicit value (pass `0` to *not*
/// snap to pixel).
/// - returns: A new rect with pixel-aligned boundaries, enclosing the original rect.
public func expandedToPixel(in scaleFactor: ScaleFactorProviding) -> CGRect {
@MainActor public func expandedToPixel(in scaleFactor: ScaleFactorProviding) -> CGRect {
return CGRect(
left: minX.flooredToPixel(in: scaleFactor),
top: minY.flooredToPixel(in: scaleFactor),
Expand All @@ -195,7 +197,7 @@ extension CGRect {
/// - parameter scaleFactor: The pixel scale to use, e.g. a UIScreen, UIView, or explicit value (pass `0` to *not*
/// snap to pixel).
/// - returns: A new rect with pixel-aligned boundaries, enclosed by the original rect.
public func contractedToPixel(in scaleFactor: ScaleFactorProviding) -> CGRect {
@MainActor public func contractedToPixel(in scaleFactor: ScaleFactorProviding) -> CGRect {
return CGRect(
left: minX.ceiledToPixel(in: scaleFactor),
top: minY.ceiledToPixel(in: scaleFactor),
Expand Down
2 changes: 1 addition & 1 deletion Paralayout/UIFont+CapInsets.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ extension UIFont {
/// The space above and below the receiver's capHeight and baseline, as displayed in a UILabel.
/// - parameter scaleFactor: The UI scale factor for pixel rounding.
/// - returns: The insets.
public func labelCapInsets(in scaleFactor: ScaleFactorProviding) -> LabelCapInsets {
@MainActor public func labelCapInsets(in scaleFactor: ScaleFactorProviding) -> LabelCapInsets {
// One would expect ceil(ascender) - floor(descender) so that the baseline would land on a pixel boundary, but
// sadly no--this is what `UILabel.sizeToFit()` does.
let lineHeight = (ascender - descender).ceiledToPixel(in: scaleFactor)
Expand Down
1 change: 1 addition & 0 deletions Paralayout/UILabel+Alignment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public struct TextRectLayoutProxy: Alignable {

// MARK: - Alignable

@MainActor
public var alignmentContext: AlignmentContext {
var alignmentBounds = proxiedLabel.textRect(
forBounds: proxiedLabel.bounds,
Expand Down
4 changes: 2 additions & 2 deletions Paralayout/UIView+Alignment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ extension Alignable {
/// - parameter otherView: The other view for the measurement.
/// - parameter otherPosition: The position in the `otherView`'s untransformed frame to use for the measurement.
/// - returns: The offset from the receiver's `position` to the `otherView`'s `otherPosition`.
public func untransformedFrameOffset(
@MainActor public func untransformedFrameOffset(
from position: Position,
to otherView: Alignable,
_ otherPosition: Position,
Expand Down Expand Up @@ -126,7 +126,7 @@ extension Alignable {
/// calculated. Defaults to `.automatic`, which will align the views in the most common way based on their
/// relationship in the view hierarchy.
/// - parameter offset: An additional offset to apply to the alignment, e.g. to leave a space between the two views.
public func align(
@MainActor public func align(
_ position: Position,
with otherView: Alignable,
_ otherPosition: Position,
Expand Down
Loading

0 comments on commit da34c78

Please sign in to comment.