[번역] UICollectionView Tutorial: Prefetching APIs

1Consumption·2020년 3월 18일
3

RayWenderLich

목록 보기
1/1

이 글은 Raywenderlich의 UICollectionView Tutorial: Prefetching APIs를 번역한 글입니다.
도저히 번역이 되지 않는 부분은 원문으로 남겨두었으니, 알려주시면 감사하겠습니다.

개발자라면, 항상 훌륭한 UX를 제공하기 위해 노력해야합니다. list를 표시하는 앱에서 이를 실천하는 한 가지 방법은 스크롤이 부드러운지를 확실히 하는 것입니다. iOS 10에서 Apple은 Collection View와 Table View가 필요하기 전에 데이터를 가져올 수 있는 UICollectionView prefetching APIs와, 일치하는 UITableView prefetching APIs를 소개했습니다.

When you come across an app with choppy scrolling, this is usually due to a long running process that’s blocking the main UI thread from updating. 당신은 터치 이벤트와 같은 것들에 반응할 수 있도록 메인 스레드를 자유롭게 유지하길 원합니다. 사용자는 데이터를 가져오고 표시하는데 시간이 오래 걸리는 것은 용서할 수 있지만, 사용자의 제스쳐에 반응하지 않으면 용서할 수 없습니다. 무거운 작업을 백그라운드 스레드로 옮기는 것은 즉각 반응하는 앱을 구축하는 첫번째 단계 입니다.

이 튜토리얼에서 당신은 이모티콘 모음을 표시하는 EmojiRater를 사용하여 작업을 시작할 것입니다. 불행히도, 그것의 스크롤링 성능은 많은 아쉬움을 남깁니다. 당신은 prefetch API를 사용하여 앱이 곧 표시할 셀을 확인하고, 백그라운드에서 관련된 데이터를 불러오게 합니다.

Getting Start

이 튜토리얼의 상단 또는 하단에 있는 자료 다운로드 버튼을 사용하여 starter project를 다운로드 하십시오. 빌드하고 실행하십시오. 시도하고 스크롤할 때 다음과 같은 내용이 표시되어야 합니다.

고통스럽지 않나요? 칠판 경험을 떠올리게 하나요? 하지만 신경쓰지 마세요. 좋은 뉴스는 당신이 이를 고칠 수 있다는 것입니다.

A little bit about the app. 앱은 좋아요/싫어요를 할 수 있는 이모지의 collection view를 표시합니다. 사용하려면 셀 중 하나를 클릭한 다음 haptic feedback이 느껴질 때까지 단단히 누릅니다. 등급 선택이 표시됩니다. 하나를 선택하고 업데이트된 collection view에서 결과를 확인합니다.

Note : 시뮬레이터에서 3D Touch를 작동시키는 데 문제가 있는 경우 먼저 "Force Touch" 기능이 있는 트랙패드가 있는 Mac 또는 MacBook이 필요합니다. 그런 다음 시스템 기본 설정 ▸ 트랙패드로 이동하여 강제 클릭 및 haptic feedback을 활성화할 수 있습니다. 3D Touch를 사용하는 iPhone이나 이러한 장치에 액세스할 수 없는 경우에도 이 튜토리얼의 기본 사항을 확인할 수 있습니다.

Xcode내의 프로젝트를 살펴봅니다. 다음은 주요 파일입니다:
EmojiRating.swift: 이모지를 나타내는 Model 입니다.
DataStore.swift: 이모지를 로드합니다.
EmojiViewCell.swift: 이모지를 표시하는 CollectionView cell 입니다.
RatingOverlayView.swift: 사용자가 이모지를 평가할 수 있는 View
EmojiViewController.swift: Collection View의 이모지를 표시합니다.

당신은 DataSourceEmojiViewController에 스크롤 성능을 향상시킬 기능을 추가할 것입니다.

Understanding Choppy Scrolling

앱이 초당 60프레임(FPS) 디스플레이 제약 조건을 충족하도록하여 부드러운 스크롤을 얻을 수 있습니다. 즉 앱이 초당 60번 UI를 고쳐야하므로 각 프레임은 내용을 render하는데 16ms가 걸립니다. The system drops frames that takes too long to show content.
따라서 앱이 프레인을 건너뛰고 다음 프레임으로 이동할 때 스크롤이 느려집니다. 프레입 드랍의 가능한 원인은 main 스레드를 차단하는 긴 작업입니다.

