This package allows QA teams and developers to easily test campaigns, force variations, and verify flag values in real-time within iOS applications. Using ABTasty QA Assistant
- Overview
- Features
- Prerequisites
- Installation
- Quick Start
- Detailed Integration
- Key Features
- Best Practices
- Troubleshooting
- Additional Resources
- Example Application
The ABTasty QA Assistant provides an intuitive in-app interface for testing A/B campaigns, feature flags, and variations during development and QA phases. It integrates with the Flagship iOS SDK to provide real-time flag updates and comprehensive campaign management, presented as a draggable overlay button and a bottom-sheet panel.
Do not ship this package to production! The QA Assistant is intended for development and QA testing only.
To prevent accidental inclusion in production, wrap initialization in a #if DEBUG compile-time check. Since this is a compiler directive, the QA Assistant code is stripped out entirely from Release builds:
private var qaAssistant: ABTastyQAAssistant?
private func toggleQAAssistant() {
#if DEBUG
if qaAssistant == nil {
qaAssistant = ABTastyQAAssistant("YOUR_ENVIRONMENT_ID", "YOUR_API_KEY")
qaAssistant?.showOverlayButton(in: self)
}
#endif
}- π― Campaign Management - View, test, and force variations for A/B tests and feature flags
- π Live Flag Updates - Real-time flag value changes when forcing variations
- π Allocation Viewer - Check traffic distribution across variations
- π¨ Draggable Overlay Button - Floating, drag-to-reposition button for easy access during testing
- π Targeting Inspection - Verify targeting rules and conditions
- π Event Tracking - Monitor all hits (events, screens, transactions) sent to Flagship
- π§ͺ Variation Testing - Force specific variations to test different experiences
- π§ Context Viewer - Inspect the current visitor context
Before integrating the ABTasty QA Assistant, ensure you have:
- Xcode with a project targeting iOS 15.0 or higher
- An ABTasty account with:
- Environment ID
- API Key
- The Flagship iOS SDK
~> 5.0.0installed in your project
ABTastyQAssistant is distributed via CocoaPods and Swift Package Manager.
Add both the Flagship SDK and QA Assistant to your Podfile:
target 'YourApp' do
use_frameworks!
# Flagship SDK
pod 'FlagShip', '~> 5.0.0'
# ABTasty QA Assistant
pod 'ABTastyQAssistant'
endThen run, from your project's root directory:
pod installAlways open the generated .xcworkspace file (not the .xcodeproj) afterwards.
Add the package to your Package.swift, or via Xcode's File > Add Package Dependenciesβ¦:
dependencies: [
.package(url: "https://github.com/flagship-io/abtasty-qa-assistant-ios.git", .upToNextMajor(from: "0.7.0"))
]Then add "ABTastyQAssistant" to your target's dependencies.
Note: the Flagship SDK's module is named
FlagShipwhen installed via CocoaPods andFlagshipwhen installed via Swift Package Manager β use the import that matches your installation method (see the Quick Start example below).
Get up and running in 5 minutes with this complete example:
Replace these placeholders with your actual values:
YOUR_ENVIRONMENT_ID- Your Flagship environment IDYOUR_API_KEY- Your Flagship API key
import UIKit
import FlagShip // "import Flagship" if you installed via Swift Package Manager
import ABTastyQAssistant
class ViewController: UIViewController {
private var qaAssistant: ABTastyQAAssistant?
private let flagValueLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
initFlagship()
}
private func initFlagship() {
// 1. Start Flagship
Flagship.sharedInstance.start(
envId: "YOUR_ENVIRONMENT_ID",
apiKey: "YOUR_API_KEY"
)
// 2. Create a visitor
_ = Flagship.sharedInstance
.newVisitor(visitorId: "user_123", hasConsented: true)
.withContext(context: ["isQA": true])
.build()
// 3. Setup live updates (register before fetching flags)
Flagship.sharedInstance.sharedVisitor?.onFlagUpdate = { [weak self] changedKeys in
DispatchQueue.main.async {
print("π Flags updated: \(changedKeys)")
self?.updateFlagValue()
}
}
// 4. Fetch flags
Flagship.sharedInstance.sharedVisitor?.fetchFlags { [weak self] in
DispatchQueue.main.async {
self?.updateFlagValue()
}
}
}
private func updateFlagValue() {
let value = Flagship.sharedInstance.sharedVisitor?
.getFlag(key: "btnTitle")
.value(defaultValue: "Default Button") ?? "No value"
flagValueLabel.text = value
}
@objc private func toggleQA() {
if qaAssistant?.isOverlayVisible == true {
qaAssistant?.hideOverlayButton()
qaAssistant?.dispose()
qaAssistant = nil
} else {
qaAssistant = ABTastyQAAssistant(
"YOUR_ENVIRONMENT_ID",
"YOUR_API_KEY",
onClose: {
print("QA Assistant closed")
}
)
qaAssistant?.showOverlayButton(in: self)
}
updateQAButton()
}
private func updateQAButton() {
let imageName = qaAssistant?.isOverlayVisible == true ? "eye.slash" : "ladybug"
navigationItem.leftBarButtonItem?.image = UIImage(systemName: imageName)
}
private func setupUI() {
title = "ABTasty QA Demo"
view.backgroundColor = .systemBackground
navigationItem.leftBarButtonItem = UIBarButtonItem(
image: UIImage(systemName: "ladybug"),
style: .plain,
target: self,
action: #selector(toggleQA)
)
flagValueLabel.textAlignment = .center
flagValueLabel.font = .systemFont(ofSize: 24, weight: .bold)
flagValueLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(flagValueLabel)
NSLayoutConstraint.activate([
flagValueLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
flagValueLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
deinit {
Flagship.sharedInstance.sharedVisitor?.onFlagUpdate = nil
qaAssistant?.dispose()
}
}Build and run your app on a simulator or device.
- Tap the bug icon in the navigation bar (top-left)
- The QA overlay button appears, floating above your content β drag it anywhere on screen
- Tap the overlay button to open the QA Assistant bottom sheet
- Browse campaigns and force different variations
- Watch the flag value update live when you select variations
First, initialize the Flagship SDK in your app:
private func initFlagship() {
// Start Flagship SDK
Flagship.sharedInstance.start(
envId: "YOUR_ENVIRONMENT_ID",
apiKey: "YOUR_API_KEY"
)
// Create a visitor with context
_ = Flagship.sharedInstance
.newVisitor(visitorId: "user_123", hasConsented: true)
.withContext(context: [
"isQA": true,
"country": "FR",
"isVip": false
// Add your custom context
])
.build()
print("β
Flagship SDK initialized")
// Fetch flags from the server
Flagship.sharedInstance.sharedVisitor?.fetchFlags {
print("β
Flags fetched")
}
}Initialize the QA Assistant with your credentials:
private func initializeQAAssistant() {
qaAssistant = ABTastyQAAssistant(
"YOUR_ENVIRONMENT_ID",
"YOUR_API_KEY",
onClose: { [weak self] in
// Called when the QA Assistant panel is closed
print("π QA Assistant closed, refreshing flags...")
self?.refreshFlags()
}
)
print("β
QA Assistant initialized")
}Show the floating overlay button on top of a UIViewController:
private func showQAAssistant() {
initializeQAAssistant()
qaAssistant?.showOverlayButton(in: self)
}
private func hideQAAssistant() {
qaAssistant?.hideOverlayButton()
qaAssistant?.dispose()
qaAssistant = nil
}
private func toggleQAAssistant() {
if qaAssistant?.isOverlayVisible == true {
hideQAAssistant()
} else {
showQAAssistant()
}
}Add a button in your navigation bar:
navigationItem.leftBarButtonItem = UIBarButtonItem(
image: UIImage(systemName: "ladybug"),
style: .plain,
target: self,
action: #selector(toggleQAAssistant)
)Enable live flag updates to automatically refresh your UI when flags are modified through the QA Assistant:
private func setupFlagUpdateListener() {
Flagship.sharedInstance.sharedVisitor?.onFlagUpdate = { [weak self] changedKeys in
DispatchQueue.main.async {
guard let self else { return }
print("π Live update received for flags: \(changedKeys)")
// Show a notification
self.showSyncBanner("π Live update: \(changedKeys.joined(separator: ", "))")
// Update your UI with new flag values
self.updateFlagValues()
}
}
print("β
Flag update listener registered")
}
private func updateFlagValues() {
guard let visitor = Flagship.sharedInstance.sharedVisitor else { return }
let buttonTitle = visitor.getFlag(key: "btnTitle").value(defaultValue: "Default") ?? "Default"
let buttonColor = visitor.getFlag(key: "btnColor").value(defaultValue: "blue") ?? "blue"
print("Updated flag values:")
print(" btnTitle: \(buttonTitle)")
print(" btnColor: \(buttonColor)")
}Important: Register the listener before fetching flags, so the very first fetch (and any subsequent QA-triggered update) is caught:
private func initFlagship() {
Flagship.sharedInstance.start(envId: "ENV_ID", apiKey: "API_KEY")
_ = Flagship.sharedInstance
.newVisitor(visitorId: "user_123", hasConsented: true)
.withContext(context: ["isQA": true])
.build()
// β
Setup listener before fetching flags
setupFlagUpdateListener()
Flagship.sharedInstance.sharedVisitor?.fetchFlags { }
}The QA Assistant provides a comprehensive interface to:
- View all campaigns - See active, inactive, and forced campaigns (A/B tests, toggles, personalizations)
- View variations - Explore different variations for each campaign with all associated flags
- Force variations - Override the default allocation and test specific variations
- Reset to original - Return to the production variation with a single tap
- Hide / show campaigns - Temporarily disable a campaign for the current session
- View allocations - Check traffic distribution across variations
- Check targeting - Verify targeting rules and conditions
See Using ABTasty QA Assistant for more information.
Real-time flag value changes when you force variations through the QA Assistant:
// Setup listener for automatic UI updates
Flagship.sharedInstance.sharedVisitor?.onFlagUpdate = { changedKeys in
print("π Flags updated: \(changedKeys)")
// Refresh your UI with new flag values
}- Campaigns Tab - Browse every campaign, its status, and the currently allocated variation
- Variations Tab - See all flags and their values for each variation of a campaign
- Allocation Tab - View traffic distribution percentages across variation groups
- Targeting Tab - Check targeting rules and audience criteria
- Events Tab - Monitor hits (events, screens, transactions) sent to Flagship in real-time
- Context Tab - See all context key-value pairs for the current visitor
Only enable the QA Assistant in debug/staging builds. Wrapping the code in #if DEBUG removes it entirely from Release/App Store builds:
private func initQAAssistant() {
#if DEBUG
qaAssistant = ABTastyQAAssistant("YOUR_ENVIRONMENT_ID", "YOUR_API_KEY")
#endif
}Clean up resources when the presenting view controller is deallocated:
deinit {
Flagship.sharedInstance.sharedVisitor?.onFlagUpdate = nil // Remove listener
qaAssistant?.hideOverlayButton()
qaAssistant?.dispose()
}Solution: Make sure you call showOverlayButton(in:) with a UIViewController whose view is already attached to a window (e.g. from viewDidAppear, not before the view hierarchy is set up):
qaAssistant?.showOverlayButton(in: self)Solution: Ensure the flag update listener is registered before the first fetchFlags call:
// Setup listener BEFORE fetching flags
Flagship.sharedInstance.sharedVisitor?.onFlagUpdate = { changedKeys in
updateFlagValues()
}
Flagship.sharedInstance.sharedVisitor?.fetchFlags { }This is expected when you force a campaign. The allocation is bypassed because you've manually selected a variation instead of letting the normal traffic allocation decide. This is working as intended.
Make sure to wrap QA Assistant code in #if DEBUG checks:
#if DEBUG
// QA Assistant code
#endifConsider using a separate CocoaPods target, or a build configuration that excludes the ABTastyQAssistant pod from your Release build, to fully exclude the package from production builds.
A complete demo application is included in the Example/ directory. To run it:
cd Example
pod install
open ABTastyQAssistant.xcworkspaceThen build and run the ABTastyQAssistant-Example scheme from Xcode.
The example demonstrates:
- Complete Flagship SDK integration
- QA Assistant setup and teardown
- Live flag updates
- Multiple flag types (string, number, VIP-targeted flags)
- Context updates and hit tracking (screens, events, transactions)
Adel, adel@abtasty.com
ABTastyQAssistant is available under the MIT license. See the LICENSE file for more info.