2026년의 iOS Screen Time API: Apple 문서에는 없는 이야기

앱 차단 앱을 출시하며 겪은 FamilyControls, DeviceActivity, 실드 익스텐션. 글: Thomas Willheim · 2026년 9월 30일

2026년 8월과 9월을 iOS 앱 차단 앱을 출시하는 데 썼어요. 이 앱은 React Native에서 Expo SDK 57과 react-native-device-activity 라이브러리(0.6.1)를 거쳐 스크린 타임 프레임워크(FamilyControls, DeviceActivity, ManagedSettings, ManagedSettingsUI)를 써요. Apple 문서는 각 타입이 무엇인지는 알려 줘요. 하지만 실제 폰에서, 밤사이에, iOS 26에서 어떻게 동작하는지는 거의 말해 주지 않아요.

이건 제가 처음에 있었으면 했던 목록이에요. 기기에서 직접 봤지만 문서에서는 찾을 수 없는 동작에는 관찰 표시를 붙였어요. 모든 iOS 버전에서 똑같지는 않을 수 있어요. React Native 라이브러리에만 해당하는 항목에는 라이브러리 표시를 붙였어요. Swift로 직접 작성한다면 건너뛰세요.

1. 구성 요소

‘Screen Time API’는 프레임워크 네 개와, 실제로는 앱 익스텐션 세 개로 이루어져 있어요.

핵심은 익스텐션이에요. 사용자의 시간이 다 떨어질 때 제 앱은 대개 일시 중단됐거나 종료된 상태라서, 실드를 다시 올리는 건 모니터 익스텐션이에요. 무슨 일이 일어날 때 앱 프로세스는 절대 없다고 가정하고 설계하세요.

2. 엔타이틀먼트: 번들 ID마다 한 번씩 신청

Family Controls (Distribution) 엔타이틀먼트는 번들 ID 단위로 승인돼요. 익스텐션이 세 개면 신청도 네 번이에요. 제 경우는 이랬어요.

승인이 나면 개발자 포털에서 각각의 App ID에 Family Controls (Distribution) capability를 켜고, App Group을 하나 만들어 네 개 모두에 붙이세요. 하나라도 빠지면 그 타깃은 프로비저닝이 안 돼요. 승인은 바로 나지 않으니, 코드를 많이 쓰기 전 첫날에 네 개를 모두 신청해 두세요.

결국 모든 익스텐션의 entitlements 파일은 똑같아져요.

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

Expo/EAS에서는 빌드 서비스가 자기가 아는 익스텐션에 대해서만 프로비저닝 프로필을 만들어요. 그래서 저는 app.json의 extra.eas.build.experimental.ios.appExtensions 아래에 각 타깃을 엔타이틀먼트와 함께 선언해요.

"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이 유일한 통로

앱과 익스텐션은 별개의 프로세스이고, 동시에 실행되는 일은 거의 없어요. 둘이 공유하는 건 App Group 컨테이너 하나뿐이고, 보통 UserDefaults(suiteName:)로 써요. 익스텐션에 필요한 모든 것(저장된 선택 항목, 실드 문구, 임계값이 발생했을 때 할 일)은 미리 거기에 써 둬야 해요.

react-native-device-activity는 이걸 끝까지 밀어붙여요. 앱이 actions_for_{activity}_{callback}_{event} 같은 키 아래에 ‘액션’ 목록을 써 두면, 모니터 익스텐션은 콜백이 발생하는 순간에 그걸 찾아봐요. 덕분에 이미 돌고 있는 모니터를 재시작하지 않고도 하는 일을 바꿀 수 있어요. 익스텐션은 읽기 전에 CFPreferencesAppSynchronize도 호출해서 앱이 마지막으로 쓴 값을 보게 돼요.

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. 토큰은 불투명하다, 그리고 선택기

선택기가 앱에 넘겨주는 건 ApplicationToken, ActivityCategoryToken, WebDomainToken뿐이에요. 설계상 앱은 토큰을 다시 ‘인스타그램’으로 바꿀 수 없어요. 할 수 있는 건 개수를 세는 것뿐이라, 제 차단 목록 화면에는 ‘앱 3개, 카테고리 1개’라고 나와요. 이게 개인정보 모델의 전부이고, 좋은 모델이에요. 제 앱은 사용자가 뭘 차단했는지 말 그대로 몰라요.

