Skip to content

Commit c597258

Browse files
UIViewRepresentable for embedded (#4520)
## Summary - Adds EmbeddedViewRepresentable which is a SwiftUI view that can display the embedded payment element's view in SwiftUI - Adds EmbeddedPaymentElementView, which is the public API of the EmbeddedViewRepresentable - Adds a snapshot test for it ## Motivation - Embedded SwiftUI ## Testing - Manual - New snapshots ## Changelog N/A --------- Co-authored-by: Yuki <[email protected]>
1 parent 93a1b74 commit c597258

15 files changed

+241
-15
lines changed

StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj

+8
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@
158158
5C0D1B932954D0EF3F3A679F /* ManualEntryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BDADF560DB0B1ED175EF50 /* ManualEntryButton.swift */; };
159159
5E00512CDFBC1C93781E20AB /* PaymentSheetLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DBDEE23A856CE8D3B49861 /* PaymentSheetLoader.swift */; };
160160
6103F2BC2BE45990002D67F8 /* SavedPaymentMethodManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6103F2BB2BE45990002D67F8 /* SavedPaymentMethodManager.swift */; };
161+
6107F5462D4C389800905BD8 /* EmbeddedViewRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6107F5452D4C389800905BD8 /* EmbeddedViewRepresentable.swift */; };
161162
610EAAF02C0F5D9400124AB2 /* FormHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 610EAAEF2C0F5D9400124AB2 /* FormHeaderView.swift */; };
162163
6117D7122CB065E7005C4EC1 /* MandateTextProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6117D7112CB065E7005C4EC1 /* MandateTextProvider.swift */; };
163164
6123E94D2D4A84800088FBE8 /* EmbeddedPaymentElement+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6123E94C2D4A84800088FBE8 /* EmbeddedPaymentElement+SwiftUI.swift */; };
@@ -174,6 +175,7 @@
174175
619AF0852BF56C5E00D1C981 /* PaymentMethodRowButtonSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619AF0842BF56C5E00D1C981 /* PaymentMethodRowButtonSnapshotTests.swift */; };
175176
619AF08A2BF56FC000D1C981 /* VerticalSavedPaymentMethodsViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619AF0882BF56F9100D1C981 /* VerticalSavedPaymentMethodsViewControllerTests.swift */; };
176177
61A799522CC2053500D7DFFA /* EmbeddedFormViewControllerSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A799512CC2053500D7DFFA /* EmbeddedFormViewControllerSnapshotTests.swift */; };
178+
61B2530F2D4D5FC5008CFC96 /* EmbeddedViewRepresentableSnapshotTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B2530E2D4D5FC5008CFC96 /* EmbeddedViewRepresentableSnapshotTest.swift */; };
177179
61C0D3B8C63EB4558AB74A7E /* StripePayments.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A1C7CFA5C9C1A8A73CFA1C0 /* StripePayments.framework */; };
178180
61C87E1B2CB818ED001B7DA9 /* CardBrandFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C87E1A2CB818ED001B7DA9 /* CardBrandFilter.swift */; };
179181
61C87E1E2CB81FAD001B7DA9 /* CardBrandFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C87E1C2CB81F9B001B7DA9 /* CardBrandFilterTests.swift */; };
@@ -586,6 +588,7 @@
586588
5FD715F32B61017198E9F952 /* BottomSheetTransitioningDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetTransitioningDelegate.swift; sourceTree = "<group>"; };
587589
6103F2BB2BE45990002D67F8 /* SavedPaymentMethodManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedPaymentMethodManager.swift; sourceTree = "<group>"; };
588590
6103F2BD2BE53737002D67F8 /* SavedPaymentMethodManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedPaymentMethodManagerTest.swift; sourceTree = "<group>"; };
591+
6107F5452D4C389800905BD8 /* EmbeddedViewRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedViewRepresentable.swift; sourceTree = "<group>"; };
589592
610EAAEF2C0F5D9400124AB2 /* FormHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormHeaderView.swift; sourceTree = "<group>"; };
590593
6117D7112CB065E7005C4EC1 /* MandateTextProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MandateTextProvider.swift; sourceTree = "<group>"; };
591594
6123E94C2D4A84800088FBE8 /* EmbeddedPaymentElement+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmbeddedPaymentElement+SwiftUI.swift"; sourceTree = "<group>"; };
@@ -604,6 +607,7 @@
604607
619AF0842BF56C5E00D1C981 /* PaymentMethodRowButtonSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodRowButtonSnapshotTests.swift; sourceTree = "<group>"; };
605608
619AF0882BF56F9100D1C981 /* VerticalSavedPaymentMethodsViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalSavedPaymentMethodsViewControllerTests.swift; sourceTree = "<group>"; };
606609
61A799512CC2053500D7DFFA /* EmbeddedFormViewControllerSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedFormViewControllerSnapshotTests.swift; sourceTree = "<group>"; };
610+
61B2530E2D4D5FC5008CFC96 /* EmbeddedViewRepresentableSnapshotTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedViewRepresentableSnapshotTest.swift; sourceTree = "<group>"; };
607611
61C87E1A2CB818ED001B7DA9 /* CardBrandFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardBrandFilter.swift; sourceTree = "<group>"; };
608612
61C87E1C2CB81F9B001B7DA9 /* CardBrandFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardBrandFilterTests.swift; sourceTree = "<group>"; };
609613
61CBE6652BED9749005F7FEB /* VerticalSavedPaymentMethodsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalSavedPaymentMethodsViewController.swift; sourceTree = "<group>"; };
@@ -1139,6 +1143,7 @@
11391143
children = (
11401144
B615E8702CA4CBEE007D684C /* EmbeddedPaymentElement.swift */,
11411145
6123E94C2D4A84800088FBE8 /* EmbeddedPaymentElement+SwiftUI.swift */,
1146+
6107F5452D4C389800905BD8 /* EmbeddedViewRepresentable.swift */,
11421147
615C2C4F2CBDBA61003F0173 /* EmbeddedFormViewController.swift */,
11431148
B6CACC9D2CB8B8E800682ECE /* EmbeddedPaymentElement+Internal.swift */,
11441149
B615E8722CA4CC04007D684C /* EmbeddedPaymentElementConfiguration.swift */,
@@ -1675,6 +1680,7 @@
16751680
B63DC6792CC06AC80011C27E /* EmbeddedPaymentElementSnapshotTests.swift */,
16761681
61FB6BCC2C8901B200F8E074 /* EmbeddedPaymentMethodsViewSnapshotTests.swift */,
16771682
61A799512CC2053500D7DFFA /* EmbeddedFormViewControllerSnapshotTests.swift */,
1683+
61B2530E2D4D5FC5008CFC96 /* EmbeddedViewRepresentableSnapshotTest.swift */,
16781684
614068E12CB0BF10003D2F12 /* EmbeddedPaymentMethodsViewTests.swift */,
16791685
64C8F350CDB5A29F62E86592 /* FlowControllerStateTests.swift */,
16801686
61D842902CB06047009D2D51 /* FormMandateProviderTests.swift */,
@@ -1993,6 +1999,7 @@
19931999
31CC9B7F2CB5F69600E84A38 /* LinkPopupURLParserTests.swift in Sources */,
19942000
31CC9B802CB5F69600E84A38 /* LinkToastSnapshotTests.swift in Sources */,
19952001
31CC9B812CB5F69600E84A38 /* LinkInstantDebitMandateViewSnapshotTests.swift in Sources */,
2002+
61B2530F2D4D5FC5008CFC96 /* EmbeddedViewRepresentableSnapshotTest.swift in Sources */,
19962003
31CC9B822CB5F69600E84A38 /* LinkURLGeneratorTests.swift in Sources */,
19972004
31CC9B852CB5F69600E84A38 /* LinkBadgeViewSnapshotTest.swift in Sources */,
19982005
CB225E962CEF80DC00054262 /* PaymentMethodTypeCollectionViewCellSnapshotTests.swift in Sources */,
@@ -2225,6 +2232,7 @@
22252232
2CE83364A23B4E3BAFD447CA /* WalletHeaderView.swift in Sources */,
22262233
99B171DC60405D4822819E0E /* PaymentMethodType.swift in Sources */,
22272234
F29DF2AD08718147C299D2C3 /* PaymentOption+Images.swift in Sources */,
2235+
6107F5462D4C389800905BD8 /* EmbeddedViewRepresentable.swift in Sources */,
22282236
820C3EDE61ADBFE4DA1E9A98 /* PaymentSheet+API.swift in Sources */,
22292237
294AE0A8838786ACABE6BCF2 /* PaymentSheet+DeferredAPI.swift in Sources */,
22302238
1B03C54F5F988C552487C564 /* PaymentSheet+PaymentMethodAvailability.swift in Sources */,

StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElement+SwiftUI.swift

+39-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import SwiftUI
99
import Combine
10+
@_spi(STP) import StripeCore
1011

1112
/// A view model that manages an `EmbeddedPaymentElement`.
1213
@MainActor
@@ -40,6 +41,8 @@ import Combine
4041
// MARK: - Internal properties
4142

4243
private(set) var embeddedPaymentElement: EmbeddedPaymentElement?
44+
45+
@Published var height: CGFloat = 0.0
4346

4447
// MARK: - Private properties
4548

@@ -48,7 +51,9 @@ import Combine
4851
// MARK: - Public APIs
4952

5053
/// Creates an empty view model. Call `load` to initialize the `EmbeddedPaymentElementViewModel`
51-
public init() {}
54+
public init() {
55+
STPAnalyticsClient.sharedClient.addClass(toProductUsageIfNecessary: Self.self)
56+
}
5257

5358
/// Asynchronously loads the EmbeddedPaymentElementViewModel. This function should only be called once to initially load the EmbeddedPaymentElementViewModel.
5459
/// Loads the Customer's payment methods, their default payment method, etc.
@@ -75,6 +80,7 @@ import Combine
7580
self.embeddedPaymentElement = embeddedPaymentElement
7681
self.embeddedPaymentElement?.delegate = self
7782
self.paymentOption = embeddedPaymentElement.paymentOption
83+
calculateAndPublishHeight(embeddedPaymentElement: embeddedPaymentElement) // compute initial height
7884
self.isLoaded = true
7985
}
8086

