Welcome to the first in a series of hands-on blog posts for the Windows Bridge for iOS. The Windows Bridge for iOS is an open-source project that allows you to create Universal Windows Platform (UWP) apps that can run on Windows 10 devices using iOS APIs and Objective-C code.
Today, we’re going to build a simple to-do list app in Xcode and use the Windows Bridge for iOS to bring it over to Windows 10, keeping all of the code in a single codebase so the project is completely portable between platforms. If you want to follow the complete, end-to-end journey, you’ll need:
- A PC running Windows 10, with Visual Studio 2015 and the Windows Bridge for iOS installed. You can download Visual Studio from the Windows Dev Center and find the latest release of the bridge on GitHub here.
- A Mac running Mac OS X 10.11 with Xcode 7 installed. If you want to run the iOS project on an actual iOS device, you’ll also need a paid Apple Developer account.
If you don’t have a PC, you can download one of our pre-built evaluation virtual machines from the Windows Bridge for iOS website. Download the package for your preferred virtualization environment and you’ll be up and running in no time – the package already includes Windows 10, Visual Studio 2015 and the iOS bridge.
If you don’t have a Mac but are curious about developing in Objective-C on Windows 10, you’ll still be able to download the source code, go through the conversion process and edit the code in Visual Studio.
Building a to-do list app in Xcode
First, download the initial to-do list project, which can be found here. Open up the ToDo.xcodeproj file and let’s examine the project structure.
In the Storyboard editor, we have a single UINavigationController as our root view controller and a UITableViewController that will be our main screen. Since the whole app is a single view, a navigation controller isn’t strictly necessary, but it leaves room for extensibility if you would like to experiment with taking the project further.
We’ve built most of the app programmatically, so the only other item of note is the “Clear” UIBarButtonItem, which has an IBAction outlet in the clearAllTodos: method in TDTableViewController.
Now let’s take a look at the classes in the left sidebar in Xcode:
- TDItem – this is our barebones data structure for holding to-do list items.
- TDTableViewController – this is where most of the logic of our app lies. This class inherits from UITableViewController and manages creating new to-do items and displaying in-progress and completed to dos.
- TDTableViewCell – this class inherits from UITableViewCell and provides the layout for both in-progress and archived to dos. It uses a pan gesture recognizer to add the swiping functionality and keeps a reference to its currently displayed TDItem. Its delegate is its parent table view controller, which is notified when a cell is swiped left (to delete a to do) or right (to archive a to do).
- TDInputTableViewCell – this class also inherits from UITableViewCell and is used to display the input field for adding new to dos. Like TDTableViewCell, its delegate is its parent table view controller which is notified when a new to do is added.
- TDLabel – finally, TDLabel inherits from UILabel and simply provides a mechanism for having a thick strikethrough through its text.
Go ahead and run the app in the iOS simulator in Xcode, and you’ll see our app starts up and runs nicely:
Try adding a few to-do items and swiping right to archive an item and left to delete it. If you quit the simulator and relaunch, you’ll notice your list disappears; we’ll examine methods of persisting data across sessions once we bring the app over to Windows.
Now copy the project directory onto a thumb drive and open it on your Windows machine. (If you’re using a Mac with a virtual machine, you can also just copy the project to a shared directory that is accessible from both the Mac and Windows sides.)
Next, let’s turn our Xcode project into a Visual Studio solution.
Using vsimporter
On your Windows machine, open up the winobjc directory and navigate to winobjc/bin. Inside, you’ll find a file called vsimporter. Vsimporter is a command-line tool that turns an Xcode project file into a Visual Studio solution. It automatically handles Storyboards and Xibs, although Visual Studio does not currently have a Storyboard editor, so any changes to our Storyboard have to be made on the Mac side. (This is why we built most of the layout programmatically.)
In a separate window, open your to-do list project directory in file explorer. Select File>Open command line prompt and you’ll see a command line window appear. Drag the vsimporter file located in winobjc/bin on top of the command line window and you should see its full path appear. With the command line window in focus, hit Enter, and then return to your to-do list project directory, which should now contain a brand new Visual Studio solution file.
Using Visual Studio and the iOS bridge
Double click the new Visual Studio solution file that was just created and Visual Studio 2015 will launch. In the Visual Studio Solution Explorer sidebar, you’ll see the top-level solution file, which you can expand to see the familiar class structure we had in Xcode. Header files are stored in their own directory in Visual Studio, but otherwise the structure should look the same.
Hit F5 to run the app, wait for it to compile, and voila!
Our iOS app is running natively on Windows 10 using Objective-C.
The first thing you’ll notice is the app doesn’t scale properly. Windows 10 runs on a wide variety of form factors with different screen sizes, so to ensure a good user experience, your app should be aware of, and respond to, the configuration it’s being run on. To accomplish this, we’re going to create a Category for our app in our app delegate called UIApplicationInitialStartupMode.
In the Solution Explorer, double click AppDelegate.m. Beneath the very first #import, add the following code:
#ifdef WINOBJC @implementation UIApplication (UIApplicationInitialStartupMode) // Let WinObjC know how to render the app + (void) setStartupDisplayMode:(WOCDisplayMode*)mode { mode.autoMagnification = TRUE; mode.sizeUIWindowToFit = TRUE; mode.fixedWidth = 0; mode.fixedHeight = 0; mode.magnification = 1.0; } @end #endif
Here, we’re using the #ifdef and #endif preprocessor directives to check to see if the WINOBJC symbol is defined, giving us the ability to include Windows-specific code. This keeps the codebase portable, since the Windows-specific code will simply be ignored if we go back to Xcode and run the app on iOS.
For a full description of the properties of the WOCDisplayMode object (autoMagnification, sizeUIWindowToFit, fixedWidth, etc), see the Using the SDK section of our project wiki on GitHub.
Now hit F5 again to run the app and you should see the to-do list app properly and responsively render. Go ahead and add a few to dos and–
Uh oh! Looks like we found a bug:
What to do when you find unsupported iOS API calls
With a little digging, we quickly find that we hit bugs when adding new to dos and archiving them. In both cases, we’re using UITableView’s beginUpdates and endUpdates instance method calls, which allow us to edit the underlying data structure and insert and move around rows in our table view and guarantees the validity of the whole transaction. A quick look at the runtime log shows that these methods aren’t supported in the iOS bridge:
What to do?
First, make sure you file a bug on GitHub. GitHub is the best way to get in touch with our team to let us know what tools you need. If you find unimplemented APIs, features you’d like to see, or bugs anywhere in the bridge, please let us know.
Next, we can use the same preprocessor directives we used to fix the app rendering problems to create workarounds specifically for this use case. Open up TDTableViewController.m in Visual Studio and let’s tweak the toDoItemDeleted:, toDoItemCompleted:, and toDoItemAdded: methods:
- (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 }
This method lets us easily share code between an Xcode project and a Visual Studio solution. When running the app on iOS, we continue to use beginUpdates and endUpdates to manage inserting and moving cells, but on Windows we simply update the underlying data structure and call reloadData which forces the entire table view to rerender.
Hit F5 and your to-do list app should run without errors.
Persisting data
Now, a to-do list app isn’t all that much use if it can’t remember your to dos, and currently our app keeps everything in memory, so every time you launch it you have to start from scratch. We can do better.
Since we have such a simple use case, we can use property list serialization to store our to dos in a .plist file. This way, we can write out the file every time a to do is added, deleted or archived, and simply read the file on app load. (A more robust implementation would only write out the relevant change every time one is made, rather than the complete list of to dos, but for the sake of simplicity we’ll just write everything out after every change.)
Head back over to TDTableViewController.m and add the following methods at the bottom:
- (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]; }
In order to store our custom TDItem object in a property list, we’ll need to convert it into an NSDictionary. Luckily, our TDItem implementation has a serialize method that does exactly that and returns an NSDictionary. What a coincidence!
Now, we simply need to update our toDoItemDeleted:, toDoItemCompleted:, toDoItemAdded:, and clearAllTodos: methods to call [self writeToDosToDisk] right before returning and add a call to [self readToDosFromDisk] at the end of viewDidLoad.
Press F5 again to run your app and your to dos will now be remembered across launches, so you’ll never forget anything at the grocery store again. The new app is completely portable across Windows 10 and iOS, so you can open your old Xcode project file up on your Mac and the app will continue to function exactly as expected.
Ready to try out your own app? Head over to GitHub to download the bridge.
Thanks for following along! You can download the complete to-do list Xcode project from our GitHub wiki, and stay tuned for more tutorial posts – we’ve just scratched the surface of the possibilities with the Windows Bridge for iOS.