UITableViewAutomaticDimension을 사용하여 UITableViewCell을 제자리에서 업데이트 한 후 불규칙한 스크롤


78

사용자가 제출 한 게시물에 대한 피드보기가있는 앱을 만들고 있습니다. 이보기에는 UITableView사용자 정의 UITableViewCell구현이 있습니다. 이 셀 안에는 UITableView주석을 표시하기위한 다른 것이 있습니다 . 요점은 다음과 같습니다.

Feed TableView
  PostCell
    Comments (TableView)
      CommentCell
  PostCell
    Comments (TableView)
      CommentCell
      CommentCell
      CommentCell
      CommentCell
      CommentCell

초기 피드는 미리보기를 위해 3 개의 댓글과 함께 다운로드되지만 댓글이 더 있거나 사용자가 댓글을 추가 또는 삭제하는 경우 PostCell내부 CommentCells댓글 테이블을 추가하거나 제거하여 피드 테이블보기 내부에서 업데이트하고 싶습니다. 의 PostCell. 나는 현재 다음 도우미를 사용하여이를 수행하고 있습니다.

// (PostCell.swift) Handle showing/hiding comments
func animateAddOrDeleteComments(startRow: Int, endRow: Int, operation: CellOperation) {
  let table = self.superview?.superview as UITableView

  // "table" is outer feed table
  // self is the PostCell that is updating it's comments
  // self.comments is UITableView for displaying comments inside of the PostCell
  table.beginUpdates()
  self.comments.beginUpdates()

  // This function handles inserting/removing/reloading a range of comments
  // so we build out an array of index paths for each row that needs updating
  var indexPaths = [NSIndexPath]()
  for var index = startRow; index <= endRow; index++ {
    indexPaths.append(NSIndexPath(forRow: index, inSection: 0))
  }

  switch operation {
  case .INSERT:
    self.comments.insertRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
  case .DELETE:
    self.comments.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
  case .RELOAD:
    self.comments.reloadRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
  }

  self.comments.endUpdates()
  table.endUpdates()

  // trigger a call to updateConstraints so that we can update the height constraint 
  // of the comments table to fit all of the comments
  self.setNeedsUpdateConstraints()
}

override func updateConstraints() {
  super.updateConstraints()
  self.commentsHeight.constant = self.comments.sizeThatFits(UILayoutFittingCompressedSize).height
}

이것은 업데이트를 잘 수행합니다. 게시물은 PostCell예상대로 내부에 추가되거나 제거 된 댓글로 업데이트 됩니다. PostCells피드 테이블에서 자동 크기 조정 을 사용하고 있습니다. 의 주석 테이블이 PostCell확장되어 모든 주석이 표시되지만 애니메이션이 약간 불안정하고 셀 업데이트 애니메이션이 발생하는 동안 테이블 종류가 12 픽셀 정도 위아래로 스크롤됩니다.

크기 조정 중 점프하는 것은 약간 성가 시지만 내 주요 문제는 나중에 발생합니다. 이제 피드에서 아래로 스크롤하면 이전처럼 스크롤이 부드럽지만 댓글을 추가 한 후 크기를 조정 한 셀 위로 스크롤하면 피드가 피드 상단에 도달하기 전에 몇 번 뒤로 건너 뜁니다. 다음 iOS8과 같이 피드에 대한 자동 크기 조정 셀을 설정 했습니다.

// (FeedController.swift)
// tableView is the feed table containing PostCells
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 560

을 제거하면 estimatedRowHeight셀 높이가 변경 될 때마다 표가 맨 위로 스크롤됩니다. 나는 지금 이것에 꽤 갇혀 있고 새로운 iOS 개발자로서 당신이 가질 수있는 팁을 사용할 수 있다고 느낍니다.

답변:


120

다음은 이러한 종류의 문제를 해결하기 위해 찾은 최상의 솔루션입니다 (스크롤링 문제 + reloadRows + iOS 8 UITableViewAutomaticDimension);

tableView가 셀을 표시하므로 사전에 모든 높이를 유지하고 (사전에서) 업데이트하여 구성됩니다.

그런 다음 - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath메서드 에서 저장된 높이를 반환합니다 .

다음과 같이 구현해야합니다.

목표 -C

- (void)viewDidLoad {
    [super viewDidLoad];

    self.heightAtIndexPath = [NSMutableDictionary new];
    self.tableView.rowHeight = UITableViewAutomaticDimension;
}

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSNumber *height = [self.heightAtIndexPath objectForKey:indexPath];
    if(height) {
        return height.floatValue;
    } else {
        return UITableViewAutomaticDimension;
    }
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    NSNumber *height = @(cell.frame.size.height);
    [self.heightAtIndexPath setObject:height forKey:indexPath];
}

스위프트 3

@IBOutlet var tableView : UITableView?
var heightAtIndexPath = NSMutableDictionary()

override func viewDidLoad() {
    super.viewDidLoad()

    tableView?.rowHeight = UITableViewAutomaticDimension
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    if let height = heightAtIndexPath.object(forKey: indexPath) as? NSNumber {
        return CGFloat(height.floatValue)
    } else {
        return UITableViewAutomaticDimension
    }
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    let height = NSNumber(value: Float(cell.frame.size.height))
    heightAtIndexPath.setObject(height, forKey: indexPath as NSCopying)
}

2
정말 간단하고 효과적인 솔루션입니다. 감사!
o15a3d4l11s2

1
궁금한 점이 있습니다. NSMutableDictionary 대신 Swift Dictionary를 사용할 수없는 이유가 있습니까? 그런데이 솔루션은 훌륭하게 작동합니다. 감사합니다!
AppreciateIt

3
@dosdos 신의 축복을 빌어 요, 친구!
adnako

5
나를 위해 여전히 작동하지 않고 자체 정의 된 imageView가 있으며 너비와 높이의 치수를 기반으로 높이 제한을 업데이트합니다. 셀 높이가 캐시 된 경우에도 스크롤은 여전히 ​​점프하고 있습니다. 특히 화면의 현재 셀과 높이가 다른 새 셀이 화면으로 스크롤되기 전에는 더욱 그렇습니다.
TonyTony

1
굉장한! 훌륭한! 정말 잘 작동합니다 !!!!!! 정말 고맙습니다! 해결책으로 표시 할 수 있다고 생각합니다.
ndominati2

30

우리는 같은 문제가있었습니다. 이는 SDK가 잘못된 높이를 강제로 적용하여 위로 스크롤 할 때 셀 점프를 유발하는 잘못된 셀 높이 추정에서 비롯됩니다. 셀을 구축 한 방법에 따라이 문제를 해결하는 가장 좋은 방법은 UITableViewDelegate방법 을 구현하는 것입니다.- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath

당신의 추정치가 셀 높이의 실제 값에 매우 가깝다면, 이것은 점프와 멍청함을 거의 제거 할 것입니다. 구현 방법은 다음과 같습니다. 논리를 얻을 수 있습니다.

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    // This method will get your cell identifier based on your data
    NSString *cellType = [self reuseIdentifierForIndexPath:indexPath];

    if ([cellType isEqualToString:kFirstCellIdentifier])
        return kFirstCellHeight;
    else if ([cellType isEqualToString:kSecondCellIdentifier])
        return kSecondCellHeight;
    else if ([cellType isEqualToString:kThirdCellIdentifier])
        return kThirdCellHeight;
    else {
        return UITableViewAutomaticDimension;
    }
}

Swift 2 지원 추가

func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    // This method will get your cell identifier based on your data
    let cellType = reuseIdentifierForIndexPath(indexPath)

    if cellType == kFirstCellIdentifier 
        return kFirstCellHeight
    else if cellType == kSecondCellIdentifier
        return kSecondCellHeight
    else if cellType == kThirdCellIdentifier
        return kThirdCellHeight
    else
        return UITableViewAutomaticDimension  
}

