Skip to content

iOS

Add the Giov chat to a native iOS app as a SwiftUI sheet or full screen, with verified identity, Keychain storage, attachments and push.

On this page

The iOS SDK (giovchat-ios, version 0.1) is a Swift package that shows the same conversation your website visitors have as a native SwiftUI screen. It has two products:

  • GiovChat is the SwiftUI chat screen, GiovChatView. It brings GiovChatCore with it.
  • GiovChatCore is the client without any UI. Use it alone if you build your own chat interface.

It has no third-party dependencies. Its source is at github.com/Fegig-Tech/giovchat-ios.

Colours, greeting, pre-chat fields, GiovBot's name and avatar, attachment rules and branding come from Settings → Chat widget in the dashboard. In code, tenantKey is your workspace ID, shown in Settings → Chat widget → Installation.

Install

Requires iOS 16 or newer (macOS 13 for the core and the screen on the Mac) and Xcode 16 or newer.

In Xcode, choose File → Add Package Dependencies…, paste the package URL, pick a dependency rule such as Up to Next Major Version from 0.1.0, and add GiovChat to your app target. Choose only GiovChatCore if you build your own UI.

Package URL
https://github.com/Fegig-Tech/giovchat-ios

Or in Package.swift:

Package.swift
dependencies: [
    .package(url: "https://github.com/Fegig-Tech/giovchat-ios", from: "0.1.0"),
],
targets: [
    .target(name: "MyApp", dependencies: [
        .product(name: "GiovChat", package: "giovchat-ios"),          // the screen; brings GiovChatCore
        // .product(name: "GiovChatCore", package: "giovchat-ios"),   // only the controller, for your own UI
    ]),
]
Note

While the version is 0.x, minor versions can contain breaking changes. Use .upToNextMinor(from: "0.1.0") to stay on 0.1.x until you upgrade by hand.

No Info.plist keys are needed. Photos use the system photo picker and files use the Files picker, and neither asks for a permission.

Show the chat

Sheet

SupportButton.swift
import SwiftUI
import GiovChat

struct SupportButton: View {
    @State private var showChat = false

    var body: some View {
        Button("Chat with us") { showChat = true }
            .sheet(isPresented: $showChat) {
                GiovChatView(
                    tenantKey: "YOUR_WORKSPACE_ID",
                    onClose: { showChat = false }
                )
            }
    }
}

onClose adds a close button to the header. Leave it out if the screen has its own way back, such as a navigation bar.

Full screen

GiovChatView fills the space it is given, so it also works as a navigation destination, a tab or a whole window:

ChatScreen.swift
import SwiftUI
import GiovChat

struct ChatScreen: View {
    var body: some View {
        GiovChatView(tenantKey: "YOUR_WORKSPACE_ID")
    }
}

The view draws every phase: loading, pre-chat form, greeting, connecting, the conversation, and an error with a "Try again" button. It also forwards the app's foreground and background changes to the chat. Opening it never creates an empty conversation: a conversation starts when the visitor sends their first message or submits the pre-chat form. A stored conversation is resumed first; a closed one is forgotten, and the next message starts a new one.

Links and attachments open through SwiftUI's openURL action, which means Safari or the mail app by default. Set your own action around the chat to open them in your app. Only http(s) and mailto: links are tappable; anything else stays plain text.

InAppLinks.swift
GiovChatView(tenantKey: "YOUR_WORKSPACE_ID")
    .environment(\.openURL, OpenURLAction { url in
        router.showInAppBrowser(url)
        return .handled
    })

Let your app own the controller

GiovChatView(tenantKey:) creates a GiovChatController, starts it when the view appears and closes it when the view goes away. Changing tenantKey, baseURL, contact, sessionKey, keepConnected or locale creates a new controller.

Create the controller yourself when something else needs it: push registration, sign-out, an unread badge, or a chat that keeps counting replies while the screen is closed.

Support.swift
import GiovChatCore

@MainActor
final class Support: ObservableObject {
    static let shared = Support()
    let chat = GiovChatController(tenantKey: "YOUR_WORKSPACE_ID", keepConnected: true, visible: false)
    private init() { Task { [chat] in await chat.start() } }
}
Chat view
GiovChatView(controller: Support.shared.chat, forwardLifecycle: false, onClose: { showChat = false })

The view never closes a controller you pass in. When the view goes away it marks the chat as not visible, so a keepConnected controller keeps counting unread replies. Call close() when you no longer need the controller.

Signed-in users

Pass the user's details through contact. Any field you provide skips the matching pre-chat field.

Chat.swift
GiovChatView(
    tenantKey: "YOUR_WORKSPACE_ID",
    contact: GiovContact(
        userId: account.id,
        userHash: account.giovUserHash,   // computed on your server
        email: account.email,
        firstName: account.firstName
    ),
    sessionKey: account.id
)

