Screen Time API trên iOS năm 2026: những điều tài liệu của Apple không nói

FamilyControls, DeviceActivity và các shield extension, rút ra từ việc phát hành một app chặn ứng dụng. Bài của Thomas Willheim · 30 tháng 9, 2026

Mình đã dành tháng 8 và tháng 9 năm 2026 để phát hành một app chặn ứng dụng trên iOS. App dùng các framework Screen Time (FamilyControls, DeviceActivity, ManagedSettings và ManagedSettingsUI) từ React Native, thông qua Expo SDK 57 và thư viện react-native-device-activity (0.6.1). Tài liệu của Apple cho bạn biết từng kiểu dữ liệu là gì. Nhưng gần như không nói gì về cách chúng hoạt động trên một chiếc điện thoại thật, qua đêm, trên iOS 26.

Đây là danh sách mà mình ước gì đã có từ đầu. Những hành vi mình thấy trên thiết bị nhưng không tìm được tài liệu nào ghi lại được gắn nhãn quan sát; chúng có thể không đúng trên mọi phiên bản iOS. Những mục chỉ liên quan tới thư viện React Native được gắn nhãn thư viện; bỏ qua chúng nếu bạn viết Swift trực tiếp.

1. Các thành phần

“Screen Time API” thực chất là bốn framework và, trên thực tế, ba app extension:

Extension mới là cốt lõi. App của mình thường đã bị treo hoặc bị tắt khi người dùng hết thời gian, nên chính monitor extension là thứ dựng shield lên lại. Hãy thiết kế như thể tiến trình app của bạn sẽ không bao giờ có mặt khi mọi chuyện xảy ra.

2. Entitlement: mỗi bundle ID một yêu cầu

Entitlement Family Controls (Distribution) được duyệt theo từng bundle ID. Với ba extension, vậy là bốn yêu cầu. Của mình là:

Khi đã được duyệt, hãy bật capability Family Controls (Distribution) trên từng App ID trong developer portal, rồi tạo một App Group và gắn nó vào cả bốn. Thiếu một cái là target đó không provision được. Việc duyệt không diễn ra ngay, nên hãy gửi cả bốn yêu cầu ngay ngày đầu tiên, trước khi bạn viết được bao nhiêu code.

File entitlements của extension nào rồi cũng giống nhau:

<key>com.apple.developer.family-controls</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
  <string>group.com.honorfit.blocker</string>
</array>

Trên Expo/EAS, dịch vụ build chỉ tạo provisioning profile cho những extension mà nó biết. Mình khai báo từng target, kèm entitlements của nó, trong extra.eas.build.experimental.ios.appExtensions ở app.json:

"appExtensions": [
  {
    "bundleIdentifier": "com.honorfit.blocker.ShieldConfiguration",
    "targetName": "ShieldConfiguration",
    "entitlements": {
      "com.apple.developer.family-controls": true,
      "com.apple.security.application-groups": ["group.com.honorfit.blocker"]
    }
  }
  // ...same for ShieldAction and ActivityMonitorExtension
]

3. App Group là kênh liên lạc duy nhất

App và các extension của nó là những tiến trình riêng biệt, gần như không bao giờ chạy cùng lúc. Thứ duy nhất chúng dùng chung là container của App Group, thường thông qua UserDefaults(suiteName:). Mọi thứ các extension cần (lựa chọn đã lưu, nội dung chữ trên shield, việc cần làm khi một ngưỡng kích hoạt) đều phải được ghi vào đó từ trước.

react-native-device-activity đẩy cách làm này tới tận cùng. App ghi một danh sách “action” dưới một khóa kiểu actions_for_{activity}_{callback}_{event}, và monitor extension tra chúng ra đúng lúc callback kích hoạt. Một hệ quả hữu ích: bạn có thể thay đổi việc một monitor đang chạy sẽ làm mà không cần khởi động lại nó. Các extension cũng gọi CFPreferencesAppSynchronize trước khi đọc, để thấy được những gì app vừa ghi mới nhất:

let appGroup = Bundle.main.object(
  forInfoDictionaryKey: "REACT_NATIVE_DEVICE_ACTIVITY_APP_GROUP") as? String
var userDefaults = UserDefaults(suiteName: appGroup)

// in each callback:
CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication)
if let actions = userDefaults?.array(forKey: triggeredBy) { /* run them */ }

