Skip to content
reception

Search documentation

Loading documentation…

    Log in
    Browse documentation

    iOS SDK

    Open & close the chat

    Place support wherever it fits your app: in settings, on a home screen, in a toolbar or menu, behind a floating button, or in your navigation. ReceptionKit supplies the chat screen; you write the entry-point code and control its position, label, style, and unread badge. The four examples below are starting points for your own design.

    View Markdown

    Settings rows and buttons call Reception.shared.openChat() to let ReceptionKit present its sheet over the current screen. A dedicated tab embeds ReceptionChatView() and uses your app’s tab selection. The examples below show both patterns, followed by dismissal options.

    Choose an entry point

    These mockups show four common placements. Choose the one that fits your app, then use the matching SwiftUI example below.

    Settings rowPart of your settings

    Settings row · Part of your settings

    Home-screen buttonIn the page content

    Home-screen button · In the page content

    Floating buttonAbove the page content

    Floating button · Above the page content

    Dedicated tabIn your app’s navigation

    Dedicated tab · In your app’s navigation

    Or anywhere else in your app

    A toolbar action, a help menu, your own custom button: the placement, label, icon, style, and unread indicator are yours to build. Keep the entry point discoverable, give it a meaningful accessible label, and show a dot or count when Reception.shared.unreadCount is greater than zero. See Unread messages for badge behavior.

    Open the chat

    Call Reception.shared.openChat() from your own settings row, toolbar, help menu, or floating button. ReceptionKit has no public ready-made settings row or floating entry button. Use your app's controls, styling, and localized labels.

    This action belongs in a SwiftUI button on the main actor:

    Swift
    Button("Contact support") {
        Reception.shared.openChat()
    }

    Reception.shared.openChat() requests the SDK chat sheet over the current screen and returns immediately. It works in SwiftUI and UIKit without changing your root view or navigation.

    ReceptionKit UI and state APIs are @MainActor isolated. For an action arriving outside the main actor, hop to it before opening. This belongs in the host callback that requests presentation:

    Swift
    Task { @MainActor in
        Reception.shared.openChat()
    }

    Avoid opening Support while another host sheet is being presented. Route the action through your app's presentation coordinator when you need to dismiss an existing modal first.

    Entry-point code examples

    Adapt these controls to your app's design. Each example reads the live Reception.shared.unreadCount and hides its badge at zero.

    Configure ReceptionKit before creating these views. Import SwiftUI and ReceptionKit in the host file, and localize the labels and unread-message plurals through your app's normal resources.

    Settings row

    Put GuideSupportSettingsRow() inside an existing settings List or Section. The row opens the SDK sheet and hides its badge at zero.

    The row uses the reusable GuideUnreadBadge defined just below it:

    Swift
    @MainActor
    struct GuideSupportSettingsRow: View {
        var body: some View {
            Button { Reception.shared.openChat() } label: {
                HStack {
                    Label("Support", systemImage: "bubble.left.and.bubble.right")
                    Spacer()
                    if Reception.shared.unreadCount > 0 {
                        GuideUnreadBadge(count: Reception.shared.unreadCount)
                    }
                    Image(systemName: "chevron.right")
                        .font(.footnote.weight(.semibold))
                        .foregroundStyle(.tertiary)
                }
            }
            .accessibilityLabel("Support")
            .accessibilityValue(Text("\(Reception.shared.unreadCount) unread messages"))
        }
    }
    
    struct GuideUnreadBadge: View {
        let count: Int
        var body: some View {
            Text(count, format: .number)
                .font(.subheadline.weight(.semibold))
                .monospacedDigit()
                .foregroundStyle(.white)
                .frame(minWidth: 26, minHeight: 26)
                .background(.indigo, in: Capsule())
        }
    }

    Home-screen entry

    Place GlassSupportHomeButton() in your home screen. This host-owned button uses native Liquid Glass on iOS 26 and regular system material on earlier versions. Adapt its colors and spacing to your app.

    The wrapper observes the SDK count; the control hides the badge at zero and opens the SDK sheet. Its glass styling uses SwiftUI’s glassEffect, independently of Reception.shared.appearance:

    Swift
    import SwiftUI
    import ReceptionKit
    
    // Place in the host home screen, after configuring ReceptionKit.
    @MainActor
    struct GlassSupportHomeButton: View {
        var body: some View {
            GlassSupportHomeControl(count: Reception.shared.unreadCount)
        }
    }
    
    @MainActor
    struct GlassSupportHomeControl: View {
        let count: Int
    
        var body: some View {
            Group {
                if #available(iOS 26.0, *) {
                    button
                        .glassEffect(.regular.interactive(),
                                     in: .rect(cornerRadius: 24))
                } else {
                    button
                        .background(.regularMaterial,
                                    in: RoundedRectangle(cornerRadius: 24))
                }
            }
            .accessibilityLabel("Support")
            .accessibilityValue(Text("\(count) unread messages"))
        }
    
        private var button: some View {
            Button { Reception.shared.openChat() } label: {
                HStack(spacing: 12) {
                    Image(systemName: "bubble.left.and.bubble.right.fill")
                        .font(.title2).foregroundStyle(.indigo)
                    VStack(alignment: .leading, spacing: 4) {
                        Text("Support").font(.headline).foregroundStyle(.primary)
                        Text("Continue the conversation")
                            .font(.subheadline).foregroundStyle(.secondary)
                    }
                    Spacer(minLength: 0)
                    if count > 0 {
                        Text(count, format: .number)
                            .font(.subheadline.weight(.semibold))
                            .monospacedDigit()
                            .foregroundStyle(.white)
                            .frame(minWidth: 26, minHeight: 26)
                            .background(.indigo, in: Capsule())
                    }
                    Image(systemName: "chevron.right")
                        .font(.footnote.weight(.semibold)).foregroundStyle(.secondary)
                }
                .padding(20)
                .frame(maxWidth: .infinity, alignment: .leading)
                .contentShape(RoundedRectangle(cornerRadius: 24))
            }
            .buttonStyle(.plain)
        }
    }

    Floating button

    Keep support within reach on a main screen with a host-owned floating button. Place it above your safe area, clear of navigation and primary actions. This example uses native Liquid Glass on iOS 26 and regular material on earlier versions; it opens the SDK sheet through Reception.shared.openChat().

    After configuration, add this overlay to your screen content:

    Swift
    .overlay(alignment: .bottomTrailing) {
        FloatingSupportButton()
            .padding(.trailing, 24)
            .padding(.bottom, 30)
    }

    The wrapper observes the live count; its reusable control supplies the native button:

    Swift
    import SwiftUI
    import ReceptionKit
    
    @MainActor
    struct FloatingSupportButton: View {
        var body: some View {
            FloatingSupportControl(count: Reception.shared.unreadCount)
        }
    }
    
    @MainActor
    struct FloatingSupportControl: View {
        let count: Int
    
        var body: some View {
            Group {
                if #available(iOS 26.0, *) {
                    button.glassEffect(.regular.interactive(), in: .capsule)
                } else {
                    button.background(.regularMaterial, in: Capsule())
                }
            }
            .accessibilityLabel("Support")
            .accessibilityValue(Text("\(count) unread messages"))
        }
    
        private var button: some View {
            Button { Reception.shared.openChat() } label: {
                HStack(spacing: 10) {
                    Label("Support", systemImage: "bubble.left.and.bubble.right.fill")
                        .font(.headline)
                    if count > 0 {
                        Text(count, format: .number)
                            .font(.subheadline.weight(.semibold))
                            .monospacedDigit()
                            .foregroundStyle(.white)
                            .frame(minWidth: 26, minHeight: 26)
                            .background(.indigo, in: Capsule())
                    }
                }
                .padding(.horizontal, 20)
                .frame(minHeight: 56)
                .contentShape(Capsule())
            }
            .buttonStyle(.plain)
        }
    }

    Dedicated Support tab

    A dedicated tab can host ReceptionChatView() directly. Add the badge to the tab's destination, using the integer overload: a zero value hides it. Reading the SDK value inside body lets SwiftUI update the badge while another tab is selected.

    Merge the Support destination into your existing TabView; the other destinations below are placeholders for your real screens:

    Swift
    @MainActor
    struct GuideSupportTabs: View {
        var body: some View {
            TabView {
                Text("Today")
                    .tabItem { Label("Today", systemImage: "house") }
                Text("Habits")
                    .tabItem { Label("Habits", systemImage: "checkmark.circle") }
                NavigationStack { ReceptionChatView() }
                    .tabItem { Label("Support", systemImage: "bubble.left.and.bubble.right") }
                    .badge(Reception.shared.unreadCount)
                Text("Settings")
                    .tabItem { Label("Settings", systemImage: "gearshape") }
            }
            .tint(.indigo)
        }
    }

    Unread monitoring begins at configuration, before the Support tab is first visited. Your app owns tab selection. Reception.shared.openChat() and notification taps forwarded with openChat: true present the SDK sheet; they do not select this tab. If your app routes a support notification into the tab, forward it with openChat: false and select the Support tab through your own navigation state. Do not also call openChat() for the same action. See Entry point.

    Choosing the tab does not itself clear the count. It updates after the visible chat successfully marks loaded replies read. Newer replies can remain unread. Network failure can leave a badge in place; do not overwrite the read-only property or keep a separate counter to force it to zero.

    Use custom navigation

    Use ReceptionChatView() when your app needs to own the sheet binding or navigation path. Configure ReceptionKit first and display only one chat at a time.

    Present a custom sheet

    Place this view in your host app. Its Boolean controls presentation and can also be set to false by host actions that must dismiss the sheet:

    Swift
    import SwiftUI
    import ReceptionKit
    
    @MainActor
    struct HelpView: View {
        @State private var showsSupport = false
    
        var body: some View {
            Button("Support") {
                showsSupport = true
            }
            .sheet(isPresented: $showsSupport) {
                ReceptionChatView()
            }
        }
    }

    When presented as a sheet, ReceptionChatView() supplies its own navigation stack and a native drag indicator. Do not add an extra NavigationStack around it. Swipe-to-dismiss works unless your host disables interactive dismissal. The optional SDK close button also dismisses a custom sheet through SwiftUI's dismiss environment.

    Push onto your navigation stack

    Place this view in the host app for a navigation-based entry point:

    Swift
    import SwiftUI
    import ReceptionKit
    
    @MainActor
    struct HelpNavigationView: View {
        var body: some View {
            NavigationStack {
                List {
                    NavigationLink("Support") {
                        ReceptionChatView()
                    }
                }
                .navigationTitle("Help")
            }
        }
    }

    In an ordinary navigation stack, the chat uses your navigation controls instead of adding the sheet close button. The user goes back with the host's Back button or navigation gesture. For programmatic dismissal, update the host's navigation path or destination binding.

    If your navigation stack is inside a sheet or another modal, test the Back button and dismissal in that hierarchy.

    Route open requests deliberately

    Reception.shared.openChat() and forwarded notification taps with openChat: true request the SDK sheet; they do not toggle showsSupport or push your custom destination. A request does nothing while chat is already visible. When an action opens a host-owned ReceptionChatView(), do not also call openChat() for that action.

    For an app that routes all support UI through its own navigation, forward a support notification with openChat: false and use the returned Boolean to decide whether to open your own destination. Keep foreground receipt separate from notification taps: receiving a notification should not automatically navigate. See Push notifications.

    Unread monitoring starts with Reception.configure(appId:), before the first custom chat appearance. Reading unreadCount in a SwiftUI view observes updates.

    Dismiss the chat

    Reception.shared.closeChat() closes the SDK sheet programmatically, for example before navigating to a deep link target or showing a logout screen. Choose dismissal according to who owns the presentation:

    PresentationDismissal
    SDK sheet from openChat() or a push tapSwipe down, use the optional SDK close button, or call Reception.shared.closeChat().
    Host sheet containing ReceptionChatView()Set your sheet binding to false, use the sheet gesture, or enable the SDK close button.
    Dedicated Support tabSelect another tab through your host tab state.
    Host navigation destinationUse Back, the navigation gesture, or update your navigation state.

    A host-owned ReceptionChatView presentation is dismissed by the host. Do not call Reception.shared.logout() merely to close a chat: it resets the support session and local data. Logout, switching to a different App ID, and successful data deletion dismiss the SDK-owned sheet, but they do not change your custom sheet binding or navigation path. Coordinate those host states during login and logout and data deletion.

    Show the close button

    showsCloseButton defaults to false. Configure these local appearance settings during app initialization on the main actor:

    Swift
    Reception.shared.appearance.showsCloseButton = true
    Reception.shared.appearance.closeIcon = .chevronDown

    closeIcon defaults to .xmark; supported values are .xmark, .chevronDown, and .arrowDown. Changing the icon alone does not make the button visible. See showsCloseButton and closeIcon for their appearance reference.

    Published remote appearance can override local values. Set Reception.shared.usesRemoteAppearance = false during initialization if your app must use only its local appearance. If the host disables interactive dismissal, provide an available dismissal control; do not rely on a remote-controlled close button as the only exit.

    closeIcon = .xmark

    closeIcon = .xmark

    closeIcon = .chevronDown

    closeIcon = .chevronDown

    closeIcon = .arrowDown

    closeIcon = .arrowDown

    Observe chat visibility

    Reception.shared.isChatOpen is an observable, read-only Boolean. It reflects whether the shared chat content has appeared, including custom ReceptionChatView() presentations. It becomes false when that content disappears.

    Use it in your host's main-actor presentation logic, for example to avoid requesting another chat:

    Swift
    if !Reception.shared.isChatOpen {
        Reception.shared.openChat()
    }

    It is visibility state, not a presentation binding or a network status. It may still be false immediately after Reception.shared.openChat() while the sheet is being presented, and it can remain true while a visible chat's app is backgrounded. Do not treat it as proof of an active connection or use it as your custom sheet's writable state. A host presentation coordinator must also account for transitions and pending requests.

    For analytics, .chatOpened is available through events. There is no matching public chat-closed event; observe visibility when your host needs that transition.

    When ReceptionKit uses the network

    Before the first valid send

    For an unused support session, configuration stores settings and reads cached appearance without starting authenticated chat requests. Opening the chat, drafting text, choosing photos, or forwarding a push does not register a device or load a conversation. Unread checks stay inactive.

    Public appearance is the exception: the configured SDK or a visible chat can fetch project appearance with the App ID while active, without a session. Disable remote appearance to use local appearance only.

    Registration begins with the first valid send attempt, even if delivery later fails.

    After activation

    A visible, active chat loads messages, reports read state, and keeps the conversation updated. Dismissal or backgrounding pauses live updates; an already-running request may still finish.

    Unread updates continue while the chat is closed and your app is active. Closing the chat also refreshes the count. See Unread refresh timing for the automatic checks and push-related pauses.

    Last active in chat

    The dashboard’s Last active in chat records recent chat activity. App launches and unread checks do not count. An unused chat cannot report activity; In chat is a separate live-connection signal. See Dashboard.

    Show unread replies

    Read Reception.shared.unreadCount in your SwiftUI entry point and hide its badge when the count is zero. The Settings row, home-screen entry, floating button, and dedicated tab examples above each show that behavior.

    After support has been used, unread updates also work while chat is closed. For automatic updates, push handling, and app-icon badge ownership, follow Unread messages.

    Verify your integration

    1. Open Support from your entry point, then dismiss it and open it again.

    2. Navigate through login and your main screens; confirm the SDK sheet opens over the current screen.

    3. Test the sheet gesture, optional close button, and navigation Back behavior in the presentation you chose.

    4. Confirm your custom sheet or path also dismisses during host account transitions.

    5. Forward a notification tap and confirm it opens exactly one intended chat. Test foreground receipt separately.

    6. Send a message, leave the chat, and follow the unread verification flow.

    If no sheet opens, check that configuration ran and whether another modal is occupying the presentation location. If the close button does not match your setting, check the active remote appearance and whether the chat is actually in a presented context.