Pass data to flows in iOS SDK
A custom tag is a custom variable with a value from your app. After you select Value comes from outside the app in a variable’s settings, your app can set its value through the Adapty SDK. For example, a text element contains “Coins:” and a custom tag. If your app passes “120” for the tag, users see “Coins: 120”.
Before you start
You need the Adapty SDK v4 or later. Versions before v4 don’t display flows.
Add custom tags to the flow
To show your app’s values in a flow, set up custom tags in the builder:
-
Create a custom tag for each value that your app passes.
-
Note each tag’s full name, including its group. Your app’s code passes each value under this name. For example, if the
usergroup contains astreaktag, the tag’s full name isuser.streak.
Pass custom tag values
Pass your app’s values to the tagResolver parameter of getFlowConfiguration, as a [String: String] dictionary:
- Each key is the full name of a custom tag, such as
user.streak. - Each value is the string that users see in place of the tag.
- Each value must be human-readable, such as “1,250”, not “1250.0”. Adapty doesn’t format or translate it.
let customTags = [
"username": "DragonSlayer42",
"coins": "1,250",
]
do {
let flowConfiguration = try await AdaptyUI.getFlowConfiguration(
forFlow: flow,
tagResolver: customTags
)
// present the flow
} catch {
// handle the error
}
The SwiftUI .flow modifier and AdaptyFlowView and the UIKit flow controller have no parameter for custom tag values. They display the values from the flow configuration you give them.
To test your custom tags, open the flow in your app, after you add the code that sets their values. The builder canvas and the preview on devices only show Initial values: the Adapty mobile app doesn’t set custom tag values.
If your app already sets custom tag values for old Paywall Builder paywalls, review how custom tags in flows differ.
What users see without a value
Users see a custom tag’s Initial value as a fallback in the following cases:
- Your app passes no value for the tag.
- The user hasn’t updated your app to a version that passes the value.
- The name in your app’s code doesn’t match the tag’s full name, including letter case.
- Another custom tag in the same text element has no value. Then every custom tag in that text element shows its Initial value.
To show nothing in place of a tag, pass an empty string. Adapty treats an empty string as a value, so the fallback doesn’t appear.
Example: show a BMI from onboarding answers
A flow can collect answers from users, and a custom tag can show a value from your app. Combine the two to show users a result that your app calculates from their answers, such as a score, a recommended plan, or a health metric:
- A flow collects the answers and sends them to your app.
- Your app calculates the result.
- Your app passes the result to the next flow as a custom tag value.
In this example, an onboarding flow collects the user’s weight and height. When the user closes the onboarding flow after its last screen, your app calculates their body mass index (BMI). Then it opens a paywall flow that shows the BMI.
Set up the flows
-
Create two flows: an onboarding flow that collects the user’s weight and height, and a paywall flow that shows their BMI.
-
Add two Number inputs to the onboarding flow:
- One for the weight in kilograms, with the Element ID
weight. - One for the height in centimeters, with the Element ID
height.
For both inputs, set the Format to Integer. An input sends its value as text, exactly as the user types it, and the sample code doesn’t read “70.5” and “70,5” the same way. Whole numbers have no decimal separator, so the code reads them the same in every locale.
- One for the weight in kilograms, with the Element ID
-
To make sure your app receives both values, end the onboarding flow with a screen that has no inputs. On that screen, add a button with the Close flow action. Inputs send their values when they lose focus or when the user moves to another screen. The Close flow action doesn’t send them. The sample code only opens the paywall if the user reached the last screen.
-
In the paywall flow, create a custom tag named
bmi. In the Initial value field, enter a dash (—). Users see the dash if your app passes no BMI. -
Add a text element with the words “Your BMI:” to the paywall flow. After these words, insert the
bmicustom tag. -
Add the onboarding flow to a placement, such as
onboarding. Add the paywall flow to a placement with the IDbmi_paywall. The code in Pass the BMI to the paywall fetches the paywall by this ID.
Pass the BMI to the paywall
Collect the weight and the height from the onboarding flow’s flow_user_input events, as in Process data from flows. To know that the user reached the last screen, watch for a flow_screen_showed event with is_last_screen set to true, as in Track flow screen views. When the user closes the onboarding flow after that screen, calculate the BMI. Then fetch the paywall’s flow configuration, and pass the BMI for the bmi custom tag.
The following code is for UIKit. It implements the AdaptyFlowControllerDelegate methods on one view controller, which is the delegate of both the onboarding flow’s controller and the paywall’s controller. The view controller must also implement the delegate’s purchase and restore methods, which this example leaves out. To see them, go to Present flows and paywalls in UIKit.
private var bodyAnswers: [String: String] = [:]
private var reachedLastScreen = false
func flowController(
_ controller: AdaptyFlowController,
didReceiveAnalyticEvent name: String,
params: [String: any Sendable]
) {
if name == "flow_screen_showed", params["is_last_screen"] as? Bool == true {
reachedLastScreen = true
return
}
guard name == "flow_user_input",
let elementId = params["element_id"] as? String,
let value = params["value"] as? String
else { return }
bodyAnswers[elementId] = value
}
func flowControllerDidDisappear(_ controller: AdaptyFlowController) {
// This delegate receives events from both flows. Reset `bodyAnswers` and
// `reachedLastScreen` whenever a flow disappears, so that the paywall
// doesn't open again when the user closes it.
let answers = bodyAnswers
let finished = reachedLastScreen
bodyAnswers.removeAll()
reachedLastScreen = false
// Number inputs send their values as strings.
guard finished,
let weight = answers["weight"].flatMap({ Double($0) }),
let height = answers["height"].flatMap({ Double($0) }),
height > 0
else { return }
let heightInMeters = height / 100
let bmi = weight / (heightInMeters * heightInMeters)
Task {
await showBMIPaywall(bmi: bmi.formatted(.number.precision(.fractionLength(1))))
}
}
func showBMIPaywall(bmi: String) async {
do {
let flow = try await Adapty.getFlow(placementId: "bmi_paywall")
let flowConfiguration = try await AdaptyUI.getFlowConfiguration(
forFlow: flow,
tagResolver: ["bmi": bmi]
)
let flowController = try AdaptyUI.flowController(
with: flowConfiguration,
delegate: self
)
present(flowController, animated: true)
} catch {
// handle the error
}
}
formatted uses the decimal separator of the user’s locale: “22.7” in English, “22,7” in German.
In SwiftUI, use the closures of the onboarding flow’s .flow modifier instead of the delegate methods:
didReceiveAnalyticEvent: Collect the weight and the height, and watch for theflow_screen_showedevent of the last screen.didDisappear: If the user reached the last screen, calculate the BMI. Then fetch the paywall’s flow configuration, and pass the BMI for thebmicustom tag. Present that configuration with a second.flowmodifier.
Save the BMI to the user’s profile
To show users different flows based on their BMI, save it as a custom attribute of their profile. In the Adapty Dashboard, create a Number custom attribute with the key bmi. Then show each flow to an audience built from the attribute.
Call the updateProfile method in flowControllerDidDisappear, right after the line that calculates bmi. Pass that Double, not the formatted string, so that a segment can compare the BMI with a threshold:
do {
let params = try AdaptyProfileParameters.Builder()
.with(customAttribute: bmi, forKey: "bmi")
.build()
try await Adapty.updateProfile(params: params)
} catch {
// handle the error
}