設定値配信 新テンプレート (β)を利用する

設定値配信 新テンプレート (β)は、設定値配信でのバナーやカルーセル等のユースケースをテンプレート化し、専用のエディタを用意したものです。

🚧

設定値配信 新テンプレート (β)の利用ついて

設定値配信 新テンプレート (β)の利用には、KARTE for Appのご契約およびプラグイン解放手続きが必要です。

詳細はサポートサイトをご参照ください。

1. 設定値配信 新テンプレート (β)で共通の実装

📘

設定値配信をすでに使っている場合は、「1-1. 導入手順」「1-2. 変数参照の実装」の対応は不要です。

1-1. 導入手順

iOS

設定値配信 新テンプレート (β)は、KarteVariablesモジュールを導入することで利用可能です。

Variablesのリファレンスも合わせてご確認ください。

  1. Podfile の編集 プロジェクトディレクトリにある Podfile を任意のエディタで開き、KarteVariables の Pod を追加します。
pod 'KarteVariables'
  1. Pod のインストール プロジェクトディレクトリで下記コマンドを実行し、Pod をインストールします。
pod install

Android

設定値配信 新テンプレート (β)は、variablesモジュールを導入することで利用可能です。

Variablesのリファレンスも合わせてご確認ください。

アプリの build.gradle (app) を任意のエディタで開き、dependencies ブロックに variables モジュールを追加します。

dependencies {
  implementation 'io.karte.android:variables:2.+'
}

1-2. 変数参照の実装

管理画面上で設定した変数は、SDKを初期化しただけでは取得されません。

変数を利用する前に、事前にKARTEから取得する必要があります。

1-2-1. SDKのインポート宣言を追加

import KarteVariables
import io.karte.android.variables.Variables
import io.karte.android.variables.Variable

1-2-2. 変数の取得

変数を取得するには Variables クラスの fetch() メソッドを呼び出します。

fetch()は要素の表示や、セグメント更新の直前の任意のタイミングで実行することが一般的です。

実行のタイミングについての注意事項等は、設定値配信のベストプラクティスも合わせてご確認ください。

※前セッションで配信済みのキャッシュをリセットしたい場合は、アプリケーション起動時(SDKの初期化直後)にも呼び出すことを推奨しております。参考: キャッシュの削除(iOS, Android

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  KarteApp.setup(appKey: "アプリケーションキー")
  Variables.fetch { isSuccessful in
      if isSuccessful {
          // 成功時の処理
      }
  }
  ...
}
override fun onCreate() {
  super.onCreate()
  KarteApp.setup(this, "アプリケーションキー")
  Variables.fetch { isSuccessful ->
    if (isSuccessful) {
        // 成功時の処理
    }
  }
}

fetch() メソッドの呼び出し時に、SDKは変数取得( _fetch_variables )イベントを送信します。

KARTE管理画面側の設定値配信の接客サービスのデフォルト設定では、トリガーに_fetch_variablesイベントが設定されているため、このイベントをトリガーに配信が行われます。

任意のイベント発火時に接客サービスを配信したい場合は、任意のイベント発火時に変数を取得する(iOS, Android)に従った実装が推奨されます。

この時、配信された接客サービスに含まれる変数は UserDefaults(iOS)やSharedPreference(Android)にキャッシュされます。

加えて、変数を取得した時点で既にキャッシュされた変数がある場合は、キャッシュされた全変数を削除した上で新たに取得した変数をキャッシュします。

また、これと同時にSDKは表示準備( _message_ready )イベントを送信します。

fetch()完了のハンドラについてはFetchCompletion(iOS, Android)が利用可能です。

詳細は設定値配信でのFetchCompletionの利用をご覧ください。

📘

fetch失敗の場合

ネットワークエラー等でfetchが失敗した場合、以前fetch成功した時のデータが存在する場合は、そちらが保持されます。

📘

変数名の重複

配信された複数の接客サービスの間で、同名の変数が定義されている場合は、接客サービスの 最終更新日時 が最近のものがキャッシュされます。

