[2024년 Screen Time API] - (1) Family Controls

김영준 ·2024년 3월 12일
0

이번에 디지털 디톡싱을 도와주는 스크린 타임 앱을 개발하게 되었다. 익숙한 Flutter로 개발을 하고 싶었으나, 스크린 타임 패키지가 개발 된게 없었고 안드로이드와 애플의 스크린 타임 기능의 구현 방식이 달랐다.

스크린 타임 기능을 위해 네이티브 개발이 필수였고 안드로이드보다 모든 면에서 깐깐한 IOS앱을 먼저 개발하기로 했다.

이 시리즈를 통해 IOS의 Screen Time API (Family Controls, Managed Settings, Device Activity)를 활용하여 앱을 개발하면서 삽질했던 기록을 남긴다. 이 시리즈가 스크린 타임 앱을 개발하는 모든 분들께 도움이 되었으면 좋겠다.

Screen Time에 어떤 기능이 있는지 다음의 공식 영상을 통해 얼추 찾아볼 수 있다

https://developer.apple.com/videos/play/wwdc2021/10123/
https://developer.apple.com/videos/play/wwdc2022/110336/

Screen Time

우선 Screen Time이 정확히 무엇인지 짚고 넘어가겠다. 당신의 아이폰에 들어가 설정 에 들어가보면 "스크린 타임" 탭을 찾아볼 수 있을 것이다.

"스크린 타임"탭에 들어가보면 본인이 휴대폰을 사용한 시간의 그래프와 아래 사용시간 제한 섹션을 찾아볼 수 있을 것이다.

해당 시리즈에서는 "스크린타임"의 앱 사용 제한, 앱 시간 제한, 다운 타임, 각 어플 사용 시간 확인 하는 법을 것이다

Screen Time API 는 "스크린 타임"의 기능을 나의 앱에서 구현 가능할 수 있도록 만든다! 여기서 주의할 점은 사실 "Screen Time API"라는 것은 없다. 다음 3개의 다른 이름의 API로 Screen Time 기능을구현 하는 것이다!

🔴 넘어가기 전 필독 사항

이 글을 읽는 당신이 스크린 타임 앱을 개발해 배포할 예정이라면 먼저 선행되어야 할 것이 있다.

Family Controls, Managed Settings, Device Activities는 사용자의 앱을 제한 시켜버릴 수 있고 보안 이슈가 발생할 수 있기 때문에 앱 배포 전에 Entitlement 승인을 받아야한다.

쉽게 말해 ScreenTime API를 사용하여 개발 할 수 있는 권한을 애플로부터 받아야한다.

이는 앱스토어 심사와는 다른 영역이며 짧으면 몇주, 길면 몇달이 걸린다고 알려져있다.

https://developer.apple.com/contact/request/family-controls-distribution

위의 링크에 개발자 계정 "어드민"으로 로그인하여 신청할 수 있다. 어떤 앱을 개발할 것인지 설명과 애플의 정책에 동의하는지 체크해주면 된다. 따로 접수되었다는 이메일이 오지는 않는다.

승인을 받기 전 Developement Entitlement로 개발은 가능하지만 Test Flight 또는 App Store 배포는 불가능하다. 승인까지 오래걸리기 때문에 미리 신청해두는 것을 추천한다!

추가적으로 유저가 Family Controls 권한을 부여하기 위해서는 아이폰에 FaceID나 비밀번호로 보안이 걸려있어야한다. 보안이 걸리지 않은 아이폰으로는 권한을 부여하고 부여받을 수 없으므로 유저에게 아이폰에 보안을 걸도록 알려줘야한다!

Family Controls의 기능

Family Controls API로 제한할 앱을 선택할 수 있다. 선택된 앱은 Managed Settings API에서 제한 할 수 있다. 제한 된 앱은 홈화면에서 어두워진 아이콘 + 앱 이름 왼쪽에 모래시계가 나타나며, 제한 앱을 열려고 하면 제한 앱 쉴드가 나타나게 된다.

앱 선택 (Family Controls)앱 제한 (Managed Settings )제한 앱 쉴드

제한 앱 쉴드도 어느정도 커스터마이징이 가능하다. 하지만 애플이 개발자에게 준 틀 안에서만 커스터마이징이 가능하다

위의 화면 중 커스터마이징이 가능한 것:

  • Material 색깔
  • Icon Asset (크기 조절 불가)
  • Title 텍스트와 글자색 (폰트 조절 불가)
  • Body 텍스트와 글자색 (폰트 조절 불가)
  • Button 1 바탕색, 텍스트, 글자색 (폰트, 패딩 조절 불가)
  • Button 2 텍스트, 글자색 (폰트, 패딩 조절 불가)

