mirror of
https://github.com/rive-app/rive-ios.git
synced 2026-01-18 17:11:28 +01:00
feat(apple): skip drawing if view is offscreen (#10077) 0dee98781d
Co-authored-by: David Skuza <david@rive.app>
This commit is contained in:
@@ -1 +1 @@
|
||||
4d5037b42d9d6425da42806c652933f6a840e04c
|
||||
0dee98781dcb9cf7eea970a03e2b8ede8be3ec89
|
||||
|
||||
@@ -11,3 +11,4 @@
|
||||
|
||||
SUPPORTS_MACCATALYST=YES
|
||||
OTHER_LDFLAGS[sdk=macosx*] = -lrive_maccatalyst -lrive_harfbuzz_maccatalyst -lrive_sheenbidi_maccatalyst -lrive_yoga_maccatalyst -lminiaudio_maccatalyst -lrive_pls_renderer_maccatalyst -lrive_cg_renderer_maccatalyst -lrive_decoders_maccatalyst
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = RIVE_MAC_CATALYST
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
E5964A982A9697B600140479 /* RiveEvent.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5964A972A9697B600140479 /* RiveEvent.mm */; };
|
||||
E599DCF92AAFA06100D1E49A /* rating_animation.riv in Resources */ = {isa = PBXBuildFile; fileRef = E599DCF82AAFA06100D1E49A /* rating_animation.riv */; };
|
||||
E599DCFA2AAFA06100D1E49A /* rating_animation.riv in Resources */ = {isa = PBXBuildFile; fileRef = E599DCF82AAFA06100D1E49A /* rating_animation.riv */; };
|
||||
F20808E22E05C3FF0082A281 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F20808E12E05C3FF0082A281 /* View+Extensions.swift */; };
|
||||
F21C3D1B2DDFCD93005F82F4 /* RiveRenderImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F21C3D1A2DDFCD93005F82F4 /* RiveRenderImage+Extensions.swift */; };
|
||||
F21F08142C66526D00FFA205 /* RiveFallbackFontDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F21F08132C66526D00FFA205 /* RiveFallbackFontDescriptor.swift */; };
|
||||
F22CF1B12D380E3700D35779 /* data_binding_test.riv in Resources */ = {isa = PBXBuildFile; fileRef = F22CF1B02D380E3700D35779 /* data_binding_test.riv */; };
|
||||
@@ -120,6 +121,7 @@
|
||||
F28DE4532C5002D900F3C379 /* RiveModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F28DE4522C5002D900F3C379 /* RiveModelTests.swift */; };
|
||||
F2B29EA22D52B5EB00CB30ED /* RiveDataBindingViewModelInstancePropertyData.h in Headers */ = {isa = PBXBuildFile; fileRef = F2B29EA02D52B5EB00CB30ED /* RiveDataBindingViewModelInstancePropertyData.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
F2B29EA32D52B5EB00CB30ED /* RiveDataBindingViewModelInstancePropertyData.m in Sources */ = {isa = PBXBuildFile; fileRef = F2B29EA12D52B5EB00CB30ED /* RiveDataBindingViewModelInstancePropertyData.m */; };
|
||||
F2BE72112E05DB8E00B66C78 /* ViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2BE72102E05DB8E00B66C78 /* ViewTests.swift */; };
|
||||
F2C003E82C933D2300339E67 /* RiveMetalDrawableView.h in Headers */ = {isa = PBXBuildFile; fileRef = F2C003E72C933D2300339E67 /* RiveMetalDrawableView.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
F2C07B1B2DA9854F00DC8C84 /* WeakContainer.h in Headers */ = {isa = PBXBuildFile; fileRef = F2C07B192DA9854F00DC8C84 /* WeakContainer.h */; };
|
||||
F2C07B1C2DA9854F00DC8C84 /* WeakContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = F2C07B1A2DA9854F00DC8C84 /* WeakContainer.m */; };
|
||||
@@ -233,6 +235,7 @@
|
||||
E5964A952A965A9300140479 /* RiveEvent.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.h; path = RiveEvent.h; sourceTree = "<group>"; };
|
||||
E5964A972A9697B600140479 /* RiveEvent.mm */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.objcpp; path = RiveEvent.mm; sourceTree = "<group>"; };
|
||||
E599DCF82AAFA06100D1E49A /* rating_animation.riv */ = {isa = PBXFileReference; lastKnownFileType = file; name = rating_animation.riv; path = "Example-iOS/Assets/rating_animation.riv"; sourceTree = SOURCE_ROOT; };
|
||||
F20808E12E05C3FF0082A281 /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
|
||||
F21C3D1A2DDFCD93005F82F4 /* RiveRenderImage+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RiveRenderImage+Extensions.swift"; sourceTree = "<group>"; };
|
||||
F21F08132C66526D00FFA205 /* RiveFallbackFontDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiveFallbackFontDescriptor.swift; sourceTree = "<group>"; };
|
||||
F22CF1B02D380E3700D35779 /* data_binding_test.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = data_binding_test.riv; sourceTree = "<group>"; };
|
||||
@@ -262,6 +265,7 @@
|
||||
F2B29EA12D52B5EB00CB30ED /* RiveDataBindingViewModelInstancePropertyData.m */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.objcpp; path = RiveDataBindingViewModelInstancePropertyData.m; sourceTree = "<group>"; };
|
||||
F2BD96D52DDCC7A200E7F49A /* Catalyst.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Catalyst.xcconfig; sourceTree = "<group>"; };
|
||||
F2BD96D62DDCC7A200E7F49A /* macOS.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = macOS.xcconfig; sourceTree = "<group>"; };
|
||||
F2BE72102E05DB8E00B66C78 /* ViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewTests.swift; sourceTree = "<group>"; };
|
||||
F2C003E72C933D2300339E67 /* RiveMetalDrawableView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RiveMetalDrawableView.h; sourceTree = "<group>"; };
|
||||
F2C07B192DA9854F00DC8C84 /* WeakContainer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WeakContainer.h; sourceTree = "<group>"; };
|
||||
F2C07B1A2DA9854F00DC8C84 /* WeakContainer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WeakContainer.m; sourceTree = "<group>"; };
|
||||
@@ -481,6 +485,7 @@
|
||||
F23992E62CB9C1C60021EF61 /* RenderContextTests.m */,
|
||||
F22CF1B22D380E6900D35779 /* DataBindingTests.swift */,
|
||||
F24FC6442DD3C83700DEE8C5 /* RiveRenderImageTests.swift */,
|
||||
F2BE72102E05DB8E00B66C78 /* ViewTests.swift */,
|
||||
);
|
||||
path = Tests;
|
||||
sourceTree = "<group>";
|
||||
@@ -503,6 +508,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F21C3D1A2DDFCD93005F82F4 /* RiveRenderImage+Extensions.swift */,
|
||||
F20808E12E05C3FF0082A281 /* View+Extensions.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@@ -733,6 +739,7 @@
|
||||
E5964A982A9697B600140479 /* RiveEvent.mm in Sources */,
|
||||
F21F08142C66526D00FFA205 /* RiveFallbackFontDescriptor.swift in Sources */,
|
||||
F2CCA9C22C9E13BA007DC0D2 /* RiveLogger.swift in Sources */,
|
||||
F20808E22E05C3FF0082A281 /* View+Extensions.swift in Sources */,
|
||||
043025F82AFA46EF00320F2E /* CDNFileAssetLoader.mm in Sources */,
|
||||
F27B615D2D35C75A003C0345 /* RiveDataBindingViewModelInstanceProperty.mm in Sources */,
|
||||
043026042AFBA04100320F2E /* RiveFactory.mm in Sources */,
|
||||
@@ -773,6 +780,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F2BE72112E05DB8E00B66C78 /* ViewTests.swift in Sources */,
|
||||
04BE5420264ACFC200427B39 /* RiveStateMachineLoadTest.mm in Sources */,
|
||||
04BE541E264AC7A600427B39 /* RiveArtboardLoadTest.mm in Sources */,
|
||||
041265262B0CB41E009400EC /* OutOfBandAssetTest.mm in Sources */,
|
||||
|
||||
94
Source/Extensions/View+Extensions.swift
Normal file
94
Source/Extensions/View+Extensions.swift
Normal file
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// UIView+Extensions.swift
|
||||
// RiveRuntime
|
||||
//
|
||||
// Created by David Skuza on 6/20/25.
|
||||
// Copyright © 2025 Rive. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
#if canImport(UIKit)
|
||||
private typealias Window = UIWindow
|
||||
private typealias View = UIView
|
||||
private typealias ScrollView = UIScrollView
|
||||
#else
|
||||
#if RIVE_MAC_CATALYST
|
||||
private typealias Window = UIWindow
|
||||
private typealias View = UIView
|
||||
private typealias ScrollView = UIScrollView
|
||||
#else
|
||||
private typealias Window = NSWindow
|
||||
private typealias View = NSView
|
||||
private typealias ScrollView = NSScrollView
|
||||
#endif
|
||||
#endif
|
||||
|
||||
extension View {
|
||||
func isOnscreen() -> Bool {
|
||||
guard let window = window, isWindowHidden(window) == false, isViewHidden(self) == false else {
|
||||
return false
|
||||
}
|
||||
|
||||
var currentView: View = self
|
||||
var rect = currentView.bounds
|
||||
while let superview = currentView.superview {
|
||||
guard isViewHidden(superview) == false else {
|
||||
return false
|
||||
}
|
||||
|
||||
rect = currentView.convert(rect, to: superview)
|
||||
|
||||
if let scrollView = superview as? ScrollView, isRectVisible(in: scrollView, rect: rect) == false {
|
||||
return false
|
||||
}
|
||||
|
||||
// Test this - TL;DR: if _some_ superview clips to bounds, we might have to skip drawing
|
||||
if superview.clipsToBounds == false {
|
||||
currentView = superview
|
||||
continue
|
||||
}
|
||||
|
||||
guard rect.intersects(superview.bounds) else {
|
||||
return false
|
||||
}
|
||||
|
||||
currentView = superview
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func isRectVisible(in scrollView: ScrollView, rect: CGRect) -> Bool {
|
||||
let visibleRect = CGRect(origin: scrollView.contentOffset, size: scrollView.bounds.size)
|
||||
return rect.intersects(visibleRect)
|
||||
}
|
||||
|
||||
#if canImport(AppKit) && !RIVE_MAC_CATALYST
|
||||
private func isWindowHidden(_ window: NSWindow) -> Bool {
|
||||
return window.contentView?.bounds.isEmpty == true || window.isVisible == false || window.alphaValue == 0
|
||||
}
|
||||
#else
|
||||
private func isWindowHidden(_ window: Window) -> Bool {
|
||||
return isViewHidden(window)
|
||||
}
|
||||
#endif
|
||||
|
||||
private func isViewHidden(_ view: View) -> Bool {
|
||||
return view.bounds.isEmpty || view.isHidden || view.alpha == 0
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(AppKit) && !RIVE_MAC_CATALYST
|
||||
extension View {
|
||||
var alpha: CGFloat {
|
||||
guard let opacity = layer?.opacity else { return 0 }
|
||||
return CGFloat(opacity)
|
||||
}
|
||||
}
|
||||
|
||||
extension ScrollView {
|
||||
var contentOffset: CGPoint {
|
||||
return bounds.origin
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -13,6 +13,11 @@ open class RiveView: RiveRendererView {
|
||||
static let layoutScaleFactorAutomatic: Double = -1
|
||||
}
|
||||
|
||||
public enum OffscreenBehavior {
|
||||
case playAndDraw
|
||||
case playAndNoDraw
|
||||
}
|
||||
|
||||
// MARK: Configuration
|
||||
internal weak var riveModel: RiveModel?
|
||||
internal var fit: RiveFit = .contain { didSet { needsDisplay() } }
|
||||
@@ -42,6 +47,8 @@ open class RiveView: RiveRendererView {
|
||||
/// - Note: On iOS, this is handled separately from `isExclusiveTouch`.
|
||||
internal var forwardsListenerEvents: Bool = false
|
||||
|
||||
public var offscreenBehavior: OffscreenBehavior = .playAndNoDraw
|
||||
|
||||
// MARK: Render Loop
|
||||
internal private(set) var isPlaying: Bool = false
|
||||
private var lastTime: CFTimeInterval = 0
|
||||
@@ -166,9 +173,9 @@ open class RiveView: RiveRendererView {
|
||||
|
||||
private func needsDisplay() {
|
||||
#if os(iOS) || os(visionOS) || os(tvOS)
|
||||
setNeedsDisplay()
|
||||
setNeedsDisplay()
|
||||
#else
|
||||
needsDisplay=true
|
||||
needsDisplay = true
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -429,6 +436,12 @@ open class RiveView: RiveRendererView {
|
||||
|
||||
}
|
||||
|
||||
open override func draw(_ rect: CGRect) {
|
||||
if offscreenBehavior == .playAndDraw || isOnscreen() {
|
||||
super.draw(rect)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITraitCollection
|
||||
#if os(iOS)
|
||||
open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
|
||||
@@ -482,7 +482,6 @@ class DataBindingTests: XCTestCase {
|
||||
stateMachine.bind(viewModelInstance: instance)
|
||||
instance.booleanProperty(fromPath: "Boolean")?.value = true
|
||||
stateMachine.advance(by: 0)
|
||||
print(stateMachine.stateChanges().contains("boolean_on"))
|
||||
}
|
||||
|
||||
// MARK: - AutoBind
|
||||
|
||||
266
Tests/ViewTests.swift
Normal file
266
Tests/ViewTests.swift
Normal file
@@ -0,0 +1,266 @@
|
||||
//
|
||||
// ViewTests.swift
|
||||
// RiveRuntimeTests
|
||||
//
|
||||
// Created by David Skuza on 6/20/25.
|
||||
// Copyright © 2025 Rive. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import RiveRuntime
|
||||
|
||||
import UIKit
|
||||
|
||||
#if !RIVE_MAC_CATALYST // iOS tests run, Catalyst tests seem to require a running app to create a window
|
||||
class ViewTests: XCTestCase {
|
||||
|
||||
// MARK: - Window Tests
|
||||
|
||||
func testIsOnscreen_WhenViewHasNoWindow_ReturnsFalse() {
|
||||
let view = UIView(frame: CGRect(origin: .zero, size: CGSize(width: 10, height: 10)))
|
||||
XCTAssertFalse(view.isOnscreen())
|
||||
}
|
||||
|
||||
func testIsOnscreen_WhenWindowIsHidden_ReturnsFalse() {
|
||||
let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 10, height: 10)))
|
||||
window.clipsToBounds = true
|
||||
let view = UIView()
|
||||
window.addSubview(view)
|
||||
window.isHidden = true
|
||||
|
||||
XCTAssertFalse(view.isOnscreen())
|
||||
}
|
||||
|
||||
func testIsOnscreen_WhenWindowIsVisible_ReturnsTrue() {
|
||||
let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 10, height: 10)))
|
||||
window.clipsToBounds = true
|
||||
let view = UIView(frame: window.bounds)
|
||||
window.addSubview(view)
|
||||
window.isHidden = false
|
||||
|
||||
XCTAssertTrue(view.isOnscreen())
|
||||
}
|
||||
|
||||
// MARK: - View Visibility Tests
|
||||
|
||||
func testIsOnscreen_WhenViewIsHidden_ReturnsFalse() {
|
||||
let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 10, height: 10)))
|
||||
window.clipsToBounds = true
|
||||
window.isHidden = false
|
||||
let view = UIView(frame: window.bounds)
|
||||
window.addSubview(view)
|
||||
view.isHidden = true
|
||||
|
||||
XCTAssertFalse(view.isOnscreen())
|
||||
}
|
||||
|
||||
func testIsOnscreen_WhenViewHasEmptyBounds_ReturnsFalse() {
|
||||
let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 10, height: 10)))
|
||||
window.clipsToBounds = true
|
||||
window.isHidden = false
|
||||
let view = UIView(frame: window.bounds)
|
||||
window.addSubview(view)
|
||||
view.frame = CGRect.zero
|
||||
|
||||
XCTAssertFalse(view.isOnscreen())
|
||||
}
|
||||
|
||||
func testIsOnscreen_WhenViewHasZeroAlpha_ReturnsFalse() {
|
||||
let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 10, height: 10)))
|
||||
window.clipsToBounds = true
|
||||
window.isHidden = false
|
||||
let view = UIView(frame: window.bounds)
|
||||
window.addSubview(view)
|
||||
view.alpha = 0
|
||||
|
||||
XCTAssertFalse(view.isOnscreen())
|
||||
}
|
||||
|
||||
func testIsOnscreen_WhenViewIsVisible_ReturnsTrue() {
|
||||
let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 10, height: 10)))
|
||||
window.clipsToBounds = true
|
||||
window.isHidden = false
|
||||
let view = UIView(frame: window.bounds)
|
||||
window.addSubview(view)
|
||||
view.isHidden = false
|
||||
view.alpha = 1.0
|
||||
|
||||
XCTAssertTrue(view.isOnscreen())
|
||||
}
|
||||
|
||||
// MARK: - Superview Visibility Tests
|
||||
|
||||
func testIsOnscreen_WhenSuperviewIsHidden_ReturnsFalse() {
|
||||
let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 10, height: 10)))
|
||||
window.clipsToBounds = true
|
||||
window.isHidden = false
|
||||
let superview = UIView(frame: window.bounds)
|
||||
let view = UIView(frame: superview.bounds)
|
||||
window.addSubview(superview)
|
||||
superview.addSubview(view)
|
||||
superview.isHidden = true
|
||||
|
||||
XCTAssertFalse(view.isOnscreen())
|
||||
}
|
||||
|
||||
func testIsOnscreen_WhenSuperviewHasEmptyBounds_ReturnsFalse() {
|
||||
let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 10, height: 10)))
|
||||
window.clipsToBounds = true
|
||||
window.isHidden = false
|
||||
let superview = UIView(frame: window.bounds)
|
||||
let view = UIView(frame: superview.bounds)
|
||||
window.addSubview(superview)
|
||||
superview.addSubview(view)
|
||||
superview.frame = CGRect.zero
|
||||
|
||||
XCTAssertFalse(view.isOnscreen())
|
||||
}
|
||||
|
||||
func testIsOnscreen_WhenSuperviewHasZeroAlpha_ReturnsFalse() {
|
||||
let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 10, height: 10)))
|
||||
window.clipsToBounds = true
|
||||
window.isHidden = false
|
||||
let superview = UIView(frame: window.bounds)
|
||||
let view = UIView(frame: superview.bounds)
|
||||
window.addSubview(superview)
|
||||
superview.addSubview(view)
|
||||
superview.alpha = 0
|
||||
|
||||
XCTAssertFalse(view.isOnscreen())
|
||||
}
|
||||
|
||||
// MARK: - ScrollView Tests
|
||||
|
||||
func testIsOnscreen_WhenInScrollViewAndContentOutsideVisibleArea_ReturnsFalse() {
|
||||
let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 10, height: 10)))
|
||||
window.clipsToBounds = true
|
||||
window.isHidden = false
|
||||
let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
|
||||
let topView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
|
||||
let bottomView = UIView(frame: CGRect(x: 0, y: 10, width: 10, height: 10))
|
||||
|
||||
window.addSubview(scrollView)
|
||||
scrollView.addSubview(topView)
|
||||
scrollView.addSubview(bottomView)
|
||||
scrollView.contentSize = CGSize(width: 10, height: 20)
|
||||
scrollView.contentOffset = .zero
|
||||
|
||||
XCTAssertFalse(bottomView.isOnscreen())
|
||||
}
|
||||
|
||||
func testIsOnscreen_WhenInScrollViewAndContentInsideVisibleArea_ReturnsTrue() {
|
||||
let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 10, height: 10)))
|
||||
window.isHidden = false
|
||||
window.clipsToBounds = false
|
||||
let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
|
||||
let contentView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
|
||||
let view = UIView(frame: CGRect(x: 0, y: 1, width: 10, height: 10))
|
||||
|
||||
window.addSubview(scrollView)
|
||||
scrollView.addSubview(contentView)
|
||||
contentView.addSubview(view)
|
||||
scrollView.contentSize = CGSize(width: 10, height: 10)
|
||||
scrollView.contentOffset = CGPoint.zero
|
||||
|
||||
XCTAssertTrue(view.isOnscreen())
|
||||
}
|
||||
|
||||
func testIsOnscreen_WhenInScrollViewAndScrolledToShowContent_ReturnsTrue() {
|
||||
let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 10, height: 10)))
|
||||
window.clipsToBounds = true
|
||||
window.isHidden = false
|
||||
let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
|
||||
let topView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
|
||||
let bottomView = UIView(frame: CGRect(x: 0, y: 10, width: 10, height: 10))
|
||||
|
||||
window.addSubview(scrollView)
|
||||
scrollView.addSubview(topView)
|
||||
scrollView.addSubview(bottomView)
|
||||
scrollView.contentSize = CGSize(width: 10, height: 20)
|
||||
scrollView.contentOffset = CGPoint(x: 0, y: 15)
|
||||
|
||||
XCTAssertTrue(bottomView.isOnscreen())
|
||||
}
|
||||
|
||||
// MARK: - Clipping and Bounds Tests
|
||||
|
||||
func testIsOnscreen_WhenSuperviewClipsToBoundsAndViewOutsideBounds_ReturnsFalse() {
|
||||
let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 10, height: 10)))
|
||||
window.clipsToBounds = true
|
||||
window.isHidden = false
|
||||
let superview = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
|
||||
let view = UIView(frame: CGRect(x: 20, y: 20, width: 10, height: 10))
|
||||
window.addSubview(superview)
|
||||
superview.addSubview(view)
|
||||
superview.clipsToBounds = true
|
||||
|
||||
XCTAssertFalse(view.isOnscreen())
|
||||
}
|
||||
|
||||
func testIsOnscreen_WhenSuperviewClipsToBoundsAndViewInsideBounds_ReturnsTrue() {
|
||||
let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 10, height: 10)))
|
||||
window.clipsToBounds = true
|
||||
window.isHidden = false
|
||||
let superview = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
|
||||
let view = UIView(frame: CGRect(x: 1, y: 1, width: 10, height: 10))
|
||||
window.addSubview(superview)
|
||||
superview.addSubview(view)
|
||||
superview.clipsToBounds = true
|
||||
|
||||
XCTAssertTrue(view.isOnscreen())
|
||||
}
|
||||
|
||||
func testIsOnscreen_WhenSuperviewDoesNotClipToBoundsAndViewOutsideBounds_ReturnsTrue() {
|
||||
let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 10, height: 10)))
|
||||
window.clipsToBounds = false
|
||||
window.isHidden = false
|
||||
let superview = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
|
||||
let view = UIView(frame: CGRect(x: 20, y: 20, width: 10, height: 10))
|
||||
window.addSubview(superview)
|
||||
superview.addSubview(view)
|
||||
superview.clipsToBounds = false
|
||||
|
||||
XCTAssertTrue(view.isOnscreen())
|
||||
}
|
||||
|
||||
// MARK: - Complex Hierarchy Tests
|
||||
|
||||
func testIsOnscreen_WithComplexViewHierarchy_AllVisible_ReturnsTrue() {
|
||||
let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 10, height: 10)))
|
||||
window.clipsToBounds = true
|
||||
window.isHidden = false
|
||||
let container1 = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
|
||||
let container2 = UIView(frame: CGRect(x: 1, y: 1, width: 10, height: 10))
|
||||
let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
|
||||
let contentView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
|
||||
let view = UIView(frame: CGRect(x: 2, y: 2, width: 10, height: 10))
|
||||
|
||||
window.addSubview(container1)
|
||||
container1.addSubview(container2)
|
||||
container2.addSubview(scrollView)
|
||||
scrollView.addSubview(contentView)
|
||||
contentView.addSubview(view)
|
||||
|
||||
scrollView.contentSize = CGSize(width: 10, height: 10)
|
||||
scrollView.contentOffset = CGPoint.zero
|
||||
|
||||
XCTAssertTrue(view.isOnscreen())
|
||||
}
|
||||
|
||||
func testIsOnscreen_WithComplexViewHierarchy_OneHiddenInChain_ReturnsFalse() {
|
||||
let window = UIWindow(frame: CGRect(origin: .zero, size: CGSize(width: 10, height: 10)))
|
||||
window.clipsToBounds = true
|
||||
window.isHidden = false
|
||||
let container1 = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
|
||||
let container2 = UIView(frame: CGRect(x: 1, y: 1, width: 10, height: 10))
|
||||
let view = UIView(frame: CGRect(x: 2, y: 2, width: 10, height: 10))
|
||||
|
||||
window.addSubview(container1)
|
||||
container1.addSubview(container2)
|
||||
container2.addSubview(view)
|
||||
container1.isHidden = true
|
||||
|
||||
XCTAssertFalse(view.isOnscreen())
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user