Apple은 당신을 돕기 위한 몇가지 유용한 툴을 제공했습니다. 먼저 긴 작업을 분리하고 그것을 백그라운드 스레드로 이동할 수 있습니다. 이는 당신이 메인스레드에서 발생하는 모든 터치 이벤트를 처리할 수 있도록 해줍니다. 백그라운드 작업이 완료되면, 작업을 기반으로 메인 스레드에서 필요한 UI 업데이트를 수행할 수 있습니다.

다음은 프레임이 드랍되는 시나리오를 보여줍니다:

작업을 백그라운드로 옮기면 다음과 같이 보여집니다.

이제 앱 성능을 향상시키기 위해 동시에 두개의 스레드가 실행 중임을 당신은 안다.

데이터를 표시하기전에 가져오는 것을 시작한다면 더 좋지 않을까요? UITableViewUICollectionView prefetching API가 제공됩니다.이 튜토리얼에서 collection view API를 사용합니다.

Loading Data Asynchronously

Apple은 앱에 동시성을 추가하는 여러가지 방법을 제공합니다. 너는 Grand Central Dispatch (GCD)를 가벼운 매커니즘으로 사용하여 작업을 동시에 실행할 수 있습니다. 또는 GCD에 구축된 Operation을 사용할 수 있습니다.

Operation은 오버헤드를 더 많이 추가하지만 재사용과 오퍼레이션 취소를 쉽게 합니다. 이 튜토리얼에서 Operation을 사용하면 더 이상 필요없는 이전에 시작했던 이모티콘 로드를 작업을 취소할 수 있습니다.

이제 EmojiRater에서 동시성을 가장 잘 활용할 수 있는 곳을 조사하기 시작할 때입니다.

EmojiViewController.swift를 열고 데이터 소스에서 collectionView(_:cellForItemAt:) 메소드를 찾습니다. 다음 코드를 보십시오.

if let emojiRating = dataStore.loadEmojiRating(at: indexPath.item) {
  cell.updateAppearanceFor(emojiRating, animated: true)
}

이모지를 표시하기 전에 데이터 소스에서 이모지를 로드합니다. 그것이 어떻게 구현되는지 알아 봅시다.

DataStore.swift를 열고 loading 메소드를 보십시오:

public func loadEmojiRating(at index: Int) -> EmojiRating? {
  if (0..<emojiRatings.count).contains(index) {
    let randomDelayTime = Int.random(in: 500..<2000)
    usleep(useconds_t(randomDelayTime * 1000))
    return emojiRatings[index]
  }
  return .none
}

이 코드는 500ms ~ 2000ms 범위의 임의 지연 후 유효한 이모지를 반환합니다. 딜레이는 다양한 조건 하에서 네트워크 리퀘스트의 인공 시뮬레이션입니다.

범인이 드러났습니다! 이모지를 불러오는 것은 메인 스레드에서 발생하였고, 16ms 임계값을 위반하여 프레임 드랍을 유발합니다. 이 문제를 해결합시다.

이 코드를 DataSource.swift의 마지막에 추가하십시오.

class DataLoadOperation: Operation {
  // 1
  var emojiRating: EmojiRating?
  var loadingCompleteHandler: ((EmojiRating) -> Void)?
  
  private let _emojiRating: EmojiRating
  
  // 2
  init(_ emojiRating: EmojiRating) {
    _emojiRating = emojiRating
  }
  
  // 3
  override func main() {
    // TBD: Work it!!
  }
}

Operation은 메인 스레드에서 이동하려는 작업을 구현하기 위해 서브 클래스로 만들어야하는 추상 클래스입니다.

다음은 코드에서 차례로 일어나는 상황입니다.
1. 이 operation에 사용할 이모지 및 comletion handler에 대한 참조를 만듭니다.
2. 이모티콘을 전달할 수 있는 지정 이니셜라이절르 만듭니다.
3. operation에 대한 실제 작업을 수행하려면 main() 메소드를 오버라이드 하십시오.

이제 다음 코드를 main()에 추가하십시오.

// 1
if isCancelled { return }
    
// 2
let randomDelayTime = Int.random(in: 500..<2000)
usleep(useconds_t(randomDelayTime * 1000))

