Pull-to-Refresh: พื้นฐาน RefreshControl และ UIRefreshControl

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-02-27 เวลาอ่าน: 8 นาที
Pull-to-Refresh เป็นรูปแบบอินเทอร์เฟซมือถือที่ผู้ใช้ดึงรายการลงด้วยนิ้วเพื่อเริ่มโหลดข้อมูลใหม่ ท่าทางจะมาพร้อมกับตัวบ่งชี้ที่มองเห็นได้ — สปินเนอร์หมุนหรือไอคอนเคลื่อนไหว — ซึ่งจะหายไปเมื่อโหลดเสร็จสิ้น จากการวิเคราะห์ UX ของ Apple HIG Pull-to-Refresh กลายเป็นกลไกมาตรฐานในการอัปเดตเนื้อหาในฟีดข่าว โซเชียลเน็ตเวิร์ก และไคลเอนต์อีเมลนับตั้งแต่เปิดตัวใน Tweetie (2008) และการกำหนดมาตรฐานโดย Apple และ Google ในเวลาต่อมา

ประเด็นสำคัญ

  • Pull-to-Refresh เป็นท่าทางการดึงรายการลงเพื่อรีเฟรชข้อมูล พร้อมด้วยตัวบ่งชี้การโหลดที่มองเห็นได้
  • ใน iOS ใช้ UIRefreshControl (iOS 6+) เพิ่มไปยัง UITableViewController หรือ UIScrollView ผ่านคุณสมบัติ refreshControl
  • ใน Android ใช้ SwipeRefreshLayout (จาก Support Library) — ตัวห่อ ViewGroup สำหรับ RecyclerView หรือ NestedScrollView
  • ทั้งสอง API รองรับการปรับแต่งสี ตัวบ่งชี้ และ callback ผ่าน listener (iOS: UIRefreshControl.target-action, Android: setOnRefreshListener)
  • Pull-to-Refresh จะถูกบล็อกโดยอัตโนมัติเมื่อรายการไม่อยู่ในตำแหน่งบนสุด — ความขัดแย้งกับการเลื่อนถูกยกเว้นในทางสถาปัตยกรรม

Pull-to-Refresh คืออะไร?

Pull-to-Refresh เป็นรูปแบบอินเทอร์เฟซผู้ใช้ที่ผู้ใช้ดึง (pull down) รายการหรือพื้นที่ที่สามารถเลื่อนได้ลงเพื่อรีเฟรชเนื้อหา ในทางสายตา ท่าทางจะมาพร้อมกับตัวบ่งชี้การโหลด (สปินเนอร์) ที่ปรากฏที่ด้านบนของหน้าจอและหายไปหลังจากได้รับข้อมูล รูปแบบนี้ได้รับความนิยมจากแอป Tweetie สำหรับ iPhone (2008) และต่อมาได้ถูกกำหนดมาตรฐานโดย Apple (iOS 6 — UIRefreshControl) และ Google (Android Support Library — SwipeRefreshLayout)

จากมุมมองทางเทคนิค Pull-to-Refresh คือการรวมกันของการเลื่อนแบบแพน (การติดตามการเคลื่อนที่ของนิ้ว) และตัวกระตุ้นเมื่อถึงเกณฑ์ ผู้ใช้ดึงรายการลง เอาชนะแรงต้าน (overscroll แบบต้านทาน) และหลังจากเกินเกณฑ์ (~80px บน iOS, ~64dp บน Android) ภาพเคลื่อนไหวของตัวบ่งชี้และการโหลดแบบอะซิงโครนัสจะเริ่มขึ้น หากผู้ใช้ปล่อยนิ้วก่อนถึงเกณฑ์ รายการจะกลับไปยังตำแหน่งเดิมโดยไม่รีเฟรช

ตามแนวทาง Material Design ไม่ควรใช้ Pull-to-Refresh เพื่อการนำทางหรือการสลับแท็บ — จุดประสงค์เดียวคือการรีเฟรชข้อมูล ที่ IT Sectr เราใช้ Pull-to-Refresh ในฟีดข่าว ฟีดคำสั่งซื้อ และแชทซึ่งความสดใหม่ของข้อมูลมีความสำคัญต่อประสบการณ์ผู้ใช้

Pull-to-Refresh ใน iOS: UIRefreshControl

