前言
上面文章提到了我做的Mac客户端Ai Chat – 你问我答。恰好我的开发机子是19款的Macbook Pro (苹果最后一款支持Touch Bar的机器)。你们有多久没用过这个Bar了?本着捣鼓的精神,我研究了如何使用它以及在此实时返回GPT的返回内容,实现了一个跑马灯效果,以下是实现方案,效果如下
实现方案
- 如何返回一个自定义的TouchBar
TouchBar的开发实际上只要遵循苹果App Kit固定的API,可以非常简单的实现你想要的自定义Toubar。
第一步在ViewController 或者 Window下面重写makeTouchBar方法,新建你的TouchBar。
open override func makeTouchBar() -> NSTouchBar? {let touchBar = NSTouchBar()touchBar.delegate = selftouchBar.customizationIdentifier = .customBartouchBar.defaultItemIdentifiers = [.textFieldItem]return touchBar}open override func makeTouchBar() -> NSTouchBar? { let touchBar = NSTouchBar() touchBar.delegate = self touchBar.customizationIdentifier = .customBar touchBar.defaultItemIdentifiers = [.textFieldItem] return touchBar }open override func makeTouchBar() -> NSTouchBar? { let touchBar = NSTouchBar() touchBar.delegate = self touchBar.customizationIdentifier = .customBar touchBar.defaultItemIdentifiers = [.textFieldItem] return touchBar }
然后重写public func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem?方法,构建你Toubar的UI,代码如下,由于Mac开发与iOS开发本质上没有太大区别,所以布局还是直接用SnapKit即可。
public func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {switch identifier {case NSTouchBarItem.Identifier.textFieldItem:let item = NSCustomTouchBarItem(identifier: .textFieldItem)let textField = NSTextField(labelWithString: "? Say Something")self.textField = textFieldtextField.isEditable = falsetextField.isBordered = falsetextField.textColor = .whitetextField.maximumNumberOfLines = 1textField.backgroundColor = NSColor.cleartextField.alignment = .centerlet scrollView = NSScrollView()scrollView.documentView = textFieldscrollView.hasHorizontalRuler = trueself.scrollView = scrollViewitem.view = scrollViewscrollView.snp.makeConstraints { make inmake.left.equalTo(item.view.snp.left).offset(0)make.top.equalTo(item.view.snp.top).offset(0)make.bottom.equalTo(item.view.snp.bottom).offset(0)make.right.equalTo(item.view.snp.right).offset(0)}textField.snp.makeConstraints { make inmake.left.equalTo(item.view.snp.left).offset(0)make.top.equalTo(item.view.snp.top).offset(0)make.bottom.equalTo(item.view.snp.bottom).offset(0)}return itemcase NSTouchBarItem.Identifier.buttonItem:let item = NSCustomTouchBarItem(identifier: identifier)let button = NSButton(title: "Send Message", target: self, action: #selector(sendButtonTapped(_:)))button.bezelColor = NSColor.systemBlueitem.view = buttonreturn itemdefault:return nil}}public func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? { switch identifier { case NSTouchBarItem.Identifier.textFieldItem: let item = NSCustomTouchBarItem(identifier: .textFieldItem) let textField = NSTextField(labelWithString: "? Say Something") self.textField = textField textField.isEditable = false textField.isBordered = false textField.textColor = .white textField.maximumNumberOfLines = 1 textField.backgroundColor = NSColor.clear textField.alignment = .center let scrollView = NSScrollView() scrollView.documentView = textField scrollView.hasHorizontalRuler = true self.scrollView = scrollView item.view = scrollView scrollView.snp.makeConstraints { make in make.left.equalTo(item.view.snp.left).offset(0) make.top.equalTo(item.view.snp.top).offset(0) make.bottom.equalTo(item.view.snp.bottom).offset(0) make.right.equalTo(item.view.snp.right).offset(0) } textField.snp.makeConstraints { make in make.left.equalTo(item.view.snp.left).offset(0) make.top.equalTo(item.view.snp.top).offset(0) make.bottom.equalTo(item.view.snp.bottom).offset(0) } return item case NSTouchBarItem.Identifier.buttonItem: let item = NSCustomTouchBarItem(identifier: identifier) let button = NSButton(title: "Send Message", target: self, action: #selector(sendButtonTapped(_:))) button.bezelColor = NSColor.systemBlue item.view = button return item default: return nil } }public func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? { switch identifier { case NSTouchBarItem.Identifier.textFieldItem: let item = NSCustomTouchBarItem(identifier: .textFieldItem) let textField = NSTextField(labelWithString: "? Say Something") self.textField = textField textField.isEditable = false textField.isBordered = false textField.textColor = .white textField.maximumNumberOfLines = 1 textField.backgroundColor = NSColor.clear textField.alignment = .center let scrollView = NSScrollView() scrollView.documentView = textField scrollView.hasHorizontalRuler = true self.scrollView = scrollView item.view = scrollView scrollView.snp.makeConstraints { make in make.left.equalTo(item.view.snp.left).offset(0) make.top.equalTo(item.view.snp.top).offset(0) make.bottom.equalTo(item.view.snp.bottom).offset(0) make.right.equalTo(item.view.snp.right).offset(0) } textField.snp.makeConstraints { make in make.left.equalTo(item.view.snp.left).offset(0) make.top.equalTo(item.view.snp.top).offset(0) make.bottom.equalTo(item.view.snp.bottom).offset(0) } return item case NSTouchBarItem.Identifier.buttonItem: let item = NSCustomTouchBarItem(identifier: identifier) let button = NSButton(title: "Send Message", target: self, action: #selector(sendButtonTapped(_:))) button.bezelColor = NSColor.systemBlue item.view = button return item default: return nil } }
同意第一响应
open override var acceptsFirstResponder: Bool {return true}open override var acceptsFirstResponder: Bool { return true }open override var acceptsFirstResponder: Bool { return true }
就这样我们启动Mac应用我们的自定义Toubar就构建好了,默认它会返回一句内容?️ Say Something
- 如何实现接受GPT流信息,并实现跑马灯效果
–
Open AI,GPT流式返回我不多赘述,很多文章都有提到Flutter如何实现OpenAi流式返回,我的Mac客户端是Flutter编写,只需要把OpenAi的接口设置为流接口,然后HttpClient使用流的方式返回即可。然后自定义一个插件,从Flutter返回流信息到Mac原生刷新TouchBar。
跑马灯效果其实实现起来和iOS原生是一样的,我是基于一个ScrollView无限长度的Label,然后计算Label的宽 如果大于TouchBar的最大宽度,每次刷新文本时候,自动滚到Label的末尾来做的 代码如下。
从插件更新文本
OpenAiStreamResPlugin.shareInstance.responseStreamText = { [weak flutterViewController] text inautoreleasepool {flutterViewController?.addText(text: text)}}OpenAiStreamResPlugin.shareInstance.responseStreamText = { [weak flutterViewController] text in autoreleasepool { flutterViewController?.addText(text: text) } }OpenAiStreamResPlugin.shareInstance.responseStreamText = { [weak flutterViewController] text in autoreleasepool { flutterViewController?.addText(text: text) } }
跑马灯效果
func addText(text: String) {if text.count == 0 {self.textField?.stringValue = "? Say Something"self.scrollView?.contentView()?.scroll(NSPoint(x: 0, y: 0))return}var newText = text.trimmingCharacters(in: .whitespacesAndNewlines)newText = newText.replacingOccurrences(of: "\n", with: "")self.textField?.stringValue = newTextif let scrollView = self.scrollView {self.textField?.sizeToFit()let textF = self.textField!if textF.frame.size.width > scrollView.frame.size.width {self.scrollView?.contentView()?.scroll(NSPoint(x: textF.frame.size.width, y: 0))}}}func addText(text: String) { if text.count == 0 { self.textField?.stringValue = "? Say Something" self.scrollView?.contentView()?.scroll(NSPoint(x: 0, y: 0)) return } var newText = text.trimmingCharacters(in: .whitespacesAndNewlines) newText = newText.replacingOccurrences(of: "\n", with: "") self.textField?.stringValue = newText if let scrollView = self.scrollView { self.textField?.sizeToFit() let textF = self.textField! if textF.frame.size.width > scrollView.frame.size.width { self.scrollView?.contentView()?.scroll(NSPoint(x: textF.frame.size.width, y: 0)) } } }func addText(text: String) { if text.count == 0 { self.textField?.stringValue = "? Say Something" self.scrollView?.contentView()?.scroll(NSPoint(x: 0, y: 0)) return } var newText = text.trimmingCharacters(in: .whitespacesAndNewlines) newText = newText.replacingOccurrences(of: "\n", with: "") self.textField?.stringValue = newText if let scrollView = self.scrollView { self.textField?.sizeToFit() let textF = self.textField! if textF.frame.size.width > scrollView.frame.size.width { self.scrollView?.contentView()?.scroll(NSPoint(x: textF.frame.size.width, y: 0)) } } }
就这样一个支持GPT实现返回信息的跑马灯TouchBar就完成了,是不是很简单?