Пользователь
0,1
рейтинг
7 июля 2014 в 16:52

Разработка → Реализуем pull to refresh и infinite scrolling на Swift

Возьмём за основу статью Знакомьтесь, Swift!, где показано как сделать простое приложение на Swift, и добавим туда такие известные и полезные штуки как pull to refresh и infinite scrolling используя встроенные возможности языка. Чтобы было еще интереснее, добавим немного асинхронности, иначе приложение будет каждый раз замирать на время обновления.



Подготовка


За основу мы берём вышеозначенный пример, поэтому будем просто дополнять его. Для начала в класс контроллера добавим 2 переменные, которые будут отвечать за количество ячеек и за текст, отображаемый в ячейках
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    var countRow = 20
    var text = "Habrapost"

И модифицируем код генерации ячеек, используя эти переменные
    func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int
    {
        return countRow
    }

    func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell!
    {
        let cell: UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "MyTestSwiftCell")
        
        cell.text = "\(text) \(indexPath.row)"
        cell.detailTextLabel.text = "Hi, \(indexPath.row)"
        cell.detailTextLabel.textColor = UIColor.purpleColor()
        
        return cell
    }

Теперь сделаем привязку TableView к контроллеру, чтобы можно было производить над ней нужные нам манипуляции. В interface builder выбираем TableView, нажимаем cmd+alt+enter и правой кнопкой тянем в появившееся окно


Вбиваем имя по которому будем обращаться


Так же добавим в TableView компонент View, на котором разместим 2 элемента, чтобы получилось следующее


Эта View нужна для того, чтобы отображать уведомление о том, что идет обновление, и нам нужно, чтобы она была видна только тогда, когда идет подгрузка новых данных (для этого будем использовать свойство tableFooterView.hidden), поэтому нужно ее скрыть вначале, и показать только потом. Так же нужно будет вручную запускать анимацию UIActivityIndicatorView, для этого, аналогично как и выше, добавим привязку




Для предварительной подготовки этих действий будет достаточно.

Pull to refresh


Теперь можно перейти непосредственно к реализации pull to refresh. В класс контролера добавим новую переменную специального класса UIRefreshControl
    var refreshControl:UIRefreshControl!

В viewDidLoad добавим код, инициализирующую эту переменную и привязывающую ее к tableView
    override func viewDidLoad() {
        super.viewDidLoad()
        
        refreshControl = UIRefreshControl()
        refreshControl.attributedTitle = NSAttributedString(string: "Идет обновление...")
        refreshControl.addTarget(self, action: "refresh:", forControlEvents: UIControlEvents.ValueChanged)
        tableView.addSubview(refreshControl)
    }

Теперь нам нужно определить функцию refresh, которая и будет вызываться каждый раз, при выполнении действия pull to refresh. Чтобы обновление происходило в асинхронном режиме, используем следующую схему (не буду вдаваться в описание подробностей, разобраться коде не трудно самостоятельно)
    func refresh(sender:AnyObject) {
        refreshBegin("Refresh",
            refreshEnd: {(x:Int) -> () in
                self.tableView.reloadData()
                self.refreshControl.endRefreshing()
            })
    }
    
    func refreshBegin(newtext:String, refreshEnd:(Int) -> ()) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
            println("refreshing")
            self.text = newtext
            sleep(2)
            
            dispatch_async(dispatch_get_main_queue()) {
                refreshEnd(0)
            }
        }
    }

В итоге, получаем


UPD: Если вы используете UITableViewController (а лучше использовать его в данном и аналогичных случаях), то код будет даже проще. В UITableViewController уже есть свойства tableView и refreshControl, поэтому не нужно делать привязку UITableView вручную и не надо в классе объявлять refreshControl. Достаточно написать в viewDidLoad следующий код и все будет работать

    override func viewDidLoad() {
        super.viewDidLoad()
        
        refreshControl = UIRefreshControl()
        refreshControl.attributedTitle = NSAttributedString(string: "Идет обновление...")
        refreshControl.addTarget(self, action: "refresh:", forControlEvents: UIControlEvents.ValueChanged)
    }


Infinite scrolling


С бесконечной прокруткой немного по сложнее, но не на много. В класс контролера добавим новую переменную loadMoreStatus, которая будет отвечать за защиту от повторного обновления, если оно уже запущено
    var loadMoreStatus = false

В viewDidLoad добавим код, который первично скроет View с информацией о подгрузке новых данных
    override func viewDidLoad() {
        super.viewDidLoad()
        
        refreshControl = UIRefreshControl()
        refreshControl.attributedTitle = NSAttributedString(string: "Идет обновление...")
        refreshControl.addTarget(self, action: "refresh:", forControlEvents: UIControlEvents.ValueChanged)
        tableView.addSubview(refreshControl)
        self.tableView.tableFooterView.hidden = true
    }

