(很高兴接受Swift或Objective-C的答案)
我的表视图有几个部分,当按下一个按钮时,我想在第0部分的末尾插入一行。再次按下该按钮,我想删除同一行。我几乎工作的代码看起来像这样:
// model is an array of mutable arrays, one for each section
- (void)pressedAddRemove:(id)sender {
self.adding = !self.adding; // this is a BOOL property
self.navigationItem.rightBarButtonItem.title = (self.adding)? @"Remove" : @"Add";
// if adding, add an object to the end of section 0
// tell the table view to insert at that index path
[self.tableView beginUpdates];
NSMutableArray *sectionArray = self.model[0];
if (self.adding) {
NSIndexPath *insertionPath = [NSIndexPath indexPathForRow:sectionArray.count inSection:0];
[sectionArray addObject:@{}];
[self.tableView insertRowsAtIndexPaths:@[insertionPath] withRowAnimation:UITableViewRowAnimationAutomatic];
// if removing, remove the object from the end of section 0
// tell the table view to remove at that index path
} else {
NSIndexPath *removalPath = [NSIndexPath indexPathForRow:sectionArray.count-1 inSection:0];
[sectionArray removeObject:[sectionArray lastObject]];
[self.tableView deleteRowsAtIndexPaths:@[removalPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
[self.tableView endUpdates];
}
这有时会正常运行,但有时不会,这取决于滚动表格视图的位置:
我可以看到这种情况发生在慢动作模拟器中,使用" Debug-> Toggle Slow Animations"。删除时反向出现同样的问题。
我发现偏移量的跳跃大小与表格滚动到第0部分的距离有关:当偏移很小时跳跃很小。当滚动接近第0部分总高度的 half 时,跳跃变得更大(问题在于它在这里最差,跳跃= =部分高度的一半)。进一步滚动,跳跃变小。当滚动表格以便只能看到少量的0部分时,跳跃很小。
你能帮助我理解这是为什么以及如何解决吗?
答案 0 :(得分:30)
在iOS 11上,UITableView使用估计的行高作为默认值。
在插入/重新加载或删除行时会导致不可预测的行为,因为UITableView在大多数情况下的内容大小都是错误的:
为了避免过多的布局计算,tableView仅为每个heightForRow
调用询问cellForRow
并记住它(在正常模式下,tableView向heightForRow
询问tableView的所有indexPath )。其余单元格的高度等于estimatedRowHeight
值,直到调用相应的cellForRow
为止。
// estimatedRowHeight mode
contentSize.height = numberOfRowsNotYetOnScreen * estimatedRowHeight + numberOfRowsDisplayedAtLeastOnce * heightOfRow
// normal mode
contentSize.height = heightOfRow * numberOfCells
一种解决方案是通过将estimatedRowHeight设置为0并为每个单元格实现estimatedRowHeight
来禁用heightForRow
模式。
当然,如果您的单元格具有动态高度(大部分时间使用繁重的布局计算,因此您有充分理由使用estimatedRowHeight
),则必须找到重现estimatedRowHeight
优化的方法在不影响tableView的contentSize的情况下。请查看AsyncDisplayKit或UITableView-FDTemplateLayoutCell。
另一种解决方案是尝试找到适合的estimatedRowHeight
。特别是在iOS 11上,您可以尝试将UITableViewAutomaticDimension
用于estimatedRowHeight:
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = UITableViewAutomaticDimension
答案 1 :(得分:3)
我不知道如何正确解决它,但是我的解决方案对我有用
generate series
答案 2 :(得分:2)
尝试禁用UIView动画,对我来说它是可行的。
UIView.setAnimationsEnabled(false)
self.tableView.deleteRows(at: [indexPath], with: .automatic)
UIView.setAnimationsEnabled(true)
答案 3 :(得分:0)
对于我来说,这是在具有多个节的UITableView上发生的,但是没有定义这些节的标题高度或视图的定义。添加以下委托方法可以为我解决此问题-希望对您有所帮助!
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 0
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return nil
}
答案 4 :(得分:0)
@GaétanZ的解决方案对我没有帮助(IOS12),但他的概念是正确的。
所以我做了下一个逻辑步骤:
如果表格内容不知道单元格有多高,那么在插入单元格之后就让“继续滚动”即可吧
private func insertBottomBubble(withCompletionHandler completion: (() -> Void)?) {
let bottomIndexPath = IndexPath(row: cbModelViewController!.viewModelsCount - 1, section: 0)
CATransaction.begin()
CATransaction.setAnimationDuration(0.9)
CATransaction.setCompletionBlock {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.scrollToBottom(withCompletionHandler: completion)
}
}
tableView.insertRows(at: [bottomIndexPath], with: isLeft == true ? .left : .right)
self.scrollToBottom(withCompletionHandler: nil) // no jump, keep it down :D
CATransaction.commit()
}
func scrollToBottom(withCompletionHandler completion: (() -> Void)?) {
let bottomMessageIndexPath = IndexPath(row: tableView.numberOfRows(inSection: 0) - 1, section: 0)
UIView.animate(withDuration: 0.45,
delay: TimeInterval(0),
options: UIView.AnimationOptions.curveEaseInOut,
animations: {
self.tableView.scrollToRow(at: bottomMessageIndexPath, at: .bottom, animated: false)
},
completion: { success in
if success {
completion?()
}
})
仅iOS 12经过测试
答案 5 :(得分:0)
我通过缓存单元格行的高度以及部分页脚和页眉的高度来修复跳转。该方法要求对节和行具有唯一的缓存标识符。
// Define caches
private lazy var sectionHeaderHeights = SmartCache<NSNumber>(type: type(of: self))
private lazy var sectionFooterHeights = SmartCache<NSNumber>(type: type(of: self))
private lazy var cellRowHeights = SmartCache<NSNumber>(type: type(of: self))
// Cache section footer height
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
let section = sections[section]
switch section {
case .general:
let view = HeaderFooterView(...)
view.sizeToFit(width: tableView.bounds.width)
sectionFooterHeights.set(cgFloat: view.bounds.height, forKey: section.cacheID)
return view
case .something:
...
}
}
// Cache cell height
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let section = sections[indexPath.section]
switch section {
case .general:
cellRowHeights.set(cgFloat: cell.bounds.height, forKey: section.cacheID)
case .phones(let items):
let item = items[indexPath.row]
cellRowHeights.set(cgFloat: cell.bounds.height, forKey: section.cacheID + item.cacheID)
case .something:
...
}
}
// Use cached section footer height
func tableView(_ tableView: UITableView, estimatedHeightForFooterInSection section: Int) -> CGFloat {
let section = sections[section]
switch section {
default:
return sectionFooterHeights.cgFloat(for: section.cacheID) ?? 44
case .something:
...
}
}
// Use cached cell height
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
let section = sections[indexPath.section]
switch section {
case .general:
return cellRowHeights.cgFloat(for: section.cacheID) ?? 80
case .phones(let items):
let item = items[indexPath.row]
return cellRowHeights.cgFloat(for: section.cacheID + item.cacheID) ?? 120
case .something:
...
}
}
用于缓存的可重用类如下所示:
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif
public class SmartCache<ObjectType: AnyObject>: NSCache<NSString, AnyObject> {
}
public extension SmartCache {
public convenience init(name: String) {
self.init()
self.name = name
}
public convenience init(type: AnyObject.Type) {
self.init()
name = String(describing: type)
}
public convenience init(limit: Int) {
self.init()
totalCostLimit = limit
}
}
extension SmartCache {
public func isObjectCached(key: String) -> Bool {
let value = object(for: key)
return value != nil
}
public func object(for key: String) -> ObjectType? {
return object(forKey: key as NSString) as? ObjectType
}
public func object(for key: String, _ initialiser: () -> ObjectType) -> ObjectType {
let existingObject = object(forKey: key as NSString) as? ObjectType
if let existingObject = existingObject {
return existingObject
} else {
let newObject = initialiser()
setObject(newObject, forKey: key as NSString)
return newObject
}
}
public func object(for key: String, _ initialiser: () -> ObjectType?) -> ObjectType? {
let existingObject = object(forKey: key as NSString) as? ObjectType
if let existingObject = existingObject {
return existingObject
} else {
let newObject = initialiser()
if let newObjectInstance = newObject {
setObject(newObjectInstance, forKey: key as NSString)
}
return newObject
}
}
public func set(object: ObjectType, forKey key: String) {
setObject(object, forKey: key as NSString)
}
}
extension SmartCache where ObjectType: NSData {
public func data(for key: String, _ initialiser: () -> Data) -> Data {
let existingObject = object(forKey: key as NSString) as? NSData
if let existingObject = existingObject {
return existingObject as Data
} else {
let newObject = initialiser()
setObject(newObject as NSData, forKey: key as NSString)
return newObject
}
}
public func data(for key: String) -> Data? {
return object(forKey: key as NSString) as? Data
}
public func set(data: Data, forKey key: String) {
setObject(data as NSData, forKey: key as NSString)
}
}
extension SmartCache where ObjectType: NSNumber {
public func float(for key: String, _ initialiser: () -> Float) -> Float {
let existingObject = object(forKey: key as NSString)
if let existingObject = existingObject {
return existingObject.floatValue
} else {
let newValue = initialiser()
let newObject = NSNumber(value: newValue)
setObject(newObject, forKey: key as NSString)
return newValue
}
}
public func float(for key: String) -> Float? {
return object(forKey: key as NSString)?.floatValue
}
public func set(float: Float, forKey key: String) {
setObject(NSNumber(value: float), forKey: key as NSString)
}
public func cgFloat(for key: String) -> CGFloat? {
if let value = float(for: key) {
return CGFloat(value)
} else {
return nil
}
}
public func set(cgFloat: CGFloat, forKey key: String) {
set(float: Float(cgFloat), forKey: key)
}
}
#if os(iOS) || os(tvOS) || os(watchOS)
public extension SmartCache where ObjectType: UIImage {
public func image(for key: String) -> UIImage? {
return object(forKey: key as NSString) as? UIImage
}
public func set(value: UIImage, forKey key: String) {
if let cost = cost(for: value) {
setObject(value, forKey: key as NSString, cost: cost)
} else {
setObject(value, forKey: key as NSString)
}
}
private func cost(for image: UIImage) -> Int? {
if let bytesPerRow = image.cgImage?.bytesPerRow, let height = image.cgImage?.height {
return bytesPerRow * height // Cost in bytes
}
return nil
}
private func totalCostLimit() -> Int {
let physicalMemory = ProcessInfo.processInfo.physicalMemory
let ratio = physicalMemory <= (1024 * 1024 * 512 /* 512 Mb */ ) ? 0.1 : 0.2
let limit = physicalMemory / UInt64(1 / ratio)
return limit > UInt64(Int.max) ? Int.max : Int(limit)
}
}
#endif
答案 6 :(得分:0)
保存估算的行高
private var cellHeight = [Int:CGFloat]()
override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeight[indexPath.row] = cell.frame.self.height
}
override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
if let height = cellHeight[indexPath.row] {
return height
}
return tableView.estimatedRowHeight
修复滚动原点Y
let indexPath = IndexPath(row: INDEX, section: 0)
tableView.beginUpdates()
tableView.insertRows(at: [indexPath], with: .fade)
tableView.endUpdates()
tableView.setContentOffset(tableView.contentOffset, animated: false)
答案 7 :(得分:0)
如果单元格的高度变化很大,就会发生此问题。 Vlad的解决方案效果很好。但是很难实现。我建议一种更简单的方法。在大多数情况下,它将为您提供帮助。
将变量private var cellHeightCache = [IndexPath: CGFloat]()
添加到控制器。并实现两个委托方法:
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeightCache[indexPath] ?? 44
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeightCache[indexPath] = cell.bounds.height
}
答案 8 :(得分:0)
每个人都在谈论估计的行高,这是真的。因此,将所有这些考虑在内是这样的想法:
将每行的高度存储在数据结构中(我选择一个字典),然后将字典中的该值用于heightForRowAtIndexPath和EstimateHeightForRowAtIndexPath方法
所以问题是,如果使用动态标签大小,如何获取行高。好简单,只需使用willDisplayCell方法找到单元格框架
这是我的全部工作版本,对于目标c感到抱歉,它只是我现在正在从事的项目:
为字典声明一个属性:
@property (strong) NSMutableDictionary *dictionaryCellHeights;
初始化字典:
self.dictionaryCellHeights = [[NSMutableDictionary alloc]init];
捕获高度:
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
NSNumber *height = [NSNumber numberWithDouble:cell.frame.size.height];
NSString *rowString = [NSString stringWithFormat:@"%d", indexPath.row];
[self.dictionaryCellHeights setObject:height forKey:rowString];
}
使用高度:
-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath{
NSNumber *height = [self getRowHeight:indexPath.row];
if (height == nil){
return UITableViewAutomaticDimension;
}
return height.doubleValue;
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
NSNumber *height = [self getRowHeight:indexPath.row];
if (height == nil){
return UITableViewAutomaticDimension;
}
return height.doubleValue;
}
-(NSNumber*)getRowHeight: (int)row{
NSString *rowString = [NSString stringWithFormat:@"%d", row];
return [self.dictionaryCellHeights objectForKey:rowString];
}
然后在插入行时:
[self.tableViewTouchActivities performBatchUpdates:^{
[self.tableViewTouchActivities insertRowsAtIndexPaths:toInsertIndexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
} completion:^(BOOL finished){
[self.tableViewTouchActivities finishInfiniteScroll];
}];
*注意-我正在使用此库进行infiniteScrolling https://github.com/pronebird/UIScrollView-InfiniteScroll/blob/master/README.md
答案 9 :(得分:0)
对于我的工作,请关闭对表视图行,节,标题的自动估计 我用heightForRowAt
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row % 2 == 1 {
if arrowsVisible {
return 20
}
return 5
}
}