微软的官方教程.storyboard/.xib-less user interface design in Xamarin.Mac介绍了如何在Xamarin.Mac中不使用Storyboard和XIB。本文记录练习这个教程中遇到的一些问题和思考。
题外话
什么是Xamarin.Mac
在苹果的macOS上编写APP,官方支持两种语言:Swift和Objective-C。微软为了证明自己是跨平台APP的开发工具和服务提供商,收购了Xamarin并将其开源,使得开发者可以用C#来跨平台开发APP。
Xamarin的基础是Mono,一个开源的C#实现。Xamarin打包了不同平台原生的API供C#使用。比如,Xamarin.Mac打包了macOS上APP开发所需要的API,Xamarin.iOS打包了iOS上API开发所需的API,等等。如果使用这些Xamarin库开发的应用,基本上可以看出是原生的应用,只不过开发语言是C#,并且APP中需要打包Mono的Runtime。
C#和JavaScript一样,都是在ECMA标准化了的语言,所以可以有Mono这种开源实现。根据Can Xamarin.Mac work with .NET Core?的说法,Xamarin目前还不能使用微软自家的跨平台开源的.Net Core来替代Mono。
除了对原生的API提供直接的封装之外,Xamarin还提供Xamarin.Forms,一个跨平台的UI抽象层。基于Xamarin.Forms的代码可以不经修改就在不同平台编译允许。适合用来编写通用的,对平台依赖性不强的UI。The Good and The Bad of Xamarin Mobile Development是了解Xamarin的一篇好文章。
正题
关于Interface Builder
微软推出的Visual Studio for Mac 2017支持Xamarin的开发,个人使用免费。微软的文档中提供了一个例子:Hello, Mac,可以帮助你快速熟悉如何用Visual Studio for Mac开发macOS APP。Visual Studio支持调用macOS自身的Xcode中的Interface Builder来设计界面,然后用C#来处理IBOutlet和IBAction的处理逻辑。
在Mac上开发UI,可以使用三种方法,一)使用XIB,二)使用Storyboard,三)直接使用代码。三比较直接,一和二需要解释下。一和二其实类似,都是在Interface Builder里面使用图形化的辅助工具拖拉生成相应的UI描述文件,然后在APP启动时加载该描述文件并自动生成相应的UI。但是有一些问题是方法一能解决,但是二解决不了的,反之亦然。
网上有好多文章,讨论XIB和Storyboard之间的差别,以及如何不使用它们:
- iOS User Interfaces: Storyboards vs. NIBs vs. Custom Code
- What is the difference between xib and storyboard? Which one is better and why?
- Interface builder is great. Storyboards are not.
- Storyboards vs. the old XIB way
- How to write an NSViewController without interface builder on macOS.
- Creating a Cocoa application without NIB files
- IB Free: Living Without Interface Builder and Loving It
- Do expert applications use Storyboards for app design or do it programmatically?
- Why I Don’t Use Interface Builder
避免使用Interface Builder
Interface Builder当然很好,即可视化,又能够方便组织UI元素直接的关系。但是我比较偏向于直接在Xamarin中直接使用代码,一是不想在Visual Studio和Interface Builder间切换,操作不太方便;二是避免Interface Builder和Visual Studio同步过程中可能存在的bug;三是从长期看,虽然写代码前期慢了一点,但是可重用性高,而且可以帮助你加深对API的理解,长期看好处不小。 文章开头介绍的.storyboard/.xib-less user interface design in Xamarin.Mac告诉你如何不用Storyboard或者XIB,并且提供了一个例子:MacXibless (sample). 但是估计写文档的和提供例子的不是一个人,根据文档的描述会出现错误,但是运行例子则OK。下面会解释。
- 首先按照文档的描述,在Visual Studio中创建一个Cocoa APP项目,取名HelloMac。
- 根据文档的描述,删掉Main.storyboard。除此之外,还可以删掉ViewController.cs。
- 根据文档的描述,创建MainWindow.cs和MainWindowController.cs
- 注意要把namespace从MacXibless替换城HelloMac
- 根据文档的描述,修改AppDelegate.cs,为AppDelegate添加一个属性
private MainWindowController mainWindowController;
,并在其DidFinishLaunching
方法中添加下面两行用来生成窗口:mainWindowController = new MainWindowController ();
mainWindowController.Window.MakeKeyAndOrderFront (this);
到此,编译运行程序,发现HelloMac的APP图标出现了,但是却出错了
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Could not find a storyboard named 'Main' in bundle NSBundle </Users/mine/Projects/HelloMac/HelloMac/bin/Debug/HelloMac.app> (loaded)'
***
可惜那篇文档里面没有告诉你原因。其实原因在Xamarin remove storyboard。你需要在Info.plist
中把Main Interface
设空。
再编译运行,错误信息消失了,但是窗口依然没有出现。经过一番调查,发现原因是AppDelegate的DidFinishLaunching
没有被调用。你可以根据StackOverflow这篇帖子NSApplicationDelegate not working without Storyboard介绍的方法来调用AppDelegate的DidFinishLaunching
。但是这种Hack似乎太过了,所以我觉得还是用XIB来调用AppDelegate的DidFinishLaunching
。
虽然我不想使用XIB或者Storyboard来做界面,但是使用XIB来做菜单是一个不错的选择。在HelloMac中创建一个MainMenu,会自动命名成MainMenu.xib
。保存以后,就可以在Info.plist
中把Main Interface
设为下拉菜单中的MainMenu。
但是还没有完,我们需要在XIB做一下和AppDelegate相关的设置。双击MainMenu.xib
,在Interface Builder中打开,在屏幕正中会显示菜单的预览。然后在Interface Builder的菜单栏选择Editor->ShowDocumentOutline
,会在预览窗口的左边展开一个DocumentOutline栏。接着选择View->Libraries->Show Library
打开UI对象库,在里面找到Object
,并双击添加,可以看到Object
出现在左边的DocumentOutline栏中。将其选中。使用快捷键ALT + CMD + 3
打开Identity Inspector,然后将其的class设置为AppDelegate。可以看到DocumentOutline栏中Object
自动变成了App Delegate
。最后,有一个至关重要的步骤,在DocumentOutline栏中按住CTRL并点击Placeholder中的File's Owner
,拖出一根线到App Delegate
。使用快捷键ALT + CMD + 6
打开Connection Inspector,可以看到File's Owner
和App Delegate
连接在一起了。保存并返回Visual Studio,然后重新编译并运行HelloMac,可以看到窗口出现了。
上述过程可以部分参考: How to create a macOS Project without Storyboards in Xcode 8 以及Creating and maintaining windows in Xcode。 关于
File's Owner
,可以参考What is the File’s Owner (in Interface builder)?的解释。
其他参考
- macOS View Controllers Tutorial | raywenderlich.com
- Xib Awakening: A Uniform Way to Load All Xibs
- steps for creating UIScrollView with Interface Builder
- 5 approach to load UIView from Xib
- Difference between NSWindowController Vs NSViewController