Skip to content

Commit

Permalink
Paywalls: improved support for dynamic type with snapshots (#2827)
Browse files Browse the repository at this point in the history
This improves the layout of the template, and adds a scroll view that
optionally scrolls if the content is too large.
This can serve as a the basis for future templates.

![iOS16-testAccessibility3
1](https://github.com/RevenueCat/purchases-ios/assets/685609/40cb6929-d7f0-49d4-a9c5-7fdea9747db9)
  • Loading branch information
NachoSoto committed Aug 24, 2023
1 parent 7f92d09 commit 6c3f65d
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 82 deletions.
23 changes: 23 additions & 0 deletions RevenueCatUI/Modifiers/ViewExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,27 @@ extension View {
}
}

@ViewBuilder
func scrollable(
_ axes: Axis.Set = .vertical,
if condition: Bool
) -> some View {
if condition {
ScrollView(axes) {
self
}
} else {
self
}
}

@ViewBuilder
func scrollBounceBehaviorBasedOnSize() -> some View {
if #available(iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4, *) {
self.scrollBounceBehavior(.basedOnSize)
} else {
self
}
}

}
87 changes: 47 additions & 40 deletions RevenueCatUI/Templates/Example1Template.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,51 +88,58 @@ private struct Example1TemplateContent: View {
@ViewBuilder
private var content: some View {
VStack {
AsyncImage(
url: self.data.headerImageURL,
transaction: .init(animation: Constants.defaultAnimation)
) { phase in
if let image = phase.image {
image
.fitToAspect(Self.imageAspectRatio, contentMode: .fill)
.edgesIgnoringSafeArea(.top)
} else if let error = phase.error {
DebugErrorView("Error loading image from '\(self.data.headerImageURL)': \(error)",
releaseBehavior: .emptyView)
} else {
Rectangle()
.hidden()
}
}
.frame(maxWidth: .infinity)
.aspectRatio(Self.imageAspectRatio, contentMode: .fit)
.clipShape(
Circle()
.offset(y: -100)
.scale(3.0)
)
ScrollView(.vertical) {
VStack {
AsyncImage(
url: self.data.headerImageURL,
transaction: .init(animation: Constants.defaultAnimation)
) { phase in
if let image = phase.image {
image
.fitToAspect(Self.imageAspectRatio, contentMode: .fill)
} else if let error = phase.error {
DebugErrorView("Error loading image from '\(self.data.headerImageURL)': \(error)",
releaseBehavior: .emptyView)
} else {
Rectangle()
.hidden()
}
}
.frame(maxWidth: .infinity)
.aspectRatio(Self.imageAspectRatio, contentMode: .fit)
.clipShape(
Circle()
.offset(y: -140)
.scale(3.0)
)
.padding(.bottom)

Spacer()
Spacer()

VStack {
Text(verbatim: self.data.localization.title)
.font(.largeTitle)
.fontWeight(.heavy)
.padding(.bottom)
Group {
Text(verbatim: self.data.localization.title)
.font(.largeTitle)
.fontWeight(.heavy)
.padding(.bottom)

Text(verbatim: self.data.localization.subtitle)
.font(.subheadline)
Text(verbatim: self.data.localization.subtitle)
.font(.subheadline)
}
.padding(.horizontal)
}
.foregroundColor(self.data.colors.foregroundColor)
.multilineTextAlignment(.center)
}
.scrollContentBackground(.hidden)
.scrollBounceBehaviorBasedOnSize()
.scrollIndicators(.automatic)
.edgesIgnoringSafeArea(.top)

Spacer()

self.offerDetails
Spacer()

self.button
}
.foregroundColor(self.data.colors.foregroundColor)
.multilineTextAlignment(.center)
.padding(.horizontal)
self.offerDetails
self.button
.padding(.horizontal)
}
.background(self.data.colors.backgroundColor)
}
Expand Down Expand Up @@ -203,7 +210,7 @@ private struct Example1TemplateContent: View {
.controlSize(.large)
}

private static let imageAspectRatio = 0.7
private static let imageAspectRatio = 1.1

}

Expand Down
33 changes: 33 additions & 0 deletions Tests/RevenueCatUITests/BaseSnapshotTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// BaseSnapshotTest.swift
//
//
// Created by Nacho Soto on 7/17/23.
//
import Nimble
import RevenueCat
@testable import RevenueCatUI
import SnapshotTesting
import XCTest

@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
class BaseSnapshotTest: TestCase {

override class func setUp() {
super.setUp()

// isRecording = true
}

}

@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
extension BaseSnapshotTest {

static let eligibleChecker: TrialOrIntroEligibilityChecker = .producing(eligibility: .eligible)
static let ineligibleChecker: TrialOrIntroEligibilityChecker = .producing(eligibility: .ineligible)
static let purchaseHandler: PurchaseHandler = .mock()

static let fullScreenSize: CGSize = .init(width: 460, height: 950)

}
32 changes: 32 additions & 0 deletions Tests/RevenueCatUITests/Helpers/DataExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// DataExtensions.swift
//
//
// Created by Nacho Soto on 7/17/23.
//