// 3
if isCancelled { return }

// 4
emojiRating = _emojiRating

// 5  
if let loadingCompleteHandler = loadingCompleteHandler {
  DispatchQueue.main.async {
    loadingCompleteHandler(self._emojiRating)
  }
}

코드 진행은 다음과 같습니다.
1. 시작하기 전에 취소를 확인하십시오. 긴 또는 집중적인 작업을 시도하기 전에 작업이 취소되었는지 정기적으로 확인해야 합니다.
2. 긴 시간의 이모지 가져오기를 시뮬레이션 하십시오. 이 코드는 익숙해 보일 것입니다.
3. 작업이 취소되었는지 확인하십시오.
4. 가져오기가 완료되었음을 나타내도록 이모지를 할당합니다.
5. 메인 스레드에 있는 completion handler를 호출하여 이모지를 보냅니다. 그 다음 UI업데이트를 트리거 하여 이모지를 표시해야합니다.

loadEmojiRating(at:)을 다음으로 교체합니다.

public func loadEmojiRating(at index: Int) -> DataLoadOperation? {
  if (0..<emojiRatings.count).contains(index) {
    return DataLoadOperation(emojiRatings[index])
  }
  return .none
}

기존 코드에서 두가지 달라진 점이 있습니다.
1. 백그라운드에서 이모지를 가져오기 위해 DataLoadOperation()를 만들었습니다.
2. 이 메소드는 이제 EmojiRating optional대신 DataLoadOperation optional을 반환합니다.

You now need to take care of the method signature change and make use of your brand new operation.

EmojiViewController.swift를 열고 collectionView(_:cellForItemAt:)에서 다음 코드를 지우십시오

if let emojiRating = dataStore.loadEmojiRating(at: indexPath.item) {
  cell.updateAppearanceFor(emojiRating, animated: true)
}

더 이상 이 data source 메소드에서 데이터를 가져오지 않습니다. 대신 앱에서 collectionView cell을 표시하려고 할 때 호출되는 delegate 메소드에서 이 작업을 수행합니다. 아직 앞서 나가지 마세요...

클래스의 상단에 다음 속성을 추가합니다.

let loadingQueue = OperationQueue()
var loadingOperations: [IndexPath: DataLoadOperation] = [:]

첫번째 프로퍼티는 오퍼레이션 큐를 보유합니다. loadingOperations은 각 로드 오퍼레이션을 해당 index path를 통해 해당 셀과 연결하여 데이터 로드 작업을 추적하는 배열입니다.(loadingOperations is an array that tracks a data load operation, associating each loading operation with its corresponding cell via its index path.)
다음 코드를 파일의 마지막에 추가하십시오.

UICollectionViewDelegate에 대한 extension을 만들고 collectionView (_ : willDisplay : forItemAt :) 델리게이트 메소드를 구현합니다. 방법을 단계별로 살펴보십시오.

  1. 데이터가 로드 된 후 셀이 업데이트 되는 방식을 처리할 클로저를 만듭니다.
  2. 셀에 대한 데이터 로드 오퍼레이션이 있는지 확인하십시오.
  3. 데이터 로딩 오퍼레이션이 완료되었는지 확인하십시오. 만약 그렇다면, 셀의 UI를 업데이트 해주고 추적 배열에서 오퍼레이션을 제거하십시오.
  4. 이모지를 가져 오지 않은 경우 데이터 로드 completion handler에 클로저를 지정하십시오.
  5. 데이터 로딩 오퍼레이션이 없는 경우 이모지에 대한 새로운 오퍼레이션을 생성하십시오.
  6. data-loading completion handler에 클로저를 추가하십시오.
  7. 오퍼레이션을 오퍼레이션큐에 넣으십시오.
  8. 오퍼레이션 추적 배열에 data loader를 넣으십시오.

셀이 colelctionView에서 제거될 때 정리를 수행해야 합니다.

UICollectionViewDelegate extension에 다음 메소드를 추가하십시오.

override func collectionView(_ collectionView: UICollectionView,
  didEndDisplaying cell: UICollectionViewCell,
  forItemAt indexPath: IndexPath) {
  if let dataLoader = loadingOperations[indexPath] {
    dataLoader.cancel()
    loadingOperations.removeValue(forKey: indexPath)
  }
}