iOS 스크린 타임 앱 선택기: 앱 카테고리 목록에서 건강 및 피트니스를 펼쳐 그 안의 앱이 보이는 화면
Apple의 FamilyActivityPicker. 사용자에게는 앱 이름과 아이콘이 보이지만, 앱이 돌려받는 건 불투명한 토큰뿐이에요.

실드 익스텐션은 달라요. 거기서는 시스템이 실드가 씌워지는 앱의 Application을 넘겨주고, 저는 그 localizedDisplayName을 실드 문구에 써요. 앱 본체는 끝까지 토큰만 봐요.

실무적인 메모 두 가지:

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

라이브러리 하나 더: 라이브러리의 blockSelection은 저장된 차단 목록에 토큰을 추가해요. 사용자가 선택을 수정하고 나면, 빠진 앱에도 실드가 그대로 남아 있어요. 그래서 저는 매번 resetBlocks()를 먼저 호출하고 현재 선택 항목을 다시 차단해요.

5. 임계값 이벤트로 사용 시간 재기

HonorFit에는 차단한 앱이 화면에 떠 있을 때만 줄어드는 잔여 시간이 있어요. DeviceActivity에는 ‘얼마나 썼는지 알려 줘’ 같은 호출이 없어요. 대신 선택 항목의 누적 사용 시간이 정해진 길이에 도달하면 한 번 발생하는 임계값 이벤트가 있어요. 그래서 저는 15초 사용마다 이벤트를 하나씩, 분 단위 이름으로 등록해요. used-0.25, used-0.5, … 잔여 시간까지요. 4분의 1 단위는 이진 부동소수점으로 정확히 표현되니까, 이름이 Number()를 거쳐도 그대로 돌아와요. 라이브러리 아래에서는 평범한 Apple API예요.

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(기본값)는 이벤트를 등록하기 전의 사용 시간은 세지 않는다는 뜻이에요. 다음 섹션의 스케줄 요령에서 이게 중요해요.

마지막 이벤트가 발생하면 모니터 익스텐션이 실드를 다시 씌우고 ‘시간이 다 됐어요’ 알림을 보내요. 15초마다 오는 이벤트는 고정된 식별자를 가진 알림 하나를 갱신하기 때문에, 알림이 제자리에서 바뀌면서 대략적인 실시간 카운트다운 역할을 해요. interruptionLevel = .passive로 하면 배너 없이 알림 센터에 조용히 들어가요.

라이브러리 이벤트나 액티비티 이름에 밑줄을 넣지 마세요. 라이브러리는 발생한 이벤트를 events_{activity}_{callback}_{eventName} 아래에 저장하고, 나중에 _로 쪼개서 다시 읽어요. used_7이라는 이벤트는 used로 돌아오고, 아무것도 일치하지 않게 돼요. 증상은 잔여 시간이 절대 줄지 않는 거였어요.

6. 스케줄은 생각한 때 시작하지 않는다

임계값 이벤트가 아무 소리 없이 끝내 발생하지 않는다면, 아마 이것 때문일 거예요.

‘지금’은 이미 과거다

관찰 반복 없는 DeviceActivitySchedule은 날짜 구성 요소(date components)로 매칭되고, 모니터는 일치하는 다음 구간을 실행해요. 현재 시각을 intervalStart로 넘기면, 프레임워크가 그걸 평가할 즈음엔 그 순간이 이미 지나 있어요. 다음 일치는 내일이고, 오늘은 아무것도 발생하지 않아요. 임계값도, 콜백도, 오류도 없이요. 해결법은 시작 시각을 앞당겨서 ‘지금’이 구간 안에 들어가게 하는 거예요.

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);

시작 시각을 앞당겨도 includesPastActivity: false 덕분에 그 전에 스크롤한 시간이 사용자에게 청구되지는 않아요. 구간을 Apple의 최소 길이 이상으로 유지해 주는 효과도 있어요. 15분보다 짧은 스케줄은 거부되는데, 자정 직전에 세션을 시작하는 사람이라면 누구나 여기에 걸려요.

‘지금’을 포함하는 스케줄은 바로 시작한다

관찰 반대의 경우도 있어요. 00:00–23:59:59 스케줄은 현재 시각을 포함하니까, 다음 자정이 아니라 지금 시작해요. 내일로 예약하는 방법을 두 가지 찾았어요.