4. Token là mờ, và bộ chọn

Bộ chọn trả cho app của bạn các ApplicationToken, ActivityCategoryToken và WebDomainToken, ngoài ra không có gì khác. Theo thiết kế, app không thể biến một token ngược lại thành “Instagram”. Điều nó làm được là đếm chúng, nên màn hình danh sách chặn của mình hiện “3 ứng dụng, 1 danh mục”. Đó là toàn bộ mô hình quyền riêng tư, và đó là một mô hình tốt: app của mình đúng nghĩa đen là không biết bạn đã chặn gì.

Bộ chọn ứng dụng Thời gian sử dụng của iOS: danh sách các danh mục ứng dụng, với danh mục Sức khỏe & Thể hình được mở ra để hiện các app bên trong
FamilyActivityPicker của Apple. Người dùng thấy tên và biểu tượng app; app của bạn chỉ nhận lại các token mờ.

Shield extension thì khác. Ở đó, hệ thống đưa cho bạn một Application cho app đang bị che chắn, và mình dùng localizedDisplayName của nó trong nội dung chữ trên shield. Còn bản thân app thì từ đầu đến cuối chỉ thấy token.

Hai lưu ý thực tế:

<DeviceActivitySelectionSheetViewPersisted
  familyActivitySelectionId="distracting-apps"
  includeEntireCategory={false}   // must be present, or nothing preloads
  onSelectionChange={(e) => setCounts(e.nativeEvent)}
  onDismissRequest={() => setPickerVisible(false)}
/>

thư viện Thêm một điều nữa: blockSelection của thư viện cộng thêm token vào một danh sách chặn đã lưu. Sau khi người dùng sửa lựa chọn, những app bị bỏ ra vẫn tiếp tục bị che chắn. Nên lần nào mình cũng gọi resetBlocks() trước rồi mới chặn lại lựa chọn hiện tại.

5. Đo mức sử dụng bằng sự kiện ngưỡng

HonorFit cho bạn một số dư phút chỉ bị trừ khi một app bị chặn đang trên màn hình. DeviceActivity không có lệnh kiểu “cho tôi biết họ đã dùng bao lâu”; nó có các sự kiện ngưỡng, kích hoạt khi tổng thời gian sử dụng cộng dồn của một lựa chọn đạt tới một khoảng thời gian. Nên mình đăng ký một sự kiện cho mỗi 15 giây sử dụng, đặt tên theo phút: used-0.25, used-0.5, … cho tới hết số dư. Các phần tư biểu diễn chính xác được trong số thực nhị phân, nên tên sự kiện qua Number() vẫn ra đúng giá trị ban đầu. Bên dưới thư viện, đây chỉ là API thuần của Apple:

let schedule = DeviceActivitySchedule(
  intervalStart: DateComponents(hour: 9, minute: 45, second: 0),  // see section 6
  intervalEnd: DateComponents(hour: 23, minute: 59, second: 59),
  repeats: false
)
let event = DeviceActivityEvent(
  applications: selection.applicationTokens,
  categories: selection.categoryTokens,
  webDomains: selection.webDomainTokens,
  threshold: DateComponents(minute: 0, second: 15),
  includesPastActivity: false          // iOS 17.4+
)
try DeviceActivityCenter().startMonitoring(
  DeviceActivityName("honorfit-distracting-apps-1790000000000"),
  during: schedule,
  events: [DeviceActivityEvent.Name("used-0.25"): event]  // ...one per step
)

includesPastActivity: false (mặc định) nghĩa là thời gian sử dụng trước khi các sự kiện được đăng ký sẽ không được tính. Điều này quan trọng cho mẹo về lịch ở phần tiếp theo.

Khi sự kiện cuối cùng kích hoạt, monitor extension dựng lại shield và gửi thông báo “hết giờ”. Mỗi sự kiện 15 giây cập nhật một thông báo có định danh cố định, nên nó được thay tại chỗ và hoạt động như một đồng hồ đếm ngược trực tiếp tương đối. interruptionLevel = .passive đưa thông báo lặng lẽ vào Trung tâm thông báo mà không hiện banner.

thư viện Đừng dùng dấu gạch dưới trong tên event hay tên activity. Thư viện lưu các sự kiện đã kích hoạt dưới khóa events_{activity}_{callback}_{eventName} và đọc ngược lại bằng cách tách theo _. Một sự kiện tên used_7 sẽ quay về thành used, và không bao giờ khớp được với cái gì. Triệu chứng là một số dư không bao giờ giảm.