Добавим определение специальной функции scrollViewDidScroll, которая вызывается каждый раз, когда происходит любая прокрутка. Если мы доматываем до конца списка, то вызывается функция loadMore, которая реализует асинхронную подгрузку новых данных
    func scrollViewDidScroll(scrollView: UIScrollView!) {
        let currentOffset = scrollView.contentOffset.y
        let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height
        let deltaOffset = maximumOffset - currentOffset
        
        if deltaOffset <= 0 {
            loadMore()
        }
    }
    
    func loadMore() {
        if ( !loadMoreStatus ) {
            self.loadMoreStatus = true
            self.activityIndicator.startAnimating()
            self.tableView.tableFooterView.hidden = false
            loadMoreBegin("Load more",
                loadMoreEnd: {(x:Int) -> () in
                    self.tableView.reloadData()
                    self.loadMoreStatus = false
                    self.activityIndicator.stopAnimating()
                    self.tableView.tableFooterView.hidden = true
                })
        }
    }
    
    func loadMoreBegin(newtext:String, loadMoreEnd:(Int) -> ()) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
            println("loadmore")
            self.text = newtext
            self.countRow += 20
            sleep(2)
            
            dispatch_async(dispatch_get_main_queue()) {
                loadMoreEnd(0)
            }
        }
    }

В результате все работает, приложение не замирает, а данные добавляются успешно


