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
9 changes: 9 additions & 0 deletions Source/Internal/Handle/ActivatedOverlayArea.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ struct ActivatedOverlayArea: Equatable {
&& $0.frame != .zero
}
}

func intersectionArea(with rect: CGRect) -> CGFloat {
spots.reduce(0) { totalArea, spot in
guard spot.frame != .zero else { return totalArea }
let intersection = rect.intersection(spot.frame)
let area = intersection.width * intersection.height
return totalArea + (area > 0 ? area : 0)
}
}
}

extension ActivatedOverlayArea {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,27 @@ struct DynamicOverlayScrollViewProxy: Equatable {
}

func findScrollView(in space: UIView) -> UIScrollView? {
space.findScrollView(in: area, coordinate: space)
let allScrollViews = space.findAllScrollViews(in: area, coordinate: space)
guard !allScrollViews.isEmpty else { return nil }

// If only one ScrollView found, return it directly
if allScrollViews.count == 1 {
return allScrollViews.first
}

// Select the ScrollView with maximum intersection area with the overlay area
// When areas are equal, prefer the last one in the array
return allScrollViews.max { scrollView1, scrollView2 in
let frame1 = space.convert(scrollView1.bounds, from: scrollView1)
let frame2 = space.convert(scrollView2.bounds, from: scrollView2)
let area1 = area.intersectionArea(with: frame1)
let area2 = area.intersectionArea(with: frame2)
return area1 <= area2
}
}

func findAllScrollViews(in space: UIView) -> [UIScrollView] {
space.findAllScrollViews(in: area, coordinate: space)
}
}

Expand Down Expand Up @@ -53,4 +73,23 @@ private extension UIView {
}
return nil
}

func findAllScrollViews(in area: ActivatedOverlayArea,
coordinate: UICoordinateSpace) -> [UIScrollView] {
let frame = coordinate.convert(bounds, from: self)
guard area.intersects(frame) else { return [] }

var scrollViews: [UIScrollView] = []

if let scrollView = self as? UIScrollView {
scrollViews.append(scrollView)
}

for subview in subviews {
let foundScrollViews = subview.findAllScrollViews(in: area, coordinate: coordinate)
scrollViews.append(contentsOf: foundScrollViews)
}

return scrollViews
}
}
100 changes: 100 additions & 0 deletions Tests/DynamicOverlayTests/DrivingScrollViewModifierTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,104 @@ class DrivingScrollViewModifierTests: XCTestCase {
ViewRenderer(view: view).render()
wait(for: [expectation], timeout: 0.1)
}

func testMultipleScrollViewsSelection() {
let container = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))

// Create multiple ScrollViews with different sizes and positions
let scrollView1 = IdentifiedScrollView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
scrollView1.id = "small"

let scrollView2 = IdentifiedScrollView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
scrollView2.id = "medium"

let scrollView3 = IdentifiedScrollView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
scrollView3.id = "large"

container.addSubview(scrollView1)
container.addSubview(scrollView2)
container.addSubview(scrollView3)

// Create an overlay area that intersects all ScrollViews
let overlayArea = ActivatedOverlayArea.active(CGRect(x: 0, y: 0, width: 150, height: 150))
let proxy = DynamicOverlayScrollViewProxy(area: overlayArea)

// Should select the ScrollView with maximum intersection area
let selectedScrollView = proxy.findScrollView(in: container) as? IdentifiedScrollView
XCTAssertNotNil(selectedScrollView)
// The 150x150 overlay intersects:
// - scrollView1 (100x100): 100*100 = 10,000 area
// - scrollView2 (200x200): 150*150 = 22,500 area (maximum)
// - scrollView3 (300x300): 150*150 = 22,500 area (maximum, but added later)
// Should select scrollView3 as it's the last one with max area
XCTAssertEqual(selectedScrollView?.id, "large")
}

func testMultipleScrollViewsWithPartialOverlap() {
let container = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))

// ScrollView in top-left quadrant
let scrollView1 = IdentifiedScrollView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
scrollView1.id = "top-left"

// ScrollView in bottom-right quadrant
let scrollView2 = IdentifiedScrollView(frame: CGRect(x: 200, y: 200, width: 200, height: 200))
scrollView2.id = "bottom-right"

container.addSubview(scrollView1)
container.addSubview(scrollView2)

// Overlay area in top-left quadrant
let overlayArea = ActivatedOverlayArea.active(CGRect(x: 50, y: 50, width: 100, height: 100))
let proxy = DynamicOverlayScrollViewProxy(area: overlayArea)

let selectedScrollView = proxy.findScrollView(in: container) as? IdentifiedScrollView
XCTAssertNotNil(selectedScrollView)
XCTAssertEqual(selectedScrollView?.id, "top-left")
}

func testFindAllScrollViews() {
let container = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))

let scrollView1 = IdentifiedScrollView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
scrollView1.id = "first"

let scrollView2 = IdentifiedScrollView(frame: CGRect(x: 200, y: 0, width: 200, height: 200))
scrollView2.id = "second"

let scrollView3 = IdentifiedScrollView(frame: CGRect(x: 0, y: 200, width: 200, height: 200))
scrollView3.id = "third"

container.addSubview(scrollView1)
container.addSubview(scrollView2)
container.addSubview(scrollView3)

// Overlay area that intersects all three
let overlayArea = ActivatedOverlayArea.active(CGRect(x: 0, y: 0, width: 400, height: 400))
let proxy = DynamicOverlayScrollViewProxy(area: overlayArea)

let allScrollViews = proxy.findAllScrollViews(in: container)
XCTAssertEqual(allScrollViews.count, 3)

let ids = (allScrollViews as? [IdentifiedScrollView])?.map { $0.id }.sorted()
XCTAssertEqual(ids, ["first", "second", "third"])
}

func testNoScrollViewWhenNoIntersection() {
let container = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))

let scrollView = IdentifiedScrollView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
scrollView.id = "isolated"
container.addSubview(scrollView)

// Overlay area that doesn't intersect the ScrollView
let overlayArea = ActivatedOverlayArea.active(CGRect(x: 300, y: 300, width: 50, height: 50))
let proxy = DynamicOverlayScrollViewProxy(area: overlayArea)

let selectedScrollView = proxy.findScrollView(in: container)
XCTAssertNil(selectedScrollView)

let allScrollViews = proxy.findAllScrollViews(in: container)
XCTAssertTrue(allScrollViews.isEmpty)
}
}
37 changes: 34 additions & 3 deletions Tests/DynamicOverlayTests/Utils/ViewRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,31 @@ class ViewRenderer<V: View> {
bounds.inset(by: safeAreaInsets)
}

private var _window: UIWindow?

var window: UIWindow {
UIApplication.shared.windows.first!
if let existingWindow = _window {
return existingWindow
}

if let windowScene = UIApplication.shared.connectedScenes
.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene,
let window = windowScene.windows.first {
_window = window
return window
}

if let windowScene = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.first,
let window = windowScene.windows.first {
_window = window
return window
}

let newWindow = UIWindow(frame: UIScreen.main.bounds)
_window = newWindow
return newWindow
}

private let hostController: UIHostingController<V>
Expand All @@ -38,9 +61,17 @@ class ViewRenderer<V: View> {
}

func render() {
if window.rootViewController !== hostController {
window.rootViewController = hostController
let targetWindow = window

if targetWindow.rootViewController !== hostController {
targetWindow.rootViewController = hostController

// Window를 보이게 만들기
if !targetWindow.isKeyWindow {
targetWindow.makeKeyAndVisible()
}
}

CATransaction.flush()
}
}