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

Objective-C中的Widgets与Today视图开发

2022-09-066.6k 阅读

一、Widgets概述

Widgets(小部件)在现代移动应用开发中扮演着重要角色,它允许用户在不打开应用主程序的情况下,快速获取关键信息或执行一些常用操作。在iOS系统中,Widgets为用户提供了一种便捷的方式来与应用进行交互,尤其是在Today视图(通知中心的扩展部分)中展示相关内容,大大提升了用户体验。

Widgets本质上是一种特殊的应用扩展,它们遵循特定的扩展编程模型。在Objective - C中开发Widgets,开发者需要深入理解iOS的扩展机制以及相关的框架和接口。与常规应用开发不同,Widgets有其独特的生命周期、资源限制和用户交互规则。

Widgets的设计初衷是为了提供简洁、高效的信息展示和操作入口。例如,天气应用的Widget可以直接在Today视图中显示当前的天气状况、温度等关键信息;日历应用的Widget能展示当天的日程安排。这使得用户无需打开应用,就能快速获取所需信息,节省时间并提高操作效率。

二、Today视图简介

Today视图是iOS通知中心的一部分,它位于通知中心的“今天”标签页。用户可以通过从屏幕顶部向下滑动来访问通知中心,然后切换到Today视图。

Today视图的主要作用是为用户提供个性化的信息汇总和快速操作入口。它整合了来自不同应用的Widgets,用户可以根据自己的需求添加或移除这些Widgets,以定制化自己的Today视图。对于开发者而言,将应用的关键功能或信息以Widget的形式展示在Today视图中,能够增加应用的曝光度和用户使用频率。

从技术角度看,Today视图中的Widgets运行在一个独立的进程空间中,与主应用程序相互隔离。这意味着Widgets需要通过特定的机制与主应用进行数据交互,同时要遵循iOS系统为其设定的资源限制,如内存使用上限等。

三、创建Objective - C的Widget项目

  1. 创建新的Widget扩展
    • 打开Xcode,创建一个新的iOS项目。在模板选择界面,选择“Single View App”作为项目模板,填写项目名称、组织标识符等信息后点击“Next”。
    • 项目创建完成后,在项目导航器中右键点击项目名称,选择“New Target”。在弹出的窗口中,选择“Today Extension”,然后点击“Next”。
    • 为Today Extension填写相关信息,如产品名称等,点击“Finish”。此时,Xcode会为你创建一个基本的Widget项目结构。
  2. Widget项目结构解析
    • MainInterface.storyboard:这是Widget的界面设计文件,类似于主应用的Storyboard。在这里,你可以通过拖拽界面元素来设计Widget的外观。
    • TodayViewController.m:这个文件包含了Widget视图控制器的实现代码。在这个类中,你需要处理Widget的生命周期事件、界面更新以及与主应用的数据交互等逻辑。
    • Info.plist:Widget的配置文件,在这里可以设置Widget的各种属性,如扩展的显示名称、支持的设备方向等。
  3. 基本代码示例TodayViewController.m文件中,以下是一个简单的示例代码,用于在Widget中显示一个标签:
#import "TodayViewController.h"
#import <NotificationCenter/NotificationCenter.h>

@interface TodayViewController () <NCWidgetProviding>
@property (nonatomic, strong) UILabel *widgetLabel;
@end

@implementation TodayViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.widgetLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 10, self.view.bounds.size.width - 20, 30)];
    self.widgetLabel.text = @"Hello, Widget!";
    [self.view addSubview:self.widgetLabel];
}

- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler {
    // Perform any setup necessary in order to update the view.
    
    // If an error is encountered, use NCUpdateResultFailed
    // If there's no update required, use NCUpdateResultNoData
    // If there's an update, use NCUpdateResultNewData
    
    completionHandler(NCUpdateResultNoData);
}

@end

在上述代码中: - viewDidLoad方法用于初始化Widget的界面,创建并添加了一个显示“Hello, Widget!”的标签。 - widgetPerformUpdateWithCompletionHandler方法是Widget的更新回调,在这里开发者可以进行数据更新逻辑,并通过completionHandler告知系统更新结果。

四、Widget界面设计

  1. 使用Interface Builder
    • MainInterface.storyboard中,你可以像设计主应用界面一样拖拽各种UI元素到Widget视图中。例如,你可以添加按钮、文本框、图像视图等。
    • 布局方面,可以使用Auto Layout来确保Widget在不同设备屏幕尺寸上都能正确显示。选择要设置布局约束的视图,然后通过Xcode的布局菜单或快捷键来添加约束。例如,要使一个按钮水平居中并距离底部一定距离,可以先选择按钮,然后点击“Editor” -> “Pin” -> “Horizontal Center in Container”,再点击“Editor” -> “Pin” -> “Bottom Space to Container”,并设置合适的距离值。
  2. 动态更新界面 Widgets通常需要根据实时数据动态更新界面。在Objective - C中,可以通过以下方式实现:
    • TodayViewController.m中定义属性来保存需要显示的数据。例如,如果要显示当前时间,可以定义一个NSDate属性。