@@ -134,17 +140,47 @@ import Combine
134140
embeddedPaymentElement?.testHeightChange()
135141
}
136142
#endif
143+
144+
private func calculateAndPublishHeight(embeddedPaymentElement: EmbeddedPaymentElement) {
145+
let newHeight = embeddedPaymentElement.view.systemLayoutSizeFitting(CGSize(width: embeddedPaymentElement.view.bounds.width, height: UIView.layoutFittingCompressedSize.height)).height
146+
147+
withAnimation(.easeInOut(duration: 0.2)) {
148+
self.height = newHeight
149+
}
150+
}
137151
}
138152

139153
// MARK: EmbeddedPaymentElementDelegate
140154

141155
extension EmbeddedPaymentElementViewModel: EmbeddedPaymentElementDelegate {
142-
143156
public func embeddedPaymentElementDidUpdateHeight(embeddedPaymentElement: EmbeddedPaymentElement) {
144-
// TODO(porter) Handle height changes when we add the UIViewRepresentable MOBILESDK-3001
157+
calculateAndPublishHeight(embeddedPaymentElement: embeddedPaymentElement)
145158
}
146159

147160
public func embeddedPaymentElementDidUpdatePaymentOption(embeddedPaymentElement: EmbeddedPaymentElement) {
148161
self.paymentOption = embeddedPaymentElement.paymentOption
149162
}
150163
}
164+
165+
// MARK: STPAnalyticsProtocol
166+
167+
@_spi(STP) extension EmbeddedPaymentElementViewModel: STPAnalyticsProtocol {
168+
nonisolated public static let stp_analyticsIdentifier = "EmbeddedPaymentElementViewModel"
169+
}
170+
171+
/// A SwiftUI view that displays payment methods. It can present a sheet to collect more details or display saved payment methods.
172+
@_spi(EmbeddedPaymentElementPrivateBeta) public struct EmbeddedPaymentElementView: View {
173+
@ObservedObject private var viewModel: EmbeddedPaymentElementViewModel
174+
175+
/// Initializes a new instance of `EmbeddedPaymentElementView`.
176+
///
177+
/// - Parameter viewModel: The view model for this payment element view.
178+
public init(viewModel: EmbeddedPaymentElementViewModel) {
179+
self.viewModel = viewModel
180+
}
181+
182+
public var body: some View {
183+
EmbeddedViewRepresentable(viewModel: viewModel)
184+
.frame(height: viewModel.height)
185+
}
186+
}

StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElementContainerView.swift

+8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ import UIKit
88