관찰 특정 날짜를 노리려고 스케줄에 연/월/일 구성 요소를 전부 넣지 마세요. 그렇게 했더니 익스텐션 콜백이 불안정해졌는데, Apple Developer Forums의 보고(스레드 729841)와도 일치해요. 시/분/초만 쓰세요.

주의할 점: 이런 자정 관련 동작은 실제 기기에서, 밤을 넘겨야만 드러나요. 대비책은 만들어 뒀지만 실제 자정을 제가 원하는 만큼 많이 계측해 보지는 못했어요. 그러니 이 섹션은 명세가 아니라 ‘제가 본 것’으로 받아들여 주세요.

7. stopMonitoring이 intervalDidEnd를 호출한다

관찰 stopMonitoring을 호출하거나, 이미 실행 중인 모니터 이름으로 startMonitoring을 호출하면, iOS는 스케줄이 자연스럽게 끝난 것처럼 익스텐션의 intervalDidEnd를 호출해요. intervalDidEnd가 안전장치로 실드를 다시 씌운다면(제 앱은 그래요. 모니터가 만료되면서 앱이 풀린 채로 남지 않게요), 앱이 모니터를 멈출 때마다 익스텐션이 잠깐 모든 걸 실드로 막아 버려요.

한 사용자가 증상을 제보했어요. HonorFit을 열 때마다 Spotify가 일시 정지된다는 거였어요. 모니터를 멈추자 익스텐션이 선택 항목에 잠깐 실드를 씌웠고, 그 때문에 Spotify가 중단된 뒤에야 앱이 실드를 다시 걷어 낸 거예요.

해결책은 두 가지이고, 둘 다 필요해요. 첫째, 멈추기 전에 익스텐션의 액션을 지워서 실행할 게 없게 하세요.

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

둘째, 모든 intervalDidEnd와 intervalDidStart 액션에 스케줄의 실제 경계에서 1분 여유를 뺀 ‘이 시각 전에는 실행 안 함’ 타임스탬프를 붙이세요. 익스텐션은 무엇이든 실행하기 전에 이걸 확인해요.

// 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
}

순수 Swift라면: 진짜 종료 시각을 App Group에 저장해 두고, intervalDidEnd가 너무 일찍 호출되면 그냥 빠져나오게 하세요.

8. iOS 26 유령 임계값

iOS 26.x에서는 eventDidReachThreshold가 실제 사용이 전혀 없는데도 발생할 때가 있어요. 스케줄을 건 직후, 또는 한동안 잠겨 있던 폰을 집어 들거나 충전기에 꽂을 때요. Apple도 개발자 포럼 스레드(809410, 811305, 814559)에서 이걸 인정했어요. 26.5 베타에서 고쳐졌다는 말이 있지만, 그 뒤로도 계속 보고가 올라왔어요. 제 앱에서는 폰이 주머니에 있었을 뿐인데 ‘시간이 다 됐어요’가 뜨는 증상이었어요. 익스텐션이 실드를 다시 씌우고 잔여 시간이 0까지 줄어든 거죠.

이벤트가 발생하는 건 막을 수 없지만, 믿지 않을 수는 있어요. 진짜 임계값에 관한 두 가지 사실이 대부분의 일을 해 줘요.

  1. 사용 시간은 실제 시계보다 빨리 갈 수 없어요. used-7은 모니터를 건 지 7분이 안 됐는데 정상적으로 발생할 수 없어요. 모든 임계값 액션에 neverTriggerBefore = armedAt + N minutes − 5s를 붙여서, 익스텐션이 불가능할 만큼 이른 발생을 건너뛰게 해요.
  2. 진짜 사용은 조금씩 쌓이고, 유령은 한꺼번에 몰려와요. 앱이 발생한 이벤트를 다시 읽을 때, 가장 낮은 임계값과 가장 높은 임계값이 약 3초 안에 발생했는데 그 사이가 1분 이상의 사용 시간에 걸쳐 있다면 유령으로 처리해요.
// 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
}

가드 2는 익스텐션이 아니라 앱에서 돌기 때문에, 유령 이벤트가 실드를 올리는 건 여전히 가능해요. 그래도 잔여 시간은 살아남고, 다음에 앱을 열면 실드가 다시 걷혀요. 익스텐션 안에서 몰림 감지를 하려면 커스텀 Swift가 필요한데, 아직 작성하지 않았어요.

