iyOmSd/Title: Swift

[Swift] Screen Time API(FamilyControls, ManagedSettings)

냄수 2025. 9. 26. 22:55
반응형

안녕하세요 

오늘은 Screen Time API중 Family Controls과 ManagedSettings에 대해서 알아보려고합니다.

 

 

문서에 써잇는 간단한 소개글을 보면 아래와 같아요

 

Managed Setting 에서 보호자는 앱을 통해 계정 잠금, 웹 트래픽 필터링, 미디어 접근 제한 등의 작업을 수행할 수 있습니다.

자녀가 기기에서 앱을 실행할 이유가 전혀 없을 수 있으므로, 앱을 실행하지 않고도 기기에서 코드를 실행할 수 있도록 기기 Device Activity 프레임워크를 활용할 수 있습니다.

개인정보 보호를 위해 Family Control 프레임워크는 가족 공유를 통해 앱을 승인하기 위해 보호자의 동의를 요구합니다.

족 공유를 통한 보호자 승인을 요구함으로써, 단일 가족 공유 그룹 외부에서의 무단 정보 접근으로부터 사용자를 보호합니다.

 

Managed Setting - 계정잠금, 트래픽 필터링, 앱차단 등 기기사용을 보다 세밀하게 제어가능

Family Control - 승인 접근, 앱, 웹 활동제한을 위한 토큰 제공

Device Activity - 앱이 실행되지않아도 코드를 실행시킬 수 있는 방법 제공, 모니터링

 


간단하게 FamilyControl 프레임워크에서 앱 카테고리 토큰을 받아서 

선택한 카테고리 앱들의 실행을 제한하는 간단한 샘플 앱을 만들어보겠습니다

 


구현시 주의사항으로

실제 디바이스 비번 설정이 안되있다면 

authenticationMethodUnavailable 에러를 만나볼 수 있습니다. 

(스크린타임 개발 디버깅 불가능)

 

해결방안으로는 아래와같이 진행하면 됩니다.

1. 설정 > Face ID 및 패스코드 - 패스코드 설정 또는 변경 - Face ID/Touch ID 재설정

2. 설정 > 스크린 타임 - 스크린 타임 켜기 - 스크린 타임 패스코드 설정 - 앱 및 웹사이트 활동 허용

 

Family Controls

FamilyControl사용을 위해서는 

Capability에서 Family Control 을 먼저 추가한뒤

코드적으로도 사용자 승인을 받아야합니다

    let authCenter = AuthorizationCenter.shared

    func checkAuth() {
        Task {
            do {
                try await authCenter.requestAuthorization(for: .individual)
            } catch {
                print("Fail: \(error)")
            }
        }
    }

 

 

FamilyActivitySelection

.familyActivityPicker 모디파이어를 사용하여 기기내의 앱 리스트를 보여줍니다.

차단할 카테고리 세부앱들은 시뮬레이터에서는 확인이 불가능하여 카테고리 전체만 차단가능하고

실기기에선 각 앱별로 선택하여 차단할 수도 있습니다.

import FamilyControls
                
struct SomeView: View {
    @State var selection = FamilyActivitySelection()
    @State var isPresented = false
    
    var body: some View {
        SomeView2()
            .familyActivityPicker(isPresented: $isPresented, selection: $selection)
            .onChange(of: selection) { old, new in
                let applications = new.applications
                let categories = new.categories
                let webDomains = new.webDomains
                print(applications, categories, webDomains)
            }
    }
}

 

사용자의 개인 정보를 보호하기 위해 FamilyActivitySelection은 사용자가 선택한 범주, 애플리케이션 및 웹 도메인을 나타내는 불투명한 값을 보유합니다.

그런 다음 이러한 불투명한 값을 ManagedSettings 및 DeviceActivity 프레임워크의 인스턴스와 메서드에 전달하여 보호 기능을 설정하고 관리할 수 있습니다.

 

extension에서 사용할 것이라면 appGroup을 이용해서 이값을 저장하고 읽어오면 됩니다.

UserDefault에 타입을 바로 저장할수는 없으니 Data형식으로 인코딩해서 저장하고 디코딩해서 읽는 방식을 사용합니다.

인코딩 디코딩 하는 예시코드

struct AppModel: Codable {
    init(selection: FamilyActivitySelection) {
        self.selection = selection
    }
    
    let selection: FamilyActivitySelection
}

