MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C中的WatchKit与Apple Watch开发

2023-10-091.9k 阅读

一、WatchKit 基础

(一)WatchKit 简介

WatchKit 是苹果公司为 Apple Watch 开发提供的一套框架。它允许开发者利用 iOS 应用已有的资源和代码基础,快速开发出与 iPhone 应用紧密结合的 Apple Watch 应用。WatchKit 使得开发者能够创建复杂且功能丰富的手表应用界面,并且通过与 iPhone 进行通信来实现各种功能。

在 Objective - C 开发环境中,WatchKit 提供了一系列的类和接口,方便开发者进行界面布局、交互处理以及数据传递等操作。它的设计理念是充分利用 Apple Watch 有限的屏幕空间和硬件资源,为用户提供简洁而高效的交互体验。

(二)WatchKit 架构

  1. WatchKit 扩展
    • WatchKit 扩展是运行在 Apple Watch 上的代码部分。它负责管理 Watch 应用的用户界面、处理用户交互以及与 iPhone 上的主应用进行通信。在 Objective - C 项目中,WatchKit 扩展是一个独立的 target。例如,当创建一个新的 WatchKit 应用项目时,Xcode 会自动生成一个 WatchKit 扩展 target。
    • 它包含了 Interface Controller 类,这些类负责管理 Watch 界面的不同屏幕。例如,以下是一个简单的 Interface Controller 类的声明:
#import <WatchKit/WatchKit.h>
#import <Foundation/Foundation.h>

@interface InterfaceController : WKInterfaceController
@end
  1. 主应用
    • 主应用运行在 iPhone 上,它可以为 WatchKit 扩展提供数据、执行复杂的计算任务等。主应用和 WatchKit 扩展之间通过 Watch Connectivity 框架进行通信。例如,如果 Watch 应用需要获取用户的位置信息,由于 Apple Watch 本身获取位置信息的能力有限,此时可以通过主应用在 iPhone 上获取位置信息,然后传递给 WatchKit 扩展。
    • 在主应用中,可以通过以下方式向 WatchKit 扩展发送消息:
#import <WatchConnectivity/WatchConnectivity.h>

@interface ViewController () <WCSessionDelegate>

@property (nonatomic, strong) WCSession *session;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    if ([WCSession isSupported]) {
        self.session = [WCSession defaultSession];
        self.session.delegate = self;
        [self.session activateSession];
    }
}

- (void)sendMessageToWatch {
    NSDictionary *message = @{@"key": @"value"};
    [self.session sendMessage:message replyHandler:^(NSDictionary *replyMessage) {
        // 处理回复消息
    } errorHandler:^(NSError *error) {
        // 处理错误
    }];
}

@end
  1. WatchKit 框架组件
    • WKInterfaceObject:这是 WatchKit 中所有界面元素的基类。像 WKInterfaceLabelWKInterfaceButton 等都是它的子类。通过这些子类,开发者可以在 Watch 界面上添加文本标签、按钮等元素。例如,添加一个文本标签:
@property (nonatomic, weak) IBOutlet WKInterfaceLabel *myLabel;
  • WKInterfaceController:管理 Watch 应用的屏幕界面。它负责加载界面、处理界面上的交互事件等。例如,当用户点击按钮时,可以在 WKInterfaceController 的方法中进行响应:
- (IBAction)buttonTapped {
    // 按钮点击处理逻辑
}

二、Apple Watch 界面开发

(一)界面布局

  1. 尺寸和分辨率
    • Apple Watch 有不同的尺寸,主要分为 38mm 和 42mm(初代),以及后续的 40mm 和 44mm 等。在设计界面布局时,需要考虑这些不同尺寸的适配。WatchKit 提供了自动布局机制,类似于 iOS 开发中的 Auto Layout,但针对 Apple Watch 的屏幕特点进行了优化。
    • 例如,在设计一个包含按钮和标签的界面时,可以通过设置界面元素的相对位置和大小来实现适配。假设要将一个按钮放在屏幕底部,并且距离底部有一定的间距,可以在 Interface Builder 中设置按钮的 Bottom Space to Container 约束:
    • 在 Objective - C 代码中,也可以通过代码来动态调整界面元素的位置和大小。例如,根据屏幕尺寸来调整标签的字体大小:
CGSize screenSize = [WKInterfaceDevice currentDevice].screenBounds.size;
if (screenSize.height == 340) {
    [self.myLabel setFont:[UIFont systemFontOfSize:14]];
} else {
    [self.myLabel setFont:[UIFont systemFontOfSize:16]];
}
  1. 布局容器
    • WKInterfaceGroup:它是一个容器类,可以包含多个界面元素,方便对一组元素进行统一的布局管理。例如,可以将一组按钮放在一个 WKInterfaceGroup 中,然后对这个组进行居中对齐等操作。在 Interface Builder 中,可以直接拖入一个 WKInterfaceGroup,然后将按钮等元素拖入其中。
    • WKInterfaceTable:用于在 Watch 界面上显示列表。如果应用需要展示一系列的数据,如联系人列表、待办事项列表等,就可以使用 WKInterfaceTable。首先在 Interface Builder 中拖入一个 WKInterfaceTable,然后创建一个自定义的 Table Row Controller 类。
    • 以下是创建一个简单的 WKInterfaceTable 的代码示例:
@property (nonatomic, weak) IBOutlet WKInterfaceTable *myTable;

// 在 interface controller 的初始化方法中设置表格行数
- (void)awakeWithContext:(id)context {
    [super awakeWithContext:context];
    [self.myTable setNumberOfRows:5 withRowType:@"TableRowController"];
}

// 配置表格行的数据
- (void)table:(WKInterfaceTable *)table willDisplayCell:(id)cell forRowAtIndex:(NSInteger)row {
    TableRowController *rowController = (TableRowController *)cell;
    [rowController.label setText:[NSString stringWithFormat:@"Row %ld", (long)row]];
}

(二)界面元素

  1. 标签(WKInterfaceLabel)
    • 标签用于在 Watch 界面上显示文本信息。可以设置文本的内容、字体、颜色等属性。例如,创建一个标签并设置其文本和字体:
@property (nonatomic, weak) IBOutlet WKInterfaceLabel *welcomeLabel;

- (void)awakeWithContext:(id)context {
    [super awakeWithContext:context];
    [self.welcomeLabel setText:@"Welcome to my Watch App"];
    [self.welcomeLabel setFont:[UIFont systemFontOfSize:16 weight:UIFontWeightMedium]];
}
  1. 按钮(WKInterfaceButton)
    • 按钮是用户交互的重要元素。可以为按钮添加点击事件处理方法。例如,创建一个按钮并处理其点击事件:
@property (nonatomic, weak) IBOutlet WKInterfaceButton *actionButton;

- (void)awakeWithContext:(id)context {
    [super awakeWithContext:context];
    [self.actionButton setTitle:@"Click me"];
}

- (IBAction)actionButtonTapped {
    // 处理按钮点击逻辑,比如显示一个提示信息
    [self presentAlertControllerWithTitle:@"Button Clicked" message:@"You clicked the button" preferredStyle:WKAlertControllerStyleAlert actions:@[[WKAlertAction actionWithTitle:@"OK" style:WKAlertActionStyleDefault handler:nil]]];
}
  1. 图像(WKInterfaceImage)
    • 图像元素用于在 Watch 界面上显示图片。可以从应用的资源中加载图片,也可以通过网络下载图片并显示。例如,从资源中加载一张图片:
@property (nonatomic, weak) IBOutlet WKInterfaceImage *myImage;

- (void)awakeWithContext:(id)context {
    [super awakeWithContext:context];
    [self.myImage setImageNamed:@"myImageName"];
}

如果要从网络加载图片,可以使用 NSURLSession 等网络请求框架,然后将下载的图片数据设置给 WKInterfaceImage。以下是一个简单的示例:

NSURL *imageURL = [NSURL URLWithString:@"http://example.com/image.jpg"];
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:imageURL completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    if (!error && data) {
        UIImage *image = [UIImage imageWithData:data];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.myImage setImage:image];
        });
    }
}];
[task resume];

三、用户交互处理

(一)手势识别

  1. 点击手势
    • 在 WatchKit 中,按钮本身就默认处理了点击手势。但对于其他界面元素,如 WKInterfaceGroupWKInterfaceImage 等,如果需要处理点击手势,可以通过添加 WKInterfaceObjectaddGestureRecognizer: 方法来实现。例如,为一个 WKInterfaceGroup 添加点击手势:
@property (nonatomic, weak) IBOutlet WKInterfaceGroup *myGroup;

- (void)awakeWithContext:(id)context {
    [super awakeWithContext:context];
    WKTapGestureRecognizer *tapGesture = [[WKTapGestureRecognizer alloc] initWithTarget:self action:@selector(groupTapped)];
    [self.myGroup addGestureRecognizer:tapGesture];
}

- (void)groupTapped {
    // 处理组的点击逻辑
    [self presentAlertControllerWithTitle:@"Group Tapped" message:@"You tapped the group" preferredStyle:WKAlertControllerStyleAlert actions:@[[WKAlertAction actionWithTitle:@"OK" style:WKAlertActionStyleDefault handler:nil]]];
}
  1. 长按手势
    • 可以为界面元素添加长按手势识别。例如,为一个标签添加长按手势:
@property (nonatomic, weak) IBOutlet WKInterfaceLabel *myLabel;

- (void)awakeWithContext:(id)context {
    [super awakeWithContext:context];
    WKLongPressGestureRecognizer *longPressGesture = [[WKLongPressGestureRecognizer alloc] initWithTarget:self action:@selector(labelLongPressed)];
    [self.myLabel addGestureRecognizer:longPressGesture];
}

- (void)labelLongPressed {
    // 处理标签长按逻辑,比如显示更多操作选项
    [self presentMenuWithItems:@[[WKMenuItem menuItemWithTitle:@"Option 1" action:@selector(option1Action)],[WKMenuItem menuItemWithTitle:@"Option 2" action:@selector(option2Action)]]];
}

- (void)option1Action {
    // 处理选项1的逻辑
}

- (void)option2Action {
    // 处理选项2的逻辑
}

(二)菜单交互

  1. 创建菜单
    • 可以为界面元素创建上下文菜单,提供更多的操作选项。通过 presentMenuWithItems: 方法来显示菜单。例如,为一个按钮创建菜单:
@property (nonatomic, weak) IBOutlet WKInterfaceButton *menuButton;

- (void)awakeWithContext:(id)context {
    [super awakeWithContext:context];
    [self.menuButton setTitle:@"Show Menu"];
}

- (IBAction)menuButtonTapped {
    [self presentMenuWithItems:@[[WKMenuItem menuItemWithTitle:@"Action 1" action:@selector(action1)],[WKMenuItem menuItemWithTitle:@"Action 2" action:@selector(action2)]]];
}

- (void)action1 {
    // 处理动作1的逻辑
    [self presentAlertControllerWithTitle:@"Action 1" message:@"You selected Action 1" preferredStyle:WKAlertControllerStyleAlert actions:@[[WKAlertAction actionWithTitle:@"OK" style:WKAlertActionStyleDefault handler:nil]]];
}

- (void)action2 {
    // 处理动作2的逻辑
    [self presentAlertControllerWithTitle:@"Action 2" message:@"You selected Action 2" preferredStyle:WKAlertControllerStyleAlert actions:@[[WKAlertAction actionWithTitle:@"OK" style:WKAlertActionStyleDefault handler:nil]]];
}
  1. 菜单样式和定制
    • 可以设置菜单的样式,如菜单项的标题颜色、背景颜色等。虽然 WatchKit 提供的定制选项相对有限,但可以通过一些技巧来实现特定的效果。例如,可以通过修改全局的外观设置来影响菜单的样式:
[[WKInterfaceController currentPage] setBackgroundColor:[UIColor lightGrayColor]];

这会改变整个界面包括菜单所在背景的颜色。

四、数据处理与存储

(一)与主应用的数据交互

  1. Watch Connectivity 框架
    • 如前文所述,主应用和 WatchKit 扩展通过 Watch Connectivity 框架进行通信。除了发送简单的消息,还可以传输文件、共享数据等。例如,主应用向 WatchKit 扩展传输一个文件:
    • 在主应用中:
NSURL *fileURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"example" ofType:@"txt"]];
[self.session transferFile:fileURL metadata:nil];
  • 在 WatchKit 扩展中接收文件:
- (void)session:(WCSession *)session didReceiveFile:(WCSessionFile *)file {
    NSData *fileData = [NSData dataWithContentsOfURL:file.fileURL];
    // 处理接收到的文件数据
}
  1. 共享数据存储
    • 可以使用 NSUserDefaults 来在主应用和 WatchKit 扩展之间共享简单的数据。例如,在主应用中保存一个用户设置:
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:@"value" forKey:@"key"];
[userDefaults synchronize];
  • 在 WatchKit 扩展中读取这个设置:
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSString *value = [userDefaults objectForKey:@"key"];

(二)Watch 本地数据存储

  1. 文件存储
    • Apple Watch 提供了本地文件存储的功能。可以使用 NSFileManager 来进行文件的创建、读取、写入等操作。例如,创建一个新文件并写入数据:
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *documentsURL = [[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] firstObject];
NSURL *fileURL = [documentsURL URLByAppendingPathComponent:@"example.txt"];
NSString *content = @"This is some sample content";
NSError *error;
[content writeToURL:fileURL atomically:YES encoding:NSUTF8StringEncoding error:&error];
if (error) {
    // 处理错误
}
  1. Core Data 存储
    • 虽然 Apple Watch 的资源有限,但对于一些较为复杂的数据管理需求,也可以使用 Core Data。首先需要在 WatchKit 扩展的 target 中添加 Core Data 框架。然后创建数据模型,定义实体、属性等。
    • 以下是一个简单的 Core Data 示例,创建一个新的实体并保存数据:
// 获取 Core Data 堆栈
NSManagedObjectContext *context = [(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];

// 创建新实体
NSEntityDescription *entity = [NSEntityDescription entityForName:@"MyEntity" inManagedObjectContext:context];
NSManagedObject *newObject = [[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:context];

// 设置实体属性
[newObject setValue:@"Some Value" forKey:@"myAttribute"];

// 保存上下文
NSError *saveError;
if (![context save:&saveError]) {
    // 处理保存错误
}

五、高级功能开发

(一)复杂界面和动画

  1. 自定义界面过渡
    • WatchKit 提供了一些默认的界面过渡效果,如 pushControllerWithName:context: 方法实现的页面推送过渡。但对于更复杂的自定义过渡效果,可以通过组合多个界面元素的显示和隐藏以及动画来实现。例如,创建一个类似淡入淡出的过渡效果:
    • 假设界面上有两个 WKInterfaceGroup,一个是当前显示的 currentGroup,另一个是即将显示的 nextGroup
[nextGroup setAlpha:0.0];
[nextGroup setHidden:NO];
[UIView animateWithDuration:0.3 animations:^{
    [currentGroup setAlpha:0.0];
    [nextGroup setAlpha:1.0];
} completion:^(BOOL finished) {
    [currentGroup setHidden:YES];
}];
  1. 动画效果
    • 可以为界面元素添加动画效果,如淡入淡出、旋转、缩放等。例如,为一个按钮添加旋转动画:
@property (nonatomic, weak) IBOutlet WKInterfaceButton *animatedButton;

- (void)startButtonAnimation {
    [self.animatedButton startAnimatingWithImagesInRange:NSMakeRange(0, 1) duration:2 repeatCount:0];
    CGAffineTransform rotationTransform = CGAffineTransformMakeRotation(M_PI);
    [self.animatedButton setTransform:rotationTransform];
}

(二)传感器和健康数据

  1. 运动传感器
    • Apple Watch 配备了运动传感器,如加速度计和陀螺仪。可以使用 Core Motion 框架来获取这些传感器的数据。例如,获取加速度计数据:
#import <CoreMotion/CoreMotion.h>

@property (nonatomic, strong) CMMotionManager *motionManager;

- (void)awakeWithContext:(id)context {
    [super awakeWithContext:context];
    self.motionManager = [[CMMotionManager alloc] init];
    if (self.motionManager.isAccelerometerAvailable) {
        self.motionManager.accelerometerUpdateInterval = 0.1;
        [self.motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue currentQueue] withHandler:^(CMAccelerometerData * _Nullable accelerometerData, NSError * _Nullable error) {
            if (!error) {
                CMAcceleration acceleration = accelerometerData.acceleration;
                // 处理加速度数据
            }
        }];
    }
}
  1. 健康数据
    • 要访问健康数据,需要使用 HealthKit 框架。首先在项目中添加 HealthKit 框架,然后请求用户授权。例如,读取用户的步数数据:
#import <HealthKit/HealthKit.h>

@property (nonatomic, strong) HKHealthStore *healthStore;

- (void)awakeWithContext:(id)context {
    [super awakeWithContext:context];
    self.healthStore = [[HKHealthStore alloc] init];
    HKObjectType *stepCountType = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
    NSArray *healthKitTypesToRead = @[stepCountType];
    [self.healthStore requestAuthorizationToShareTypes:nil readTypes:[NSSet setWithArray:healthKitTypesToRead] completion:^(BOOL success, NSError * _Nullable error) {
        if (success) {
            // 授权成功,读取步数数据
            HKQuantityType *stepCountType = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
            NSDate *startDate = [NSDate dateWithTimeIntervalSinceNow:-24 * 60 * 60];
            HKQuery *query = [[HKStatisticsQuery alloc] initWithQuantityType:stepCountType quantitySamplePredicate:[HKQuery predicateForSamplesWithStartDate:startDate endDate:[NSDate date] options:HKQueryOptionNone] statisticsOptions:HKStatisticsOptionCumulativeSum completionHandler:^(HKStatisticsQuery * _Nonnull query, HKStatistics * _Nullable result, NSError * _Nullable error) {
                if (!error && result) {
                    HKQuantity *quantity = [result sumQuantity];
                    double stepCount = [quantity doubleValueForUnit:[HKUnit countUnit]];
                    // 处理步数数据
                }
            }];
            [self.healthStore executeQuery:query];
        } else {
            // 处理授权失败
        }
    }];
}