@property (nonatomic, strong) NSDate *currentDate;
- 在`widgetPerformUpdateWithCompletionHandler`方法中更新数据,并根据新数据更新界面。例如:
- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler {
    self.currentDate = [NSDate date];
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateFormat:@"HH:mm:ss"];
    self.widgetLabel.text = [formatter stringFromDate:self.currentDate];
    completionHandler(NCUpdateResultNewData);
}

在上述代码中,获取当前时间并格式化为“HH:mm:ss”的字符串,然后更新标签的文本内容。

五、Widget与主应用的数据交互

  1. 使用App Groups
    • 配置App Groups:首先,在Xcode的项目设置中,为应用和Widget扩展启用App Groups。在项目导航器中选择项目,然后在“Capabilities”选项卡中,找到“App Groups”并开启它。点击“+”号添加一个新的App Group,格式通常为“group.com.yourcompany.yourapp”。
    • 在主应用中写入数据:在主应用中,可以使用NSUserDefaults结合App Group来存储数据。例如:
NSUserDefaults *sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.yourcompany.yourapp"];
[sharedDefaults setObject:@"Some data" forKey:@"widgetDataKey"];
[sharedDefaults synchronize];
- **在Widget中读取数据**:在Widget的`TodayViewController.m`中读取数据:
NSUserDefaults *sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.yourcompany.yourapp"];
NSString *dataFromApp = [sharedDefaults objectForKey:@"widgetDataKey"];
if (dataFromApp) {
    self.widgetLabel.text = dataFromApp;
}
  1. 使用URL Scheme
    • 定义URL Scheme:在主应用的Info.plist文件中,添加一个CFBundleURLTypes数组。在数组中添加一个字典,设置CFBundleURLName为自定义的名称,CFBundleURLSchemes为自定义的URL Scheme,例如“yourappwidget”。
    • 在Widget中启动主应用并传递数据:在Widget中,可以使用UIApplicationopenURL:options:completionHandler:方法来启动主应用并传递数据。例如:
NSURL *url = [NSURL URLWithString:@"yourappwidget://?data=SomeValue"];
if ([[UIApplication sharedApplication] canOpenURL:url]) {
    [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
}
- **在主应用中接收数据**:在主应用的`AppDelegate.m`中,实现`application:openURL:options:`方法来接收数据:
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
    NSString *data = [[url query] componentsSeparatedByString:@"="].lastObject;
    // 处理接收到的数据
    return YES;
}

六、Widget的生命周期

  1. 初始化阶段
    • init方法:这是TodayViewController的初始化方法,类似于主应用视图控制器的初始化。在这里可以进行一些基本的属性初始化等操作。
    • awakeFromNib方法:当视图控制器从nib文件加载后调用。在Widget开发中,如果使用了MainInterface.storyboard,可以在这个方法中进行一些与界面相关的初始化设置,比如为控件添加初始值等。
  2. 视图加载与显示阶段
    • viewDidLoad方法:这个方法在视图加载到内存后调用,是进行界面布局和初始化操作的主要场所。如前面代码示例中,在这个方法中创建并添加了显示标签。
    • viewWillAppear:方法:在视图即将显示时调用。可以在此方法中进行一些数据预加载操作,以确保视图显示时数据已准备好。
    • viewDidAppear:方法:视图已经显示在屏幕上后调用。如果需要在视图显示后执行一些动画或统计操作,可以在这个方法中实现。
  3. 更新阶段
    • widgetPerformUpdateWithCompletionHandler方法:系统会定期调用这个方法来询问Widget是否有更新。开发者需要在此方法中检查数据是否有变化,如果有变化则更新界面,并通过completionHandler告知系统更新结果。
  4. 内存管理与销毁阶段
    • viewWillDisappear:方法:在视图即将从屏幕上移除时调用。可以在此方法中进行一些资源清理操作,如取消网络请求等。
    • viewDidDisappear:方法:视图已经从屏幕上移除后调用。此时可以进行一些更彻底的资源释放操作。
    • dealloc方法:当TodayViewController对象即将被销毁时调用。在这里需要释放所有引用的资源,如释放图片占用的内存等。