1
결국 heightForRowAtIndexPath 메서드를 구현하고 결과를 캐싱하여 약간의 관련이 있고 피드가 길 수 있기 때문에 성능을 개선했습니다. 사용자가 피드의 게시물 중 하나에 댓글을 추가 / 삭제하거나로드 할 때 스크롤 중에 다시 계산되도록 해당 셀의 높이 계산을 무효화합니다. 저키 함이 사라졌습니다. 코드를 단순화하고 새로운 높이 계산 기능을 활용할 수 있기를 바랍니다.하지만 내 TableViewCell에서 충분히 잘 작동하도록 할 수 없었습니다
Bryan Alger

2
위에서 설명한 방법으로 시도해 보셨습니까? 이것은 iOS 8에서 수행해야하는 방식이며 프레임 워크가 이미 처리하므로 높이를 계산해서는 안됩니다. heightForRowAtIndexPath 메서드를 구현하는 경우 단순히 SDK의 동작을 재정의하는 것입니다.
Gabriel Cartier

7
@BryanAlger가 정확합니다. 자동 행 높이는 높이 변동이 많은 행이 많은 테이블에는 사용할 수 없습니다. 안정적이고 부드러운 스크롤을 위해서는 heightForRowAtIndexPath 메서드에 올바른 결과를 제공해야하며, 가급적이면 캐시 된 행 높이를 사용해야합니다. 그렇지 않으면 tableView가 contentSize를 업데이트해야 할 때, 특히 다른 뷰 컨트롤러를 푸시하거나 표시하고 돌아올 때와 같은 경우에 멍청이 생길 것입니다. 불행히도 자동 행 높이는 몇 개의 행이있는 간단한 tableView에만 사용할 수 있습니다.
Jamie Hamick 2015 년

2
heightForRowAtIndexPath를 구현하는 경우 자동 셀 높이 차원의 기능을 사용하지 않습니다. 물론 구현하면 작동하지만 셀은 동적이 아닙니다.
Gabriel Cartier

1
제 경우에는에서 이미 셀 구성, 높이 계산 및 높이 캐싱을 구현 heightForRowAtIndexPath했지만 여전히 UITableView스크롤 이 불안정했습니다 . @GabrielCartier의 답변에 따라 셀 유형에 따라 더 구체적인 논리를 추가하면 실제로 문제를 해결하고 해결했습니다. 감사합니다!
Sakiboy

23

dosdos 답변은 Swift 2 에서 나를 위해 일했습니다.

ivar 선언

var heightAtIndexPath = NSMutableDictionary()

func에서 viewDidLoad ()

func viewDidLoad() {
  .... your code
  self.tableView.rowHeight = UITableViewAutomaticDimension
}

그런 다음 다음 두 가지 방법을 추가하십시오.

override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
   let height = self.heightAtIndexPath.objectForKey(indexPath)
   if ((height) != nil) {
     return CGFloat(height!.floatValue)
   } else {
    return UITableViewAutomaticDimension
   }
 }

override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
  let height = cell.frame.size.height
  self.heightAtIndexPath.setObject(height, forKey: indexPath)
}

SWIFT 3 :

var heightAtIndexPath = [IndexPath: CGFloat]()

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return self.heightAtIndexPath[indexPath] ?? UITableViewAutomaticDimension
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    self.heightAtIndexPath[indexPath] = cell.frame.size.height
}

1
감사합니다! 잘 작동합니다 :)
Michael

놀랍고 깜박임이 전혀 제거되었습니다.
AVEbrahimi

SWIFT 3에 대한 업데이트 추가 (viewDidLoad의 self.tableView.rowHeight = UITableViewAutomaticDimension을 잊지 마세요)
MLBDG