UIRefreshControl คือตัวควบคุม iOS มาตรฐานสำหรับ Pull-to-Refresh พร้อมใช้งานตั้งแต่ iOS 6 UIRefreshControl ถูกเพิ่มไปยัง UITableViewController ผ่านคุณสมบัติ refreshControl (iOS 10+) หรือเป็นมุมมองย่อยของตารางในเวอร์ชันก่อนหน้า ประกอบด้วยสปินเนอร์ในตัวที่มีสีที่ปรับแต่งได้ (tintColor), คุณลักษณะ title และสตริงที่มีแอตทริบิวต์พร้อมป้ายกำกับ (เช่น "กำลังอัปเดต...")

UIRefreshControl ทำงานผ่านกลไก target-action: เมื่อท่าทางถูกเปิดใช้งาน เมธอดที่ระบุจะถูกเรียก (เช่น refresh(_:)) ภายในเมธอดจะมีการโหลดข้อมูลแบบอะซิงโครนัส หลังจากเสร็จสิ้น จะเรียก endRefreshing() ซึ่งซ่อนตัวบ่งชี้พร้อมภาพเคลื่อนไหว UIRefreshControl จัดการความไวของท่าทางโดยอัตโนมัติ — จะทำงานเมื่อตารางอยู่ในตำแหน่งบนสุดเท่านั้น (contentOffset.y <= 0)

คุณสมบัติ tintColor กำหนดสีของสปินเนอร์ attributedTitle อนุญาตให้แสดงข้อความเช่น "อัปเดตเมื่อ 2 นาทีที่แล้ว" หลังจากเสร็จสิ้น ตั้งแต่ iOS 10 UIRefreshControl รองรับภาพเคลื่อนไหวแบบกำหนดเองผ่าน UIActivityIndicatorView หรือมุมมองแบบกำหนดเองแบบถาวร ที่ IT Sectr เรากำหนดค่า tintColor ตามแบรนด์และแสดงเวลาอัปเดตล่าสุดผ่าน attributedTitle — ซึ่งเพิ่มความไว้วางใจของผู้ใช้ในข้อมูล

Pull-to-Refresh ใน Android: SwipeRefreshLayout

SwipeRefreshLayout คือ ViewGroup จาก Android Support Library (androidx.swiperefreshlayout) ที่ห่อเนื้อหาที่สามารถเลื่อนได้ (RecyclerView, NestedScrollView, ListView) และเพิ่มฟังก์ชัน Pull-to-Refresh แตกต่างจาก UIRefreshControl (ซึ่งเป็นตัวควบคุม ไม่ใช่คอนเทนเนอร์) SwipeRefreshLayout เป็นคอนเทนเนอร์ที่สกัดกั้นเหตุการณ์สัมผัสขององค์ประกอบย่อยและกระตุ้นตัวบ่งชี้การรีเฟรชเมื่อเกินเกณฑ์

SwipeRefreshLayout ใช้ตัวบ่งชี้ความคืบหน้าแบบวงกลม Material Design พร้อมการปรับแต่งสีผ่าน setColorSchemeColors() เมธอด setOnRefreshListener กำหนด callback onRefresh() ซึ่งทำการโหลดแบบอะซิงโครนัส หลังจากเสร็จสิ้น จะเรียก setRefreshing(false) เพื่อซ่อนตัวบ่งชี้ สำคัญ: setRefreshing(true) จะเรียก onRefresh() อีกครั้ง — ดังนั้นสำหรับการเริ่มรีเฟรชด้วยโปรแกรม ให้ใช้แฟล็กหรือเมธอด post

คุณสมบัติ setProgressBackgroundColorSchemeResource เปลี่ยนพื้นหลังของตัวบ่งชี้ setSize(SwipeRefreshLayout.LARGE) กำหนดขนาดสปินเนอร์ ในเค้าโครง XML SwipeRefreshLayout ห่อ RecyclerView: swipe_refresh_layout → recycler_view ตามข้อมูลของ Google I/O 2024 SwipeRefreshLayout ถูกใช้ใน 85% ของแอป Android ที่มีฟีดเนื้อหา ที่ IT Sectr เราห่อหน้าจอทั้งหมดที่มีรายการที่โหลดแบบอะซิงโครนัสใน SwipeRefreshLayout — ซึ่งให้ประสบการณ์ผู้ใช้ที่สอดคล้องกันในทุกเวอร์ชัน Android

Material Pull-to-Refresh (Android 12+)

