iyOmSd/Title: Swift

[Swift] Screen Time API(DeviceActivity)

냄수 2025. 10. 27. 22:08
반응형

앞선 게시물에서 정리하지않은 Screen Time관련 기능중 

DeviceActivity를 정리하려합니다.

 

1. DeviceActivity를 사용해서 특정시간에 코드를 실행시키는 방법과

2. 기기 사용시간을 모니터링 하는 방법을 알아볼거에요

3. 마지막으로는 사용시간 데이터로 시각화를 위해 차트화 하는 방법까지 해볼예정입니다.

 

DeviceActivity란

앱이 실행되지않아도 코드를 실행 할 수 있는 방법을 제공

사용자 기기를 모니터링하고 조건 충족시 자동으로 코드실행 가능

- 특정 시간대에 코드실행 가능

- 특정 앱을 일정 시간 이상 사용시 이벤트 발생하여 코드실행 가능

 

 

Device Activity Monitor Extension과 Device Activity Report Extension으로 구분됩니다.

 

먼저 

Device Activity Monitor Extension

사용자 기기사용을 모니터링 합니다.

특정 조건시 이벤트를 트리거합니다.

File -> New -> Target에서

추가할 수 있습니다.

 

DeviceActivityMonitor를 준수해서

구현할 수 있는 함수는 아래와 같습니다.

/// 활동 시작전에 startMonitoring의 warningTime분 전에 알림 (시작 n분전, 종료 n분전)
override func intervalWillStartWarning(for activity: DeviceActivityName)
override func intervalWillEndWarning(for activity: DeviceActivityName)

/// 장치 활동 간격이 시작 (startMonitoring의 intervalStart 시간에 해당시 호출됨)
override func intervalDidStart(for activity: DeviceActivityName) 
/// 장치 활동 간격이 끝 (startMonitoring의 intervalEnd 시간에 해당시 호출됨)
override func intervalDidEnd(for activity: DeviceActivityName)

/// 활동이 지정한 임계값에 도달할 예정일때 호출됨
override func eventWillReachThresholdWarning(_ event: DeviceActivityEvent.Name, activity: DeviceActivityName)
/// 활동이 지정한 임계값에 도달하면 호출됨
override func eventDidReachThreshold(_ event: DeviceActivityEvent.Name, activity: DeviceActivityName)

 

 

위의 함수를 실행시키기위해선 DeviceActivityCenter 인스턴스를 사용합니다.

deviceActivityCenter.startMonitoring를 호출해서 모니터링을 등록합니다

let deviceActivityCenter = DeviceActivityCenter()
// 이벤트 이름에 맞는 이벤트 생성
private let events: [DeviceActivityEvent.Name: DeviceActivityEvent] = [
    .encouraged: .init(threshold: DateComponents(minute: 1))
]
var startDate: Date = .now
var startDateComponent: DateComponents { DateComponents(from: startDate) }
var endDate: Date = .now
var endDateComponent: DateComponents { DateComponents(from: endDate) }

func startMonitor() {
    do {
        try deviceActivityCenter.startMonitoring(
            .testName,
            during: .init(
                intervalStart: startDateComponent,
                intervalEnd: endDateComponent,
                repeats: true,
                warningTime: .init(minute: 1) // 1분전
            ),
            events: events
        )
        syncActivityList()
        print("설정완료")
    } catch {
        print(error)
    }
}

// MARK: - 이름정의

extension DeviceActivityName {
    static let testName = Self("testName")
}

extension DeviceActivityEvent.Name {
    static let encouraged = Self("encouraged")
}

제가 직접 테스트해본결과 start와 end시간 사이에 최소 15분 이상 필요합니다.

15분내 간격일때 너무 짧다고 에러를 발생시킵니다

 

여기서 주의할점이있는데

intervalStart값을 넣기위해 DateComponents타입생성시

Calendar.current.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date) 이런식으로

년월일시간분초 값이 모두 존재해야 정상적으로 등록되서 이벤트발생합니다

에러도 없고 이벤트도 없어서 디버깅하기 어려웠습니다. ㅠ

 

테스트전엔 반드시 권한을 가져와야합니다.

let authCenter = AuthorizationCenter.shared

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

 

활성화중인 이벤트 리스트를 보는 방법은 아래와 같습니다.

var allActivieString: [String] = []