관찰 반대 방향의 관련 문제도 있어요. 발생해야 할 임계값이 발생하지 않는 거예요. iOS 26에서 앱을 들락날락하며 쓰면, 새 모니터의 첫 15초 임계값은 제때 발생하는데 그 뒤의 것들은 하나로 합쳐지거나 아예 누락되는 걸 봤어요(used-0.25는 발생했는데 used-0.5부터는 끝내 발생하지 않았어요). 제 우회책은 이미 사용 시간을 잰 모니터는 낡은 것으로 보고, 앱이 백그라운드로 갈 때마다 남은 잔여 시간에 대해 새 모니터를 다시 거는 거예요. 재시작할 때마다 최대 15초의 사용 시간을 봐주게 되는데, 저는 그 정도는 괜찮아요.

9. 실드는 앱을 열 수 없다

누구나 떠올릴 설계는 ‘시간 더 얻기’ 버튼이 있는 실드에서 그 버튼을 누르면 앱이 열리는 거예요. 그런데 공식적으로 지원되는 방법이 없어요. Apple DTS는 실드 익스텐션이 부모 앱을 실행할 API는 없다고 말해요. ShieldActionDelegate는 버튼 누름에 ShieldActionResponse(.none, .close, .defer)로 응답하는데, 그중 어느 것도 뭔가를 열지 않아요.

관찰 흔히 찾을 수 있는 우회법(ShieldAction 익스텐션에서 NSExtensionContext().open(url) 호출, 라이브러리에서는 openUrl로 노출)은 2026년 9월에 실제 기기의 최신 iOS에서 테스트했을 때 아무 효과가 없었어요. 버튼을 누르면 실드가 닫힐 뿐이었어요. 버튼을 누를 때 알림을 보내는 방법도 제 경우엔 불안정했어요.

대신 제가 실제로 쓰는 방법: 실드에는 ‘닫기’ 버튼 하나만 두고, 실드가 표시될 때마다 ShieldConfiguration 익스텐션이 로컬 알림을 보내요. 사용자가 그 배너를 누르면 앱으로 들어가요. 앱은 알림 페이로드를 미리 App Group에 써 두고, 포그라운드로 올라오면 알림을 지워요. iOS는 한 번 표시할 때 실드 설정을 여러 번 요청할 수 있어서, 알림 전송에는 디바운스를 걸어요.

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
}

꼼수이고, 사용자가 알림을 허용해야만 통하지만, 제 경우 안정적으로 앱으로 들어갈 수 있었던 유일한 길이에요.

10. prebuild에서 커스텀 익스텐션 지키기

라이브러리 Expo를 쓴다면 expo prebuild가 react-native-device-activity 설정 플러그인을 실행하는데, 이 플러그인은 기본적으로 실행할 때마다 라이브러리의 기본 익스텐션 소스를 ./targets에 복사해요. 제가 수정한 ShieldConfiguration이 소리 없이 덮어써졌어요. 해결책은 플러그인 옵션 하나예요.

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

대가는 이거예요. 라이브러리를 업그레이드할 때 라이브러리의 targets/와 공유 Swift 코드를 내 것과 손으로 diff해서 변경 사항을 다시 적용해야 해요.

11. Xcode 26, 시뮬레이터, 그리고 테스트

12. 그리고 Android에서는

비교하자면, Android에는 공식적인 대응 기능이 없어서 직접 만들어야 해요. HonorFit의 Android 차단 기능은 포그라운드 서비스(앱 차단은 나열된 유형 어디에도 맞지 않아서 specialUse 유형)로, 화면이 켜져 있는 동안 1초에 한 번 UsageStatsManager를 폴링해서 어떤 앱이 앞에 있는지 알아내요. 차단한 앱이 떠 있고 잔여 시간이 0이면, 그 위에 TYPE_APPLICATION_OVERLAY 잠금 화면을 그려요(‘다른 앱 위에 표시’). 부팅 리시버와 워치독 알람이 재부팅, 앱 업데이트, 그리고 공격적인 제조사 배터리 관리 기능 이후에 서비스를 다시 살려요.

트레이드오프는 iOS와 정반대 방향이에요. 실제 패키지 이름이 보이니, 개인정보 보호는 OS가 강제하는 게 아니라 개발자가 지키는 약속이에요. 차단한 앱이 최대 폴링 간격 한 번만큼은 보일 수 있고, 오버레이 아래에서 계속 실행돼요. 그리고 포그라운드 서비스 유형, 오버레이 권한, 패키지 공개 상태에 대해 Play Console 신고서를 작성해야 해요. 심사가 가장 까다로운 AccessibilityService는 일부러 피했어요.

