肇鑫的技术博客

业精于勤,荒于嬉

List更改选中状态的背景色正确办法

macOS中,默认List选中之后的背景是深蓝色。这会导致我的按钮变得不明显,我希望修改这个背景色。

询问AI,AI给我的反馈是通过background修改。但是通过background修改的背景色,会比系统自带的小一圈,十分丑陋。我再次询问AI,如果要修改系统的默认选中背景,要如何做。AI回答说,要修改那个很复杂。然后给了我一个错误的解法。

我只要换个AI来问。第二个AI给出的也是通过background修改。不过它在说思路的时候,应该是通过listRowBackground修改。于是我自己尝试用listRowBackground来修改。这次成功了。

.listRowBackground(selectedLanguage == locale ? Color.gray.opacity(0.15) : Color.clear)

当使用listRowBackground,List的选中就不要绑定selectedLanguage了。因为那样会导致.listRowBackground和高亮色被同时使用。

高亮色就是List选中的背景色的正式说法。

因为上面说的原因,我们需要手动指令selectedLanguage,可以通过点击或者onHover。看你的界面的需要。

SwiftUI与Combine

SwiftUI大大简化了Combine的使用。比如在使用iCloud同步Core Data的时候,会有多个NSPersistentStoreRemoteChangeNotificationPostOptionKey通知。这时如果我们如果要更新界面,就会导致界面会重复计算多次。

这时我们就可以使用Combine的debounce方法。在SwiftUI中,只需要这样调用就可以了。

private let updateUIPublisher = NotificationCenter.default.publisher(for: .updateUI)
    .debounce(for: 0.2, scheduler: RunLoop.main)

Core Data的准备阶段

稍微记录一下Core Data端的准备代码。如果不设置,是不会获得通知的。

if let description = container.persistentStoreDescriptions.first {
      description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
      reigsteriCloudCoreDataSyncing()
}

extension PersistenceController {
  func reigsteriCloudCoreDataSyncing() {
    NotificationCenter.default.addObserver(forName: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator, queue: nil) { _ in
      DispatchQueue.main.async {
        NotificationCenter.default.post(name: .updateUI, object: container.persistentStoreCoordinator)
      }
    }
  }
}

SwiftUI中的fileImporter与AppKit的NSOpenPanel在使用时的区别

原本我以为二者是等价的。但是实际上,二者有一个重要区别。如果不掌握,你就没法使用fileImporter。

今天我就遇到了这个问题。当我使用fileImporter打开桌面的截图后,NSImage居然崩溃了。

func generataScreenshots(urls: [URL]) {
  var screenshots = [NSImage]()

  for url in urls {
    let image = NSImage(contentsOf: url)! // nil crash
    screenshots.append(createScreenshot(image: image))
  }

  self.screenshots = screenshots
  self.showScreenshotsPreviewView = true
}

我很疑惑。因为用户选中的文件的权限是默认开启的。难不成Xcode把这个也给搞坏了?我将用户选择文件的权限由默认的只读改成读、写。结果还是崩溃。但是当我添加了Download文件夹为读、写之后。重新选中一个Download文件夹下的图片,这时系统弹出是否允许应用访问Download文件夹,我选择允许之后,这次居然成功运行了。

看起来的确还是和权限有关系。我将NSImage的代码前面加了一行Data的代码。采用Data 读取这个图片。结果这次,Xcode的输出给出了正确的提示,说没有权限打开URL。

查看fileImporter的文档说明,注释中写到:

This dialog provides security-scoped URLs. Call the startAccessingSecurityScopedResource method to access or bookmark the URLs, and the stopAccessingSecurityScopedResource method to release the access.
这段对话提供了安全范围的URL。调用startAccessingSecurityScopedResource方法来访问或将URL添加到书签,以及调用stopAccessingSecurityScopedResource方法来释放访问权限。

修改代码

func generataScreenshots(urls: [URL]) {
  var screenshots = [NSImage]()

  for url in urls {
    if url.startAccessingSecurityScopedResource() {
      let image = NSImage(contentsOf: url)!
      screenshots.append(createScreenshot(image: image))

      url.stopAccessingSecurityScopedResource()
    }
  }

  self.screenshots = screenshots
  self.showScreenshotsPreviewView = true
}

这回就可以了。

结论

fileImporter获取的URL必须使用startAccessingSecurityScopedResource()进行处理,不然是不能直接读取的。除非你已经plist中明确了该文件夹的权限,类似Download文件夹这种Xcode可以添加的才行。

而NSOpenPanel,打开同样的图片,直接使用就可以正常运行。

func openPanel() {
  let panel = NSOpenPanel()
  panel.allowedContentTypes = allowedContentTypes
  panel.allowsMultipleSelection = true

  let response = panel.runModal()
  var screenshots = [GeneralImage]()

  if response == .OK {
    for url in panel.urls {
      let image = NSImage(contentsOf: url)!
      screenshots.append(createScreenshot(image: image))
    }
  }

  self.screenshots = screenshots
  self.showScreenshotsPreviewView = true
}