private func syncActivityList() {
    allActivieString = deviceActivityCenter.activities.map { activity in
        "\(activity.rawValue) " + startToEndTimeString(name: activity)
    }
}

앞 게시글에서 정리한 ManagedSetting을 이용한다면

특정시간에 앱을 차단하는 기능을 구현할 수 있습니다.

class DeviceActivityMonitorExtension: DeviceActivityMonitor {
    let store = ManagedSettingsStore()

    /// 장치 활동 간격이 시작되었음을 나타냅니다
    override func intervalDidStart(for activity: DeviceActivityName) {
        super.intervalDidStart(for: activity)
        
        if let selection = loadSelectedApps() {
            print("앱 목록 로드성공")
            blockSelectedApps(selection)
        }
    }

    private 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)
            return model.selection
        } catch {
            print("앱 목록 로드 실패: \(error)")
            return nil
        }
    }
    
    private func blockSelectedApps(_ selection: FamilyActivitySelection) {
        // 앱 차단 설정
        store.shield.applications = selection.applicationTokens.isEmpty ?
            nil : selection.applicationTokens
        
        // 카테고리 차단 설정
        store.shield.applicationCategories = selection.categoryTokens.isEmpty
        ? nil
        : .specific(selection.categoryTokens)
        
        // 웹 도메인 차단 설정 (필요한 경우)
        store.shield.webDomains = selection.webDomainTokens.isEmpty
        ? nil
        : selection.webDomainTokens
    }
       
}

특정시간이 되면

loadSelectedApps()를 통해서 활동을 선택한 앱목록을 가져와서

blockSelectedApps()함수로 전달해서 선택된 앱들을 차단합니다.

 

 

모니터링 중단은

deviceActivityCenter.stopMonitoring()

를 사용합니다.

 

앱차단 해제는

    func unblockApps() {
        // 모든 차단 해제
        store.shield.applications = nil
        store.shield.applicationCategories = nil
        store.shield.webDomains = nil
    }

nil을 넣어주면 해제됩니다

 

 

이제 앱별 사용량을 체크할 수 있는 기능을 봐볼까요?

Device Activity Report Extension

사용자의 기기 사용 데이터 수집가능

어떤 앱을 얼마나 사용했는지, 특정시간대에 얼마나 사용했는지 

1분주기로 업데이트가능

 

File -> New -> Target에서

이번엔 이 기능을 추가해줍니다.

 

DeviceActivityReport.Context

DeviceActivity 데이터를 기반으로 그릴 View 유형을 보고서에 알려주는 사용자 지정 가능한 유형입니다

 

public extension DeviceActivityReport.Context {
    // If your app initializes a DeviceActivityReport with this context, then the system will use
    // your extension's corresponding DeviceActivityReportScene to render the contents of the
    // report.
    static let totalActivity = Self("Total Activity")
}

생성하면 기본적으로 생성되있습니다.

이 타입은 어떤 DeviceActivityReportScene타입을 부를것인지 선택하는 이름입니다.

 

public protocol DeviceActivityReportScene : AppExtensionScene {

    /// The context of the scene.
    ///
    /// When your app creates a `DeviceActivityReport` with this context, the
    /// system uses this scene to render the report's content.
    var context: DeviceActivityReport.Context { get }
}

 

기본적으로 context를 작성해야하고

여기서 정의한 context가 앱타겟에서 불러올 ReportScene이 됩니다.

struct TotalActivityReport: DeviceActivityReportScene {
    // Define which context your scene will represent.
    // DeviceActivityReport호출시 해당 컨텍스트에 해당하는 Report 불러옴
    let context: DeviceActivityReport.Context = .totalActivity  
    
