In-App Review trên iOS: Dùng SKStoreReviewController hay requestReview cho đúng từng phiên bản iOS

2026-08-11 16 phút đọc 3 lượt xemBởi Uwmee Software

Trên Android, việc hiển thị popup xin đánh giá ngay trong ứng dụng do In-App Review API của Google Play đảm nhiệm. Trên iOS, cơ chế tương đương thuộc về StoreKit và đã trải qua bốn thế hệ giao diện lập trình khác nhau, mỗi thế hệ dành cho một khoảng phiên bản hệ điều hành riêng.

Đây chính là chỗ gây nhầm lẫn nhiều nhất: rất nhiều đoạn mã tìm được trên mạng dùng lời gọi đã bị đánh dấu ngừng hỗ trợ, hoặc dùng đúng lời gọi nhưng đặt sai chỗ khiến popup không bao giờ hiện ra. Bài viết này đi hết từ lịch sử API, giới hạn cứng do Apple đặt ra, tới chiến lược chọn thời điểm gọi sao cho vừa tăng lượt đánh giá vừa không vi phạm quy định.

1. Android và iOS: cùng ý tưởng, khác cơ chế

Tiêu chíAndroid (Google Play)iOS (App Store)
Thư việnPlay Core Review, thêm qua trình quản lý phụ thuộcStoreKit, có sẵn trong hệ điều hành
Cách hoạt độngYêu cầu luồng đánh giá trước, sau đó mới khởi chạyGọi một lời đề nghị duy nhất, hệ thống tự quyết định
Giới hạn tần suấtDo Google kiểm soát, không công bố con số cụ thểTối đa ba lần cho mỗi ứng dụng trong vòng ba trăm sáu mươi lăm ngày
Người dùng có tắt được khôngKhông có công tắc riêngCó, trong phần cài đặt của App Store
Biết kết quả người dùng chấm mấy saoKhôngKhông
Hiện trên bản thử nghiệm nội bộCó, nếu cài từ kênh phân phối của chợ ứng dụngKhông hiện trên bản TestFlight
Hiện trên bản gỡ lỗiKhôngCó, nhưng đánh giá không được gửi đi

Khác biệt ở hai dòng cuối rất hay bị hiểu ngược, và là nguyên nhân của phần lớn các báo cáo lỗi giả. Chi tiết ở mục 7.

2. Bốn thế hệ API và chọn cái nào cho dự án của bạn

Thế hệLời gọiÁp dụng từTrạng thái
Thứ nhấtSKStoreReviewController không tham sốiOS 10.3Đã ngừng hỗ trợ, không dùng cho dự án mới
Thứ haiSKStoreReviewController kèm tham số cảnh giao diệniOS 14Còn dùng được nhưng đã bị đánh dấu ngừng hỗ trợ ở các bản iOS gần đây
Thứ baBiến môi trường requestReview trong SwiftUIiOS 16Cách viết được khuyến nghị cho dự án SwiftUI
Thứ tưAppStore.requestReview kèm tham số cảnh giao diệniOS 18Cách viết được khuyến nghị cho dự án UIKit

Nguyên tắc chọn rất đơn giản: nếu ứng dụng dựng bằng SwiftUI thì dùng thế hệ thứ ba, nếu dựng bằng UIKit và có hỗ trợ iOS 18 trở lên thì dùng thế hệ thứ tư, còn nếu vẫn phải hỗ trợ các phiên bản cũ hơn thì viết nhánh điều kiện theo phiên bản. Điều quan trọng cần hiểu: cả bốn thế hệ đều dẫn tới cùng một hộp thoại hệ thống và cùng chịu chung giới hạn tần suất. Đổi sang lời gọi mới không làm popup xuất hiện nhiều hơn.

3. Mã nguồn cho SwiftUI

import SwiftUI
import StoreKit

