【问题标题】:Messages showing up on wrong side and with incorrect size/content (MessageKit)消息显示在错误的一侧并且大小/内容不正确(MessageKit)
【发布时间】:2019-08-04 09:48:14
【问题描述】:

我在我正在开发的应用程序中使用MessageKit 作为聊天功能,在将部分插入messagesCollectionView 时遇到了一个奇怪的布局错误。打开对话时,线程从缓存中正确加载,但是在加载下一页消息(数据源分页)并且消息在批量更新中添加到UICollectionView(参见下面的代码)时,消息错误出,出现在错误的索引,错误的大小,错误的一面,偶尔还有错误的文字。

我尝试更改添加消息的方式,但我尝试的所有方式都导致了这种情况...该应用程序的早期版本通过reloadData() 完全重新加载了messagesCollectionView,但即使使用该实现,此错误也会在首次打开线程时发生(似乎在重新加载数据源并且集合视图一直滚动到底部时。我不太了解MessageKitUICollectionView 来解决这个问题,希望能更好地了解其复杂性的人会发现这个错误!

//
//  ThreadDetailViewController.swift
//  --
//
//  Created by Jai Smith on 7/18/19.
//  Copyright © 2019 --. All rights reserved.
//

import UIKit
import MessageKit
import InputBarAccessoryView
import Alamofire
import os.log
import Kingfisher

class ThreadDetailViewController: MessagesViewController {

    // MARK: Properties

    var thread: MessageThread!
    var messages: Int = 0
    var reachedEnd: Bool = false
    var loadLock: Bool = false
    var page: PageManager?

    var scrollingUp: Bool = false
    var shouldFetchNextPage: Bool = false

    var messagesToAdd: Int?
    var insertingSections: Bool = false

    // MARK: Overrides

    override func viewDidLoad() {
        super.viewDidLoad()

        // set title
        self.title = thread.title

        // add notification observers
        NotificationCenter.default.addObserver(self, selector: #selector(receivedMessage), name: Notification.Name("receivedMessage"), object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(socketStateChanged), name: Notification.Name("sockState"), object: nil)

        // scroll to bottom when typing
        self.scrollsToBottomOnKeyboardBeginsEditing = true

        // set delegate/source
        messagesCollectionView.messagesDisplayDelegate = self
        messagesCollectionView.messagesDataSource = self
        messagesCollectionView.messagesLayoutDelegate = self
        messagesCollectionView.contentInset = UIEdgeInsets(top: 2.5, left: 0, bottom: 2.5, right: 0)
        messageInputBar = CustomInputBar()
        messageInputBar.delegate = self
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        // start scrolled all the way to the bottom of already loaded messages
        messages = thread.messages.count
        messagesCollectionView.reloadData()
        messagesCollectionView.scrollToBottom(animated: false)

        // load messages
        self.loadMessages()
    }

    override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        if indexPath.section == 0, let amount = messagesToAdd {
            animateMessageInsertion(amount: amount)
            self.messagesToAdd = nil
        }
    }

    // MARK: UIScrollViewDelegate

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        scrollingUp = scrollView.panGestureRecognizer.translation(in: scrollView.superview).y > 0
    }

    // MARK: Navigation

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        super.prepare(for: segue, sender: sender)

        switch segue.identifier {
        case "ProfileDetail":
            if let profileTableViewController = segue.destination as? ProfileTableViewController {
                profileTableViewController.user = getConversants().first
            }

        default:
            fatalError("Unexpected Segue Identifier: \(segue.identifier ?? "nil")")
        }
    }

    // MARK: Public Methods

    @objc func socketStateChanged(_ notification: Notification) {
        if let status = notification.object as? Int {
            if status == 0 {
                os_log("Connecting...", log: OSLog.default, type: .debug)
            } else if status == 1 {
                // enable send button if text is entered
                if !self.messageInputBar.inputTextView.text.isEmpty {
                    self.messageInputBar.sendButton.isEnabled = true
                }
            }
        }
    }

    @objc func receivedMessage(_ notification: Notification) {
        if let message = notification.object as? Message {
            self.thread.messages.append(message)
            self.messages += 1
            self.messagesCollectionView.insertSections([self.thread.messages.count - 1])
            self.messagesCollectionView.scrollToBottom(animated: true)
        }
    }

    @objc func loadMessages() {
        // lock (prevent this method from being called twice)
        self.loadLock = true

        Credentials.shared.session.request(API.shared.messaging.history, method: .get, parameters: ["hash": thread.hash], encoding: URLEncoding.default)
            .validate(statusCode: [200])
            .validate(contentType: ["application/json"])
            .responseJSON { response in
                defer {
                    // unlock
                    self.loadLock = false

                    if self.shouldFetchNextPage {
                        self.loadMoreMessages()
                    }
                }

                switch response.result {
                case .success:
                    guard let data = response.data, let page = try? JSONDecoder().decode(PageManager.self, from: data) else {
                        os_log("Error deserializing PageManager for ThreadDetail", log: OSLog.default, type: .error)

                        return
                    }

                    // decode messages
                    var messages = [Message]()
                    while !page.results.isAtEnd {
                        do {
                            let message = try page.results.decode(Message.self)
                            messages.append(message)
                        } catch {
                            os_log("Error deserializing Message", log: OSLog.default, type: .error)
                            _ = try? page.results.decode(AnyCodable.self)
                        }
                    }

                    self.page = page
                    self.thread.messages = messages.reversed()
                    self.messages = self.thread.messages.count
                    self.messagesCollectionView.reloadData()
                    self.messagesCollectionView.scrollToBottom(animated: false)

                case .failure:
                    os_log("Error loading messages for thread", log: OSLog.default, type: .error)
                }
        }
    }

    // MARK: Private Methods

    private func getConversants() -> [User] {
        var conversants = [User]()
        for user in thread.participants.filter({ user in return user.id != Credentials.shared.user.id }) {
            conversants.append(user)
        }

        if conversants.isEmpty {
            conversants.append(Credentials.shared.user)
        }

        return conversants
    }

    // https://stackoverflow.com/a/32691888/11722138
    private func animateMessageInsertion(amount: Int) {
        guard !insertingSections else {
            return
        }

        insertingSections = true

        let contentHeight = self.messagesCollectionView.contentSize.height
        let offsetY = self.messagesCollectionView.contentOffset.y
        let bottomOffset = contentHeight - offsetY

        CATransaction.begin()
        CATransaction.setDisableActions(true)

        self.messagesCollectionView.performBatchUpdates({
            if amount > 0 {
                self.messages += amount
                self.messagesCollectionView.insertSections(IndexSet(integersIn: self.thread.messages.count - amount..<self.thread.messages.count))
            }
        }, completion: { finished in
            defer {
                self.insertingSections = false
            }

            os_log("Finished inserting new messages, animating...", log: OSLog.default, type: .debug)
            self.messagesCollectionView.contentOffset = CGPoint(x: 0, y: self.messagesCollectionView.contentSize.height - bottomOffset)
            CATransaction.commit()
        })
    }

    private func loadMoreMessages() {
        guard !loadLock else {
            return
        }

        guard let page = page else {
            shouldFetchNextPage = true
            return
        }

        guard page.next != nil else {
            return
        }

        loadLock = true

        page.getNextPage(completion: { page in
            defer {
                self.loadLock = false
            }

            guard let page = page else {
                os_log("Error loading next page", log: OSLog.default, type: .error)

                return
            }

            // decode new messages
            var messages = [Message]()
            while !page.results.isAtEnd {
                do {
                    let message = try page.results.decode(Message.self)
                    messages.append(message)
                } catch {
                    os_log("Error deserializing Message", log: OSLog.default, type: .error)
                    _ = try? page.results.decode(AnyCodable.self)
                }
            }

            // append to messages array
            self.thread.messages.insert(contentsOf: messages.reversed(), at: 0)
            self.page = page

            // queue messages to be added
            self.messagesToAdd = messages.count

            // trigger insertion if top message is already visible
            if self.messagesCollectionView.indexPathsForVisibleItems.contains(where: { indexPath in return indexPath.section == 0 }), let amount = self.messagesToAdd {
                self.animateMessageInsertion(amount: amount)
                self.messagesToAdd = nil
            }
        })
    }
}