    // Define the custom configuration and the resulting view for this report.
    let content: ([AppUsage]) -> TotalActivityView

이런식으로 구현합니다.

여러 Report를 구현할 수도 있으므로 각 범위에 따라서 다르게 선택할 수 있습니다.

 

앱타겟에서 사용하는 뷰 예시는 아래와같습니다.

struct DeviceReportView: View {
    var body: some View {
        VStack {
            Text("사용량 통계")
            DeviceActivityReport(context, filter: filter)
        }
    }
}

DeviceActivityReport 뷰타입에 넣어줄 context가 여기에 해당합니다.

 

DeviceActivityResults

필터링된 장치 활동 결과의 비동기 시퀀스

for await in 으로 요소에 접근가능합니다.

 

DeviceActivityData

iOS16+

특정 디바이스에서 특정 사용자의 활동을 나타내는 핵심 데이터 타입

 

주요 속성 (Instance Properties)

 

1. activitySegments: 사용자의 활동을 세그먼트별로 구분한 데이터

2. device: 활동 보고서와 연관된 디바이스 정보

3. lastUpdatedDate: 시스템이 이 디바이스의 데이터를 마지막으로 업데이트한 날짜

4. segmentInterval: 각 활동 세그먼트의 간격

5. user: 활동 보고서와 연관된 사용자 정보

 

주요 하위 구조체들

 

1. DeviceActivityData.ActivitySegment

 

특정 날짜 간격 동안의 사용자 활동을 나타냅니다.

 

주요 속성:

 

- categories: 활동 세그먼트 동안의 카테고리별 디바이스 활동

- dateInterval: 활동 세그먼트의 날짜 간격

- firstPickup: 활동 세그먼트 동안 사용자가 처음 디바이스를 집어든 시간

- longestActivity: 활동 세그먼트 동안 가장 긴 활동 세션의 날짜 간격

- totalActivityDuration: 활동 세그먼트 동안의 총 활동 시간

- totalPickupsWithoutApplicationActivity: 앱을 사용하지 않고 디바이스를 집어든 횟수

 

2. DeviceActivityData.Device

 

활동 데이터를 보고할 디바이스를 나타냅니다.

 

주요 속성:

 

- model: 디바이스 모델 (iPhone, iPad, Mac 등)

- name: 사용자가 설정한 디바이스 이름

 

3. DeviceActivityData.User

 

활동과 연관된 사용자 정보를 나타냅니다.

 

주요 속성:

 

- appleID: 사용자의 Apple ID

- nameComponents: 사용자 이름

- role: 사용자 유형 (가족 역할 등)

 

4. DeviceActivityData.CategoryActivity

 

사용자의 앱 및 웹 도메인 활동을 카테고리별로 표현합니다.

 

주요 속성:

 

- applications: 이 카테고리에 기여한 앱 활동

- category: 활동의 카테고리

- totalActivityDuration: 이 카테고리의 총 활동 시간

- webDomains: 이 카테고리에 기여한 웹 도메인 활동

 

로그 코드를 참고해서 필요한 속성을 골라쓰시면 됩니다.

private func printLog(data: DeviceActivityResults<DeviceActivityData>) async -> [AppUsage] {
    let formatter = DateComponentsFormatter()
    formatter.allowedUnits = [.day, .hour, .minute, .second]
    formatter.unitsStyle = .abbreviated
    formatter.zeroFormattingBehavior = .dropAll
    let thisDevice = UIDevice.current.model
    
    var appUsages: [AppUsage] = []
    for await value in data {
        print("🟣device.name",value.device.name) // 연결된 모든 디바이스 (아이폰, 아이패드) NS, NS iPad
        print("🟣lastUpdatedDate",formatter.string(for: value.lastUpdatedDate))
        print("🟣segmentInterval", value.segmentInterval) // 측정 간격
        print("🟣name", value.user.nameComponents) // 사용자이름 ( given: 남수 family: 김)
        print("🟣user apple id", value.user.appleID) // apple id
        print("🟣user role", value.user.role) // individual
        
        
        for await activity in value.activitySegments {
            print("🔵activity dateInterval", activity.dateInterval) // 측정 간격
            print("🔵activity firstPickup", activity.firstPickup) // 처음 폰든시간 9/21 오후 11시2분2초
            print("🔵activity longestActivity", activity.longestActivity) // 제일긴 활동시간 Date to Date
            print("🔵activity totalActivityDuration", activity.totalActivityDuration) // 3072초
            print("🔵activity totalPickupsWithoutApplicationActivity", activity.totalPickupsWithoutApplicationActivity)
            
            for await category in activity.categories {
                print("🟤category.localizedDisplayName", category.category.localizedDisplayName) // social, Other, 큰카테고리
                print("🟤category.debugDescription", category.category.token.debugDescription) // 128 bytes, 토큰 해독불가
                print("🟤category.totalActivityDuration", category.totalActivityDuration) // 1668초, 690초

                for await application in category.applications {
                    print("🟢app localizedDisplayName", application.application.localizedDisplayName) // instagram, 카카오톡
                    print("🟢app bundleIdentifier", application.application.bundleIdentifier) // com.burbn.instagram. com.iwilab.KakaoTalk
                    print("🟢app numberOfNotifications", application.numberOfNotifications) // 0, 10
                    print("🟢app numberOfPickups", application.numberOfPickups) // 1, 5
                    print("🟢app totalActivityDuration", application.totalActivityDuration) // 1228초, 430초
                    let usage = AppUsage(
                        title: application.application.localizedDisplayName?.lowercased() ?? "",
                        time: Int(application.totalActivityDuration)
                    )
                    appUsages.append(usage)
                }
                
                for await webDomain in category.webDomains {
                    print("⚫️web domain", webDomain.webDomain.domain) // weather.naver.com, naver.com
                    print("⚫️web totalActivityDuration", webDomain.totalActivityDuration) // 5초
                    print("⚫️web debugDescription", webDomain.webDomain.token.debugDescription) // 128bytes
                }
            }
            
        }
    }
    print("------------------")
    return appUsages
}
 🟣device.name Optional("NS")
 🟣lastUpdatedDate nil
 🟣segmentInterval daily(during: 2025-09-21 15:00:00 +0000 to 2025-09-22 15:00:00 +0000)
 🟣name Optional(givenName: 남수 familyName: 김 )
 🟣user apple id Optional("ㅁㅁㅁㅁ@naver.com")
 🟣user role individual
 🔵activity dateInterval 2025-09-21 15:00:00 +0000 to 2025-09-22 15:00:00 +0000
 🔵activity firstPickup Optional(2025-09-21 23:02:02 +0000)
 🔵activity longestActivity Optional(2025-09-22 01:14:31 +0000 to 2025-09-22 01:24:45 +0000)
 🔵activity totalActivityDuration 3072.8046337366104
 🔵activity totalPickupsWithoutApplicationActivity 2
 🟤category.localizedDisplayName Optional("Social")
 🟤category.debugDescription Optional(ActivityCategoryToken(data: 128 bytes: 䍌éî/ÑI©éjMMô
 j7;ª$.èýô`gøcœØ~v.¡‹À0œÕƒ¥
 ¢2¯«û^
 ‡SåRgŸÆ^8£±jóÄ«Âè,‰š·Û­9£:ŃÄäãCV–ª¶ŠÙBçâ6d˜ Å"g’/))
 🟤category.totalActivityDuration 1668.274539232254
 🟢app localizedDisplayName Optional("Instagram")
 🟢app bundleIdentifier Optional("com.burbn.instagram")
 🟢app bundleIdentifier Optional("com.burbn.instagram")
 🟢app numberOfNotifications 0
 🟢app numberOfPickups 1
 🟢app totalActivityDuration 1228.57754611969
 🟢app localizedDisplayName Optional("카카오톡")
 🟢app bundleIdentifier Optional("com.iwilab.KakaoTalk")
 🟢app bundleIdentifier Optional("com.iwilab.KakaoTalk")
 🟢app numberOfNotifications 10
 🟢app numberOfPickups 5
 🟢app totalActivityDuration 430.60449719429016
 🟤category.localizedDisplayName Optional("Productivity & Finance")
 🟤category.debugDescription Optional(ActivityCategoryToken(data: 128 bytes: 䍌éî/ÑI©éjMMô
 j7;ª$.èýô`gøcœØ~v.¡‹À0œÕƒ¥
 ¢2¯«û^
 ‡SåRgŸÆ^8£±jóÄ«Âè,‰š·Û­9£:ŃÄäãCV–ª¶Š*LýìÓ)ûŠZDˆ§9))
 🟤category.totalActivityDuration 417.86487793922424
 🟢app localizedDisplayName Optional("토스")
 🟢app bundleIdentifier Optional("com.vivarepublica.cash")
 🟢app bundleIdentifier Optional("com.vivarepublica.cash")
 🟢app numberOfNotifications 6
 🟢app numberOfPickups 4
 🟢app totalActivityDuration 408.36211693286896
 🟢app localizedDisplayName Optional("모바일출입카드")
 🟢app bundleIdentifier Optional("kr.co.adcaps.mobilecard")
 🟢app bundleIdentifier Optional("kr.co.adcaps.mobilecard")
 🟢app numberOfNotifications 0
 🟢app numberOfPickups 0
 🟢app totalActivityDuration 9.502761006355286
 🟤category.localizedDisplayName Optional("Entertainment")
 🟤category.debugDescription Optional(ActivityCategoryToken(data: 128 bytes: 䍌éî/ÑI©éjMMô
 j7;ª$.èýô`gøcœØ~v.¡‹À0œÕƒ¥¢2¯«û^
 ‡SåRgŸÆ^8£±jóÄ«Âè,‰š·Û­9£:ŃÄäãCV–ª¶Š~²=ë¡ËŽÃAƒ(CÏÝ))
 🟤category.totalActivityDuration 31.47321093082428
 🟢app localizedDisplayName Optional("네이버 웹툰")
 🟢app bundleIdentifier Optional("com.nhncorp.NaverWebtoon")
 🟢app bundleIdentifier Optional("com.nhncorp.NaverWebtoon")
 🟢app numberOfNotifications 0
 🟢app numberOfPickups 0
 🟢app totalActivityDuration 22.481863021850586
 🟢app localizedDisplayName Optional("YouTube")
 🟢app bundleIdentifier Optional("com.google.ios.youtube")
 🟢app bundleIdentifier Optional("com.google.ios.youtube")
 🟢app numberOfNotifications 1
 🟢app numberOfPickups 0
 🟢app totalActivityDuration 2.891474962234497
 ⚫️web domain Optional("weather.naver.com")
 ⚫️web totalActivityDuration 5.285457968711853
 ⚫️web debugDescription Optional(WebDomainToken(data: 128 bytes: 䍌éî/ÑI©éjMMô
 j7;ª$.üë0üb’wߒ“cg&î“Øe½´ÛðJ®û¾è
 P
 q¢±ïw´7ÌÆóÄ«Âè,‰š·Û­9£:ŃÄäãCV–ª¶Š÷ä•]r    &v˜m¼Ò•))
 ⚫️web domain Optional("naver.com")
 ⚫️web totalActivityDuration 0.8144149780273438
 ⚫️web debugDescription Optional(WebDomainToken(data: 128 bytes: 䍌éî/ÑI©éjMMô
 j7;ª$.üë0üb’wߒ“cg&î“Ø|¹£Á»[ï<ð÷¸JŠYë:ÂèòÆ^8£±jóÄ«Âè,‰š·Û­9£:ŃÄäãCV–ª¶Š™0?Á3ã(œýä˜R0))
 🟣device.name Optional("NS iPad")
 🟣lastUpdatedDate nil
 🟣segmentInterval daily(during: 2025-09-21 15:00:00 +0000 to 2025-09-22 15:00:00 +0000)
 🟣name Optional(givenName: 남수 familyName: 김 )
 🟣user apple id Optional("ㅁㅁㅁㅁ@naver.com")
 🟣user role individual

이런식으로 

기기별로, 활동별로, 카테고리별로, 앱, 웹 이런식으로 하위까지 사용량을 확인할 수 있습니다.

 

 

이제 이 사용량을 토대로 차트화 시켜볼까요?

 

차트구현

Scene타입의 content에 사용할 타입을 먼저 정의할겁니다

앱 타이틀과 시간을 저장할 구조체도 같이 정의합니다.

let content: (String) -> TotalActivityView // 기본 생성시 

let content: ([AppUsage]) -> TotalActivityView // << 수정


struct AppUsage {
    let title: String
    let time: Int
}

 

 

이제 앱타겟에서 사용할 뷰를 정의합니다

struct DeviceReportView: View {
    @State private var context: DeviceActivityReport.Context = .totalActivity
    @State private var filter: DeviceActivityFilter = .init(
        segment: .daily(
            during: .init(
                start: .now.addingTimeInterval(-3600),
                end: .now.addingTimeInterval(3600)
            )
        ),
        devices: .all
    )
    var body: some View {
        VStack {
            Text("사용량 통계")
            DeviceActivityReport(context, filter: filter)
        }
    }
}

 

위에서 이미했지만 다시 코드를 불러오겠습니다.

로그부분은 다제거하고 필요한 코드만 작성했습니다.

struct TotalActivityReport: DeviceActivityReportScene {
    // Define which context your scene will represent.
    let context: DeviceActivityReport.Context = .totalActivity  // DeviceActivityReport호출시 해당 컨텍스트에 해당하는 Report 불러옴
    
    // Define the custom configuration and the resulting view for this report.
    let content: ([AppUsage]) -> TotalActivityView
    
    func makeConfiguration(representing data: DeviceActivityResults<DeviceActivityData>) async -> [AppUsage] {
        // Reformat the data into a configuration that can be used to create
        // the report's view.
        let formatter = DateComponentsFormatter()
        formatter.allowedUnits = [.day, .hour, .minute, .second]
        formatter.unitsStyle = .abbreviated
        formatter.zeroFormattingBehavior = .dropAll
        
        return await printLog(data: data)
    }
    
    func containsIPhone(_ name: String) -> Bool {
      return name.range(of: "iPhone", options: .caseInsensitive) != nil
    }
    
    private func printLog(data: DeviceActivityResults<DeviceActivityData>) async -> [AppUsage] {
        let formatter = DateComponentsFormatter()
        formatter.allowedUnits = [.day, .hour, .minute, .second]
        formatter.unitsStyle = .abbreviated
        formatter.zeroFormattingBehavior = .dropAll
        let thisDevice = UIDevice.current.model
        
        var appUsages: [AppUsage] = []
        for await value in data {
            for await activity in value.activitySegments {
                for await category in activity.categories {
                    for await application in category.applications {
                        let usage = AppUsage(
                            title: application.application.localizedDisplayName?.lowercased() ?? "",
                            time: Int(application.totalActivityDuration)
                        )
                        appUsages.append(usage)
                    }
                }
                
            }
        }

        return appUsages
    }
}

4중 for문이라니 ,,, (가능하다면 flatMap으로 배열 평탄화 가공하셔도 좋을거같네요)

 

리포트 데이터를 받아서 구현할 뷰를 작성해보겠습니다.


import SwiftUI
import Charts

struct AppUsage {
    let title: String
    let time: Int
}

struct TotalActivityView: View {
    let totalActivity: String
    let total: Int
    let data: [AppUsage]
    @State private var selectedAngle: Int?
    private let dataRange: [Range<Int>]
    // 선택한 차트 각도에 해당하는 선택된 아이템반환
    private var selectedItem: AppUsage? {
        guard let selectedAngle else { return nil }
        if let selectedIndex = dataRange.firstIndex(where: { range in
            range.contains(selectedAngle)
        }) {
            return data[selectedIndex]
        }
        return nil
    }
    
    init(data: [AppUsage]) {
        self.data = data
        var total: Int = 0
        self.dataRange = data.map {
            let newTotal = total + $0.time
            let result = total..<newTotal
            total = newTotal
            return result
       }
        self.totalActivity = "\(total)s"
        self.total = total
    }
    
    var body: some View {
        Text("전체 \(totalActivity)")
        
        Chart(data, id: \.title) { value in
            SectorMark(
                angle: .value("appName", value.time),
                innerRadius: .ratio(0.618), // 황금비율 (안쪽원)
                outerRadius: .ratio(1), // 전체원의 크기
                angularInset: 3 // 차트 데이터간 간격
            )
            .cornerRadius(10)
            .opacity(selectedItem?.title == value.title ? 1 : 0.5) // 선택된아이템 강조
            .foregroundStyle(by: .value("color", value.title))
        }
        .chartLegend(.visible) // 범주 노출
        .chartAngleSelection(value: $selectedAngle) // 선택된 각도 바인딩 (Double 혹은 Int 타입만 가능)
        .chartBackground { proxy in
            GeometryReader { geometry in
                if let plotFrame = proxy.plotFrame { // 차트가 그려진프레임
                    let frame = geometry[plotFrame] // Ahchor<CGRect> -> CGRect 형변환
                    VStack {
                        Text("사용량")
                        if let selectedItem {
                            Text(selectedItem.title)
                                .bold()
                            Text("time: \(selectedItem.time)s")
                            Text("\(Double(selectedItem.time) / Double(total) * 100)%")
                        }
                    }
                    .position(x: frame.midX, y: frame.midY) // 차트프레임 좌표 적용시켜야 중앙에뜸
                }
            }
        }
        .scaledToFit()
    }
}

차트를 터치하면 해당 앱의 이름, 사용시간, 비율을 확인 할 수 있습니다.

 

시뮬레이터에선 앱사용량 제공을 해주지않아서 실기기에서 테스트하는걸 권장드려요!!

반응형