struct CheckoutSuccessView: View {
    @Environment(\.requestReview) private var requestReview
    @AppStorage("successfulOrders") private var successfulOrders = 0

    var body: some View {
        VStack {
            Text("Đặt hàng thành công")
        }
        .onAppear {
            successfulOrders += 1
            if successfulOrders == 3 {
                Task {
                    try? await Task.sleep(for: .seconds(2))
                    requestReview()
                }
            }
        }
    }
}

Hai chi tiết trong đoạn mã trên không phải để cho đẹp. Thứ nhất, lời gọi nằm trong khối chạy sau khi màn hình đã hiện xong và có độ trễ hai giây, để người dùng kịp nhìn thấy kết quả thành công trước khi bị hộp thoại che mất. Thứ hai, điều kiện dùng dấu bằng đúng bằng ba chứ không phải lớn hơn hoặc bằng ba, để tránh gọi lại ở mọi đơn hàng sau đó.

4. Mã nguồn cho UIKit

import StoreKit
import UIKit

enum ReviewPrompter {
    static func requestIfAppropriate() {
        guard let scene = UIApplication.shared.connectedScenes
            .first(where: { $0.activationState == .foregroundActive })
            as? UIWindowScene else { return }

        if #available(iOS 18.0, *) {
            AppStore.requestReview(in: scene)
        } else {
            SKStoreReviewController.requestReview(in: scene)
        }
    }
}

Việc lấy đúng cảnh giao diện đang ở trạng thái hoạt động phía trước là bắt buộc. Nếu bạn lấy phần tử đầu tiên trong danh sách cảnh mà không lọc theo trạng thái, trên iPad chạy nhiều cửa sổ hoặc khi ứng dụng vừa quay lại từ nền, bạn có thể lấy trúng một cảnh không hiển thị và popup sẽ im lặng không xuất hiện.

5. Giới hạn cứng do Apple đặt ra

  • Tối đa ba lần mỗi ba trăm sáu mươi lăm ngày cho mỗi ứng dụng trên mỗi thiết bị. Đây là cửa sổ trượt chứ không phải theo năm dương lịch.
  • Hệ thống toàn quyền quyết định. Lời gọi của bạn chỉ là một đề nghị. Không có giá trị trả về, không có hàm gọi lại, không có cách nào biết popup đã hiện hay chưa.
  • Người dùng tắt được hoàn toàn. Trong phần cài đặt của App Store có công tắc cho phép chặn mọi hộp thoại đánh giá trong ứng dụng.
  • Người đã đánh giá phiên bản hiện tại sẽ không thấy popup nữa.
  • Không được gọi để phản hồi thao tác của người dùng. Đây là quy định của Apple chứ không phải khuyến nghị: hộp thoại này phải xuất hiện một cách tự nhiên trong luồng sử dụng, không phải sau khi người dùng bấm một nút nào đó. Trường hợp cần nút bấm thì dùng cách ở mục 6.

Hệ quả thực tế của giới hạn ba lần: bạn chỉ có ba cơ hội mỗi năm cho mỗi người dùng, và nếu tiêu phí chúng vào những thời điểm người dùng đang bực bội thì bạn vừa mất cơ hội vừa nhận về đánh giá thấp. Vì vậy phần chiến lược ở mục 8 quan trọng hơn phần mã nguồn rất nhiều.

6. Nút Đánh giá ứng dụng đặt trong màn hình cài đặt

Vì không được gọi hộp thoại hệ thống để phản hồi một cú chạm, nút đánh giá thủ công phải làm theo cách khác: mở thẳng trang ứng dụng trên App Store ở chế độ viết đánh giá.

func openWriteReviewPage(appID: String) {
    let urlString = "https://apps.apple.com/app/id" + appID + "?action=write-review"
    guard let url = URL(string: urlString) else { return }
    UIApplication.shared.open(url)
}

