Swift软件开发工具包快速入门

云眼About 11 min

Swift软件开发工具包快速入门

欢迎阅读 云眼灰度发布 的 Swift SDK 快速入门指南。

按照本指南中的步骤可以完成创建灰度发布标帜、推出标帜,以及运行 A/B 测试等操作。

第一部分:创建和配置灰度发布标帜

1. 创建“全栈灰度发布”项目

需要一个 云眼帐户才能完成本指南的操作。如果还没有帐户,可以在云眼平台(https://app.eyeofcloud.com/register)注册一个免费帐户。如果已有帐户,请打开或创建一个新的灰度发布项目。open in new window

对于新注册的帐号,在欢迎页面里点击“创建灰度发布项目”。

对于已经建立项目的帐号,点击切换项目->新建项目->创建灰度发布项目,即可创建新项目。

create_project_created.gif

2. 在项目中创建buy标帜

灰度发布项目创建完毕后,转到主页面菜单栏上“灰度发布”页面,点击创建标帜,将新标帜命名为“buy”。

3. 在buy标帜中添加变量

buy标帜建立后,点击此标帜,在标帜设置-缺省变量处依次添加变量,本案例为了简化操作和节省时间,变量名称和缺省值都采用云眼平台提供的默认值。

4. 在事件管理中增加事件buy

转到菜单栏上“事件管理”页面,点击新建事件按钮。将事件标识命名为buy,点击创建事件,创建完成。

第二部分:编码实现灰度发布和AB测试

1. 安装 云眼灰度发布 Swift SDK。

要求

  • Swift 客户端应用程序必须使用 Swift 5 或更高版本。
  • 支持的最低操作系统版本是 iOS/9.0 和 tvOS/9.0。

云眼 Swift SDK 可通过 CocoaPods 或 Swift Package Manager (SPM) 进行分发。可以将此 SDK 与用 Swift 和 Objective-C 编写的应用程序一起使用。

将此行添加到 Podfile: 苹果 Podfile

pod 'EyeofcloudSwiftSDK','~> 3.10.1'

运行命令: Shell pod install

有关更多安装信息,请参阅 CocoaPods 入门指南open in new window

完整的源代码可在giteeopen in new window上找到。

2. 调用SDK方法,创建eyeofcloudClient实例

在代码合适的位置(在 MyApplication.java 文件里)创建eyeofcloudClient实例,并把实例放在全局变量中,以便后续使用它。创建eyeofcloudClient实例需要传入sdkKey。本演示程序为了演示方便从页面输入sdkKey,但是实际应用时,需要直接写在代码里。sdkKey可以在云眼平台上拿到。

在代码能够获得用户id和属性信息的位置,通过调用eyeofcloudClient实例的方法createUserContext,得到user对象,user对象非常重要,它包含AB测试需要的2个主要方法decide和trackEvent。调用createUserContext需要传入两个参数:userId 和 attributes。 可以用设备加随机数作为userId,只要能够唯一识别用户即可。attributes是key、value对组成的数组,一般基于业务系统中的用户属性信息来构建。 user对象产生后,可以把它作为属性变量添加到app对象上,以便在其他地方可以使用。

ViewController.swift

import UIKit
import Eyeofcloud

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let eyeofcloudClient = EyeofcloudClient(sdkKey: "YOUR_SDK_KEY", defaultLogLevel: .error)
        // this Eyeofcloud initialization is asynchronously. for other methods see the Swift SDK reference
        eyeofcloudClient.start { result in
            switch result {
            case .success(let datafile):
                // --------------------------------
                // to get rapid demo results, generate random users. Each user always sees the same variation unless you reconfigure the flag rule.
                // --------------------------------
                var hasOnFlags = false
                
                for _ in 0...9 {
                    let userId = String(Int.random(in: 1000...9999))
                    // --------------------------------
                    // Create hardcoded user & bucket user into a flag variation
                    // --------------------------------
                    let user = eyeofcloudClient.createUserContext(userId: userId)
                    // "product_sort" corresponds to a flag key in your Eyeofcloud project
                    let decision = user.decide(key: "product_sort")
                    // did decision fail with a critical error?
                    if decision.variationKey == nil || decision.variationKey == "" {
                        print("decision error: \(decision.reasons)" )
                    }
                    // get a dynamic configuration variable
                    // "sort_method" corresponds to a variable key in your Eyeofcloud project
                    let sortMethod: String? = decision.variables.getValue(jsonPath: "sort_method")
                                        
                    // always returns false until you enable a flag rule in your Eyeofcloud project
                    if decision.enabled {
                        // Keep count how many visitors had the flag enabled
                        hasOnFlags = true
                    }
                    
                    // --------------------------------
                    // Mock what the users sees with print statements (in production, use flag variables to implement feature configuration)
                    // --------------------------------
                    print("\n\nFlag \(decision.enabled ? "on" : "off"). User number \(user.userId) saw flag variation: \(decision.variationKey ?? "") and got products sorted by: \(String(describing: sortMethod!)) config variable as part of flag rule: \(decision.ruleKey ?? "")")
                }
                
                if !hasOnFlags {
                    var projectId: Any?
                    if let config: Any = try? JSONSerialization.jsonObject(with: datafile, options: []), let convertedDict = config as? [String: Any] {
                        projectId = convertedDict["projectId"]
                    }
                    
                    print("\n\nFlag was off for everyone. Some reasons could include:\n1. Your sample size of visitors was too small. Rerun, or increase the iterations in the FOR loop\n2. Check your SDK key. Verify in Settings>Environments that you used the right key for the environment where your flag is toggled to ON.\ncheck your key at  https://app.eyeofcloud.com/v2/projects/\(String(describing: projectId!))/settings/implementation")
                }
            case .failure(_):
                print("Eyeofcloud client invalid. Verify in Settings>Environments that you used the primary environment's SDK key")
            }
        }
    }
}