커스터마이징 방법은 추후 4번째 글에서 다룰 예정이다

Family Controls 실습

이제 Screen Time 과 Family Controls의 주요기능을 어느정도 알았으니 실제로 구현해보겠다

1️⃣ XCode 프로젝트 생성

XCode -> Create New Project -> App을 클릭해 "AppLimiter" 라는 프로젝트를 생성하겠다. 해당 실습에서는 XCode 15.2버전, IOS 17.3.1버전의 실제 아이폰 기기를 사용했다.

2️⃣ Signing & Capabilites 설정

Family Controls API를 사용하려면 애플 개발자 계정으로 Signing을 진행하며 실제 아이폰 기기에서 개발해야한다. 시뮬레이터에서는 제한할 앱을 선택할 수가 없기 때문이다.

AppLimiter -> Signing & Capabilities 탭으로 들어간다.

1) 우선 Team을 선택해 개발자 계정 Signing을 완료해준다
2) +Capablity 를 클릭한다

3) Family Controls를 검색하여 엔터키를 눌러 Capability에 추가해준다

3️⃣ 코딩 시작!

우선 폴더 구조는 다음과 같다

XCode에서 기본적으로 AppLimiter 프로젝트를 생성하면 AppLimiterApp.swiftContentView.swift 파일이 있다. ContentView.swift 파일을 삭제했으며 Views/Home.swiftViewModels/FamilyViewModel.swift 파일을 추가하였다.

// AppLimiterApp.swift

import SwiftUI
import FamilyControls

@main
struct AppLimiterApp: App {
    // 자식에게 전달해줄 FamlyViewModel StateObject 선언 
    @StateObject var familyViewModel = FamilyViewModel()
    
    var body: some Scene {
        WindowGroup {
            Home()
                .environmentObject(familyViewModel) // familyViewModel을 모든 자식 트리에 전달 t
                .onAppear(){
                    // 앱이 실행 되자마자 Family Controls 권한을 묻는다
                    // 원하는 곳에서 아래의 Task를 복붙하여 Family Controls 권한을 물을 수 있다
                    Task {
                        do {
                            try await familyViewModel.familyCenter.requestAuthorization(for: .individual)
                            // Family Controls 권한 허용 성공
                            // ...
                        } catch {
                            print("Failed request: \(error)")
                            // Family Controls 권한 허용 실패
                            // ...
                        }
                       
                    }
                }
        }
    }
}
// ViewModels/FamilyViewModel.swift

import Foundation
import FamilyControls

class FamilyViewModel: ObservableObject {
    
    // Family Controls 권한 센터
    let familyCenter = AuthorizationCenter.shared
    
    // 선택된 앱 목록
    @Published var appSelection = FamilyActivitySelection(includeEntireCategory: true)
    

}
// Views/Home.swift

import SwiftUI
import FamilyControls

struct Home: View {
    @EnvironmentObject var familyViewModel: FamilyViewModel
    @State var showPicker = false
    
    var body: some View {
        VStack{
            Text("선택 된 앱 개수 \(familyViewModel.appSelection.applicationTokens.count)")
            Text("선택 된 카테고리 개수 \(familyViewModel.appSelection.categoryTokens.count)")
            
            if (familyViewModel.appSelection.applicationTokens.count == 0) {
                Spacer()
                Text("선택 된 앱이 없습니다")
                Spacer()
            } else {
                List{
                    ForEach(Array(familyViewModel.appSelection.applicationTokens), id: \.self) { applicationToken in
                        ZStack (alignment: Alignment(horizontal: .leading, vertical: .center)){
                            HStack{
                                Label(applicationToken)
                                    .scaleEffect(CGSize(width: 0.95, height: 0.95))
                                Spacer()
                            }
                            Label(applicationToken)
                                .labelStyle(.iconOnly)
                                .scaleEffect(CGSize(width: 1.6, height: 1.6))
                        }
                        .frame(height: 50)
                        .listRowSeparator(.hidden)
                        .listRowInsets(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16))
                    }
                }
                 .listStyle(.plain)
                 .listStyle(.grouped)
            }
            
            // FamilyActivityPicker(selection: $familyViewModel.appSelection)
            
            Button (action: {
                showPicker = true
            }) {
                Text("앱 선택하기")
            }.familyActivityPicker(isPresented: $showPicker, selection: $familyViewModel.appSelection)
        }
    }
}

#Preview {
    @StateObject var familyViewModel = FamilyViewModel()
    return Home().environmentObject(familyViewModel)
}