Tham số hành động ở cuối địa chỉ là phần quan trọng: thiếu nó, người dùng chỉ được đưa tới trang giới thiệu ứng dụng và phải tự cuộn xuống tìm chỗ viết đánh giá, tỉ lệ hoàn thành rơi rất mạnh. Trên Android, đường dẫn tương đương là địa chỉ mở trực tiếp trang ứng dụng trong Google Play.

Mô hình kết hợp hiệu quả nhất trong thực tế: dùng hộp thoại hệ thống cho luồng tự động ở những thời điểm đẹp, và đặt thêm một nút đánh giá thủ công trong màn hình cài đặt cho những người dùng chủ động muốn ủng hộ.

7. Tám lý do popup không hiện và cách phân biệt

Nguyên nhânCách nhận biếtXử lý
Đang chạy bản TestFlightBản cài từ ứng dụng TestFlightĐây là hành vi đúng, không phải lỗi. Hộp thoại không bao giờ hiện trên bản thử nghiệm qua TestFlight, xem bài TestFlight là gì
Đã đủ ba lần trong nămThiết bị đã cài và dùng ứng dụng lâu, đã từng thấy popupThử trên thiết bị khác hoặc chờ qua cửa sổ thời gian
Người dùng đã tắt trong cài đặtKhông bao giờ thấy popup của bất kỳ ứng dụng nàoKiểm tra công tắc trong phần cài đặt của App Store
Lấy sai cảnh giao diệnMã nguồn lấy phần tử đầu tiên mà không lọc trạng tháiLọc theo trạng thái hoạt động phía trước như mục 4
Gọi khi ứng dụng chưa ở trạng thái hoạt độngGọi ngay trong hàm khởi tạo hoặc lúc ứng dụng vừa quay lại từ nềnDời lời gọi vào sau khi giao diện đã hiện, thêm độ trễ ngắn
Gọi không trên luồng chínhGọi bên trong khối xử lý kết quả mạngChuyển lời gọi về luồng chính
Bị một lớp phủ khác che mấtCó hộp thoại hoặc màn hình đang trình bày cùng lúcChờ lớp phủ kia đóng xong rồi mới gọi
Người dùng đã đánh giá phiên bản hiện tạiTài khoản đã từng viết đánh giáĐây là hành vi đúng

8. Chiến lược chọn thời điểm: phần quyết định thành bại

Với ba cơ hội mỗi năm, việc chọn thời điểm quan trọng hơn mọi thứ khác. Ba nguyên tắc:

Nguyên tắc thứ nhất: chỉ hỏi sau một khoảnh khắc thành công

Thời điểm tốt là ngay sau khi người dùng vừa nhận được giá trị: hoàn tất đơn hàng, xuất xong tệp, thắng một màn chơi, hoàn thành chuỗi bảy ngày liên tiếp. Thời điểm tệ là sau khi có lỗi mạng, sau màn hình thanh toán thất bại, hoặc ngay khi vừa mở ứng dụng lần đầu.

Nguyên tắc thứ hai: cộng dồn tín hiệu, không dựa vào một sự kiện

struct ReviewGate {
    static func shouldAsk(sessions: Int, successEvents: Int, daysSinceInstall: Int,
                          lastAskedDaysAgo: Int, hadRecentCrash: Bool) -> Bool {
        guard !hadRecentCrash else { return false }
        guard daysSinceInstall >= 7 else { return false }
        guard sessions >= 5 else { return false }
        guard successEvents >= 2 else { return false }
        guard lastAskedDaysAgo >= 120 else { return false }
        return true
    }
}

Điều kiện về sự cố gần đây là chi tiết ít người nghĩ tới nhưng rất đáng giá: xin đánh giá ngay sau khi ứng dụng vừa văng là cách nhanh nhất để nhận một sao. Nếu bạn đang thu thập dữ liệu sự cố, hãy dùng nó làm cổng chặn.

Nguyên tắc thứ ba: giãn cách giữa các lần hỏi