GiovContact also takes phone and lastName.

  • userHash is the HMAC-SHA256 (lowercase hex) of userId, keyed with the identity secret from Settings → Chat widget → Visitor details. Compute it on your server and send it to the app; never ship the secret. See Identify visitors for server examples.
  • Without userHash, userId is recorded as an unverified claim and doesn't link the chat to an existing contact.
  • sessionKey separates stored conversations of the same device, for example one per account.
  • Sign-out and account switching: call await controller.reset(), or change sessionKey. The previous user's conversation, stored session and queued messages are dropped, and the next user starts fresh. If the device is registered for push, unregister the token first:
SignOut.swift
let chat = Support.shared.chat
_ = await chat.unregisterPushToken(deviceToken: deviceToken)   // before reset(), which forgets the stored session
await chat.reset()

Keep the conversation across launches

Nothing to set up: by default the session is stored in the Keychain, so visitors resume the same conversation next time.

The session token is a bearer secret that proves the device owns the conversation. KeychainGiovSessionStore keeps it as a generic password that is:

  • readable after the first unlock since the device started, so an app woken by a push can read it;
  • this device only, so it is never synced through iCloud Keychain or restored to another device.

To share the session with an app extension, pass a Keychain access group:

Chat.swift
GiovChatView(
    tenantKey: "YOUR_WORKSPACE_ID",
    sessionStore: KeychainGiovSessionStore(accessGroup: "TEAMID.group.example")
)

To keep the session somewhere else, implement GiovSessionStore (three async methods: read(scope:), write(scope:session:) and delete(scope:)). InMemoryGiovSessionStore forgets the conversation when the process ends. On macOS the item goes to the data protection keychain when the app has a keychain entitlement (signed with a team), and to the login keychain otherwise.

The store is keyed by API, workspace, userId and sessionKey, not by name, email or phone. Filling in the pre-chat form keeps the conversation, and signing in as someone else never resumes the previous person's one.

Storage failures are reported through onError as a GiovChatError of kind .storage and never crash the chat. If a read fails, for example because the Keychain refuses reads until the first unlock after a restart, the SDK tries again before starting a new conversation, so the stored one is never replaced by mistake.

Warning

Never log the session token or share it between users.

Attachments

Attachments need nothing from you. Once the conversation has started, and the published GiovBot agent allows attachments, the composer shows an attach button. It offers the system photo picker and the Files picker, limited to the types the agent accepts. The screen checks the picks against the agent's rules, shows any problem as a sentence, lists the files as removable chips and uploads them with progress when the visitor sends.

  • Files must be one of the supported types (images, PDF, text and CSV, Office documents, audio), within the agent's size limit (at most 10 MB), and at most 5 per message. The server checks again.
  • Photos in formats the server doesn't take, such as HEIC, are re-encoded as JPEG.
  • Every image gets its size and a tiny blurred placeholder, so other viewers, including your team, see its shape before it loads.

No permission is requested and no Info.plist usage strings are needed, because both pickers run outside your app's process. Only add NSPhotoLibraryUsageDescription or NSCameraUsageDescription if your own code uses the photo library or camera.

Unread replies and badges

By default the chat connects only while it's on screen. To count replies that arrive while it's closed, create the controller with keepConnected: true, as in Let your app own the controller. It stays connected while your app is in the foreground, and it only resumes an existing conversation and never starts one in the background.

Read the total anywhere with GiovUnread:

Badge.swift
import GiovChatCore

@MainActor
final class Badge: ObservableObject {
    @Published var unread = 0
    private var observation: GiovObservation?

    init() {
        observation = GiovUnread.observe { [weak self] count in self?.unread = count }   // all chats
        // GiovUnread.observe(tenantKey: "YOUR_WORKSPACE_ID") { ... } for one workspace
    }
}

GiovUnread.count(tenantKey:) returns the current number once. Opening the chat clears the count. You can also set onUnreadCountChange on a controller.

Lifecycle

GiovChatView forwards the scene's foreground and background changes to its controller itself. For a controller that outlives the chat screen, add .giovLifecycle(_:) once, high in your view tree, and pass forwardLifecycle: false to the chat view:

RootView.swift
RootView().giovLifecycle(Support.shared.chat)

When the app goes to the background, the connection stays open for a 5 second grace period and then closes, so replies arrive as push notifications. When it returns, the chat reconnects and catches up from history. Dropped connections are retried automatically.

A message you send shows at once as "Sending…" and is queued, so text typed while reconnecting is never lost. A message that can't be delivered shows "Not sent · Tap to retry" and a Discard option.

Push notifications

When the chat is closed and the app is in the background, replies arrive as push notifications.

  1. Add your Apple push key and/or Firebase service account in Settings → Chat widget → Push notifications. Notifications are sent from your app, with your keys. See Push credentials.
  2. Add the Push Notifications capability to your app target.
  3. Register each device token with the controller:
AppDelegate.swift
import UIKit
import UserNotifications

final class AppDelegate: NSObject, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
    ) -> Bool {
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, _ in
            guard granted else { return }
            Task { @MainActor in UIApplication.shared.registerForRemoteNotifications() }
        }
        return true
    }

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        Task { @MainActor in
            _ = await Support.shared.chat.registerPushToken(deviceToken: deviceToken)   // never throws
        }
    }
}

// In your App: @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
  • APNs is the default. The environment defaults to sandbox in DEBUG builds and production otherwise. Development builds from Xcode get sandbox tokens, and App Store and TestFlight builds get production ones. Pass environment: if your build configurations differ.
  • If your app receives pushes through Firebase Messaging, register the FCM token instead: registerPushToken(fcmToken, platform: .fcm).
  • It registers straight away when the conversation is connected, and otherwise as soon as it connects, so you can call it at start-up and again whenever the system hands out a new token. Called before the visitor has a conversation, it waits for the first one.
  • The token is remembered and registered again by itself for each new conversation of the same visitor. reset() forgets it.
  • The result is a GiovPushResult. result.registered is true on success; otherwise result.failure is .unsupported, .noConversation (the controller was closed or reset first) or .failed.
  • unregisterPushToken(deviceToken:) stops pushes on sign-out. It works while the chat is closed or disconnected, so call it before reset().
  • A tapped notification carries { "type": "giov.message", "tenantId": "...", "conversationId": "...", "messageId": "..." }. Handle it in userNotificationCenter(_:didReceive:withCompletionHandler:) and present GiovChatView when type == "giov.message". The chat catches up from history.

Language

The interface follows the device language: English, French, Spanish, Portuguese or German, with English for others. Text you write in the dashboard (greeting, placeholder, bot name) is shown as written and isn't translated.

Chat.swift
GiovChatView(
    tenantKey: "YOUR_WORKSPACE_ID",
    locale: "pt-BR",
    strings: { $0.withOverrides(sendMessage: "Enviar", startChat: "Começar") }
)

withOverrides takes any string by name, such as sendMessage, composerPlaceholder or startChat; GiovChatStrings.all holds the built-in tables. For a controller you own, pass locale: and overrideStrings: to GiovChatController. Push registrations carry the chat's language.

Appearance

  • Theme: the dashboard setting (light, dark, auto) applies, and auto follows the device. Override it with theme.
  • Colour: the dashboard's main colour applies. Override it with primaryColor.
  • Surface colours: backgroundColor, headerColor, visitorBubbleColor and agentBubbleColor.

Set them with .giovChatStyle(_:). Every field is optional, and anything you leave out keeps the dashboard value.

ChatStyle.swift
GiovChatView(tenantKey: "YOUR_WORKSPACE_ID")
    .giovChatStyle(GiovChatStyle(
        primaryColor: Color(red: 0.05, green: 0.65, blue: 0.91),
        theme: .dark,              // .light, .dark or .auto (device)
        backgroundColor: nil,
        headerColor: .black,
        visitorBubbleColor: nil,
        agentBubbleColor: nil
    ))

Text on coloured surfaces (header, visitor bubbles, buttons) is near-black or white, whichever reads better. Layout, sizes, spacing, fonts and the branding footer stay under the SDK's control. The text uses the system font and follows Dynamic Type.

Branding: Starter and Pro always show Powered by GiovChat. Business and Advanced can turn it off in Settings → Chat widget → Appearance. The footer's look isn't affected by style overrides.

Accessibility: touch targets are at least 44 pt, controls have VoiceOver labels in the chosen language, errors are announced, and text follows Dynamic Type up to the third accessibility size. The typing dots stand still when Reduce Motion is on.

Errors

Failures reach onError as a GiovChatError. It is a typed error with a kind, an English message for your logs, and, where they apply, status, code and retryAfterSeconds. The SDK never shows server error text to visitors; it maps each kind to a translated sentence.