99
/// The view that's vended to the merchant, containing the embedded view. We use this to be able to swap out the embedded view with an animation when `update` is called.
1010
class EmbeddedPaymentElementContainerView: UIView {
11+
12+
/// Return the default size to let Auto Layout manage the height.
13+
/// Overriding intrinsicContentSize values and setting `invalidIntrinsicContentSize` forces SwiftUI to update layout immediately,
14+
/// resulting in abrupt, non-animated height changes.
15+
override var intrinsicContentSize: CGSize {
16+
return super.intrinsicContentSize
17+
}
18+
1119
var needsUpdateSuperviewHeight: () -> Void = {}
1220
private var contentView: EmbeddedPaymentMethodsView
1321
private var bottomAnchorConstraint: NSLayoutConstraint!

StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentMethodsView.swift

+8-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,14 @@ protocol EmbeddedPaymentMethodsViewDelegate: AnyObject {
2525

2626
/// The view for an embedded payment element
2727
class EmbeddedPaymentMethodsView: UIView {
28-
28+
29+
/// Return the default size to let Auto Layout manage the height.
30+
/// Overriding intrinsicContentSize values and setting `invalidIntrinsicContentSize` forces force SwiftUI to update layout immediately,
31+
/// resulting in abrupt, non-animated height changes.
32+
override var intrinsicContentSize: CGSize {
33+
return super.intrinsicContentSize
34+
}
35+
2936
private let appearance: PaymentSheet.Appearance
3037
private let rowButtonAppearance: PaymentSheet.Appearance
3138
private let customer: PaymentSheet.CustomerConfiguration?
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//
2+
// EmbeddedViewRepresentable.swift
3+
// StripePaymentSheet
4+
//
5+
// Created by Nick Porter on 1/30/25.
6+
//
7+
8+
import SwiftUI
9+
@_spi(STP) import StripeCore
10+
@_spi(STP) import StripeUICore
11+
12+
struct EmbeddedViewRepresentable: UIViewRepresentable {
13+
@ObservedObject var viewModel: EmbeddedPaymentElementViewModel
14+
15+
public func makeUIView(context: Context) -> UIView {
16+
let containerView = UIView()
17+
containerView.backgroundColor = .clear
18+
containerView.layoutMargins = .zero
19+
20+
guard let embeddedPaymentElement = viewModel.embeddedPaymentElement else {
21+
stpAssertionFailure("embeddedPaymentElement was nil in EmbeddedViewRepresentable.makeUIView(). Ensure you do not show the EmbeddedPaymentElementView before isLoaded is true on the EmbeddedPaymentElementViewModel.")
22+
return containerView
23+
}
24+
embeddedPaymentElement.presentingViewController = UIWindow.visibleViewController
25+
26+
let paymentElementView = embeddedPaymentElement.view
27+
paymentElementView.translatesAutoresizingMaskIntoConstraints = false
28+
containerView.addSubview(paymentElementView)
29+
30+
NSLayoutConstraint.activate([
31+
paymentElementView.topAnchor.constraint(equalTo: containerView.topAnchor),
32+
paymentElementView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
33+
paymentElementView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor)
34+
])
35+
36+
return containerView
37+
}
38+
39+
public func updateUIView(_ uiView: UIView, context: Context) {
40+
// Update the presenting view controller in case it has changed
41+
viewModel.embeddedPaymentElement?.presentingViewController = UIWindow.visibleViewController
42+
}
43+
}
44+
45+
// MARK: UIWindow and UIViewController helpers
46+
47+
extension UIWindow {
48+
static var visibleViewController: UIViewController? {
49+
UIApplication.shared.stp_hackilyFumbleAroundUntilYouFindAKeyWindow()?.rootViewController?.findTopMostPresentedViewController()
50+
}
51+
}

StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/BottomSheetViewController.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ extension BottomSheetViewController: UIScrollViewDelegate {
478478
extension BottomSheetViewController: PaymentSheetAuthenticationContext {
479479

480480
func authenticationPresentingViewController() -> UIViewController {
481-
return findTopMostPresentedViewController() ?? self
481+
return findTopMostPresentedViewController()
482482
}
483483

484484
func configureSafariViewController(_ viewController: SFSafariViewController) {

StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/EmbeddedPaymentMethodsViewSnapshotTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ class EmbeddedPaymentMethodsViewSnapshotTests: STPSnapshotTestCase {
7474
appearance.embeddedPaymentElement.row.additionalInsets = 20
7575

7676
let embeddedView = EmbeddedPaymentMethodsView(initialSelection: nil,
77-
paymentMethodTypes: [.stripe(.card), .stripe(.cashApp), .stripe(.klarna)],
77+
paymentMethodTypes: [.stripe(.card), .stripe(.cashApp), .stripe(.afterpayClearpay)],
7878
savedPaymentMethod: nil,
7979
appearance: appearance,
8080
shouldShowApplePay: true,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//
2+
// EmbeddedViewRepresentableSnapshotTest.swift
3+
// StripePaymentSheetTests
4+
//
5+
// Created by Nick Porter on 1/31/25.
6+
//
7+
8+
import XCTest
9+
import SwiftUI
10+
import StripeCoreTestUtils
11+
@testable import StripePayments
12+
@testable import StripePaymentsTestUtils
13+
@_spi(EmbeddedPaymentElementPrivateBeta) @testable import StripePaymentSheet
14+
@testable import StripeUICore
15+
16+
@MainActor
17+
class EmbeddedViewRepresentableSnapshotTest: STPSnapshotTestCase {
18+
19+
func testEmbeddedPaymentElementView() async throws {
20+
let intentConfig = EmbeddedPaymentElement.IntentConfiguration(
21+
mode: .payment(amount: 1000, currency: "USD"),
22+
paymentMethodTypes: ["card", "cashapp", "us_bank_account","link", "apple_pay", "afterpay_clearpay"]
23+
) { _, _, _ in
24+
// In these tests, we don't call confirm, so pass an empty handler.
25+
}
26+
27+
var config = EmbeddedPaymentElement.Configuration._testValue_MostPermissive(isApplePayEnabled: false)
28+
config.apiClient = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey)
29+
30+
// Create our SwiftUI view
31+
let viewModel = EmbeddedPaymentElementViewModel()
32+
let swiftUIView = EmbeddedPaymentElementView(viewModel: viewModel)
33+
.animation(nil) // Disable animations for testing
34+
try await viewModel.load(intentConfiguration: intentConfig, configuration: config)
35+
36+
// Embed `swiftUIView` in a UIWindow for rendering
37+
let hostingVC = makeWindowWithEmbeddedView(swiftUIView)
38+
viewModel.embeddedPaymentElement?.presentingViewController = hostingVC
39+
40+
// Assume the hostingVC only has 1 subview...
41+
XCTAssertFalse(hostingVC.view.subviews.isEmpty)
42+
let subview = hostingVC.view.subviews[0]
43+
44+
verify(subview, identifier: "before_height_change")
45+
46+
// Simulate a height change
47+
viewModel.testHeightChange()
48+
49+
verify(subview, identifier: "after_height_change")
50+
51+
// We need to set presentingViewController during testing since the UIApplication.shared.window is nil during testing
52+
viewModel.embeddedPaymentElement?.presentingViewController = hostingVC
53+
54+
// Toggle height back to original state
55+
viewModel.testHeightChange()
56+
57+
verify(subview, identifier: "after_second_height_change")
58+
59+
viewModel.embeddedPaymentElement?.presentingViewController = hostingVC
60+
61+
// Toggle height back to original state
62+
viewModel.testHeightChange()
63+
64+
verify(subview, identifier: "after_third_height_change")
65+
}
66+
67+
// MARK: - Helpers
68+
69+
private func createEmbeddedPaymentElement(
70+
intentConfiguration: EmbeddedPaymentElement.IntentConfiguration,
71+
configuration: EmbeddedPaymentElement.Configuration
72+
) async throws -> EmbeddedPaymentElement {
73+
return try await EmbeddedPaymentElement.create(
74+
intentConfiguration: intentConfiguration,
75+
configuration: configuration
76+
)
77+
}
78+
79+
/// Wraps a SwiftUI `EmbeddedViewRepresentable` in a UIWindow to ensure
80+
/// the SwiftUI content is actually rendered prior to snapshotting.
81+
private func makeWindowWithEmbeddedView(
82+
_ swiftUIView: some View,
83+
width: CGFloat = 320,
84+
height: CGFloat = 800
85+
) -> UIViewController {
86+
// Create a UIHostingController for a SwiftUI view.
87+
let hostingController = UIHostingController(rootView: swiftUIView)
88+
hostingController.view.layoutMargins = .zero
89+
hostingController.view.preservesSuperviewLayoutMargins = false
90+
91+
// Create a UIWindow & set its rootViewController to our hosting controller.
92+
let window = UIWindow(frame: CGRect(x: 0, y: 0, width: width, height: height))
93+
window.rootViewController = hostingController
94+
window.isHidden = false
95+
96+
// Force layout so SwiftUI draws its content.
97+
hostingController.view.setNeedsLayout()
98+
hostingController.view.layoutIfNeeded()
99+
100+
return hostingController
101+
}
102+
103+
func verify(
104+
_ view: UIView,
105+
identifier: String? = nil,
106+
file: StaticString = #filePath,
107+
line: UInt = #line
108+
) {
109+
STPSnapshotVerifyView(view, identifier: identifier, file: file, line: line)
110+
}
111+
}

0 commit comments

Comments
 (0)