// MARK: Extensions

extension ThreadDetailViewController: MessagesDataSource {
    func currentSender() -> SenderType {
        return Credentials.shared.user
    }

    func messageForItem(at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageType {
        // check if more messages should be loaded
        if !loadLock, indexPath.section < 4, page == nil ? true : scrollingUp {
            loadMoreMessages()
            loadLock = true
        }

        return thread.messages[indexPath.section]
    }

    func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int {
        return messages
    }

    func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
        if indexPath.section - 1 >= 0 {
            let prevMessage = self.thread.messages[indexPath.section - 1]
            if message.sentDate.timeIntervalSince(prevMessage.sentDate).isLess(than: 24 * 3600.0) {
                return nil
            }
        }

        return NSAttributedString(string: message.sentDate.displayFormatDate(), attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)])
    }

    func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
        if indexPath.section - 1 >= 0 {
            let prevMessage = self.thread.messages[indexPath.section - 1]
            if prevMessage.sender.senderId == message.sender.senderId && cellTopLabelAttributedText(for: message, at: indexPath) == nil {
                return nil
            }
        }

        return NSAttributedString(string: message.sender.displayName, attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)])
    }

    func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
        if indexPath.section + 1 < numberOfSections(in: messagesCollectionView) {
            let nextMessage = self.thread.messages[indexPath.section + 1]
            if nextMessage.sentDate.timeIntervalSince(message.sentDate).isLessThanOrEqualTo(3600.0) {
                return nil
            }
        }

        return NSAttributedString(string: message.sentDate.displayFormatTime(), attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption2)])
    }
}

