NSCollectionViewItems 在更新数据并调用 NSCollectionView.reloadData() 后呈现不正确

NSCollectionViewItems renders incorrectly after updating data and calling NSCollectionView.reloadData()

我有一个包含搜索和项目列表的简单应用程序 (NSCollectionView)。当用户在搜索中键入内容时,我通过调用 NSCollectionView.reloadData() 过滤数据数组和 re-render 项目列表,但它呈现不正确。

下面是它现在如何工作的一个例子:

启动应用程序时的初始状态(查看第 5 项,它有标题、文本和 link):

在搜索中输入 'test' 后。查看项目,标题和 link 不见了:

此外,如果我们检查层次结构,NSCollectionView 与初始状态一样有 6 个项目:

这是我的 ViewController.swift:

import Cocoa
import PureLayout

class ViewController: NSViewController {
    let searchView = NSView()
    let search = NSTextField()
    var searchingString: String?

    let cardList = NSCollectionView()

    let cardId = NSUserInterfaceItemIdentifier("CardID")

    let cardsData: [(type: String, label: String?, text: String, link: String?, tags: [String])] = [
        (
            type: "text",
            label: nil,
            text: "Alternatively one can use paddle.com - They charge a higher fee, tho.",
            link: nil,
            tags: ["#text"]
        ),
        (
            type: "text",
            label: "Essential YC Advice",
            text:
                """
                1. Запускайся как можно раньше. Запустить посредственный продукт и постепенно изменять его через общение с пользователями лучше, чем в одиночку доводить свой проект до «идеального».
                2. Работай над тем, что действительно нужно людям.
                3. Делай вещи, которые не масштабируются.
                4. Найди решение «90/10». 90/10 — это когда ты можешь на 90% решить проблему пользователя, приложив всего 10% усилий.
                5. Найди 10-100 пользователей, которым по-настоящему нужен твой продукт. Маленькая группа людей, которые тебя любят, лучше большой группы, которым ты просто нравишься.
                6. На каком-то этапе каждый стартап имеет нерешенные проблемы. Успех определяется не тем, есть ли у тебя изначально такие проблемы, а что ты делаешь, чтобы их как можно быстрее решить.
                7. Не масштабируй свою команду и продукт, пока ты не построил что-то, что нужно людям.
                8. Стартапы в любой момент времени могут хорошо решать только одну проблему.
                9. Игнорируй своих конкурентов. В мире стартапов ты скорее умрешь от суицида, чем от убийства.
                10. Будь хорошим человеком. Ну или хотя бы просто не будь придурком.
                11. Спи достаточное количество времени и занимайся спортом.

                Многие пункты подробно расписаны здесь:
                http://blog.ycombinator.com/ycs-essential-startup-advice/
                """,
            link: nil,
            tags: ["#text", "#advice", "#launch", "#startup_hero", "#telegram_channel"]
        ),
        (
            type: "text",
            label: nil,
            text:
                """
                If I was you, I'd start trying to build a small following.
                it helps a lot (especially if your making products for makers)

                Indie Hackers / Reddit Startup + Side Project great places ...
                And slowly but surely you'll build twitter + email list.
                follow @AndreyAzimov example! with his medium posts + twitter
                """,
            link: nil,
            tags: ["#text", "#advice"]
        ),
        (
            type: "link",
            label: "Paddle",
            text: "Like Stripe but maybe avaliable in Uzbekistan and Russia",
            link: "https://paddle.com/",
            tags: ["#link", "#payment", "#payment_service"]
        ),
        (
            type: "link",
            label: "How to Test and Validate Startup Ideas: – The Startu...",
            text: "One of the first steps to launching a successful startup is building your Minimal Viable Product. In essence, a Minimal Viable Product (MVP) is all about verifying the fact that there is a market…",
            link: "https://medium.com/swlh/how-to-start-a-startup-e4f002ff3ee1",
            tags: ["#link", "#article", "#medium", "#read_later", "#validating", "#validating_idea"]
        ),
        (
            type: "link",
            label: "The best Slack groups for UX designers – UX Collect...",
            text: "Tons of companies are using Slack to organize and facilitate how their employees communicate on a daily basis. Slack has now more than 5 million daily active users and more than 60,000 teams around…",
            link: "https://uxdesign.cc/the-best-slack-groups-for-ux-designers-25c621673d9c",
            tags: ["#link", "#article", "#medium", "#read_later", "#ux", "#community"]
        )
    ]

