iOS中Screen Time Control(屏幕使用时间)开发,屏蔽禁止使用app,FamilyControls进阶

上一篇文章,初步了解了如何去屏蔽app,这篇文章将去介绍如果指定时间去屏蔽app

1. 首先,我们需要为目标添加一个设备监视器扩展。它可作为子设备上的后台服务,提供各种功能。

选择

File > New > Target > Choose Device Activity Monitor Extension

image.png
image.png
image.png

我们创建了DeviceActivityMonitorExtension类,主要用了这个类的intervalDidStart和intervalDidEnd方法

image.png

2. 接下来创建一个model进行全局的统一管理,也是为了保存下来用户选择进行屏蔽的app的数据,为了可以在DeviceActivityMonitorExtension中使用

import SwiftUI
import FamilyControls
import DeviceActivity

class DeviceActivityManager: ObservableObject {
    static let shared = DeviceActivityManager()
    
    private init() {}
    // app选择器
    // AppStorage是swiftUI特有的保存数据方式
    // 保存在AppStorage中,这里面采用group保存
   // group命名一般是group+bundle id
    // 因为主程序和扩展程序DeviceActivityMonitorExtension交互数据需要采用group形式
    @AppStorage("selection", store: UserDefaults(suiteName: "group.com.screentimedemo.test"))
    var selection = FamilyActivitySelection()
    
    // 监控类
    let deviceActivityCenter = DeviceActivityCenter()
    
    let activityNameId = UUID().uuidString
    
    // 设置app开始屏蔽的时间
    func startMonitoring() {
        
        let scheduleStartTime =  Date()
        // 当前时间+5分钟   60 * 20
        let scheduleEndTime =  Date() + 1200
        
        let startTime = Calendar.current.dateComponents([.hour, .minute], from: scheduleStartTime)
        let endTime = Calendar.current.dateComponents([.hour, .minute], from: scheduleEndTime)
        
        // 监控开始和结束时间,这块时间可以通过用户自己来动态设置获取到
        // demo这里就写死了
        let schedule = DeviceActivitySchedule(
            intervalStart: startTime,
            intervalEnd: endTime,
            repeats: true)
        
        // 该条监控的唯一标识符,因为可能存在多条监控,该标识符可以全局来设置保存作为区别
        // demo就随意设置了
        let activityName = DeviceActivityName(activityNameId)
        
        do {
            // 开始监控,下发命令给设备,设置到达了startTiem的时候,就会调用
            // DeviceActivityMonitorExtension里面的intervalDidStart方法
            // 时间到了endTime就会调用intervalDidEnd方法
            try deviceActivityCenter.startMonitoring(activityName,
                                                     during: schedule)
            print("startMonitoring")
        } catch {
            print("DEBUG: Error: \(error.localizedDescription)")
            print("DEBUG: Error recoverySuggestion: \(String(describing: (error as? LocalizedError)?.recoverySuggestion))")
        }
    }
    
    // 停止监控 立马调用DeviceActivityMonitorExtension里面intervalDidEnd方法
    func stopMonitoring() {
        let activityName = DeviceActivityName(activityNameId)
        deviceActivityCenter.stopMonitoring([activityName])
    }
    
}

// MARK: - FamilyActivitySelection Parser
//AppStorage不支持FamilyActivitySelection,所以需要扩展下
extension FamilyActivitySelection: RawRepresentable {
    public init?(rawValue: String) {
        guard let data = rawValue.data(using: .utf8),
              let result = try? JSONDecoder().decode(FamilyActivitySelection.self, from: data)
        else {
            return nil
        }
        self = result
    }
    
    public var rawValue: String {
        guard let data = try? JSONEncoder().encode(self),
              let result = String(data: data, encoding: .utf8)
        else {
            return "[]"
        }
        return result
    }
}

3. 主类的代码如下

import SwiftUI
import FamilyControls

struct ContentView: View {
    
    @State  var isPresented =  false
    // 替代 @State  var screenTimeSelection =  FamilyActivitySelection ()
    // 采用model管理保存
    @StateObject  var manager =  DeviceActivityManager.shared
    
    var body: some View {
        VStack(spacing: 50) {
            Button {
                Task {
                    do {
                        try await AuthorizationCenter.shared.requestAuthorization(for: .individual)
                    } catch {
                        print("Failed to enroll Aniyah with error: \(error)")
                    }
                }
            } label: {
                Text("获取screenTime屏幕权限")
            }
            
            Button {
                isPresented =  true
            } label: {
                Text("选择app界面")
            }
            
            Button {
                blockApp()
            } label: {
                Text("屏蔽app")
            }
            
            Button {
                stopBlockApp()
            } label: {
                Text("解除app")
            }
        }
        .padding()
        .familyActivityPicker(isPresented: $isPresented, selection: $manager.selection)
    }
    
    func blockApp() {
        manager.startMonitoring()
    }
    
    func stopBlockApp() {
        manager.stopMonitoring()
    }
}

运行界面如下

image.png

4. 接下来在主程序下发屏蔽app的命令后,扩展程序接收到命令进一步执行操作

import DeviceActivity
import SwiftUI
import ManagedSettings

// Optionally override any of the functions below.
// Make sure that your class name matches the NSExtensionPrincipalClass in your Info.plist.
class DeviceActivityMonitorExtension: DeviceActivityMonitor {
    
    @StateObject var manager = DeviceActivityManager.shared
    
