ประเด็นสำคัญ
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 ในฟีดข่าว ฟีดคำสั่งซื้อ และแชทซึ่งความสดใหม่ของข้อมูลมีความสำคัญต่อประสบการณ์ผู้ใช้
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 — ซึ่งเพิ่มความไว้วางใจของผู้ใช้ในข้อมูล
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
เริ่มตั้งแต่ 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 เป็นรูปแบบที่ง่ายต่อการนำไปใช้ แต่มีข้อผิดพลาดทั่วไปหลายประการที่ลดประสบการณ์ผู้ใช้ มาดูและวิธีป้องกันกัน
ที่ IT Sectr เราเพิ่มการตรวจสอบ isRefreshing ในทุกโปรเจกต์หลังจากพบคำขอซ้ำในบันทึกเซิร์ฟเวอร์ทดสอบ — ปรากฏว่าผู้ใช้ที่มีนิ้วเร็วกำลังกระตุ้นการรีเฟรชมากถึง 3 ครั้งติดต่อกัน
เพิ่ม Pull-to-Refresh ไปยัง UITableViewController ด้วยสีสปินเนอร์แบบกำหนดเองและชื่อเรื่องที่มีแอตทริบิวต์ หลังจากโหลดข้อมูลแล้ว ตัวบ่งชี้จะถูกซ่อน
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
ห่อ RecyclerView ใน SwipeRefreshLayout ด้วยสีตัวบ่งชี้แบบกำหนดเอง onRefresh เริ่มโหลดและซ่อนตัวบ่งชี้หลังจากเสร็จสิ้น
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 จะถูกยกเลิกโดยอัตโนมัติ ป้องกันการรั่วไหลของหน่วยความจำ
SwiftUI สมัยใหม่มีตัวปรับแต่ง .refreshable ที่เพิ่ม Pull-to-Refresh ให้กับ List หรือ ScrollView โดยอัตโนมัติ
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
คำถามที่พบบ่อย
ใช่ SwiftUI มีตัวปรับแต่ง .refreshable สำหรับ List หรือ Scrollview พร้อมใช้งานตั้งแต่ iOS 15 ภายใน closure โค้ดโหลดข้อมูลแบบอะซิงโครนัสจะถูกดำเนินการ SwiftUI จัดการตัวบ่งชี้การรีเฟรชโดยอัตโนมัติและบล็อกการทำงานซ้ำจนกว่าการโหลดปัจจุบันจะเสร็จสิ้น — นี่เป็นแนวทางมาตรฐานที่แนะนำสำหรับโปรเจกต์ใหม่
ใช้แฟล็ก isRefreshing: ตั้งค่าเป็น true เมื่อเริ่มโหลดและ false หลังจากเสร็จสิ้น ใน iOS UIRefreshControl จะบล็อกการเรียกซ้ำโดยอัตโนมัติจนกว่าจะเรียก endRefreshing() ใน Android ให้ตรวจสอบ SwipeRefreshLayout.isRefreshing ที่จุดเริ่มต้นของ onRefresh(): ถ้าเป็น true — ให้ return รับประกันหนึ่งคำขอต่อหนึ่งท่าทาง
UIRefreshControl และ SwipeRefreshLayout จะทำงานเมื่อรายการอยู่ในตำแหน่งบนสุดเท่านั้น (contentOffset == 0) สถาปัตยกรรมไม่รวมความขัดแย้ง: ตราบใดที่รายการถูกเลื่อนแม้เพียง 1px ท่าทาง Pull-to-Refresh จะไม่ทำงาน หากเกิดความขัดแย้ง ให้ตรวจสอบ nestedScrollingEnabled ใน Android หรือมี GestureRecognizer แบบกำหนดเองที่สกัดกั้นการสัมผัส
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