Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Examples/PopoversExample/Playground/PopoverReaderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@ struct PopoverReaderViewBackground: View {
Circle()
.fill(Color.blue, strokeBorder: Color.white, lineWidth: 3)
.frame(width: 16, height: 16)
.position(context.frame.point(at: .top))
.position(context.frame.point(at: .top, isRightToLeft: context.isRightToLeft))
.zIndex(1)

Templates.CurveConnector(
start: context.frame.point(at: .top),
end: context.window.frameTagged("Frame-Tagged View").point(at: .bottom)
start: context.frame.point(at: .top, isRightToLeft: context.isRightToLeft),
end: context.window.frameTagged("Frame-Tagged View").point(at: .bottom, isRightToLeft: context.isRightToLeft)
)
.stroke(
Color.blue,
Expand All @@ -88,7 +88,7 @@ struct PopoverReaderViewBackground: View {
.fill(Color.blue, strokeBorder: Color.white, lineWidth: 3)
.frame(width: 16, height: 16)
.position(
context.window.frameTagged("Frame-Tagged View").point(at: .bottom)
context.window.frameTagged("Frame-Tagged View").point(at: .bottom, isRightToLeft: context.isRightToLeft)
)
.zIndex(1)
}
Expand Down
6 changes: 3 additions & 3 deletions Examples/PopoversExample/Showroom/TipView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ struct TipView: View {
PopoverReader { context in

Templates.CurveConnector(
start: context.frame.point(at: .bottom),
end: context.window.frameTagged("TipView").point(at: .top)
start: context.frame.point(at: .bottom, isRightToLeft: context.isRightToLeft),
end: context.window.frameTagged("TipView").point(at: .top, isRightToLeft: context.isRightToLeft)
)
.stroke(
Color(UIColor(hex: 0xFFAD46)),
Expand All @@ -57,7 +57,7 @@ struct TipView: View {
.fill(Color(UIColor(hex: 0xFFAD46)))
.frame(width: 16, height: 16)
.position(
context.window.frameTagged("TipView").point(at: .top)
context.window.frameTagged("TipView").point(at: .top, isRightToLeft: context.isRightToLeft)
)
}
}
Expand Down
28 changes: 19 additions & 9 deletions Sources/Popover+Calculations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,28 @@ public extension Popover {
originAnchor: originAnchor,
popoverAnchor: popoverAnchor,
originFrame: attributes.sourceFrame().inset(by: attributes.sourceFrameInset),
popoverSize: size ?? .zero
popoverSize: size ?? .zero,
isRightToLeft: context.isRightToLeft
)

let screenEdgePadding = attributes.screenEdgePadding

let leadingPadding = context.isRightToLeft ? screenEdgePadding.right : screenEdgePadding.left
let trailingPadding = context.isRightToLeft ? screenEdgePadding.left : screenEdgePadding.right

let safeWindowFrame = window.safeAreaLayoutGuide.layoutFrame
let maxX = safeWindowFrame.maxX - screenEdgePadding.right

let minX = safeWindowFrame.minX + leadingPadding
let maxX = safeWindowFrame.maxX - trailingPadding
let minY = safeWindowFrame.minY + screenEdgePadding.top
let maxY = safeWindowFrame.maxY - screenEdgePadding.bottom

/// Popover overflows on left/top side.
if popoverFrame.origin.x < screenEdgePadding.left {
popoverFrame.origin.x = screenEdgePadding.left
if popoverFrame.origin.x < minX {
popoverFrame.origin.x = minX
}
if popoverFrame.origin.y < screenEdgePadding.top {
popoverFrame.origin.y = screenEdgePadding.top
if popoverFrame.origin.y < minY {
popoverFrame.origin.y = minY
}

/// Popover overflows on the right/bottom side.
Expand All @@ -66,7 +73,8 @@ public extension Popover {
let popoverFrame = attributes.position.relativeFrame(
selectedAnchor: context.selectedAnchor ?? popoverAnchors.first ?? .bottom,
containerFrame: attributes.sourceFrame().inset(by: attributes.sourceFrameInset),
popoverSize: size ?? .zero
popoverSize: size ?? .zero,
isRightToLeft: context.isRightToLeft
)

return popoverFrame
Expand Down Expand Up @@ -118,12 +126,14 @@ public extension Popover {
popoverAnchors: popoverAnchors,
containerFrame: frame,
popoverSize: size,
targetPoint: point
targetPoint: point,
isRightToLeft: context.isRightToLeft
)
let popoverFrame = attributes.position.relativeFrame(
selectedAnchor: closestAnchor,
containerFrame: frame,
popoverSize: size
popoverSize: size,
isRightToLeft: context.isRightToLeft
)

context.selectedAnchor = closestAnchor
Expand Down
6 changes: 5 additions & 1 deletion Sources/Popover+Context.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ public extension Popover {
return UIWindow()
}
}


public var isRightToLeft: Bool {
window.effectiveUserInterfaceLayoutDirection == .rightToLeft
}

/**
The bounds of the window in which the `Popover` is being presented, or the `zero` frame if the popover has not been presented yet.
*/
Expand Down
113 changes: 55 additions & 58 deletions Sources/Popover+Positioning.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,53 +24,33 @@ public extension CGRect {
| |
X──────────────X──────────────X
bottomLeft bottom bottomRight

- parameter at: The positional anchor of the popover.
- parameter isRightToLeft: Indicates if the device's orientation is inverted.
*/
func point(at anchor: Popover.Attributes.Position.Anchor) -> CGPoint {
func point(at anchor: Popover.Attributes.Position.Anchor, isRightToLeft: Bool) -> CGPoint {
let leadingX = isRightToLeft ? origin.x + width : origin.x
let trailingX = isRightToLeft ? origin.x : origin.x + width

switch anchor {
case .topLeft:
return origin
case .top:
return CGPoint(
x: origin.x + width / 2,
y: origin.y
)
case .topRight:
return CGPoint(
x: origin.x + width,
y: origin.y
)
case .right:
return CGPoint(
x: origin.x + width,
y: origin.y + height / 2
)
case .bottomRight:
return CGPoint(
x: origin.x + width,
y: origin.y + height
)
case .bottom:
return CGPoint(
x: origin.x + width / 2,
y: origin.y + height
)
case .bottomLeft:
return CGPoint(
x: origin.x,
y: origin.y + height
)
case .left:
return CGPoint(
x: origin.x,
y: origin.y + height / 2
)
case .center:
return CGPoint(
x: origin.x + width / 2,
y: origin.y + height / 2
)
}
case .topLeft:
return CGPoint(x: leadingX, y: origin.y)
case .top:
return CGPoint(x: origin.x + width / 2, y: origin.y)
case .topRight:
return CGPoint(x: trailingX, y: origin.y)
case .right:
return CGPoint(x: trailingX, y: origin.y + height / 2)
case .bottomRight:
return CGPoint(x: trailingX, y: origin.y + height)
case .bottom:
return CGPoint(x: origin.x + width / 2, y: origin.y + height)
case .bottomLeft:
return CGPoint(x: leadingX, y: origin.y + height)
case .left:
return CGPoint(x: leadingX, y: origin.y + height / 2)
case .center:
return CGPoint(x: origin.x + width / 2, y: origin.y + height / 2)
}
}
}

Expand All @@ -81,15 +61,17 @@ public extension Popover.Attributes.Position {
- parameter popoverAnchor: The anchor of the popover that attaches to `originAnchor`.
- parameter originFrame: The source frame.
- parameter popoverSize: The size of the popover.
- parameter isRightToLeft: Indicates if the device's orientation is inverted.
*/
func absoluteFrame(
originAnchor: Anchor,
popoverAnchor: Anchor,
originFrame: CGRect,
popoverSize: CGSize
popoverSize: CGSize,
isRightToLeft: Bool
) -> CGRect {
/// Get the origin point from the origin frame.
let popoverOrigin = originFrame.point(at: originAnchor)
let popoverOrigin = originFrame.point(at: originAnchor, isRightToLeft: isRightToLeft)

/// Adjust `popoverOrigin` to account for `popoverAnchor.`
switch popoverAnchor {
Expand Down Expand Up @@ -146,16 +128,25 @@ public extension Popover.Attributes.Position {
- parameter popoverAnchor: The popover's position within the container frame.
- parameter containerFrame: The reference frame.
- parameter popoverSize: The size of the popover.
- parameter isRightToLeft: Indicates if the device's orientation is inverted.
*/
func relativeOrigin(
popoverAnchor: Anchor,
containerFrame: CGRect,
popoverSize: CGSize
popoverSize: CGSize,
isRightToLeft: Bool,
) -> CGPoint {
let physicalLeftX = containerFrame.origin.x
let physicalRightX = containerFrame.origin.x + containerFrame.width - popoverSize.width

/// Flip origin points based on layout orientation
let leadingX = isRightToLeft ? physicalRightX : physicalLeftX
let trailingX = isRightToLeft ? physicalLeftX : physicalRightX

switch popoverAnchor {
case .topLeft:
return CGPoint(
x: containerFrame.origin.x,
x: leadingX,
y: containerFrame.origin.y
)
case .top:
Expand All @@ -165,17 +156,17 @@ public extension Popover.Attributes.Position {
)
case .topRight:
return CGPoint(
x: containerFrame.origin.x + containerFrame.width - popoverSize.width,
x: trailingX,
y: containerFrame.origin.y
)
case .right:
return CGPoint(
x: containerFrame.origin.x + containerFrame.width - popoverSize.width,
x: trailingX,
y: containerFrame.origin.y + containerFrame.height / 2 - popoverSize.height / 2
)
case .bottomRight:
return CGPoint(
x: containerFrame.origin.x + containerFrame.width - popoverSize.width,
x: trailingX,
y: containerFrame.origin.y + containerFrame.height - popoverSize.height
)
case .bottom:
Expand All @@ -185,12 +176,12 @@ public extension Popover.Attributes.Position {
)
case .bottomLeft:
return CGPoint(
x: containerFrame.origin.x,
x: leadingX,
y: containerFrame.origin.y + containerFrame.height - popoverSize.height
)
case .left:
return CGPoint(
x: containerFrame.origin.x,
x: leadingX,
y: containerFrame.origin.y + containerFrame.height / 2 - popoverSize.height / 2
)
case .center:
Expand All @@ -207,19 +198,22 @@ public extension Popover.Attributes.Position {
- parameter containerFrame: The reference frame.
- parameter popoverSize: The size of the popover.
- parameter targetPoint: The point to check for the closest anchor.
- parameter isRightToLeft: Indicates if the device's orientation is inverted.
*/
func relativeClosestAnchor(
popoverAnchors: [Anchor],
containerFrame: CGRect,
popoverSize: CGSize,
targetPoint: CGPoint
targetPoint: CGPoint,
isRightToLeft: Bool
) -> Popover.Attributes.Position.Anchor {
var (closestAnchor, closestDistance): (Popover.Attributes.Position.Anchor, CGFloat) = (.bottom, .infinity)
for popoverAnchor in popoverAnchors {
let origin = relativeOrigin(
popoverAnchor: popoverAnchor,
containerFrame: containerFrame,
popoverSize: popoverSize
popoverSize: popoverSize,
isRightToLeft: isRightToLeft
)

/// Comparing distances, so no need to square the distance (saves processing power).
Expand All @@ -238,16 +232,19 @@ public extension Popover.Attributes.Position {
- parameter selectedAnchor: The popover's position within the container frame.
- parameter containerFrame: The reference frame.
- parameter popoverSize: The size of the popover.
- parameter isRightToLeft: Indicates if the device's orientation is inverted.
*/
func relativeFrame(
selectedAnchor: Popover.Attributes.Position.Anchor,
containerFrame: CGRect,
popoverSize: CGSize
popoverSize: CGSize,
isRightToLeft: Bool
) -> CGRect {
let origin = relativeOrigin(
popoverAnchor: selectedAnchor,
containerFrame: containerFrame,
popoverSize: popoverSize
popoverSize: popoverSize,
isRightToLeft: isRightToLeft
)

let frame = CGRect(origin: origin, size: popoverSize)
Expand Down
26 changes: 18 additions & 8 deletions Sources/PopoverContainerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,13 @@ struct PopoverContainerView: View {
.onChanged { value in

func update() {
let adjustedTranslation = CGSize(
width: popover.context.isRightToLeft ? -value.translation.width : value.translation.width,
height: value.translation.height
)

/// Apply the offset.
applyDraggingOffset(popover: popover, translation: value.translation)
applyDraggingOffset(popover: popover, translation: adjustedTranslation)

/// Update the visual frame to account for the dragging offset.
popover.context.frame = CGRect(
Expand All @@ -119,10 +124,13 @@ struct PopoverContainerView: View {
}
}
.onEnded { value in
let horizontalTranslation = popover.context.isRightToLeft
? -value.predictedEndTranslation.width
: value.predictedEndTranslation.width

/// The expected dragging end point.
let finalOrigin = CGPoint(
x: popover.context.staticFrame.origin.x + value.predictedEndTranslation.width,
x: popover.context.staticFrame.origin.x + horizontalTranslation,
y: popover.context.staticFrame.origin.y + value.predictedEndTranslation.height
)

Expand Down Expand Up @@ -184,14 +192,16 @@ struct PopoverContainerView: View {

/// Get the offset of a popover in order to place it in its correct location.
func popoverOffset(for popover: Popover) -> CGSize {
guard popover.context.size != nil else { return .zero }
guard let size = popover.context.size else { return .zero }
let frame = popover.context.staticFrame
let offset = CGSize(
width: frame.origin.x + ((selectedPopover == popover) ? selectedPopoverOffset.width : 0),
height: frame.origin.y + ((selectedPopover == popover) ? selectedPopoverOffset.height : 0)
)

return offset
let adjustedHorizontalOffset: CGFloat = if popover.context.isRightToLeft {
popover.context.window.frame.width - frame.origin.x - size.width
} else {
frame.origin.x
}

return CGSize(width: adjustedHorizontalOffset, height: frame.origin.y)
}

// MARK: - Dragging
Expand Down
Loading