Docs
grovs.io Start for free

In-app messages

Show in-app messages from the Grovs dashboard in your iOS app, hold them back on some screens, and react to taps and answers.

Requires iOS SDK 3.1.0

In-app messages are dashboard campaigns: a banner, modal, full-screen cover, system alert, question or your own HTML. The SDK downloads live campaigns, picks one on the device for each app open, screen view or event, and draws it in its own window above your app. To create campaigns, see In-app messages.

No code is required. Once the SDK is configured, campaigns show. Use the calls below to keep messages off some screens, pause them during a flow, or react to what the user did.

How messages reach the device

  • Sync. At start, on return to foreground if the last sync is over a minute old, and every 15 minutes while the app is in use. Dashboard changes apply at the next sync.
  • Triggers. App open, a screen view (automatic or trackScreenView), or a custom event from track, optionally filtered by its properties.
  • Decision. Schedule, conditions and frequency caps are checked on the device. If several campaigns match one event, the highest priority shows.
  • One at a time. While a message is on screen, the newest highest priority match waits. It shows once the screen is free, if it still qualifies.
  • Offline. Synced messages still show. After 48 hours without a sync, nothing shows until a sync succeeds.

Messages show only while the SDK is enabled and authenticated and the app is in the foreground. A message that can't be drawn (the app has no window, say) reports GrovsError.inAppDisplayFailed (5) to grovsDidEncounterError. It retries at its next trigger.

React to messages

Set Grovs.inAppDelegate to an object adopting GrovsInAppDelegate. Every method is optional and runs on the main thread. The delegate is held weakly, so keep a strong reference.

Swift
import Grovs
 
final class MedlyInAppHandler: NSObject, GrovsInAppDelegate {
 
    // Return false to stop this message. It isn't counted as seen, and nothing else shows for this event.
    func grovsShouldShowInAppMessage(campaignID: String) -> Bool {
        return !CheckoutFlow.isActive
    }
 
    func grovsDidShowInAppMessage(campaignID: String) {}
 
    // buttonID is nil for a tap on the message itself. payload is set for a key-value action.
    func grovsDidTapInAppMessage(campaignID: String, buttonID: String?, payload: [String: Any]?) {
        if let screen = payload?["screen"] as? String {
            Router.shared.open(screen)
        }
    }
 
    // reason: close_button, backdrop, swipe, app or orientation
    func grovsDidDismissInAppMessage(campaignID: String, reason: String) {}
 
    // Called once an answered question has closed, in place of the dismissal.
    func grovsDidAnswerInAppMessage(campaignID: String, value: Any, comment: String?) {}
}
 
let inAppHandler = MedlyInAppHandler()
Grovs.inAppDelegate = inAppHandler
MethodWhen it's called
grovsShouldShowInAppMessage(campaignID:)Before a message shows. Return false to stop it.
grovsDidShowInAppMessage(campaignID:)The message is on screen and counted as viewed.
grovsDidTapInAppMessage(campaignID:buttonID:payload:)A button or the message was tapped. payload carries the key-value pairs of a key-value action.
grovsDidDismissInAppMessage(campaignID:reason:)The message closed without a click. reason is close_button, backdrop, swipe, app (closed by the SDK or your app) or orientation (device turned to a disallowed orientation; not a dashboard dismissal).
grovsDidAnswerInAppMessage(campaignID:value:comment:)A question was answered and the message closed. value is an Int (1 to 5) for stars, or the chosen option's ID for a choice. comment is nil unless one was sent.

Button actions

Each button, and the message itself, has one action. The SDK performs it, reports the click and calls grovsDidTapInAppMessage.

ActionWhat the SDK does
CloseCloses the message.
Open URLOpens the URL with UIApplication.shared.open.
Key-valueCloses the message and passes the pairs to grovsDidTapInAppMessage as payload. Route in your own code.
Request push permissionShows the system prompt and registers for remote notifications if allowed. If the person declined before, opens your app's settings. Skips people who already allowed notifications.
Request app ratingShows the system review prompt (SKStoreReviewController). Apple decides whether it appears.
Open storeOpens your app's App Store page if the project has one configured. Otherwise campaigns with this action aren't shown.

Hold messages back

Swift
// Pause during checkout or a video. The newest highest priority message waits.
Grovs.pauseInAppMessages()
 
// Show messages again, starting with the one that waited, if it still qualifies.
Grovs.resumeInAppMessages()
 
