![5a908347b5b6673cbb0c35c3b94d5aac.png](https://i-blog.csdnimg.cn/blog_migrate/c7ba7378adf90a280023d92ead484a4c.jpeg)
程序中跟目录模块相似的,也是通过使用Outline View组件显示内容的模块有:备注、搜索、角色、符号、字典等其它五个。设计完成本模块后将不再对其它模块进行说明,一切请查看Github上代码。目录模块的主要功能有:显示目录、目录项的增删改及排序,当前目录只支持章节两级结构。其主要代码将放在ViewController.swift中,我们需要留意的是其事件的处理与操作的流程。在用Swift开发macOS程序, 四、界面设计中,我们是实现过目录组件,希望你没有忘记,今天我们将拿过来用用。
进行到这里,我们将需要处理较复杂的业务逻辑。但要放心是,想信我们,一切都会不是问题。处理这类问题,首先要了解我们的业务,其次需要了我们用来处理业务的对象,如Outline View。像打仗一样,要做到“知己知彼“,方能“百战不殆”。
〜〜〜〜〜 概述 〜〜〜〜〜
从前面的布局设计中可以看出,我们是通过使用Outline View组件来实现目录模块业务的。所以在这里需要我们较详细地去了解Outline View组件,我们需要实现的业务有:显示目录、增加目录项、删除目录项、选择并修改目录项、拖移目录项进行排序等。
跟目录相关的,我们还需要实现各个章节的打开与关闭,章节在标题标签栏的呈现与排序,章节在内容区的呈现。
完成后效果如下:
![e3fd4b14c524c9566a3ad7b940052bdb.png](https://i-blog.csdnimg.cn/blog_migrate/9417337afbd65321a8045d101171d308.jpeg)
〜〜〜〜〜 正文 〜〜〜〜〜
一、 查看组所需要实现的协议方法。
在Xcode中,应用一个较复杂的组件,一般需要实现它的delegate、dataSource两类委托。一般代码文档里有说明告诉我们需要实现哪些委托方法,我们也可以通过Xcode输出错误信息的方式来获取需要实现委托方法的信息,下面以Outline View来作说明:
首先,需要通过Main.storyboard,建立目录Outline View组件在ViewController.swift文件中的Outlet,命名为“catalogOutlineView”;
@IBOutlet
其次,确定在ViewController.swift的viewDidLoad()中实现其delegate、dataSource两类委托;
catalogOutlineView
再次,建立拓展类,确定引用该两类协议;
extension
最后,我们Build and Run,我们将在Debug Area看到下面的信息,说明使用数据源需要实现4个委托方法。
Illegal NSOutlineView data source
(<test.ViewController: 0x600002c05d80>).
Must implement
outlineView:numberOfChildrenOfItem:,
outlineView:isItemExpandable:,
outlineView:child:ofItem:
and outlineView:objectValueForTableColumn:byItem:
二、了解Outline View。
现在,我们建立一个临时工程"Temp"来看看,该如何实现NSOutlineView的数据呈现与增、删、改操作。这是我个人学习时常用的方式,一遇到不熟悉的组件就会新建一个独立的工程,然后慢慢的试着应用。
- 添加Outline View。
- 打开Main.storyboard,在最下的ViewController添加Outline View;
- 拖动上下左右边框到出边缘参考线,这时上下左右边距相等;
- 在Size栏,点击上下左右宽高约束,图里右侧的示意为:组件边距不变随窗体变化大小;
- 选择Document Outline中的Outline View项;
- 设置Attributes栏中,Content Mode项为Cell Based,Columns项为1,去掉勾选Headers;
- 选择Document Outline中的Table Column项,设置Identify栏中,identifier项为“title”。
![2e1715b22c5cf0a286c359364f7085fa.png](https://i-blog.csdnimg.cn/blog_migrate/29e3f6145d0ecc12caaa3114a68fe1e6.jpeg)
![81cfeb9d5571c873c3f41036f295af64.png](https://i-blog.csdnimg.cn/blog_migrate/f01521dffc0b3dc843903ad3fff3d406.jpeg)
![97884263d3c894e54fe66b28a3a7d8d5.png](https://i-blog.csdnimg.cn/blog_migrate/8736f2a12ff6e2b56acec2694c57fe91.jpeg)
- 应用Outline View之Cell Based模式。
- 选择Document Outline中的Outline View项,建立ViewController.swift的Outlet为outlineView;
- 在viewDidLoad方法中添加代码:outlineView.dataSource = self;
- 添ViewController的类拓展,用来实现NSOutlineViewDataSource;
- Build and Run,显示Temp程序窗口;
- 输出错误提示。
![dbf069402368b8a6112e0082688e1053.png](https://i-blog.csdnimg.cn/blog_migrate/a7eed23eae8c72e5aff42d8a25511001.jpeg)
6. 我们按提示使用数组实现其4个协议方法:
import
7. Build And Run,双击各项均可编辑,但无法保存编辑结果。
![bff08ccd317ba5786ea7356ef4119e8f.png](https://i-blog.csdnimg.cn/blog_migrate/47eae5bb1299d91e7d47f3f46e116979.jpeg)
8. 得到如上结构是不能满足我们的需要,因为我们需要在各项前添加图标。
- 应用Outline View之View Based模式。
- 选择Document Outline,设置Attributes栏中,Content Mode项为View Based;再选择Table Cell View项;
- 设置Identify栏中,identifier项为“tableCellView”。
![32bd3a1e367c8fafc3042aa5bc9d180a.png](https://i-blog.csdnimg.cn/blog_migrate/0252bee38654c4daf882949a5d9f9411.jpeg)
3. 在outlineView.delegate =self下添加;
outlineView
4. 添ViewController的类拓展,用来实现NSOutlineViewDelegate,并实现第一个协议方法。注意,需要在代码里明确指出表格里的文字域为可编辑;
extension
5. Build And Run,两次单击各项可编辑。
- 为OutlineView添加Icon。
- 添加3个Image Assets,作为目录一二三级的图标,分别为“catalogLevel0”、“catalogLevel1”、“catalogLevel2”;
- 移除Document Outline栏的Table Cell View项;
- 从library中添加Image & Text Table Cell View组件到Document Outline栏的title项下,即替换原Table Cell View项,改其Identify栏中Identifier值为“titleCellView”;
- 在上述委托方法里添加下列代码,其中Image Literal会出现下拉窗口,让你选择Image Assets。
tableCellView
5. Build And Run,如下图:
![118d80f51ac5de724830e1413a91b07a.png](https://i-blog.csdnimg.cn/blog_migrate/4f069199f4db612da7001373b7fef8ba.jpeg)
- 其它功能的实现。
到了这里已经实现了呈现的样式,但还有几个事情需要做到:第一,单击要打开对应的章节;第二,两次单击后进入编辑状态时需要全选文本;第三,完成修改需要保存;第四,上下移动章节项;第五,需要支持右键菜单。
- 单击要打开对应的章节,需要一个相关事件,这个我找到了。
/// 点击各行
2. 两次单击后进入编辑状态时,同时全选文本。这个我找了两天,用各种方法都不行,只好自定义一个NSTextField叫JYHTextField,并设置Table View Cell的Identity的Class项的值为“JYHTextField”。
/**
3. 完成修改需要保存。这一个需要NSTextField的委托NSTextFieldDelegate。
/// 完成编辑
4. 上下移动章节项,即Outline View实现拖动。首先,需要实现NSOutlineViewDataSource里的三个协议方法,下面有说明;然后,通过拖动与放置的位置,使用NSOutlineView的moveItem方法就可以实现。需要说明的是,我在实际应用时是修改原数据,通过更新OutlineView来实现的。
在这之前: 首先,需要在ViewController.swift文件的viewDidLoad方法里,注册拖移类型catalogOutlineView.registerForDraggedTypes([NSPasteboard.PasteboardType.string]); 然后,需要定义属性,var at = -1。
/// 接收方,拖移放置的位置。
Build and Run,拖动效果,三合一PS后如下:
![e60223eb553e131a839062b008ff252e.png](https://i-blog.csdnimg.cn/blog_migrate/6602b4c2c2d8fcbb68514bd3ce25f6a2.jpeg)
5. 需要支持右键菜单。
首先,在Main.storyboard中为View Controller Scene添加一个Menu组件;
其次,设计好Menu组件的菜单项;
再次,为ViewController.swift添加Menu组件的Outlet为catalogMenu;
最后,为ViewController.swift添加catalogMenu各菜单项的Action。
![8a03ab3dbd8e1d874fb194111436a534.png](https://i-blog.csdnimg.cn/blog_migrate/304a33b5346aedac6d724c9d941ba9a9.jpeg)
- 使用NSTreeController实现NSOutlineView。
使用NSTreeController实现NSOutlineView是一种较方便及较常用的方式。本案未使用该方式,只是看到网上相应的教程特别少或旧,所以讲一下。熟悉了这个,那么使用NSArrayController实现NSTableView你也会,因为方式方法都一样。好,一切还是从新建一个“Temp”工程开始。
- 在ViewController.swift上添加一个Chapter类,并在ViewController类上添加数组如下,代码部分只有这么多;
import
2. 在Storyboard中的ViewController里添加一个Outline View组件及三个Push Button。Outline View设置为单列,无头,ViewBased。三个Push Button分别改为Title为“Add”、“AddChildren”、“Delete”,自已调整布局备用。再添加一个Tree Controller到ViewController,结果显示如下:
![c20d6a9882d82526e3ec2b45c6ba5863.png](https://i-blog.csdnimg.cn/blog_migrate/07cf6621477fa09b89519323218225b0.jpeg)
3. 选择Tree Controller的Attributes栏,设置Children项的值为“sub”,Leaf项的值为“end”,Mode为“Class”,ClassName为“Temple.Chapter”,注意“Temple.”不能少。意思是Tree Controller对应的节点对象是刚才我们定义的“Chapter”类。
4. 选择Tree Controller的Buildings栏,设置Controller Content组中的Build to项,勾选并选择View Controller,因为我们设置的数据源novel在这里,设置Model Key Path项值为“self.novel”,注意"self."不能少。
5. 选择Outline View的Buildings栏,设置Outline View Content组中的Build to项,勾选并选择Tree Controller,即上一步我们设置的,Controller Key保持“arrangedObjects”不变;再选择Table View Cell的Buildings栏,设置Outline View Content组中的Build to项,勾选并选择Table Cell View,设置Model Key Path值为“objectValue.title”,注意“objectValue.”不能少。
6. 建立Action,按住“control”键拖到Tree Controller上,在下拉窗口上选择“Add”项,完成该Action的创建;同理建其它两个Action。运行效果如下图:
![2995ca046eb1ebfcd04884fc052dd004.png](https://i-blog.csdnimg.cn/blog_migrate/c12c46444f08b3bc52012f2f06938cce.jpeg)
注意:在这里,“AddChild”功能是不通的,需要你自己去实现。
三、设计目录模块,即将上面所讲的应用到目录模块上。
目录模块的设计需要解决两个问题,即:模块功能、业务逻辑。模块功能说起来比较简单,就是目录节点的增删改与排序。下面先说一个业务逻辑:
A、目录模块的业务逻辑的相关数据与功能:
- 增。
- 新建文件功能。新建文件时,会创建一章一节两个目录节点;
- 新建节点功能。新建节点时会新建对应的节点文件。表现为在Ceche文件夹中创建一个以catalog.creation为识别名称的txt文件,作为该节点内容的保存对象,约定名格式为:content2138092789.txt。其中2138092789为当前catalog的creation,注意:改原定前缀chapter为content了,改的原因是我们有章与节两个节点。代码相应的内容都已作更改;
- 更新目录、当前章节与已打开章节的数据。首先,为Works.CatalogData添加新节点;其次,重新设置Works的currentContent为当前节点对象,同步Works.InfoData.currentContent为当前节点对象;最后,更新已打开章节的数据,即为Works.InfoData.contentTitleOnBar添加当前节点对象到数组最前,并去重;
- 相应界面呈现。首先,CatalogOutlineView中选择到该节点;其次,TabsBarView呈现该节点标题,并为当前状态;最后IdeaTextView显示该节点的信息,ContentView显示该节点内容并获取了焦点,等待用户输入。
- 删。
- 更新目录、当前章节与已打开章节的数据。首先,为Works.CatalogData删除该节点;其次,删除Works.InfoData.contentTitleOnBar对应的元素;最后,如果删除的是Index为0的元素,需要获取新的Index为0的元素,作为Works的currentContent的值及Works.InfoData.currentContent的值。
- 相应界面呈现。相应数据更新Works.CatalogData为基础更新CatalogOutlineView,以Works.InfoData.contentTitleOnBar为基础更新TabsBarView,以Works.InfoData.currentContent为基础更新IdeaTextView、ContentTextView;
- 删除节点文件。按约定名删除。
- 改。
- 更新目录、当前章节与已打开章节的数据。首先,为Works.CatalogData修改该节点Catalog.title;其次,修改Works.InfoData.contentTitleOnBar对应的元素;最后,如果修改的是Index为0的元素,则同步修改Works.InfoData.currentContent的值。
- 相应界面呈现。相应数据更新Works.CatalogData为基础更新CatalogOutlineView,以Works.InfoData.contentTitleOnBar为基础更新TabsBarView。
- 排序。
- 相应数据更新。首先,为Works.CatalogData修改该节点顺序;
- 相应界面呈现。相应数据更新Works.CatalogData为基础更新CatalogOutlineView。
B、目录功能实现的自定义视图的支持。
- 新添章、节的对话框。
从上面可以看出,我们的目录支持的是两级结构,分别对应为Chapter(章)与Section(节)。所以在目录添加时,我们使用了对话框,让用户去选择创建的是Chapter还是Sction。
- 新建一个Windows文件夹;
- 需要新建一个CatalogWindowController类继承于NSWindowController,并放在Windows文件夹里,文件名为CatalogWindowController.swift。
- 建立NSWindowController时,Xcode会为我们准备一个对应的.xib文件,叫CatalogWindowController.xib。像Main.storyboard一样,点击后打开所见即所得的窗体设计。
- 我们可以在ViewController.swift中实现一个CatalogWindowController,然后可以通过下列代码显示它。为什么会有delegate呢?是因为窗体设置好了数据,需要ViewController.swift知道并进行下一步的处理。
catalogWindowController
- 标题标签与标签栏。
我们还借用了Web上的Tab功能,用在TitleTabsView的显示上,所以我们需要自己定义一个TabsView,Xcode提供的不符合原型的要求,这是我们自己要设计的原因。看到了没,产品经理多厉害。同时为了管理多个Tab,我们又设计了TitleTabsView。
- TitleTab是由显示标题的Label与关闭的按钮组成;
- 我们需要建立一个TitleTabView类,继承于NSView,并保存在Views文件夹里,名称为TitleTabView.swift;
- 为了学习.xib在视图中的应用,我就没有用代码写。于是新建一个同名的.xib,保存在NSView里。然后,添加Label与Button,设置好束备用;
- 这个时候该注意了。需要将Document Outline中Custom View上的File's Owner的Identify的Class项设置为TitleTabView。然后建立包括Custom View在内的TitleTabView.swift的Outlet,分别为view、label、button。
- 建立两个事件,分别响应tab被点击与关闭。
- 最后建立TabsBarView类,放在Views文件夹里。接收Catalog数组,显示Tabs。
- 右键菜单。
- 首先,需要在main.Storyboard里,拖一个菜单到View Controller Scene上,设计好它:
- Add,打开当前章节;
- Move Up,上移一步;
- Move Down,下移一步;
- Move to First,移为最前;
- Move to Last,移为最后;
- Delete,删除该章节。
- 然后选择Catalog Outline View的Connections栏的menu项的右边加号上,将其拖到设计好的菜单上,注意是整个菜单。
- 最后,为各项建立ViewController.swift的Action,这样就完成了右键菜单设计。
C、目录功能实现的自定义视图的支持,注意我们为Catalog添加了sub属性,已表现为树结构。
由于页面布局上存在6个Outline View,所以在实现NSOutlineViewDelegate、NSOutlineViewDataSource与NSTextFieldDelegate协议时,都需要通过Switch分成6路处理,分别以各视图名为头。以NSOutlineViewDelegate其中一个方法为例代码实现如下:
//
- CatalogOutlineView的数据关联与呈现
我们需要将Works.catalogData作为CatalogOutlineView的数据源,选择实现NSOutlineViewDelegate、NSOutlineViewDataSource中的协议,来实现CatalogOutlineView的呈现;
/**
- CatalogOutlineView的选择与修改。
实现用了NSOutlineViewDelegate、NSTextFieldDelegate的协议。
// NSOutlineViewDelegate
- CatalogOutlineView的排序。
- 目录项移动前原位置数据。
需要保存目录项移动前原位置数据,设计了DragItem、DragItems。
/**
2. 节点的控制是通过处理原数据来实现。
节点的控制方法需要建立对应的测试单元。
///
3. 拖移需要实现NSOutlineViewDataSource支持的拖移的三个协议方法。我们像前面一样通过switch,进行分流处理。
在CatalogOutlineView排序中,我们实现的方式是:章只能在章的级别中移动,节只能在节的级别中移动。
/// 结束拖移,章在章间移动、节在节间移动
四、标题标签栏排序的实现。
- 标题标签栏排序的实现。
- 重写TitleTabView的,mouseDown、mouseDragged、mouseUp,在mouseDown中记录标签的原位置,在mouseDragged中更新标签位置用来同步鼠标位置,在mouseUp中获取最终位置,用来进行重新排序。
- 结果是不错,只是标签在当前层中移动,会被上层的标签遮住,这样肯定不行。我以为这个难不到我,可以通过bringSubviewToFront()方法移到最顶层,但不能想像的是,macOS开发中不支持这个方法。
- 后来使用的方法是,使用了两层标签,上层为替身标签。mouseDown时,藏其它替身标签,移动当前替身标签,最终是实现了在最顶层移动标签,但这个花了我大把时间。
- 最终实现。
- 修改TitleTabView,删除了mouseDown、buttonClick方法,同时作了以下修改。
//
2. 添加StandinTabView类继承于TitleTabView。并重写TitleTabView的,mouseDown、mouseDragged、mouseUp。
//
3. 在TabsBarView中实现移动与排序。
/// 实现标签的委托方法。
目录模块的设计到这里算是完成了。
- “iWriter”原码与素材,请访问我的Github的iWriter工程。
下一篇,文本模块的实现。
让我们在这里,遇见明天的自己!姜友华