上一篇文章,初步了解了如何去屏蔽app,这篇文章将去介绍如果指定时间去屏蔽app
1. 首先,我们需要为目标添加一个设备监视器扩展。它可作为子设备上的后台服务,提供各种功能。
选择
File > New > Target > Choose Device Activity Monitor Extension
我们创建了DeviceActivityMonitorExtension类,主要用了这个类的intervalDidStart和intervalDidEnd方法
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()
}
}
运行界面如下
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这个类,不然会报错找不到
这里还需要注意的是DeviceActivityManager.shared这里面要想获取到里面的selection属性,需要采用group,前面提到了主程序和扩展程序进行数据的交互只能通过group来进行,所以接下来还要进行一下group的配置
先去apple开发者平台配置下group
命名规则group+bundle id
然后Xcode添加group
这里如果在apple的开发者平台配置了group,点击刷新是能看到这组group的,然后勾选上
这里,扩展程序需要同样的配置,扩展程序的bundle id命名一般是主程序id+扩展程序命名id,这边所有的配置id都需要去apple开发者平台去配置下
到这里,这块代码已经完成,可以运行程序去屏蔽app操作下,可以看到app被成功屏蔽
进一步拓展
上面的app被屏蔽的界面,这块是系统默认的,这块的文字内容也是可以自定义扩展的
我们先来创建一个ShieldConfiguration扩展程序
File > New > Target > Choose Shield Configuration
修改代码如下
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,可以看见界面变成这样了
这里需要注意:界面的布局不能改变,这块是apple限制死,只能修改ShieldConfiguration公开的属性
主按钮和副按钮点击方法都是默认的,这里可以通过Shield Action Extension扩展程序来修改方法的操作
这里面还有一个扩展程序,算是一个比较大头的扩展程序,屏幕使用时间的扩展程序
这块在后面进行单独模块的介绍