3. 调用decide(),获取并使用变量。

在buy页面加载时,调用user的一个重要方法:decide,获得当前用户在灰度标帜buy的抽签分桶结果decision对象,decision里包含抽中的各个变量的值。我们可以用这些变量值控制页面显示,业务逻辑,AI参数,推荐算法等。

BuyViewModel.java

package com.eyeofcloud.demo.flag.ui.buy;

import Swift.os.Bundle;
import Swift.view.LayoutInflater;
import Swift.view.View;
import Swift.view.ViewGroup;
import Swift.widget.Button;
import Swift.widget.TextView;

import Swiftx.annotation.NonNull;
import Swiftx.appcompat.app.AlertDialog;
import Swiftx.fragment.app.Fragment;
import Swiftx.lifecycle.ViewModelProvider;
import Swiftx.navigation.NavController;
import Swiftx.navigation.Navigation;

import com.eyeofcloud.demo.flag.MyApplication;
import com.eyeofcloud.demo.flag.R;
import com.eyeofcloud.demo.flag.databinding.FragmentBuyBinding;

public class BuyFragment extends Fragment {

    private FragmentBuyBinding binding;

    public View onCreateView(@NonNull LayoutInflater inflater,
                             ViewGroup container, Bundle savedInstanceState) {

        MyApplication app = (MyApplication) getActivity().getApplication();

        BuyViewModel buyViewModel =
                new ViewModelProvider(this).get(BuyViewModel.class);

        binding = FragmentBuyBinding.inflate(inflater, container, false);
        View root = binding.getRoot();

        final TextView buttonTextView = binding.button;
        buyViewModel.getStringVariable().observe(getViewLifecycleOwner(), buttonTextView::setText);

        final TextView ruleKeyTextView = binding.ruleKey;
        buyViewModel.getRuleKey().observe(getViewLifecycleOwner(), ruleKeyTextView::setText);

        final TextView variationKeyTextView = binding.variationKey;
        buyViewModel.getVariationKey().observe(getViewLifecycleOwner(), variationKeyTextView::setText);

        final TextView stringTextView = binding.stringValue;
        buyViewModel.getStringVariable().observe(getViewLifecycleOwner(), stringTextView::setText);

        Button button = binding.button;
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // trackEvent
                app.getUser().trackEvent("buy");
            }
        });

        if (app.getSdkKey() != null && !app.getSdkKey().isEmpty()) {
            buyViewModel.init(app);
        } else {
            AlertDialog.Builder builder = new AlertDialog.Builder(getContext());

            String connection = MyApplication.getConnectivityStatusString(getContext());
            // Set the message show for the Alert time
            builder.setMessage("Connection status: " + connection +",SDK Key尚未设置,是否使用缺省SDK Key:" + MyApplication.DEFUAULT_SDK_KEY + "?");
            // Set Alert Title
            builder.setTitle("注意!");
            // Set Cancelable false for when the user clicks on the outside the Dialog Box then it will remain show
            builder.setCancelable(false);
            // Set the positive button with yes name Lambda OnClickListener method is use of DialogInterface interface.
            builder.setPositiveButton("是,用缺省", (dialog, which) -> {
                // When the user click yes button then app will close
                //finish();
                app.setSdkKey(MyApplication.DEFUAULT_SDK_KEY);
                buyViewModel.init(app);
                NavController navController = Navigation.findNavController(getActivity(), R.id.nav_host_fragment_activity_main);
                navController.navigate(R.id.navigation_buy);
            });
            // Set the Negative button with No name Lambda OnClickListener method is use of DialogInterface interface.
            builder.setNegativeButton("否,去设置", (dialog, which) -> {
                // If user click no then dialog box is canceled.
                dialog.cancel();
                NavController navController = Navigation.findNavController(getActivity(), R.id.nav_host_fragment_activity_main);
                navController.navigate(R.id.navigation_config);
            });
            // Create the Alert dialog
            AlertDialog alertDialog = builder.create();
            // Show the Alert Dialog box
            alertDialog.show();
        }
        return root;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;
    }

}

4. 调用trackEvent方法,将事件发出

BuyFragment.java

package com.eyeofcloud.demo.flag.ui.buy;

import Swift.os.Bundle;
import Swift.view.LayoutInflater;
import Swift.view.View;
import Swift.view.ViewGroup;
import Swift.widget.Button;
import Swift.widget.TextView;

import Swiftx.annotation.NonNull;
import Swiftx.appcompat.app.AlertDialog;
import Swiftx.fragment.app.Fragment;
import Swiftx.lifecycle.ViewModelProvider;
import Swiftx.navigation.NavController;
import Swiftx.navigation.Navigation;

import com.eyeofcloud.demo.flag.MyApplication;
import com.eyeofcloud.demo.flag.R;
import com.eyeofcloud.demo.flag.databinding.FragmentBuyBinding;

public class BuyFragment extends Fragment {

    private FragmentBuyBinding binding;

    public View onCreateView(@NonNull LayoutInflater inflater,
                             ViewGroup container, Bundle savedInstanceState) {

        MyApplication app = (MyApplication) getActivity().getApplication();

        BuyViewModel buyViewModel =
                new ViewModelProvider(this).get(BuyViewModel.class);

        binding = FragmentBuyBinding.inflate(inflater, container, false);
        View root = binding.getRoot();

        final TextView buttonTextView = binding.button;
        buyViewModel.getStringVariable().observe(getViewLifecycleOwner(), buttonTextView::setText);

        final TextView ruleKeyTextView = binding.ruleKey;
        buyViewModel.getRuleKey().observe(getViewLifecycleOwner(), ruleKeyTextView::setText);

        final TextView variationKeyTextView = binding.variationKey;
        buyViewModel.getVariationKey().observe(getViewLifecycleOwner(), variationKeyTextView::setText);

        final TextView stringTextView = binding.stringValue;
        buyViewModel.getStringVariable().observe(getViewLifecycleOwner(), stringTextView::setText);

        Button button = binding.button;
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // trackEvent
                app.getUser().trackEvent("buy");
            }
        });

        if (app.getSdkKey() != null && !app.getSdkKey().isEmpty()) {
            buyViewModel.init(app);
        } else {
            AlertDialog.Builder builder = new AlertDialog.Builder(getContext());

            String connection = MyApplication.getConnectivityStatusString(getContext());
            // Set the message show for the Alert time
            builder.setMessage("Connection status: " + connection +",SDK Key尚未设置,是否使用缺省SDK Key:" + MyApplication.DEFUAULT_SDK_KEY + "?");
            // Set Alert Title
            builder.setTitle("注意!");
            // Set Cancelable false for when the user clicks on the outside the Dialog Box then it will remain show
            builder.setCancelable(false);
            // Set the positive button with yes name Lambda OnClickListener method is use of DialogInterface interface.
            builder.setPositiveButton("是,用缺省", (dialog, which) -> {
                // When the user click yes button then app will close
                //finish();
                app.setSdkKey(MyApplication.DEFUAULT_SDK_KEY);
                buyViewModel.init(app);
                NavController navController = Navigation.findNavController(getActivity(), R.id.nav_host_fragment_activity_main);
                navController.navigate(R.id.navigation_buy);
            });
            // Set the Negative button with No name Lambda OnClickListener method is use of DialogInterface interface.
            builder.setNegativeButton("否,去设置", (dialog, which) -> {
                // If user click no then dialog box is canceled.
                dialog.cancel();
                NavController navController = Navigation.findNavController(getActivity(), R.id.nav_host_fragment_activity_main);
                navController.navigate(R.id.navigation_config);
            });
            // Create the Alert dialog
            AlertDialog alertDialog = builder.create();
            // Show the Alert Dialog box
            alertDialog.show();
        }
        return root;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;
    }

}

5. 运行一下小程序

至此,编码工作完成,但是灰度发布标志还没有启动。我们先看一下在不启动灰度标帜,用缺省值来运行小程序时界面的样子。此时,按钮显示的是缺省的“购买”两个字。

第三部分:运行灰度发布实验

小程序产品或运营人员,在云眼灰度发布平台上操作来改变按钮的文字,并跟踪和评估各个变体的转化效果。

1. 修改buy标帜变量形成不同变体

点击buy标帜,将缺省变量中string variable的默认值设置为“购买”。添加两个变体:变体1和变体2,变体1中设置string variable值为“立即购买”,变体2中设置string variable值为“我想要”。

创建变体
create_variation.gif

2. 为灰度标帜buy配置AB测试规则

配置完buy标帜的变量和变体后,开始配置buy标帜的AB测试规则,在development环境下点击“增加规则”->“AB测试”, 实验受众选择缺省的所有访客,百分比为100%,指标设置为事件buy的唯一转化,变体1和变体2的流量权重设置为50%对50% 点击保存。

创建变体
config_ab_experiment.gif

3. 启动并运行灰度发布。

buy标帜规则设置完成后,我们打开buy标帜。

然后模拟用户访问buy页面。可以看到,此次访问结果为变体1,购买按钮的文字信息为“立即购买”,说明将用户分桶到了变体1。我们可以卸载并重新访问小程序,来模拟另一个新用户,这次购买按钮的文字信息是“我想要”,说明用户分桶到了变体2。实际运营时,可以做很多种实验,比如定向到不同受众,配置各种类型的指标,以及人工智能流量调整等,我们会有专门视频来讲解。

|||

第四部分、分析结果,推出获胜变体

分析结果,推出获胜变体。小程序灰度发布标帜推出一段时间后,我们分析实验结果,根据实验结果推出获胜变体。

1. 查看实验报告,分析结果。

点击buy标帜上AB测试中的“查看结果”按钮。

根据变体1和变体2主指标数值和统计显著性来判定获胜者,统计显著性一般要大于95%。本案例中可以看出变体1胜出,指标提升55.69%,统计显著性达到98%。

2. 根据实验结果推出获胜变体。

确定胜出变体后,我们可以将将少部分流量(比如5%)继续用于AB测试,其余流量(95%)推出胜出的变体1。

本教程只是指导您完成最简单的灰度发布,在灰度发布之前进行 A/B 测试。

下表显示灰度发布和 A/B 测试之间的差异:

灰度发布A/B 测试规则
可以将标帜推广到一定比例的一般用户群(或特定受众),或者在遇到错误时回滚。在投资交付之前,通过 A/B 测试标帜进行实验,这样您就知道要构建什么。跟踪用户在标帜变体中的行为,然后使用 云眼平台 统计引擎解释实验结果。

回顾和结论

为了实施本案例的AB测试,我们总共做了4部分的工作,可以简单概括为:实验的设计、实现、实施和行动,与PDCA过程类似。

每部分工作都需要处理AB测试相关的信息,并由相关人员来负责完成,我们把参与AB测试的人员分为业务人员和技术人员两大类。

部分工作内容概括处理信息参与者
创建灰度发布标帜设计变量、事件业务人员:产品、运营、数据; 技术人员:开发、QA
灰度发布编码实现实现变量、事件技术人员:开发、QA
配置并运行灰度发布实施实验、变体、指标、流量业务人员:产品、运营、数据
分析结果,推出获胜变体行动实验、变体、指标、流量、结果、推出业务人员:产品、运营、数据

从表格中,我们可以看到,只有第一部分工作是由业务人员和技术人员共同完成的,而他们共同处理的信息只有变量和事件,在后面的第三和第四部分中更多的信息,技术人员不需要知道。这样的好处是,可以尽最大可能减少业务人员和技术人员之间的相互依赖。业务人员在实施AB测试过程中,不需要依赖技术人员;技术人员在实现AB测试时也不依赖业务人员。大家可以相对独立、高效的进行工作。这是云眼灰度发布/AB测试方案的一大优势。 另外,云眼AB测试结束后可以无缝的实现对特定人群的定向发布,做到AB测试和灰度发布的有机结合,这是云眼产品的另一大特点。此外,云眼产品在AB测试/灰度发布各个环节里还有很多独特的功能,我们将在后续的视频中详细介绍。

祝贺!您已成功设置并启动了第一个灰度发布实验。虽然此示例侧重于优化销售过程,但 云眼 实验平台可以支持任何场景的实验用例。

培训视频可以从这里观看open in new window

本案例的 Swift App demo代码可以从这里获取open in new window

Last update:
Contributors: “zhangweixue”,zhangweixue