เริ่มตั้งแต่ Android 12 (Material You) Google แนะนำให้ใช้ Material Pull-to-Refresh ใหม่จากไลบรารี material-1.6.0+ (androidx.compose.material3.pulltorefresh สำหรับ Compose) API ใหม่ใช้ตัวบ่งชี้แบบเคลื่อนไหวพร้อมรองรับภาพเคลื่อนไหวแบบสปริงและสีที่ปรับเปลี่ยนตามวอลเปเปอร์ SwipeRefreshLayout ยังคงเข้ากันได้สำหรับเวอร์ชันที่ต่ำกว่า Android 12

แนวปฏิบัติที่ดีที่สุดและข้อผิดพลาดทั่วไป

Pull-to-Refresh เป็นรูปแบบที่ง่ายต่อการนำไปใช้ แต่มีข้อผิดพลาดทั่วไปหลายประการที่ลดประสบการณ์ผู้ใช้ มาดูและวิธีป้องกันกัน

  • การรีเฟรชซ้ำ — ผู้ใช้อาจดึงรายการหลายครั้งก่อนที่การโหลดจะเสร็จสิ้น วิธีแก้ไข: ตั้งค่าแฟล็ก isRefreshing เมื่อเริ่มต้นและตรวจสอบใน onRefresh() ใน iOS endRefreshing() จะถูกเรียกหลังจากเสร็จสิ้นเท่านั้น การบล็อกท่าทางใน UIRefreshControl เป็นแบบในตัว
  • ขาดการตอบกลับ — ตัวบ่งชี้การโหลดควรปรากฏหลังจากผู้ใช้เกินเกณฑ์เท่านั้น อย่าแสดงตัวบ่งชี้ทันทีเมื่อสัมผัส — ทำให้ผู้ใช้สับสน iOS และ Android ทำสิ่งนี้โดยอัตโนมัติ
  • ไม่สนใจระยะเวลาการรีเฟรช — หากข้อมูลรีเฟรชใน 200 ms ควรแสดงตัวบ่งชี้อย่างน้อย 500 ms เพื่อให้ผู้ใช้สังเกตเห็นการอัปเดต UIRefreshControl มีระยะเวลาภาพเคลื่อนไหวขั้นต่ำ ใน Android ใช้ Handler.postDelayed สำหรับเวลาแสดงผลขั้นต่ำ
  • ข้อขัดแย้งกับแป้นพิมพ์ — เมื่อเปิดแป้นพิมพ์ Pull-to-Refresh อาจทำงานโดยไม่ได้ตั้งใจ ซ่อนแป้นพิมพ์เมื่อเริ่มท่าทางผ่าน view.endEditing(true) ใน iOS และ InputMethodManager.hideSoftInputFromWindow() ใน Android
  • ใช้เพื่อวัตถุประสงค์อื่นนอกจากการรีเฟรช — อย่าใช้ Pull-to-Refresh เพื่อการนำทาง (การสลับแท็บ การกลับไป) สิ่งนี้ละเมิด HIG ของทั้งสองแพลตฟอร์มและทำให้ผู้ใช้สับสน

ที่ IT Sectr เราเพิ่มการตรวจสอบ isRefreshing ในทุกโปรเจกต์หลังจากพบคำขอซ้ำในบันทึกเซิร์ฟเวอร์ทดสอบ — ปรากฏว่าผู้ใช้ที่มีนิ้วเร็วกำลังกระตุ้นการรีเฟรชมากถึง 3 ครั้งติดต่อกัน

ตัวอย่างโค้ดใน Swift และ Kotlin

ตัวอย่างที่ 1: UIRefreshControl ใน iOS (Swift)

เพิ่ม Pull-to-Refresh ไปยัง UITableViewController ด้วยสีสปินเนอร์แบบกำหนดเองและชื่อเรื่องที่มีแอตทริบิวต์ หลังจากโหลดข้อมูลแล้ว ตัวบ่งชี้จะถูกซ่อน

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 addTarget กับเหตุการณ์ .valueChanged จะทำงานเมื่อท่าทางถูกเปิดใช้งาน endRefreshing() เป็นสิ่งจำเป็น — หากไม่มีตัวบ่งชี้จะหมุนตลอดไป การโหลดแบบอะซิงโครนัสจำลองด้วย DispatchQueue.main.asyncAfter — ในโปรเจกต์จริง ให้ใช้ URLSession หรือ async/await

ตัวอย่างที่ 2: SwipeRefreshLayout ใน Android (Kotlin)

ห่อ RecyclerView ใน SwipeRefreshLayout ด้วยสีตัวบ่งชี้แบบกำหนดเอง 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 ดำเนินการ coroutine ภายในวงจรชีวิตของ fragment — เมื่อ fragment ถูกทำลาย coroutine จะถูกยกเลิกโดยอัตโนมัติ ป้องกันการรั่วไหลของหน่วยความจำ