extension ThreadDetailViewController: InputBarAccessoryViewDelegate {
    func inputBar(_ inputBar: InputBarAccessoryView, textViewTextDidChangeTo text: String) {
        if !SocketManager.shared.sock.isConnected {
            inputBar.sendButton.isEnabled = false
        }
    }

    func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) {
        inputBar.inputTextView.text = ""
        SocketManager.shared.sendMessage(text, hash: thread.hash)
    }
}

extension ThreadDetailViewController: MessagesLayoutDelegate {
    func messagePadding(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIEdgeInsets {
        return UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
    }

    func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
        return messageTopLabelAttributedText(for: message, at: indexPath)?.size().height ?? 0
    }

    func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
        return messageBottomLabelAttributedText(for: message, at: indexPath)?.size().height ?? 0
    }

    func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat {
        return cellTopLabelAttributedText(for: message, at: indexPath)?.size().height ?? -5
    }
}

extension ThreadDetailViewController: MessagesDisplayDelegate {
    func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) {
        if let index = UserList.index, let id = (message.sender as? User)?.id, let avatarURL = index[id]?.avatar {
            var image: UIImage = UIImage(named: "placeholder")!
            KingfisherManager.shared.retrieveImage(with: avatarURL, completionHandler: { result in
                switch result {
                case .success:
                    do {
                        image = try result.get().image
                        messagesCollectionView.reloadItems(at: [indexPath])
                    } catch {
                        os_log("Couldn't retrieve image with Kingfisher", log: OSLog.default, type: .error)
                    }

                case .failure:
                    os_log("Failed to load profile image for %@ in Message", log: OSLog.default, type: .error, index[id]?.displayName ?? "nil")
                }
            })

            var initials: String
            if let user = index[id] {
                initials = "\(user.firstName.capitalized.first ?? " ")\(user.lastName.capitalized.first ?? " ")"
            } else {
                initials = "nil"
            }

            avatarView.set(avatar: Avatar(image: image, initials: initials))
        } else {
            avatarView.set(avatar: Avatar(image: UIImage(named: "placeholder"), initials: "\(message.sender.displayName.first ?? "?")"))
        }
    }
}

这里的问题应该很明显,但是标有“Jai”的消息应该出现在左侧,并且消息气泡应该包含其中包含的文本。

更新: 似乎MessageKit 正在计算消息大小和消息的发件人在数据源中的索引关闭,然后从正确的索引中获取文本...不能弄清楚发生在哪里。

【问题讨论】:

    标签: ios swift uicollectionview messagekit


    【解决方案1】:

    几个小时后,我用不同的策略在UICollectionView 中插入部分而不出现这种错误行为,我已经将UICollectionView 颠倒过来,现在将部分添加到底部,效果很好。我最初避免使用这种方法,因为害怕引入大型消息线程的性能问题(我在几个论坛上读到过),但到目前为止还没有遇到任何类似的问题。如果有人遇到类似于我上面描述的问题并试图将部分插入UICollectionView 的顶部,我建议像我一样翻转UICollectionView。它使整个过程变得更加容易。

    (对于那些好奇的人,上述问题源于更新数据源并将部分插入UICollectionView,导致单元重新加载,并在此过程中从数据源中不正确的索引中获取数据......我'确定有办法纠正这个问题,但如果翻转 UICollectionView 与您的应用程序设计配合使用,它会更容易且不那么麻烦)

    【讨论】:

    • 这非常有帮助。我刚刚安装了 MessageKit 并正在努力将其集成到我的项目中。我正在努力查看我们应该在哪个函数下加载我们的消息。您能否就您是如何开始的给予我任何指导?
    • @techgirl 我建议通过MessageKit GitHub repo 中的示例项目!我就是这样开始的。您的对话视图控制器有一个定义良好的结构,因此您可能只需复制示例代码并对其进行调整以供您自己使用! MessagesDataSource.messageForItem 是从字面上加载每条消息的内容,但您还需要其他一些东西才能使其全部工作(它本质上只是一个精美的表格视图)。
    • 非常感谢您的指导。祝你项目顺利,新年快乐。
    • 你愿意在房间里聊天吗?
    猜你喜欢
    • 2015-07-03
    • 1970-01-01
    • 1970-01-01
    • 2013-07-16
    • 1970-01-01
    • 2020-04-26
    • 1970-01-01
    • 2016-06-27
    • 1970-01-01
    相关资源
    最近更新 更多