Pull-to-Refresh: 基本、RefreshControlとUIRefreshControl

著者: IT Sectr 公開日: 2026-02-27 読了時間: 8 分
Pull-to-Refreshは、ユーザーが指でリストを下に引っ張り、新しいデータの読み込みを開始するモバイルインターフェースパターンです。このジェスチャーには、視覚的なインジケーター(回転するスピナーまたはアニメーションアイコン)が伴い、読み込みが完了すると消えます。Apple HIGのUX分析によると、Pull-to-RefreshはTweetie(2008)での導入とその後のAppleとGoogleによる標準化以来、ニュースフィード、ソーシャルネットワーク、メールクライアントにおける標準的なコンテンツ更新メカニズムとなっています。

重要なポイント

  • Pull-to-Refreshは、視覚的な読み込みインジケーターを伴い、データを更新するためにリストを下に引っ張るジェスチャーです。
  • iOSではUIRefreshControl(iOS 6+)を使用し、refreshControlプロパティを介してUITableViewControllerまたはUIScrollViewに追加します。
  • AndroidではSwipeRefreshLayout(Support Libraryから)を使用します。RecyclerViewまたはNestedScrollView用のViewGroupラッパーです。
  • 両方のAPIは、リスナーを介した色、インジケーター、コールバックのカスタマイズをサポートしています(iOS: UIRefreshControl.target-action、Android: setOnRefreshListener)。
  • リストが最上部にない場合、Pull-to-Refreshは自動的にブロックされます — スクロールとの競合はアーキテクチャ的に排除されています。

Pull-to-Refreshとは?

Pull-to-Refreshは、ユーザーがリストやスクロール可能な領域を下に引っ張って(pull down)コンテンツを更新するユーザーインターフェースパターンです。視覚的には、ジェスチャーには画面上部に表示される読み込みインジケーター(スピナー)が伴い、データを受信すると消えます。このパターンはiPhone向けアプリTweetie(2008)によって普及し、その後Apple(iOS 6 — UIRefreshControl)とGoogle(Android Support Library — SwipeRefreshLayout)によって標準化されました。

技術的な観点から見ると、Pull-to-Refreshはパンニング(指の変位の追跡)としきい値に達したときのトリガーの組み合わせです。ユーザーは抵抗(抵抗性オーバースクロール)に打ち勝ってリストを下に引っ張り、しきい値(iOSで約80px、Androidで約64dp)を超えると、インジケーターのアニメーションと非同期読み込みが開始されます。ユーザーがしきい値に達する前に指を離すと、リストは更新されずに元の位置に戻ります。

Material Designガイドラインによると、Pull-to-Refreshはナビゲーションやタブ切り替えに使用すべきではありません — その唯一の目的はデータの更新です。IT Sectrでは、データの鮮度がユーザーエクスペリエンスにとって重要なニュースフィード、注文フィード、チャットでPull-to-Refreshを使用しています。

iOSのPull-to-Refresh: UIRefreshControl

UIRefreshControlは、Pull-to-Refresh用の標準iOSコントロールで、iOS 6から利用可能です。UIRefreshControlは、refreshControlプロパティ(iOS 10+)を介してUITableViewControllerに追加されるか、以前のバージョンではテーブルのサブビューとして追加されます。カスタマイズ可能な色(tintColor)、title属性、ラベル付きの属性付き文字列(例:「更新中...」)を備えた組み込みスピナーが含まれています。

UIRefreshControlはtarget-actionメカニズムを介して動作します:ジェスチャーがアクティブになると、指定されたメソッド(例:refresh(_:))が呼び出されます。メソッド内で、非同期データ読み込みが実行されます。完了後、endRefreshing()が呼び出され、アニメーションでインジケーターを非表示にします。UIRefreshControlは自動的にジェスチャーの感度を管理します — テーブルが最上部にある場合(contentOffset.y <= 0)にのみトリガーされます。

tintColorプロパティはスピナーの色を設定します。attributedTitleを使用すると、完了後に「2分前に更新」などのテキストを表示できます。iOS 10以降、UIRefreshControlはUIActivityIndicatorViewまたは永続的なカスタムビューを介したカスタムアニメーションをサポートしています。IT Sectrでは、ブランドに合わせてtintColorを設定し、attributedTitleを介して最終更新時刻を表示しています — これにより、ユーザーのデータに対する信頼が高まります。

AndroidのPull-to-Refresh: SwipeRefreshLayout

SwipeRefreshLayoutは、Android Support Library(androidx.swiperefreshlayout)のViewGroupで、スクロール可能なコンテンツ(RecyclerView、NestedScrollView、ListView)をラップし、Pull-to-Refresh機能を追加します。UIRefreshControl(コントロールであり、コンテナではない)とは異なり、SwipeRefreshLayoutは子のタッチイベントをインターセプトし、しきい値を超えると更新インジケーターをトリガーするコンテナです。

