Skip to main content

Exemplos reativos (Combine e async/await)

Exemplos de como usar o GoAB SDK de forma reativa no iOS com Combine e async/await.

Configuração com Combine

Setup básico

import Combine
import SwiftUI

class ExperimentViewModel: ObservableObject {
let sdk = GoABSDKFactory.create(
accountId: 12345,
apiToken: "your-api-token",
timeoutSeconds: 30
)

@Published var experimentValues: [String: Any] = [:]
@Published var isLoading = false

func initialize() {
isLoading = true
Task {
try? await sdk.initialize()
await MainActor.run {
loadExperiments()
isLoading = false
}
}
}

private func loadExperiments() {
experimentValues = [
"button_text": sdk.getValue("button_text", defaultValue: "Clique Aqui"),
"show_banner": sdk.getValue("show_banner", defaultValue: true),
"button_color": sdk.getValue("button_color", defaultValue: "#FF0000")
]
}
}

struct ContentView: View {
@StateObject private var viewModel = ExperimentViewModel()

var body: some View {
VStack {
if viewModel.isLoading {
ProgressView()
} else {
Text(viewModel.experimentValues["button_text"] as? String ?? "Clique Aqui")
if viewModel.experimentValues["show_banner"] as? Bool == true {
Text("Banner").padding()
}
}
}
.onAppear { viewModel.initialize() }
}
}

ViewModel com @Published

ExperimentViewModel completo

import Combine

class ExperimentViewModel: ObservableObject {
let sdk = GoABSDKFactory.create(
accountId: 12345,
apiToken: "your-api-token",
timeoutSeconds: 30
)

@Published var experiments: [String: Any] = [:]
@Published var isLoading = false
@Published var errorMessage: String?

func initialize() {
isLoading = true
errorMessage = nil
Task {
do {
try await sdk.initialize()
await MainActor.run { loadExperiments() }
} catch {
await MainActor.run {
errorMessage = "Erro ao inicializar: \(error.localizedDescription)"
}
}
await MainActor.run { isLoading = false }
}
}

private func loadExperiments() {
experiments = [
"button_text": sdk.getValue("button_text", defaultValue: "Clique Aqui"),
"show_banner": sdk.getValue("show_banner", defaultValue: true),
"button_color": sdk.getValue("button_color", defaultValue: "#FF0000"),
"max_retries": sdk.getValue("max_retries", defaultValue: 3),
"api_endpoint": sdk.getValue("api_endpoint", defaultValue: "https://api.prod.com")
]
}

func refreshExperiments() {
isLoading = true
Task {
sdk.refreshExperiments()
await MainActor.run { loadExperiments() }
await MainActor.run { isLoading = false }
}
}

func clearError() {
errorMessage = nil
}
}

Uso na View

struct MainView: View {
@StateObject private var viewModel = ExperimentViewModel()

var body: some View {
VStack {
if viewModel.isLoading {
ProgressView()
}

Text(viewModel.experiments["button_text"] as? String ?? "Clique Aqui")

if viewModel.experiments["show_banner"] as? Bool == true {
Text("Banner").padding()
}

if let error = viewModel.errorMessage {
Text(error).foregroundColor(.red)
Button("OK") { viewModel.clearError() }
}

Button("Atualizar") { viewModel.refreshExperiments() }
}
.onAppear { viewModel.initialize() }
}
}

Combine com transformações

ExperimentPublisherManager

import Combine

class ExperimentPublisherManager {
private let sdk: GoABSDK

init(sdk: GoABSDK) {
self.sdk = sdk
}

func buttonTextPublisher() -> AnyPublisher<String, Never> {
Just(sdk.getValue("button_text", defaultValue: "Clique Aqui") as? String ?? "Clique Aqui")
.eraseToAnyPublisher()
}

func showBannerPublisher() -> AnyPublisher<Bool, Never> {
Just(sdk.getValue("show_banner", defaultValue: true) as? Bool ?? true)
.eraseToAnyPublisher()
}

func buttonColorPublisher() -> AnyPublisher<String, Never> {
Just(sdk.getValue("button_color", defaultValue: "#FF0000") as? String ?? "#FF0000")
.eraseToAnyPublisher()
}

func allExperimentsPublisher() -> AnyPublisher<[String: Any], Never> {
Publishers.CombineLatest3(
buttonTextPublisher(),
showBannerPublisher(),
buttonColorPublisher()
)
.map { buttonText, showBanner, buttonColor in
[
"button_text": buttonText,
"show_banner": showBanner,
"button_color": buttonColor
]
}
.eraseToAnyPublisher()
}

func buttonTextWithPrefix() -> AnyPublisher<String, Never> {
buttonTextPublisher()
.map { "🔘 \($0)" }
.eraseToAnyPublisher()
}
}

Uso com transformações

class MainViewController: UIViewController {
private let sdk = GoABSDKFactory.create(...)
private var cancellables = Set<AnyCancellable>()

override func viewDidLoad() {
super.viewDidLoad()
let manager = ExperimentPublisherManager(sdk: sdk)

Task {
try? await sdk.initialize()
await MainActor.run { observeExperiments(manager: manager) }
}
}

private func observeExperiments(manager: ExperimentPublisherManager) {
manager.buttonTextWithPrefix()
.receive(on: DispatchQueue.main)
.sink { [weak self] text in
self?.button.setTitle(text, for: .normal)
}
.store(in: &cancellables)

manager.buttonColorPublisher()
.receive(on: DispatchQueue.main)
.sink { [weak self] hex in
self?.button.backgroundColor = UIColor(hex: hex)
}
.store(in: &cancellables)

manager.allExperimentsPublisher()
.receive(on: DispatchQueue.main)
.sink { experiments in
print("Experimentos: \(experiments)")
}
.store(in: &cancellables)
}
}

