美文网首页MacOS开发 技术集锦
如何编写一个 macOS 版的 TableView

如何编写一个 macOS 版的 TableView

作者: 匠人案牍 | 来源:发表于2020-04-12 19:49 被阅读0次

    项目目的

    1. 在 macOS 的许多软件中,我们都可以看到有 SpiltView 和 TableView 的身影,比较著名的像 Mac 电脑的 App Store, keynote 等等,还有一些三方开发的像 QQ, 微信,这些软件都使用了 SplitView 和 TableView. 下图的 Mac App Store 整个大的框架就是 SpiltView(蓝色框),而在左侧列表就是一个 TableView:
    Screen Shot 2020-04-01 at 20.49.25
    1. 任何一个软件的开发都少不了数据,这些数据可能是软件本身存储的数据,可能是软件运行过程中需要的数据,比如微信的聊天记录就是其存储的数据,而软件的一些诸如图标,按钮名称则是其运行过程中需要的数据.因此我们要考虑一种或多种比较高效的存储数据的方式,方便代码的维护.从简单粗暴的角度上出发,我们的确可以把一些诸如按钮名称,列表内容等写死在程序里,但这会带来一个很明显的问题,万一哪天我们要修改代码,很有可能只是一个数据的改动,就造成了软件的崩溃,甚至是无法运行.在这个简单的 SplitViewDemo 项目中,我们会尝试使用 .plist 文件来存储 TableView 的列表数据,只是为了给读者展示一种使用方法.

    2. 接下来我们会做一个 SplitViewDemo.app,软件并没有什么特别的功能,选择对应的左侧列表选项后,软件右侧将出现对应的一些文字,但从演示的角度来讲,它可以说明一些框架或者功能如何使用,这也是 SplitViewDemo 的目的所在。

    项目流程

    新建一个 Xcode 工程并搭建基本框架

    打开 Xcode,如果您从来没有开发过任何 Xcode 的项目,那么我们会看到下图这个界面:

    image-20200401211456504

    如果您曾经开发过 Xcode 的项目,那么我们在上图中 No Recent Projects 那里,会看到之前开发过的项目名称.

    当然啦,这并不是重点,无论看到哪个,我们都是点击上图中的 Create a new Xcode project 来新建一个工程项目.

    另外,如果您两个图都没有看到,别担心,您只是之前关闭了这个窗口,我们可以在打开 Xcode 后导航到左上角的菜单栏,然后选择 Window > Welcome to Xcode 就能够显示上图的内容.

    Screen Shot 2020-04-01 at 21.21.45 Screen Shot 2020-04-01 at 21.22.56

    还有一种方法可以直接新建一个 Xcode 工程项目,就是打开 Xcode 后,点击左上角菜单栏中的 File > New > Project, 这样也可以直接新建工程项目.

    紧接着 Xcode 会询问我们要创建一个怎样的项目,大类的选择有 iOS, watchOS, tvOS, macOS, Cross-platform, 就像字面意思一样,要开发哪个平台的项目,就选择哪个对应的名字.接着要选择的是在对应平台下,我们要开发具体什么东西,是软件还是游戏,框架还是库.此处我们选择 macOS 下的 App,并点击 Next 以继续.

    image-20200401213045954

    然后是一些基本信息的填写,Product Name 就是字面意思,这个软件叫什么名字,此处我们填写为 SplitViewDemo,Team 就是开发团队,一般来讲我们都需要注册一个开发者账号,当然,如果您开发的软件只是自己用着玩儿,那么可以注册一个免费的开发者账号,如果您想要将自己的软件上架到 App Store,那么您一定要注册一个付费的开发者账号,可以到苹果的开发者软件去注册,面向大众的开发者是 99 美元一年(已经很良心了),面向企业内部的是 299 美元一年. Organization Name 就是您团队的名字,比如 myCompany, Organization Identifier 一般是在您确定好 Organization Name 后直接生成的,比如 com.myCompany.我们要将 Language 设置为 Swift,因为我们的项目会用 Swift 来编写,User Interface 有三种选项, SwiftUI, Storyboard 和 XIB,建议您选择 Storyboard,因为目前苹果的要求,如果您要上架自己的 App,必须是 Storyboard,然后下面的复选框,暂时都先不勾选,因为我们用不上.

    image-20200401215822346

    上图的信息填写好以后,点击 Next,我们需要明确将项目存储在哪里,此处我只是为了演示方便,将项目存放在了桌面上,如果您自己在操作时,建议您把项目放在一个您熟悉的位置,比如 GitHub,或者使用苹果家的 iCloud 功能将其备份好,这一点很重要.

    image-20200401220419284

    选择好合适的路径存储后,点击 Create,就完成了新项目的创建工作.

    编辑软件的界面

    我们先来看看成品的界面:

    image-20200401221231968

    很普通对不对,没有任何的美化,但作为演示它足够了,在界面的左侧是一个简单的 TableView,右侧是根据选择不同的列表信息后,对应显示的内容.

    如何做出这种效果?在 Xcode 的左侧选择 Main.storyboard,我们会看到这样的界面:

    Screen Shot 2020-04-02 at 21.28.58

    在 storyboard 窗口中的 View Controller 窗口是我们不需要的,选中它,然后键盘上按下 Delete,我们会看到这样的界面:

    Screen Shot 2020-04-02 at 21.32.22

    接着,我们会像拼接画像一样,把 SplitView 和 TableView 拼接到界面中去.

    首先是 Split View Controller,找到 Xcode 右上角的 "+" 图标,点击它,就可以看到系统为我们准备好的各种框架以及一个搜索框,就像这样:

    Screen Shot 2020-04-02 at 21.37.01

    现在,请搜索一下 Split View Controller 并把它拖出来到 Storyboard 上,然后放手,此时 Storyboard 界面就已经有了 SplitView,那么和它头顶的 Window 有什么关系,如果这时候直接运行 app 就可以看到 SplitView 了么?答案是否定的,我们还需要把 Window 和 SplitView 连接起来,让它们建立关系.

    打开 document outline 窗口,这个窗口是所有按钮,框架,标签的一个显示窗口,通过这个窗口我们可以更加方便准确地选择我们真正需要的东西,document outline 窗口就是如下图红色框选中的区域:

    Screen Shot 2020-04-02 at 21.43.04

    在 document outline 中选中 Window Controller Scene 下方的 Window Controller,保持它的选中状态,然后键盘上按住 control 键,或者按住鼠标右键,将 Window Controller 拉到 Split View Controller 上,这时会有一条蓝色的线,松手后我们会看到这个:

    Screen Shot 2020-04-02 at 21.50.02

    点击选择 window content,这样我们就建立了一个连接,意思就是上面那个 Window,它的内容就是下面这个 Split View,如下图:

    Screen Shot 2020-04-02 at 21.57.51

    刚才我们设置了 Split View Controller,现在要做的就是 Table View Controller 了,像添加 Split View Controller 一样,点击右上角的 "+",跟着搜索 Table View,把 Table View 拉出来,放在 Split View Controller 右侧上方的那个 Controller 中,就像下图这样:

    image-20200402230313735

    跟着我们把 Table View 的那个窗口拉大,直到撑满整个背后的 Controller,就像这样:

    image-20200402231130187

    这个时候有一个小小的注意点,我们最终成型的 Demo 只有左侧一个 Table,但如果此时运行一下当前的 App,会发现是这样的:

    Screen Shot 2020-04-09 at 21.14.38

    红色圆圈处有两根线,证明左侧实际上有两列 Table,因此我们需要稍微做一些修改,在 document outline 中选中 View Controller Scene > View Controller > Bordered Scroll View - Table View > Clip View > Table View,然后调出它的 Attribute Inspector(快捷键是 option + command+5),把 Columns 的数值调整为 1,就像这样:

    Screen Shot 2020-04-02 at 23.29.40

    然后再运行一遍,此时我们会发现这个 SplitViewDemo 变成了这样子:

    image-20200409222057788

    当然,在这个粗糙的界面中,有些东西我们是不要的,就是界面左上角那个 table view 的表头,英文是 header,可以简单粗暴地在 xcode 的设置界面将它关闭,在 document outline 中选择第一个 View Controller Scene 下方的 Table View,跟着在它的 Attribute Inspector 里面把 Headers 的勾选去掉即可,如下图所示:

    Screen Shot 2020-04-09 at 22.26.45

    同样的操作,我们在下方那个 View Controller 的框架中加入 一个 Label,并把它拉大到充满整个框架。

    这个时候如果我们再运行一次 SplitViewDemo,就会发现大体的框架都有了,只要把相应的内容填进去,整个软件就可以算大功告成。

    既然说到填充内容,我们需要先做一件事情,两个 View Controller Scene 还没有它们的 controller 文件,我们需要创建两个 Controller 文件来控制 SplitViewDemo 中两边显示的内容,左侧是一个列表,右侧是根据选择列表中某个选项后,相应变化的 Label。接下来就是定义 Controller 了,首先选择 Xcode 中的 File > New > File... 将出现以下窗口:

    image-20200410221045212

    此处我们需要选择的是 macOS 下的 Cocoa Class,点击 Next 后,按照下面的图示进行设置,注意一定不要勾选 Also create XIB file for user interface:

    image-20200410223012964

    设置好后,再次点击 Next,就需要选择相应的存储位置,一般我们都推荐将所有相关的文件全部存放在一个指定路径下,就像这样:

    image-20200410223702357

    此时我们就在 SplitViewDemo.app 这个项目中成功创建了一个 SourceViewController.swift 文件,如下图所示:

    Screen Shot 2020-04-10 at 22.38.59

    稍后就会用这个文件来控制 SplitViewDemo 左侧列表显示的内容,不过现在我们还要照着上面的方法创建另一个 DetailViewController.swift 文件,具体方法就不赘述了,完成以后应该像下面这个样子:

    Screen Shot 2020-04-10 at 22.46.35

    两个控制文件都创建好了,Storyboard 里面该有的控件也有了,那就应该让控制文件与控件产生关联,打开 Main.storyboard,并在 document outline 中选择第一个 View Controller Scene 下面的 View Controller Scene,并打开其 Identity Inspector,在 Custom Class 中将 Class 设置成 SourceViewController,如下图:

    Screen Shot 2020-04-10 at 22.54.57

    如果我们细心一点就会发现,document outline 中的第一个 View Controller Scene 名字已经变成了 Source View Controller,同时在 Storyboard 中,Split View Controller 下方左侧的 View Controller 灰色字,也变成了 Source View Controller,同样的道理,我们再来一次,这次去设置 Detail View Controller。

    接着我们需要告诉 Table View,它显示的内容,都被控制的方式,都来自 Souce View Controller,所以在document outline 中,选中 Source View Controller Scene 下面的 Table View,再在键盘上按住 Ctrl 键别松手,把 Table View 拖到 Source View Controller 上去,这时我们会看到:

    image-20200410231006652

    要建立连接,就需要选中 dataSource 和 delegate,连续两次按照上面的操作执行即可。还没完,我们在 SourceViewController.swift 文件中,还需要进行一点小小的修改,把下面这段代码:

    class SourceViewController: NSViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do view setup here.
        }
    }
    

    修改成这个样子:

    class SourceViewController: NSViewController,NSTableViewDataSource,NSTableViewDelegate {
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do view setup here.
        }   
    }
    

    也就是在 NSViewController 的后面,多加了NSTableViewDataSource 和 NSTableViewDelegate,请注意用逗号隔开。这个时候,整个项目的框架部分算是搭好了,只要我们把一些相应的内容填充进去,SplitViewDemo 就会显示我们需要的内容,不过我们得注意的一个点是整个界面的大小,如果我们将 SplitViewDemo 的窗口拉大或者缩小,就会看到它并没有匹配到相应的大小变化,对于这一点,暂时不用去管它,我们会在稍后进行修改。

    填充具体内容

    我们打开 Main.storboard,接下来要选择一些内容,请注意如果要选择某个 Label,或者某个 TableView,都尽量在 document outline 中进行选择,这样能够减少出错的概率。好了,我们当前要在 document outline 中选择的是 Source View Controller Scene 下的 Table View,此时打开 assistant 编辑页面,如果一切顺利,我们会同时打开 Storyboard 和 SourceViewController 的编辑页面,就像下面图中的样子:

    image-20200411105256567

    选中 Table View 的前提下,按住键盘上的 Ctrl 键,把 Table View 拖出来,拖到 SourceViewController 编辑页面中,并处于 override func viewDidLoad() 的上方,就像下面图中这个样子,然后松手:

    Screen Shot 2020-04-11 at 10.57.39

    这个时候我们会看到 Xcode 询问我们要建立一个怎样的连接,是 outlet 还是 action,如果是 outlet,那么它的意思就相当于“参数”,如果是 action,它的意思就相当于操作,会在点击或者按住后执行相应的行为。我们要设置为 outlet,把 name 设置为 tableView,其它的用默认就好:

    Screen Shot 2020-04-11 at 11.06.37

    同样的操作,这次我们先在 document outline 中选择 Detail View Controller Scene 下的 Label,如果不出意外,那么 assistant 界面会变成 DetailViewController 的代码界面,按住 Ctrl 把 Label 拉到 override func viewDidLoad 的上方,Connection 还是选择 outlet,把 name 设置为 labelText,最后它应该像这个样子:

    image-20200411220444238

    这个时候我们回到 Main.storyboard ,在 document outline 中点选 Source View Controller Scene,那么 assistant 界面会出现 SourceViewController 的代码界面,在 override func viewDidLoad 这个函数的下方添加一个函数,叫 numberOfRows,就像下面这样:

    class SourceViewController: NSViewController,NSTableViewDataSource,NSTableViewDelegate {
        @IBOutlet var tableView: NSTableView!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do view setup here.
        }
      // 以下就是我们要添加的 numberOfRows 函数
        func numberOfRows(in tableView: NSTableView) -> Int {
            return 100
        }
    }
    

    然后再运行一下 SplitViewDemo,不出意外的话,我们看到的画面是这样的:

    image-20200411223632100

    左侧的 table 列表中会出现 100 个 Table View Cell,也就是说 numberOfRows 决定了列表一共有多少行,当然也可能出现一些意外,比如运行时没有任何显示,一片空白,此时建议读者回顾一下,看是否没有在 document outline 中设置 DataSource 和 Delegate,或者在代码里面是不是少了 NSTableViewDataSource 和 NSTableViewDelegate。如果没什么问题,我们继续看看这个 numberOfRows,在实际工作中我们肯定是要动态地确定到底有多少行,而不是写死代码说它就 100 行,而且每行到底显示些什么东西?数据从哪里来? 这些都是会变化的,我们可以考虑从 excel 读取数据,从 json 读取数据等等,此处显示时我们会从 plist 文件读取数据,那么接下来就是要在 SplitViewDemo 这个项目中加入一个 plist 文件来放数据,具体操作如下:

    1. 在 Xcode 中选择屏幕左上角的 File > New > File... 快捷键是 ⌘N;
    Screen Shot 2020-04-12 at 14.24.54
    1. Xcode 将弹出一个窗口询问我们要创建什么文件,与之前创建 SourceViewController 文件的窗口一样,这里我们要选择的是 macOS 下的 Property List,它在 Resource 这个大分类下方;
    image-20200412143139328
    1. 点击 Next,Save As 那里把名字设置为 LanguageList,其它的与下图保持一致,然后点击 Create,这样就创建好了一个 plist 文件;
    image-20200412143227029
    1. 在创建的 Language.plist 文件中,我们点击 Root 旁边的加号,就可以新建信息,要新建的内容可以看看下面的表格:
    Screen Shot 2020-04-12 at 14.55.33
    语言 内容
    简体中文 凤凰台上凤凰游,凤去台空江自流。
    吴宫花草埋幽径,晋代衣冠成古丘。
    三山半落青天外,二水中分白鹭洲。
    总为浮云能蔽日,长安不见使人愁。
    繁體中文 飛雪連天射白鹿,笑書神俠倚碧鴛
    English e hold these truths to be self-evident, that all men are created equal, that they are endowed by their Creator with certain unalienable Rights, that among these are Life, Liberty, and the pursuit of Happiness.
    Deutsch Sonne kann nicht ohne Schein, Mensch nicht ohne Liebe sein.
    русский язык где есть жизнь, есть счастье
    Le français Je ne suis pas d'accord avec ce que vous dites, mais je me battrai jusqu'à la mort pour que vous ayez le droit de le dire.

    不用去管这些文字的内容,我只是随意用一些翻译软件找了一些各个语言中比较有名的语句,如果有任何纰漏,忽略就好,但如果有任何冒犯,请让我知道。总之,我们的 plist 文件创建好了之后是这个样子的:

    image-20200412151942845

    每一种语言会对应它后面的一句话,这个形式有点像是字典

    var someDictionary:[String:String] = [:]
    

    另外,我们只想要把字典的键显示在 table view 中,字典的值根据所选键的不同显示在 Label 里面,那么我们需要一个数组来存储所有的键,

    var languageList:[String] = []
    var languageDictionary:[String:String] = [:]
    

    所有的数据都是来自这个 plist 文件,我们还需要一个办法通过 SourceViewController 来读取 LanguageList.plist 文件,其实也很简单,只要一句代码:

    let languageDic = Bundle.main.path(forResource: "languageList",ofType:"plist")
    

    读取成功后,就需要把键值对存入一个字典,把键存入一个数组,方便取用:

    let dic = NSDictionary(contentsOfFile: languageDic!)
    for (key,value) in dic! {
     languageDictionary.updateValue(value as! String, forKey: key as! String)
    }
    for key in languageDictionary.keys 
    {
     languageList.append(key)
    }
    

    这个时候,我们知道了要显示的具体内容,也知道要显示多少排,那么刚才的 numberOfRows 函数就可以改一改了:

    func numberOfRows(in tableView: NSTableView) -> Int {
            return languageDictionary.count
    }
    

    目前为止,在 SourceViewController 中的代码应该看起来是这个样子:

    image-20200412162620894

    然后我们试着运行一下,由于在 plist 文件中我们只设置了五种语言,因此在 App 里面也应该只有五行,就像下面这样:

    image-20200412162739415

    行数确定好了,我们就要确定每行显示什么内容,在 Apple 的官方文档中有这么一个方法可以实现显示列表内容,即 tableView(_:viewFor:row:),具体怎么用,可以看看代码演示:

    func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
            // 先创建一个 tableView 的视图,由于只有一列,可直接用列标识符来代表这个视图,并把它伪装成一个 NSTableCellView
            guard let vw = tableView.makeView(withIdentifier: tableColumn!.identifier, owner: self) as? NSTableCellView else {
                return nil
            }
            //这个视图要显示的内容就是 languageList 列表中对应 row 的字符
            vw.textField?.stringValue = languageList[row]
            return vw
    }
    

    此时的代码,应该是这样的:

    image-20200412164728132

    好了,现在再运行一次软件,我们就可以看到相应的列表内容:

    image-20200412164832941

    紧接着就要考虑,如何让左侧列表选中某个 cell 选项后,右侧 Label 对应发生变化,显示相应的语句,为此我们需要加入一个 tableViewSelectionDidChange 函数,就像这样:

    func tableViewSelectionDidChange(_ notification: Notification) {
            guard tableView.selectedRow != -1 else { return }
            guard let splitVC = parent as? NSSplitViewController else {return}
            if let detail = splitVC.children[1] as? DetailViewController
            {
                detail.labelChange(country: languageDictionary[languageList[tableView.selectedRow]]!)
            }
        }
    

    此处需要详细解释一下代码,我也看了一会才理解,大意就是,现在 tableview 中选择的行有了变动,我们要让 DetailViewController 知道这个变化,可是 DetailViewController 管的不是 TableView,而是 Label,但是DetailViewController 和 SourceViewController 都 隶属于 SplitViewController 名下,是 SplitViewController 数组中的一员,可以通过 SplitViewController 来转达相应的变化。

    另外在上述代码中还调用了 DetailViewController 中的一个函数 labelChange,这个函数还没有在 DetailViewController 进行定义,不能直接用,我们需要在 DetailViewController 中最后添加以下代码:

    func labelChange(country:String) {
            labelText.stringValue = country
    }
    

    这样才可以正常使用。试试看,运行一下,如果没有意外,软件运行后应该可以跟随列表的选择而显示不同的语言文字:

    image-20200412170035838

    进行简单的界面适配

    大体上来说,软件编到这个地方,其主体功能已经完善了,现在我们要做的是让它稍微人性化一点,比如在软件刚刚启动时,我们没有选择任何列表的内容,但右侧还是会显示 “Label” 字样,这并不是我们预期的,可以做一个小的修改,让软件在刚刚启动时,Label 的 isHidden 参数为 true,然后当我们实际选择某行后,再把 Label 的 isHidden 属性设置为 false,这样就可以啦,如下:

    class DetailViewController: NSViewController {
    
        @IBOutlet var labelText: NSTextField!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            labelText.isHidden = true
            // Do view setup here.
        }
        func labelChange(country:String) {
            labelText.isHidden = false
            labelText.stringValue = country
        }
    }
    

    再比如,我们每次运行软件进行测试的时候,发现好像左侧的列表顺序是不定的,这在实际工作过程中很容易造成困扰,影响体验,比如当用户想要改变某个偏好设置时,他没有办法凭借记忆找到大概的位置,而是要一个一个仔细地查找,解决这个问题也很简单,方法并不唯一,比如可以加搜索框,也可以对列表进行排序。此处演示排序的解决方案,只需要在 SourceViewController 的代码中,viewDidLoad 这段代码的最后,加入一句:

    languageList.sort()
    

    这样就可以实现排序的功能,试试看再运行软件,这下每次运行时,简体中文和繁體中文都会出现在列表的末尾,而不是随机出现。

    然后我们接着看,运行这个软件时,如果我们这么大或者缩小软件的窗口就会是这么个效果:

    image-20200412183822148

    TableView 并没有随着软件窗口的拉伸与压缩而进行相应合适的拉伸和压缩,对于我们这个演示软件来说,它没什么太过复杂的界面,所以可以选中 document outline 中的 SourceViewController Scene > Source View Controller > View > Bordered Scroll View - Table View > Clip View > Table View,然后打开左上角 Editor 菜单,选择 Resolve Auto Layout Issues,然后选择第二个 Reset to Suggested Constraints

    image-20200412184420108

    最后,软件运行时,其界面顶上的 “Window” 字样并没有太大的作用,可以在 document outline 中选中 Winodw Controller Scene 下方的 Window,然后在其 Attributes Inspector 中勾选 Hide Title。

    项目总结

    1. 这个简单粗糙的项目,展示了 SplitView,TableView,Label 的一些用法,可以帮助我们在初期学习时快速掌握一些技巧;
    2. 此项目还有很多可以完善的地方,比如选择列表选项后,是否可以出现不同的界面,如图片,下级列表,偏好设置窗口等等;
    3. 框架建立完成后,数据可以用哪里获取,本项目中是从 plist 文件获取的数据,那么是否有方法直接从网络获取数据,这也是一个可以继续研究下去的方向。

    关注我的公众号,回复 SplitViewDemo 即可获得源码文件

    匠人案牍

    相关文章

      网友评论

        本文标题:如何编写一个 macOS 版的 TableView

        本文链接:https://www.haomeiwen.com/subject/omibmhtx.html