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 弹出的)必定是最全的颜色选择器。

 

实例代码:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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("释放啦")
}
}
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("释放啦") } }
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 *