快照测试中UIScreen.main.bounds值异常问题求助
问题分析与解决方案
原因
swift-snapshot-testing 的 .image(on: .iPhone13(.portrait)) 仅负责设置快照渲染时的容器尺寸,但不会修改全局的 UIScreen.main 状态。在单元测试环境中,UIScreen.main 默认模拟的是初代 iPhone SE(320x480)的尺寸,所以会出现不符合预期的结果。
解决方案
方案1:改用视图自身的边界(推荐)
这是iOS开发的最佳实践——避免直接依赖 UIScreen.main.bounds,改用当前视图控制器的 view.bounds 或 safeAreaLayoutGuide 计算尺寸。快照测试时,vc.view 会被自动设置为你指定的 iPhone 13 尺寸,布局逻辑会和快照渲染容器尺寸对齐。
把代码中依赖 UIScreen.main 的部分替换:
// 原代码 let size = UIScreen.main.bounds.size // 替换为 let size = view.bounds.size // 或考虑安全区域 let size = view.safeAreaLayoutGuide.layoutFrame.size
方案2:依赖注入(兼容旧代码)
如果业务代码必须依赖屏幕尺寸,可通过依赖注入将尺寸传入视图控制器,测试时手动传入 iPhone 13 的尺寸:
修改 OnboardingViewController:
class OnboardingViewController: UIViewController { private let screenSize: CGSize // 正常环境默认用UIScreen.main尺寸,测试时可传自定义值 init(screenSize: CGSize = UIScreen.main.bounds.size) { self.screenSize = screenSize super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // 布局时使用self.screenSize }
更新测试代码:
func test_onboarding_iPhone13() { let iPhone13Size = CGSize(width: 390, height: 844) let vc = OnboardingViewController(screenSize: iPhone13Size) assertSnapshot(matching: vc, as: .image(on: .iPhone13(.portrait)), record: true) }
方案3:临时替换UIScreen.main(不推荐)
如果无法修改业务代码,可通过 Runtime Swizzling 临时替换 UIScreen.main 的实现,测试期间返回指定尺寸。但这种方式侵入性强,易引发其他测试副作用,谨慎使用:
import ObjectiveC extension UIScreen { static func mockMainScreen(bounds: CGRect) { let originalSelector = #selector(getter: main) let swizzledSelector = #selector(getter: mockedMain) let originalMethod = class_getClassMethod(self, originalSelector) let swizzledMethod = class_getClassMethod(self, swizzledSelector) method_exchangeImplementations(originalMethod!, swizzledMethod!) mockedBounds = bounds } static func restoreMainScreen() { let originalSelector = #selector(getter: main) let swizzledSelector = #selector(getter: mockedMain) let originalMethod = class_getClassMethod(self, originalSelector) let swizzledMethod = class_getClassMethod(self, swizzledSelector) method_exchangeImplementations(originalMethod!, swizzledMethod!) } private static var mockedBounds: CGRect = .zero @objc private static var mockedMain: UIScreen { let screen = UIScreen() object_setIvar(screen, class_getInstanceVariable(UIScreen.self, "_bounds"), &mockedBounds) return screen } }
测试中使用:
override func setUp() { super.setUp() let iPhone13Bounds = CGRect(x: 0, y: 0, width: 390, height: 844) UIScreen.mockMainScreen(bounds: iPhone13Bounds) } override func tearDown() { UIScreen.restoreMainScreen() super.tearDown() } func test_onboarding_iPhone13() { let vc = OnboardingViewController() assertSnapshot(matching: vc, as: .image(on: .iPhone13(.portrait)), record: true) }
内容的提问来源于stack exchange,提问作者Marcus FK
相关产品推荐
相关产品推荐