이 코드는 셀에 연결된 기존 데이터 로드 작업을 확인합니다. 존재하는 경우 다운로드를 취소하고 작업을 추적하는 어레이에서 작업을 제거합니다.

앱을 빌드하고 실행하십시오. 이모진를 스크롤하여 앱 성능 향상을 확인합니다.

collection view cell이 표시될 것으로 예상하여 데이터를 불러올 수 있으면 더 좋습니다. prefetch API를 사용하여 이를 수행하고 EmojiRater에 추가 향상을 제공합니다.

Enabling UICollectionView Prefetching

UICollectionViewDataSourcePrefetching 프로토콜은 collection view의 데이터가 곧 필요할수도 있음을 미리 경고합니다. 이 정보를 사용하여 셀이 visible할 때 데이터를 이미 사용할 수 있도록 데이터를 prefetching할 수 있습니다. This works in conjunction with the concurrency work you've already done — the key difference being when the work gets kicked off.

아래 다이어그램은 이것이 어떻게 작동하는지를 보여줍니다. 사용자는 collection view에서 위로 스크롤하고 있습니다. 노란색 셀이 곧 view에 들어갈 것입니다. 이것이 프레임3에서 발행하고, 우리는 프레임1에 있다고 가정합시다.

prefetch 프로토콜을 채택하면 보여지기 시작할 수 있는 다음셀에 대해 앱에 알립니다. prefetching 트리거가 없으면, 프레임3에서 데이터를 불러오기 시작하고 셀의 데이터는 나중에 표시됩니다. prefetch 떄문에 셀 데이터는 셀이 보이기 전까지 준비가 됩니다.

EmojiViewController.swift를 열고 파일의 끝에 다음 코드를 삽입하십시오.

// MARK: - UICollectionViewDataSourcePrefetching
extension EmojiViewController: UICollectionViewDataSourcePrefetching {
  func collectionView(_ collectionView: UICollectionView,
      prefetchItemsAt indexPaths: [IndexPath]) {
    print("Prefetch: \(indexPaths)")
  }
}

EmojiViewController는 이제 UICollectionViewDataSourcePrefetching을 채택했고 and 요구되는 delegate 메소드를 구현했습니다. 이 구현은 곧 볼 수 있는 index path를 출력합니다.
viewDidLoad()에서 super.viewDidLoad(): 다음에 아래 코드를 삽입 하십시오.

collectionView?.prefetchDataSource = self

이것은 EmojiViewController를 collection view의 prefetching data source 설정합니다.

앱을 빌드하고 실행하면 스크롤 하기 전에 Xcode 콘솔을 확인하십시오. 다음과 같이 보일 것입니다:

Prefetch: [[0, 10], [0, 11], [0, 12], [0, 13], [0, 14], [0, 15]]

이들은 아직 보이지 않는 셀들에 해당합니다. 이제 더 스크롤하고 콘솔로그를 확인하십시오. 당신은 아직 보지이 않는 index path 기반으로 로그 메세지를 봐야합니다. 이것이 어떻게 작동하는지 잘 이해할 때까지 위아래로 스크롤 해보십시오.

당신은 왜 이 delegate 메소드가 작업할 index path를 제공하는지 궁금할 것입니다. 이 메소드에서 데이터 로딩 프로세스를 개시한 다음 collectionView(_:cellForItemAt:) 또는 collectionView(_:willDisplay:forItemAt:)에서 결과를 처리해야합니다. 셀이 즉시 필요한 경우 delegate 메소드는 호출되지 않습니다. 그러므로 당신은 이 메소드로 셀에 데이터를 로드하는데 의존하면 안됩니다.

Prefetching Data Asyncronously

EmojiViewController.swift에서 , print()을 다음과 같이 바꿔서 collectionView(_:prefetchItemsAt:) 를 수정하십시오.

for indexPath in indexPaths {
  // 1
  if let _ = loadingOperations[indexPath] {
    continue
  }
  // 2
  if let dataLoader = dataStore.loadEmojiRating(at: indexPath.item) {
    // 3
    loadingQueue.addOperation(dataLoader)
    loadingOperations[indexPath] = dataLoader
  }
}

