iOS 11.4+
让教师从应用程序的内容中分配活动,并查看学生的进度。
一、概述
教育应用程序提供对书籍和视频等资源的访问,同时通过交互式可视化、游戏和评估来加强学习。ClassKit允许你组织教学材料,这样老师就可以给学生分配活动,并看到他们的进步。
ClassKit环境由教师设备和许多通过iCloud通信的学生设备组成。每台设备都会运行你的应用程序(以及其他教育应用程序)以及苹果的作业应用程序,ClassKit作为设备的中心。通过作业,教师可以看到你的应用程序向ClassKit公开哪些可分配的内容。然后他们可以根据这些内容创建作业,并监控所有学生的进度。同时,学生利用作业来接收直接链接到应用程序内容的作业。
image.png
ClassKit不会替换应用程序中的任何现有逻辑或存储机制,也不会使用它来生成任何新的用户界面。相反,你可以使用ClassKit来宣传你已经拥有的结构,这样老师们就可以使用苹果的Schoolwork应用程序根据应用程序的内容创建作业,并通过这些作业来衡量学生的进度。
注意:
ClassKit是为使用Apple学校管理器和管理的Apple ID的教育机构设计的。如果教育是你的目标市场,考虑采用ClassKit。
二、在应用程序中启用ClassKit
准备应用程序和开发环境以采用ClassKit。
采用ClassKit可以让你的应用程序参与到一个虚拟教室中,这个教室跨越了许多通过iCloud进行通信的设备。要参与这个教育生态系统,首先要启用ClassKit功能。然后,为了测试你的应用程序与这个生态系统的交互,在你的开发设备上安装苹果的学校作业应用程序,并通过在开发模式下运行来模拟iCloud的交互。
1、启用ClassKit功能:
要访问虚拟教室环境,请在Xcode中为应用程序启用ClassKit功能。
image.png当您启用ClassKit功能时,Xcode会自动将ClassKit环境授权添加到您的授权文件中。它还将相应的功能添加到应用程序ID中。有关启用功能的详细信息,请参阅Xcode帮助中的“添加功能”。
2、在你的设备上安装Schoolwork应用程序
Schoolwork应用程序提供了一个界面,教师可以使用该界面查看应用程序提供的内容,基于该内容创建作业,并通过这些作业监控学生的进度。学生使用相同的应用程序接收作业,并直接链接到应用程序中的内容。Schoolwork通过根据登录用户的角色改变其行为来提供这两种体验。有关用户角色的更多信息,请参阅关于ClassKit和用户角色。
要测试应用程序的ClassKit采用情况,需要在自己的开发设备上安装Schoolwork。这允许您验证应用程序发送到ClassKit的数据。它还可以让你体验老师和学生在教育环境中使用你的应用程序时看到的东西。
要获取Schoolwork,请从应用商店下载。
注意:
你不能在模拟器中测试ClassKit的行为,因为在那个环境中没有Schoolwork。
3、使用开发模式进行本地测试
当您通过appstore分发支持ClassKit的应用程序时,它将以生产模式运行。在这种模式下,教师所做的作业传播到学生的设备上,进度通过iCloud返回到教师的设备上。但在开发过程中,您可能无法访问满是托管设备的教室。因此,您在开发模式下进行测试,将所有数据本地存储在单个设备上,根据需要在教师和学生角色之间切换。Xcode会自动为您处理模式选择,但您可以在开发模式下控制角色(学生或教师),如开发期间测试应用程序中所述。
三、在开发过程中测试应用程序
使用开发模式在没有托管Apple ID的情况下测试应用程序。
当您通过appstore分发支持ClassKit的应用程序时,它将以生产模式运行。在这种模式下,ClassKit与设备上的作业共享数据,也可以通过iCloud与其他设备共享数据,以便在教师设备上完成的作业可以传输到学生的设备上,或者让学生报告的进度返回给做作业的老师。要定义这些角色并启用这些连接,用户必须使用注册于Apple School Manager的教育机构颁发的托管Apple ID登录设备。
在开发过程中,您可能无法访问具有托管Apple ID的用户。相反,您可以在开发模式下运行应用程序。在Xcode默认启用的这种模式下,您可以在设备设置中手动设置用户的角色,并且所有的ClassKit数据都将保存在本地设备上。不会向iCloud发送任何内容,因此不需要托管Apple ID。然而,classkitapi的行为与在生产模式下的行为一样,因此您可以测试您的ClassKit采用情况。
注意:
你不能在模拟器中测试ClassKit的行为,因为在那个环境中没有作业。相反,在安装了Schoolwork应用程序的iOS设备上测试ClassKit的采用情况。
1、在ClassKit开发人员设置中选择一个角色:
在iOS设备上安装Schoolwork后,会出现新的开发人员设置,用于控制ClassKit开发模式行为。转到设备上的“设置”>“开发人员”,然后选择ClasskitAPI。
image.png
在出现的视图中,为设备选择教师或学生角色。您选择的角色会更改ClassKit路由数据的方式,如About ClassKit和User Roles中所述。
image.png您可以根据需要在教师和学生角色之间来回切换。例如,从教师开始,运行应用程序来建立可分配的内容。然后用作业来布置作业。接下来,切换到学生模式并在应用程序中完成作业。最后,切换回教师模式以查看报告的学生进度。如果你想重新开始,点击重设开发数据(显示在上面屏幕截图中角色选择的下方)来清除学生和教师数据库。
如果以后要使用真实的托管帐户进行测试,如下面的“在生产模式下使用受管Apple ID进行测试”中所述,请返回“偏移”设置退出开发模式。
2、更改角色后刷新缓存的ClassKit数据
由于开发模式将教师和学生的数据存储在一台设备上各自的专用数据库中,因此在您更改角色后,从内存中刷新任何缓存的ClassKit数据非常重要。否则,您可能在转换为学生角色后使用教师资料库中的快取项目,反之亦然。这会导致未定义的行为。
刷新数据最简单的方法是重新启动应用程序,在开始以新角色操作时,强制ClassKit重新加载或重新创建数据,而不是依赖内存中可能过时的数据。
3、或者,在生产模式下使用托管的Apple ID进行测试
您可以使用开发模式完全限定ClassKit的采用,如上所述。但如果您确实需要在用户使用托管Apple ID登录的环境中从Xcode启动应用程序,则可以手动选择生产模式。
当您在Xcode中为应用程序启用ClassKit功能时,Xcode会向构建添加一个ClassKit环境权限,并将其值设置为development。默认情况下,此授权值启用ClassKit并将应用程序选择到ClassKit的开发功能中。作为appstore提交过程的一部分,Xcode自动将授权的值更改为production,这将停用通过appstore分发的应用实例的开发模式行为。
要覆盖此行为并在开发过程中选择生产模式,请通过打开应用程序的授权文件并将ClassKit授权的值设置为production来手动修改授权:
image.png从应用程序的角度来看,ClassKit API在生产模式下的行为与在开发模式下的行为完全相同,但框架仅在您使用托管Apple ID登录设备时与学校工作和网络交互。如果您在生产模式下没有使用托管Apple ID登录,ClassKit将您视为非托管用户,静默地删除通过API报告的任何数据。类似地,除非学校的IT管理员在Apple school Manager中启用学生进度,否则ClassKit会删除数据,如在Apple school Manager中使用Schoolwork管理学生进度中所述。
重要:
如果在应用授权中选择了生产模式,请确保在开发设备上禁用ClassKit开发模式。在“设置”中,选择“开发人员”>“ClassKit API”>“关闭”。否则,无论应用程序的设置如何,ClassKit都会将所有数据保存在设备上。
四、将ClassKit整合到教育应用程序中
完成布置作业和记录学生进度的过程。
您可以在现有的教育应用程序中采用ClassKit,使教师能够创建作业并通过这些作业监控学生的进度。这个示例代码项目演示了ClassKit在一个允许用户阅读剧本的应用程序中的采用。在开始之前,请务必阅读在应用程序中启用ClassKit,以了解如何配置环境以使用ClassKit,并在开发期间测试应用程序以准备调试ClassKit的采用。
1、从现有的教育应用程序开始
GreatPlays应用程序提供了一个可导航的层次结构,包括剧本、表演和场景,以及测试读者理解力的测验。该应用程序使用一个简单的数据模型来表示一组播放,共享PlayLibrary实例保存Play实例,每个实例包含一个Act实例数组,依此类推。这些都是独立于ClassKit而存在的。
image.png在本例中,在启动时,将从应用程(:didFinishLaunchingWithOptions:)方法中向库中添加单个剧本莎士比亚的《哈姆雷特》的结构。
注意:
在为教育市场编写应用程序时,考虑支持共享iPad,如优化共享iPad应用程序中所述。这个示例应用程序不使用任何持久本地存储,并在其信息列表文件。
在一个真正的应用程序中,除了结构之外,你还可以添加剧本的文本,以及为每个场景量身定做的问答题。您也可以支持其他播放,或者随应用程序分发,或者稍后下载。
2、定义可分配内容
采用ClassKit的第一个任务是定义应用程序的可分配内容。您将一个可分配的内容单元表示为CLSContext实例,然后通过将上下文分组到一个层次结构中来建立上下文之间的关系。对于剧本的读者来说,老师可能需要布置一个小测验,一个单独的场景,一个动作(包括所有的场景),甚至整个剧本。因此,现有的模型层次结构为上下文层次结构提供了一个很好的模板。
因为ClassKit是在你的应用程序已经完成的基础上分层的,所以最好将类工具包支持隔离到类扩展中。避免中断正常的程序流程。因此,示例应用程序声明了一个节点协议,模型对象可以在扩展中采用该协议,以便与相关上下文轻松关联:
protocol Node {
var parent: Node? { get }
var children: [Node]? { get }
var identifier: String { get }
var contextType: CLSContextType { get }
}
在采用此协议时,模型对象会公开其直接祖先和后代、唯一标识符和指示其包含的内容类型的CLSContextType值。例如,下面所示的Act节点扩展将其父级定义为包含它的play,将其子级定义为包含它的场景。它提供了一个行为所特有的标识符,以及一个上下文类型CLSContextType.chapter,这是对戏剧中某个行为角色的合理近似。
extension Act: Node {
var parent: Node? {
return play
}
var children: [Node]? {
return scenes
}
var identifier: String {
return "Act \(number)"
}
var contextType: CLSContextType {
return .chapter
}
}
此外,节点协议的扩展为所有模型对象提供了处理标识符的默认行为。特别是,采用该协议的模型对象可以报告自己的标识符路径(从一个节点到另一个节点的一组标识符字符串的集合),并从标识符路径找到子节点:
extension Node {
var identifierPath: [String] {
var pathComponents: [String] = [identifier]
if let parent = self.parent {
pathComponents = parent.identifierPath + pathComponents
}
return pathComponents
}
/// Finds a node in the play list hierarchy by its identifier path.
func descendant(matching identifierPath: [String]) -> Node? {
if let identifier = identifierPath.first {
if let child = children?.first(where: { $0.identifier == identifier }) {
return child.descendant(matching: Array(identifierPath.suffix(identifierPath.count - 1)))
} else {
return nil
}
} else {
return self
}
}
}
3、向老师宣传你的内容
上下文是你的应用程序向教师发布其可分配内容的机制。你告诉ClassKit的上下文在苹果的作业应用程序中显示为任务,老师可以根据你的内容创建作业。因此,尽快(并且以原子方式)声明上下文是很重要的。否则,教师将无法在作业中看到你的应用程序内容,或者可能只看到部分任务列表。
通过在应用程序启动时或下载动态内容后立即声明静态内容的上下文层次结构来处理此问题。在play reader应用程序中,通过将上下文声明作为在addPlay方法中构建新play实例的完整步骤来完成此操作:
func addPlay(_ play: Play, creatingContexts: Bool = true) {
if !plays.contains(where: { $0.title == play.title }) {
plays.append(play)
// Give ClassKit a chance to set up its contexts.
if creatingContexts {
setupContext(play: play)
}
}
}
通过请求所有叶节点的数据存储来声明整个播放上下文层次结构,这也隐式声明了作为叶节点祖先的所有上下文:
func setupContext(play: Play) {
for act in play.acts {
for scene in act.scenes {
// Get the deepest path: the quiz if there is one, or the scene if not.
let path = scene.quiz?.identifierPath ?? scene.identifierPath
// Asking for a context causes it (and its ancestors) to be built, as needed.
CLSDataStore.shared.mainAppContext.descendant(matchingIdentifierPath: path) { _, _ in }
}
}
}
因为此时只声明上下文,所以不需要对返回的值执行任何操作。
4、按需构建上下文
每当您向数据存储(CLSDataStore)请求上下文时,无论是在声明过程中还是因为您想激活上下文,数据存储都会首先在存储上下文的数据库中查找。如果上下文在那里是可用的,可能是以前发布的应用程序,数据存储将返回该上下文。但是如果它不可用,数据存储将要求它的委托来构建上下文。
通过定义与模型层次结构平行的上下文,可以方便地构建新的上下文。在实现CLSDataStoreDelegate协议的createContext(forI)时标识符:parentContext:parentIdentifierPath:)方法,则可以使用模型对象的特性来通知上下文创建。
在play reader中,PlayLibrary类的共享实例扮演delegate的角色,同样使用一个扩展。它的ClassKit扩展包括setupClassKit()方法,该方法将自己指定为委托:
func setupClassKit() {
CLSDataStore.shared.delegate = self
}
扩展还实现了委托回调,依赖于每个模型对象的节点扩展来提供创建上下文所需的数据:
func createContext(forIdentifier identifier: String, parentContext: CLSContext, parentIdentifierPath: [String]) -> CLSContext? {
// Find a node in the model hierarchy based on the identifier path.
let identifierPath = parentIdentifierPath + [identifier]
guard let playIdentifier = identifierPath.first,
let play = PlayLibrary.shared.plays.first(where: { $0.identifier == playIdentifier }),
let node = play.descendant(matching: Array(identifierPath.suffix(identifierPath.count - 1))) else {
return nil
}
// Use the node to create and customize a context.
let context = CLSContext(type: node.contextType, identifier: identifier, title: node.identifier)
context.topic = .literacyAndWriting
// Users of 11.3 rely on a user activity instead.
if #available(iOS 11.4, *),
let path = identifierPath.joined(separator: "/").addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) {
// Use custom URLs to locate activities.
// Comment this assignment to rely on a user activity for all users.
context.universalLinkURL = URL(string: "greatplays://" + path)
}
// No need to save: the framework handles that automatically.
os_log("%s Built", node.identifierPath.description)
return context
}
5、从应用程序扩展构建上下文
通过定义一个名为ClassKitContextProvider的目标,应用程序包括一个ClassKit上下文提供程序应用程序扩展。ClassKit扩展的主类符合CLSContextProvider协议。Schoolwork调用此协议的updateDescents(of:completion:)方法,在教师浏览可分配内容时创建或更新给定上下文的子对象。这样,即使在老师第一次运行主应用程序之前,学校作业也可以发布应用程序可分配内容的最新版本。
使用传入上下文的标识符路径,update方法在数据模型中找到相应的节点,然后找到该节点的子节点:
// Start with plays as child nodes. Then look for a descendant.
var childNodes: [Node] = PlayLibrary.shared.plays
if let identifier = identifierPath.first,
let play = PlayLibrary.shared.plays.first(where: { $0.identifier == identifier }),
let node = play.descendant(matching: Array(identifierPath.suffix(identifierPath.count - 1))) {
childNodes = node.children ?? []
}
update方法然后查找与节点处于同一层次结构级别的现有子上下文,并创建任何缺少的子上下文:
let predicate = NSPredicate(format: "%K = %@",
CLSPredicateKeyPath.parent as CVarArg,
context)
CLSDataStore.shared.contexts(matching: predicate) { childContexts, _ in
for childNode in childNodes {
if !childContexts.contains(where: { $0.identifier == childNode.identifier }),
let childContext = PlayLibrary.shared.createContext(forIdentifier: childNode.identifier,
parentContext: context,
parentIdentifierPath: identifierPath) {
context.addChildContext(childContext)
}
}
在这个应用程序中,上下文永远不会改变,所以当循环找到一个现有的上下文时,它将移动到下一个迭代而不采取任何操作。如果你的应用程序有可以更改的上下文,请利用这个机会重新配置上下文。不管怎样,在完成循环后,保存更新并调用完成处理程序:
CLSDataStore.shared.save { error in
if let error = error {
os_log("Save error: %s", error.localizedDescription)
} else {
os_log("Saved")
}
completion(error)
}
}
6、用活动记录进度
当上下文声明应用程序的结构时,您可以使用CLSActivity实例来报告这些上下文的进度。例如,对于一个场景的上下文,相应的活动报告了学生阅读了多少场景,以及他们花了多长时间阅读。
除了标识符扩展之外,示例应用程序还包括另一个对Node的扩展,该扩展提供了处理ClassKit活动的默认行为。模型对象使用自己的标识符路径来定位匹配的上下文,然后使用上下文来管理活动。例如,在扩展中定义的startActivity()方法开始一个活动:
func startActivity(asNew: Bool = false) {
os_log("%s Start", identifierPath.description)
CLSDataStore.shared.mainAppContext.descendant(matchingIdentifierPath: identifierPath) { context, _ in
// Activate the context.
context?.becomeActive()
if asNew == false,
let activity = context?.currentActivity {
// Re-start the existing activity
activity.start()
} else {
// Create and start an activity.
context?.createNewActivity().start()
}
CLSDataStore.shared.save { error in
guard error == nil else {
os_log("%s Start save error: %s", self.identifierPath.description, error!.localizedDescription)
return
}
}
}
}
节点还定义了将进度报告为任务完成和结束活动的一部分的方法。请注意,所有这些方法每次都检索上下文,而不是存储对它的引用。这样做很重要,因为由于网络同步,底层实例可能会在调用之间发生变化。
7、用户开始活动时开始录制
通常调用方法来记录视图控制器中的活动。考虑一个阅读特定场景的任务。场景的视图控制器知道场景何时出现在屏幕上,并且在场景实例上有一个控制柄。因此,控制器处于最佳位置,可以告诉场景从其viewDidAppease(:)方法开始录制活动:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
scene?.startActivity()
}
请注意,当学生开始读取场景时,控制器不会设置startActivity()方法的asNew参数,而是将其默认值设置为false。因此,以前停止的活动(如果可用)将恢复。这允许用户开始读取场景,然后导航到应用程序中的其他位置(例如查看以前的场景),而不必完成当前尝试。当用户返回时,进度和持续时间将从用户停止的地方恢复。
相反,startQuiz()方法会将startQuiz()方法的asNew参数设置为true。测验一旦开始,必须在用户可以转移到另一个任务之前完成。因此,该应用程序将每次新尝试视为一项新活动。
func start() {
startActivity(asNew: true)
}
对于特定活动,如何处理此问题取决于您定义的任务的特性。
8、用户滚动时添加进度
当场景视图控制器(管理滚动视图)可见时,它使用其内容偏移的知识作为学生阅读场景的程度的指示。对于每次滚动视图代理更新,控制器通过测量滚动位置作为学生已读多少的代理向场景报告一个新的进度值:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let position = sceneText.contentOffset.y + sceneText.frame.size.height
let total = sceneText.contentSize.height
// The scroll view can bounce, so use care to bound the progress.
let progress = Double(max(0, min(1, position / total)))
scene?.update(progress: progress)
}
9、当用户停止活动时停止录制
当活动在其视图中结束时,控制器通知viewWillDisappear(_:) 方法:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
scene?.stopActivity()
}
10、使用活动项报告其他指标
活动自动报告持续时间和进度。但有时您需要提供有关活动的其他度量。例如,您可能希望报告测验分数,或记录在解决问题时使用提示的次数。为此,使用活动项。
在GreatPlays中,节点协议扩展为此提供了addScore()和addQuantity()方法。当用户完成测验时,但在结束活动之前,将调用这些函数来报告测验结果:
func record() {
// The score is the primary metric for a quiz.
addScore(score, title: "Score", primary: true)
addQuantity(Double(hints), title: "Hints")
markAsDone()
stopActivity()
}
11、将活动标记为已完成
在记录测验分数的同时,应用程序还会调用markAsDone()方法,如前一节所示,该方法反过来调用数据存储的completeAllAssignedActivities(matching:)方法来指示学生已经完成了尝试。
func markAsDone() {
if #available(iOS 12.2, *) {
os_log("%s Done", identifierPath.description)
CLSDataStore.shared.completeAllAssignedActivities(matching: identifierPath)
}
}
在测试结束后,学生不能返回并更改任何内容,因此应用程序可以安全地将活动标记为已完成。相比之下,阅读场景没有容易检测到的终点,因此在这种情况下,应用程序不会调用完成方法。相反,学生决定何时在作业中标记活动完成。
12、指定主要活动项
添加活动项时,可以选择将其中一项作为主要项。小学在教师看到的总结结果中扮演着更重要的角色。在play reader示例中,对于测验,分数被视为主要项目,如上所示。或者,您可以选择不将任何活动项目设置为主要项目,在这种情况下,进度将成为向教师显示的最显著的结果。addScore()方法演示了将活动项注册为主活动项和不注册活动项的两种方法:
if primary {
activity.primaryActivityItem = item
} else {
activity.addAdditionalActivityItem(item)
}
如果您确实决定设置主项,请确保始终为给定活动设置相同类型的主项。例如,一旦将分数项注册为测验活动的主要项,则必须始终以这种方式使用分数。以后将提示量设为主数量将生成错误。
网友评论