📘

キャッシュのリセット

キャッシュには特に有効期限はありませんが、変数の新規取得時以外に、アプリデータ削除(アンインストール等)および renewVisitorId() によるユーザ紐付け解除時に、同様に全ての変数値の削除が行われます。

  • 参考: アプリケーションのログアウトに対応する(iOS, Android

2. 設定値配信 新テンプレート (β)の利用方法

現在提供している以下テンプレートの利用方法を記載しています。

  • シンプルバナー
  • カルーセルバナー

2-1. データモデルを定義する

管理画面で設定した接客のバナーをアプリ側で使用するために、以下のようなデータモデルを定義します。

struct RemoteConfigTemplate: Codable {
    let content: Content
    
    struct Content: Codable {
        let data: [Data]
        let config: Config
    }
    
    struct Data: Codable {
        let imageUrl: URL
        let linkUrl: String?
        let index: Int
    }
    
    struct Config: Codable {
        let templateType: TemplateType
    }
    
    enum TemplateType: String, Codable {
        case simpleBanner
        case carouselWithoutMargin
    }
    
    // カルーセルバナー表示用
    struct ParsedImageData: Hashable {
        let index: Int
        let imageUrl: URL
        let image: UIImage
        let linkUrl: URL?
    }
}
data class RemoteConfigTemplate(
    val content: Content
) {
    data class Content(
        val data: List<Data>,
        val config: Config
    )

    data class Data(
        val imageUrl: String,
        val linkUrl: String?,
        val index: Int
    )

    data class Config(
        val templateType: TemplateType
    )

    enum class TemplateType {
        SIMPLE_BANNER,
        CAROUSEL_WITHOUT_MARGIN;

        companion object {
            fun fromString(value: String): TemplateType {
                return when (value) {
                    "simpleBanner" -> SIMPLE_BANNER
                    "carouselWithoutMargin" -> CAROUSEL_WITHOUT_MARGIN
                    else -> throw Exception("Unknown template type: $value")
                }
            }
        }
    }

    companion object {
        fun fromJsonString(jsonString: String): RemoteConfigTemplate? {
            return try {
                val root = JSONObject(jsonString)
                val content = root.getJSONObject("content")
                val dataArray = content.getJSONArray("data")
                val config = content.getJSONObject("config")

                val dataList = mutableListOf<Data>()
                for (i in 0 until dataArray.length()) {
                    val dataObj = dataArray.getJSONObject(i)
                    dataList.add(
                        Data(
                            imageUrl = dataObj.getString("imageUrl"),
                            linkUrl = dataObj.optString("linkUrl").takeIf { it.isNotEmpty() },
                            index = dataObj.getInt("index")
                        )
                    )
                }

                val templateType = TemplateType.fromString(config.getString("templateType"))

                RemoteConfigTemplate(
                    Content(
                        data = dataList,
                        config = Config(templateType)
                    )
                )
            } catch (e: Exception) {
                null
            }
        }
    }
}

2-2. 管理画面で実装用IDを保存する

設定値配信 新テンプレート (β)の接客を管理画面で作成し、実装用IDを入力して保存します。

詳細はサポートサイトをご参照ください。

2-3. 配信された値を使用する

変数オブジェクトの取得(iOS, Android)において、設定値配信 新テンプレート (β)用のプレフィックスKRT_CONFIG_TEMPLATE$)と実装用IDを合わせた文字列をキーに指定することで、接客で設定した値をJSON文字列で取得できます。

例として、管理画面の接客で設定した実装用IDがtop_bannerの場合は、キーはKRT_CONFIG_TEMPLATE$top_bannerとなります。

以下のように値を取り出してデコードすることで、それぞれの接客タイプ(シンプルバナー、カルーセルバナー)に適したデータモデルを使用できます。

シンプルバナー

let prefix = "KRT_CONFIG_TEMPLATE$"
let implementationId = "{{実装用ID}}"
let configKey = "\(prefix)\(implementationId)"