    // 主要用于管理设备上的设置,比如屏蔽app,禁止app的使用
    private let store = ManagedSettingsStore()
    
    // 开始监控
    override func intervalDidStart(for activity: DeviceActivityName) {
        
        super.intervalDidStart(for: activity)
        
        // Handle the start of the interval.
        
        blockApp()
    }
    
    // 结束监控
    override func intervalDidEnd(for activity: DeviceActivityName) {
        super.intervalDidEnd(for: activity)
        
        // Handle the end of the interval.
        
        stopBlockApp()
    }
    
    override func eventDidReachThreshold(_ event: DeviceActivityEvent.Name, activity: DeviceActivityName) {
        super.eventDidReachThreshold(event, activity: activity)
        
        // Handle the event reaching its threshold.
    }
    
    override func intervalWillStartWarning(for activity: DeviceActivityName) {
        super.intervalWillStartWarning(for: activity)
        
        // Handle the warning before the interval starts.
    }
    
    override func intervalWillEndWarning(for activity: DeviceActivityName) {
        super.intervalWillEndWarning(for: activity)
        
        // Handle the warning before the interval ends.
    }
    
    override func eventWillReachThresholdWarning(_ event: DeviceActivityEvent.Name, activity: DeviceActivityName) {
        super.eventWillReachThresholdWarning(event, activity: activity)
        
        // Handle the warning before the event reaches its threshold.
    }
    
    // 真正屏蔽app的功能方法
    func blockApp() {
        let applications = manager.selection.applicationTokens
        let categories = manager.selection.categoryTokens
        if !applications.isEmpty {
            print("applications: \(applications)")
        }
        if !categories.isEmpty {
            print("categories: \(categories)")
        }
        // 屏蔽应用
        store.shield.applications = applications.isEmpty ? nil : applications
        store.shield.applicationCategories = categories.isEmpty ? nil : .specific(categories)
        store.shield.webDomainCategories = categories.isEmpty ? nil : .specific(categories)
    }
    
    func stopBlockApp() {
        store.shield.applications = nil
        store.shield.applicationCategories = nil
        store.shield.webDomains = nil
    }
}

这里,扩展程序需要单独的添加DeviceActivityManager这个类,不然会报错找不到

image.png

这里还需要注意的是DeviceActivityManager.shared这里面要想获取到里面的selection属性,需要采用group,前面提到了主程序和扩展程序进行数据的交互只能通过group来进行,所以接下来还要进行一下group的配置

先去apple开发者平台配置下group
命名规则group+bundle id


image.png

然后Xcode添加group

image.png
image.png

这里如果在apple的开发者平台配置了group,点击刷新是能看到这组group的,然后勾选上

image.png
image.png

这里,扩展程序需要同样的配置,扩展程序的bundle id命名一般是主程序id+扩展程序命名id,这边所有的配置id都需要去apple开发者平台去配置下

image.png

到这里,这块代码已经完成,可以运行程序去屏蔽app操作下,可以看到app被成功屏蔽

image.png

进一步拓展

image.png

上面的app被屏蔽的界面,这块是系统默认的,这块的文字内容也是可以自定义扩展的

我们先来创建一个ShieldConfiguration扩展程序

File > New > Target > Choose Shield Configuration

image.png

修改代码如下

import ManagedSettings
import ManagedSettingsUI
import UIKit

// Override the functions below to customize the shields used in various situations.
// The system provides a default appearance for any methods that your subclass doesn't override.
// Make sure that your class name matches the NSExtensionPrincipalClass in your Info.plist.
class ShieldConfigurationExtension: ShieldConfigurationDataSource {
    override func configuration(shielding application: Application) -> ShieldConfiguration {
        // Customize the shield as needed for applications.
        ShieldConfiguration(
           backgroundBlurStyle: UIBlurEffect.Style.systemChromeMaterialDark,
           backgroundColor: UIColor.red,
           icon: UIImage.init(systemName: "sun.max"),
           title: ShieldConfiguration.Label(text: "app主标题", color: .white),
           subtitle: ShieldConfiguration.Label(text: "\n \n \n \n app副标题", color: .white),
           primaryButtonLabel: ShieldConfiguration.Label(text: "主按钮", color: UIColor.white),
           primaryButtonBackgroundColor: UIColor.systemPink,
           secondaryButtonLabel: ShieldConfiguration.Label(text: "副按钮", color: UIColor.white)
       )
    }
    
    override func configuration(shielding application: Application, in category: ActivityCategory) -> ShieldConfiguration {
        // Customize the shield as needed for applications shielded because of their category.
        ShieldConfiguration()
    }
    
    override func configuration(shielding webDomain: WebDomain) -> ShieldConfiguration {
        // Customize the shield as needed for web domains.
        ShieldConfiguration()
    }
    
    override func configuration(shielding webDomain: WebDomain, in category: ActivityCategory) -> ShieldConfiguration {
        // Customize the shield as needed for web domains shielded because of their category.
        ShieldConfiguration()
    }
}

这时候运行程序,然后打开被屏蔽的app,可以看见界面变成这样了

image.png

这里需要注意:界面的布局不能改变,这块是apple限制死,只能修改ShieldConfiguration公开的属性

主按钮和副按钮点击方法都是默认的,这里可以通过Shield Action Extension扩展程序来修改方法的操作

image.png

这里面还有一个扩展程序,算是一个比较大头的扩展程序,屏幕使用时间的扩展程序

image.png

这块在后面进行单独模块的介绍

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容