위와 같이 코드를 작성하고 실행 시켜보면 AppLimiterApp.swift에서 Home().onAppear() 내의 Task에 의해 스크린 타임 권한을 묻는다. 스크린 타인 권한 부여하기 위해서는 아이폰이 반드시 FaceID나 비밀번호로 보안이 걸려있어야한다.

스크린 타임 권한 요청FaceID로 권한 승인권한 승인 완료

권한이 완료 되면 다음과 같은 화면이 뜰 것이다.

맨 아래에 있는 "앱 선택하기" 버튼을 눌러보면 "활동 선택" 팝업이 뜰 것이다. 원하는 앱 또는 카테고리르 선택하고 완료하면 홈화면에 선택된 앱이 뜰 것이다!

활동 선택 팝업"소셜 미디어" 카테고리 선택 + "TV" 앱 선택앱 선택 완료

4️⃣ 코드 설명 & 커스텀 디자인

👉 Family Activity Selection

우선 ViewModels/FamilyViewModel.swift를 살펴보자

@Published var appSelection = FamilyActivitySelection(includeEntireCategory: true) 에 includeEntireCategory 가 true 로 설정 되어 들어간다. 다음과 같이 수정하고 앱을 다시 실행 시켜보자

@Published var appSelection = FamilyActivitySelection()

앱을 다시 실행 시킨 후 "앱 선택하기 버튼"을 눌러 똑같이 "소셜 미디어" 카테고리와 "TV"앱을 선택하고 완료해보자.
홈화면에 선택된 앱 개수는 1, 선택된 카테고리 개수는 1, 그리고 애플 TV만 선택된 앱으로 뜰 것이다.

FamilyActivitySelection()을 호출하면 기본적으로 includeEntireCategory에 false가 적용된다. 이는 category selection과 application selection을 완전 구분하게 되서 그렇다.

👉 Family Activity Picker

Views/Home.swift를 살펴보자

// FamilyActivityPicker(selection: $familyViewModel.appSelection) 줄이 주석 처리 되어있는 것을 확인할 수 있다. 해당 줄의 주석 처리를 제거 하고 바로 아래 있는 "앱 선택하기" 버튼 4줄을 주석 처리하고 앱을 실행하면!

(버튼과 팝업) 형태가 아닌 페이지에 View로 바로 띄울 수 있게 된다

디자인
어떤 형태의 Family Activiy Picker이든 디자인은 커스터마이징 할 수 없다. 그나마 headerText, footerText를 추가할 수 있으며 라이트 모드, 다크 모드로 변경 할 수 있다

FamilyActivityPicker(headerText: "헤더", footerText: "푸터", selection: $familyViewModel.appSelection)
	.environment(\.colorScheme, .light)
라이트 모드다크 모드

다크모드는 배경색이 검은색이지만 라이트 모드는 배경색이 옅은 회색이라 거슬린다. 라운드 처리와 패딩을 추가해주면 그나마 나아보인다.

👉 Label

선택된 앱은 Label에 선택된 앱의 applicationToken을 넣어주면 아이콘과 앱 이름이 뜨게 된다. 다른 Image 나 Text같은 뷰로는 표시할 수 없다. 이는 애플이 개인정보를 매우 중요하게 여겨 개발자가 유저가 선택한 앱을 수집할 수 없도록 하기 위한 조치이다.

Label은 List에 최적화 되어있다는 점이다. List 내에서 볼 때는 생각보다 괜찮지만 List밖으로 빼거나 디자인을 바꾸고 싶으면 생각보다 아이콘이 작아보인다. 문제는 applicationToken을 적용한 label은 디자인 변화가 안먹힌다는 점이다. 폰트, 아이콘 사이즈를 조정할 수가 없다. 이유는 왜인지 잘 모르겠다. 그나마 scaleEffect가 먹히기 때문에 이와 ZStack을 활용해 크기를 조정 할 수 있다.

기본적인 List 내의 LabelList의 배경, 패딩, Seperator 제거 후 Label 크기 조정

좌측 UI는 아래의 코드이며 우측 UI는 Home.swift에 List로 감싸져있는 코드다

List{
    ForEach(Array(familyViewModel.appSelection.applicationTokens), id: \.self) { applicationToken in
        Label(applicationToken)
    }
}

마치는 글

소스 코드는 아래 링크에서 찾아볼 수 있다
https://github.com/youngjun625/screen_time_kr

다음 글 (2) Managed Settings에서 오늘 선택된 앱을 제한하고, 앱 사용 시간을 설정하며, 특정 시간에 앱 제한을 설정하고 푸는 법을 다루도록 하겠다!

profile
창업과 개발

0개의 댓글

관련 채용 정보