6. Lịch không bắt đầu khi bạn nghĩ

Nếu các sự kiện ngưỡng của bạn lặng lẽ không bao giờ kích hoạt, nhiều khả năng là do chuyện này.

“Bây giờ” đã là quá khứ

quan sát Một DeviceActivitySchedule không lặp lại được so khớp theo các thành phần ngày giờ, và monitor chạy khoảng thời gian khớp tiếp theo. Nếu bạn truyền giờ hiện tại làm intervalStart, thì tới lúc framework xét nó, thời điểm đó đã trôi qua. Lần khớp tiếp theo là ngày mai, và hôm nay không có gì kích hoạt: không ngưỡng, không callback, không lỗi. Cách sửa là lùi giờ bắt đầu lại để “bây giờ” nằm trong khoảng thời gian đó:

const now = new Date();
const backdated = new Date(now.getTime() - 15 * 60_000);
const intervalStart =
  backdated.getDate() === now.getDate()
    ? { hour: backdated.getHours(), minute: backdated.getMinutes(), second: backdated.getSeconds() }
    : { hour: 0, minute: 0, second: 0 };   // don't wrap past midnight

await startMonitoring(activityName,
  { intervalStart, intervalEnd: { hour: 23, minute: 59, second: 59 }, repeats: false },
  events);

Lùi giờ bắt đầu không thể tính phí người dùng cho những lần lướt trước đó, nhờ includesPastActivity: false. Nó cũng giữ khoảng thời gian trên mức tối thiểu của Apple: lịch ngắn hơn 15 phút sẽ bị từ chối, chuyện này sẽ làm khó bất kỳ ai bắt đầu một phiên ngay trước nửa đêm.

Lịch chứa “bây giờ” sẽ bắt đầu ngay lập tức

quan sát Mặt ngược lại: một lịch 00:00–23:59:59 chứa thời điểm hiện tại, nên nó bắt đầu ngay bây giờ, chứ không phải lúc nửa đêm tiếp theo. Mình tìm được hai cách để đặt lịch cho ngày mai:

quan sát Đừng dùng đầy đủ các thành phần năm/tháng/ngày trong lịch để nhắm tới một ngày cụ thể. Các callback của extension trở nên thất thường khi mình thử, khớp với những báo cáo trên Apple Developer Forums (thread 729841). Chỉ dùng giờ/phút/giây thôi.

Một lưu ý: những hành vi quanh nửa đêm này chỉ lộ ra trên thiết bị thật, qua đêm. Mình đã dựng các điều kiện bảo vệ xung quanh chúng nhưng chưa theo dõi được nhiều lần nửa đêm thật như mình muốn, nên hãy coi phần này là “những gì mình thấy”, không phải đặc tả.

7. stopMonitoring kích hoạt intervalDidEnd

quan sát Gọi stopMonitoring, hoặc gọi startMonitoring với tên của một monitor đang chạy, khiến iOS gọi intervalDidEnd trên extension của bạn như thể lịch đã kết thúc bình thường. Nếu intervalDidEnd của bạn dựng lại shield như một lưới an toàn (của mình thì có, để một monitor không thể hết hạn rồi bỏ mặc các app không bị chặn), thì mỗi lần app dừng một monitor, extension sẽ che chắn mọi thứ trong chốc lát.

Một người dùng đã báo triệu chứng này: Spotify bị tạm dừng mỗi lần họ mở HonorFit. Việc dừng monitor khiến extension che chắn lựa chọn trong một khoảnh khắc, làm Spotify bị tạm ngưng, trước khi app gỡ shield ra lại.

Hai cách sửa, cần cả hai. Thứ nhất, xóa các action của extension trước khi dừng, để nó không còn gì để chạy:

function stopActivities(names: string[]): void {
  for (const name of names) RNDA.cleanUpAfterActivity(name); // clear actions first
  RNDA.stopMonitoring(names);                                // then stop
}

Thứ hai, gắn cho mọi action của intervalDidEnd và intervalDidStart một mốc thời gian “không sớm hơn” đặt tại ranh giới thật của lịch, trừ đi một phút dự phòng. Extension kiểm tra mốc này trước khi chạy bất cứ thứ gì:

