이 글은 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를 사용하여 앱이 곧 표시할 셀을 확인하고, 백그라운드에서 관련된 데이터를 불러오게 합니다.
이 튜토리얼의 상단 또는 하단에 있는 자료 다운로드 버튼을 사용하여 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의 이모지를 표시합니다.
당신은 DataSource
와 EmojiViewController
에 스크롤 성능을 향상시킬 기능을 추가할 것입니다.
앱이 초당 60프레임(FPS) 디스플레이 제약 조건을 충족하도록하여 부드러운 스크롤을 얻을 수 있습니다. 즉 앱이 초당 60번 UI를 고쳐야하므로 각 프레임은 내용을 render하는데 16ms가 걸립니다. The system drops frames that takes too long to show content.
따라서 앱이 프레인을 건너뛰고 다음 프레임으로 이동할 때 스크롤이 느려집니다. 프레입 드랍의 가능한 원인은 main 스레드를 차단하는 긴 작업입니다.
Apple은 당신을 돕기 위한 몇가지 유용한 툴을 제공했습니다. 먼저 긴 작업을 분리하고 그것을 백그라운드 스레드로 이동할 수 있습니다. 이는 당신이 메인스레드에서 발생하는 모든 터치 이벤트를 처리할 수 있도록 해줍니다. 백그라운드 작업이 완료되면, 작업을 기반으로 메인 스레드에서 필요한 UI 업데이트를 수행할 수 있습니다.
다음은 프레임이 드랍되는 시나리오를 보여줍니다:
작업을 백그라운드로 옮기면 다음과 같이 보여집니다.
이제 앱 성능을 향상시키기 위해 동시에 두개의 스레드가 실행 중임을 당신은 안다.
데이터를 표시하기전에 가져오는 것을 시작한다면 더 좋지 않을까요? UITableView
및 UICollectionView
prefetching API가 제공됩니다.이 튜토리얼에서 collection view API를 사용합니다.
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 :)
델리게이트 메소드를 구현합니다. 방법을 단계별로 살펴보십시오.
셀이 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에 추가 향상을 제공합니다.
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 메소드는 호출되지 않습니다. 그러므로 당신은 이 메소드로 셀에 데이터를 로드하는데 의존하면 안됩니다.
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.
UICollectionViewDataSourcePrefetching
에는 데이터가 더 이상 필요하지 않다는 것을 알려주는 optional delegate 메소드가 있습니다. 이는 사용자가 스크롤을 매우 빨리하고 중간 셀이 보이지 않기 때문에 발생할 수 있습니다. delegate 메소드를 사용하여 보류 중인 데이터로드 오퍼레이션을 취소할 수 있습니다.
EmojiViewController.swift에 UICollectionViewDataSourcePrefetching
프로토콜 구현부에 다음 메소드를 추가하십시오.
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하여 모든 로드 오퍼레이션을 찾습니다. 그것은 오퍼레이션을 취소하고 오퍼레이션 추적 딕셔너리에서 삭제할 것입니다.
앱을 빌드하고 실행 하십시오. 정말 빨리 스크롤을 할 때, 오퍼레이션은 시작 되었을 수도 있는 오퍼레이션이 취소되어야합니다. 시각적으로, 많이 변하는 것은 없습니다.
셀 재사용으로 인해 이전에 보였던 일부 셀을 다시 가져와야할 수도 있습니다. 강아지에 로딩 표시기가 있다면 당황하지마세요.
잘 읽었습니다.