코드는 메소드가 수신하는 index path를 반복하여 다음을 수행합니다:
1. 이 셀에 기존 로드 오퍼레이션이 있는지 확인합니다. 만약 있다면 아무것도 하지 않습니다.
2. 로딩 오퍼레이션이 발견되지 않는다면 데이터 로드 오퍼레이션을 만듭니다.
3. 오퍼레이션을 큐에 넣고 데이터 로드 오퍼레이션을 추적하는 딕셔너리를 업데이트 합니다.

collectionView(_:prefetchItemsAt:)에 전달된 index paths는 Collection view의 geometric distance를 기준으로 우선순위에 따라 정렬됩니다. 이는 가장 필요할 것 같은 셀을 불러오도록 해줍니다.

로드 오퍼레이션의 결과를 처리하기 위해 collectionView(_:willDisplay:forItemAt:)에 이전에 코드를 추가한 것을 상기 하십시오. 아래 해당 메소드의 주요 내용을 보십시오:

override func collectionView(_ collectionView: UICollectionView,
    willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
  // ...
  let updateCellClosure: (EmojiRating?) -> Void = { [weak self] emojiRating in
    guard let self = self else {
      return
    }
    cell.updateAppearanceFor(emojiRating, animated: true)
    self.loadingOperations.removeValue(forKey: indexPath)
  }
  
  if let dataLoader = loadingOperations[indexPath] {
    if let emojiRating = dataLoader.emojiRating {
      cell.updateAppearanceFor(emojiRating, animated: false)
      loadingOperations.removeValue(forKey: indexPath)
    } else {
      dataLoader.loadingCompleteHandler = updateCellClosure
    }
  } else {
    // ...
  }  
}

셀 업데이트 클로저를 생성한 후 오퍼레이션을 추적하는 배열을 확인합니다. 만약 셀이 나타나고 이모지가 사용 가능하다면 셀의 UI가 업데이트 됩니다. 데이터 로드 오퍼레이션으로 전달 된 클로저는 셀의 UI도 업데이트 합니다.

이는 프리페치 트리거링 오퍼레이션에서 업데이트 중인 셀의 UI에 이르기까지 모든 것이 연관되는 방식입니다.

앱을 빌드하고 실행하고 이모지를 스크롤 하십시오. 당신이 스크롤하는 이모지들은 전보다 더 빨리 보여질 것입니다.

여기서 퀴즈! 개선할 수 잇는 것을 발견할 수 있습니까? 다음 섹션의 타이틀을 보지 마세요. 음, 만약 당신이 정말 빠르게 스크롤을 한다면 collection view는 볼 수 없는 이모지를 불러올 것입니다. What's a obsessive programmer to do? Read on.

Canceling a Prefetch

UICollectionViewDataSourcePrefetching 에는 데이터가 더 이상 필요하지 않다는 것을 알려주는 optional delegate 메소드가 있습니다. 이는 사용자가 스크롤을 매우 빨리하고 중간 셀이 보이지 않기 때문에 발생할 수 있습니다. delegate 메소드를 사용하여 보류 중인 데이터로드 오퍼레이션을 취소할 수 있습니다.

EmojiViewController.swiftUICollectionViewDataSourcePrefetching 프로토콜 구현부에 다음 메소드를 추가하십시오.

func collectionView(_ collectionView: UICollectionView,
  cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
  for indexPath in indexPaths {
    if let dataLoader = loadingOperations[indexPath] {
      dataLoader.cancel()
      loadingOperations.removeValue(forKey: indexPath)
    }
  }
}

코드 index paths를 loop하여 모든 로드 오퍼레이션을 찾습니다. 그것은 오퍼레이션을 취소하고 오퍼레이션 추적 딕셔너리에서 삭제할 것입니다.

앱을 빌드하고 실행 하십시오. 정말 빨리 스크롤을 할 때, 오퍼레이션은 시작 되었을 수도 있는 오퍼레이션이 취소되어야합니다. 시각적으로, 많이 변하는 것은 없습니다.

셀 재사용으로 인해 이전에 보였던 일부 셀을 다시 가져와야할 수도 있습니다. 강아지에 로딩 표시기가 있다면 당황하지마세요.

profile
개발자가되고싶어요

1개의 댓글

comment-user-thumbnail
2021년 2월 8일

잘 읽었습니다.

답글 달기