13. 체크리스트

증상유력한 원인해결법
타깃 하나만 프로비저닝 실패그 번들 ID에 엔타이틀먼트나 App Group이 빠짐모든 번들 ID에 대해 Family Controls (Distribution)를 신청하고 App Group을 붙이기
임계값 이벤트가 한 번도 안 옴intervalStart = 지금이라서 모니터가 내일로 예약됨시작 시각을 앞당기기. 구간은 15분 이상 유지
내일용 모니터가 오늘 실행됨스케줄이 현재 시각을 포함함오늘 이미 끝난 [00:00, T) 스케줄, 또는 가드를 건 액션과 repeats: true
앱을 열 때 다른 앱에 실드가 잠깐 씌워짐stopMonitoring이 intervalDidEnd를 호출함멈추기 전에 액션을 지우기. ‘이 시각 전에는 실행 안 함’ 가드 걸기
폰을 놔뒀을 뿐인데 ‘시간이 다 됐어요’iOS 26 유령 임계값실제 시계 기준 가드와 몰림 필터
뒤쪽 임계값이 끝내 안 옴들락날락 사용 시 누락되거나 합쳐짐백그라운드로 갈 때마다 새 모니터를 다시 걸기
실드 버튼으로 앱이 안 열림지원되는 API 없음표시될 때 ShieldConfiguration에서 알림 보내기
발생한 이벤트가 일치하지 않음(RN)이벤트 이름의 밑줄used_7 말고 used-7 쓰기
선택기가 빈 상태로 열림(RN)includeEntireCategory prop 누락값은 아무거나, prop을 넘기기

14. 짧은 답변

익스텐션마다 Family Controls 엔타이틀먼트가 필요한가요?

네. 번들 ID 단위로 승인돼요. 앱 본체에 DeviceActivityMonitor, ShieldConfiguration, ShieldAction 익스텐션까지 하면 승인 네 건이고, 각 App ID에는 공유 App Group도 필요해요.

DeviceActivity의 eventDidReachThreshold가 왜 발생하지 않나요?

제 경우엔 대개 ‘지금’ 시작하는 반복 없는 스케줄이 원인이었고, 그런 스케줄은 내일로 예약돼요. intervalStart를 앞당기세요. iOS 26에서 앱을 들락날락하며 쓸 때는 1분 미만 단위의 뒤쪽 임계값도 누락됐는데, 새 모니터를 다시 걸어 주니 나아졌어요.

ShieldAction이나 ShieldConfiguration 익스텐션에서 부모 앱을 열 수 있나요?

지원되는 API로는 안 되고, 제가 테스트한 최신 iOS에서는 NSExtensionContext 우회법도 아무 효과가 없었어요. 제가 실제로 쓰는 건 실드가 표시될 때 ShieldConfiguration에서 보내는 로컬 알림이에요.

iOS 시뮬레이터에서 Screen Time API를 테스트할 수 있나요?

일부만요. iOS 26 시뮬레이터에서 권한 요청 창과 선택기는 뜨지만, 권한 허용은 끝나지 않았고, 선택기에는 보여 줄 게 없었고, 모니터 익스텐션은 한 번도 실행되지 않았어요. 실제 기기를 쓰세요.

15. 이 모든 게 필요했던 이유

저는 팔굽혀펴기를 해서 스크린타임을 버는 앱 차단 앱 HonorFit을 만들었어요. 양심 시스템으로 동작해요. 횟수는 직접 입력하고, 카메라도 AI도 네트워크 호출도 없어요. 위에 적은 건 전부 ‘번 시간은 차단한 앱이 화면에 떠 있을 때만 줄어든다’를 iPhone에서 동작하게 만드는 데 필요했던 것들이에요. 실제로 돌아가는 모습을 보고 싶다면 App Store와 Google Play에 있어요. 이런 앱을 만들고 싶어서가 아니라 쓰고 싶어서 오셨다면, 팔굽혀펴기 앱 차단 앱이 어떻게 동작하는지와 경쟁 앱까지 포함한 현재 나와 있는 앱들의 솔직한 비교도 정리해 뒀어요. 여기 없는 스크린 타임 동작을 겪으셨거나, 위 내용 중 최신 iOS에서는 틀린 게 있다면 꼭 알려 주세요.

계속 읽기