LoginSignup
11
1

More than 5 years have passed since last update.

UIRefreshControlを追加するのにサブクラスは要らない

Last updated at Posted at 2018-12-06

とりあえずこれを見てほしい。

突然ですが、こういう実装をよく見ませんか?

RefreshScrollView.swift
import UIKit

protocol RefreshScrollViewDelegate: class {
    func didBeginRefresh(in: RefreshScrollView)
}

class RefreshScrollView: UIScrollView {

    weak var refreshDelegate: RefreshScrollViewDelegate?

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.commonInit()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.commonInit()
    }

    private func commonInit() {
        self.refreshControl = UIRefreshControl(frame: CGRect(x: 0, y: 0, width: 40, height: 40))
        self.refreshControl?.addTarget(self, action: #selector(self.didPullDownScrollView), for: .valueChanged)
    }
}

// MARK: - Selector
extension RefreshScrollView {

    @objc
    private func didPullDownScrollView() {
        self.refreshDelegate?.didBeginRefresh(in: self)
    }
}

これが何かというと、UIScrollViewにUIRefreshControl(引っ張り更新とかをするときのアレ)をデフォルトでくっつけるUIScrollViewのサブクラスですね。

イケてなくないですか?

そういう自分も今まで、UIRefreshControlが必要なUIScrollViewが複数必要なアプリを開発するときに、こういう実装をしてきたのですが、なんだかモヤモヤしていました。
その理由はいくつかあるので、挙げていきます。

1.サブクラスを設定し忘れる可能性がある

サブクラスを作成しているため、xib上でCustomClassを指定するか、コード上でサブクラスを使用しなくてはならなくなってしまいます。
そんなの作るときに気をつければいいだろって話ではあるのですが、うっかりで普通のUIScrollViewを使って「あれ?」っていうことも100%起こらないとは言い切れないです。

2.RefreshScrollViewDelegate

UIRefreshControlが引っ張られた段階で、更新をする処理を実行するための通知をするために、RefreshScrollViewDelegateという新たなDelegateを追加作成しています。
UIKitのパーツは、delegateなどをxibから直接実装するクラスに紐づけてコード量を減らせたりします。
ですが、Swiftで新たにDelegateを作成して同じようなことをしようとする場合は@objcをつける必要があったりして、言語的な制約が増えるので、結局以下のようにコード上で書くのがありがちなパターンだったりします。

@IBOutlet private var scrollView: RefreshScrollView! {
    willSet {
        newValue.refreshDelegate = self
    }
}

しかし、これもサブクラスの設定し忘れと同じように、willSetの内容をうっかり書き忘れると、UIRefreshControlが引っ張られたタイミングが取得できずに、リロードできないみたいなミスが発生する可能性があるのでモヤモヤします。

3.UITableViewやUICollectionViewで使えない

こういうサブクラスを作ったときにありがちなのは、更にUITableViewやUICollectionViewにUIRefreshControlをくっつけるためのRefreshTableViewや、RefreshCollectionViewといったサブクラスを作成してしまうことです。
これは、UITableViewやUICollectionViewがUIScrollViewを継承しているがために、RefreshScrollViewを作成するだけでは、それらのクラスに対応できないが故の問題です。

UIRefreshControlの仕様が共通していた場合などに、変更があれば、全てのサブクラスに対して変更をかける必要が出てきたりして、保守性が下がってしまうということもあり、それもモヤモヤするところです。

解決策を考えてみた

・サブクラスを使用しない
・Delegateの設定し忘れをできるだけ防げる
・UITableViewやUICollectionViewなどのUIScrollViewを継承したクラスでも共通して使える

上記の3つのモヤモヤを解決できるUIRefreshControlを追加する方法を紹介します。

そのコードがこれ

UIScrollView+.swift
// MARK: - Refresh Control
protocol UIScrollViewRefreshControlDelegate: UIScrollViewDelegate {
    func didBeginRefresh(in scrollView: UIScrollView)
}

extension UIScrollView {

    // 色とかカスタマイズとかするなら、このメソッドの引数に足してあげてください
    func setupRefreshControl() {
        self.refreshControl = UIRefreshControl(frame: CGRect(x: 0, y: 0, width: 40, height: 40))
        self.refreshControl?.addTarget(self, action: #selector(self.didPullDownScrollView), for: .valueChanged)
    }

    @objc
    private func didPullDownScrollView() {
        if let delegate = self.delegate as? UIScrollViewRefreshControlDelegate {
            delegate.didBeginRefresh(in: self)
        }
    }
}

解説

extension

サブクラスを使用しないようにするためにUIScrollViewのextension上でUIRefreshControlをセットするメソッドを追加しました。
これをすることで、UIScrollViewをデフォルトで使用することができますし、UITableViewなどのUIScrollViewを継承するクラスでも、setupRefreshControl()を呼べば、UIRefreshControlを追加することができるので、複数箇所に同じような処理を実装する必要はありません。

UIScrollViewRefreshControlDelegate

今回新たにUIScrollViewRefreshControlDelegateを作成しました。
最初のサブクラスのDelegateと違い、この実装のミソである部分はUIScrollViewDelegateを継承しているということです。

そうすることによって、UIScrollViewDelegateに設定されたクラスに、UIScrollViewRefreshControlDelegateが加えて実装されていれば、UIScrollViewが引っ張られた際にUIScrollViewDelegateの実体のクラスをUIScrollViewRefreshControlDelegateにキャストして通知を送ることができます。

@objc
private func didPullDownScrollView() {
    // Delegateの実体のクラスがUIScrollViewRefreshControlDelegateを実装している場合に通知する。
    if let delegate = self.delegate as? UIScrollViewRefreshControlDelegate {
        delegate.didBeginRefresh(in: self)
    }
}

UITableViewDelegateやUICollectionViewDelegateもUIScrollViewDelegateが継承されているので、それらのDelegateに設定したクラスに追加で、UIScrollViewRefreshControlDelegateを実装すれば、UIScrollViewと同じようにUIRefreshControlを引っ張ったときの通知を受け取ることができます。

実際の実装はこんな感じ

ViewController.swift
class ViewController: UIViewController {

    @IBOutlet private weak var scrollView: UIScrollView! {
        willSet {
            // ここかxib上で設定
            newValue.delegate = self
            // UIRefreshControlを追加
            newValue.setupRefreshControl()
        }
    }
}

extension ViewController: UIScrollViewDelegate {}

extension ViewController: UIScrollViewRefreshControlDelegate {

    func didBeginRefresh(in scrollView: UIScrollView) {
        // 更新処理する。
        // 更新が終わったらendRefreshing()を呼ぶ
        scrollView.refreshControl?.endRefreshing()
    }
}

まとめ

・UIRefreshControlをデフォルトで追加するサブクラスは必要ない。
・UIScrollViewのextensionでやれば、UITableViewやUICollectionViewにも対応できる。
・UIScrollViewDelegateを継承するDelegateにキャストして通知すれば、完全に新規のDelegateを追加する必要もない。

11
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
1