Cache e refresh com CurrentValueSubject

CachedExperimentManager

import Combine

class CachedExperimentManager: ObservableObject {
private let sdk: GoABSDK

@Published var experiments: [String: Any] = [:]
@Published var isRefreshing = false
@Published var lastRefresh: Date?

init(sdk: GoABSDK) {
self.sdk = sdk
}

func loadExperiments() {
experiments = [
"button_text": sdk.getValue("button_text", defaultValue: "Clique Aqui"),
"show_banner": sdk.getValue("show_banner", defaultValue: true),
"button_color": sdk.getValue("button_color", defaultValue: "#FF0000"),
"max_retries": sdk.getValue("max_retries", defaultValue: 3),
"api_endpoint": sdk.getValue("api_endpoint", defaultValue: "https://api.prod.com")
]
lastRefresh = Date()
}

func refreshExperiments() {
isRefreshing = true
sdk.refreshExperiments()
loadExperiments()
isRefreshing = false
}

func getExperimentValue(key: String, defaultValue: Any) -> Any {
experiments[key] ?? defaultValue
}
}

Uso com cache

struct CachedExperimentsView: View {
private let sdk: GoABSDK
@StateObject private var experimentManager: CachedExperimentManager

init() {
let sdk = GoABSDKFactory.create(
accountId: 12345,
apiToken: "your-api-token",
timeoutSeconds: 30
)
self.sdk = sdk
_experimentManager = StateObject(wrappedValue: CachedExperimentManager(sdk: sdk))
}

var body: some View {
VStack {
Text(experimentManager.experiments["button_text"] as? String ?? "Clique Aqui")

if experimentManager.isRefreshing {
ProgressView()
}

if let last = experimentManager.lastRefresh {
Text("Última atualização: \(last, style: .relative) atrás")
}

Button("Atualizar") { experimentManager.refreshExperiments() }
}
.task {
try? await sdk.initialize()
experimentManager.loadExperiments()
}
}
}

Tratamento de erros com Result

SafeExperimentManager

class SafeExperimentManager {
private let sdk: GoABSDK

init(sdk: GoABSDK) {
self.sdk = sdk
}

func getExperiment(key: String, defaultValue: Any) -> Result<Any, Error> {
do {
let value = sdk.getValue(key, defaultValue: defaultValue)
return .success(value)
} catch {
return .failure(error)
}
}

func getAllExperiments() -> Result<[String: Any], Error> {
do {
let experiments: [String: Any] = [
"button_text": sdk.getValue("button_text", defaultValue: "Clique Aqui"),
"show_banner": sdk.getValue("show_banner", defaultValue: true),
"button_color": sdk.getValue("button_color", defaultValue: "#FF0000")
]
return .success(experiments)
} catch {
return .failure(error)
}
}

func getExperimentWithRetry(key: String, defaultValue: Any, maxRetries: Int = 3) async -> Result<Any, Error> {
var retries = 0
while retries < maxRetries {
let result = getExperiment(key: key, defaultValue: defaultValue)
if case .success = result { return result }
retries += 1
try? await Task.sleep(nanoseconds: UInt64(retries) * 1_000_000_000)
}
return getExperiment(key: key, defaultValue: defaultValue)
}
}

Uso com tratamento de erro

struct SafeExperimentsView: View {
private let sdk: GoABSDK
private let safeManager: SafeExperimentManager

init() {
let sdk = GoABSDKFactory.create(
accountId: 12345,
apiToken: "your-api-token",
timeoutSeconds: 30
)
self.sdk = sdk
safeManager = SafeExperimentManager(sdk: sdk)
}

var body: some View {
VStack {
Text(buttonText)
if showBanner { Text("Banner") }
}
.task { await loadSafe() }
}

@State private var buttonText = "Clique Aqui"
@State private var showBanner = true

private func loadSafe() async {
try? await sdk.initialize()

switch safeManager.getExperiment(key: "button_text", defaultValue: "Clique Aqui") {
case .success(let value):
await MainActor.run { buttonText = value as? String ?? "Clique Aqui" }
case .failure(let error):
print("Erro: \(error)")
await MainActor.run { buttonText = "Clique Aqui" }
}

switch safeManager.getExperiment(key: "show_banner", defaultValue: true) {
case .success(let value):
await MainActor.run { showBanner = value as? Bool ?? true }
case .failure:
await MainActor.run { showBanner = true }
}
}
}

Async/await direto (sem Combine)

Para fluxos simples, async/await pode ser suficiente:

struct SimpleExperimentsView: View {
@State private var buttonText = "Clique Aqui"
@State private var showBanner = true

private let sdk = GoABSDKFactory.create(
accountId: 12345,
apiToken: "your-api-token",
timeoutSeconds: 30
)

var body: some View {
VStack {
Text(buttonText)
if showBanner { Text("Banner") }
}
.task { await loadExperiments() }
}

private func loadExperiments() async {
try? await sdk.initialize()
await MainActor.run {
buttonText = sdk.getValue("button_text", defaultValue: "Clique Aqui") as? String ?? "Clique Aqui"
showBanner = sdk.getValue("show_banner", defaultValue: true) as? Bool ?? true
}
}
}

Próximos passos