ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • UITableView Infinite Scrolling Tutorial
    Raywenderlich/Articles 2021. 8. 5. 14:43

    https://www.raywenderlich.com/5786-uitableview-infinite-scrolling-tutorial

     

    UITableView Infinite Scrolling Tutorial

    In this tutorial you will learn how to implement an Infinite Scrolling UITableView in your iOS app using a paginated REST API.

    www.raywenderlich.com

    Version

    Swift 4.2, iOS 12, Xcode 10

     

    Note: 이 튜토리얼(tutorial)은 Xcode 9 및 Xcode 10, iOS 11 및 iOS 12에서 모두 작동(works)한다.

     

    무한 스크롤(infinite scrolling)을 사용하면, 사용자(users)는 콘텐츠(content)를 지속적으로(continuously) 불러올(load) 수 있으므로 페이지네이션(pagination)이 따로 필요하지 않다. 앱(app)은 일부 초기 데이터(initial data)를 불러온(loads) 다음, 사용자(user)가 보이는 콘텐츠(visible content)의 하단(bottom)에 도달(reaches)하면 나머지 정보(rest of the information)를 추가(adds)한다.

    TwitterFacebook 같은 소셜 미디어(social media) 기업들(companies)은 이 기술(technique)을 수년 동안(over the years) 대중화(popular)했다. 그 모바일(mobile) 애플리케이션(applications)을 확인해(look at) 보면 무한 스크롤(infinite scrolling)이 작동(in action)하는 것을 볼 수 있다.

    이 튜토리얼(tutorial)에서는 REST API을 사용해 데이터를 가져(fetches)오는 iOS 앱에 무한 스크롤(infinite scrolling)을 추가(add)하는 방법을 배운다. 특히(in particular), Stack Overflow 또는 Mathematics와 같은 특정 사이트의 유저(moderators) 목록(list)을 표시(display)하기 위해 Stack Exchange REST API를 통합(integrate)한다.

    앱 경험(app experience)을 개선(improve)하기 위해 UITableView와 UICollectionView 모두에 대해 Apple이 iOS 10에서 도입(introduced)한 Prefetching API를 사용한다. 이는 스크롤링 성능 향상(improve scrolling performances)을 위한 최적화(optimizations)를 수행(performs)하는 적응형 기술(adaptive technology)입니다. Data source prefetching은 데이터를 표시(display)하기 전에 이를 준비(prepare)하는 메커니즘(mechanism)을 제공(provides)한다. 정보(information)를 가져오는(fetching) 데 시간이 걸리는 대규모 데이터 소스(data sources)에 이 기술(technology)을 구현(implementing)하면 사용자 경험(user experience)에 큰 영향(dramatic impact)을 미칠 수 있다.

     

    Getting Started

    이 튜토리얼(tutorial)에서는 Stack Exchange REST API를 사용하여 특정 사이트(specific site)의 기여자(moderators)를 표시(display)하는 iOS 앱(app)인 ModeratorsExplorer를 사용한다.

    튜토리얼(tutorial)의 상단(top) 또는 하단(bottom)에 있는 Download Materials 링크(link)를 사용하여 시작 프로젝트(starter project)를 다운로드(downloading)한다. 다운로드(downloaded)가 완료되면, Xcode에서 ModeratorsExplorer.xcodeproj를 연다(open).

    해당 기능에 집중(focused)할 수 있도록, 시작 프로젝트(starter project)에는 무한 스크롤(infinite scrolling)과 관련 없는(unrelated) 모든 것이 이미 설정(set up)되어 있다.

    Views에서 Main.storyboard를 열고(open), 여기에 포함(contained)된 뷰 컨트롤러(view controllers)를 확인한다:

     

    왼쪽의 뷰 컨트롤러(view controller)는 앱(app)의 기본 네비게이션 컨트롤러(root navigation controller)이다. 나머지 뷰 컨트롤러들은 다음과 같다:

    1. ModeratorsSearchViewController: 사이트(site)를 검색(search for)할 수 있는 텍스트 필드(text field)가 포함(contains)되어 있다. 다음 뷰(view)로 이동하는 버튼(button)도 포함(contains)되어 있다.
    2. ModeratorsListViewController: 주어진 사이트(given site)의 기여자(moderators)를 나열(lists)하는 테이블(table)을 포함(includes)하고 있다. ModeratorTableViewCell 유형(type)의 각 테이블 셀(table cell)에는 두 개의 레이블(labels)이 있다(includes). 하나는 기여자(moderator)의 이름을 표시(display)하고, 다른 하나는 평판(reputation)을 표시한다. 새 콘텐츠(content)를 요청(requested)할 때 사용하는 인디케이터(indicator)도 있다.

    앱(app)을 빌드(build)하고 실행(run)하면 초기 화면(initial screen)이 표시된다:

     

     

    Find Moderators!를 탭(tapping on)하면 무한 애니메이션(animates indefinitely)이 나타나는 스피너(spinner)가 표시된다. 이 튜토리얼(tutorial)의 뒷부분에서 초기 콘텐츠(initial content)가 로드(loaded)되면 해당 스피너(spinner)를 숨길(hide) 것이다.

     

    Get Acquainted with Stack Exchange API

    Stack Exchange API는 Stack Exchange 네트워크(network)에서 항목(items)을 쿼리(query)하는 메커니즘(mechanism)을 제공(provides)한다.

    이 튜토리얼(tutorial)에서는 /users/moderators API를 사용할 것이다. 이름에서 알 수 있듯이(implies) 특정 사이트(specific site)의 기여자(moderators) 목록(list)을 반환(returns)함다.

    API 응답(response)은 페이지 방식으로 처리된다(paginated). 기여자(moderators) 목록(list)을 처음 요청(request)하면 전체 목록(whole list)을 받을(receive) 수 없다. 대신(instead), 제한된 수(limited number)의 기여자(moderators, 한 페이지)와 해당 시스템(system)의 총 기여자(moderators) 수를 나타내는(indicating) 숫자가 포함된 목록(list)을 받게 된다.

    페이지네이션(pagination)은 많은 공개(public) API의 대한 일반적(common)인 기술(technique)이다. 가지고 있는 모든 데이터를 보내는(sending) 대신, 제한된 양(limited amount)의 데이터를 보내며(send) 더 필요할 때 또 다른 요청(request)을 한다. 이렇게 하면 서버 자원(server resources)을 절약(saves)하고, 더 빠른(faster) 응답(response)을 제공(provides)한다.

    JSON 응답(response)은 다음과 같다. 명확히 하기 위해(for clarity) 페이지네이션(pagination)과 관련된(related to) 필드(fields)만 표시하였다:

    {
    
      "has_more": true,
      "page": 1,
      "total": 84,
      "items": [
     
        ...
        ...
      ]
    }

    응답(response)에는 시스템(system)의 총 기여자(moderators) 수(84)와 요청된(requested) 페이지(1)가 포함(includes)된다. 이 정보(information)와 받은(received) 기여자(moderators) 목록(list)을 사용하여, 전체 목록(complete list)을 표시하기 위해 요청(request)해야 하는 항목(items) 및 페이지(pages) 수를 결정(determine)할 수 있다.

    이 API에 대해 자세히 알아보려면 Usage of /users/moderators 를 확인(visit)해 본다.

     

    Show Me The Moderators

    Note: 이 튜토리얼(tutorial)에서는 URLSession을 사용하여 네트워크 클라이언트(network client)를 구현(implement)한다. 이에 익숙(familiar with)하지 않은 경우 URLSession Tutorial: Getting Started 또는 Networking with URLSession에서 학습할 수 있다.

    API에서 기여자(moderators)의 첫 번째 페이지(page)를 불러오는(loading) 것부터 시작한다.

    Networking에서 StackExchangeClient.swift를 열고(open), fetchModerators(with:page:completion:)를 찾는다(find). 이 메서드를 다음으로 바꾼다(replace):

    func fetchModerators(with request: ModeratorRequest, page: Int, completion: @escaping (Result<PagedModeratorResponse, DataResponseError>) -> Void) {
      let urlRequest = URLRequest(url: baseURL.appendingPathComponent(request.path))
      //URLRequest 생성자를 사용하여 request를 생성한다. moderators를 가져오는 데 필요한 path를 baseURL에 추가한다.
      //최종 경로는 다음과 같다.
      //http://api.stackexchange.com/2.2/users/moderators
      let parameters = ["page": "\(page)"].merging(request.parameters, uniquingKeysWith: +)
      //원하는 page 번호에 대한 query parameter를 작성하여 ModeratorRequest instance에 정의된 default parameters(page와 site가 제외되어 있음)와 병합한다.
      //page는 request을 수행할 때마다 자동으로 계산되고, site는 ModeratorsSearchViewController의 UITextField에서 읽어온다.
      let encodedURLRequest = urlRequest.encode(with: parameters)
      //이전 단계에서 생성한 parameters로 URL을 인코딩한다.
      //최종 URL은 다음과 같다.
      //http://api.stackexchange.com/2.2/users/moderators?site=stackoverflow&page=1&filter=!-*jbN0CeyJHb&sort=reputation&order=desc
      //해당 request로 URLSessionDataTask를 생성한다.
      
      session.dataTask(with: encodedURLRequest, completionHandler: { data, response, error in
        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.hasSuccessStatusCode,
              let data = data else {
          completion(Result.failure(DataResponseError.network))
          return
        }
        //URLSession data task에서 반환된 response를 확인한다. 유효하지 않으면 completion handler를 호출하고 network error를 반환한다.
        
        guard let decodedResponse = try? JSONDecoder().decode(PagedModeratorResponse.self, from: data) else {
          completion(Result.failure(DataResponseError.decoding))
          return
        }
        //response가 유효하면, Swift Codable API를 사용하여 JSON을 PagedModeratorResponse 객체로 디코딩한다.
        //오류가 발견되면 decoding error와 함께 completion handler를 호출 호출한다.
    
        completion(Result.success(decodedResponse))
        //마지막으로 모든 것이 정상이면 completion handler를 호출하여, UI에 새로운 content를 사용할 수 있음을 알린다.
      }).resume()
    }

    다음으로 기여자 목록(moderators list)을 작업한다. ViewModels에서 ModeratorsViewModel.swift를 열고(open), fetchModerators의 기존 정의(existing definition)를 다음으로 바꾼다(replace):

    func fetchModerators() {
      guard !isFetchInProgress else {
        return
      }
      //fetch request가 이미 진행 중인 경우 코드를 벗어난다(bail out).
      //이렇게 하면 여러 requests가 발생하는 것을 방지할 수 있다. 나중에 더 자세히 설명한다.
      
      isFetchInProgress = true
      //fetch request가 진행 중이 아니라면, isFetchInProgress를 true로 설정하고 request을 보낸다.
      
      client.fetchModerators(with: request, page: currentPage) { result in
        switch result {
        case .failure(let error):
          DispatchQueue.main.async {
            self.isFetchInProgress = false
            self.delegate?.onFetchFailed(with: error.reason)
          }
          //request가 실패하면 delegate에게 failure 이유를 알리고 사용자에게 특정 alert를 표시한다.
        case .success(let response):
          DispatchQueue.main.async {
            self.isFetchInProgress = false
            self.moderators.append(contentsOf: response.moderators)
            self.delegate?.onFetchCompleted(with: .none)
          }
          //성공하면 moderators list에 새로운 items을 추가하고, delegate에게 사용 가능한 데이터가 있음을 알린다.
        }
      }
    }
    Note: success와 failure 모두에서 delegate가 기본 스레드(main thread)인 DispatchQueue.main에서 작업(perform)하도록 해야 한다. 이는 요청(request)이 백그라운드 스레드(background thread)에서 발생(happens)하고 UI 요소(elements)를 조작(manipulate)해야 하기 때문에 필요하다.

    앱(app)을 빌드(build)하고 실행(run)한다. 텍스트 필드(text field)에 stackoverflow를 입력(type)하고 Find Moderators를 누른다(tap on). 다음과 같은 목록이 표시된다:

     

    테이블(table)의 끝까지 스크롤(scroll)해도 나머지 데이터(rest of the data)가 없다는 것을 알(notice) 수 있다.

    기본적으로(by default) API 요청(request)은 각 페이지(page)에 대해 30개 항목(items)만 반환(returns)하므로, 앱(app)은 처음 30개 항목(items)이 있는 첫 번째 페이지(first page)를 표시(shows)한다.

    나머지 기여자(rest of the moderators)를 요청(request)할 수 있도록 앱(app)을 수정(modify)해야 한다. 이를 받으면(receive) 목록(list)에 새 항목(items)으로 추가(add)해야 한다. 각 요청(request)에 따라 전체 목록(full list)을 점진적으로(incrementally) 작성(build)하고, 준비되는 즉시(as soon as) 테이블 뷰(table view)에 표시(show)한다.

    또한 사용자(user)가 목록(list)을 아래로 스크롤(scrolls down)할 때 반응(react)할 수 있도록, 사용자 인터페이스(user interface)를 수정(modify)해야 한다. 불러온(loaded) 기여자(moderators) 목록(list)의 끝에 가까워지면 새로운 페이지(page)를 요청(request)해야 한다.

    네트워크 요청(network requests)은 시간이 오래 걸릴(take a long time) 수 있으므로, 기여자(moderator) 정보(information)를 아직 사용(available)할 수 없는 경우 스피너 인디케이터(spinning indicator) 뷰(view)를 표시(displaying)하여 사용자 경험(user experience)을 개선(improve)해야 한다.

     

    Infinite Scrolling: Requesting Next Pages

    API의 다음 페이지(pages)를 요청(request)하려면 뷰 모델(view model) 코드를 수정(modify)해야 한다. 수행해야 할 작업에 대한 개요(overview)는 다음과 같다:

    • UI가 요청(request) 메서드(method)를 호출(calls)할 때, 다음에 필요한 페이지(page)를 알 수 있도록 수신된(received) 마지막 페이지(last page)를 추적(track)한다.
    • 기여자(moderators)의 전체 목록(full list)을 작성(build)한다. API에서 새 페이지(new page)를 받으면(receive), 이전처럼 교체(replacing)하는 대신(instead of) 기여자 목록(moderator’s list)에 추가(add)해야 한다. 응답(response)을 받으면(get), 지금까지 받은(received) 모든 기여자(moderators)를 포함(include)하도록 테이블 뷰(table view)를 업데이트(update)할 수 있다.

    ModeratorsViewModel.swift를 열고(open), fetchModerators() 아래(below)에 다음 메서드(method)를 추가(add)한다:

    private func calculateIndexPathsToReload(from newModerators: [Moderator]) -> [IndexPath] {
      let startIndex = moderators.count - newModerators.count
      let endIndex = startIndex + newModerators.count
      return (startIndex..<endIndex).map { IndexPath(row: $0, section: 0) }
    }

    이(utility)는 API에서 받은(received) 기여자(moderators)의 마지막 페이지(last page)에 대한 색인 경로(index paths)를 계산(calculates)한다. 전체 테이블 뷰(whole table view)를 다시 로드(reloading)하는 대신(instead of) 변경(changed)된 내용(content)만 새로 고치(refresh)는 데 사용한다(use).

    이제 fetchModerators()로 이동한다. success의 전체 내용(entire content)을 다음으로 교체(replace)한다:

    DispatchQueue.main.async {
      self.currentPage += 1
      self.isFetchInProgress = false
      //response이 성공하면, 검색할 page 번호를 증가시킨다. API request pagination의 기본값은 30개 이다.
      //첫 번째 page를 fetch하면 처음 30개의 items을 검색한다. 두 번째 request에서는 다음 30개를 검색한다.
      //전체 moderators list를 받을 때까지 retrieval mechanism이 계속 된다.
      self.total = response.total
      //서버에서 사용 가능한 총 moderators count를 저장한다.
      //나중에 이 정보를 이용하여 새 page를 request해야 하는지 여부를 결정한다.
      self.moderators.append(contentsOf: response.moderators)
      //새로 반환된 moderators를 저장한다.
    
      if response.page > 1 {
        //해당 page가 첫 page가 아닌 경우, reload할 index paths를 계산하여 table view content를 update하는 방법을 결정한다.
        let indexPathsToReload = self.calculateIndexPathsToReload(from: response.moderators)
        self.delegate?.onFetchCompleted(with: indexPathsToReload)
      } else {
        self.delegate?.onFetchCompleted(with: .none)
      }
    }

    이제 전체 기여자 목록(total list of moderators)에서 모든 페이지(pages)를 요청(request)하고 모든 정보(information)를 집계(aggregate)할 수 있다. 그러나(however), 스크롤(scrolling)할 때 적절한(appropriate) 페이지(pages)를 동적으로(dynamically) 요청(request)해야 한다.

     

    Building the Infinite User Interface

    사용자 인터페이스(user interface)에서 무한 스크롤(infinite scrolling)이 작동하도록 하려면, 먼저 테이블 뷰(table view)에, 셀 수(number of cells)가 불러온(loaded) 기여자(moderators) 수가 아닌 총 기여자 수(total number of moderators)임을 알려야 한다. 이렇게 하면 아직 기여자(moderators)를 받지(received) 않았더라도 사용자(user)가 첫 페이지(first page)를 지나 스크롤할 수 있다. 그런 다음(then), 사용자(user)가 마지막 기여자(last moderator)를 스크롤(scrolls)하면 새 페이지(new page)를 요청(request)해야 한다.

    Prefetching API를 사용하여 새 페이지(new pages)를 불러올(load) 시기를 결정(determine)한다. 시작하기 전에(before starting), 잠시 시간을 내어(take a moment) 이 새로운 API의 작동 방식(works)을 살펴 본다(understand).

    UITableView는 다음 두 가지 메서드(methods)를 사용하여 UITableViewDataSourcePrefetching 프로토콜(protocol)을 정의(defines)한다.

    • tableView(_:prefetchRowsAt:): 이 메서드(method)는 현재 스크롤 방향(current scroll direction)과 속도(speed)를 기반으로(based on) 가져올(prefetch) 셀(cells)의 인덱스 경로(index paths)를 수신(receives)한다. 일반적으로(usually) 여기에서 해당 항목(items)에 대한 데이터 작업(operations)을 시작(kick off)하는 코드를 작성한다.
    • tableView(_:cancelPrefetchingForRowsAt:): 가져오기(prefetch) 작업(operations)을 취소(cancel)해야 할 때 호출(triggers)되는 선택적 메서드(optional method)이다. 테이블 뷰(table view)가 가져올 것으로 예상(anticipated)했지만 더 이상 필요하지 않은 항목(items)에 대한 인덱스 경로(index paths) 배열(array)을 수신(receives)한다. 이는 사용자(user)가 스크롤 방향(scroll directions)을 변경(changes)하는 경우 발생(happen)할 수 있다.

    두 번째 메서드는 선택 사항(optional)이고, 지금은 새로운 콘텐츠(content) 검색(retrieving)에만 관심(interested in)이 있으므로 첫 번째 메서드(method)만 사용한다.

    Note: 테이블 뷰(table view) 대신 컬렉션 뷰(collection view)를 사용하는 경우, UICollectionViewDataSourcePrefetching을 구현(implementing)하여 유사한(similar) 동작(behaviour)을 구현할 수 있다.

    Controllers 그룹(group)에서 ModeratorsListViewController.swift를 열고(open), 훑어 본다(quick look). 이 컨트롤러(controller)는 UITableView에 대한 데이터 소스(data source)를 구현(implements)하고, viewDidLoad()에서 fetchModerators()를 호출(calls)하여 기여자(moderators)의 첫 번째 페이지(first page)를 불러온다(load). 그러나 사용자(user)가 목록(list)을 아래로 스크롤(scrolls down)해도 아무 작업을 수행하지 않는다. 여기가 Prefetching API를 사용해야(rescue)하는 지점이다.

    먼저(first), Prefetching을 사용할 테이블 뷰(table view)를 알려줘야 한다. viewDidLoad()를 찾아 테이블 뷰(table view)에 대한 데이터 소스(data source)를 설정한 행(line) 바로 아래에 다음을 삽입(insert)한다:

    tableView.prefetchDataSource = self

    컨트롤러(controller)가 아직 필요한 메서드(required method)를 구현(implement)하지 않았기 때문에 컴파일러(compiler)는 오류(complain)를 보여준다. 파일(file) 끝에 다음 확장(extension)를 추가한다(add):

    extension ModeratorsListViewController: UITableViewDataSourcePrefetching {
      func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
        
      }
    }

    곧 논리(logic)를 구현(implement)할 것이지만, 그렇게 하기 전에 도움이 될 두 가지 메서드(utility methods)가 필요하다. 파일(file)의 끝으로 이동하여 새 확장(extension)을 추가(add)한다:

    private extension ModeratorsListViewController {
      func isLoadingCell(for indexPath: IndexPath) -> Bool {
        return indexPath.row >= viewModel.currentCount
      }
    
      func visibleIndexPathsToReload(intersecting indexPaths: [IndexPath]) -> [IndexPath] {
        let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows ?? []
        let indexPathsIntersection = Set(indexPathsForVisibleRows).intersection(indexPaths)
        return Array(indexPathsIntersection)
      }
    }
    • isLoadingCell(for:): 해당 인덱스 경로(index path)의 셀(cell)이 지금까지 수신(received)한 기여자 수(the count of the moderators)를 초과(beyond)하는지 여부를 결정(determine)할 수 있다.
    • visibleIndexPathsToReload(intersecting:): 이 메서드(method)는 새 페이지(new page)를 받을(receive) 때 다시 로드(reload)해야 하는 테이블 뷰(table view)의 셀(cells)을 계산(calculates)한다. 이전(previously)에 뷰 모델(view model)이 계산(calculated)하여 전달(passed)한 IndexPaths와 현재 보이는(visible) 인덱스 경로들의 접점(intersection)을 계산(calculates)한다. 현재 화면(screen)에 표시되지 않는 셀(cell)을 새로 고치(refreshing)지 않으려면 이를 사용한다.

    이 두 가지 메서드(methods)를 사용하여 tableView(_:prefetchRowsAt:)의 구현(implementation)을 변경(change)할 수 있다. 다음과 같이 변경(replace)한다:

    func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
      if indexPaths.contains(where: isLoadingCell) {
        viewModel.fetchModerators()      
      }
    }

    테이블 뷰(table view)가 인덱스 경로(index paths) 목록(list)을 가져오기(prefetch) 시작하면 기여자(moderators) 목록(list)에 아직 불러오지(loaded) 않은 항목이 있는지 확인(checks)한다. 그렇다면 뷰 모델(view model)에 새로운 기여자(moderatos) 페이지(page)를 요청(request)해야 한다. tableView(_:prefetchRowsAt:)는 여러 번(multiple times) 호출(called)될 수 있으므로(since), 뷰 모델(view model)은 isFetchInProgress 속성(property)을 확인하여 처리 방법(deal with)을 알아내고 완료(finished)될 때까지 후속 요청(subsequent requests)을 무시(ignores)한다.

    이제 UITableViewDataSource 프로토콜(protocol) 구현(implementation)을 약간(few) 변경(changes)할 차례이다. 연결된(associated) 확장(extension)을 찾아 다음으로 바꾼다(replace):

    extension ModeratorsListViewController: UITableViewDataSource {
      func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.totalCount
        //list가 아직 complete되지 않은 경우에도 table view에서 예상되는 모든 moderators에 대한 row을 표시할 수 있도록
        //이미 받은 moderators 수를 반환하는 대신, 서버에서 사용 가능한 총 moderators 수를 반환한다.
      }
      
      func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifiers.list, 
                   for: indexPath) as! ModeratorTableViewCell
        if isLoadingCell(for: indexPath) {
          cell.configure(with: .none)
          //current cell에 대한 moderator를 받지 못한 경우, empty value으로 cell을 configure한다.
          //이 경우 cell은 spinning indicator view를 표시한다.
        } else {
          cell.configure(with: viewModel.moderator(at: indexPath.row))
          //moderator가 이미 목록에 있는 경우, cell에 전달하여 name과 reputation를 표시한다.
        }
        return cell
      }
    }

    거의 다 됐다(you're almost there). API에서 데이터를 수신(receive)할 때, 사용자 인터페이스(user interface)를 새로 고쳐야(refresh) 한다. 이 경우에서는 받은(received) 페이지(page)에 따라(depending on) 다르게 행동해야 한다.

    첫 번째 페이지(first page)를 받으면(receive), 인디케이터(main waiting indicator)를 숨기고(hide) 테이블 뷰(table view)를 표시하여 콘텐츠(content)을 다시 불러와야(reload) 한다.

    그러나 다음 페이지(next pages)를 수신(receive)하면, 이전에 추가한 visibleIndexPathsToReload(intersecting:) 메서드(method)를 사용하여 현재 화면(screen)에 있는 셀(cells)을 다시 불러와야(reload) 한다.

    ModeratorsListViewController.swift에서 onFetchCompleted(with:)를 찾아 다음으로 교체(replace)한다:

    func onFetchCompleted(with newIndexPathsToReload: [IndexPath]?) {
      guard let newIndexPathsToReload = newIndexPathsToReload else {
        //newIndexPathsToReload가 nil(첫 page)이면 indicator view를 숨기고 table view를 표시한 후 reload한다.
        indicatorView.stopAnimating()
        tableView.isHidden = false
        tableView.reloadData()
        return
      }
    
      
      let indexPathsToReload = visibleIndexPathsToReload(intersecting: newIndexPathsToReload)
      tableView.reloadRows(at: indexPathsToReload, with: .automatic)
      //newIndexPathsToReload가 nil이 아닌 경우(다음 page), reload해야 하는 visible cells을 찾아 table view에 해당 cells만 reloading하도록 한다.
    }

     

    Infinite Scrolling in Action

    모든 노력(hard work)의 결과(result)를 볼 시간이다.

    앱(app)을 빌드(build)하고 실행(run)한다. 앱(app)이 실행(launches)되면, 검색 뷰 컨트롤러(search view controller)가 표시된다.

    텍스트 필드(text field)에 stackoverflow를 입력(type)하고 Find Moderators! 버튼(button)을 탭(tap)한다. 첫 번째 요청(request)이 완료(completes)되고 인디케이터(waiting indicator)가 사라지면(disappears), 초기 콘텐츠(initial content)가 표시된다. 맨 아래로 스크롤(scrolling)하면, 아직 받지(received) 못한 기여자(moderators)에 대한 로딩 인디케이터(loading indicator)를 보여주는 몇 개의 셀(cells)을 볼(notice) 수 있다.

     

    요청(request)이 완료(completes)되면 앱(app)은 스피너(spinners)를 숨기고, 셀(cell)에 기여자 정보(moderator information)를 표시(shows)한다. 더 이상 사용할(available) 수 있는 항목(items)이 없을 때까지, 무한 로딩(infinite loading) 메커니즘(mechanism)이 계속(continues)된다.

     

    Note: 실제 디바이스(actual device)에서 네트워크(network) 활동(activities)이 너무 빠르게(quickly) 발생(occur)하여 셀(cells)을 불러오는(spinning) 것을 볼 수 없을 경우, Settings 앱(app)의 Developer 섹션(section)에서 일부 네트워크(network) 설정(settings)을 토글(toggling)하여 작동을 확인할 수 있다. Network Link Conditioner 섹션(section)으로 이동하여 활성화(enable)하고 프로필(profile)을 선택(select)한다. Very Bad Network는 좋은 선택(choice)이다.
    시뮬레이터(simulator)에서 실행(running) 중인 경우, Network Link Conditioner included in the Advanced Tools for Xcode를 사용하여 네트워크(network) 속도(speed)를 변경(change)할 수 있다. 이것은 연결 속도(connection speeds)가 최적(optimal)보다 낮을 때 앱(apps)에 어떤 일이 발생(happens)하는지 확인(conscious)해야 하기 때문에 확장(arsenal)에서 사용할 수 있는 좋은 도구(tool)이다.

     

    Where to Go From Here?

    이 튜토리얼(tutorial)의 상단(top) 또는 하단(bottom)에 있는 Download Materials 링크(link)를 사용하여 프로젝트(project)의 완성된 버전(completed version)을 다운로드(download)할 수 있다.

    무한 스크롤(infinite scrolling)을 구현(achieve)하고 iOS의 Prefetching API를 활용(take advantage)하는 방법을 배웠다. 이제 사용자(users)는 잠재적인(potentially) 셀 수(number of cells) 제한 없이(unlimited) 스크롤(scroll)할 수 있다. 또한 Stack Exchange API와 같이 페이지가 매겨진(paginated) REST API를 처리(deal with)하는 방법도 배웠다.

    iOS의 prefetching API에 대해 자세히 알아보려면 Apple의 What's New in UICollectionView in iOS 10 문서(documentation), 우리의 iOS 10 by Tutorials 책 또는 Sam Davies의 iOS 10: Collection View Data Prefetching 무료(free) 스크린캐스트(screencast)를 확인해 본다.

    그동안(meantime) 질문(questions)이나 의견(comments)이 있다면, 아래의 포럼 토론(forum discussion)에 참여(join)할 수 있다.

Designed by Tistory.