You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

快照测试中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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 16:39:53