// TypeScript: register the safety net
RNDA.configureActions({
  activityName,
  callbackName: 'intervalDidEnd',
  actions: [{
    type: 'blockSelection',
    familyActivitySelectionId: 'distracting-apps',
    neverTriggerBefore: new Date(intervalEndMs - 60_000),
  }],
});

// Swift, in the DeviceActivityMonitor extension
if let neverTriggerBefore = neverTriggerBefore {
  let now = Date().timeIntervalSince1970 * 1000
  if now < neverTriggerBefore { return false }   // skip this action
}

Nếu viết Swift thuần: lưu thời điểm kết thúc thật vào App Group và cho intervalDidEnd thoát ra khi bị gọi sớm.

8. Ngưỡng “ma” trên iOS 26

Trên iOS 26.x, eventDidReachThreshold đôi khi kích hoạt dù chẳng có chút sử dụng thật nào: ngay sau khi lên lịch, hoặc khi một chiếc điện thoại đã khóa màn hình một lúc được cầm lên hay cắm sạc. Apple đã thừa nhận vấn đề này trong các thread trên diễn đàn lập trình viên (809410, 811305, 814559). Nghe nói lỗi đã được sửa trong một bản beta 26.5, nhưng sau đó người ta vẫn tiếp tục báo lỗi. Trong app của mình, triệu chứng là “hết giờ” sau khi điện thoại nằm trong túi, vì extension dựng lại shield và số dư bị trừ về 0.

Bạn không ngăn được sự kiện kích hoạt, nhưng bạn có thể không tin nó. Hai đặc điểm của ngưỡng thật làm được phần lớn công việc:

  1. Thời gian sử dụng không thể chạy nhanh hơn đồng hồ thật. used-7 không thể kích hoạt hợp lệ sớm hơn 7 phút sau khi monitor được bật. Mọi action ngưỡng đều mang neverTriggerBefore = armedAt + N minutes − 5s, nên extension bỏ qua những lần kích hoạt sớm một cách vô lý.
  2. Sử dụng thật đến từng chút một; ngưỡng ma đến thành một loạt. Khi app đọc lại các sự kiện đã kích hoạt, nó coi đó là ngưỡng ma nếu ngưỡng thấp nhất và cao nhất kích hoạt cách nhau trong khoảng 3 giây nhưng trải dài từ một phút sử dụng trở lên.
// Guard 1: drop thresholds that fired before they could have been reached
if (at < startedAt + minutes * 60_000 - 5_000) continue;

// Guard 2: lowest and highest fired in the same instant, a minute or more apart
if (highest.minutes - lowest.minutes >= 1 &&
    Math.abs(highest.at - lowest.at) <= 3_000) {
  return { usedMinutes: 0, partialMs: 0 };   // phantom: ignore the lot
}

Điều kiện bảo vệ thứ 2 chạy trong app, không phải trong extension, nên một ngưỡng ma vẫn có thể dựng shield lên. Nhưng số dư thì được giữ nguyên, và lần tới khi app được mở, nó sẽ gỡ shield ra lại. Phát hiện kiểu kích hoạt hàng loạt ngay trong extension sẽ cần Swift tự viết, mà mình thì chưa viết.

quan sát Có một vấn đề liên quan theo chiều ngược lại: những ngưỡng lẽ ra phải kích hoạt mà không kích hoạt. Khi dùng app kiểu vào ra liên tục trên iOS 26, mình thấy ngưỡng 15 giây đầu tiên của một monitor mới kích hoạt đúng giờ, còn những ngưỡng sau thì bị gộp lại hoặc mất hẳn (used-0.25 kích hoạt, từ used-0.5 trở lên thì không bao giờ). Cách lách của mình là coi bất kỳ monitor nào đã đo được mức sử dụng là cũ, và bật một monitor mới cho phần số dư còn lại mỗi khi app chuyển xuống nền. Như vậy mỗi lần khởi động lại sẽ bỏ qua tối đa 15 giây sử dụng, và mình chấp nhận được chuyện đó.

9. Shield không mở được app của bạn

Thiết kế hiển nhiên là một nút trên shield ghi “Kiếm thêm thời gian” và mở app của bạn. Không có cách chính thức nào để làm vậy; Apple DTS nói rằng không có API nào để shield extension khởi chạy app chính của nó. Một ShieldActionDelegate trả lời một lần bấm nút bằng một ShieldActionResponse (.none, .close hoặc .defer), và không cái nào mở được thứ gì.