SwipeRefreshLayoutは、setColorSchemeColors()を介した色のカスタマイズが可能な円形のMaterial Design進行状況インジケーターを使用します。setOnRefreshListenerメソッドはonRefresh()コールバックを設定し、その中で非同期読み込みが実行されます。完了後、setRefreshing(false)が呼び出されてインジケーターを非表示にします。重要:setRefreshing(true)はonRefresh()を再度呼び出します — そのため、プログラムで更新を開始するには、フラグまたはpostメソッドを使用してください。

setProgressBackgroundColorSchemeResourceプロパティはインジケーターの背景を変更します。setSize(SwipeRefreshLayout.LARGE)はスピナーのサイズを設定します。XMLレイアウトでは、SwipeRefreshLayoutがRecyclerViewをラップします:swipe_refresh_layout → recycler_view。Google I/O 2024によると、SwipeRefreshLayoutはコンテンツフィードを持つAndroidアプリの85%で使用されています。IT Sectrでは、非同期で読み込まれるリストを持つすべての画面をSwipeRefreshLayoutでラップしています — これにより、すべてのAndroidバージョンで一貫したUXが提供されます。

Material Pull-to-Refresh(Android 12+)

Android 12(Material You)以降、Googleはmaterial-1.6.0+ライブラリ(Composeの場合はandroidx.compose.material3.pulltorefresh)の新しいMaterial Pull-to-Refreshの使用を推奨しています。新しいAPIは、スプリングアニメーションのサポートと壁紙に基づくアダプティブカラーを備えたアニメーションインジケーターを使用します。SwipeRefreshLayoutはAndroid 12未満のバージョンでも互換性があります。

ベストプラクティスとよくある間違い

Pull-to-Refreshは実装が簡単なパターンですが、UXを低下させるよくある間違いがいくつかあります。それらと防止方法を見てみましょう。

  • 二重更新 — ユーザーは読み込みが完了する前にリストを複数回引っ張る可能性があります。解決策:開始時にisRefreshingフラグを設定し、onRefresh()でチェックします。iOSでは、endRefreshing()は完了後にのみ呼び出されます。UIRefreshControlでのジェスチャーブロッキングは組み込まれています。
  • フィードバックの欠如 — 読み込みインジケーターは、ユーザーがしきい値を超えた後に厳密に表示される必要があります。タッチ時にすぐにインジケーターを表示しないでください — ユーザーを混乱させます。iOSとAndroidはこれを自動的に処理します。
  • 更新時間の無視 — データが200ミリ秒で更新される場合、ユーザーが更新に気付くようにインジケーターを少なくとも500ミリ秒表示する必要があります。UIRefreshControlには最小アニメーション時間があります。Androidでは、最小表示時間にHandler.postDelayedを使用します。
  • キーボードとの競合 — キーボードが開いていると、Pull-to-Refreshが誤ってトリガーされる可能性があります。iOSではview.endEditing(true)、AndroidではInputMethodManager.hideSoftInputFromWindow()を使用して、ジェスチャー開始時にキーボードを非表示にします。
  • 更新以外の目的での使用 — ナビゲーション(タブ切り替え、戻る)にPull-to-Refreshを使用しないでください。これは両方のプラットフォームのHIGに違反し、ユーザーを混乱させます。

IT Sectrでは、テストサーバーのログで重複リクエストを発見した後、すべてのプロジェクトにisRefreshingチェックを追加しました — 指の速いユーザーが最大3回連続で更新をトリガーしていたことが判明しました。

SwiftとKotlinのコード例

例1: iOSのUIRefreshControl(Swift)

カスタムスピナー色と属性付きタイトルでUITableViewControllerにPull-to-Refreshを追加します。データ読み込み後、インジケーターが非表示になります。

swift
import UIKit

class FeedTableViewController: UITableViewController {

    private var items: [String] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.refreshControl = UIRefreshControl()
        refreshControl?.tintColor = .systemBlue
        refreshControl?.attributedTitle = NSAttributedString(
            string: "引っ張って更新"
        )
        refreshControl?.addTarget(
            self,
            action: #selector(refreshData),
            for: .valueChanged
        )
    }

    @objc private func refreshData() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
            self.items = FeedService().fetchLatest()
            self.tableView.reloadData()
            self.refreshControl?.endRefreshing()
        }
    }
}

tableView.refreshControlプロパティ(iOS 10+)がUIRefreshControlを設定します。.valueChangedイベントを持つaddTargetは、ジェスチャーがアクティブになったときに発生します。endRefreshing()は必須です — これがないとインジケーターが無限に回転し続けます。非同期読み込みはDispatchQueue.main.asyncAfterでシミュレートされています — 実際のプロジェクトでは、URLSessionまたはasync/awaitを使用します。

