diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 014d866e9b..8c9bf1c390 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -35,6 +35,10 @@ 2C8EC6E12CCD7BA700D6CCF8 /* ComponentOverrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8EC6E02CCD7BA300D6CCF8 /* ComponentOverrides.swift */; }; 2C8EC71B2CCDD43900D6CCF8 /* ComponentViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8EC71A2CCDD43500D6CCF8 /* ComponentViewState.swift */; }; 2C8EC7202CCF276100D6CCF8 /* LocalizedPartials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8EC71F2CCF275E00D6CCF8 /* LocalizedPartials.swift */; }; + 2C91068A2CE22D3500189565 /* FlexVStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9106892CE22D3500189565 /* FlexVStack.swift */; }; + 2C91068C2CE22D4F00189565 /* JustifyContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C91068B2CE22D4F00189565 /* JustifyContent.swift */; }; + 2C91068E2CE2481A00189565 /* SizeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C91068D2CE2481800189565 /* SizeModifier.swift */; }; + 2C9106FF2CE29F1400189565 /* StoredEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6FEE42AA940B700780B45 /* StoredEvent.swift */; }; 2CAB87F72CAAB13200247013 /* CornerBorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAB87F62CAAB13200247013 /* CornerBorder.swift */; }; 2CB8CF9327BF538F00C34DE3 /* PlatformInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CB8CF9227BF538F00C34DE3 /* PlatformInfo.swift */; }; 2CC791552CC0452100FBE120 /* PurchaseButtonComponentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC791522CC0452100FBE120 /* PurchaseButtonComponentViewModel.swift */; }; @@ -431,7 +435,6 @@ 4FDF10F12A7262D8004F3680 /* SK2ProductFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FDF10EF2A7262D8004F3680 /* SK2ProductFetcher.swift */; }; 4FE0685F2A5F54C500B8F56C /* PackageTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE0685E2A5F54C500B8F56C /* PackageTypeTests.swift */; }; 4FE6669F2A2F95A1004EEAFC /* PaywallExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6669E2A2F95A1004EEAFC /* PaywallExtensions.swift */; }; - 4FE6FEE52AA940B800780B45 /* StoredEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6FEE42AA940B700780B45 /* StoredEvent.swift */; }; 4FE6FEEA2AA940E300780B45 /* PaywallEventStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6FEE72AA940E300780B45 /* PaywallEventStoreTests.swift */; }; 4FE6FEEB2AA940E300780B45 /* PaywallEventSerializerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6FEE82AA940E300780B45 /* PaywallEventSerializerTests.swift */; }; 4FEF41AB2B4F2F3400CD699F /* MapAppStoreDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FEF41AA2B4F2F3400CD699F /* MapAppStoreDetector.swift */; }; @@ -1018,13 +1021,13 @@ FDAADFD32BE2B99900BD1659 /* MockStoreKit2ObserverModePurchaseDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAADFD22BE2B99900BD1659 /* MockStoreKit2ObserverModePurchaseDetector.swift */; }; FDAADFD42BE2B99900BD1659 /* MockStoreKit2ObserverModePurchaseDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAADFD22BE2B99900BD1659 /* MockStoreKit2ObserverModePurchaseDetector.swift */; }; FDAADFD72BE2C67700BD1659 /* StoreKit2ObserverModePurchaseDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAADFD52BE2C67700BD1659 /* StoreKit2ObserverModePurchaseDetectorTests.swift */; }; - FDC892D12CCAD0EE000AEB9F /* MockStoreKit2PurchaseIntentListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC892D02CCAD0EE000AEB9F /* MockStoreKit2PurchaseIntentListener.swift */; }; - FDC892D22CCAD0EE000AEB9F /* MockStoreKit2PurchaseIntentListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC892D02CCAD0EE000AEB9F /* MockStoreKit2PurchaseIntentListener.swift */; }; FDAC7B532CD3D67600DFC0D9 /* WinBackOfferEligibilityCalculatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAC7B522CD3D67600DFC0D9 /* WinBackOfferEligibilityCalculatorType.swift */; }; FDAC7B552CD3D7A500DFC0D9 /* WinBackOfferEligibilityCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAC7B542CD3D7A200DFC0D9 /* WinBackOfferEligibilityCalculator.swift */; }; FDAC7B572CD3FD8500DFC0D9 /* MockWinBackOfferEligibilityCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAC7B562CD3FD8500DFC0D9 /* MockWinBackOfferEligibilityCalculator.swift */; }; FDAC7B582CD3FD8500DFC0D9 /* MockWinBackOfferEligibilityCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAC7B562CD3FD8500DFC0D9 /* MockWinBackOfferEligibilityCalculator.swift */; }; FDAC7B5B2CD4085800DFC0D9 /* PurchasesWinBackOfferTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDAC7B5A2CD4085800DFC0D9 /* PurchasesWinBackOfferTests.swift */; }; + FDC892D12CCAD0EE000AEB9F /* MockStoreKit2PurchaseIntentListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC892D02CCAD0EE000AEB9F /* MockStoreKit2PurchaseIntentListener.swift */; }; + FDC892D22CCAD0EE000AEB9F /* MockStoreKit2PurchaseIntentListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC892D02CCAD0EE000AEB9F /* MockStoreKit2PurchaseIntentListener.swift */; }; FDC892FE2CD157F1000AEB9F /* WinBackOffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC892FD2CD157F1000AEB9F /* WinBackOffer.swift */; }; /* End PBXBuildFile section */ @@ -1221,6 +1224,9 @@ 2C8EC6E02CCD7BA300D6CCF8 /* ComponentOverrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentOverrides.swift; sourceTree = ""; }; 2C8EC71A2CCDD43500D6CCF8 /* ComponentViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentViewState.swift; sourceTree = ""; }; 2C8EC71F2CCF275E00D6CCF8 /* LocalizedPartials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPartials.swift; sourceTree = ""; }; + 2C9106892CE22D3500189565 /* FlexVStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexVStack.swift; sourceTree = ""; }; + 2C91068B2CE22D4F00189565 /* JustifyContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustifyContent.swift; sourceTree = ""; }; + 2C91068D2CE2481800189565 /* SizeModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SizeModifier.swift; sourceTree = ""; }; 2CAB87F62CAAB13200247013 /* CornerBorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CornerBorder.swift; sourceTree = ""; }; 2CB8CF9227BF538F00C34DE3 /* PlatformInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformInfo.swift; sourceTree = ""; }; 2CC7914B2CC0452100FBE120 /* PackageComponentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageComponentView.swift; sourceTree = ""; }; @@ -2220,11 +2226,11 @@ FDAADFCE2BE2B84500BD1659 /* StoreKit2ObserverModePurchaseDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2ObserverModePurchaseDetector.swift; sourceTree = ""; }; FDAADFD22BE2B99900BD1659 /* MockStoreKit2ObserverModePurchaseDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreKit2ObserverModePurchaseDetector.swift; sourceTree = ""; }; FDAADFD52BE2C67700BD1659 /* StoreKit2ObserverModePurchaseDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2ObserverModePurchaseDetectorTests.swift; sourceTree = ""; }; - FDC892D02CCAD0EE000AEB9F /* MockStoreKit2PurchaseIntentListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreKit2PurchaseIntentListener.swift; sourceTree = ""; }; FDAC7B522CD3D67600DFC0D9 /* WinBackOfferEligibilityCalculatorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WinBackOfferEligibilityCalculatorType.swift; sourceTree = ""; }; FDAC7B542CD3D7A200DFC0D9 /* WinBackOfferEligibilityCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WinBackOfferEligibilityCalculator.swift; sourceTree = ""; }; FDAC7B562CD3FD8500DFC0D9 /* MockWinBackOfferEligibilityCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockWinBackOfferEligibilityCalculator.swift; sourceTree = ""; }; FDAC7B5A2CD4085800DFC0D9 /* PurchasesWinBackOfferTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesWinBackOfferTests.swift; sourceTree = ""; }; + FDC892D02CCAD0EE000AEB9F /* MockStoreKit2PurchaseIntentListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreKit2PurchaseIntentListener.swift; sourceTree = ""; }; FDC892FD2CD157F1000AEB9F /* WinBackOffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WinBackOffer.swift; sourceTree = ""; }; FECF627761D375C8431EB866 /* StoreProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreProduct.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -4512,6 +4518,7 @@ 88AD01352C74196600AA1F2B /* Components */ = { isa = PBXGroup; children = ( + 2C91068D2CE2481800189565 /* SizeModifier.swift */, 2C08B3342CDD16550024857B /* PaywallComponentViewModel.swift */, 2C08B3162CDA443A0024857B /* ViewModelFactory.swift */, 2C8EC71F2CCF275E00D6CCF8 /* LocalizedPartials.swift */, @@ -4574,7 +4581,9 @@ 88B1BAEA2C813A3C001B7EE5 /* Stack */ = { isa = PBXGroup; children = ( + 2C91068B2CE22D4F00189565 /* JustifyContent.swift */, 2C08B3062CD55D590024857B /* FlexHStack.swift */, + 2C9106892CE22D3500189565 /* FlexVStack.swift */, 88B1BAE82C813A3C001B7EE5 /* StackComponentView.swift */, 88B1BAE92C813A3C001B7EE5 /* StackComponentViewModel.swift */, ); @@ -5533,6 +5542,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2C9106FF2CE29F1400189565 /* StoredEvent.swift in Sources */, B3A36AAE26BC76340059EDEA /* CustomerInfoManager.swift in Sources */, F5FCD3EA27DA0D0B003BDC04 /* PriceFormatterProvider.swift in Sources */, B3083A132699334C007B5503 /* Offering.swift in Sources */, @@ -5834,9 +5844,7 @@ 5766C622282DAA700067D886 /* GetIntroEligibilityResponse.swift in Sources */, 35E840CC270FB70D00899AE2 /* ManageSubscriptionsHelper.swift in Sources */, 6E38843A0CAFD551013D0A3F /* StoreProduct.swift in Sources */, - 4FE6FEE52AA940B800780B45 /* PaywallStoredEvent.swift in Sources */, FD20467F2CB82F2000166727 /* StoreKit2PurchaseIntentListener.swift in Sources */, - 4FE6FEE52AA940B800780B45 /* StoredEvent.swift in Sources */, B34605BD279A6E380031CA74 /* CallbackCacheStatus.swift in Sources */, 42F1DF385E3C1F9903A07FBF /* ProductsFetcherSK1.swift in Sources */, 805B60C97993B311CEC93EAF /* ProductsFetcherSK2.swift in Sources */, @@ -6233,6 +6241,7 @@ 1E5F8F782C46BBD90041EECD /* CustomerCenterAction.swift in Sources */, 1ED4CA9F2CC25A5F0021AB8F /* SafariView.swift in Sources */, 887A60CC2C1D037000E1A461 /* PaywallFontProvider.swift in Sources */, + 2C91068E2CE2481A00189565 /* SizeModifier.swift in Sources */, 887A60B82C1D037000E1A461 /* Template1View.swift in Sources */, 887A60C62C1D037000E1A461 /* LoadingPaywallView.swift in Sources */, 88B1BAF22C813A3C001B7EE5 /* SpacerComponentView.swift in Sources */, @@ -6284,6 +6293,7 @@ 2C2AEB0F2CA64E0E00A50F38 /* Template1Preview.swift in Sources */, 88EA80ED2C8771A7003E6675 /* TemplateComponentsView+Extensions.swift in Sources */, 88A543E32C37A4970039C6A5 /* Template7View.swift in Sources */, + 2C91068C2CE22D4F00189565 /* JustifyContent.swift in Sources */, 887A60CB2C1D037000E1A461 /* TemplateBackgroundImageView.swift in Sources */, 353FDC112CA4472B0055F328 /* StoreProduct+Extensions.swift in Sources */, C3AD12BA2C6EA61F00A4F86F /* CompatibilityNavigationStack.swift in Sources */, @@ -6336,6 +6346,7 @@ 357CEC702C5940CE00A80837 /* ColorFromAppearance.swift in Sources */, 887A60832C1D037000E1A461 /* VersionDetector.swift in Sources */, 88B1BAFC2C813A3C001B7EE5 /* ImageComponentView.swift in Sources */, + 2C91068A2CE22D3500189565 /* FlexVStack.swift in Sources */, 887A60872C1D037000E1A461 /* ViewExtensions.swift in Sources */, 2C8EC6DB2CCC23B700D6CCF8 /* Template5Preview.swift in Sources */, 88B1BB022C813A3C001B7EE5 /* StackComponentView.swift in Sources */, diff --git a/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentView.swift b/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentView.swift index feb71ab72a..a903e74cc2 100644 --- a/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentView.swift +++ b/RevenueCatUI/Templates/Components/Packages/Package/PackageComponentView.swift @@ -85,7 +85,7 @@ struct PackageComponentView_Previews: PreviewProvider { margin: .zero )) ], - dimension: .vertical(.leading), + dimension: .vertical(.leading, .start), spacing: 0, backgroundColor: nil, padding: .init(top: 10, diff --git a/RevenueCatUI/Templates/Components/SizeModifier.swift b/RevenueCatUI/Templates/Components/SizeModifier.swift new file mode 100644 index 0000000000..11b32af8ae --- /dev/null +++ b/RevenueCatUI/Templates/Components/SizeModifier.swift @@ -0,0 +1,71 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// SizeModifier.swift +// +// Created by Josh Holtz on 11/11/24. + +import RevenueCat +import SwiftUI + +#if PAYWALL_COMPONENTS + +struct SizeModifier: ViewModifier { + + var size: PaywallComponent.Size + + func body(content: Content) -> some View { + content + .applyWidth(size.width) + .applyHeight(size.height) + } + +} + +extension View { + + @ViewBuilder + func applyWidth(_ sizeConstraint: PaywallComponent.SizeConstraint) -> some View { + switch sizeConstraint { + case .fit: + self + case .fill: + self + .frame(maxWidth: .infinity) + case .fixed(let value): + self + .frame(width: CGFloat(value)) + } + } + + @ViewBuilder + func applyHeight(_ sizeConstraint: PaywallComponent.SizeConstraint) -> some View { + switch sizeConstraint { + case .fit: + self + case .fill: + self + .frame(maxHeight: .infinity) + case .fixed(let value): + self + .frame(height: CGFloat(value)) + } + } + +} + +extension View { + + func size(_ size: PaywallComponent.Size) -> some View { + self.modifier(SizeModifier(size: size)) + } + +} + +#endif diff --git a/RevenueCatUI/Templates/Components/Stack/FlexHStack.swift b/RevenueCatUI/Templates/Components/Stack/FlexHStack.swift index 354467be3b..b7e8d695ca 100644 --- a/RevenueCatUI/Templates/Components/Stack/FlexHStack.swift +++ b/RevenueCatUI/Templates/Components/Stack/FlexHStack.swift @@ -15,12 +15,9 @@ import SwiftUI #if PAYWALL_COMPONENTS -enum JustifyContent { - case start, center, end, spaceBetween, spaceAround, spaceEvenly -} - @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) struct FlexHStack: View { + let alignment: VerticalAlignment let justifyContent: JustifyContent let spacing: CGFloat? diff --git a/RevenueCatUI/Templates/Components/Stack/FlexVStack.swift b/RevenueCatUI/Templates/Components/Stack/FlexVStack.swift new file mode 100644 index 0000000000..ddf6fec714 --- /dev/null +++ b/RevenueCatUI/Templates/Components/Stack/FlexVStack.swift @@ -0,0 +1,92 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// FlexVStack.swift +// +// Created by Josh Holtz on 11/1/24. + +import SwiftUI + +#if PAYWALL_COMPONENTS + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +struct FlexVStack: View { + let alignment: HorizontalAlignment + let justifyContent: JustifyContent + let spacing: CGFloat? + let componentViewModels: [PaywallComponentViewModel] + + let onDismiss: () -> Void + + init( + alignment: HorizontalAlignment, + spacing: CGFloat?, + justifyContent: JustifyContent, + componentViewModels: [PaywallComponentViewModel], + onDismiss: @escaping () -> Void + ) { + self.alignment = alignment + self.spacing = spacing + self.justifyContent = justifyContent + self.componentViewModels = componentViewModels + self.onDismiss = onDismiss + } + + var body: some View { + VStack(alignment: self.alignment, spacing: self.spacing) { + switch justifyContent { + case .start: + ForEach(0.. some View { - if let width = width { - switch width.type { - case .fit: - content - case .fill: - content - .frame(maxWidth: .infinity) - case .fixed: - if let value = width.value { - content - .frame(width: CGFloat(value)) - } else { - content - } - } - } else { - content + // Default - Fit + StackComponentView( + // swiftlint:disable:next force_try + viewModel: try! .init( + component: .init( + components: [ + .text(.init( + text: "text_1", + color: .init(light: .hex("#000000")))) + ], + size: .init( + width: .fit, + height: .fit + ), + backgroundColor: .init(light: .hex("#ff0000")) + ), + localizedStrings: [ + "text_1": .string("Hey") + ]), + onDismiss: {} + ) + .previewLayout(.sizeThatFits) + .previewDisplayName("Default - Fit") + + // Default - Fill Fit Fixed Fill + HStack(spacing: 0) { + StackComponentView( + // swiftlint:disable:next force_try + viewModel: try! .init( + component: .init( + components: [ + .text(.init( + text: "text_1", + color: .init(light: .hex("#000000")))) + ], + size: .init( + width: .fill, + height: .fit + ), + backgroundColor: .init(light: .hex("#ff0000")) + ), + localizedStrings: [ + "text_1": .string("Hey") + ]), + onDismiss: {} + ) + + StackComponentView( + // swiftlint:disable:next force_try + viewModel: try! .init( + component: .init( + components: [ + .text(.init( + text: "text_1", + color: .init(light: .hex("#000000")))) + ], + size: .init( + width: .fit, + height: .fit + ), + backgroundColor: .init(light: .hex("#0000ff")) + ), + localizedStrings: [ + "text_1": .string("Hey") + ]), + onDismiss: {} + ) + + StackComponentView( + // swiftlint:disable:next force_try + viewModel: try! .init( + component: .init( + components: [ + .text(.init( + text: "text_1", + color: .init(light: .hex("#000000")))) + ], + size: .init( + width: .fixed(100), + height: .fit + ), + backgroundColor: .init(light: .hex("#00ff00")) + ), + localizedStrings: [ + "text_1": .string("Hey") + ]), + onDismiss: {} + ) + + StackComponentView( + // swiftlint:disable:next force_try + viewModel: try! .init( + component: .init( + components: [ + .text(.init( + text: "text_1", + color: .init(light: .hex("#000000")))) + ], + size: .init( + width: .fill, + height: .fit + ), + backgroundColor: .init(light: .hex("#ff0000")) + ), + localizedStrings: [ + "text_1": .string("Hey") + ]), + onDismiss: {} + ) } + .previewLayout(.sizeThatFits) + .previewDisplayName("Default - Fill Fit Fixed Fill") } } -extension View { +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +fileprivate extension StackComponentViewModel { + + convenience init( + component: PaywallComponent.StackComponent, + localizedStrings: PaywallComponent.LocalizationDictionary + ) throws { + let validator = PackageValidator() + let factory = ViewModelFactory() + let offering = Offering(identifier: "", serverDescription: "", availablePackages: []) + + let viewModels = try component.components.map { component in + try factory.toViewModel( + component: component, + packageValidator: validator, + offering: offering, + localizedStrings: localizedStrings + ) + } - func width(_ width: PaywallComponent.WidthSize? = nil) -> some View { - self.modifier(WidthModifier(width: width)) + self.init( + component: component, + viewModels: viewModels + ) } } #endif + +#endif diff --git a/RevenueCatUI/Templates/Components/Stack/StackComponentViewModel.swift b/RevenueCatUI/Templates/Components/Stack/StackComponentViewModel.swift index 9c0c9532ea..bb2ae9746e 100644 --- a/RevenueCatUI/Templates/Components/Stack/StackComponentViewModel.swift +++ b/RevenueCatUI/Templates/Components/Stack/StackComponentViewModel.swift @@ -19,6 +19,10 @@ import SwiftUI @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) class StackComponentViewModel { + enum StackStrategy { + case normal, lazy, flex + } + private let component: PaywallComponent.StackComponent let viewModels: [PaywallComponentViewModel] @@ -31,30 +35,45 @@ class StackComponentViewModel { self.viewModels = viewModels } - var shouldUseVStack: Bool { - switch self.dimension { - case .vertical: - if viewModels.count < 3 { - return true - } - return false - case .horizontal, .zlayer: - return false + var vstackStrategy: StackStrategy { + // Ensure vertical + guard case let .vertical(_, distribution) = self.dimension else { + return .normal + } + + // Normal stragety for fit + switch self.component.size.height { + case .fit: + return .normal + case .fill, .fixed: + break + } + + // Normal strategy if start + guard case .start = distribution else { + return .flex + } + + // WIP: Look deeper in tree + if self.components.count > 3 { + return .lazy + } else { + return .normal } } - var shouldUseFlex: Bool { - guard let widthType = self.component.width?.type else { - return false + var hstackStrategy: StackStrategy { + // Ensure horizontal + guard case .horizontal = self.dimension else { + return .normal } - switch widthType { + // Not stragety for fit + switch self.component.size.width { case .fit: - return false - case .fill: - return true - case .fixed: - return true + return .normal + case .fill, .fixed: + return .flex } } @@ -82,8 +101,8 @@ class StackComponentViewModel { component.margin.edgeInsets } - var width: PaywallComponent.WidthSize? { - component.width + var size: PaywallComponent.Size { + component.size } var cornerRadiuses: CornerBorderModifier.RaidusInfo? { diff --git a/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template1Preview.swift b/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template1Preview.swift index 0fdb262f3e..34c14cecda 100644 --- a/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template1Preview.swift +++ b/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template1Preview.swift @@ -76,7 +76,7 @@ private enum Template1Preview { margin: .zero )) ], - dimension: .vertical(.center), + dimension: .vertical(.center, .start), spacing: 0, backgroundColor: nil, padding: .init(top: 0, @@ -115,7 +115,6 @@ private enum Template1Preview { .package(package), .purchaseButton(purchaseButton) ], - width: .init(type: .fill, value: nil), spacing: 30, backgroundColor: nil, margin: .init(top: 0, @@ -129,7 +128,6 @@ private enum Template1Preview { .image(catImage), .stack(contentStack) ], - width: .init(type: .fill, value: nil), spacing: 20, backgroundColor: nil ) diff --git a/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template5Preview.swift b/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template5Preview.swift index df2060deb6..152e5c0a78 100644 --- a/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template5Preview.swift +++ b/RevenueCatUI/Templates/Components/TemplateComponentsViewPreviews/Template5Preview.swift @@ -97,7 +97,7 @@ private enum Template5Preview { margin: .zero )) ], - dimension: .vertical(.leading), + dimension: .vertical(.leading, .start), spacing: 0, backgroundColor: nil, padding: PaywallComponent.Padding(top: 10, @@ -136,7 +136,7 @@ private enum Template5Preview { textStyle: .caption )) ], - dimension: .vertical(.center), + dimension: .vertical(.center, .start), spacing: 10, backgroundColor: nil, margin: .init(top: 20, @@ -172,7 +172,6 @@ private enum Template5Preview { .purchaseButton(purchaseButton) ], dimension: .horizontal(.center, .start), - width: .init(type: .fill, value: nil), spacing: 0, backgroundColor: nil ) @@ -184,8 +183,7 @@ private enum Template5Preview { .stack(packagesStack), .stack(purchaseButtonStack) ], - dimension: .vertical(.leading), - width: .init(type: .fill, value: nil), + dimension: .vertical(.leading, .start), spacing: 30, backgroundColor: nil, margin: .init(top: 0, @@ -199,7 +197,6 @@ private enum Template5Preview { .image(catImage), .stack(contentStack) ], - width: .init(type: .fill, value: nil), spacing: 20, backgroundColor: nil ) diff --git a/Sources/Paywalls/Components/Common/Dimension.swift b/Sources/Paywalls/Components/Common/Dimension.swift index e5fd2e16d1..ffcf51b848 100644 --- a/Sources/Paywalls/Components/Common/Dimension.swift +++ b/Sources/Paywalls/Components/Common/Dimension.swift @@ -20,7 +20,7 @@ public extension PaywallComponent { enum Dimension: Codable, Sendable, Hashable { - case vertical(HorizontalAlignment) + case vertical(HorizontalAlignment, FlexDistribution) case horizontal(VerticalAlignment, FlexDistribution) case zlayer(TwoDimensionAlignment) @@ -28,9 +28,10 @@ public extension PaywallComponent { var container = encoder.container(keyedBy: CodingKeys.self) switch self { - case .vertical(let alignment): + case .vertical(let alignment, let distribution): try container.encode(DimensionType.vertical.rawValue, forKey: .type) try container.encode(alignment, forKey: .alignment) + try container.encode(distribution, forKey: .distribution) case .horizontal(let alignment, let distribution): try container.encode(DimensionType.horizontal.rawValue, forKey: .type) try container.encode(alignment, forKey: .alignment) @@ -48,7 +49,8 @@ public extension PaywallComponent { switch type { case .vertical: let alignment = try container.decode(HorizontalAlignment.self, forKey: .alignment) - self = .vertical(alignment) + let distribution = try container.decode(FlexDistribution.self, forKey: .distribution) + self = .vertical(alignment, distribution) case .horizontal: let alignment = try container.decode(VerticalAlignment.self, forKey: .alignment) let distribution = try container.decode(FlexDistribution.self, forKey: .distribution) @@ -64,7 +66,7 @@ public extension PaywallComponent { } public static func vertical() -> Dimension { - return .vertical(.center) + return .vertical(.center, .start) } // swiftlint:disable:next nesting diff --git a/Sources/Paywalls/Components/Common/PaywallComponentPropertyTypes.swift b/Sources/Paywalls/Components/Common/PaywallComponentPropertyTypes.swift index 9ca13b65cc..31e17e66fa 100644 --- a/Sources/Paywalls/Components/Common/PaywallComponentPropertyTypes.swift +++ b/Sources/Paywalls/Components/Common/PaywallComponentPropertyTypes.swift @@ -171,19 +171,69 @@ public extension PaywallComponent { } - enum WidthSizeType: String, Codable, Sendable, Hashable, Equatable { - case fit, fill, fixed + struct Size: Codable, Sendable, Hashable, Equatable { + + public let width: SizeConstraint + public let height: SizeConstraint + + public init(width: PaywallComponent.SizeConstraint, height: PaywallComponent.SizeConstraint) { + self.width = width + self.height = height + } + } - struct WidthSize: Codable, Sendable, Hashable, Equatable { + enum SizeConstraint: Codable, Sendable, Hashable { + + case fit + case fill + case fixed(UInt) + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) - public init(type: WidthSizeType, value: Int? ) { - self.type = type - self.value = value + switch self { + case .fit: + try container.encode(SizeConstraintType.fit.rawValue, forKey: .type) + case .fill: + try container.encode(SizeConstraintType.fill.rawValue, forKey: .type) + case .fixed(let value): + try container.encode(SizeConstraintType.fixed.rawValue, forKey: .type) + try container.encode(value, forKey: .value) + } } - public let type: WidthSizeType - public let value: Int? + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(SizeConstraintType.self, forKey: .type) + + switch type { + case .fit: + self = .fit + case .fill: + self = .fill + case .fixed: + let value = try container.decode(UInt.self, forKey: .value) + self = .fixed(value) + } + } + + // swiftlint:disable:next nesting + private enum CodingKeys: String, CodingKey { + + case type + case value + + } + + // swiftlint:disable:next nesting + private enum SizeConstraintType: String, Decodable { + + case fit + case fill + case fixed + + } } diff --git a/Sources/Paywalls/Components/PaywallStackComponent.swift b/Sources/Paywalls/Components/PaywallStackComponent.swift index 7c3515f91d..ee0303ac4a 100644 --- a/Sources/Paywalls/Components/PaywallStackComponent.swift +++ b/Sources/Paywalls/Components/PaywallStackComponent.swift @@ -22,7 +22,7 @@ public extension PaywallComponent { let type: ComponentType public let components: [PaywallComponent] - public let width: WidthSize? + public let size: Size public let spacing: CGFloat? public let backgroundColor: ColorScheme? public let dimension: Dimension @@ -36,8 +36,8 @@ public extension PaywallComponent { public init( components: [PaywallComponent], - dimension: Dimension = .vertical(.center), - width: WidthSize? = nil, + dimension: Dimension = .vertical(.center, .start), + size: Size = .init(width: .fill, height: .fit), spacing: CGFloat? = nil, backgroundColor: ColorScheme? = nil, padding: Padding = .zero, @@ -48,7 +48,7 @@ public extension PaywallComponent { overrides: ComponentOverrides? = nil ) { self.components = components - self.width = width + self.size = size self.spacing = spacing self.backgroundColor = backgroundColor self.type = .stack @@ -66,7 +66,7 @@ public extension PaywallComponent { struct PartialStackComponent: PartialComponent { public let visible: Bool? - public let width: WidthSize? + public let size: Size? public let spacing: CGFloat? public let backgroundColor: ColorScheme? public let dimension: Dimension? @@ -79,7 +79,7 @@ public extension PaywallComponent { public init( visible: Bool? = true, dimension: Dimension? = nil, - width: WidthSize? = nil, + size: Size? = nil, spacing: CGFloat? = nil, backgroundColor: ColorScheme? = nil, padding: Padding? = nil, @@ -89,7 +89,7 @@ public extension PaywallComponent { shadow: Shadow? = nil ) { self.visible = visible - self.width = width + self.size = size self.spacing = spacing self.backgroundColor = backgroundColor self.dimension = dimension