cometchat-ios-customization — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited cometchat-ios-customization (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Ground truth:CometChatUIKitSwift ~> 5(+CometChatCallsSDK ~> 5) — Pods/SPM.swiftinterface+ui-kit/ios. Official docs: https://www.cometchat.com/docs/ui-kit/ios/overview · Docs MCP:claude mcp add --transport http cometchat-docs https://www.cometchat.com/docs/mcp(or fetch the URL directly without MCP). Verify symbols against the installed package/source before relying on them.
This skill teaches advanced customization of CometChat iOS UI Kit — custom views, message templates, text formatters, event listeners, and request builder filters.
Replace the entire list item with a custom view:
let conversations = CometChatConversations()
conversations.set(listItemView: { (conversation: Conversation) -> UIView in
let customView = UIView()
customView.backgroundColor = .secondarySystemBackground
let nameLabel = UILabel()
nameLabel.font = .boldSystemFont(ofSize: 16)
if let user = conversation.conversationWith as? User {
nameLabel.text = user.name
} else if let group = conversation.conversationWith as? Group {
nameLabel.text = group.name
}
customView.addSubview(nameLabel)
nameLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
nameLabel.leadingAnchor.constraint(equalTo: customView.leadingAnchor, constant: 16),
nameLabel.centerYAnchor.constraint(equalTo: customView.centerYAnchor)
])
return customView
})conversations.set(leadingView: { (conversation: Conversation) -> UIView in
let avatarView = UIView()
avatarView.backgroundColor = .systemBlue
avatarView.layer.cornerRadius = 24
let initialsLabel = UILabel()
initialsLabel.textColor = .white
initialsLabel.font = .boldSystemFont(ofSize: 14)
initialsLabel.textAlignment = .center
if let user = conversation.conversationWith as? User {
initialsLabel.text = String(user.name?.prefix(2).uppercased() ?? "")
} else if let group = conversation.conversationWith as? Group {
initialsLabel.text = String(group.name?.prefix(2).uppercased() ?? "")
}
avatarView.addSubview(initialsLabel)
initialsLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
initialsLabel.centerXAnchor.constraint(equalTo: avatarView.centerXAnchor),
initialsLabel.centerYAnchor.constraint(equalTo: avatarView.centerYAnchor),
avatarView.widthAnchor.constraint(equalToConstant: 48),
avatarView.heightAnchor.constraint(equalToConstant: 48)
])
return avatarView
})conversations.set(titleView: { (conversation: Conversation) -> UIView in
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 8
let nameLabel = UILabel()
nameLabel.font = .boldSystemFont(ofSize: 16)
let verifiedBadge = UIImageView(image: UIImage(systemName: "checkmark.seal.fill"))
verifiedBadge.tintColor = .systemBlue
verifiedBadge.isHidden = true
if let user = conversation.conversationWith as? User {
nameLabel.text = user.name
// Show badge for verified users (custom metadata)
if let metadata = user.metadata, metadata["verified"] as? Bool == true {
verifiedBadge.isHidden = false
}
} else if let group = conversation.conversationWith as? Group {
nameLabel.text = group.name
}
stackView.addArrangedSubview(nameLabel)
stackView.addArrangedSubview(verifiedBadge)
return stackView
})conversations.set(subtitleView: { (conversation: Conversation) -> UIView in
let label = UILabel()
label.font = .systemFont(ofSize: 14)
label.textColor = .secondaryLabel
if let lastMessage = conversation.lastMessage as? TextMessage {
label.text = lastMessage.text
} else if let lastMessage = conversation.lastMessage as? MediaMessage {
switch lastMessage.messageType {
case .image:
label.text = "📷 Photo"
case .video:
label.text = "🎥 Video"
case .audio:
label.text = "🎵 Audio"
case .file:
label.text = "📎 File"
default:
label.text = "Attachment"
}
} else {
label.text = "No messages yet"
}
return label
})conversations.set(trailView: { (conversation: Conversation) -> UIView in
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .trailing
stackView.spacing = 4
// Time label
let timeLabel = UILabel()
timeLabel.font = .systemFont(ofSize: 12)
timeLabel.textColor = .tertiaryLabel
if let timestamp = conversation.lastMessage?.sentAt {
let date = Date(timeIntervalSince1970: TimeInterval(timestamp))
let formatter = DateFormatter()
formatter.dateFormat = "h:mm a"
timeLabel.text = formatter.string(from: date)
}
// Unread badge
let badge = UILabel()
badge.font = .boldSystemFont(ofSize: 12)
badge.textColor = .white
badge.backgroundColor = .systemBlue
badge.textAlignment = .center
badge.layer.cornerRadius = 10
badge.clipsToBounds = true
let unreadCount = conversation.unreadMessageCount
badge.isHidden = unreadCount == 0
badge.text = unreadCount > 99 ? "99+" : "\(unreadCount)"
stackView.addArrangedSubview(timeLabel)
stackView.addArrangedSubview(badge)
NSLayoutConstraint.activate([
badge.widthAnchor.constraint(greaterThanOrEqualToConstant: 20),
badge.heightAnchor.constraint(equalToConstant: 20)
])
return stackView
})conversations.set(options: { (conversation: Conversation?) -> [CometChatConversationOption] in
guard let conversation = conversation else { return [] }
var options: [CometChatConversationOption] = []
// CometChatConversationOption.init takes NO `id`; the click handler is the
// `onClick` initializer parameter (not a settable property) and its closure is
// (User?, Int, CometChatConversationOption, UIViewController?) -> Void.
// Pin conversation
let pinOption = CometChatConversationOption(
title: "Pin",
titleColor: nil,
icon: UIImage(systemName: "pin.fill"),
titleFont: nil,
iconTint: .white,
backgroundColor: .systemYellow,
onClick: { user, index, option, controller in
print("Pin conversation")
}
)
options.append(pinOption)
// Mute conversation
let muteOption = CometChatConversationOption(
title: "Mute",
titleColor: nil,
icon: UIImage(systemName: "bell.slash.fill"),
titleFont: nil,
iconTint: .white,
backgroundColor: .systemGray,
onClick: { user, index, option, controller in
print("Mute conversation")
}
)
options.append(muteOption)
// Delete conversation
let deleteOption = CometChatConversationOption(
title: "Delete",
titleColor: nil,
icon: UIImage(systemName: "trash.fill"),
titleFont: nil,
iconTint: .white,
backgroundColor: .systemRed,
onClick: { user, index, option, controller in
print("Delete conversation")
}
)
options.append(deleteOption)
return options
})conversations.add(options: { (conversation: Conversation?) -> [CometChatConversationOption] in
let archiveOption = CometChatConversationOption(
title: "Archive",
titleColor: nil,
icon: UIImage(systemName: "archivebox.fill"),
titleFont: nil,
iconTint: .white,
backgroundColor: .systemPurple,
onClick: { user, index, option, controller in
print("Archive conversation")
}
)
return [archiveOption]
})⚠️ `CometChatMessageTemplate` is a `struct` with ONE memberwise initializer — there is NOCometChatMessageTemplate(type:category:)convenience init, and the label order is `category` first, then `type`. The init requires ALL of these labels:category:type:contentView:bubbleView:headerView:footerView:bottomView:options:(the trailingstatusInfoView:/replyView:default tonil). Passnilfor any view closure you don't need — you cannot omit the label. Verified:CometChatMessageTemplate.swift:25-47.
let messageList = CometChatMessageList()
// Get default templates — the argument is required (pass nil for defaults).
// Starting from this list (not []) keeps every built-in bubble.
// CometChatUIKit.getDataSource() → CometChatUIKit.swift:287; getAllMessageTemplates(additionalConfiguration:) → DataSource.swift:63
var templates = CometChatUIKit.getDataSource().getAllMessageTemplates(additionalConfiguration: nil)
// Create custom template for a specific message type.
// ALL labels below are required by the only initializer (CometChatMessageTemplate.swift:25).
// `category` comes BEFORE `type`. Each view closure is (BaseMessage?, MessageBubbleAlignment, UIViewController?) -> UIView?.
let customTemplate = CometChatMessageTemplate(
category: "custom",
type: "custom_poll",
// Custom content view — the inner bubble content
contentView: { message, alignment, controller in
guard let customMessage = message as? CustomMessage,
let data = customMessage.customData else {
return UIView()
}
let pollView = PollBubbleView()
pollView.configure(with: data)
return pollView
},
// Custom bubble view (wraps content). alignment is MessageBubbleAlignment (.left/.right/.center — UIKitConstants.swift:85).
bubbleView: { message, alignment, controller in
let bubble = UIView()
bubble.backgroundColor = alignment == .right
? CometChatTheme.primaryColor.withAlphaComponent(0.1)
: UIColor.secondarySystemBackground
bubble.layer.cornerRadius = 12
return bubble
},
// Custom header view (above the bubble)
headerView: { message, alignment, controller in
let label = UILabel()
label.text = "📊 Poll"
label.font = .boldSystemFont(ofSize: 12)
label.textColor = .secondaryLabel
return label
},
// Custom footer view (below the bubble)
footerView: { message, alignment, controller in
let label = UILabel()
label.text = "Tap to vote"
label.font = .systemFont(ofSize: 11)
label.textColor = .tertiaryLabel
return label
},
// bottomView — pass nil if unused (label still required)
bottomView: nil,
// Custom options (long press menu). Closure is (BaseMessage?, Group?, UIViewController?) -> [CometChatMessageOption]?
options: { message, group, controller in
let viewResultsOption = CometChatMessageOption(
id: "view_results",
title: "View Results",
icon: UIImage(systemName: "chart.bar.fill"),
onItemClick: { message in
// Show poll results — closure receives the BaseMessage? only
}
)
return [viewResultsOption]
}
)
// APPEND to the default templates (do NOT start from []) so every built-in bubble survives.
templates.append(customTemplate)
// set(templates:) → CometChatMessageList + Properties.swift:54 (replaces the list, so we passed the merged array).
// add(templates:) (line 63) merges into the existing list if you prefer.
messageList.set(templates: templates)class PollBubbleView: UIView {
private let questionLabel = UILabel()
private let optionsStack = UIStackView()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupUI()
}
private func setupUI() {
questionLabel.font = .boldSystemFont(ofSize: 16)
questionLabel.numberOfLines = 0
optionsStack.axis = .vertical
optionsStack.spacing = 8
addSubview(questionLabel)
addSubview(optionsStack)
questionLabel.translatesAutoresizingMaskIntoConstraints = false
optionsStack.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
questionLabel.topAnchor.constraint(equalTo: topAnchor, constant: 12),
questionLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
questionLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
optionsStack.topAnchor.constraint(equalTo: questionLabel.bottomAnchor, constant: 12),
optionsStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
optionsStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
optionsStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12)
])
}
func configure(with data: [String: Any]) {
questionLabel.text = data["question"] as? String
optionsStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
if let options = data["options"] as? [String] {
for option in options {
let button = UIButton(type: .system)
button.setTitle(option, for: .normal)
button.backgroundColor = .systemGray5
button.layer.cornerRadius = 8
button.contentEdgeInsets = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 12)
optionsStack.addArrangedSubview(button)
}
}
}
}class HashtagFormatter: CometChatTextFormatter {
override func getRegex() -> String {
return "#[a-zA-Z0-9_]+"
}
override func prepareMessageString(
baseMessage: BaseMessage,
regexString: String,
alignment: MessageBubbleAlignment,
formattingType: FormattingType
) -> NSAttributedString {
guard let textMessage = baseMessage as? TextMessage,
let text = textMessage.text else {
return NSAttributedString()
}
let attributedString = NSMutableAttributedString(string: text)
// Default text attributes
let defaultAttributes: [NSAttributedString.Key: Any] = [
.font: CometChatTypography.Body.regular,
.foregroundColor: alignment == .right ? UIColor.white : UIColor.label
]
attributedString.addAttributes(defaultAttributes, range: NSRange(location: 0, length: text.count))
// Highlight hashtags
let regex = try? NSRegularExpression(pattern: regexString, options: [])
let matches = regex?.matches(in: text, options: [], range: NSRange(location: 0, length: text.count)) ?? []
for match in matches {
let hashtagAttributes: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.systemBlue,
.font: CometChatTypography.Body.medium
]
attributedString.addAttributes(hashtagAttributes, range: match.range)
}
return attributedString
}
override func onTextTapped(
baseMessage: BaseMessage,
tappedText: String,
controller: UIViewController?
) {
// Handle hashtag tap
print("Hashtag tapped: \(tappedText)")
// Navigate to hashtag search, etc.
}
}
// Usage
let messageList = CometChatMessageList()
messageList.textFormatter = [
CometChatMentionsFormatter(), // Built-in mentions
HashtagFormatter() // Custom hashtags
]class CustomURLFormatter: CometChatTextFormatter {
override func getRegex() -> String {
return "https?://[^\\s]+"
}
override func prepareMessageString(
baseMessage: BaseMessage,
regexString: String,
alignment: MessageBubbleAlignment,
formattingType: FormattingType
) -> NSAttributedString {
guard let textMessage = baseMessage as? TextMessage,
let text = textMessage.text else {
return NSAttributedString()
}
let attributedString = NSMutableAttributedString(string: text)
let regex = try? NSRegularExpression(pattern: regexString, options: [])
let matches = regex?.matches(in: text, options: [], range: NSRange(location: 0, length: text.count)) ?? []
for match in matches {
let urlAttributes: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.systemBlue,
.underlineStyle: NSUnderlineStyle.single.rawValue
]
attributedString.addAttributes(urlAttributes, range: match.range)
}
return attributedString
}
override func onTextTapped(
baseMessage: BaseMessage,
tappedText: String,
controller: UIViewController?
) {
if let url = URL(string: tappedText) {
UIApplication.shared.open(url)
}
}
}import CometChatSDK
class MessageEventListener: CometChatMessageEventListener {
func ccMessageSent(message: BaseMessage, status: MessageStatus) {
switch status {
case .inProgress:
print("Message sending...")
case .success:
print("Message sent successfully")
case .error:
print("Message failed to send")
}
}
func ccMessageEdited(message: BaseMessage, status: MessageStatus) {
print("Message edited: \(message.id)")
}
func ccMessageDeleted(message: BaseMessage) {
print("Message deleted: \(message.id)")
}
func ccMessageRead(message: BaseMessage) {
print("Message read: \(message.id)")
}
func ccLiveReaction(reaction: TransientMessage) {
print("Live reaction received")
}
}
// Register listener
let listener = MessageEventListener()
CometChatMessageEvents.addListener("message-listener", listener)
// Remove listener when done
CometChatMessageEvents.removeListener("message-listener")class UserEventListener: CometChatUserEventListener {
func ccUserBlocked(user: User) {
print("User blocked: \(user.name ?? "")")
}
func ccUserUnblocked(user: User) {
print("User unblocked: \(user.name ?? "")")
}
}
// Register
CometChatUserEvents.addListener("user-listener", UserEventListener())class GroupEventListener: CometChatGroupEventListener {
func ccGroupCreated(group: Group) {
print("Group created: \(group.name ?? "")")
}
func ccGroupDeleted(group: Group) {
print("Group deleted: \(group.name ?? "")")
}
func ccGroupMemberAdded(messages: [ActionMessage], usersAdded: [User], groupAddedIn: Group, addedBy: User) {
print("Members added to group")
}
func ccGroupMemberKicked(action: ActionMessage, kickedUser: User, kickedBy: User, kickedFrom: Group) {
print("Member kicked from group")
}
func ccGroupMemberBanned(action: ActionMessage, bannedUser: User, bannedBy: User, bannedFrom: Group) {
print("Member banned from group")
}
func ccOwnershipChanged(group: Group, newOwner: GroupMember) {
print("Group ownership changed")
}
}
// Register
CometChatGroupEvents.addListener("group-listener", GroupEventListener())class CallEventListener: CometChatCallEventListener {
func ccCallAccepted(call: Call) {
print("Call accepted")
}
func ccCallRejected(call: Call) {
print("Call rejected")
}
func ccCallEnded(call: Call) {
print("Call ended")
}
func ccOutgoingCall(call: Call) {
print("Outgoing call initiated")
}
func onCallInitiated(call: Call) { // CometChatCallEventListener has onCallInitiated, not ccCallInitiated
print("Call initiated")
}
}
// Register
CometChatCallEvents.addListener("call-listener", CallEventListener())let conversations = CometChatConversations()
// Only show user conversations (no groups)
// ConversationRequestBuilder exposes setConversationType(conversationType:), not set(conversationType:)
conversations.set(conversationsRequestBuilder: ConversationRequest.ConversationRequestBuilder()
.setConversationType(conversationType: .user)
.set(limit: 30)
)Note:ConversationRequestBuilderhas no tags filter — its only methods areset(limit:),setConversationType(conversationType:), andbuild(). To filter by tags, fetch and filter the resulting conversations client-side.
let messageList = CometChatMessageList()
// Only show text and image messages
messageList.set(messagesRequestBuilder: MessagesRequest.MessageRequestBuilder()
.set(uid: user.uid ?? "")
.set(types: [MessageTypeConstants.text, MessageTypeConstants.image])
.set(limit: 30)
)
// Only show messages from a specific time range
let startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date())!
messageList.set(messagesRequestBuilder: MessagesRequest.MessageRequestBuilder()
.set(uid: user.uid ?? "")
.set(timeStamp: Int(startDate.timeIntervalSince1970))
.set(limit: 50)
)
// Hide deleted messages
messageList.set(messagesRequestBuilder: MessagesRequest.MessageRequestBuilder()
.set(uid: user.uid ?? "")
.hideDeletedMessages(hide: true)
.set(limit: 30)
)let users = CometChatUsers()
// Only show users with specific role
users.set(usersRequestBuilder: UsersRequest.UsersRequestBuilder()
.set(roles: ["admin", "moderator"])
.set(limit: 30)
)
// Only show friends
users.set(usersRequestBuilder: UsersRequest.UsersRequestBuilder()
.friendsOnly(true)
.set(limit: 30)
)
// Search users
users.set(usersRequestBuilder: UsersRequest.UsersRequestBuilder()
.set(searchKeyword: "john")
.set(limit: 30)
)let groups = CometChatGroups()
// Only show joined groups
groups.set(groupsRequestBuilder: GroupsRequest.GroupsRequestBuilder()
.set(joinedOnly: true)
.set(limit: 30)
)
// Only show public groups
groups.set(groupsRequestBuilder: GroupsRequest.GroupsRequestBuilder()
.set(groupType: .public)
.set(limit: 30)
)
// Search groups
groups.set(groupsRequestBuilder: GroupsRequest.GroupsRequestBuilder()
.set(searchKeyWord: "team")
.set(limit: 30)
)⚠️ REGISTRATION IS THE CATCH.DataSourceDecoratoris public (DataSourceDecorator.swift:12), but the only mechanism that installs a decorator —ChatConfigurator.enable { ... }— is internal to CometChatUIKitSwift.ChatConfiguratorshows up in the public module interface (uikit.swiftinterface:8591) as a class with no public members (onlydeinit); itsenable/getDataSourcestatics are NOT public. The kit's own extensions (Polls/Stickers/etc.) callChatConfigurator.enablefrom inside the module viaExtensionDataSource.addExtension(), but app code cannot call it. So a hand-rolledDataSourceDecoratorsubclass compiles but has no public way to be registered in this kit version.
>
Use the public component-level setters instead for the common cases: - Attachment options →CometChatMessageComposer.set(attachmentOptions:)(§7a below). Public. VerifiedCometChatMessageComposer + Properties.swift:44. - Custom message type / override a built-in bubble / per-type long-press options → build aCometChatMessageTemplateand apply it withCometChatMessageList.set(templates:)/add(templates:)(§3 and §7b). Public. VerifiedCometChatMessageList + Properties.swift:54,63. - Message long-press options: there is NOset(options:)/add(options:)for message options onCometChatMessageList(only Conversations/Users/Groups/GroupMembers/CallLogs expose those). Message options are customized only through a template's `options` closure (§7b).
>
The decorator subclass below is documented for completeness / for code that lives inside a kit fork — its method signatures are the source of truth for the protocol — but prefer the component setters above for app integration.
set(attachmentOptions:) REPLACES the attachment list with whatever the closure returns — there is no public "super" list at the composer level, so to keep the built-in photo/video/file actions you must construct them yourself or accept that only your returned actions show. The closure is (User?, Group?, UIViewController?) -> [CometChatMessageComposerAction].
let composer = CometChatMessageComposer()
composer.set(attachmentOptions: { user, group, controller in
// NOTE: returning ONLY this array hides the default attachments.
// Re-add any defaults you still want here.
let location = CometChatMessageComposerAction(
id: "location",
text: "Send Location",
startIcon: UIImage(systemName: "location.fill"),
startIconTint: CometChatTheme.iconColorPrimary,
textColor: CometChatTheme.textColorPrimary,
textFont: CometChatTypography.Body.regular,
onActionClick: { /* present location picker, then send (see §8) */ }
)
return [location]
})To replace the content view of a built-in type (e.g. text) — or attach a custom long-press option to it — start from the default template for that type, rebuild it with your closures, and apply via add(templates:). CometChatMessageTemplate is a struct, so you create a NEW one (you cannot mutate a let from getAllMessageTemplates); copy category/type from the default and supply the closures you want.
let messageList = CometChatMessageList()
// Built-in text type identifiers (CometChatSDK constants):
// category = "message", type = "text" (matches MessagesDataSource.getTextMessageTemplate → "message"/"text")
let overriddenText = CometChatMessageTemplate(
category: "message",
type: "text",
contentView: { message, alignment, controller in // ← override the bubble content
let label = UILabel()
label.numberOfLines = 0
label.text = (message as? TextMessage)?.text
label.textColor = alignment == .right ? .white : .label
return label
},
bubbleView: nil, headerView: nil, footerView: nil, bottomView: nil,
options: { message, group, controller in // ← add a long-press option to text bubbles
let star = CometChatMessageOption(
id: "star",
title: "Star",
icon: UIImage(systemName: "star.fill"),
onItemClick: { msg in print("starred \(msg?.id ?? 0)") }
)
return [star]
}
)
// add(templates:) MERGES by "category_type" key — it overwrites the default text template
// with ours and leaves every OTHER built-in bubble intact (CometChatMessageList + Properties.swift:63).
messageList.add(templates: [overriddenText])class CustomDataSource: DataSourceDecorator {
// ⚠️ Signature MUST match the protocol exactly — it takes `additionalConfiguration:`.
// Omitting the parameter does NOT override the kit method; your templates never appear.
override func getAllMessageTemplates(
additionalConfiguration: AdditionalConfiguration?
) -> [CometChatMessageTemplate] {
// Call super to KEEP every default template (text/image/video/file/…).
var templates = super.getAllMessageTemplates(additionalConfiguration: additionalConfiguration)
// Append — never replace the array, or all default bubbles disappear.
// ⚠️ Same init rule as §3: `category` first, then `type`, and ALL view-closure labels
// are required (pass nil). NO `CometChatMessageTemplate(type:category:)` shorthand exists.
let customTemplate = CometChatMessageTemplate(
category: "custom",
type: "custom_type",
contentView: { message, alignment, controller in UIView() },
bubbleView: nil, headerView: nil, footerView: nil, bottomView: nil, options: nil
)
templates.append(customTemplate)
return templates
}
// Add a composer attachment option WITHOUT wiping the defaults (photo/video/file/…).
// Same rule: start from super's list, then append.
// ⚠️ `additionalConfiguration: AdditionalConfiguration` is REQUIRED with NO default value
// in the protocol (DataSource.swift:74) and the decorator (DataSourceDecorator.swift:24).
// Do NOT add `= AdditionalConfiguration()` here — match the signature exactly.
override func getAttachmentOptions(
controller: UIViewController,
user: User?,
group: Group?,
id: [String: Any]?,
additionalConfiguration: AdditionalConfiguration
) -> [CometChatMessageComposerAction]? {
var options = super.getAttachmentOptions(
controller: controller, user: user, group: group,
id: id, additionalConfiguration: additionalConfiguration
) ?? []
// ⚠️ There is NO zero-arg `CometChatMessageComposerAction()` init. The only
// initializer requires id/text/startIcon/startIconTint/textColor/textFont
// (endIcon, tints, colors, border, onActionClick all default).
// Verified: CometChatMessageComposerAction.swift:28-56.
let location = CometChatMessageComposerAction(
id: "location",
text: "Send Location",
startIcon: UIImage(systemName: "location.fill"),
startIconTint: CometChatTheme.iconColorPrimary,
textColor: CometChatTheme.textColorPrimary,
textFont: CometChatTypography.Body.regular,
onActionClick: { /* present location picker, then send (see §8) */ }
)
options.append(location)
return options
}
override func getAllMessageTypes() -> [String]? {
var types = super.getAllMessageTypes() ?? []
types.append("custom_type")
return types
}
override func getAllMessageCategories() -> [String]? {
var categories = super.getAllMessageCategories() ?? []
categories.append("custom")
return categories
}
// ⚠️ `additionalConfiguration: AdditionalConfiguration` is REQUIRED (no default) on this
// method too — omitting it means you DON'T override the protocol method and your option
// never appears. Verified: DataSource.swift:67, DataSourceDecorator.swift:36.
override func getMessageOptions(
loggedInUser: User,
messageObject: BaseMessage,
controller: UIViewController?,
group: Group?,
additionalConfiguration: AdditionalConfiguration
) -> [CometChatMessageOption]? {
var options = super.getMessageOptions(
loggedInUser: loggedInUser,
messageObject: messageObject,
controller: controller,
group: group,
additionalConfiguration: additionalConfiguration
) ?? []
// Add custom option. ⚠️ The click handler is `onItemClick: (BaseMessage?) -> Void`
// — there is NO `onClick` property and NO 4-arg closure. Set it via the initializer
// (it's a struct; `onItemClick` is the only callback). Verified: CometChatMessageOption.swift:30,33-45.
let customOption = CometChatMessageOption(
id: "custom_action",
title: "Custom Action",
icon: UIImage(systemName: "star.fill"),
onItemClick: { message in
print("Custom action triggered for \(message?.id ?? 0)")
}
)
options.append(customOption)
return options
}
}
// ⚠️ NO PUBLIC REGISTRATION. The only installer, `ChatConfigurator.enable { dataSource in
// return CustomDataSource(dataSource: dataSource) }`, is INTERNAL to CometChatUIKitSwift
// and does NOT compile from app code (ChatConfigurator exposes no public members —
// uikit.swiftinterface:8591). Subclassing `ExtensionDataSource` does not help either: its
// base `addExtension()` is empty and the only way to make it do anything is to call the same
// internal `ChatConfigurator.enable` from inside it.
// → For app integration, use the public component setters in §7a / §7b instead.
// (This decorator block stands only as the authoritative signature reference, and works
// for code compiled INSIDE a kit fork.)A custom contentView/template only renders messages of that type; you still have to send one. Use `CometChatUIKit.sendCustomMessage(message:)`, not CometChat.sendCustomMessage(...). The UIKit wrapper stamps muid + sender and fires the ccMessageSent event, which is what makes the new bubble append to the open list. The raw SDK call skips those, giving the two classic failures: a "cannot determine recipient" state and the realtime message replacing an existing bubble instead of appending (no muid for dedup).
// Build with the receiver set — receiverUid + .user (or a guid + .group)
let custom = CustomMessage(
receiverUid: selectedUser.uid,
receiverType: .user, // CometChat.ReceiverType
customData: ["question": "Lunch?", "options": ["Yes", "No"]],
type: "custom_poll" // MUST match the template/contentView type
)
// ✅ kit path — sets muid + sender, emits ccMessageSent → list appends
CometChatUIKit.sendCustomMessage(message: custom)
// ❌ raw SDK — no muid/sender/event → recipient error + realtime replace
// CometChat.sendCustomMessage(message: custom, onSuccess: { _ in }, onError: { _ in })Verified: CustomMessage(receiverUid:receiverType:customData:type:) (CometChatSDK) and static CometChatUIKit.sendCustomMessage(message:) (CometChatUIKitSwift).
Sound is a customization sub-dimension. The UI Kit plays incoming/outgoing message + call sounds via CometChatSoundManager — mute it, swap custom audio, or play a specific sound. The full API + recipe lives in `cometchat-ios-theming` (Sound section). Verify the access path against the installed kit before relying on it.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.