SwiftUI – 奇怪的 NSColorPanel

Base on macOS 10.15, Xcode 11.7.

 

1、首先,NSColorPanel 有一个静态“单例”对象:NSColorPanel.shared。我们可以直接使用这个静态对象。这个静态对象也可以被多个组件共享,但要注意如果共享的话,获得的 color 值也是共享的。

 

2、但 NSColorPanel 又不纯粹的“单例”模式,它允许我们手动新建 NSColorPanel 对象,而不使用 shared 静态对象。

(手动新建对象之前,最好调用一次 NSColorPanel 的静态函数,或 shared 静态属性,确保已经创建了 shared 静态对象后,才新建对象。否则 NSColorPanel() 返回的就会是 shared 对象。)

 

3、点击 NSColorWell 对象唤起的 NSColorPanel,似乎就是 NSColorPanel.shared 静态对象。但是几个 NSColorWell 的 color 属性并不会互相影响!我猜测 NSColorWell 组件应该是注册了 NSColorPanel.shared.accessoryView 属性,每次收到新的 NSColorPanel.shared.color 值的时候,会判断当前的 accessoryView 是不是自己。👈  accessoryView 这个猜测被验证是错的。好奇它到底是怎么区分不同的 NSColorWell 的啊。

 

4、NSColorPanel 继承自 NSPanel,所以它的 isReleasedWhenClosed = false。但实际上,对于手动新建的 NSColorPanel 对象,它是会在窗口关闭后自动释放的

 

5、由于 shared 是静态对象,所以 MyNSColorPanel.setPickerMask() 对 shared 对象是无效的。即 NSColorPanel.shared 对象(包括 NSColorWell 弹出的)必定是最全的颜色选择器。

 

实例代码:

import SwiftUI

struct ColorView: View {
    @State var hovered: Bool = false
    @State var color1: NSColor = .red
    @State var color2: NSColor = .green
    @State var color3: NSColor = .blue
    @State var coordinator: Coordinator? = nil
    
    #if DEBUG
    private let deallocPrinter = DeallocPrinter(forType: String(describing: Self.self))
    #endif
    
    var body: some View {
        VStack {
            Text("颜色啊")
            
            VStack(spacing: 1) {
                HStack {
                    ForEach(Theme.colors, id: \.self) { color in
                        ZStack {
                            Rectangle()
                            .cornerRadius(10)
                            .foregroundColor(Color(color))
                            .frame(width: 30, height: 50)
                            
                            HStack {
                                Text(color.isLight() ? "L" : "D")
                                    .foregroundColor(color.isLight() ? Color.black : Color.white)
                            }.font(.body)
                        }
                    }
                }
                Divider()
                HStack {
                    Rectangle().fill(Color(color1)).frame(width: 80, height: 30)
                    Rectangle().fill(Color(color2)).frame(width: 80, height: 30)
                    Rectangle().fill(Color(color3)).frame(width: 80, height: 30)
                }
                HStack {
                    Button("Color1"){
//                        let _ = MyNSColorPanel.shared
                        MyNSColorPanel.setPickerMask([.grayModeMask, .rgbModeMask, .colorListModeMask])

                        let cp = MyNSColorPanel()     // ps: 应该把这个 cp 设置成结存储属性才对。
                                                      // 否则这样每次点击 color1 按钮都会新建一个 NSColorPanel 窗口来 =。=# 
                                                      // 算了,这里懒得改了。😄
                        log.debug("new address: \(cp)")
                        log.debug("shared address: \(MyNSColorPanel.shared)")
                        log.debug("isReleasedWhenClosed? \(cp.isReleasedWhenClosed)")
                        cp.color = self.color1
                        
                        cp.setTarget(self.coordinator!)
                        cp.setAction(#selector(Coordinator.onColorChanged(sender:)))
                        cp.orderFront(nil)
                    }.frame(width: 80, height: 30)
                    
                    Button("Color2"){
                        let cp = NSColorPanel.shared
                        log.debug("new address: \(cp)")
                        log.debug("shared address: \(NSColorPanel.shared)")
                        cp.color = self.color2
                        
                        cp.orderFront(nil)
                    }.frame(width: 80, height: 30)
                    
                    CustomNSColorWell(color: $color3).frame(width: 80, height: 30)
                }
            }
        }
        .onAppear(){
            if self.coordinator == nil {
                self.coordinator = Coordinator(color: self.$color1)
            }
        }
        .onReceive(NotificationCenter.default.publisher(for: NSColorPanel.colorDidChangeNotification, object: NSColorPanel.shared), perform: { v in
            log.debug("接收到 colorpannel 消息: \(v)")

            self.color2 = NSColorPanel.shared.color
        })
    }
    
    // 自定义协同器类
    class Coordinator: NSObject {
        // 绑定 SwiftUI 中需要交互的数据
        @Binding var color: NSColor
        
        // 注意!这是在构造器中传递 @Binding 属性的正确方式
        init(color: Binding<NSColor>) {
            self._color = color
        }
        
        @objc func onColorChanged(sender: NSColorPanel){
            log.verbose("Coordinator color1 changed: \(sender)")
            self.color = sender.color
        }
    }
}

struct ColorView_Previews: PreviewProvider {
    static var previews: some View {
        ColorView()
    }
}


struct CustomNSColorWell: NSViewRepresentable {
    typealias NSViewType = NSColorWell
    
    @Binding var color: NSColor
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(color: $color)
    }
    
    func makeNSView(context: Context) -> NSColorWell {
        let colorWell = NSColorWell()
        colorWell.target = context.coordinator
        colorWell.action = #selector(Coordinator.onColorChanged(sender:))
        return colorWell
    }
    
    func updateNSView(_ nsView: NSColorWell, context: Context) {
        nsView.color = color
    }
    
    // 自定义协同器类
    class Coordinator: NSObject {
        // 绑定 SwiftUI 中需要交互的数据
        @Binding var color: NSColor
        
        // 注意!这是在构造器中传递 @Binding 属性的正确方式
        init(color: Binding<NSColor>) {
            self._color = color
        }
        
        @objc func onColorChanged(sender: NSColorWell){
            log.verbose("NSColorWell color3 changed \(sender)")
            self.color = sender.color
        }
    }
}


class MyNSColorPanel: NSColorPanel {
    deinit {
        log.debug("释放啦")
    }
}

 

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top