    var filteredCards: [(type: String, label: String?, text: String, link: String?, tags: [String])] = []

    func filterCards() {
        filteredCards = cardsData.filter { cardData in
            var isValidByTags = true
            var isValidByWords = true

            if let searchingArray = searchingString?.split(separator: " ") {
                let searchingTags = searchingArray.filter { [=11=].hasPrefix("#") }
                let searchingWords = searchingArray.filter { ![=11=].hasPrefix("#") }

                searchingTags.forEach { searchingTag in
                    var isValidTag = false

                    cardData.tags.forEach { cardDataTag in
                        if cardDataTag.lowercased() == searchingTag.lowercased() {
                            isValidTag = true
                        }
                    }

                    if !isValidTag {
                        isValidByTags = false
                    }
                }

                searchingWords.forEach { searchingWord in
                    if !(cardData.label != nil && (cardData.label?.lowercased().contains(searchingWord.lowercased()))!) && !cardData.text.lowercased().contains(searchingWord.lowercased()) {
                        isValidByWords = false
                    }
                }
            }

            return isValidByTags && isValidByWords
        }
    }

    func configureSearch() {
        search.placeholderString = "Enter one or more tags devided by space and some words. For example, #link #work Design"
        search.layer = CALayer()
        search.delegate = self

        search.layer?.backgroundColor = .white
        search.layer?.cornerRadius = 16

        searchView.addSubview(search)

        search.autoPinEdge(toSuperviewEdge: .top, withInset: 10)
        search.autoPinEdge(toSuperviewEdge: .right, withInset: 20)
        search.autoPinEdge(toSuperviewEdge: .bottom, withInset: 10)
        search.autoPinEdge(toSuperviewEdge: .left, withInset: 20)

        view.addSubview(searchView)

        searchView.autoSetDimension(.height, toSize: 50)
        searchView.autoPinEdge(toSuperviewEdge: .top)
        searchView.autoPinEdge(toSuperviewEdge: .right)
        searchView.autoPinEdge(.bottom, to: .top, of: cardList)
        searchView.autoPinEdge(toSuperviewEdge: .left)
    }

    func configureCardList() {
        let flowLayout = NSCollectionViewFlowLayout()
        flowLayout.minimumInteritemSpacing = 0
        flowLayout.minimumLineSpacing = 0

        cardList.collectionViewLayout = flowLayout

        cardList.frame = NSRect(x: 0, y: 0, width: 1200, height: 628)
        cardList.delegate = self
        cardList.dataSource = self

        let nib = NSNib(nibNamed: NSNib.Name(rawValue: "Card"), bundle: nil)
        cardList.register(nib, forItemWithIdentifier: cardId)

        view.addSubview(cardList)

        cardList.autoPinEdge(toSuperviewEdge: .right)
        cardList.autoPinEdge(toSuperviewEdge: .bottom)
        cardList.autoPinEdge(toSuperviewEdge: .left)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        filterCards()

        configureCardList()
        configureSearch()
    }
}

extension ViewController: NSCollectionViewDataSource, NSCollectionViewDelegate, NSCollectionViewDelegateFlowLayout, NSTextFieldDelegate {
    func collectionView(_ cardList: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
        return filteredCards.count
    }

    func collectionView(_ cardList: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
        let card = cardList.makeItem(withIdentifier: cardId, for: indexPath) as! Card

        card.data = filteredCards[indexPath.item]

        return card
    }

    func collectionView(_ cardList: NSCollectionView, layout cardListLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize {
        return CGSize(width: 240, height: 240)
    }

    override func controlTextDidChange(_ obj: Notification) {
        searchingString = search.stringValue

        filterCards()

        cardList.reloadData()
    }
}

这是我的 Card.swift:

import Cocoa
import PureLayout

class Card: NSCollectionViewItem {
    let labelView = NSTextView()
    let linkView = NSTextView()
    let textView = NSTextView()

    var data: (type: String, label: String?, text: String, link: String?, tags: [String])?

    func configureLabelView() {
        labelView.font = NSFont(name: "SF Pro Display Medium", size: 15)
        labelView.isEditable = false
        labelView.isFieldEditor = false
        labelView.isHidden = false

        labelView.string = (data?.label)!

        labelView.autoSetDimension(.height, toSize: 16)
        labelView.autoPinEdge(toSuperviewEdge: .top, withInset: 10)
        labelView.autoPinEdge(toSuperviewEdge: .right, withInset: 10)
        labelView.autoPinEdge(toSuperviewEdge: .left, withInset: 10)
    }

    func configureLinkView() {
        linkView.font = NSFont(name: "SF Pro Display", size: 13)
        linkView.isEditable = false
        linkView.isFieldEditor = false
        linkView.isHidden = false

        linkView.string = (URL(string: (data?.link)!)?.host)!

        let attributedString = NSMutableAttributedString(string: linkView.string)
        let range = NSRange(location: 0, length: linkView.string.count)
        let url = URL(string: (data?.link)!)

        attributedString.setAttributes([.link: url!], range: range)
        linkView.textStorage?.setAttributedString(attributedString)

        linkView.linkTextAttributes = [.underlineStyle: NSUnderlineStyle.styleSingle.rawValue]

        linkView.autoSetDimension(.height, toSize: 16)
        linkView.autoPinEdge(toSuperviewEdge: .right, withInset: 10)
        linkView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 10)
        linkView.autoPinEdge(toSuperviewEdge: .left, withInset: 10)
    }

    func configureTextView() {
        textView.font = NSFont(name: "SF Pro Display", size: 13)
        textView.textContainer?.lineBreakMode = .byTruncatingTail
        textView.isEditable = false
        textView.isFieldEditor = false

        textView.string = (data?.text)!

        if data?.label != nil && (data?.label?.count)! > 0 {
            configureLabelView()

            textView.autoPinEdge(.top, to: .bottom, of: labelView, withOffset: 10)
        } else {
            textView.autoPinEdge(toSuperviewEdge: .top, withInset: 10)
        }

        if data?.link != nil && (data?.link?.count)! > 0 && data?.type == "link" {
            configureLinkView()

            textView.autoPinEdge(.bottom, to: .top, of: linkView, withOffset: -10)
        } else {
            textView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 10)
        }

        textView.autoPinEdge(toSuperviewEdge: .right, withInset: 10)
        textView.autoPinEdge(toSuperviewEdge: .left, withInset: 10)
    }

    override func viewDidAppear() {
        super.viewDidAppear()

        view.layer?.borderWidth = 1
        view.layer?.borderColor = .black

        view.addSubview(labelView)
        view.addSubview(textView)
        view.addSubview(linkView)

        labelView.isHidden = true
        linkView.isHidden = true

        configureTextView()
    }
}

此外,我在控制台中收到这样的错误:

Set the NSUserDefault NSConstraintBasedLayoutVisualizeMutuallyExclusiveConstraints to YES to have -[NSWindow visualizeConstraints:] automatically called when this happens.  And/or, set a symbolic breakpoint on LAYOUT_CONSTRAINTS_NOT_SATISFIABLE to catch this in the debugger.
2018-09-17 20:38:25.505130+0500 Taggy[1317:23304] [Layout] Unable to simultaneously satisfy constraints:
(
    "<NSLayoutConstraint:0x60400009e050 NSTextView:0x600000121ae0.bottom == NSView:0x604000121ae0.bottom - 10   (active)>",
    "<NSLayoutConstraint:0x600000095b80 NSTextView:0x600000121a40.bottom == NSTextView:0x600000121ae0.top - 10   (active)>",
    "<NSLayoutConstraint:0x600000092c00 NSTextView:0x600000121a40.bottom == NSView:0x604000121ae0.bottom - 10   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x60400009e050 NSTextView:0x600000121ae0.bottom == NSView:0x604000121ae0.bottom - 10   (active)>

Set the NSUserDefault NSConstraintBasedLayoutVisualizeMutuallyExclusiveConstraints to YES to have -[NSWindow visualizeConstraints:] automatically called when this happens.  And/or, set a symbolic breakpoint on LAYOUT_CONSTRAINTS_NOT_SATISFIABLE to catch this in the debugger.

这里是GitHub link 如果你想玩玩 - https://github.com/SilencerWeb/taggy-app/tree/development

欢迎任何形式的帮助,我正在努力解决这个问题 2 天:(

通过将 Card 注册为 class 而不是 nib 来解决。

之前:

let nib = NSNib(nibNamed: NSNib.Name(rawValue: "Card"), bundle: nil)
cardList.register(nib, forItemWithIdentifier: cardId)

之后:

cardList.register(Card.self, forItemWithIdentifier: cardId)