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)
我有一个包含搜索和项目列表的简单应用程序 (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)