quan sát Cách lách bạn sẽ tìm thấy (NSExtensionContext().open(url) từ ShieldAction extension, mà thư viện cung cấp dưới tên openUrl) không có tác dụng gì trên iOS hiện tại khi mình test trên thiết bị vào tháng 9 năm 2026. Nút bấm chỉ đóng shield lại. Gửi thông báo khi bấm nút cũng không ổn định với mình.

Thứ mình dùng thay vào đó: shield chỉ có một nút “Đóng”, và ShieldConfiguration extension gửi một thông báo cục bộ mỗi khi shield được hiển thị. Người dùng chạm vào banner và được đưa vào app. App ghi sẵn nội dung thông báo vào App Group từ trước, và gỡ thông báo đi khi app lên foreground. iOS có thể hỏi cấu hình shield nhiều lần cho mỗi lần hiển thị, nên việc gửi được debounce:

func postShieldShownNotification(placeholders: [String: String?]) {
  guard let config = userDefaults?.dictionary(forKey: "honorfitShieldShownNotification"),
        let payload = config["payload"] as? [String: Any] else { return }

  let minInterval = config["minIntervalSeconds"] as? Double ?? 30
  let now = Date().timeIntervalSince1970
  let lastSentAt = userDefaults?.double(forKey: "honorfitShieldShownNotificationLastSentAt") ?? 0
  if now - lastSentAt < minInterval { return }
  userDefaults?.set(now, forKey: "honorfitShieldShownNotificationLastSentAt")

  // builds a UNMutableNotificationContent and adds a UNNotificationRequest
  // with a fixed identifier, so repeats replace rather than stack
  sendNotification(contents: payload, placeholders: placeholders)
}

class ShieldConfigurationExtension: ShieldConfigurationDataSource {
  override func configuration(shielding application: Application) -> ShieldConfiguration {
    postShieldShownNotification(placeholders: [
      "applicationOrDomainDisplayName": application.localizedDisplayName
    ])
    return buildShield(/* title, subtitle, colors from the App Group */)
  }
  // ...same hook in the category and web-domain overloads
}

Đây là một mẹo chắp vá, và nó phụ thuộc vào việc người dùng cho phép thông báo, nhưng đó là con đường duy nhất vào app hoạt động ổn định với mình.

10. Giữ extension tùy chỉnh qua prebuild

thư viện Nếu bạn dùng Expo, expo prebuild sẽ chạy config plugin của react-native-device-activity, mà theo mặc định sẽ chép mã nguồn extension gốc của thư viện vào ./targets mỗi lần chạy. ShieldConfiguration mình đã tùy chỉnh bị thay thế mà không hề có cảnh báo. Cách sửa là một tùy chọn của plugin:

["react-native-device-activity", {
  "appleTeamId": "XXXXXXXXXX",
  "appGroup": "group.com.honorfit.blocker",
  "copyToTargetFolder": false
}]

Cái giá phải trả: khi nâng cấp thư viện, bạn phải tự so sánh targets/ và phần Swift dùng chung của nó với bản của bạn, rồi áp lại các thay đổi của mình.

11. Xcode 26, Simulator và việc test

12. Còn trên Android

Để so sánh, Android không có thứ gì tương đương được hỗ trợ chính thức, nên bạn tự xây lấy. Trình chặn Android của HonorFit là một foreground service (loại specialUse, vì chặn ứng dụng không thuộc loại nào trong danh sách) gọi UsageStatsManager mỗi giây khi màn hình đang bật để biết app nào đang ở phía trước. Khi một app bị chặn đang mở và số dư bằng 0, nó vẽ một màn hình khóa TYPE_APPLICATION_OVERLAY đè lên trên (“Hiển thị trên các ứng dụng khác”). Một boot receiver và một watchdog alarm khởi động lại service sau khi khởi động lại máy, cập nhật app, và khi bị các trình quản lý pin quá tay của hãng điện thoại tắt đi.

Những đánh đổi ở đây ngược hẳn với iOS. Bạn thấy được tên package thật, nên quyền riêng tư là một lời hứa bạn tự giữ chứ không phải thứ hệ điều hành ép buộc. Một app bị chặn có thể vẫn hiện ra trong tối đa một chu kỳ kiểm tra, và nó vẫn tiếp tục chạy bên dưới lớp phủ. Và bạn sẽ phải viết các bản khai báo trên Play Console cho loại foreground service, quyền hiển thị lớp phủ và quyền xem các package khác. Mình cố tình tránh dùng AccessibilityService, thứ bị xét duyệt khắt khe nhất.

