{"id":"c4a03bc6-9d1e-4297-8323-e702ad565aeb","shortId":"4pVMmR","kind":"skill","title":"cometchat-ios-customization","tagline":"Customize CometChat iOS UI Kit beyond theming — custom views, message templates, text formatters, and event handling.","description":"## Purpose\n\nThis skill teaches advanced customization of CometChat iOS UI Kit — custom views, message templates, text formatters, event listeners, and request builder filters.\n\n---\n\n## 1. Custom Views in CometChatConversations\n\n### Custom List Item View\n\nReplace the entire list item with a custom view:\n\n```swift\nlet conversations = CometChatConversations()\n\nconversations.set(listItemView: { (conversation: Conversation) -> UIView in\n    let customView = UIView()\n    customView.backgroundColor = .secondarySystemBackground\n    \n    let nameLabel = UILabel()\n    nameLabel.font = .boldSystemFont(ofSize: 16)\n    \n    if let user = conversation.conversationWith as? User {\n        nameLabel.text = user.name\n    } else if let group = conversation.conversationWith as? Group {\n        nameLabel.text = group.name\n    }\n    \n    customView.addSubview(nameLabel)\n    nameLabel.translatesAutoresizingMaskIntoConstraints = false\n    NSLayoutConstraint.activate([\n        nameLabel.leadingAnchor.constraint(equalTo: customView.leadingAnchor, constant: 16),\n        nameLabel.centerYAnchor.constraint(equalTo: customView.centerYAnchor)\n    ])\n    \n    return customView\n})\n```\n\n### Custom Leading View (Avatar Area)\n\n```swift\nconversations.set(leadingView: { (conversation: Conversation) -> UIView in\n    let avatarView = UIView()\n    avatarView.backgroundColor = .systemBlue\n    avatarView.layer.cornerRadius = 24\n    \n    let initialsLabel = UILabel()\n    initialsLabel.textColor = .white\n    initialsLabel.font = .boldSystemFont(ofSize: 14)\n    initialsLabel.textAlignment = .center\n    \n    if let user = conversation.conversationWith as? User {\n        initialsLabel.text = String(user.name?.prefix(2).uppercased() ?? \"\")\n    } else if let group = conversation.conversationWith as? Group {\n        initialsLabel.text = String(group.name?.prefix(2).uppercased() ?? \"\")\n    }\n    \n    avatarView.addSubview(initialsLabel)\n    initialsLabel.translatesAutoresizingMaskIntoConstraints = false\n    NSLayoutConstraint.activate([\n        initialsLabel.centerXAnchor.constraint(equalTo: avatarView.centerXAnchor),\n        initialsLabel.centerYAnchor.constraint(equalTo: avatarView.centerYAnchor),\n        avatarView.widthAnchor.constraint(equalToConstant: 48),\n        avatarView.heightAnchor.constraint(equalToConstant: 48)\n    ])\n    \n    return avatarView\n})\n```\n\n### Custom Title View\n\n```swift\nconversations.set(titleView: { (conversation: Conversation) -> UIView in\n    let stackView = UIStackView()\n    stackView.axis = .horizontal\n    stackView.spacing = 8\n    \n    let nameLabel = UILabel()\n    nameLabel.font = .boldSystemFont(ofSize: 16)\n    \n    let verifiedBadge = UIImageView(image: UIImage(systemName: \"checkmark.seal.fill\"))\n    verifiedBadge.tintColor = .systemBlue\n    verifiedBadge.isHidden = true\n    \n    if let user = conversation.conversationWith as? User {\n        nameLabel.text = user.name\n        // Show badge for verified users (custom metadata)\n        if let metadata = user.metadata, metadata[\"verified\"] as? Bool == true {\n            verifiedBadge.isHidden = false\n        }\n    } else if let group = conversation.conversationWith as? Group {\n        nameLabel.text = group.name\n    }\n    \n    stackView.addArrangedSubview(nameLabel)\n    stackView.addArrangedSubview(verifiedBadge)\n    \n    return stackView\n})\n```\n\n### Custom Subtitle View\n\n```swift\nconversations.set(subtitleView: { (conversation: Conversation) -> UIView in\n    let label = UILabel()\n    label.font = .systemFont(ofSize: 14)\n    label.textColor = .secondaryLabel\n    \n    if let lastMessage = conversation.lastMessage as? TextMessage {\n        label.text = lastMessage.text\n    } else if let lastMessage = conversation.lastMessage as? MediaMessage {\n        switch lastMessage.messageType {\n        case .image:\n            label.text = \"📷 Photo\"\n        case .video:\n            label.text = \"🎥 Video\"\n        case .audio:\n            label.text = \"🎵 Audio\"\n        case .file:\n            label.text = \"📎 File\"\n        default:\n            label.text = \"Attachment\"\n        }\n    } else {\n        label.text = \"No messages yet\"\n    }\n    \n    return label\n})\n```\n\n### Custom Tail View\n\n```swift\nconversations.set(trailView: { (conversation: Conversation) -> UIView in\n    let stackView = UIStackView()\n    stackView.axis = .vertical\n    stackView.alignment = .trailing\n    stackView.spacing = 4\n    \n    // Time label\n    let timeLabel = UILabel()\n    timeLabel.font = .systemFont(ofSize: 12)\n    timeLabel.textColor = .tertiaryLabel\n    \n    if let timestamp = conversation.lastMessage?.sentAt {\n        let date = Date(timeIntervalSince1970: TimeInterval(timestamp))\n        let formatter = DateFormatter()\n        formatter.dateFormat = \"h:mm a\"\n        timeLabel.text = formatter.string(from: date)\n    }\n    \n    // Unread badge\n    let badge = UILabel()\n    badge.font = .boldSystemFont(ofSize: 12)\n    badge.textColor = .white\n    badge.backgroundColor = .systemBlue\n    badge.textAlignment = .center\n    badge.layer.cornerRadius = 10\n    badge.clipsToBounds = true\n    \n    let unreadCount = conversation.unreadMessageCount\n    badge.isHidden = unreadCount == 0\n    badge.text = unreadCount > 99 ? \"99+\" : \"\\(unreadCount)\"\n    \n    stackView.addArrangedSubview(timeLabel)\n    stackView.addArrangedSubview(badge)\n    \n    NSLayoutConstraint.activate([\n        badge.widthAnchor.constraint(greaterThanOrEqualToConstant: 20),\n        badge.heightAnchor.constraint(equalToConstant: 20)\n    ])\n    \n    return stackView\n})\n```\n\n---\n\n## 2. Custom Swipe Actions\n\n### Custom Options\n\n```swift\nconversations.set(options: { (conversation: Conversation?) -> [CometChatConversationOption] in\n    guard let conversation = conversation else { return [] }\n    var options: [CometChatConversationOption] = []\n    \n    // Pin conversation\n    let pinOption = CometChatConversationOption(\n        id: \"pin\",\n        title: \"Pin\",\n        icon: UIImage(systemName: \"pin.fill\"),\n        backgroundColor: .systemYellow,\n        iconTint: .white\n    )\n    pinOption.onClick = { _, _, _, _ in\n        // Handle pin action\n        print(\"Pin conversation: \\(conversation.conversationId ?? \"\")\")\n    }\n    options.append(pinOption)\n    \n    // Mute conversation\n    let muteOption = CometChatConversationOption(\n        id: \"mute\",\n        title: \"Mute\",\n        icon: UIImage(systemName: \"bell.slash.fill\"),\n        backgroundColor: .systemGray,\n        iconTint: .white\n    )\n    muteOption.onClick = { _, _, _, _ in\n        // Handle mute action\n        print(\"Mute conversation: \\(conversation.conversationId ?? \"\")\")\n    }\n    options.append(muteOption)\n    \n    // Delete conversation\n    let deleteOption = CometChatConversationOption(\n        id: \"delete\",\n        title: \"Delete\",\n        icon: UIImage(systemName: \"trash.fill\"),\n        backgroundColor: .systemRed,\n        iconTint: .white\n    )\n    deleteOption.onClick = { _, _, _, _ in\n        // Handle delete action\n        print(\"Delete conversation: \\(conversation.conversationId ?? \"\")\")\n    }\n    options.append(deleteOption)\n    \n    return options\n})\n```\n\n### Add Options (Keep Default + Add Custom)\n\n```swift\nconversations.add(options: { (conversation: Conversation?) -> [CometChatConversationOption] in\n    let archiveOption = CometChatConversationOption(\n        id: \"archive\",\n        title: \"Archive\",\n        icon: UIImage(systemName: \"archivebox.fill\"),\n        backgroundColor: .systemPurple,\n        iconTint: .white\n    )\n    archiveOption.onClick = { _, _, _, _ in\n        print(\"Archive conversation\")\n    }\n    \n    return [archiveOption]\n})\n```\n\n---\n\n## 3. Custom Message Templates\n\n### Creating a Custom Message Template\n\n```swift\nlet messageList = CometChatMessageList()\n\n// Get default templates\nvar templates = CometChatUIKit.getDataSource().getAllMessageTemplates()\n\n// Create custom template for a specific message type\nlet customTemplate = CometChatMessageTemplate(\n    type: \"custom_poll\",\n    category: \"custom\"\n)\n\n// Custom content view\ncustomTemplate.contentView = { message, alignment, controller in\n    guard let customMessage = message as? CustomMessage,\n          let data = customMessage.customData else {\n        return UIView()\n    }\n    \n    let pollView = PollBubbleView()\n    pollView.configure(with: data)\n    return pollView\n}\n\n// Custom header view (above the bubble)\ncustomTemplate.headerView = { message, alignment, controller in\n    let label = UILabel()\n    label.text = \"📊 Poll\"\n    label.font = .boldSystemFont(ofSize: 12)\n    label.textColor = .secondaryLabel\n    return label\n}\n\n// Custom footer view (below the bubble)\ncustomTemplate.footerView = { message, alignment, controller in\n    let label = UILabel()\n    label.text = \"Tap to vote\"\n    label.font = .systemFont(ofSize: 11)\n    label.textColor = .tertiaryLabel\n    return label\n}\n\n// Custom bubble view (wraps content)\ncustomTemplate.bubbleView = { message, alignment, controller in\n    let bubble = UIView()\n    bubble.backgroundColor = alignment == .right \n        ? CometChatTheme.primaryColor.withAlphaComponent(0.1)\n        : UIColor.secondarySystemBackground\n    bubble.layer.cornerRadius = 12\n    return bubble\n}\n\n// Custom options (long press menu)\ncustomTemplate.options = { message, group, controller in\n    let viewResultsOption = CometChatMessageOption(\n        id: \"view_results\",\n        title: \"View Results\",\n        icon: UIImage(systemName: \"chart.bar.fill\")\n    )\n    viewResultsOption.onClick = { _, _, _, _ in\n        // Show poll results\n    }\n    \n    return [viewResultsOption]\n}\n\ntemplates.append(customTemplate)\nmessageList.set(templates: templates)\n```\n\n### Custom Poll Bubble View Example\n\n```swift\nclass PollBubbleView: UIView {\n    \n    private let questionLabel = UILabel()\n    private let optionsStack = UIStackView()\n    \n    override init(frame: CGRect) {\n        super.init(frame: frame)\n        setupUI()\n    }\n    \n    required init?(coder: NSCoder) {\n        super.init(coder: coder)\n        setupUI()\n    }\n    \n    private func setupUI() {\n        questionLabel.font = .boldSystemFont(ofSize: 16)\n        questionLabel.numberOfLines = 0\n        \n        optionsStack.axis = .vertical\n        optionsStack.spacing = 8\n        \n        addSubview(questionLabel)\n        addSubview(optionsStack)\n        \n        questionLabel.translatesAutoresizingMaskIntoConstraints = false\n        optionsStack.translatesAutoresizingMaskIntoConstraints = false\n        \n        NSLayoutConstraint.activate([\n            questionLabel.topAnchor.constraint(equalTo: topAnchor, constant: 12),\n            questionLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),\n            questionLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),\n            \n            optionsStack.topAnchor.constraint(equalTo: questionLabel.bottomAnchor, constant: 12),\n            optionsStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),\n            optionsStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),\n            optionsStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12)\n        ])\n    }\n    \n    func configure(with data: [String: Any]) {\n        questionLabel.text = data[\"question\"] as? String\n        \n        optionsStack.arrangedSubviews.forEach { $0.removeFromSuperview() }\n        \n        if let options = data[\"options\"] as? [String] {\n            for option in options {\n                let button = UIButton(type: .system)\n                button.setTitle(option, for: .normal)\n                button.backgroundColor = .systemGray5\n                button.layer.cornerRadius = 8\n                button.contentEdgeInsets = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 12)\n                optionsStack.addArrangedSubview(button)\n            }\n        }\n    }\n}\n```\n\n---\n\n## 4. Text Formatters\n\n### Creating a Custom Text Formatter\n\n```swift\nclass HashtagFormatter: CometChatTextFormatter {\n    \n    override func getRegex() -> String {\n        return \"#[a-zA-Z0-9_]+\"\n    }\n    \n    override func prepareMessageString(\n        baseMessage: BaseMessage,\n        regexString: String,\n        alignment: MessageBubbleAlignment,\n        formattingType: FormattingType\n    ) -> NSAttributedString {\n        \n        guard let textMessage = baseMessage as? TextMessage,\n              let text = textMessage.text else {\n            return NSAttributedString()\n        }\n        \n        let attributedString = NSMutableAttributedString(string: text)\n        \n        // Default text attributes\n        let defaultAttributes: [NSAttributedString.Key: Any] = [\n            .font: CometChatTypography.Body.regular,\n            .foregroundColor: alignment == .right ? UIColor.white : UIColor.label\n        ]\n        attributedString.addAttributes(defaultAttributes, range: NSRange(location: 0, length: text.count))\n        \n        // Highlight hashtags\n        let regex = try? NSRegularExpression(pattern: regexString, options: [])\n        let matches = regex?.matches(in: text, options: [], range: NSRange(location: 0, length: text.count)) ?? []\n        \n        for match in matches {\n            let hashtagAttributes: [NSAttributedString.Key: Any] = [\n                .foregroundColor: UIColor.systemBlue,\n                .font: CometChatTypography.Body.medium\n            ]\n            attributedString.addAttributes(hashtagAttributes, range: match.range)\n        }\n        \n        return attributedString\n    }\n    \n    override func onTextTapped(\n        baseMessage: BaseMessage,\n        tappedText: String,\n        controller: UIViewController?\n    ) {\n        // Handle hashtag tap\n        print(\"Hashtag tapped: \\(tappedText)\")\n        // Navigate to hashtag search, etc.\n    }\n}\n\n// Usage\nlet messageList = CometChatMessageList()\nmessageList.textFormatter = [\n    CometChatMentionsFormatter(),  // Built-in mentions\n    HashtagFormatter()              // Custom hashtags\n]\n```\n\n### URL Formatter with Custom Preview\n\n```swift\nclass CustomURLFormatter: CometChatTextFormatter {\n    \n    override func getRegex() -> String {\n        return \"https?://[^\\\\s]+\"\n    }\n    \n    override func prepareMessageString(\n        baseMessage: BaseMessage,\n        regexString: String,\n        alignment: MessageBubbleAlignment,\n        formattingType: FormattingType\n    ) -> NSAttributedString {\n        \n        guard let textMessage = baseMessage as? TextMessage,\n              let text = textMessage.text else {\n            return NSAttributedString()\n        }\n        \n        let attributedString = NSMutableAttributedString(string: text)\n        \n        let regex = try? NSRegularExpression(pattern: regexString, options: [])\n        let matches = regex?.matches(in: text, options: [], range: NSRange(location: 0, length: text.count)) ?? []\n        \n        for match in matches {\n            let urlAttributes: [NSAttributedString.Key: Any] = [\n                .foregroundColor: UIColor.systemBlue,\n                .underlineStyle: NSUnderlineStyle.single.rawValue\n            ]\n            attributedString.addAttributes(urlAttributes, range: match.range)\n        }\n        \n        return attributedString\n    }\n    \n    override func onTextTapped(\n        baseMessage: BaseMessage,\n        tappedText: String,\n        controller: UIViewController?\n    ) {\n        if let url = URL(string: tappedText) {\n            UIApplication.shared.open(url)\n        }\n    }\n}\n```\n\n---\n\n## 5. Event Listeners\n\n### Message Events\n\n```swift\nimport CometChatSDK\n\nclass MessageEventListener: CometChatMessageEventListener {\n    \n    func ccMessageSent(message: BaseMessage, status: MessageStatus) {\n        switch status {\n        case .inProgress:\n            print(\"Message sending...\")\n        case .success:\n            print(\"Message sent successfully\")\n        case .error:\n            print(\"Message failed to send\")\n        }\n    }\n    \n    func ccMessageEdited(message: BaseMessage, status: MessageStatus) {\n        print(\"Message edited: \\(message.id)\")\n    }\n    \n    func ccMessageDeleted(message: BaseMessage) {\n        print(\"Message deleted: \\(message.id)\")\n    }\n    \n    func ccMessageRead(message: BaseMessage) {\n        print(\"Message read: \\(message.id)\")\n    }\n    \n    func ccLiveReaction(reaction: TransientMessage) {\n        print(\"Live reaction received\")\n    }\n}\n\n// Register listener\nlet listener = MessageEventListener()\nCometChatMessageEvents.addListener(\"message-listener\", listener)\n\n// Remove listener when done\nCometChatMessageEvents.removeListener(\"message-listener\")\n```\n\n### User Events\n\n```swift\nclass UserEventListener: CometChatUserEventListener {\n    \n    func ccUserBlocked(user: User) {\n        print(\"User blocked: \\(user.name ?? \"\")\")\n    }\n    \n    func ccUserUnblocked(user: User) {\n        print(\"User unblocked: \\(user.name ?? \"\")\")\n    }\n}\n\n// Register\nCometChatUserEvents.addListener(\"user-listener\", UserEventListener())\n```\n\n### Group Events\n\n```swift\nclass GroupEventListener: CometChatGroupEventListener {\n    \n    func ccGroupCreated(group: Group) {\n        print(\"Group created: \\(group.name ?? \"\")\")\n    }\n    \n    func ccGroupDeleted(group: Group) {\n        print(\"Group deleted: \\(group.name ?? \"\")\")\n    }\n    \n    func ccGroupMemberAdded(messages: [ActionMessage], usersAdded: [User], groupAddedIn: Group, addedBy: User) {\n        print(\"Members added to group\")\n    }\n    \n    func ccGroupMemberKicked(message: ActionMessage, kickedUser: User, kickedBy: User, kickedFrom: Group) {\n        print(\"Member kicked from group\")\n    }\n    \n    func ccGroupMemberBanned(message: ActionMessage, bannedUser: User, bannedBy: User, bannedFrom: Group) {\n        print(\"Member banned from group\")\n    }\n    \n    func ccOwnershipChanged(group: Group, newOwner: GroupMember) {\n        print(\"Group ownership changed\")\n    }\n}\n\n// Register\nCometChatGroupEvents.addListener(\"group-listener\", GroupEventListener())\n```\n\n### Call Events\n\n```swift\nclass CallEventListener: CometChatCallEventListener {\n    \n    func ccCallAccepted(call: Call) {\n        print(\"Call accepted\")\n    }\n    \n    func ccCallRejected(call: Call) {\n        print(\"Call rejected\")\n    }\n    \n    func ccCallEnded(call: Call) {\n        print(\"Call ended\")\n    }\n    \n    func ccOutgoingCall(call: Call) {\n        print(\"Outgoing call initiated\")\n    }\n    \n    func ccCallInitiated(call: Call) {\n        print(\"Call initiated\")\n    }\n}\n\n// Register\nCometChatCallEvents.addListener(\"call-listener\", CallEventListener())\n```\n\n---\n\n## 6. Request Builder Filters\n\n### Filter Conversations\n\n```swift\nlet conversations = CometChatConversations()\n\n// Only show user conversations (no groups)\nconversations.set(conversationsRequestBuilder: ConversationsRequest.ConversationsRequestBuilder()\n    .set(conversationType: .user)\n    .set(limit: 30)\n)\n\n// Only show conversations with specific tags\nconversations.set(conversationsRequestBuilder: ConversationsRequest.ConversationsRequestBuilder()\n    .set(tags: [\"vip\", \"premium\"])\n    .set(limit: 30)\n)\n```\n\n### Filter Messages\n\n```swift\nlet messageList = CometChatMessageList()\n\n// Only show text and image messages\nmessageList.set(messagesRequestBuilder: MessagesRequest.MessageRequestBuilder()\n    .set(uid: user.uid ?? \"\")\n    .set(types: [CometChatConstants.MessageType.text, CometChatConstants.MessageType.image])\n    .set(limit: 30)\n)\n\n// Only show messages from a specific time range\nlet startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date())!\nmessageList.set(messagesRequestBuilder: MessagesRequest.MessageRequestBuilder()\n    .set(uid: user.uid ?? \"\")\n    .set(timestamp: Int(startDate.timeIntervalSince1970))\n    .set(limit: 50)\n)\n\n// Hide deleted messages\nmessageList.set(messagesRequestBuilder: MessagesRequest.MessageRequestBuilder()\n    .set(uid: user.uid ?? \"\")\n    .hideDeletedMessages(hide: true)\n    .set(limit: 30)\n)\n```\n\n### Filter Users\n\n```swift\nlet users = CometChatUsers()\n\n// Only show users with specific role\nusers.set(usersRequestBuilder: UsersRequest.UsersRequestBuilder()\n    .set(roles: [\"admin\", \"moderator\"])\n    .set(limit: 30)\n)\n\n// Only show friends\nusers.set(usersRequestBuilder: UsersRequest.UsersRequestBuilder()\n    .friendsOnly(true)\n    .set(limit: 30)\n)\n\n// Search users\nusers.set(usersRequestBuilder: UsersRequest.UsersRequestBuilder()\n    .set(searchKeyword: \"john\")\n    .set(limit: 30)\n)\n```\n\n### Filter Groups\n\n```swift\nlet groups = CometChatGroups()\n\n// Only show joined groups\ngroups.set(groupsRequestBuilder: GroupsRequest.GroupsRequestBuilder()\n    .set(joinedOnly: true)\n    .set(limit: 30)\n)\n\n// Only show public groups\ngroups.set(groupsRequestBuilder: GroupsRequest.GroupsRequestBuilder()\n    .set(groupType: .public)\n    .set(limit: 30)\n)\n\n// Search groups\ngroups.set(groupsRequestBuilder: GroupsRequest.GroupsRequestBuilder()\n    .set(searchKeyWord: \"team\")\n    .set(limit: 30)\n)\n```\n\n---\n\n## 7. DataSource Decorator Pattern\n\nFor advanced customization, use the DataSource decorator pattern:\n\n```swift\nclass CustomDataSource: DataSourceDecorator {\n    \n    override func getAllMessageTemplates() -> [CometChatMessageTemplate] {\n        var templates = super.getAllMessageTemplates()\n        \n        // Add custom template\n        let customTemplate = CometChatMessageTemplate(type: \"custom_type\", category: \"custom\")\n        templates.append(customTemplate)\n        \n        return templates\n    }\n    \n    override func getAllMessageTypes() -> [String]? {\n        var types = super.getAllMessageTypes() ?? []\n        types.append(\"custom_type\")\n        return types\n    }\n    \n    override func getAllMessageCategories() -> [String]? {\n        var categories = super.getAllMessageCategories() ?? []\n        categories.append(\"custom\")\n        return categories\n    }\n    \n    override func getMessageOptions(\n        loggedInUser: User,\n        messageObject: BaseMessage,\n        controller: UIViewController?,\n        group: Group?\n    ) -> [CometChatMessageOption]? {\n        var options = super.getMessageOptions(\n            loggedInUser: loggedInUser,\n            messageObject: messageObject,\n            controller: controller,\n            group: group\n        ) ?? []\n        \n        // Add custom option\n        let customOption = CometChatMessageOption(\n            id: \"custom_action\",\n            title: \"Custom Action\",\n            icon: UIImage(systemName: \"star.fill\")\n        )\n        customOption.onClick = { _, _, _, _ in\n            print(\"Custom action triggered\")\n        }\n        options.append(customOption)\n        \n        return options\n    }\n}\n\n// Register custom data source\nChatConfigurator.enable { dataSource in\n    return CustomDataSource(dataSource: dataSource)\n}\n```\n\n---\n\n## Best Practices\n\n1. **Keep custom views lightweight** — Complex views affect scroll performance\n2. **Reuse views when possible** — Use cell reuse patterns\n3. **Handle all message types** — Don't forget edge cases\n4. **Test with real data** — Use actual conversations for testing\n5. **Clean up listeners** — Remove event listeners when views are deallocated\n6. **Use weak references** — Avoid retain cycles in closures","tags":["cometchat","ios","customization","skills","agent-skills","ai-agent","chat","claude-code","cursor","messaging","nextjs","react"],"capabilities":["skill","source-cometchat","skill-cometchat-ios-customization","topic-agent-skills","topic-ai-agent","topic-chat","topic-claude-code","topic-cometchat","topic-cursor","topic-messaging","topic-nextjs","topic-react","topic-react-native","topic-ui-kit"],"categories":["cometchat-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/cometchat/cometchat-skills/cometchat-ios-customization","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add cometchat/cometchat-skills","source_repo":"https://github.com/cometchat/cometchat-skills","install_from":"skills.sh"}},"qualityScore":"0.463","qualityRationale":"deterministic score 0.46 from registry signals: · indexed on github topic:agent-skills · 27 github stars · SKILL.md body (23,123 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-05-18T19:04:53.044Z","embedding":null,"createdAt":"2026-05-07T13:05:12.829Z","updatedAt":"2026-05-18T19:04:53.044Z","lastSeenAt":"2026-05-18T19:04:53.044Z","tsv":"'-12':807,822,827 '-7':1452 '0':404,779,947,969,1086 '0.1':697 '0.removefromsuperview':840 '1':44,1693 '10':396 '11':675 '12':355,388,649,700,797,802,812,817,870,874 '14':143,282 '16':83,110,213,777 '2':156,169,423,1703 '20':417,420 '24':134 '3':566,1712 '30':1396,1412,1437,1482,1504,1515,1526,1545,1558,1569 '4':346,877,1722 '48':184,187 '5':1124,1732 '50':1467 '6':1372,1743 '7':1570 '8':206,783,864,868,872 '9':898 '99':407,408 'a-za-z0':894 'accept':1336 'action':426,466,494,522,1662,1665,1674 'actionmessag':1266,1281,1296 'actual':1728 'ad':1275 'add':531,535,1593,1654 'addedbi':1271 'addsubview':784,786 'admin':1500 'advanc':25,1575 'affect':1700 'align':607,638,662,687,694,906,938,1047 'archiv':548,550,562 'archivebox.fill':554 'archiveopt':545,565 'archiveoption.onclick':559 'area':120 'attach':320 'attribut':930 'attributedstr':924,989,1065,1106 'attributedstring.addattributes':942,984,1101 'audio':311,313 'avatar':119 'avatarview':129,189 'avatarview.addsubview':171 'avatarview.backgroundcolor':131 'avatarview.centerxanchor':178 'avatarview.centeryanchor':181 'avatarview.heightanchor.constraint':185 'avatarview.layer.cornerradius':133 'avatarview.widthanchor.constraint':182 'avoid':1747 'backgroundcolor':458,486,514,555 'badg':234,381,383,413 'badge.backgroundcolor':391 'badge.clipstobounds':397 'badge.font':385 'badge.heightanchor.constraint':418 'badge.ishidden':402 'badge.layer.cornerradius':395 'badge.text':405 'badge.textalignment':393 'badge.textcolor':389 'badge.widthanchor.constraint':415 'ban':1305 'bannedbi':1299 'bannedfrom':1301 'bannedus':1297 'basemessag':902,903,914,993,994,1043,1044,1055,1110,1111,1138,1164,1174,1182,1637 'bell.slash.fill':485 'best':1691 'beyond':10 'block':1225 'boldsystemfont':81,141,211,386,647,775 'bool':247 'bottom':871 'bottomanchor':825 'bubbl':635,659,681,691,702,740 'bubble.backgroundcolor':693 'bubble.layer.cornerradius':699 'builder':42,1374 'built':1018 'built-in':1017 'button':853,876 'button.backgroundcolor':861 'button.contentedgeinsets':865 'button.layer.cornerradius':863 'button.settitle':857 'byad':1449 'calendar.current.date':1448 'call':1324,1332,1333,1335,1339,1340,1342,1346,1347,1349,1353,1354,1357,1361,1362,1364,1369 'call-listen':1368 'calleventlisten':1328,1371 'case':302,306,310,314,1143,1148,1154,1721 'categori':600,1602,1625,1630 'categories.append':1627 'cccallaccept':1331 'cccallend':1345 'cccalliniti':1360 'cccallreject':1338 'ccgroupcreat':1248 'ccgroupdelet':1256 'ccgroupmemberad':1264 'ccgroupmemberban':1294 'ccgroupmemberkick':1279 'cclivereact':1188 'ccmessagedelet':1172 'ccmessageedit':1162 'ccmessageread':1180 'ccmessages':1136 'ccoutgoingcal':1352 'ccownershipchang':1309 'ccuserblock':1220 'ccuserunblock':1228 'cell':1709 'center':145,394 'cgrect':758 'chang':1317 'chart.bar.fill':725 'chatconfigurator.enable':1684 'checkmark.seal.fill':220 'class':744,886,1030,1132,1216,1244,1327,1583 'clean':1733 'closur':1751 'coder':765,768,769 'cometchat':2,6,28 'cometchat-ios-custom':1 'cometchatcalleventlisten':1329 'cometchatcallevents.addlistener':1367 'cometchatconstants.messagetype.image':1434 'cometchatconstants.messagetype.text':1433 'cometchatconvers':48,65,1381 'cometchatconversationopt':434,444,449,477,505,542,546 'cometchatgroup':1532 'cometchatgroupeventlisten':1246 'cometchatgroupevents.addlistener':1319 'cometchatmentionsformatt':1016 'cometchatmessageeventlisten':1134 'cometchatmessageevents.addlistener':1200 'cometchatmessageevents.removelistener':1209 'cometchatmessagelist':578,1014,1418 'cometchatmessageopt':715,1642,1659 'cometchatmessagetempl':596,1589,1598 'cometchatsdk':1131 'cometchattextformatt':888,1032 'cometchattheme.primarycolor.withalphacomponent':696 'cometchattypography.body.medium':983 'cometchattypography.body.regular':936 'cometchatuikit.getdatasource':584 'cometchatus':1488 'cometchatusereventlisten':1218 'cometchatuserevents.addlistener':1236 'complex':1698 'configur':829 'constant':109,796,801,806,811,816,821,826 'content':603,684 'control':608,639,663,688,711,997,1114,1638,1650,1651 'convers':64,68,69,124,125,196,197,272,273,334,335,432,433,438,439,446,469,474,497,502,525,540,541,563,1377,1380,1385,1399,1729 'conversation.conversationid':470,498,526 'conversation.conversationwith':87,96,149,162,228,255 'conversation.lastmessage':288,297,361 'conversation.unreadmessagecount':401 'conversations.add':538 'conversations.set':66,122,194,270,332,430,1388,1403 'conversationsrequest.conversationsrequestbuilder':1390,1405 'conversationsrequestbuild':1389,1404 'conversationtyp':1392 'creat':570,586,880,1253 'custom':4,5,12,26,32,45,49,60,116,190,238,266,328,424,427,536,567,572,587,598,601,602,630,654,680,703,738,882,1022,1027,1576,1594,1600,1603,1616,1628,1655,1661,1664,1673,1681,1695 'customdatasourc':1584,1688 'custommessag':612,615 'custommessage.customdata':618 'customopt':1658,1677 'customoption.onclick':1670 'customtempl':595,734,1597,1605 'customtemplate.bubbleview':685 'customtemplate.contentview':605 'customtemplate.footerview':660 'customtemplate.headerview':636 'customtemplate.options':708 'customurlformatt':1031 'customview':73,115 'customview.addsubview':101 'customview.backgroundcolor':75 'customview.centeryanchor':113 'customview.leadinganchor':108 'cycl':1749 'data':617,627,831,835,844,1682,1726 'datasourc':1571,1579,1685,1689,1690 'datasourcedecor':1585 'date':364,365,379,1454 'dateformatt':371 'day':1450 'dealloc':1742 'decor':1572,1580 'default':318,534,580,928 'defaultattribut':932,943 'delet':501,507,509,521,524,1177,1261,1469 'deleteopt':504,528 'deleteoption.onclick':518 'done':1208 'edg':1720 'edit':1169 'els':92,158,251,293,321,440,619,920,1061 'end':1350 'entir':55 'equalto':107,112,177,180,794,799,804,809,814,819,824 'equaltoconst':183,186,419 'error':1155 'etc':1010 'event':19,38,1125,1128,1214,1242,1325,1737 'exampl':742 'fail':1158 'fals':104,174,250,789,791 'file':315,317 'filter':43,1375,1376,1413,1483,1527 'font':935,982 'footer':655 'foregroundcolor':937,980,1097 'forget':1719 'formatt':17,37,370,879,884,1025 'formatter.dateformat':372 'formatter.string':377 'formattingtyp':908,909,1049,1050 'frame':757,760,761 'friend':1507 'friendson':1511 'func':772,828,890,900,991,1034,1041,1108,1135,1161,1171,1179,1187,1219,1227,1247,1255,1263,1278,1293,1308,1330,1337,1344,1351,1359,1587,1609,1621,1632 'get':579 'getallmessagecategori':1622 'getallmessagetempl':585,1588 'getallmessagetyp':1610 'getmessageopt':1633 'getregex':891,1035 'greaterthanorequaltoconst':416 'group':95,98,161,164,254,257,710,1241,1249,1250,1252,1257,1258,1260,1270,1277,1287,1292,1302,1307,1310,1311,1315,1321,1387,1528,1531,1536,1549,1560,1640,1641,1652,1653 'group-listen':1320 'group.name':100,167,259,1254,1262 'groupaddedin':1269 'groupeventlisten':1245,1323 'groupmemb':1313 'groups.set':1537,1550,1561 'groupsrequest.groupsrequestbuilder':1539,1552,1563 'groupsrequestbuild':1538,1551,1562 'grouptyp':1554 'guard':436,610,911,1052 'h':373 'handl':20,464,492,520,999,1713 'hashtag':951,1000,1003,1008,1023 'hashtagattribut':977,985 'hashtagformatt':887,1021 'header':631 'hide':1468,1478 'hidedeletedmessag':1477 'highlight':950 'horizont':204 'https':1038 'icon':454,482,510,551,722,1666 'icontint':460,488,516,557 'id':450,478,506,547,716,1660 'imag':217,303,1423 'import':1130 'init':756,764 'initi':1358,1365 'initialslabel':136,172 'initialslabel.centerxanchor.constraint':176 'initialslabel.centeryanchor.constraint':179 'initialslabel.font':140 'initialslabel.text':152,165 'initialslabel.textalignment':144 'initialslabel.textcolor':138 'initialslabel.translatesautoresizingmaskintoconstraints':173 'inprogress':1144 'int':1463 'io':3,7,29 'item':51,57 'john':1523 'join':1535 'joinedon':1541 'keep':533,1694 'kick':1290 'kickedbi':1284 'kickedfrom':1286 'kickedus':1282 'kit':9,31 'label':277,327,348,642,653,666,679 'label.font':279,646,672 'label.text':291,304,308,312,316,319,322,644,668 'label.textcolor':283,650,676 'lastmessag':287,296 'lastmessage.messagetype':301 'lastmessage.text':292 'lead':117 'leadinganchor':800,815 'leadingview':123 'left':869 'length':948,970,1087 'let':63,72,77,85,94,128,135,147,160,200,207,214,226,241,253,276,286,295,338,349,359,363,369,382,399,437,447,475,503,544,576,594,611,616,622,641,665,690,713,748,752,842,852,912,917,923,931,952,959,976,1012,1053,1058,1064,1069,1076,1093,1117,1197,1379,1416,1446,1486,1530,1596,1657 'lightweight':1697 'limit':1395,1411,1436,1466,1481,1503,1514,1525,1544,1557,1568 'list':50,56 'listen':39,1126,1196,1198,1203,1204,1206,1212,1239,1322,1370,1735,1738 'listitemview':67 'live':1192 'locat':946,968,1085 'loggedinus':1634,1646,1647 'long':705 'match':960,962,973,975,1077,1079,1090,1092 'match.range':987,1104 'mediamessag':299 'member':1274,1289,1304 'mention':1020 'menu':707 'messag':14,34,324,568,573,592,606,613,637,661,686,709,1127,1137,1146,1151,1157,1163,1168,1173,1176,1181,1184,1202,1211,1265,1280,1295,1414,1424,1440,1470,1715 'message-listen':1201,1210 'message.id':1170,1178,1186 'messagebubblealign':907,1048 'messageeventlisten':1133,1199 'messagelist':577,1013,1417 'messagelist.set':735,1425,1455,1471 'messagelist.textformatter':1015 'messageobject':1636,1648,1649 'messagesrequest.messagerequestbuilder':1427,1457,1473 'messagesrequestbuild':1426,1456,1472 'messagestatus':1140,1166 'metadata':239,242,244 'mm':374 'moder':1501 'mute':473,479,481,493,496 'muteopt':476,500 'muteoption.onclick':490 'namelabel':78,102,208,261 'namelabel.centeryanchor.constraint':111 'namelabel.font':80,210 'namelabel.leadinganchor.constraint':106 'namelabel.text':90,99,231,258 'namelabel.translatesautoresizingmaskintoconstraints':103 'navig':1006 'newown':1312 'normal':860 'nsattributedstr':910,922,1051,1063 'nsattributedstring.key':933,978,1095 'nscoder':766 'nslayoutconstraint.activate':105,175,414,792 'nsmutableattributedstr':925,1066 'nsrang':945,967,1084 'nsregularexpress':955,1072 'nsunderlinestyle.single.rawvalue':1100 'ofsiz':82,142,212,281,354,387,648,674,776 'ontexttap':992,1109 'option':428,431,443,530,532,539,704,843,845,849,851,858,958,965,1075,1082,1644,1656,1679 'options.append':471,499,527,1676 'optionsstack':753,787 'optionsstack.addarrangedsubview':875 'optionsstack.arrangedsubviews.foreach':839 'optionsstack.axis':780 'optionsstack.bottomanchor.constraint':823 'optionsstack.leadinganchor.constraint':813 'optionsstack.spacing':782 'optionsstack.topanchor.constraint':808 'optionsstack.trailinganchor.constraint':818 'optionsstack.translatesautoresizingmaskintoconstraints':790 'outgo':1356 'overrid':755,889,899,990,1033,1040,1107,1586,1608,1620,1631 'ownership':1316 'pattern':956,1073,1573,1581,1711 'perform':1702 'photo':305 'pin':445,451,453,465,468 'pin.fill':457 'pinopt':448,472 'pinoption.onclick':462 'poll':599,645,729,739 'pollbubbleview':624,745 'pollview':623,629 'pollview.configure':625 'possibl':1707 'practic':1692 'prefix':155,168 'premium':1409 'preparemessagestr':901,1042 'press':706 'preview':1028 'print':467,495,523,561,1002,1145,1150,1156,1167,1175,1183,1191,1223,1231,1251,1259,1273,1288,1303,1314,1334,1341,1348,1355,1363,1672 'privat':747,751,771 'public':1548,1555 'purpos':21 'question':836 'questionlabel':749,785 'questionlabel.bottomanchor':810 'questionlabel.font':774 'questionlabel.leadinganchor.constraint':798 'questionlabel.numberoflines':778 'questionlabel.text':834 'questionlabel.topanchor.constraint':793 'questionlabel.trailinganchor.constraint':803 'questionlabel.translatesautoresizingmaskintoconstraints':788 'rang':944,966,986,1083,1103,1445 'reaction':1189,1193 'read':1185 'real':1725 'receiv':1194 'refer':1746 'regex':953,961,1070,1078 'regexstr':904,957,1045,1074 'regist':1195,1235,1318,1366,1680 'reject':1343 'remov':1205,1736 'replac':53 'request':41,1373 'requir':763 'result':718,721,730 'retain':1748 'return':114,188,264,326,421,441,529,564,620,628,652,678,701,731,893,921,988,1037,1062,1105,1606,1618,1629,1678,1687 'reus':1704,1710 'right':695,873,939 'role':1494,1499 'scroll':1701 'search':1009,1516,1559 'searchkeyword':1522,1565 'secondarylabel':284,651 'secondarysystembackground':76 'send':1147,1160 'sent':1152 'sentat':362 'set':1391,1394,1406,1410,1428,1431,1435,1458,1461,1465,1474,1480,1498,1502,1513,1521,1524,1540,1543,1553,1556,1564,1567 'setupui':762,770,773 'show':233,728,1383,1398,1420,1439,1490,1506,1534,1547 'skill':23 'skill-cometchat-ios-customization' 'sourc':1683 'source-cometchat' 'specif':591,1401,1443,1493 'stackview':201,265,339,422 'stackview.addarrangedsubview':260,262,410,412 'stackview.alignment':343 'stackview.axis':203,341 'stackview.spacing':205,345 'star.fill':1669 'startdat':1447 'startdate.timeintervalsince1970':1464 'status':1139,1142,1165 'string':153,166,832,838,847,892,905,926,996,1036,1046,1067,1113,1120,1611,1623 'subtitl':267 'subtitleview':271 'success':1149,1153 'super.getallmessagecategories':1626 'super.getallmessagetemplates':1592 'super.getallmessagetypes':1614 'super.getmessageoptions':1645 'super.init':759,767 'swift':62,121,193,269,331,429,537,575,743,885,1029,1129,1215,1243,1326,1378,1415,1485,1529,1582 'swipe':425 'switch':300,1141 'system':856 'systemblu':132,222,392 'systemfont':280,353,673 'systemgray':487 'systemgray5':862 'systemnam':219,456,484,512,553,724,1668 'systempurpl':556 'systemr':515 'systemyellow':459 'tag':1402,1407 'tail':329 'tap':669,1001,1004 'tappedtext':995,1005,1112,1121 'teach':24 'team':1566 'templat':15,35,569,574,581,583,588,736,737,1591,1595,1607 'templates.append':733,1604 'tertiarylabel':357,677 'test':1723,1731 'text':16,36,878,883,918,927,929,964,1059,1068,1081,1421 'text.count':949,971,1088 'textmessag':290,913,916,1054,1057 'textmessage.text':919,1060 'theme':11 'time':347,1444 'timeinterv':367 'timeintervalsince1970':366 'timelabel':350,411 'timelabel.font':352 'timelabel.text':376 'timelabel.textcolor':356 'timestamp':360,368,1462 'titl':191,452,480,508,549,719,1663 'titleview':195 'top':867 'topanchor':795 'topic-agent-skills' 'topic-ai-agent' 'topic-chat' 'topic-claude-code' 'topic-cometchat' 'topic-cursor' 'topic-messaging' 'topic-nextjs' 'topic-react' 'topic-react-native' 'topic-ui-kit' 'trail':344 'trailinganchor':805,820 'trailview':333 'transientmessag':1190 'trash.fill':513 'tri':954,1071 'trigger':1675 'true':224,248,398,1479,1512,1542 'type':593,597,855,1432,1599,1601,1613,1617,1619,1716 'types.append':1615 'ui':8,30 'uiapplication.shared.open':1122 'uibutton':854 'uicolor.label':941 'uicolor.secondarysystembackground':698 'uicolor.systemblue':981,1098 'uicolor.white':940 'uid':1429,1459,1475 'uiedgeinset':866 'uiimag':218,455,483,511,552,723,1667 'uiimageview':216 'uilabel':79,137,209,278,351,384,643,667,750 'uistackview':202,340,754 'uiview':70,74,126,130,198,274,336,621,692,746 'uiviewcontrol':998,1115,1639 'unblock':1233 'underlinestyl':1099 'unread':380 'unreadcount':400,403,406,409 'uppercas':157,170 'url':1024,1118,1119,1123 'urlattribut':1094,1102 'usag':1011 'use':1577,1708,1727,1744 'user':86,89,148,151,227,230,237,1213,1221,1222,1224,1229,1230,1232,1238,1268,1272,1283,1285,1298,1300,1384,1393,1484,1487,1491,1517,1635 'user-listen':1237 'user.metadata':243 'user.name':91,154,232,1226,1234 'user.uid':1430,1460,1476 'usereventlisten':1217,1240 'users.set':1495,1508,1518 'usersad':1267 'usersrequest.usersrequestbuilder':1497,1510,1520 'usersrequestbuild':1496,1509,1519 'valu':1451 'var':442,582,1590,1612,1624,1643 'verifi':236,245 'verifiedbadg':215,263 'verifiedbadge.ishidden':223,249 'verifiedbadge.tintcolor':221 'vertic':342,781 'video':307,309 'view':13,33,46,52,61,118,192,268,330,604,632,656,682,717,720,741,1696,1699,1705,1740 'viewresultsopt':714,732 'viewresultsoption.onclick':726 'vip':1408 'vote':671 'weak':1745 'white':139,390,461,489,517,558 'wrap':683 'yet':325 'z0':897 'za':896","prices":[{"id":"96d64234-89cf-4b20-8c28-25db497a534f","listingId":"c4a03bc6-9d1e-4297-8323-e702ad565aeb","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"cometchat","category":"cometchat-skills","install_from":"skills.sh"},"createdAt":"2026-05-07T13:05:12.829Z"}],"sources":[{"listingId":"c4a03bc6-9d1e-4297-8323-e702ad565aeb","source":"github","sourceId":"cometchat/cometchat-skills/cometchat-ios-customization","sourceUrl":"https://github.com/cometchat/cometchat-skills/tree/main/skills/cometchat-ios-customization","isPrimary":false,"firstSeenAt":"2026-05-07T13:05:12.829Z","lastSeenAt":"2026-05-18T19:04:53.044Z"}],"details":{"listingId":"c4a03bc6-9d1e-4297-8323-e702ad565aeb","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"cometchat","slug":"cometchat-ios-customization","github":{"repo":"cometchat/cometchat-skills","stars":27,"topics":["agent-skills","ai-agent","chat","claude-code","cometchat","cursor","messaging","nextjs","react","react-native","ui-kit"],"license":null,"html_url":"https://github.com/cometchat/cometchat-skills","pushed_at":"2026-05-18T05:04:24Z","description":"Add CometChat chat to any React, Next.js, React Native, Angular, Android, iOS, or Flutter project through your AI coding agent. Works with Claude Code, Cursor, Codex, VS Code Copilot, Windsurf, Cline, Kiro, and 50+ more agents.","skill_md_sha":"ea9e7a2e28355be8bd0ea73f57c180d4b01bc8e2","skill_md_path":"skills/cometchat-ios-customization/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/cometchat/cometchat-skills/tree/main/skills/cometchat-ios-customization"},"layout":"multi","source":"github","category":"cometchat-skills","frontmatter":{"name":"cometchat-ios-customization","license":"MIT","description":"Customize CometChat iOS UI Kit beyond theming — custom views, message templates, text formatters, and event handling.","compatibility":"CometChatUIKitSwift ^5; iOS 13+"},"skills_sh_url":"https://skills.sh/cometchat/cometchat-skills/cometchat-ios-customization"},"updatedAt":"2026-05-18T19:04:53.044Z"}}