func setShieldRestrictions() {
    let applications = selectionToDiscourage
    
    do {
        let model = AppModel(selection: applications)
        let appData = try JSONEncoder().encode(model)
        userDefaults.set(appData, forKey: "testKey")
    } catch {
        print(error)
    }
}
    

func loadSelectedApps() -> FamilyActivitySelection? {
    let userDefaults = UserDefaults(suiteName: "group.test.familyControl")
    guard let data = userDefaults?.data(forKey: "testKey") else {
        return nil
    }
    
    do {
        let model = try JSONDecoder().decode(AppModel.self, from: data)
        print(model)
        return model.selection
    } catch {
        print("앱 목록 로드 실패: \(error)")
        return nil
    }
}

 

 

Managed Settings

ManagedSettingsStore

현재 사용자 또는 장치에 설정을 적용하는 데이터 저장소.

관리 설정 데이터 저장소는 기능을 기준으로 설정을 그룹화합니다.

각 그룹은 관련 설정(예: 기본값, 가능한 최소값 및 최대값)에 대한 관련 데이터를 포함합니다.

FamilyActivitySelection타입으로 선택한값을 ManagedSettingsStore에 전달합니다

import ManagedSettings

@Observable
final class ManagedModel {
    private let store = ManagedSettingsStore()
    var selectionToDiscourage: FamilyActivitySelection = .init()
    
    func setShieldRestrictions() {
        let applications = selectionToDiscourage
        
        store.shield.applications = applications.applicationTokens.isEmpty ? nil : applications.applicationTokens
        store.shield.applicationCategories = applications.categoryTokens.isEmpty
        ? nil
        : .specific(applications.categoryTokens)
    }
}

 

store.shield(ShieldSettings타입) 을통해서 앱이나 웹사이트를 제한 할 수 있습니다.

 

Shield

앱이 차단되는 화면을 커스텀하고싶으면 addTarget에서 아래중 원하는것을 선택하면됩니다.

Shield Configuration - ShieldConfigurationDataSource

차단시 UI설정

 

    override func configuration(shielding application: Application) -> ShieldConfiguration {
        // Customize the shield as needed for applications.
        ShieldConfiguration(
            backgroundBlurStyle: .dark,
            backgroundColor: .black,
            icon: nil,
            title: .init(text: "앱 차단⚠️!!", color: .systemPink),
            subtitle: .init(text: "서브타이틀임", color: .systemPink),
            primaryButtonLabel: .init(text: "primary버튼", color: .systemPink),
            primaryButtonBackgroundColor: .black,
            secondaryButtonLabel: .init(text: "secondary버튼", color: .systemPink)
        )
    }
    
    override func configuration(shielding application: Application, in category: ActivityCategory) -> ShieldConfiguration {
        // Customize the shield as needed for applications shielded because of their category.
        ShieldConfiguration(
            backgroundBlurStyle: .dark,
            backgroundColor: .black,
            icon: nil,
            title: .init(text: "카테고리 차단⚠️!!", color: .systemPink),
            subtitle: .init(text: "서브타이틀임\(category.localizedDisplayName)", color: .systemPink),
            primaryButtonLabel: .init(text: "primary버튼", color: .systemPink),
            primaryButtonBackgroundColor: .black,
            secondaryButtonLabel: .init(text: "secondary버튼", color: .systemPink)
        )
    }

 

CGV앱을 제한걸고(시뮬에서 테스트불가능해서 실제폰에서 테스트) CGV앱을 실행시키면 앱 차단이 노출됩니다.

이와 다르게 생산성 및 금융이라는 카테고리를 제한시키고 앱을 실행시키면 카테고리 차단이 노출됩니다.

카테고리가 더 상위개념으로 카테고리를 차단하면 앱차단보다 우선순위로 노출됩니다.

 

 

Shield Action Extension - ShieldActionDelegate

차단시 각 버튼에 따른 이벤트 처리

    override func handle(action: ShieldAction, for application: ApplicationToken, completionHandler: @escaping (ShieldActionResponse) -> Void) {
        // Handle the action as needed.
        switch action {
        case .primaryButtonPressed:
            completionHandler(.close)
        case .secondaryButtonPressed:
            completionHandler(.defer)
        @unknown default:
            fatalError()
        }
    }

 

defer은 버튼을 눌러도 아무동작이 없습니다.  참고하세요

 

반응형