ตัวอย่างที่ 3: SwiftUI .refreshable (iOS 15+)

SwiftUI สมัยใหม่มีตัวปรับแต่ง .refreshable ที่เพิ่ม Pull-to-Refresh ให้กับ List หรือ ScrollView โดยอัตโนมัติ

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 รับ async-closure ที่ทำงานเมื่อ Pull-to-Refresh SwiftUI จะแสดงและซ่อนตัวบ่งชี้การรีเฟรชโดยอัตโนมัติ จัดการสภาวะการแข่งขัน (ไม่เริ่มการรีเฟรชใหม่จนกว่าการรีเฟรชปัจจุบันจะเสร็จสิ้น) และปรับภาพเคลื่อนไหวให้เข้ากับแพลตฟอร์ม สำหรับ iOS 15+ นี่เป็นวิธีที่แนะนำในการนำ Pull-to-Refresh ไปใช้ใน SwiftUI

คำถามที่พบบ่อย

Pull-to-Refresh ทำงานใน SwiftUI หรือไม่?

ใช่ SwiftUI มีตัวปรับแต่ง .refreshable สำหรับ List หรือ Scrollview พร้อมใช้งานตั้งแต่ iOS 15 ภายใน closure โค้ดโหลดข้อมูลแบบอะซิงโครนัสจะถูกดำเนินการ SwiftUI จัดการตัวบ่งชี้การรีเฟรชโดยอัตโนมัติและบล็อกการทำงานซ้ำจนกว่าการโหลดปัจจุบันจะเสร็จสิ้น — นี่เป็นแนวทางมาตรฐานที่แนะนำสำหรับโปรเจกต์ใหม่

จะป้องกันการรีเฟรชซ้ำได้อย่างไร?

ใช้แฟล็ก isRefreshing: ตั้งค่าเป็น true เมื่อเริ่มโหลดและ false หลังจากเสร็จสิ้น ใน iOS UIRefreshControl จะบล็อกการเรียกซ้ำโดยอัตโนมัติจนกว่าจะเรียก endRefreshing() ใน Android ให้ตรวจสอบ SwipeRefreshLayout.isRefreshing ที่จุดเริ่มต้นของ onRefresh(): ถ้าเป็น true — ให้ return รับประกันหนึ่งคำขอต่อหนึ่งท่าทาง

Pull-to-Refresh ขัดแย้งกับการเลื่อนรายการหรือไม่?

UIRefreshControl และ SwipeRefreshLayout จะทำงานเมื่อรายการอยู่ในตำแหน่งบนสุดเท่านั้น (contentOffset == 0) สถาปัตยกรรมไม่รวมความขัดแย้ง: ตราบใดที่รายการถูกเลื่อนแม้เพียง 1px ท่าทาง Pull-to-Refresh จะไม่ทำงาน หากเกิดความขัดแย้ง ให้ตรวจสอบ nestedScrollingEnabled ใน Android หรือมี GestureRecognizer แบบกำหนดเองที่สกัดกั้นการสัมผัส

สรุป

  • Pull-to-Refresh เป็นรูปแบบการรีเฟรชข้อมูลด้วยท่าทางดึงลง ซึ่งกำหนดมาตรฐานโดย Apple และ Google ในทุกแพลตฟอร์มมือถือ
  • UIRefreshControl ใน iOS — ตัวควบคุมที่มี target-action, tintColor, attributedTitle และ endRefreshing() ที่จำเป็น
  • SwipeRefreshLayout ใน Android — คอนเทนเนอร์ ViewGroup ที่มี setOnRefreshListener, setColorSchemeColors และ isRefreshing
  • Material Pull-to-Refresh (Android 12+) — API ใหม่พร้อมภาพเคลื่อนไหวแบบสปริง แนะนำสำหรับโปรเจกต์ใหม่
  • SwiftUI .refreshable — ตัวปรับแต่งแบบประกาศพร้อม async-closure พร้อมใช้งานตั้งแต่ iOS 15
  • แฟล็ก isRefreshing ป้องกันการรีเฟรชซ้ำ — จำเป็นในทั้งสองแพลตฟอร์ม
  • Pull-to-Refresh ไม่ได้มีไว้สำหรับการนำทาง — สำหรับการรีเฟรชเนื้อหาตาม Material Design และ Apple HIG เท่านั้น

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม