在 iOS SDK 中处理来自流程的数据

当用户在流程中的输入框中输入内容、回答问卷或切换开关时,SDK 会通过其分析回调将该值传递给你的应用。

应用通常会将这些数据用于:

  • 在自有后端注册用户:获取用户在引导流程中填写的邮箱和姓名,在流程结束时创建其账号。
  • 保存答案和偏好设置:记录用户的选择,以便应用后续使用——例如,将其作为自定义属性写入 Adapty 用户画像
  • 自定义后续流程:将问卷答案保存为自定义属性,再针对特定版位进行定向,使每个市场细分看到不同的流程,或其中不同的付费墙。
  • 接入第三方分析平台:将答案转发至 Amplitude、Mixpanel 或你正在使用的其他产品分析工具。

输入框和可选项组会自动上报其值。为了在代码中区分不同的输入框,请在编辑工具中为每个输入框设置有意义的 Element ID,并为每个可选项组设置 Group ID

开始之前

您需要:

  • Adapty SDK v4 或更高版本:流程回调在旧版本中不存在。
  • Flow & Paywall Builder 中构建的流程:只有流程才会通过此回调上报输入值。
  • 最近发布的流程版本:流程仅在您发布此功能上线后的新版本时才会上报输入值。如果您的应用没有收到任何内容,请重新发布一个新版本的流程后再试。

接收输入值

输入值与流程中的其他所有分析事件一样,通过同一个回调接收,事件名称为 flow_user_input。请在注册其他流程事件处理器的同时注册该回调。

闭包和委托方法接收相同的两个参数,因此读取值的代码在两种方式下完全一致。

didReceiveAnalyticEvent 回调会传递流程中的所有分析事件,包括页面浏览事件

  • name 参数包含事件名称。若要筛选用户输入事件,请将 nameflow_user_input 进行比较。
  • element_type 参数表示元素类别。
  • 输入值根据元素类型存储在不同的参数中:
    • 文本框、选择器和开关将用户输入存储在 value
    • 可选组在 item_idsitem_titles 中报告当前选中的选项
func handleFlowInput(name: String, params: [String: any Sendable]) {
    guard name == "flow_user_input",
          let elementId = params["element_id"] as? String,
          let elementType = params["element_type"] as? String
    else { return }

    // The screen the input sits on. Pair it with elementId to tell apart
    // two fields that share an Element ID on different screens.
    let screenId = params["instanceId"] as? String

    switch elementType {
    case "text_input", "email_input", "number_input", "phone_input":
        let text = params["value"] as? String
    case "date_picker", "time_picker", "date_time_picker":
        // Integer Unix time in milliseconds, not the seconds Date expects.
        let date = (params["value"] as? Int).map { Date(timeIntervalSince1970: Double($0) / 1000) }
    case "single_choice":
        let optionId = (params["item_ids"] as? [String])?.first
    case "multi_choice":
        let optionIds = params["item_ids"] as? [String]
    case "toggle":
        let isOn = params["value"] as? Bool
    default:
        break
    }
}

要确认回调是否触发,请在你自己的应用测试版本中与输入进行交互。如果你的回调处理程序未收到事件,请检查前提条件。请确保该流程是在此功能可用之后才发布的。

当您的应用接收到输入时

Important

默认输入值或选中值不会通过此回调传递到你的应用。如果用户接受了标记为 Set as default 的选项并继续操作,不会触发任何事件。不要把缺少事件理解为”未作答”——用户只是保留了默认值。

以下元素会触发此事件:

  • 文本、电子邮件、数字和电话输入框
  • 日期、时间和日期时间选择器
  • 单选和多选组以及开关

以下元素不会触发:

  • 密码输入框、产品选择器和标签切换
  • Header 元素内部的输入框(该元素在所有屏幕之间共享)
  • 存在重复或缺失选项 Element ID 的可选组,或者 Group ID 在另一个屏幕上被复用的可选组。此类选项组不会发送任何内容,而非发送部分答案。

以下情况会触发此事件:

  • 输入框失去焦点时。已清空的输入框报告空字符串;用户从未编辑过的输入框不报告任何内容。占位符不是值。如果用户返回输入框、编辑后再次离开,则会触发第二次事件。
  • 用户在选择新值后关闭选择器时。未做修改直接关闭不会报告任何内容。
  • 用户点击某个选项或开关时。多选事件会列出所有已选选项,因此取消选中最后一个选项时会发送两个空数组。

以下情况不会触发此事件:

  • 用户正在输入时。不存在按键流,只有输入框失去焦点时所持有的值。
  • 用户提交或关闭流程时。此时仍处于编辑状态的值可能会丢失;投递限制一节介绍了如何针对最后一屏进行相应的设计。
  • 在无用户交互的情况下设置值时。标记为 Set as default 的选项在屏幕打开时即为预选状态,Set Variable 操作可以根据其他交互来选中选项或填充输入框。两者均不会发送事件;预填充的输入框只有在用户编辑后才会报告。