13. Danh sách kiểm tra

Triệu chứngNguyên nhân có thểCách sửa
Provisioning thất bại cho một targetBundle ID đó thiếu entitlement hoặc App GroupXin Family Controls (Distribution) và gắn App Group cho mọi bundle ID
Không bao giờ có sự kiện ngưỡng nàointervalStart = bây giờ, nên monitor bị đặt lịch cho ngày maiLùi giờ bắt đầu; giữ khoảng thời gian ≥ 15 phút
Monitor dành cho ngày mai lại chạy hôm nayLịch chứa thời điểm hiện tạiLịch [00:00, T) đã hết hạn trong hôm nay, hoặc repeats: true với các action có điều kiện bảo vệ
Các app bị che chắn chớp nhoáng khi app của bạn mởstopMonitoring kích hoạt intervalDidEndXóa action trước khi dừng; bảo vệ bằng mốc “không sớm hơn”
“Hết giờ” sau khi điện thoại để yên một lúcNgưỡng ma trên iOS 26Bảo vệ theo đồng hồ thật cộng với bộ lọc kích hoạt hàng loạt
Các ngưỡng về sau không bao giờ kích hoạtBị mất/gộp khi dùng ngắt quãngBật lại một monitor mới mỗi khi app xuống nền
Nút trên shield không mở được appKhông có API chính thứcGửi thông báo từ ShieldConfiguration khi shield hiển thị
Các sự kiện đã kích hoạt không khớp (RN)Có dấu gạch dưới trong tên sự kiệnDùng used-7, không dùng used_7
Bộ chọn mở ra trống trơn (RN)Thiếu prop includeEntireCategoryTruyền nó vào, với giá trị bất kỳ

14. Trả lời nhanh

Mình có cần entitlement Family Controls cho từng extension không?

Có. Nó được cấp theo từng bundle ID. App chính cộng với một DeviceActivityMonitor, một ShieldConfiguration và một ShieldAction extension là bốn lần duyệt, và mỗi App ID cũng cần được gắn App Group dùng chung.

Vì sao eventDidReachThreshold của DeviceActivity không kích hoạt?

Với mình, thường là do một lịch không lặp lại bắt đầu từ “bây giờ”, nên bị đặt sang ngày mai. Hãy lùi intervalStart lại. Trên iOS 26, khi dùng app kiểu vào ra liên tục, các ngưỡng dưới một phút về sau cũng bị mất; bật lại một monitor mới giúp ích.

ShieldAction hay ShieldConfiguration extension có mở được app chính không?

Không bằng API chính thức nào, và cách lách bằng NSExtensionContext không có tác dụng gì trên iOS hiện tại khi mình test. Thứ mình đang dùng là một thông báo cục bộ gửi từ ShieldConfiguration khi shield được hiển thị.

Có test được Screen Time API trên iOS Simulator không?

Được một phần. Hộp thoại cấp quyền và bộ chọn có hiện ra trên simulator iOS 26, nhưng việc cấp quyền không hoàn tất, bộ chọn không có gì để liệt kê, và monitor extension không bao giờ chạy. Hãy dùng thiết bị thật.

15. Vì sao mình cần tất cả những thứ này

Mình làm HonorFit, một app chặn ứng dụng mà bạn kiếm thời gian sử dụng màn hình bằng cách hít đất. App chạy theo cơ chế tự giác: bạn tự nhập số lần tập, và không có camera, không AI, không gọi mạng. Mọi thứ ở trên là những gì cần làm để “số phút bạn kiếm được chỉ bị trừ khi một app bị chặn đang trên màn hình” chạy được trên iPhone. Nếu bạn muốn xem nó chạy thế nào, app có trên App Store và Google Play. Nếu bạn tới đây vì muốn có một app như vậy chứ không phải muốn tự làm, mình đã viết về cách các app chặn ứng dụng bằng hít đất hoạt động và một bài tổng hợp trung thực về các app hiện có, kể cả đối thủ. Nếu bạn gặp một hành vi của Screen Time không có ở đây, hoặc thấy điều gì ở trên sai trên một bản iOS mới hơn, mình rất muốn được nghe.

Đọc tiếp