불행히도 나를 위해 실제로 도움이되지 않았습니다. 여기서 자동 레이아웃은 약간 이상합니다.
nickdnk

1
안녕하세요, 타이핑 된 사전을 사용하기 위해 답변을 편집했습니다. 내 자신의 빠른 4 코드를 대답으로 붙여넣고 싶었지만 편집으로 충분하다는 것을 알았습니다. 괜찮 으시길 바랍니다.
manmal

3

@dosdos 솔루션이 잘 작동합니다.

하지만 추가해야 할 것이 있습니다

@dosdos 답변 다음

스위프트 3/4

@IBOutlet var tableView : UITableView!
var heightAtIndexPath = NSMutableDictionary()

override func viewDidLoad() {
    super.viewDidLoad()

    tableView?.rowHeight = UITableViewAutomaticDimension
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    if let height = heightAtIndexPath.object(forKey: indexPath) as? NSNumber {
        return CGFloat(height.floatValue)
    } else {
        return UITableViewAutomaticDimension
    }
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    let height = NSNumber(value: Float(cell.frame.size.height))
    heightAtIndexPath.setObject(height, forKey: indexPath as NSCopying)
}

그런 다음 원할 때 마다이 줄을 사용하십시오. 저는 textDidChange 내부에서 사용합니다.

  1. 먼저 Tableview를 다시로드
  2. 업데이트 제약
  3. 마지막으로 위로 이동 Tableview

    tableView.reloadData()
    self.tableView.layoutIfNeeded()
    self.tableView.setContentOffset(CGPoint.zero, animated: true)
    

2

나도 같은 문제에 직면했다. 해결 방법을 찾았지만 멍청이를 완전히 고치는 것은 아닙니다. 그러나 이전의 고르지 않은 스크롤에 비해 훨씬 나은 것 같습니다.

당신의에서 UITableView위임 방법 :cellForRowAtIndexPath:, 셀을 반환하기 전에 제약을 업데이트하려면 다음 두 가지 방법을 사용해보십시오. (신속한 언어)

cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()

편집 :tableView.estimatedRowHeight 더 부드러운 스크롤을 얻으려면 값을 가지고 놀아야 할 수도 있습니다 .


5
이 메서드를 사용하지 않는 것이 좋습니다. cellForRowAtIndexPath와 같은 메서드에서 자동 레이아웃 메서드를 호출하면 TableView의 성능에 큰 영향을 미칠 수 있습니다.
Gabriel Cartier

1

팔로우 @dosdos 답변을 .

나는 또한 구현하는 것이 흥미로웠다. tableView(tableView: didEndDisplayingCell: forRowAtIndexPath:

특히 셀이 이미 화면에 표시되는 동안 셀이 제약 조건을 동적으로 변경하는 내 코드의 경우. 이와 같이 사전을 업데이트하면 셀이 두 번째로 표시 될 때 도움이됩니다.

var heightAtIndexPath = [NSIndexPath : NSNumber]()

....

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = UITableViewAutomaticDimension

....

extension TableViewViewController: UITableViewDelegate {

    //MARK: - UITableViewDelegate

    func tableView(tableView: UITableView,
                   estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {

        let height = heightAtIndexPath[indexPath]

        if let height = height {

            return CGFloat(height)
        }
        else {

            return UITableViewAutomaticDimension
        }
    }

    func tableView(tableView: UITableView,
                   willDisplayCell cell: UITableViewCell,
                                   forRowAtIndexPath indexPath: NSIndexPath) {

        let height: NSNumber = CGRectGetHeight(cell.frame)
        heightAtIndexPath[indexPath] = height
    }

    func tableView(tableView: UITableView,
                   didEndDisplayingCell cell: UITableViewCell,
                                        forRowAtIndexPath indexPath: NSIndexPath) {

        let height: NSNumber = CGRectGetHeight(cell.frame)
        heightAtIndexPath[indexPath] = height
    }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.