Đặt khoảng cách tối thiểu khoảng bốn tháng giữa hai lần gọi. Ba lần trải đều trong năm luôn tốt hơn ba lần dồn trong một tháng, vì mỗi lần rơi vào một trạng thái tâm lý khác nhau của người dùng.

9. Ranh giới cấm: popup tự chế để lọc đánh giá xấu

Mô hình từng phổ biến một thời: hiện một hộp thoại tự viết hỏi bạn có hài lòng không, ai bấm có thì dẫn sang App Store, ai bấm không thì dẫn sang biểu mẫu góp ý nội bộ. Cả Apple lẫn Google đều xếp cách làm này vào nhóm thao túng đánh giá và ứng dụng có thể bị từ chối hoặc bị gỡ.

Ranh giới rõ ràng:

Được phépKhông được phép
Gọi hộp thoại hệ thống ở thời điểm hợp lýDùng popup tự chế để lọc người không hài lòng ra khỏi luồng đánh giá
Đặt nút đánh giá thủ công trong màn hình cài đặtChặn tính năng cho tới khi người dùng đánh giá
Có kênh góp ý nội bộ đặt song song, luôn hiện diệnTặng vật phẩm, tiền hoặc quyền lợi để đổi lấy đánh giá năm sao
Nhắc một lần rồi thôiHiện lại liên tục cho tới khi người dùng chịu bấm

Cách làm đúng và vẫn hiệu quả: đặt kênh góp ý nội bộ ở một chỗ dễ thấy và độc lập, đồng thời trả lời nghiêm túc mọi đánh giá tiêu cực nhận được. Bài cách trả lời đánh giá tiêu cực trên Google Play và App Store nói kỹ phần này, và bài chiến lược tăng điểm đánh giá tự nhiên bàn về bức tranh tổng thể.

10. Flutter: một thư viện cho cả hai nền tảng

import 'package:in_app_review/in_app_review.dart';

class ReviewService {
  final InAppReview _inAppReview = InAppReview.instance;

  Future<void> askForReview() async {
    if (await _inAppReview.isAvailable()) {
      await _inAppReview.requestReview();
    }
  }

  // Dùng cho nút Đánh giá ứng dụng đặt trong màn hình cài đặt
  Future<void> openStorePage() async {
    await _inAppReview.openStoreListing(appStoreId: '1234567890');
  }
}
HàmHành vi trên iOSHành vi trên Android
isAvailableKiểm tra phiên bản hệ điều hành có hỗ trợ hay khôngKiểm tra dịch vụ Google Play có sẵn hay không
requestReviewGọi hộp thoại của StoreKitGọi luồng In-App Review của Google Play
openStoreListingMở trang App Store, bắt buộc truyền mã ứng dụngMở trang Google Play theo tên gói

Lưu ý riêng cho Flutter: mã ứng dụng trên App Store là bắt buộc khi mở trang chợ, và đó là chuỗi số nằm trong địa chỉ trang ứng dụng chứ không phải định danh gói. Nhầm hai giá trị này là lỗi phổ biến nhất khi tích hợp.

11. React Native và Expo

import * as StoreReview from 'expo-store-review';

export async function askForReview() {
  if (await StoreReview.isAvailableAsync()) {
    await StoreReview.requestReview();
  } else {
    const url = StoreReview.storeUrl();
    if (url) Linking.openURL(url);
  }
}

Nhánh dự phòng ở đây có ý nghĩa thực tế: trên các thiết bị Android không có dịch vụ của Google, hàm kiểm tra sẽ trả về giá trị phủ định và bạn vẫn cần một đường thoát để người dùng đánh giá được.

12. Test đúng trên từng môi trường