// Drop the waiting message and stay paused. Pass true to also close the one on screen.
Grovs.discardInAppMessages(dismissCurrent: true)
 
// No message shows on these screens; messages wait until the user leaves them.
Grovs.setInAppExcludedScreens(["Checkout", "Onboarding"])

Excluded screens match tracked screen names: the name passed to trackScreenView, or the one automatic tracking resolved. Case is ignored. Pause, exclusions and the delegate survive a second configure call.

Questions

A message can ask for a star rating (1 to 5) or one choice from a list. One tap answers.

  • A comment box (up to 500 characters, with Send and Skip) and a thank-you text can follow.
  • A high enough star rating can be followed by the system review prompt.
  • An answered campaign isn't shown again on that device.

The answer is reported on tap, so it counts even if the app closes during the comment. grovsDidAnswerInAppMessage runs once the message closes. The answer is also an event (in_app_answered) that can trigger another campaign.

Custom HTML messages

The HTML template shows your page in a web view. The SDK injects window.grovs before your scripts run:

MemberDescription
grovs.close()Closes the message (reported as close_button).
grovs.track(name, properties)Tracks a custom event with campaign_id added. The message stays open. The first call counts as the message's click. Names are 1 to 100 characters and can't be reserved names.
grovs.open(url)Opens a link and closes the message. Only works within 2 seconds of a tap inside the message.
grovs.setAttribute(key, value)Sets a user attribute. value is a string, number, boolean or null.
grovs.requestPushPermission()Shows the push permission prompt. Resolves "granted", "denied" or "unsupported".
grovs.user, grovs.eventRead-only copies of the user's attributes and the triggering event's properties.
grovs.capabilities.pushPermission"granted", "denied", "prompt" or "unsupported".

Or use data attributes, no script needed:

HTML
<button data-grovs-action="close">Not now</button>
<button data-grovs-action="open" data-grovs-url="https://medly.sqd.link/refill">Refill now</button>
<button data-grovs-action="track" data-grovs-event="refill_interest">Tell me more</button>

Plain <a href> links go through grovs.open. A #fragment link scrolls the page.

Personalization. Write {{user.first_name}} or {{event.plan}} in text and in alt, title, value, placeholder, aria-* and data-* attributes. Add a fallback with {{user.first_name | default: "there"}}. If a field has no value and no fallback, the message isn't shown.

Links. javascript:, vbscript:, data:, blob:, file:, about:, intent: and plain http: links are refused. Use https:, mailto:, tel:, sms: or your app's own scheme.

Layout. Full screen, modal, or a top or bottom banner. The SDK sizes the message to the page's reported height and keeps it hidden until the page is ready.

Test messages

Send test on a dashboard campaign sends it to every device of one person, matched by Grovs.userIdentifier. The device shows it at its next sync, ignoring triggers, conditions and limits, once the screen is free and messages aren't paused. To see it sooner, background the app and return after a minute.

A test message isn't counted or reported. Events it tracks and attributes it sets are logged, not sent. If the SDK can't show it, or its pictures fail to load after 10 tries, the test is dropped with a log warning.

Orientation and iPad

A campaign targets portrait, landscape or both. It shows only in that orientation and closes if the device turns, with reason orientation.

On iPad the SDK draws in the scene holding the key window. Orientation follows that window's shape, not the device: a split-screen window can be portrait on a sideways device. Closing the window closes its message.

Messages keep the status bar hidden when your app hides it and return the keyboard to your app after a comment. VoiceOver focus moves to the message and back when it closes. The VoiceOver escape gesture closes dismissible messages.

Debugging

Swift
Grovs.setDebug(level: .info)
Grovs.setInAppDebugLogging(true)

setInAppDebugLogging(true) logs, at info level, each campaign considered for an event and why it was skipped: outside its schedule, failed condition, frequency cap reached, lower priority, not yet synced, or unsupported by this SDK version.

Reserved event names

The SDK reports message views, clicks, dismissals and answers as events. track(_:) and grovs.track in HTML messages refuse these names:

in_app_viewed, in_app_clicked, in_app_dismissed, in_app_answered, in_app_commented, in_app_held, push_open, push_delivered, push_held, journey_entered, journey_held, journey_path, plus the system names view, open, install, reinstall, app_open, time_spent, reactivation, user_referred, custom and screen_view.

See also

Last updated 2026-10-26
Was this page helpful?