UIKit Integration
A complete example showing SpendOwl integration in a UIKit app.Project Setup
Add SpendOwl Package
In Xcode, go to File → Add Package Dependencies and add:
Copy
https://github.com/spendowl/spendowl-ios
Complete Example
AppDelegate.swift
Configure SpendOwl when the app launches:Copy
import UIKit
import SpendOwl
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Enable logging in debug builds
#if DEBUG
SpendOwl.enableLogging = true
#endif
// Configure SpendOwl
SpendOwl.configure(apiKey: "your-api-key")
// Setup window
window = UIWindow(frame: UIScreen.main.bounds)
let navigationController = UINavigationController(
rootViewController: AttributionViewController()
)
window?.rootViewController = navigationController
window?.makeKeyAndVisible()
return true
}
}
AttributionViewController.swift
Display attribution data and manage user identity:Copy
import UIKit
import SpendOwl
class AttributionViewController: UITableViewController {
// MARK: - Properties
private var attribution: AttributionResult?
private var error: SpendOwlError?
private var isLoading = false
private var userId: String = ""
private enum Section: Int, CaseIterable {
case attribution
case userIdentity
case status
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
// MARK: - Setup
private func setupUI() {
title = "SpendOwl Demo"
navigationController?.navigationBar.prefersLargeTitles = true
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
tableView.register(TextFieldCell.self, forCellReuseIdentifier: "TextFieldCell")
}
// MARK: - Actions
private func fetchAttribution() {
isLoading = true
error = nil
tableView.reloadSections(IndexSet(integer: Section.attribution.rawValue), with: .automatic)
Task { @MainActor in
do {
self.attribution = try await SpendOwl.attribution()
} catch let err as SpendOwlError {
self.error = err
} catch {
self.error = .unknown(error)
}
self.isLoading = false
self.tableView.reloadSections(
IndexSet(integer: Section.attribution.rawValue),
with: .automatic
)
}
}
private func setUserId() {
guard !userId.isEmpty else { return }
SpendOwl.setUserId(userId)
showAlert(title: "User ID Set", message: "User ID: \(userId)")
}
private func clearUserId() {
SpendOwl.clearUserId()
userId = ""
tableView.reloadSections(IndexSet(integer: Section.userIdentity.rawValue), with: .automatic)
showAlert(title: "User ID Cleared", message: nil)
}
private func showAlert(title: String, message: String?) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
// MARK: - UITableViewDataSource
override func numberOfSections(in tableView: UITableView) -> Int {
Section.allCases.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch Section(rawValue: section)! {
case .attribution:
if isLoading { return 1 }
if attribution != nil { return 6 } // 5 fields + button
if error != nil { return 2 }
return 1
case .userIdentity:
return 3
case .status:
return 2
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch Section(rawValue: section)! {
case .attribution: return "Attribution"
case .userIdentity: return "User Identity"
case .status: return "Status"
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch Section(rawValue: indexPath.section)! {
case .attribution:
return attributionCell(for: indexPath)
case .userIdentity:
return userIdentityCell(for: indexPath)
case .status:
return statusCell(for: indexPath)
}
}
private func attributionCell(for indexPath: IndexPath) -> UITableViewCell {
if isLoading {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
var config = cell.defaultContentConfiguration()
config.text = "Fetching..."
cell.contentConfiguration = config
let spinner = UIActivityIndicatorView(style: .medium)
spinner.startAnimating()
cell.accessoryView = spinner
return cell
}
if let attribution {
let fields: [(String, String)] = [
("Status", attribution.status.displayName),
("Campaign", attribution.campaignName ?? "-"),
("Ad Group", attribution.adGroupName ?? "-"),
("Keyword", attribution.keyword ?? "-"),
("Country", attribution.countryOrRegion ?? "-")
]
if indexPath.row < fields.count {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
var config = cell.defaultContentConfiguration()
config.text = fields[indexPath.row].0
config.secondaryText = fields[indexPath.row].1
cell.contentConfiguration = config
cell.selectionStyle = .none
return cell
}
}
if let error, indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
var config = cell.defaultContentConfiguration()
config.text = "Error"
config.secondaryText = error.localizedDescription
config.secondaryTextProperties.color = .systemRed
cell.contentConfiguration = config
cell.selectionStyle = .none
return cell
}
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
var config = cell.defaultContentConfiguration()
config.text = "Fetch Attribution"
config.textProperties.color = .systemBlue
cell.contentConfiguration = config
return cell
}
private func userIdentityCell(for indexPath: IndexPath) -> UITableViewCell {
switch indexPath.row {
case 0:
let cell = tableView.dequeueReusableCell(
withIdentifier: "TextFieldCell",
for: indexPath
) as! TextFieldCell
cell.configure(placeholder: "User ID", text: userId) { [weak self] text in
self?.userId = text
}
return cell
case 1:
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
var config = cell.defaultContentConfiguration()
config.text = "Set User ID"
config.textProperties.color = .systemBlue
cell.contentConfiguration = config
return cell
case 2:
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
var config = cell.defaultContentConfiguration()
config.text = "Clear User ID"
config.textProperties.color = .systemRed
cell.contentConfiguration = config
return cell
default:
return UITableViewCell()
}
}
private func statusCell(for indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
var config = cell.defaultContentConfiguration()
switch indexPath.row {
case 0:
config.text = "Configured"
config.secondaryText = SpendOwl.isConfigured ? "Yes" : "No"
config.image = UIImage(systemName: SpendOwl.isConfigured ? "checkmark.circle.fill" : "xmark.circle.fill")
config.imageProperties.tintColor = SpendOwl.isConfigured ? .systemGreen : .systemRed
case 1:
config.text = "SDK Version"
config.secondaryText = SpendOwl.sdkVersion
default:
break
}
cell.contentConfiguration = config
cell.selectionStyle = .none
return cell
}
// MARK: - UITableViewDelegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
switch Section(rawValue: indexPath.section)! {
case .attribution:
let buttonRow = (attribution != nil) ? 5 : (error != nil) ? 1 : 0
if indexPath.row == buttonRow {
fetchAttribution()
}
case .userIdentity:
if indexPath.row == 1 {
setUserId()
} else if indexPath.row == 2 {
clearUserId()
}
case .status:
break
}
}
}
// MARK: - Helper Extension
extension AttributionStatus {
var displayName: String {
switch self {
case .attributed: return "Attributed"
case .organic: return "Organic"
case .unknown: return "Unknown"
}
}
}
// MARK: - TextFieldCell
class TextFieldCell: UITableViewCell {
private let textField = UITextField()
private var onTextChange: ((String) -> Void)?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupTextField()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupTextField() {
textField.translatesAutoresizingMaskIntoConstraints = false
textField.autocapitalizationType = .none
textField.autocorrectionType = .no
textField.addTarget(self, action: #selector(textChanged), for: .editingChanged)
contentView.addSubview(textField)
NSLayoutConstraint.activate([
textField.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
textField.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
textField.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12),
textField.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12)
])
selectionStyle = .none
}
func configure(placeholder: String, text: String, onTextChange: @escaping (String) -> Void) {
textField.placeholder = placeholder
textField.text = text
self.onTextChange = onTextChange
}
@objc private func textChanged() {
onTextChange?(textField.text ?? "")
}
}
Key Patterns
MainActor for UI Updates
Always update UI on the main thread:Copy
Task { @MainActor in
self.attribution = try await SpendOwl.attribution()
self.tableView.reloadData()
}
SceneDelegate Setup
For apps using SceneDelegate:Copy
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
// SpendOwl is already configured in AppDelegate
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
window?.rootViewController = UINavigationController(
rootViewController: AttributionViewController()
)
window?.makeKeyAndVisible()
}
}
Authentication Integration
Set user ID in your login flow:Copy
class LoginViewController: UIViewController {
func login(email: String, password: String) {
Task { @MainActor in
do {
let user = try await AuthService.login(email: email, password: password)
// Set SpendOwl user ID
SpendOwl.setUserId(user.id)
// Navigate to main app
let homeVC = HomeViewController()
navigationController?.setViewControllers([homeVC], animated: true)
} catch {
showError(error)
}
}
}
}
Download Example
The complete example project is available on GitHub:SpendOwlDemo-UIKit
Download the complete UIKit example