使用WatchKit force touch MenuItem调用外部函数

5
我需要实现一个 WatchKit 的 force-touch MenuItem,调用一个位于不继承 WKInterfaceController 的独立类中的 saveWorkout() 方法。
我意识到每个类都需要至少一个指定的初始化器。我猜这是关键吗?
顺便说一下,当我使用模拟器时,“ saveSession() reached ”打印语句记录到控制台,但在使用设备时则没有记录。即使在使用设备时,所有其他打印语句也会记录到控制台。有点奇怪。
我的初始化尝试会引发各种错误,例如:

1. 类“DashboardController”的未实现初始化器 'init()' 的致命错误

2. 在调用时缺少参数“context”

Dashboard.swift

class DashboardController: WKInterfaceController {

@IBOutlet var timerLabel: WKInterfaceTimer!
@IBOutlet weak var milesLabel: WKInterfaceLabel!

// var wSM: WorkoutSessionManager
    
//init(wSM: WorkoutSessionManager) {
//  self.wSM = wSM
//  super.init()
//  }


override func awakeWithContext(context: AnyObject?) {
    super.awakeWithContext(context)
    
    addMenuItemWithItemIcon(.Accept, title: "Save", action: #selector(DashboardController.saveSession))
}

override func willActivate() {
    super.willActivate()
    print("Dashboard controller reached")
}

func saveSession() {
    //wSM.saveWorkout()
    print("saveSession() reached")    
    }

WorkoutSessionManager.swift

class WorkoutSessionContext {

let healthStore: HKHealthStore
let activityType: HKWorkoutActivityType
let locationType: HKWorkoutSessionLocationType

init(healthStore: HKHealthStore, activityType: HKWorkoutActivityType = .Other, locationType: HKWorkoutSessionLocationType = .Unknown) {
    
    self.healthStore = healthStore
    self.activityType = activityType
    self.locationType = locationType
}
}

protocol WorkoutSessionManagerDelegate: class {
// ... protocol methods
}

class WorkoutSessionManager: NSObject, HKWorkoutSessionDelegate {

let healthStore: HKHealthStore
let workoutSession: HKWorkoutSession

init(context: WorkoutSessionContext) {
    self.healthStore = context.healthStore
    self.workoutSession = HKWorkoutSession(activityType: context.activityType, locationType: context.locationType)
    self.currentActiveEnergyQuantity = HKQuantity(unit: self.energyUnit, doubleValue: 0.0)
    self.currentDistanceQuantity = HKQuantity(unit: self.distanceUnit, doubleValue: 0.0)
    
    super.init()

    self.workoutSession.delegate = self
}

func saveWorkout() {
    guard let startDate = self.workoutStartDate, endDate = self.workoutEndDate else {return}

// ...code...

好的,让我们先试着让它工作起来。你的一般方法看起来很好。请仔细检查Xcode是否正确识别了你的选择器,并尝试删除类名。此外,参见这个答案,可能会有模拟器的问题:https://dev59.com/oY7ea4cB1Zd3GeqPE7d6#32359881 - jamesk
我已经注释掉了类实例化和方法调用,因为它们一开始就不起作用。实际上,我看到了同样的链接很多次,但是因为我在真实设备上测试,所以将其写下了。所以这次我在SIM上再次进行了测试,并尝试了禁用技巧,然后我得到了我的打印语句DashboardController saveSession() reached。所以由于某种奇怪的原因,我可以在SIM上获得打印语句,但在真实设备上却不能。谢谢你提醒我那个链接。我刚刚点赞了那个问题和答案,并添加了自己的答案,其中有一些更加明确的内容。 - Edison
没问题!不需要编写自定义初始化程序。如果您取消注释那些被注释的行,并在saveWorkout()函数(在您的WorkoutSessionManager类中)的guard语句之前放置一个打印语句,会发生什么? - jamesk
好的,代码现在已经更改了几次,也提到了几个不同的错误。当您仅使用let wSM = WorkoutSessionManager()语句和saveSession()函数时,您会遇到哪个错误? - jamesk
致命错误。它编译了,但我得到了一个运行时错误。也许WatchKit不允许从扩展中调用外部函数。但一些苹果公司的视频中却可以实现。他们没有展示所有的代码。 - Edison
显示剩余2条评论
1个回答

4
致命错误是(或者曾经)由以下代码行引起的: let wSM = WorkoutSessionManager() 这一行创建了一个新的WorkoutSessionManager实例并调用了init()
Swift为任何提供默认值且没有提供至少一个初始化器的结构体或类提供了一个名为init()的默认初始化器。但是,WorkoutSessionManager没有为healthStoreworkoutSession属性提供默认值(而且这些属性也不是可选的),并且它提供了自己的名为init(context:)的初始化器,因此它没有默认初始化器。
您需要使用指定的初始化器init(context:)(传递适当的WorkoutSessionContext实例)来创建WorkoutSessionManager实例,或者为WorkoutSessionManager提供名为init()的默认初始化器。
如何进行前者取决于您应用程序的其余部分的实现方式以及DashboardController的呈现方式。我假设您正在尝试重新创建WWDC 2015 Session 203中显示的“Fit”应用程序。
在该演示中,初始控制器是ActivityInterfaceController的一个实例,该控制器负责通过在故事板中创建的转场呈现下一个界面。您可以在ActivityInterfaceController类中看到以下代码:
override func contextForSegueWithIdentifier(segueIdentifier: String) -> AnyObject? {
    let activityType: HKWorkoutActivityType

    switch segueIdentifier {
    case "Running":
        activityType = .Running

    case "Walking":
        activityType = .Walking

    case "Cycling":
        activityType = .Cycling

    default:
        activityType = .Other
    }

    return WorkoutSessionContext(healthStore: self.healthStore, activityType: activityType)
}

上述函数创建并返回一个新的WorkoutSessionContext实例,使用初始控制器持有的HKHealthStore实例。 该函数返回的上下文被传递到与相关segue对应的目标界面控制器中的awakeWithContext中。
对于代码中的转换,您可以使用等效的函数传递上下文实例,例如pushControllerWithName(context:),这也会导致awakeWithContext
如果您的初始控制器类似于上述内容,则可以在DashboardController类的awakeWithContext中访问传递的上下文,并使用它来配置WorkoutSessionManager的新实例。
class DashboardController: WKInterfaceController
{
    // ...

    var wSM: WorkoutSessionManager?

    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)

        if context is WorkoutSessionContext {
            wSM = WorkoutSessionManager(context: context as! WorkoutSessionContext)
        }

        addMenuItemWithItemIcon(.Accept, title: "Save", action: #selector(DashboardController.saveSession))
    }

    // ...
}

以这种方式创建WorkoutSessionManager实例避免了调用(不存在的)init()初始化程序,并允许重复使用HKHealthStore实例。是否可以采用这种方法取决于您的其余代码和您呈现DashboardController的方式。
请注意,应避免创建多个WorkoutSessionManager实例。使用singleton提供一个WorkoutSessionManager的单个实例,该实例在您的扩展中共享。

您可能会发现苹果的SpeedySloth示例在您的整体方法方面很有用: https://developer.apple.com/library/prerelease/content/samplecode/SpeedySloth - jamesk
不幸的是,我收到了错误信息 fatal error: unexpectedly found nil while unwrapping an Optional value。为什么 WorkoutSessionManager 会为空?当我删除 ? 查看发生了什么时,我得到了错误信息 Class 'DashboardController' has no initializers。然后,为了证明关于找到 nil 的错误消息是有效的,我在 saveSession() 函数中将 wSM 设为可选项 ?,并且没有崩溃,这证实了编译器关于 nil 的原始警告。顺便说一下,这些都是运行时错误。它们发生在我按菜单项调用 saveSession() 时。 - Edison
如果传递给awakeWithContext的上下文为nil或不是WorkoutSessionContext的实例,则WorkoutSessionManager将为nil。您能否发布用于创建和传递该上下文的代码? - jamesk
让我们在聊天中继续这个讨论 - jamesk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接