欢迎你阅读针对Windows Bridge for iOS上手系列博客的第一篇文章。Windows Bridge for iOS 是一个开发源代码项目,你可以用它来创建Windows平台上统一的(UWP)应用,这些app可以运行于Windows10设备之上,而它们所使用的却是iOS的API和Objective-C的代码。 
今天,我们将在Xcode中构建一个简单的待办事项列表应用并使用Windows Bridge for iOS 来将其带到Windows10中来,要保持将所有的代码都放到一个的代码库中,以利于项目在平台之间的完全可移植性。如果你想完全跟着整个过程一步一步走一遍,可能需要做如下准备: 一台运行Windows 10的PC,并且安装了Visual Studio 2015 和 Windows Bridge for iOS。你可以从Windows Dev Center下载Visual Studio,并且在这里的GitHub上找到bridge的最新版本。 一台运行 Mac OS X 10.11 操作系统的Mac电脑,并且安装了Xcode 7。如果你想要在实际的iOS设备上运行iOS项目,就还需要有一个付费的苹果开发者账户.
如果你没有一台 PC 的话,可以从 Windows Bridge for iOS 的网站上下载我们的一个预先构建好的评估版本的虚拟机。根据你偏爱使用的虚拟机环境现在对应包,然后你就可以在任何时间运行它了——这个包已经包含了 Windows10,Visual Studio2015 以及 iOS bridge。 如果你没有 Mac,但是又对在 Windows10 上使用 Objective-C 进行开发感到好奇的话,仍然可以去下载源代码,演练这整个过程并且在 Visual Studio 中编辑代码。 在 Xcode 中构建一个待办事项列表应用首先,下载初始的待办事项列表项目,可以在这里找到。打开 ToDo.xcodeproj 文件,然后让我们来看看项目的架构。 
在 Storyboard 编辑器中,我们有一个 UINavigationController 来充当根视图控制器,还有一个 UITableViewController 将作为我们的主屏界面来显示。因为整个应用就只有一个视图,所以导航控制器并不是必须得有的,不过如果你想体验一下进一步改进这个项目的过程,那还是为这个预留下空间比较好,这样就方便后面再进行扩展了。 我们已经构建了这个应用的大多数东西,因此唯一需要注意的就是“Clear” UIBarButtonItem,它已经与 TDTableViewController 中的 clearAllTodos: 建立了一个 IBAction 的 outlet 关联。 现在让我们来看看Xcode中左侧边栏中的类: TDItem – 这是我们用来持有待办事项列表项的数据结构。 TDTableViewController – 这就是我们应用的大多数业务逻辑的所在之处。这个类集成自 UITableViewController ,管理着创建新的待办事项列表项和展示正在处理和已经完成的待办事项这些功能。 TDTableViewCell – 这个类继承自 UITableViewCell ,并且为处理中和存档的待办事项都提供了布局。它使用了一个平移手势识别器来添加删除功能,并且还维持了一个到其当前所展示的 TDItem. 的引用。它的委托方式其父表格视图控制器,当一个格子被向左滑动(来进行待办事项删除)或者向右滑动(来进行待办事项的归档)时它就会被通知到。 TDInputTableViewCell – 这个类也是集成自UITableViewCell,用来展示添加新的待办事项的输入框。它类似于 TDTableViewCell, 其委托方是它的父表格视图控制器,当有新的待办事项被添加的时候会被通知到。 TDLabel – 最后是 TDLabel,它继承自 UILabel,并且简单提供了一个让其整个文本上有根厚删除线的机制。
继续并在Xcode中的iOS模拟器中运行app,你会看到app启动了并且运行得很漂亮: 
尝试一下添加一些待办事项,向右滑动来归档一个待办事项,还有向左滑动删除一个。如果你退出模拟器并重新启动,会发现列表小时了;在我们把app带到Windows上时,会去检查一下存储会话数据的方法。 现在将项目的目录复制到一个主驱动器上,并且在你的Windows虚拟机上打开。 (如果你使用的是Mac上的一个虚拟机,也可以将项目复制到一个Mac和Windows虚拟机都可以访问到的共享目录。) 接下来,让我们将Xcode项目弄到Visual Studio 解决方案中。 使用 vsimporter
在我们的 Windows 机器上,打开 winobjc 目录并导航至 winobjc/bin ,你会发现一个叫做 vsimporter. 的文件。Vsimporter 是一个命令行工具,它能将一个 Xcode 项目转换成一个 Visual Studio 解决方案。它会自动处理 Storyboards 和 Xibs,尽管目前 Visual Studio 还并没有一个 Storyboard 编辑器,因此对 Storyboard 的修改现在还都必须在 Mac 上进行。 (这就是我们为什么要事先会构建好大多数的布局。) 在一个独立的文件浏览器窗口中打开你的待办事项列表项目的目录。选择 File > Open command line prompt ,你会看见一个命令行窗口会显示出来。将 vsimporter 文件拖动到目录已经切换到winobjc/bin 目录的命令行窗口之上,你应该会看到起全路径会显示出来。让命令行窗口处在获得焦点的状态,然后敲下 Enter 键,然后再返回你的待办事项列表项目的目录,现在应该会包含一个新的 Visual Studio 解决方案文件。 使用 Visual Studio 和 iOS birdge 双击刚才创建的 Visual Studio 解决方案,Visual Studio 2015 就会启动。在侧边栏的 Visual Studio 解决方案浏览器里,你可以看到最顶层的解决方案文件,展看后就可以看到在 Xcode 中已经很熟悉的类结构。除开头文件在 Visual Studio 自己的目录中外,其他的结构都应该是一样的。 
按下“F5”运行程序,等待编译完成后,就成功运行了。 
我们用 Objective-C 写的的 iOS 应用就跑在了 Windows10 上。 你首先会注意到的是这个应用并没有合适的缩放比例。Win10 系统运行在多种不同设备上,要适应多种屏幕尺寸,为了保证良好的用户体验,你的应用需要对所允许设备的配置进行感知并反馈。为了做到这点,我们将为这个应用生成一个 Category,即 app 委托UIApplicationInitialStartupMode。 在解决方案浏览器中,双击 AppDelegate.m。 在最开头的 #import 底下,加上下面这些代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #ifdef WINOBJC
@implementation UIApplication (UIApplicationInitialStartupMode)
+ ( void ) setStartupDisplayMode:(WOCDisplayMode*)mode
{
mode.autoMagnification = TRUE;
mode.sizeUIWindowToFit = TRUE;
mode.fixedWidth = 0;
mode.fixedHeight = 0;
mode.magnification = 1.0;
}
@end
#endif
|
这里,我们使用了 #ifdef 和 #endif 预处理指令来检查 WINOBJC 符号是否已经定义了,加入我们能包含 Windows 的特殊代码。这样就保持了代码库的可移植性, 因为 Windows 的特殊代码在我们回头到 Xcode 中编辑并且在 iOS 上运行应用时会简单地被忽略掉。 对于这个 WOCDisplayMode 对象属性的完整描述(autoMagnification,sizeUIWindowToFit, fixedWidth 等等), 可以看看 GitHub上我们的项目维基的 SDK 一节的内容。 现在再次按下 F5 来运行应用,你会看到待办事项列表应用能正常的响应窗口尺寸了。继续来添加一些待办事项,然后—— 呃哦! 看起来我们发现了一个 bug: 
当你发现了一个不支持的 iOS API 调用时该怎么办稍微深挖一下,我们会快速地发现我们是在添加新的待办事项还有对他们进行归档时引发的问题。两种情况下,我们都使用了 UITableView的beginUpdates 和 endUpdates 实体方法调用,他们能让我们修改依赖的数据结构并插入和溢出表格视图中的行并保证整个过程的正确性。快速的看一眼运行时日志会显示出这些方法在 iOS bridge 中并不支持: 
那该怎么办呢? 首先,确保你会在 GitHub 上提交发现的这个问题。GitHub 是通我们团队保持沟通的最佳方式,以让我们了解你需要什么工具。如果你在 bridge 中找到了想要有却没有实现的 API,特性,或者是任何的问题,请让我们知道。 接下来,我们同样可以使用我们之前用来解决渲染问题的预处理指令来变通地处理这种情况。在 Visual Studio 中打开 TDTableViewController.m,我们来改改 toDoItemDeleted:, toDoItemCompleted: 还有 toDoItemAdded: 方法: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | - ( void )toDoItemDeleted:(id)todoItem
{
#ifdef WINOBJC
[_toDoItems removeObject:todoItem];
[self.tableView reloadData];
#else
NSUInteger index = [_toDoItems indexOfObject:todoItem];
[self.tableView beginUpdates];
[_toDoItems removeObject:todoItem];
[self.tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:index inSection:TODO_SECTION]]
withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
#endif
}
- ( void )toDoItemCompleted:(id)todoItem
{
#ifdef WINOBJC
[_toDoItems removeObject:todoItem];
[_completedItems insertObject:todoItem atIndex:0];
[self.tableView reloadData];
#else
NSUInteger index = [_toDoItems indexOfObject:todoItem];
[self.tableView beginUpdates];
[_toDoItems removeObject:todoItem];
[_completedItems insertObject:todoItem atIndex:0];
[self.tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:index inSection:TODO_SECTION]]
withRowAnimation:UITableViewRowAnimationLeft];
[self.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:0 inSection:COMPLETE_SECTION]]
withRowAnimation:UITableViewRowAnimationLeft];
[self.tableView endUpdates];
#endif
}
#pragma mark - TDInputTableViewCell delegate methods
- ( void )toDoItemAdded:(TDItem*) todoItem
{
#ifdef WINOBJC
[_toDoItems insertObject:todoItem atIndex:0];
[self.tableView reloadData];
#else
[self.tableView beginUpdates];
[_toDoItems insertObject:todoItem atIndex:0];
[self.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:0 inSection:TODO_SECTION]]
withRowAnimation:UITableViewRowAnimationTop];
[self.tableView endUpdates];
#endif
}
|
这个方法让我们可以轻松的在 Xcode 项目和 Visual Studio 解决方案中共享代码。当在 iOS 上运行应用的时候,我们还是使用 beginUpdates 和 endUpdates 来数据格子的插入和移除,但是在 Windows 上我们就只是简单的更新依赖的数据调用,并调用 reloadData,它会强制让整个表格视图重新渲染。 按下 F5,你的 app 运行应该不会报错了。
持久化数据
现在,待办事项列表 app 如果不能记忆你的添加的待办事项,那就并不能完全投入使用,当前我们的应用把所有的东西都存储在内存里面,每次你启动它都得从头开始。我们当然可以做得更好一点。 因为我们已经有了这样一个简单的使用场景,我们可以使用属性列表序列化来将待办事项存储在一个 .plist 文件中。这样,我们就可以在每次创建、删除或者归档一个待办事项时写入文件,并且在 app 加载时读取这个文件。(更加稳定的一个实现就是每次一条数据发生变化时就将相关的变化写入文件,而不是整个待办事项的清单,不过简单起见,我们还是会在所有的变化发生之后再全部写入文件。) 回到 TDTableViewController.m,在最底下添加下面这些方法: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | - ( void )writeToDosToDisk
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^( void ) {
NSMutableArray *allItems = [[NSMutableArray alloc] init];
[_toDoItems enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
TDItem *item = obj;
[allItems addObject:[item serialize]];
}];
[_completedItems enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
TDItem *item = obj;
[allItems addObject:[item serialize]];
}];
NSArray *directories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documents = [directories firstObject];
NSString *filePath = [documents stringByAppendingPathComponent:@"todos.plist"];
if ([allItems writeToFile:filePath atomically:YES]) {
NSLog(@"Successfully wrote to dos to disk.");
}
});
}
- ( void )readToDosFromDisk
{
NSArray *directories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documents = [directories firstObject];
NSString *filePath = [documents stringByAppendingPathComponent:@"todos.plist"];
NSArray *loadedToDos = [NSArray arrayWithContentsOfFile:filePath];
[loadedToDos enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSDictionary *dict = obj;
NSString * string = [[dict allKeys] firstObject];
BOOL complete = ((NSNumber*)[[dict allValues] firstObject]).boolValue;
TDItem *toDo = [TDItem todoItemWithText: string isComplete:complete];
if (toDo.completed) {
[_completedItems addObject:toDo];
}
else {
[_toDoItems addObject:toDo];
}
}];
[self.tableView reloadData];
}
|
为了在一个属性清单中存储我们自定义的 TDItem 对象,我们需要将其转换成一个 NSDictionary。幸运的是,我们的 TDItem 实现有一个 serialize 方法能做这件事儿并返回一个 NSDictionary,真是太巧了!
现在,我们只需要简单的更新一下 toDoItemDeleted:, toDoItemCompleted:,toDoItemAdded:, 和 clearAllTodos: 方法,在返回之前立即调用一下 [self writeToDosToDisk],并且在 viewDidLoad 的结尾向[self readToDosFromDisk]增加一个调用。 再次按下 F5 运行 app,待办事项现在就回在多次启动的时候回忆起之前东西了,这样你就不再会忘记要去杂货店买什么东西了。新的 app 能完全地在 Windows 和 iOS 之间移植,因此你可以在自己的 Mac 上打开原来的 XCode 项目文件,app 还是会像预期的那样运行。 准备好尝试你自己的应用了吗? 去 GitHub 下载 bridge 吧。 感谢陪伴! 你可以从我们的 GitHub 维基上下载到完整的待办事项列表 XCode 项目, 并对更多教程文章保持关注——对于 Windows Bridge for iOS 所具备可能性,我们还只是摸到了皮毛。
|