import Foundation
import RevenueCat
import RevenueCatUI

// MARK: - Extensions

extension Offering {

var paywallWithLocalImage: PaywallData {
return self.paywall!.withLocalImage
}

}

extension PaywallData {

var withLocalImage: Self {
var copy = self
copy.assetBaseURL = URL(fileURLWithPath: Bundle.module.bundlePath)
copy.config.headerImageName = "image.png"

return copy
}

}
77 changes: 77 additions & 0 deletions Tests/RevenueCatUITests/PaywallViewDynamicTypeTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//
// PaywallViewDynamicTypeTests.swift
//
//
// Created by Nacho Soto on 7/17/23.
//

import Nimble
import RevenueCat
@testable import RevenueCatUI
import SnapshotTesting
import SwiftUI

@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
class PaywallViewDynamicTypeTests: BaseSnapshotTest {

func testXSmall() {
Self.test(.xSmall)
}

func testSmall() {
Self.test(.small)
}

func testMedium() {
Self.test(.medium)
}

func testLarge() {
Self.test(.xLarge)
}

func testXLarge() {
Self.test(.xLarge)
}

func testXXLarge() {
Self.test(.xxLarge)
}

func testXXXLarge() {
Self.test(.xxxLarge)
}

func testAccessibility1() {
Self.test(.accessibility1)
}

func testAccessibility3() {
Self.test(.accessibility3)
}

func testAccessibility5() {
Self.test(.accessibility5)
}

}

@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
private extension PaywallViewDynamicTypeTests {

static func test(_ type: DynamicTypeSize) {
Self.createView(type)
.snapshot(size: Self.fullScreenSize)
}

private static func createView(_ type: DynamicTypeSize) -> some View {
let offering = TestData.offeringWithIntroOffer

return PaywallView(offering: offering,
paywall: offering.paywallWithLocalImage,
introEligibility: Self.eligibleChecker,
purchaseHandler: Self.purchaseHandler)
.dynamicTypeSize(type)
}

}
49 changes: 7 additions & 42 deletions Tests/RevenueCatUITests/PaywallViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,11 @@ import Nimble
import RevenueCat
@testable import RevenueCatUI
import SnapshotTesting
import XCTest

#if !os(macOS)

@available(iOS 16.0, tvOS 16.0, watchOS 9.0, *)
class PaywallViewTests: TestCase {

override class func setUp() {
super.setUp()

// isRecording = true
}
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
class PaywallViewTests: BaseSnapshotTest {

func testSamplePaywall() {
let offering = TestData.offeringWithNoIntroOffer
Expand All @@ -23,7 +16,7 @@ class PaywallViewTests: TestCase {
introEligibility: Self.eligibleChecker,
purchaseHandler: Self.purchaseHandler)

view.snapshot(size: Self.size)
view.snapshot(size: Self.fullScreenSize)
}

func testSamplePaywallWithIntroOffer() {
Expand All @@ -34,7 +27,7 @@ class PaywallViewTests: TestCase {
introEligibility: Self.eligibleChecker,
purchaseHandler: Self.purchaseHandler)

view.snapshot(size: Self.size)
view.snapshot(size: Self.fullScreenSize)
}

func testSamplePaywallWithIneligibleIntroOffer() {
Expand All @@ -45,7 +38,7 @@ class PaywallViewTests: TestCase {
introEligibility: Self.ineligibleChecker,
purchaseHandler: Self.purchaseHandler)

view.snapshot(size: Self.size)
view.snapshot(size: Self.fullScreenSize)
}

func testSamplePaywallWithLoadingEligibility() {
Expand All @@ -59,7 +52,7 @@ class PaywallViewTests: TestCase {
purchaseHandler: Self.purchaseHandler
)

view.snapshot(size: Self.size)
view.snapshot(size: Self.fullScreenSize)
}

func testDarkMode() {
Expand All @@ -72,35 +65,7 @@ class PaywallViewTests: TestCase {

view
.environment(\.colorScheme, .dark)
.snapshot(size: Self.size)
}

private static let eligibleChecker: TrialOrIntroEligibilityChecker = .producing(eligibility: .eligible)
private static let ineligibleChecker: TrialOrIntroEligibilityChecker = .producing(eligibility: .ineligible)
private static let purchaseHandler: PurchaseHandler = .mock()

private static let size: CGSize = .init(width: 460, height: 950)

}

// MARK: - Extensions

private extension Offering {

var paywallWithLocalImage: PaywallData {
return self.paywall!.withLocalImage
}

}

private extension PaywallData {

var withLocalImage: Self {
var copy = self
copy.assetBaseURL = URL(fileURLWithPath: Bundle.module.bundlePath)
copy.config.headerImageName = "image.png"

return copy
.snapshot(size: Self.fullScreenSize)
}

}
Expand Down

0 comments on commit 6c3f65d

Please sign in to comment.