KindMeaning
.networkThe network couldn't be reached, or a request timed out (timedOut).
.sessionExpiredThe stored session is gone. The SDK forgets it, and the next message starts a new conversation.
.conversationClosedYour team closed the conversation. The next message starts a new one.
.rateLimitedToo many requests. Wait retryAfterSeconds.
.workspaceBusyThe workspace is at its plan's limit of open conversations.
.identityRejectedThe workspace requires a signed identity and the userHash is wrong.
.attachmentRejectedAn upload was refused (type, size or the agent's policy).
.sendFailedA message couldn't be delivered.
.invalidResponseThe server answered something the protocol doesn't allow.
.disconnectedThe chat was closed while a call was in flight.
.storageThe session store failed to read, write or delete.
.serverAny other refusal.

Privacy manifest

GiovChatCore ships a PrivacyInfo.xcprivacy, so it applies whether you use the screen or only the core. It declares no tracking, no tracking domains and no required-reason APIs. It declares the data the SDK sends to Giov so the business can provide support, all linked to the visitor, none used for tracking, with the purpose App Functionality:

Data typeWhen
Customer SupportEvery chat message.
Photos or Videos, Other User ContentImages and files the visitor attaches.
Name, Email Address, Phone NumberWhen your app passes them in GiovContact or the visitor fills in the pre-chat form.
User IDWhen your app passes userId.
Device IDThe push token, when your app registers one.
Coarse LocationThe approximate location (country and city) Giov derives from the visitor's IP address and shows to your team with the conversation.

Xcode merges it into your app's privacy report. Your App Store privacy answers should cover the same data; leave out what your integration never sends, for example no userId or no push.

The SDK never asks for your identity secret, tracks visitors, collects analytics, or contacts anything other than the Giov API (and the image URLs messages carry). It never asks for photo library, camera, contacts or notification permission; your app decides about notifications.

Reference

GiovChatView

Two initialisers. GiovChatView(tenantKey:) owns its controller:

ParameterTypeDefaultDescription
tenantKeyStringrequiredYour workspace ID
baseURLStringhttps://api.giovchat.appAPI base URL override
contactGiovContact?nilSigned-in user's details; also pre-fills the pre-chat form
sessionKeyString?nilSeparates stored conversations, for example per account
sessionStore(any GiovSessionStore)?KeychainWhere the session lives between launches
keepConnectedBoolfalseStay connected while hidden to count unread replies
localeString?Device languageInterface language
strings((GiovChatStrings) -> GiovChatStrings)?nilReplace interface strings
onClose(() -> Void)?nilShows a close button in the header
onError((GiovChatError) -> Void)?nilEvery failure, with a kind
onConversationStart((String) -> Void)?nilCalled with the new conversation's ID
onConnectionStatusChange((GiovConnectionStatus) -> Void)?nil.connecting, .connected or .disconnected
onUnreadCountChange((Int) -> Void)?nilUnread replies while hidden

GiovChatView(controller:) shows a controller you own and never closes it:

ParameterTypeDefaultDescription
controllerGiovChatControllerrequiredThe controller to show
autoStartBooltrueStart the controller when the screen first appears. Safe for one that is already started
forwardLifecycleBooltrueForward foreground and background changes. Turn off when you use .giovLifecycle(_:)
initialContactGiovContact?nilPre-fills the pre-chat form
onClose(() -> Void)?nilShows a close button in the header

Modifiers

ModifierDescription
.giovChatStyle(_:)Overrides the chat's colours and theme with a GiovChatStyle
.giovLifecycle(_:)Forwards the scene's foreground and background changes to a controller

GiovChatController

Created with GiovChatController(tenantKey:). It takes the same connection options as the view (baseURL, contact, sessionKey, sessionStore, keepConnected, locale, the four callbacks) plus visible (default true), overrideStrings and backgroundGraceMs (default 5000). It runs on the main actor and is an ObservableObject.

MemberDescription
stateThe GiovChatUiState the screen draws
observe { state in }Calls you with the state now and after every change; returns a GiovObservation
start()Starts the controller. Nothing happens before it
setVisible(_:)Tells it whether the chat is on screen
onForeground(), onBackground()Forward app lifecycle changes
send(_:files:), submitContact(_:)Send a message; submit the pre-chat details
retry()Start over after an error
retryMessage(_:), discardMessage(_:)Retry or drop a message that wasn't delivered
notifyTyping()Tell the team the visitor is typing
checkFiles(_:alreadyAttached:), attachmentURL(_:)Check picked files against the agent's rules; get an authenticated download URL
registerPushToken(deviceToken:environment:), registerPushToken(_:platform:environment:)Register an APNs or FCM token. Never throws
unregisterPushToken(deviceToken:), unregisterPushToken(_:)Stop pushes for this device. Call before reset()
reset()Forget the conversation, stored session and outbox (sign-out)
close()Release the controller. It can't be used afterwards

Other types

TypeDescription
GiovContactuserId, userHash, email, phone, firstName, lastName
GiovChatStyleprimaryColor, theme, backgroundColor, headerColor, visitorBubbleColor, agentBubbleColor
GiovThemeMode.light, .dark, .auto
GiovSessionStoreProtocol for your own session storage
KeychainGiovSessionStoreThe default store; takes service and accessGroup
GiovUnreadcount(tenantKey:) and observe(tenantKey:_:) for badges
GiovPushPlatform, GiovPushEnvironment.apns or .fcm; .sandbox or .production
GiovChatErrorThe typed error described in Errors

If you use no SwiftUI, GiovChatCore is the same engine. Read controller.state, subscribe with observe, and call the methods above.

Loading…