您将接收到的内容

回调会传递两个事件。通过 name 进行过滤,只处理 flow_user_input 事件。响应的 JSON 数据格式如下:

{
  "name": "flow_user_input",
  "instanceId": "scr_registration",
  "isBackendEvent": false,
  "isCustomerEvent": true,
  "element_id": "email",
  "element_type": "email_input",
  "value": "jane@example.com"
}
参数说明
name输入事件为 flow_user_input,屏幕浏览事件为 flow_screen_showed
instanceId输入框所在屏幕的 ID。元素 ID 在同一屏幕内唯一,在整个流程中不保证唯一。如果流程中有多个屏幕包含输入框,过滤事件时需将 instanceIdelement_id 结合使用。
element_id输入框Element ID,或可选择分组Group ID
element_type发送事件的元素类型,决定以下哪个参数携带输入值。
value仅适用于文本字段、选择器和开关。 输入值:文本字段为字符串,选择器为整数,开关为布尔值。
item_ids仅适用于单选和多选分组。 已选项的 Element ID,顺序与编辑工具中选项的排列顺序一致。单选组有一个条目,多选组可有任意数量。
item_titles仅适用于单选和多选分组。 item_ids 中各选项对应的标题,顺序相同。该字段不会为空:没有标题的选项会上报其 ID。
isCustomerEvent工具标志位,此事件始终为 true。用于标记 SDK 传递给回调的事件。如果一个处理器将所有流程事件转发至埋点系统,可通过此标志而非 name 进行过滤。
isBackendEvent工具标志位,此事件始终为 false。用于标记 Adapty 同步记录至自身分析系统的事件。false 表示用户输入的内容仅到达你的应用,不会发送到其他地方——Adapty 不会接收或存储这些数据。

各元素上报内容说明:

编辑工具中的元素element_type存储值的参数内容说明
文本数字电话号码输入框text_inputnumber_inputphone_inputvalue用户输入的原始字符串。数字以字符串形式传递,不是数值类型。
邮箱输入框email_inputvalue用户输入的原始字符串,即使未通过编辑工具的格式校验也会上报。使用前请在你的一侧进行验证。
密码输入框不发送任何事件。
日期输入框date_pickervalue所选日期本地零点的 Unix 时间戳(毫秒),整数类型。
时间输入框time_pickervalue精确到分钟的 Unix 时间戳(毫秒),整数类型。
日期 & 时间输入框date_pickertime_pickervalue包含两个元素:日期选择器和时间选择器,各自发送独立事件。
Type 下拉菜单中切换为 Date & Time 的输入框date_time_pickervalue精确到分钟的 Unix 时间戳(毫秒),整数类型。
单选分组single_choiceitem_idsitem_titles两个数组。item_ids:包含已选项 Element ID 的数组。item_titles:包含该选项标题的数组。
多选分组multi_choiceitem_idsitem_titles两个数组。item_ids:所有已选项的 Element ID,顺序与编辑工具中的排列一致。item_titles:对应的标题,顺序相同。未选中任何选项时两个数组均为空。
开关分组togglevalue布尔值。

根据答案进行分支判断时,请比较 item_ids,而非 item_titles。标题是派生值:优先使用选项的 Element Title(如已设置),其次是默认语言环境中的文本,最后是 Element ID。使用其他语言查看流程的用户看到的文本可能不同。

事件示例

以下示例展示了每个事件的可用属性,注释中附有说明性的示例值。

文本、邮箱、数字和电话输入(点击展开)
func handleFlowInput(name: String, params: [String: any Sendable]) {
    name;                      // "flow_user_input"
    params["name"];            // "flow_user_input"
    params["instanceId"];      // "scr_J260KU5q"
    params["isCustomerEvent"]; // true
    params["isBackendEvent"];  // false
    params["element_id"];      // "email"
    params["element_type"];    // "email_input"
    params["value"];           // "jane@example.com"   (String)
}
日期、时间和日期时间选择器(点击展开)
func handleFlowInput(name: String, params: [String: any Sendable]) {
    params["element_id"];      // "birthday"
    params["element_type"];    // "date_picker"
    params["value"];           // 645408000000   (Unix milliseconds — 1990-06-15, local midnight)
}
单选(点击展开)
func handleFlowInput(name: String, params: [String: any Sendable]) {
    params["element_id"];      // "experience"
    params["element_type"];    // "single_choice"
    params["item_ids"];        // ["pro"]
    params["item_titles"];     // ["I train professionally"]
}
多选(点击展开)
func handleFlowInput(name: String, params: [String: any Sendable]) {
    params["element_id"];      // "interests"
    params["element_type"];    // "multi_choice"
    params["item_ids"];        // ["sports", "music"]
    params["item_titles"];     // ["Sports", "Music"]
}
Toggle (Click to expand)
func handleFlowInput(name: String, params: [String: any Sendable]) {
    params["element_id"];      // "reminders"
    params["element_type"];    // "toggle"
    params["value"];           // true   (Bool)
}