Вот таким нехитрым способом можно реализовать pull to refresh и infinite scrolling, и само собой, из-за асинхронного обновления, можно, например, делать запросы JSON к серверу простым синхронным способом и это не помешает работе приложения.
@Shannon
карма
22,7
рейтинг 0,1
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (19)

  • +2
    Вообще говоря UIRefreshControl предназначен только для работы в связке с UITableViewController. То, что он работает и при простом добавлении в иерархию UITableView – счастливое стечение обстоятельств. Например в iOS6/7 в такой конфигурации (просто subview UITableVIew) возникали проблемы с анимациями. А у вас не возникают?
    Note: Because the refresh control is specifically designed for use in a table view that's managed by a table view controller, using it in a different context can result in undefined behavior.
    • 0
      Пока не возникали, по крайней мере в двух несвязанных приложениях ничего такого не было (сравнил с UITableViewController, анимация работает идентично)
      • +1
        То есть вот такого «прыжка» в момент срабатывания обновления у вас не возникает?
        www.youtube.com/watch?v=kEjJWfWKGSg
        • 0
          обратите внимание на спиннер. Он тоже тормозит. Есть вероятность, что нечто сетевое или базаданное выполняется в UI треде, поэтому тормозит весь UI. Если это так — это проблема не UIRefreshControl.

          Вообще можно github.com/samvermette/SVPullToRefresh. Он к UIScrollView цепляется.
          • +1
            Нет, дело именно в том, что UIRefreshControl неправильно инициализируется. Немного покопавшись в runtime легко убедиться, что UITableViewController при его инциализации дополнительно привязывает UIRefreshControl к UITableView – у таблицы вызывается приватный метод:

            - (void)_setRefreshControl:(UIRefreshControl *)obj;
            

            Впоследствии UITableView использует эту связь с UIRefreshControl – как раз для вычисления правильного contentInset при овер-скроллинге.
        • 0
          Точно, есть такой прыжок, действительно
    • +1
      Можно ведь делать UITableController, которому присваивать в качестве tableView вашу таблицу, а в качестве refreshControl собственно UIRefreshControl.
      • –1
        Часто нет возможности (или желания) наследовать свой контроллер от UITableViewController. Можно конечно создавать временный экземпляр UITableViewController – только для того, чтобы правильно инициализировать UIRefreshControl для своей таблицы. Или просто использовать приватные API, чтобы воспроизвести поведение UITableViewController при инициализации. О «радиусе кривизны» таких решений можно спорить – спасибо Apple за «классный» API.
        • 0
          Если поразмышлять, то при использовании UITableView не так много ситуаций когда UIViewController будет предпочтительнее UITableViewController (если все необходимые дополнительные элементы размещать на tableHeaderView и tableFooterView). Практически, если в каком-либо виде нужна UITableView, то изначально лучше использовать UITableViewController, и не придется думать о «радиусе кривизны»
        • 0
          Dreddik, насколько я понял, имел ввиду не наследование, а просто создание временного UITableViewController. Это фиксит проблему с «прыжком» из-за неправильной инициализации о которой Вы говорили ( omaksim )
          К сожалению, конкретно в Swift не силен, но на Xamarin фикс выглядит так:

              [Register ("MyViewController")]
              partial class MyViewController
              {
                  [Outlet]
                  public UITableView myTableView { get; set; }
          
                  // ...
              }
          
              public partial class MyViewController : UIViewController
              {
                  public UIRefreshControl myRefreshControl { get; set; }
          
                  public override void ViewDidLoad()
                  {
                      base.ViewDidLoad();
          
                      // ...
          
                      this.myRefreshControl = new UIRefreshControl();
                      this.myRefreshControl.AttributedTitle = new NSAttributedString("Обновляем...");
                      this.myRefreshControl.AddTarget(this, new ObjCRuntime.Selector("RefreshSource"), UIControlEvent.ValueChanged);
          
                      #region Fix the Jump problem
                      UITableViewController tableViewController = new UITableViewController();
                      tableViewController.TableView = this.myTableView;
                      tableViewController.RefreshControl = this.myRefreshControl;
                      #endregion
          
                      #region Fix the unwanted first showing
                      this.myRefreshControl.BeginRefreshing();
                      this.myRefreshControl.EndRefreshing();
                      #endregion
          
                      // ...
                  }
          
                  [Export("RefreshSource")]
                  private async void RefreshSource()
                  {
                      #region Edit source data
                      await Task.Run(() =>
                          {
                              Thread.Sleep(3000);
                          });
                      #endregion
          
                      this.myTableView.ReloadData();
                      this.myRefreshControl.EndRefreshing();
                  }
              }
          
    • 0
      а еще счастливое стечение обстоятельств работает с UICollectionView
  • +1
    Было бы просто отлично, если бы каждый кто реализовывал infinite scrolling ещё и реализовал кнопочку по его отключению…
  • 0
    Если я все правильно понял, то у Вас вот здесь ошибка

    let cell: UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "MyTestSwiftCell")
            
            cell.text = "\(text) \(indexPath.row)"
            cell.detailTextLabel.text = "Hi, \(indexPath.row)"
            cell.detailTextLabel.textColor = UIColor.purpleColor()
            
            return cell
    

    Вы каждый раз создаете ячейку для таблицы, игнорируя механизм переиспользования ячеек UITableView. Из-за этого скролл talbeView может заметно подтормаживать, лучше поправить, а то наверняка новички, изучающие Swift, наткнутся на эту статью и потащут это к себе в проект.
    • 0
      Привожу корректный код:
             var tableViewCell = tableView.dequeueReusableCellWithIdentifier(CellReuseId) as? UITableViewCell;
              if  (!tableViewCell){
                  tableViewCell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: CellReuseId);
                  tableViewCell.detailTextLabel.textColor = UIColor.purpleColor()
              }
              tableViewCell.text = "\(text) \(indexPath.row)"
              tableViewCell.detailTextLabel.text = "Hi, \(indexPath.row)"
              return tableViewCell;
      

      Где CellReuseId некая строка идентификатор
      let CellReuseId = "Row";
      
      • 0
        блин, не верно.
        там где идет обращение к свойствам tableViewCell нужно добавить!
        т.е.
        tableViewCell!.text =

        вот ничерта не читаем этот свифт…
        • 0
          Попробовал разные варианты, но такой код так и не заработал, внутрь if не заходит. Посмотрел, что возвращает dequeueReusableCellWithIdentifier (возвращает nil), по идее должен работать такой код

                 var cell = tableView.dequeueReusableCellWithIdentifier("MyTestSwiftCell") as UITableViewCell
                  if  (cell == nil){
                      cell.detailTextLabel.textColor = UIColor.purpleColor()
                      cell.text = "\(text) \(indexPath.row)"
                      cell.detailTextLabel.text = "Hi, \(indexPath.row)"
                  }
                  return  cell


          Но dequeueReusableCellWithIdentifier никогда не возвращает nil, даже если делать custom ячейки. Чтобы заставить его работать, например, можно использовать такую проверку в случае с Subtitle (detailTextLabel.text по умолчанию содержит текст «Subtitle»)

          if ( cell.detailTextLabel.text == "Subtitle" ) 

          или, если добавляется картинка, то
          if ( cell.imageView.image == nil )

          Еще тут возникает трудность с pull to refresh, надо учесть, что ячейки при таком условии не будут обновлены, потому что будут использоваться уже созданные
          • 0
            ох, извините, я не указал. этот код работает для xib. в сторибоардах же, apple побеспокоилась и за нас (спасибо ей) и под капотом сделала часть кода, который мы раньше писали.
            метод dequeueReusableCellWithIdentifier сам создает ячейку, если ее нет в кэше.
            финальный вариант

                   var tableViewCell = tableView.dequeueReusableCellWithIdentifier(CellReuseId) as? UITableViewCell;
                     tableViewCell!.detailTextLabel.textColor = UIColor.purpleColor()
                    tableViewCell!.text = "\(text) \(indexPath.row)"
                    tableViewCell!.detailTextLabel.text = "Hi, \(indexPath.row)"
                    return tableViewCell;
            

            и все там должно перезагружаться, если нет — то Вы что-то напутали. залейте в гит проект, посмотрим :)
            • 0
              Да, в случае с сторибордами так и есть. Так же проверил этот код различными способами, и он, в итоге, ничем не отличается от более читаемого варианта
                      var tableViewCell = tableView.dequeueReusableCellWithIdentifier(CellReuseId) as UITableViewCell
                      tableViewCell.detailTextLabel.textColor = UIColor.purpleColor()
                      tableViewCell.text = "\(text) \(indexPath.row)"
                      tableViewCell.detailTextLabel.text = "Hi, \(indexPath.row)"
                      return tableViewCell
              
  • 0
    А есть какое-либо стандартное решение сделать pull to refresh внизу таблицы?

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.