例2: AndroidのSwipeRefreshLayout(Kotlin)

カスタムインジケーター色でSwipeRefreshLayoutにRecyclerViewをラップします。onRefreshが読み込みを開始し、完了後にインジケーターを非表示にします。

kotlin
class FeedFragment : Fragment() {

    private var _binding: FragmentFeedBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentFeedBinding.inflate(inflater, container, false)

        binding.swipeRefreshLayout.setColorSchemeColors(
            resources.getColor(R.color.brand_blue, null),
            resources.getColor(R.color.brand_green, null)
        )
        binding.swipeRefreshLayout.setOnRefreshListener {
            loadData()
        }
        return binding.root
    }

    private fun loadData() {
        viewModelScope.launch {
            try {
                val result = repository.getLatestFeed()
                adapter.submitList(result)
            } finally {
                binding.swipeRefreshLayout.isRefreshing = false
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

setColorSchemeColorsは回転するMaterial Designインジケーターの色を設定します。isRefreshing = falseはfinallyブロックで呼び出され、読み込みエラー時でもインジケーターを非表示にします。ViewModelScope.launchはフラグメントのライフサイクル内でコルーチンを実行します — フラグメントが破棄されると、コルーチンは自動的にキャンセルされ、メモリリークを防ぎます。

例3: SwiftUI .refreshable(iOS 15+)

最新のSwiftUIは.refreshable修飾子を提供し、ListまたはScrollViewに自動的にPull-to-Refreshを追加します。

swift
import SwiftUI

struct FeedView: View {

    @State private var items: [String] = []

    var body: some View {
        List(items, id: \.self) { item in
            Text(item)
        }
        .refreshable {
            items = await FeedService().fetchLatestAsync()
        }
    }
}

.refreshable修飾子は、Pull-to-Refreshで実行されるasyncクロージャを受け取ります。SwiftUIは自動的に更新インジケーターを表示・非表示にし、状態の競合を管理し(現在の更新が完了するまで新しい更新を開始しない)、プラットフォームに合わせてアニメーションを適応させます。iOS 15+では、これがSwiftUIでPull-to-Refreshを実装する推奨方法です。

よくある質問

SwiftUIでPull-to-Refreshは動作しますか?

はい、SwiftUIはListまたはScrollView用に.refreshable修飾子を提供しており、iOS 15から利用可能です。クロージャ内で非同期データ読み込みコードが実行されます。SwiftUIは自動的に更新インジケーターを管理し、現在の読み込みが完了するまで繰り返しのトリガーをブロックします — これは新しいプロジェクトの標準的な推奨アプローチです。

二重更新を防ぐには?

isRefreshingフラグを使用します:読み込み開始時にtrue、完了後にfalseに設定します。iOSでは、endRefreshing()が呼び出されるまでUIRefreshControlが自動的に繰り返し呼び出しをブロックします。Androidでは、onRefresh()の開始時にSwipeRefreshLayout.isRefreshingをチェックします:trueの場合はreturnします。これにより、1ジェスチャーあたり1リクエストが保証されます。

Pull-to-Refreshはリストのスクロールと競合しますか?

UIRefreshControlとSwipeRefreshLayoutは、リストが最上部にある場合(contentOffset == 0)にのみトリガーされます。アーキテクチャが競合を排除しています:リストが1pxでもスクロールされている間は、Pull-to-Refreshジェスチャーはアクティブになりません。競合が発生した場合は、AndroidのnestedScrollingEnabled、またはタッチをインターセプトするカスタムGestureRecognizerの有無を確認してください。

まとめ

  • Pull-to-Refreshは、AppleとGoogleがすべてのモバイルプラットフォームで標準化した、下に引っ張るジェスチャーによるデータ更新パターンです。
  • iOSのUIRefreshControl — target-action、tintColor、attributedTitle、必須のendRefreshing()を備えたコントロール。
  • AndroidのSwipeRefreshLayout — setOnRefreshListener、setColorSchemeColors、isRefreshingを備えたViewGroupコンテナ。
  • Material Pull-to-Refresh(Android 12+) — スプリングアニメーションを備えた新しいAPIで、新規プロジェクトに推奨。
  • SwiftUI .refreshable — asyncクロージャを備えた宣言的修飾子、iOS 15から利用可能。
  • isRefreshingフラグは二重更新を防止します — 両方のプラットフォームで必須。
  • Pull-to-Refreshはナビゲーション用ではなく、Material DesignとApple HIGに従ったコンテンツ更新専用です。

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください