使用自定义维度重新排序集合视图单元格时出现问题
Posted
技术标签:
【中文标题】使用自定义维度重新排序集合视图单元格时出现问题【英文标题】:Problems reordering Collection View Cell with custom dimensions 【发布时间】:2016-09-25 20:12:47 【问题描述】:我想对集合视图中的单元格重新排序,并为每个单元格自定义大小。 在集合视图的每个单元格中都有一个带有单词的标签。 我用这段代码设置了每个单元格的尺寸:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
let word = textArray[indexPath.row]
let font = UIFont.systemFont(ofSize: 17)
let fontAttributes = [NSFontAttributeName: font]
var size = (word as NSString).size(attributes: fontAttributes)
size.width = size.width + 2
return size
我使用以下代码重新排序集合视图:
override func viewDidLoad()
super.viewDidLoad()
self.installsStandardGestureForInteractiveMovement = false
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(gesture:)))
self.collectionView?.addGestureRecognizer(panGesture)
func handlePanGesture(gesture: UIPanGestureRecognizer)
switch gesture.state
case UIGestureRecognizerState.began :
guard let selectedIndexPath = self.collectionView?.indexPathForItem(at: gesture.location(in: self.collectionView)) else
break
collectionView?.beginInteractiveMovementForItem(at: selectedIndexPath)
print("Interactive movement began")
case UIGestureRecognizerState.changed :
collectionView?.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
print("Interactive movement changed")
case UIGestureRecognizerState.ended :
collectionView?.endInteractiveMovement()
print("Interactive movement ended")
default:
collectionView?.cancelInteractiveMovement()
print("Interactive movement canceled")
override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)
// Swap values if sorce and destination
let change = textArray[sourceIndexPath.row]
textArray.remove(at: sourceIndexPath.row)
textArray.insert(change, at: destinationIndexPath.row)
// Reload data to recalculate dimensions for the cells
collectionView.reloadData()
视图如下所示:
问题是在重新排序期间,单元格在 indexPath 处保持原始单元格的尺寸,因此在重新排序期间,视图如下所示: 目前我已经解决了在重新排序结束时重新加载数据的问题,以重新计算正确的尺寸。 如何在交互式移动和重新排序自定义大小的单元格期间也保持单元格的正确尺寸?
【问题讨论】:
对不起,如果我在语言上犯了一些错误,我是意大利人 【参考方案1】:这整个星期都困扰着我,所以我今晚坐下来尝试寻找解决方案。我认为您需要的是为您的集合视图自定义布局管理器,它可以随着顺序的变化动态调整每个单元格的布局。
下面的代码显然比你上面的布局更粗糙,但从根本上实现了你想要的行为:当单元格重新排序时,关键是移动到新的布局,“即时”发生,不需要任何临时调整。
这一切的关键是视图控制器的 sourceData 变量中的 didSet 函数。当这个数组的值改变时(通过按下排序按钮 - 我对你的手势识别器的粗略近似),这会自动触发所需单元格尺寸的重新计算,然后还会触发布局以清除自身并重新计算并重新加载集合视图数据。
如果您对此有任何疑问,请告诉我。希望能帮助到你!
更新:好的,我了解您现在想要做什么,我认为附加的更新代码可以让您到达那里。而不是使用内置的交互方法,我认为考虑到我已经实现了自定义布局管理器来使用委托的方式更容易:当平移手势识别器选择一个单元格时,我们创建一个基于该单词的子视图,该单词随手势。同时在后台我们从数据源中移除单词并刷新布局。当用户选择一个位置来放置单词时,我们反转该过程,告诉代理将单词插入数据源并刷新布局。如果用户将单词拖到集合视图之外或到无效位置,则单词会简单地放回它开始的位置(使用将原始索引存储为标签标签的狡猾技术)。
希望能帮到你!
[文字由***提供]
import UIKit
class ViewController: UIViewController, bespokeCollectionViewControllerDelegate
let sourceText : String = "So Midas, king of Lydia, swelled at first with pride when he found he could transform everything he touched to gold; but when he beheld his food grow rigid and his drink harden into golden ice then he understood that this gift was a bane and in his loathing for gold, cursed his prayer"
var sourceData : [String]!
didSet
refresh()
var sortedCVController : UICollectionViewController!
var sortedLayout : bespokeCollectionViewLayout!
var sortButton : UIButton!
var sortDirection : Int = 0
override func viewDidLoad()
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
sortedLayout = bespokeCollectionViewLayout(contentWidth: view.frame.width - 200)
sourceData =
let components = sourceText.components(separatedBy: " ")
return components
()
sortedCVController = bespokeCollectionViewController(sourceData: sourceData, collectionViewLayout: sortedLayout, frame: CGRect(origin: CGPoint(x: 100, y: 100), size: CGSize(width: view.frame.width - 200, height: view.frame.height - 200)))
(sortedCVController as! bespokeCollectionViewController).delegate = self
sortedCVController.collectionView!.frame = CGRect(origin: CGPoint(x: 100, y: 100), size: CGSize(width: view.frame.width - 200, height: view.frame.height - 200))
sortButton =
let sB : UIButton = UIButton(frame: CGRect(origin: CGPoint(x: 25, y: 100), size: CGSize(width: 50, height: 50)))
sB.setTitle("Sort", for: .normal)
sB.setTitleColor(UIColor.black, for: .normal)
sB.addTarget(self, action: #selector(sort), for: .touchUpInside)
sB.layer.borderColor = UIColor.black.cgColor
sB.layer.borderWidth = 1.0
return sB
()
view.addSubview(sortedCVController.collectionView!)
view.addSubview(sortButton)
func refresh() -> Void
let dimensions : [CGSize] =
var d : [CGSize] = [CGSize]()
let font = UIFont.systemFont(ofSize: 17)
let fontAttributes = [NSFontAttributeName : font]
for item in sourceData
let stringSize = ((item + " ") as NSString).size(attributes: fontAttributes)
d.append(CGSize(width: stringSize.width, height: stringSize.height))
return d
()
if self.sortedLayout != nil
sortedLayout.dimensions = dimensions
if let _ = sortedCVController
(sortedCVController as! bespokeCollectionViewController).sourceData = sourceData
self.sortedLayout.cache.removeAll()
self.sortedLayout.prepare()
if let _ = self.sortedCVController
self.sortedCVController.collectionView?.reloadData()
func sort() -> Void
sourceData = sortDirection > 0 ? sourceData.sorted(by: $0 > $1 ) : sourceData.sorted(by: $0 < $1 )
sortDirection = sortDirection + 1 > 1 ? 0 : 1
func didMoveWord(atIndex: Int)
sourceData.remove(at: atIndex)
func didPlaceWord(word: String, atIndex: Int)
print(atIndex)
if atIndex >= sourceData.count
sourceData.append(word)
else
sourceData.insert(word, at: atIndex)
func pleaseRefresh()
refresh()
protocol bespokeCollectionViewControllerDelegate
func didMoveWord(atIndex: Int) -> Void
func didPlaceWord(word: String, atIndex: Int) -> Void
func pleaseRefresh() -> Void
class bespokeCollectionViewController : UICollectionViewController
var sourceData : [String]
var movingLabel : UILabel!
var initialOffset : CGPoint!
var delegate : bespokeCollectionViewControllerDelegate!
init(sourceData: [String], collectionViewLayout: bespokeCollectionViewLayout, frame: CGRect)
self.sourceData = sourceData
super.init(collectionViewLayout: collectionViewLayout)
self.collectionView = UICollectionView(frame: frame, collectionViewLayout: collectionViewLayout)
self.collectionView?.backgroundColor = UIColor.white
self.collectionView?.layer.borderColor = UIColor.black.cgColor
self.collectionView?.layer.borderWidth = 1.0
self.installsStandardGestureForInteractiveMovement = false
let pangesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(gesture:)))
self.collectionView?.addGestureRecognizer(pangesture)
required init?(coder aDecoder: NSCoder)
fatalError("init(coder:) has not been implemented")
func handlePanGesture(gesture: UIPanGestureRecognizer)
guard let _ = delegate else return
switch gesture.state
case UIGestureRecognizerState.began:
guard let selectedIndexPath = self.collectionView?.indexPathForItem(at: gesture.location(in: self.collectionView)) else break
guard let selectedCell : UICollectionViewCell = self.collectionView?.cellForItem(at: selectedIndexPath) else break
initialOffset = gesture.location(in: selectedCell)
let index : Int =
var i : Int = 0
for sectionCount in 0..<selectedIndexPath.section
i += (self.collectionView?.numberOfItems(inSection: sectionCount))!
i += selectedIndexPath.row
return i
()
movingLabel =
let mL : UILabel = UILabel()
mL.font = UIFont.systemFont(ofSize: 17)
mL.frame = selectedCell.frame
mL.textColor = UIColor.black
mL.text = sourceData[index]
mL.layer.borderColor = UIColor.black.cgColor
mL.layer.borderWidth = 1.0
mL.backgroundColor = UIColor.white
mL.tag = index
return mL
()
self.collectionView?.addSubview(movingLabel)
delegate.didMoveWord(atIndex: index)
case UIGestureRecognizerState.changed:
if let _ = movingLabel
movingLabel.frame.origin = CGPoint(x: gesture.location(in: self.collectionView).x - initialOffset.x, y: gesture.location(in: self.collectionView).y - initialOffset.y)
case UIGestureRecognizerState.ended:
print("Interactive movement ended")
if let selectedIndexPath = self.collectionView?.indexPathForItem(at: gesture.location(in: self.collectionView))
guard let _ = movingLabel else return
let index : Int =
var i : Int = 0
for sectionCount in 0..<selectedIndexPath.section
i += (self.collectionView?.numberOfItems(inSection: sectionCount))!
i += selectedIndexPath.row
return i
()
delegate.didPlaceWord(word: movingLabel.text!, atIndex: index)
UIView.animate(withDuration: 0.25, animations:
self.movingLabel.alpha = 0
self.movingLabel.removeFromSuperview()
, completion: _ in
self.movingLabel = nil )
else
if let _ = movingLabel
delegate.didPlaceWord(word: movingLabel.text!, atIndex: movingLabel.tag)
UIView.animate(withDuration: 0.25, animations:
self.movingLabel.alpha = 0
self.movingLabel.removeFromSuperview()
, completion: _ in
self.movingLabel = nil )
default:
collectionView?.cancelInteractiveMovement()
print("Interactive movement canceled")
override func numberOfSections(in collectionView: UICollectionView) -> Int
guard !(self.collectionViewLayout as! bespokeCollectionViewLayout).cache.isEmpty else return 0
return (self.collectionViewLayout as! bespokeCollectionViewLayout).cache.last!.indexPath.section + 1
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
guard !(self.collectionViewLayout as! bespokeCollectionViewLayout).cache.isEmpty else return 0
var n : Int = 0
for element in (self.collectionViewLayout as! bespokeCollectionViewLayout).cache
if element.indexPath.section == section
if element.indexPath.row > n
n = element.indexPath.row
print("Section \(section) has \(n) elements")
return n + 1
override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)
let change = sourceData[sourceIndexPath.row]
sourceData.remove(at: sourceIndexPath.row)
sourceData.insert(change, at: destinationIndexPath.row)
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
// Clean
for subview in cell.subviews
subview.removeFromSuperview()
let label : UILabel =
let l : UILabel = UILabel()
l.font = UIFont.systemFont(ofSize: 17)
l.frame = CGRect(origin: CGPoint.zero, size: cell.frame.size)
l.textColor = UIColor.black
let index : Int =
var i : Int = 0
for sectionCount in 0..<indexPath.section
i += (self.collectionView?.numberOfItems(inSection: sectionCount))!
i += indexPath.row
return i
()
l.text = sourceData[index]
return l
()
cell.addSubview(label)
return cell
class bespokeCollectionViewLayout : UICollectionViewLayout
var cache : [UICollectionViewLayoutAttributes] = [UICollectionViewLayoutAttributes]()
let contentWidth: CGFloat
var dimensions : [CGSize]!
init(contentWidth: CGFloat)
self.contentWidth = contentWidth
super.init()
required init?(coder aDecoder: NSCoder)
fatalError("init(coder:) has not been implemented")
override func prepare() -> Void
guard self.dimensions != nil else return
if cache.isEmpty
var xOffset : CGFloat = 0
var yOffset : CGFloat = 0
var rowCount = 0
var wordCount : Int = 0
while wordCount < dimensions.count
let nextRowCount : Int =
var totalWidth : CGFloat = 0
var numberOfWordsInRow : Int = 0
while totalWidth < contentWidth && wordCount < dimensions.count
if totalWidth + dimensions[wordCount].width >= contentWidth
break
else
totalWidth += dimensions[wordCount].width
wordCount += 1
numberOfWordsInRow += 1
return numberOfWordsInRow
()
var columnCount : Int = 0
for count in (wordCount - nextRowCount)..<wordCount
let index : IndexPath = IndexPath(row: columnCount, section: rowCount)
let newAttribute : UICollectionViewLayoutAttributes = UICollectionViewLayoutAttributes(forCellWith: index)
let cellFrame : CGRect = CGRect(origin: CGPoint(x: xOffset, y: yOffset), size: dimensions[count])
newAttribute.frame = cellFrame
cache.append(newAttribute)
xOffset += dimensions[count].width
columnCount += 1
xOffset = 0
yOffset += dimensions[0].height
rowCount += 1
override var collectionViewContentSize: CGSize
guard !cache.isEmpty else return CGSize(width: 100, height: 100)
return CGSize(width: self.contentWidth, height: cache.last!.frame.maxY)
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
var layoutAttributes = [UICollectionViewLayoutAttributes]()
if cache.isEmpty
self.prepare()
for attributes in cache
if attributes.frame.intersects(rect)
layoutAttributes.append(attributes)
return layoutAttributes
【讨论】:
感谢您的工作,我将尝试理解并实施这一点。我会及时通知你 @ale00 没问题,我喜欢它。我很欣赏它相对复杂,但如果你运行它并通过代码工作,它应该是有意义的。我发现在这种情况下,定制的集合视图布局可以提供更多控制。 我已经运行了代码并且它运行良好,然后我尝试使用手势识别器实现重新排序,但是如果我在实际行中移动单元格,一个单元格会消失,如果我将它移到外面应用程序崩溃。这是我现在的代码:gist.github.com/ale00/a34c575e1d4f6f16774c68a921759d3b,更改从第 88 行开始。也许我做错了。 (抱歉英语语言错误) @ale00 请参阅上面的更新代码。我认为这现在实现了您正在寻找的东西,所以如果您认为它对您有所帮助,请随时接受它! 很抱歉我的不活动。当我尝试在我的应用程序中实现它时,我注意到集合视图的尺寸是在开始时设置的并且永远不会改变,所以如果我改变设备的方向会有视觉问题以上是关于使用自定义维度重新排序集合视图单元格时出现问题的主要内容,如果未能解决你的问题,请参考以下文章
重新加载行以更新 UITableView 中的单元格高度时出现问题