Skip to content

Commit

Permalink
Fixes iOS PaywallFooter height (#247)
Browse files Browse the repository at this point in the history
  • Loading branch information
JayShortway authored Oct 18, 2024
1 parent d15464c commit 533f1d1
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,9 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
Expand All @@ -26,19 +22,17 @@ public actual fun PaywallFooter(
options: PaywallOptions,
mainContent: @Composable ((PaddingValues) -> Unit)?,
) {
var height by remember { mutableStateOf(0.dp) }
val paywallComposable = @Composable {
UIKitPaywall(
options = options,
footer = true,
modifier = Modifier
.fillMaxWidth()
.animateContentSize()
.wrapContentHeight()
.clip(
RoundedCornerShape(topStart = DefaultCornerRadius, topEnd = DefaultCornerRadius)
)
.animateContentSize()
.height(height),
onHeightChange = { newHeight -> height = newHeight.dp }
),
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,67 @@
package com.revenuecat.purchases.kmp.ui.revenuecatui

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.interop.UIKitViewController
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.UIKitViewController
import cocoapods.PurchasesHybridCommonUI.RCPaywallFooterViewController
import cocoapods.PurchasesHybridCommonUI.RCPaywallViewController
import com.revenuecat.purchases.kmp.mappings.toIosOffering
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.pointed
import objcnames.classes.RCOffering
import platform.UIKit.UIView

@Composable
internal fun UIKitPaywall(
options: PaywallOptions,
footer: Boolean,
modifier: Modifier = Modifier,
onHeightChange: (Int) -> Unit = { },
) {
val density = LocalDensity.current

/**
* Our intrinsic content size according to UIKit.
*/
var intrinsicContentSizePx by remember { mutableStateOf(0) }

// We remember this wrapper so we can keep a reference to RCPaywallViewController, even during
// recompositions. RCPaywallViewController itself is not yet instantiated here.
val viewControllerWrapper = remember { ViewControllerWrapper(null) }

// Keeping references to avoid them being deallocated.
val dismissRequestedHandler: (RCPaywallViewController?) -> Unit =
remember(options.dismissRequest) { { options.dismissRequest() } }
val delegate = remember(options.listener, onHeightChange) {
IosPaywallDelegate(options.listener, onHeightChange)
val delegate = remember(options.listener) {
IosPaywallDelegate(options.listener) {
// UIKit reports that our height was updated, so we're updating intrinsicContentSizePx
// to force a new measurement phase (below).
viewControllerWrapper.value?.view
?.getIntrinsicContentSizeOfFirstSubView()
?.also { intrinsicContentSizePx = with(density) { it.dp.roundToPx() } }
}
}
// We remember this wrapper so we can keep a reference to RCPaywallViewController, even during
// recompositions. RCPaywallViewController itself is not yet instantiated here.
val viewControllerWrapper = remember { ViewControllerWrapper(null) }

UIKitViewController(
modifier = modifier,
modifier = modifier.layout { measurable, constraints ->
val placeable = measurable.measure(
if (constraints.minHeight == 0 && constraints.maxHeight > 0)
// We are being asked to wrap our own content height. We will use the measurement
// done by UIKit.
constraints.copy(minHeight = intrinsicContentSizePx)
else constraints
)

layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
},
factory = {
val viewController = if (footer) RCPaywallFooterViewController(
offering = options.offering?.toIosOffering() as? RCOffering,
Expand All @@ -41,12 +76,28 @@ internal fun UIKitPaywall(
)

viewController
.also {
// The first subview has an actual intrinsic content size. We keep a reference
// so we can use it in our measurement phase (above).
it.view.getIntrinsicContentSizeOfFirstSubView()
?.also { intrinsicContentSizePx = with(density) { it.dp.roundToPx() } }
}
.apply { setDelegate(delegate) }
.also { viewControllerWrapper.value = it }
},
)
}

private fun UIView.getIntrinsicContentSize(): Int {
var size: Int? = null
memScoped { size = intrinsicContentSize.ptr.pointed.height.toInt() }
return size!!
}

private fun UIView.getIntrinsicContentSizeOfFirstSubView(): Int? =
(subviews.firstOrNull() as? UIView)?.getIntrinsicContentSize()


/**
* Can be [remembered][remember] before the RCPaywallViewController is instantiated, so as to
* "reserve" a spot in the Compose slot table.
Expand Down

0 comments on commit 533f1d1

Please sign in to comment.