Môi trườngHộp thoại có hiện khôngĐánh giá có được gửi khôngDùng để kiểm tra điều gì
Máy giả lậpKhôngVị trí và thời điểm gọi trong luồng giao diện
Thiết bị thật, bản gỡ lỗi từ XcodeCó, không bị giới hạn tần suấtKhôngLogic điều kiện, độ trễ, tương tác với các lớp phủ khác
Bản TestFlightKhông bao giờKhôngKhông dùng để kiểm tra tính năng này
Bản phát hành trên App StoreCó, chịu giới hạn ba lần mỗi nămHành vi thật

Kết luận quan trọng từ bảng này: đừng bao giờ báo lỗi vì popup không hiện trên bản TestFlight. Đó là hành vi thiết kế của Apple, không phải lỗi tích hợp. Hãy ghi rõ điều này vào tài liệu bàn giao cho đội kiểm thử, nếu không bạn sẽ nhận báo cáo lỗi giả ở mọi đợt phát hành.

13. Đo hiệu quả sau khi tích hợp

Ba chỉ số nên theo dõi trong ba tháng đầu:

  1. Số lượt đánh giá mới mỗi tuần so với giai đoạn trước khi tích hợp. Đây là chỉ số phản ứng nhanh nhất.
  2. Điểm trung bình của các đánh giá mới tách riêng khỏi điểm trung bình tích lũy. Nếu điểm mới thấp hơn điểm cũ, vấn đề nằm ở thời điểm gọi chứ không nằm ở chất lượng sản phẩm.
  3. Tỉ lệ đánh giá trên số người dùng hoạt động, để so sánh công bằng giữa các tháng có lượng tải khác nhau.

Điểm đánh giá là một trong những yếu tố ảnh hưởng trực tiếp tới tỉ lệ chuyển đổi từ lượt xem trang sang lượt tải, nên đây là hạng mục thuộc phạm vi tối ưu hiển thị trên chợ ứng dụng, xem bài ASO App Store Optimization.

14. Khi nào KHÔNG nên gọi In-App Review

  • Trong ba ngày đầu sau khi cài đặt. Người dùng chưa đủ trải nghiệm để có ý kiến, và bạn vừa đốt một trong ba cơ hội của năm.
  • Ngay sau khi ứng dụng vừa gặp sự cố hoặc vừa báo lỗi.
  • Trong luồng thanh toán. Bất kỳ thứ gì che màn hình lúc người dùng đang chuẩn bị trả tiền đều làm giảm tỉ lệ hoàn tất, xem thêm bài thiết kế màn hình Paywall.
  • Ngay sau khi vừa phát hành một bản cập nhật lớn thay đổi giao diện. Hãy chờ người dùng làm quen, thường là hai tuần.
  • Khi ứng dụng đang có lỗi nghiêm trọng chưa vá. Trong giai đoạn này, mỗi lời mời đánh giá là một lời mời cho điểm một sao.
  • Với nhóm người dùng vừa gửi phản hồi tiêu cực qua kênh nội bộ. Hãy loại họ khỏi luồng cho tới khi vấn đề của họ được xử lý xong.

Kết luận

Tích hợp popup xin đánh giá trên iOS chỉ mất chừng mười lăm phút viết mã, nhưng phần quyết định kết quả nằm ở ba chỗ: chọn đúng lời gọi cho phiên bản hệ điều hành, hiểu rằng bạn chỉ có ba cơ hội mỗi năm cho mỗi người dùng, và tuyệt đối không dùng popup tự chế để lọc đánh giá xấu.

Uwmee Software nhận phát triển ứng dụng iOS và Android trọn gói, tích hợp sẵn luồng xin đánh giá đúng chuẩn của cả hai chợ ứng dụng. Đọc thêm bài In-App Review API trên Android và bài chiến lược tăng điểm đánh giá tự nhiên.

Câu hỏi thường gặp

Bạn cần giải pháp phần mềm chuyên nghiệp?

Uwmee Software cung cấp dịch vụ 12 tester Google Play & thiết kế website cao cấp giúp nâng tầm thương hiệu của bạn.