guard let data = Variables.variable(forKey: configKey).string?.data(using: .utf8) else {
    // キー(configKey)に対応するデータが見つからない場合の処理
    return
}

guard let decoded = try? JSONDecoder().decode(RemoteConfigTemplate.self, from: data) else {
    // キー(configKey)に対応するデータが見つからない場合の処理
    return
}
val prefix = "KRT_CONFIG_TEMPLATE$"
val implementationId = "{{実装用ID}}"
val configKey = "${prefix}${implementationId}"

val variable = Variables.get(configKey)
val variableString = variable.string("")
if (variableString.isEmpty()) {
    // キー(configKey)に対応するデータが見つからない場合の処理
    return
}

val template = RemoteConfigTemplate.fromJsonString(variableString)
if (template == null) {
    // キー(configKey)に対応するデータが見つからない場合の処理
    return
}

View構築時の実装例

guard let data = decoded.content.data.first else {
    return
}

// data.imageUrl, data.linkUrlを使って任意のViewを構築する
let (imageData, _) = try await URLSession.shared.data(from: data.imageUrl)
let iv = UIImageView(frame: .zero)
iv.image = UIImage(data: imageData)
iv.clipsToBounds = true
iv.contentMode = .scaleAspectFit
iv.translatesAutoresizingMaskIntoConstraints = false
val data = template.content.data.firstOrNull()

// data.imageUrl, data.linkUrlを使って任意のViewを構築する
@Composable
private fun SimpleBannerCompose(configKey: String, data: RemoteConfigTemplate.Data) {
    val context = LocalContext.current
    var hasTrackedOpen by remember { mutableStateOf(false) }
    
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp),
        elevation = 4.dp
    ) {
        SubcomposeAsyncImage(
            model = data.imageUrl,
            contentDescription = "Simple Banner",
            modifier = Modifier
                .fillMaxWidth()
                .clickable {
                    data.linkUrl?.let { linkUrl -> 
                        try {
                            val intent = Intent(Intent.ACTION_VIEW, Uri.parse(linkUrl))
                            context.startActivity(intent)
                        } catch (e: Exception) {
                            Log.e("RemoteConfigTemplate", "Failed to open link: $linkUrl", e)
                        }
                    }
                },
            contentScale = ContentScale.Fit,
            loading = {
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(200.dp),
                    contentAlignment = Alignment.Center
                ) {
                    CircularProgressIndicator(
                        color = MaterialTheme.colors.primary,
                        modifier = Modifier.size(32.dp)
                    )
                }
            },
            error = {
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(200.dp),
                    contentAlignment = Alignment.Center
                ) {
                    Text(
                        text = "Failed to load image",
                        color = MaterialTheme.colors.error
                    )
                }
            }
        )
    }
}

カルーセルバナー

let prefix = "KRT_CONFIG_TEMPLATE$"
let implementationId = "{{実装用ID}}"
let configKey = "\(prefix)\(implementationId)"

guard let data = Variables.variable(forKey: configKey).string?.data(using: .utf8) else {
    // キー(configKey)に対応するデータが見つからない場合の処理
    return
}

guard let decoded = try? JSONDecoder().decode(RemoteConfigTemplate.self, from: data) else {
    // キー(configKey)に対応するデータが見つからない場合の処理
    return
}
val prefix = "KRT_CONFIG_TEMPLATE$"
val implementationId = "{{実装用ID}}"
val configKey = "${prefix}${implementationId}"

val variable = Variables.get(configKey)
val variableString = variable.string("")
if (variableString.isEmpty()) {
    // キー(configKey)に対応するデータが見つからない場合の処理
    return
}

val template = RemoteConfigTemplate.fromJsonString(variableString)
if (template == null) {
    // キー(configKey)に対応するデータが見つからない場合の処理
    return
}

View構築時の実装例

let processedImageData = try await ImageLoader().fetchImagesWithLinkUrl(of: decoded)

for val in processedImageData {
    // 任意のViewを構築する
}

// 画像URLの並列処理とソート処理
struct ImageLoader {
    func fetchImagesWithLinkUrl(of model: RemoteConfigTemplate) async throws -> [RemoteConfigTemplate.ParsedImageData] {
        // 並列リクエストで取得した画像を並び替えるため、インデックス付きのMapを定義する
        // [index: (imageUrl, image, linkUrl)]
        var temp = [Int: ParsedImageData]()
        try await withThrowingTaskGroup(of: (Int, URL, Data, String?).self) { group in
            for content in model.content.data {
                group.addTask {
                    let data = try await self.loadData(from: content.imageUrl)
                    return (content.index, content.imageUrl, data, content.linkUrl)
                }
            }
            for try await (index, imageUrl, data, linkUrl) in group {
                if let img = UIImage(data: data) {
                    temp[index] = .init(index: index, imageUrl: imageUrl, image: img, linkUrl: URL(string: linkUrl ?? ""))
                }
            }
        }
        // インデックス付きMapを、インデックス(番号)順でソートされた配列に変換する
        let images: [ParsedImageData] = temp.sorted(by: { $0.key < $1.key })
            .reduce(into: [ParsedImageData]()) { acc, elem in
                acc.append(elem.value)
            }
        return images
    }

    private func loadData(from url: URL) async throws -> Data {
        let (data, _) = try await URLSession.shared.data(from: url)
        return data
    }
}
val dataList = template.content.data

@Composable
private fun CarouselWithoutMarginCompose(configKey: String, dataList: List<RemoteConfigTemplate.Data>) {
    val context = LocalContext.current
    if (dataList.isEmpty()) return
    val pagerState = rememberPagerState(pageCount = { dataList.size })

    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
            .height(200.dp),
        elevation = 4.dp
    ) {

        HorizontalPager(
            state = pagerState,
            modifier = Modifier.fillMaxSize()
        ) { page ->
            val data = dataList[page]

            SubcomposeAsyncImage(
                model = data.imageUrl,
                contentDescription = "Carousel item ${page + 1}",
                modifier = Modifier
                    .fillMaxSize()

                contentScale = ContentScale.Crop,
                loading = {
                    Box(
                        modifier = Modifier.fillMaxSize(),
                        contentAlignment = Alignment.Center
                    ) {
                        CircularProgressIndicator(
                            color = MaterialTheme.colors.primary,
                            modifier = Modifier.size(32.dp)
                        )
                    }
                },
                error = {
                    Box(
                        modifier = Modifier.fillMaxSize(),
                        contentAlignment = Alignment.Center
                    ) {
                        Text(
                            text = "Failed to load image",
                            color = MaterialTheme.colors.error
                        )
                    }
                }
            )
        }
    }
}

2-4. インプレッションを計測する

バナーが表示されたタイミングで、message_openイベントを送信することでインプレッションを計測することが可能です。

iOS

message_open は、TrackerクラスのtrackOpen(variables:)メソッドで送信することが可能です。

let prefix = "KRT_CONFIG_TEMPLATE$"
let implementationId = "{{実装用ID}}"
let configKey = "\(prefix)\(implementationId)"
let variable = Variables.variable(forKey: configKey)

// バナーが表示されたタイミングで、下記処理を実行
Tracker.trackOpen(variables: [variable])

Android

message_open は、VariablesクラスのtrackOpen()メソッドで送信することが可能です。

val prefix = "KRT_CONFIG_TEMPLATE$"
val implementationId = "{{実装用ID}}"
val configKey = "${prefix}${implementationId}"
val variable = Variables.get(configKey)

// バナーが表示されたタイミングで、下記処理を実行
Variables.trackOpen(listOf(variable))

2-5. タップを計測する

バナーがタップされたタイミングで、message_clickイベントを送信することでタップを計測することが可能です。

iOS

message_clickは、TrackerクラスのtrackClick(variables:)メソッドで送信することが可能です。

データモデルから引数を設定して、シンプルバナーもしくはカルーセルバナーのタップイベントを送信する実装例

class TapGestureRecognizer: UITapGestureRecognizer {
    let index: Int
    let linkUrl: URL

    init(target: AnyObject, action: Selector, index: Int, linkUrl: URL) {
        self.index = index
        self.linkUrl = linkUrl
        super.init(target: target, action: action)
    }
}

@objc
private func imageTapped(_ sender: TapGestureRecognizer) {
    let variable = Variables.variable(forKey: configKey)
		if variable.isDefined {
		    // senderからlinkUrlとindexを取得
		    let values: [String: JSONConvertible] = [
		        "url": JSONConvertibleConverter.convert(sender.linkUrl.absoluteString),
		        "banner_template": [
		            "position_no": JSONConvertibleConverter.convert(sender.index)
		        ]
		    ]
		    Tracker.trackClick(variable: variable, values: values)
		}

    if UIApplication.shared.canOpenURL(sender.linkUrl) {
        UIApplication.shared.open(sender.linkUrl)
    }
}

Android

message_clickは、VariablesクラスのtrackClick()メソッドで送信することが可能です。

データモデルから引数を設定して、シンプルバナーもしくはカルーセルバナーのタップイベントを送信する実装例

data.linkUrl?.let { linkUrl ->
    // URLを開く前にクリック(イベント)を計測する
    val variable = Variables.get(configKey)
    if (variable.isDefined) {
        val values = mapOf(
            "url" to linkUrl,
            "banner_template" to mapOf(
                "position_no" to page
            )
        )
        Variables.trackClick(listOf(variable), values)
        try {
            val intent = Intent(Intent.ACTION_VIEW, Uri.parse(linkUrl))
            context.startActivity(intent)
        } catch (e: Exception) {
            Log.e("BannerTemplate", "Failed to open link: $linkUrl", e)
        }
    }
}

Appendix

設定値配信 新テンプレート (β)と設定値配信の比較

設定値配信 新テンプレート (β)と設定値配信には、共通点と相違点がそれぞれあります。

相違点においては、管理画面側の設定方法やアプリ側の実装方法が、既存の設定値配信とは異なるため、ご注意ください。

共通点

相違点

設定値配信 新テンプレート (β)設定値配信
接客の設定方法詳細はサポートサイトをご確認ください詳細はサポートサイトをご確認ください
配信された値を取得するキー設定値配信 新テンプレート (β)用のプレフィックスと接客で設定した実装用ID接客で設定した変数名
配信された値の取得時の形式接客で設定した値をテンプレート固有のJSONデータ形式に変換した文字列接客で設定した値の文字列

設定値配信 新テンプレート (β)のデータ型

設定値配信 新テンプレート (β)には、シンプルバナーとカルーセルバナーのテンプレートがあります。

Variablesモジュールを使って値を取得し、パースすることで、テンプレートそれぞれの実装に適したデータ型を簡単に利用できます。

設定値配信 新テンプレート (β)の接客を管理画面で作成すると、変数オブジェクトを取得した際の戻り値(iOS, Android)として、以下のような構造のJSONデータを文字列で返します。

シンプルバナー

{
  "content": {
    "data": [
      {
        "imageUrl": "https://example.com/image.png",
        "linkUrl": "https://example.com",
        "index": 0
      }
    ],
    "config": {
      "templateType": "simpleBanner"
    }
  },
  "version": "v1"
}

カルーセルバナー

{
  "content": {
    "data": [
      {
        "imageUrl": "https://example.com/image1.png",
        "linkUrl": "https://example.com/page1",
        "index": 0
      },
      {
        "imageUrl": "https://example.com/image2.png",
        "linkUrl": "https://example.com/page2",
        "index": 1
      }
    ],
    "config": {
      "templateType": "carouselWithoutMargin"
    }
  },
  "version": "v1"
}