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:
GiovChatis the SwiftUI chat screen,GiovChatView. It bringsGiovChatCorewith it.GiovChatCoreis 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.
https://github.com/Fegig-Tech/giovchat-iosOr in 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
]),
]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
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:
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.
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.
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() } }
}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.
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.
userHashis the HMAC-SHA256 (lowercase hex) ofuserId, 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,userIdis recorded as an unverified claim and doesn't link the chat to an existing contact. sessionKeyseparates stored conversations of the same device, for example one per account.- Sign-out and account switching: call
await controller.reset(), or changesessionKey. 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:
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:
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.
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:
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().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.
- 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.
- Add the Push Notifications capability to your app target.
- Register each device token with the controller:
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
sandboxin DEBUG builds andproductionotherwise. Development builds from Xcode get sandbox tokens, and App Store and TestFlight builds get production ones. Passenvironment: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.registeredistrueon success; otherwiseresult.failureis.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 beforereset().- A tapped notification carries
{ "type": "giov.message", "tenantId": "...", "conversationId": "...", "messageId": "..." }. Handle it inuserNotificationCenter(_:didReceive:withCompletionHandler:)and presentGiovChatViewwhentype == "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.
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,visitorBubbleColorandagentBubbleColor.
Set them with .giovChatStyle(_:). Every field is optional, and anything you leave out keeps the dashboard value.
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.
| Kind | Meaning |
|---|---|
.network | The network couldn't be reached, or a request timed out (timedOut). |
.sessionExpired | The stored session is gone. The SDK forgets it, and the next message starts a new conversation. |
.conversationClosed | Your team closed the conversation. The next message starts a new one. |
.rateLimited | Too many requests. Wait retryAfterSeconds. |
.workspaceBusy | The workspace is at its plan's limit of open conversations. |
.identityRejected | The workspace requires a signed identity and the userHash is wrong. |
.attachmentRejected | An upload was refused (type, size or the agent's policy). |
.sendFailed | A message couldn't be delivered. |
.invalidResponse | The server answered something the protocol doesn't allow. |
.disconnected | The chat was closed while a call was in flight. |
.storage | The session store failed to read, write or delete. |
.server | Any 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 type | When |
|---|---|
| Customer Support | Every chat message. |
| Photos or Videos, Other User Content | Images and files the visitor attaches. |
| Name, Email Address, Phone Number | When your app passes them in GiovContact or the visitor fills in the pre-chat form. |
| User ID | When your app passes userId. |
| Device ID | The push token, when your app registers one. |
| Coarse Location | The 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:
| Parameter | Type | Default | Description |
|---|---|---|---|
tenantKey | String | required | Your workspace ID |
baseURL | String | https://api.giovchat.app | API base URL override |
contact | GiovContact? | nil | Signed-in user's details; also pre-fills the pre-chat form |
sessionKey | String? | nil | Separates stored conversations, for example per account |
sessionStore | (any GiovSessionStore)? | Keychain | Where the session lives between launches |
keepConnected | Bool | false | Stay connected while hidden to count unread replies |
locale | String? | Device language | Interface language |
strings | ((GiovChatStrings) -> GiovChatStrings)? | nil | Replace interface strings |
onClose | (() -> Void)? | nil | Shows a close button in the header |
onError | ((GiovChatError) -> Void)? | nil | Every failure, with a kind |
onConversationStart | ((String) -> Void)? | nil | Called with the new conversation's ID |
onConnectionStatusChange | ((GiovConnectionStatus) -> Void)? | nil | .connecting, .connected or .disconnected |
onUnreadCountChange | ((Int) -> Void)? | nil | Unread replies while hidden |
GiovChatView(controller:) shows a controller you own and never closes it:
| Parameter | Type | Default | Description |
|---|---|---|---|
controller | GiovChatController | required | The controller to show |
autoStart | Bool | true | Start the controller when the screen first appears. Safe for one that is already started |
forwardLifecycle | Bool | true | Forward foreground and background changes. Turn off when you use .giovLifecycle(_:) |
initialContact | GiovContact? | nil | Pre-fills the pre-chat form |
onClose | (() -> Void)? | nil | Shows a close button in the header |
Modifiers
| Modifier | Description |
|---|---|
.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.
| Member | Description |
|---|---|
state | The 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
| Type | Description |
|---|---|
GiovContact | userId, userHash, email, phone, firstName, lastName |
GiovChatStyle | primaryColor, theme, backgroundColor, headerColor, visitorBubbleColor, agentBubbleColor |
GiovThemeMode | .light, .dark, .auto |
GiovSessionStore | Protocol for your own session storage |
KeychainGiovSessionStore | The default store; takes service and accessGroup |
GiovUnread | count(tenantKey:) and observe(tenantKey:_:) for badges |
GiovPushPlatform, GiovPushEnvironment | .apns or .fcm; .sandbox or .production |
GiovChatError | The 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.