Skip to content

Commit 8097087

Browse files
joshdholtzclaude
andcommitted
Fix PaywallView styling timing issue with safe forced layout updates
Fixes an issue where styles applied to RevenueCatUI.Paywall component were not being applied immediately. The problem was that the paywall's native view setup was deferred until layoutSubviews, causing a timing issue with React Native's style application. This solution preserves the original view hierarchy timing while forcing immediate layout updates when React Native applies style changes, with robust protection against infinite layout loops. Changes: - Override reactSetFrame, setBounds, and setFrame methods - Call setNeedsLayout + layoutIfNeeded to force immediate layout updates - Add isInLayoutUpdate flag to prevent infinite layout loops - Use dispatch_async to break synchronous recursion chains - Only trigger layout for actual frame/bounds changes - Preserve original view controller hierarchy setup timing This approach is safer than restructuring the view hierarchy timing as it: - Maintains SwiftUI environment context - Preserves safe area and layout guide behavior - Works with React Native's layout system - Protected against infinite layout loops Fixes #1366 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a718688 commit 8097087

File tree

1 file changed

+48
-1
lines changed

1 file changed

+48
-1
lines changed

react-native-purchases-ui/ios/PaywallViewWrapper.m

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ @interface PaywallViewWrapper ()
2323

2424
@property(strong, nonatomic) RCPaywallViewController *paywallViewController;
2525
@property(nonatomic) BOOL addedToHierarchy;
26+
@property(nonatomic) BOOL isInLayoutUpdate;
2627

2728
@end
2829

@@ -38,15 +39,58 @@ - (instancetype)initWithPaywallViewController:(RCPaywallViewController *)paywall
3839
return self;
3940
}
4041

41-
4242
- (void)reactSetFrame:(CGRect)frame
4343
{
4444
NSLog(@"RNPaywalls - reactSetFrame: %@", NSStringFromCGRect(frame));
4545

4646
[super reactSetFrame: frame];
47+
48+
// Only trigger layout if we're not already in a layout update to prevent infinite loops
49+
if (!self.isInLayoutUpdate) {
50+
[self setNeedsLayout];
51+
// Use dispatch_async to break potential synchronous loops
52+
dispatch_async(dispatch_get_main_queue(), ^{
53+
if (!self.isInLayoutUpdate) {
54+
[self layoutIfNeeded];
55+
}
56+
});
57+
}
58+
}
59+
60+
- (void)setBounds:(CGRect)bounds {
61+
CGRect oldBounds = self.bounds;
62+
[super setBounds:bounds];
63+
64+
// Only trigger layout if bounds actually changed and we're not in a layout update
65+
if (!self.isInLayoutUpdate && !CGRectEqualToRect(oldBounds, bounds)) {
66+
[self setNeedsLayout];
67+
dispatch_async(dispatch_get_main_queue(), ^{
68+
if (!self.isInLayoutUpdate) {
69+
[self layoutIfNeeded];
70+
}
71+
});
72+
}
73+
}
74+
75+
- (void)setFrame:(CGRect)frame {
76+
CGRect oldFrame = self.frame;
77+
[super setFrame:frame];
78+
79+
// Only trigger layout if frame actually changed and we're not in a layout update
80+
if (!self.isInLayoutUpdate && !CGRectEqualToRect(oldFrame, frame)) {
81+
[self setNeedsLayout];
82+
dispatch_async(dispatch_get_main_queue(), ^{
83+
if (!self.isInLayoutUpdate) {
84+
[self layoutIfNeeded];
85+
}
86+
});
87+
}
4788
}
4889

4990
- (void)layoutSubviews {
91+
// Set flag to prevent infinite layout loops
92+
self.isInLayoutUpdate = YES;
93+
5094
[super layoutSubviews];
5195

5296
CGSize size = self.bounds.size;
@@ -74,6 +118,9 @@ - (void)layoutSubviews {
74118
self.addedToHierarchy = YES;
75119
}
76120
}
121+
122+
// Clear flag after layout is complete
123+
self.isInLayoutUpdate = NO;
77124
}
78125

79126
- (void)setOptions:(NSDictionary *)options {

0 commit comments

Comments
 (0)