交付与限制

Warning

流程会发送原始值——电子邮件地址、电话号码以及用户输入的任何其他内容。请将分析回调传递的所有内容视为个人数据,不要将其写入任何你不会写入用户电子邮件地址的地方。

  • 后值覆盖前值:每个字段只触发一个事件,携带的是用户最终确认的值,而非逐键输入的流。如果用户编辑了某个字段,只有最后一个版本会传递给你。
  • 无提交保证:值会在用户浏览流程时传递给你,用户可以在任意时刻离开。在将一组答案视为完整之前,请等待流程关闭。
  • 尽力传递:如果用户在某个字段仍处于焦点状态或选择器仍处于打开状态时关闭流程或将应用切换到后台,该值可能会丢失。

为确保最后一个字段的值可靠传递,请以一个没有输入框和选择器的屏幕结束流程,并通过用户的明确操作(而非该屏幕出现时自动)关闭流程。跳转到最终屏幕会使前一个字段失去焦点,从而触发其值的发送。

每次处理程序接收到输入值时,请将其保存下来,并在流程关闭时一次性发送完整的数据集。要捕获这一时机,可以在 UIKit 中为 AdaptyFlowControllerDelegate 实现 flowControllerDidDisappear,或者在 SwiftUI 中向 .flow 修饰符传入 didDisappear 闭包。无论用户是完成流程还是主动关闭,这两种方式都会在流程视图离开屏幕后触发。

使用场景

在后端注册用户

待数据陆续到来后先收集,等流程关闭时再一并发送,这样一次请求就能携带完整的答案集合。

无论用户是正常完成流程还是中途退出,视图都会消失。在调用后端之前,请先检查所需字段是否已填写。

流程无法展示来自后端的错误信息。回调没有返回值,SDK 也没有向运行中的流程发送数据的方法。如果注册失败(例如邮箱已被使用),请在流程关闭后通过您自己的 UI 显示错误提示。

private var flowAnswers: [String: String] = [:]

func handleFlowInput(name: String, params: [String: any Sendable]) {
    guard name == "flow_user_input",
          let elementId = params["element_id"] as? String,
          let value = params["value"] as? String
    else { return }

    flowAnswers[elementId] = value
}

func flowControllerDidDisappear(_ controller: AdaptyFlowController) {
    guard flowAnswers["email"] != nil else { return }

    // Send flowAnswers to your backend here to create the account.

    flowAnswers.removeAll()
}

使用数据丰富用户画像

为了将用户输入的内容关联到其用户画像,并避免重复询问相同信息,请在数据输入时更新用户画像

例如,如果你的流程中有一个 Element ID 为 name 的文本输入框和一个 Element ID 为 email 的邮箱输入框:

func handleFlowInput(name: String, params: [String: any Sendable]) {
    guard name == "flow_user_input",
          let elementId = params["element_id"] as? String,
          let value = params["value"] as? String
    else { return }

    let builder = AdaptyProfileParameters.Builder()

    switch elementId {
    case "name":
        builder.with(firstName: value)
    case "email":
        builder.with(email: value)
    default:
        return
    }

    // Delegate methods are synchronous; kick off the async update in a Task.
    Task {
        do {
            try await Adapty.updateProfile(params: builder.build())
        } catch {
            // handle the error
        }
    }
}

自定义后续显示的流程

测验答案也可以决定用户在后续版位中看到什么——可以是不同的流程,或其中不同的付费墙。

例如,在用户引导流程中询问用户的运动经历,然后为每个群体展示各自的流程,配以不同的产品和文案。

  1. 在流程中添加测验。将可选组的 Group ID 设为 experience,并为每个选项设置有意义的 Element ID。
  2. 处理答案并为用户设置自定义属性
func handleFlowInput(name: String, params: [String: any Sendable]) {
    guard name == "flow_user_input",
          params["element_id"] as? String == "experience",
          let optionId = (params["item_ids"] as? [String])?.first
    else { return }

    let builder = AdaptyProfileParameters.Builder()
    // Set the custom attribute 'experience' to the option the user selected
    // (beginner, amateur, or pro).
    try? builder.with(customAttribute: optionId, forKey: "experience")

    Task {
        do {
            try await Adapty.updateProfile(params: builder.build())
        } catch {
            // handle the error
        }
    }
}
  1. 为每个自定义属性值创建市场细分
  2. 创建版位,并为每个市场细分添加目标受众
  3. 在您的应用中展示该版位的流程