Enable on-site search signals in an iOS app
The on-site search signal captures the queries users enter in your app's search screen and the number of results returned. It helps identify popular search terms, discover content gaps and segment users by the terms they searched for - for example, a search for "mortgage" or "business insurance" can trigger a targeted email or push notification with an offer.
Availability: Premium and Ultimate
Languages: Swift and Objective-C
Implementation considerations
When to fire the signal
In iOS apps, UISearchResultsUpdating calls updateSearchResults(for:) on every keystroke. Choose a firing strategy that captures a deliberate search rather than every character:
- Debounced text change - fire after the user stops typing for a set interval (for example, 500 ms). Recommended when results update as the user types and there is no explicit submit step. Use
DispatchWorkItemwithDispatchQueue.main.asyncAfterto cancel and reschedule on each keystroke. - Search bar submit - fire when the user taps the Search key on the keyboard via
UISearchBarDelegate.searchBarSearchButtonClicked(_:). Best for search screens with an explicit submit step. - On view disappearance - fire in
viewWillDisappear(_:)to capture the final query state when the user navigates away. Useful when neither of the above is practical.
Effect values
Use the effect field to indicate whether the search returned results:
"positive"- the search returned one or more results"negative"- the search returned zero results
Contact mapping
Include the audience field to attribute this signal to a contact in Connect. See How behavior signals update Connect data for identifier formats and contact resolution details.
Configuration
Before adding behavior signals, integrate the Connect SDK into your app. See the guide for your development language: Swift or Objective-C.
Method
func logSignal(_ data: [String: Any]?) -> BoolSends the signal to the Acoustic Connect endpoint. Initialize the Connect SDK before calling this method using ConnectSDK.shared.enable(with:).
Pass all fields as a flat [String: Any] dictionary. The SDK handles the signal structure automatically.
- (BOOL)logSignal:(NSDictionary *)values;Sends the signal to the Acoustic Connect endpoint. Initialize the Connect SDK before calling this method using [[ConnectApplicationHelper sharedInstance] enableFramework:withPostMessageUrl:].
Pass all fields as an NSDictionary. The SDK handles the signal structure automatically.
Signal fields
Required
| Field | Type | Description |
|---|---|---|
numberOfResults | Number | Number of results returned for the search term |
searchTerm | String | The word or phrase the user searched for |
signalType | String | Signal type. Value: "onSiteSearch". |
Optional
| Field | Type | Description |
|---|---|---|
audience | [String: Any] | Key-value pairs for contact mapping. Keys must match contact attribute names exactly as they appear in Connect, including capitalization and spacing. |
category | String | Signal category. Value: "Behavior". |
description | String | A description of the signal |
effect | String | Describes the effect of the signal on engagement. Use "positive" when results are found, "negative" when the search returns zero results. |
name | String | A label to differentiate this signal from others (for example, "product search"). Max 256 characters |
Things to know
numberOfResultsaccepts a native SwiftInt.If a required field is missing or invalid, the entire signal is discarded.
Basic example
import Connect
ConnectCustomEvent.sharedInstance().logSignal([
"signalType": "onSiteSearch",
"searchTerm": "wireless headphones",
"numberOfResults": 24,
"effect": "positive"
])#import <Connect/Connect.h>
[[ConnectCustomEvent sharedInstance] logSignal:@{
@"signalType": @"onSiteSearch",
@"searchTerm": @"wireless headphones",
@"numberOfResults": @24,
@"effect": @"positive"
}];Complete example
This example fires an on-site search signal in a live-search implementation. A 500 ms debounce ensures the signal fires after the user pauses, not on every keystroke. Cancel the debounce on dismiss to avoid firing a signal after the screen is dismissed.
import Connect
// MARK: - Debounce state
private var searchDebounce: DispatchWorkItem?
// MARK: - Signal helper
private func sendOnSiteSearchSignal(query: String, resultCount: Int) {
var signal: [String: Any] = [
"signalType": "onSiteSearch",
"searchTerm": query,
"numberOfResults": resultCount,
"effect": resultCount > 0 ? "positive" : "negative"
]
if UserSession.shared.isAuthenticated, let email = UserSession.shared.email {
signal["audience"] = ["Email Address": email]
}
let accepted = ConnectCustomEvent.sharedInstance().logSignal(signal)
NSLog("[Connect] onSiteSearch signal accepted: \(accepted)")
}
// MARK: - UISearchResultsUpdating
extension SearchViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
refreshResults() // update the table immediately
let query = searchController.searchBar.text?.trimmingCharacters(in: .whitespaces) ?? ""
guard !query.isEmpty else { return }
searchDebounce?.cancel()
let work = DispatchWorkItem { [weak self] in
guard let self else { return }
self.sendOnSiteSearchSignal(query: query, resultCount: self.results.count)
}
searchDebounce = work
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: work)
}
}
// MARK: - Cancel debounce on dismiss
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
searchDebounce?.cancel()
}import Connect
struct SearchScreen: View {
@State private var searchText = ""
@State private var results: [Product] = Catalog.allProducts
@State private var searchTask: Task<Void, Never>?
var body: some View {
List(results) { product in
// ...
}
.searchable(text: $searchText, prompt: "Search the shop")
.onChange(of: searchText) { query in
let trimmed = query.trimmingCharacters(in: .whitespaces)
refreshResults(query: trimmed)
guard !trimmed.isEmpty else { return }
searchTask?.cancel()
searchTask = Task {
try? await Task.sleep(nanoseconds: 500_000_000)
guard !Task.isCancelled else { return }
sendOnSiteSearchSignal(query: trimmed, resultCount: results.count)
}
}
.onDisappear {
searchTask?.cancel()
}
}
private func sendOnSiteSearchSignal(query: String, resultCount: Int) {
var signal: [String: Any] = [
"signalType": "onSiteSearch",
"searchTerm": query,
"numberOfResults": resultCount,
"effect": resultCount > 0 ? "positive" : "negative"
]
if UserSession.shared.isAuthenticated, let email = UserSession.shared.email {
signal["audience"] = ["Email Address": email]
}
let accepted = ConnectCustomEvent.sharedInstance().logSignal(signal)
NSLog("[Connect] onSiteSearch signal accepted: \(accepted)")
}
}#import <Connect/Connect.h>
// Signal helper
- (void)sendOnSiteSearchSignalWithQuery:(NSString *)query resultCount:(NSInteger)resultCount {
NSMutableDictionary *signal = [@{
@"signalType": @"onSiteSearch",
@"searchTerm": query,
@"numberOfResults": @(resultCount),
@"effect": resultCount > 0 ? @"positive" : @"negative"
} mutableCopy];
if ([[UserSession shared] isAuthenticated] && [[UserSession shared] email]) {
signal[@"audience"] = @{ @"Email Address": [[UserSession shared] email] };
}
BOOL accepted = [[ConnectCustomEvent sharedInstance] logSignal:signal];
NSLog(@"[Connect] onSiteSearch signal accepted: %@", accepted ? @"true" : @"false");
}
// UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
[self refreshResults]; // update the table immediately
NSString *query = [searchController.searchBar.text
stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet] ?: @"";
if (query.length == 0) return;
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector(fireSearchSignal:)
object:nil];
[self performSelector:@selector(fireSearchSignal:)
withObject:query
afterDelay:0.5];
}
- (void)fireSearchSignal:(NSString *)query {
[self sendOnSiteSearchSignalWithQuery:query resultCount:self.results.count];
}
// Cancel debounce on dismiss
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[NSObject cancelPreviousPerformRequestsWithTarget:self];
}Verification
After typing a query in the search bar and pausing for 500 ms, check the Xcode console for:
[Connect] onSiteSearch signal accepted: true
The false value means the SDK rejected the call - see Troubleshooting below. The signal does not fire on every keystroke - only after the debounce interval elapses. Once the console confirms acceptance, verify the signal is available in Connect.
Troubleshooting
Signal not appearing in Connect?
- Confirm the Connect SDK is initialized before any signal calls. See Method.
- Capture the return value and check that it is
true:let accepted = ConnectCustomEvent.sharedInstance().logSignal(signal). Afalsereturn indicates the SDK rejected the call. - Verify the
appKeyandpostURLmatch the Connect org you're checking.
Signal marked as invalid in Connect?
- Confirm all required fields are present:
signalType,searchTerm,numberOfResults.
Signal fires on every keystroke?
- Add debouncing as shown in the complete example. A
DispatchWorkItemcancelled and rescheduled on eachupdateSearchResultscall is the standard iOS pattern. - Alternatively, implement
UISearchBarDelegate.searchBarSearchButtonClicked(_:)and fire only on explicit submit.
Contact not created or updated?
- Verify attribute key names in
audiencematch exactly how they appear in Connect - including capitalization and spacing.
Related pages
Updated about 1 hour ago