七、优化Widget性能

  1. 减少资源占用
    • 内存管理:Widgets运行在有限的内存环境中,因此要严格控制内存使用。避免在Widget中创建大量的临时对象或加载过大的图片。例如,如果需要显示图片,尽量使用合适尺寸的图片,避免加载原图导致内存占用过高。可以使用UIImageimageWithContentsOfFile:方法从文件系统加载图片,而不是使用imageNamed:方法,因为后者会将图片缓存到内存中,可能导致内存占用不断增加。
    • CPU使用:避免在主线程中执行复杂的计算任务。如果需要进行一些数据处理,可以使用NSOperationQueue将任务放到后台线程执行。例如,假设有一个计算任务,需要对一组数据进行排序,可以这样实现:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
    NSArray *dataArray = @[@5, @3, @7, @1];
    NSArray *sortedArray = [dataArray sortedArrayUsingSelector:@selector(compare:)];
    // 将排序后的数据传递到主线程更新界面
    dispatch_async(dispatch_get_main_queue(), ^{
        self.widgetLabel.text = [sortedArray description];
    });
}];
  1. 优化更新频率
    • 合理设置更新策略:在widgetPerformUpdateWithCompletionHandler方法中,不要每次都进行更新操作,而是要根据实际数据变化情况来决定是否更新。例如,可以记录上一次更新的时间和数据版本号,只有当数据版本号发生变化或者距离上次更新时间超过一定阈值时才进行更新。
@property (nonatomic, strong) NSDate *lastUpdateDate;
@property (nonatomic, NSInteger) dataVersion;

- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler {
    NSDate *now = [NSDate date];
    NSTimeInterval timeSinceLastUpdate = [now timeIntervalSinceDate:self.lastUpdateDate];
    if (timeSinceLastUpdate > 60 || self.dataVersion != [self getCurrentDataVersion]) {
        // 进行数据更新和界面更新操作
        self.lastUpdateDate = now;
        self.dataVersion = [self getCurrentDataVersion];
        completionHandler(NCUpdateResultNewData);
    } else {
        completionHandler(NCUpdateResultNoData);
    }
}

- (NSInteger)getCurrentDataVersion {
    // 模拟获取数据版本号的方法
    return arc4random() % 10;
}
- **使用缓存**:对于一些不经常变化的数据,可以使用缓存来减少更新次数。例如,如果Widget需要显示一篇文章摘要,而文章内容很少更新,可以将文章摘要缓存到本地,每次更新时先检查缓存数据是否有效,如果有效则直接使用缓存数据,无需重新获取。

八、处理用户交互

  1. 按钮点击等基本交互
    • 在Widget界面上添加按钮等交互控件后,可以通过创建IBAction方法来处理点击事件。在MainInterface.storyboard中,将按钮的点击事件连接到TodayViewController.m中的IBAction方法。例如:
- (IBAction)buttonTapped:(id)sender {
    self.widgetLabel.text = @"Button Tapped!";
}
  1. 传递交互数据到主应用
    • 如果需要将用户在Widget中的交互数据传递到主应用,可以结合前面提到的App Groups或URL Scheme来实现。例如,使用App Groups,当用户在Widget中点击按钮后,可以将点击状态保存到App Groups的NSUserDefaults中,主应用在合适的时机读取这个状态。
- (IBAction)buttonTapped:(id)sender {
    NSUserDefaults *sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.yourcompany.yourapp"];
    [sharedDefaults setBool:YES forKey:@"widgetButtonTapped"];
    [sharedDefaults synchronize];
}
- 在主应用中读取数据:
NSUserDefaults *sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.yourcompany.yourapp"];
BOOL buttonTapped = [sharedDefaults boolForKey:@"widgetButtonTapped"];
if (buttonTapped) {
    // 处理按钮点击后的逻辑
}

九、测试与发布Widget

  1. 本地测试
    • 在Xcode中,选择Widget的Scheme(可以通过Xcode左上角的Scheme选择菜单切换到Widget的Scheme)。然后点击运行按钮,Xcode会启动模拟器并打开通知中心,显示Widget。
    • 可以在模拟器中模拟各种操作,如旋转屏幕、与Widget进行交互等,以测试Widget在不同情况下的表现。同时,可以通过Xcode的调试工具,如断点调试、日志输出等,来排查代码中的问题。
  2. 提交到App Store
    • 准备素材:需要为Widget准备相关的截图和描述信息。截图应展示Widget在不同设备和状态下的外观,描述信息要清晰说明Widget的功能和特点。
    • 配置项目:确保Widget的Info.plist文件中各项配置正确,包括扩展的显示名称、支持的设备等。同时,要检查代码中是否有不符合App Store审核指南的内容,如隐私政策声明等。
    • 提交审核:在Xcode中,选择“Product” -> “Archive”来创建应用的归档文件。归档完成后,在“Organizer”窗口中选择归档文件,点击“Submit to App Store”。按照App Store Connect的提示逐步完成提交过程,等待审核通过后,用户就可以在App Store中下载并使用包含Widget的应用了。

通过以上步骤和技术要点,开发者可以在Objective - C中成功开发出功能丰富、性能优良的Widgets,并将其集成到Today视图中,为用户提供更好的使用体验。在开发过程中,要不断优化代码,遵循苹果的设计规范和开发指南,以确保Widget的质量和稳定性。