swift方法参考
import Cocoa
@IBDesignable
open class CustomButton: NSButton {
private let titleLayer = CATextLayer()
private var isMouseDown = false
public static func circularButton(title: String, radius: Double, center: CGPoint) -> CustomButton {
with(CustomButton()) {
$0.title = title
$0.frame = CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2)
$0.cornerRadius = radius
$0.font = .systemFont(ofSize: radius * 2 / 3)
}
}
override open var wantsUpdateLayer: Bool { true }
@IBInspectable public var isHandCursor: Bool = false
@IBInspectable override public var title: String {
didSet {
setTitle()
}
}
@IBInspectable public var textColor: NSColor = .labelColor {
didSet {
titleLayer.foregroundColor = textColor.cgColor
}
}
@IBInspectable public var activeTextColor: NSColor = .labelColor {
didSet {
if state == .on {
titleLayer.foregroundColor = textColor.cgColor
}
}
}
@IBInspectable public var cornerRadius: Double = 0 {
didSet {
layer?.cornerRadius = cornerRadius
}
}
@IBInspectable public var hasContinuousCorners: Bool = true {
didSet {
if #available(macOS 10.15, *) {
layer?.cornerCurve = hasContinuousCorners ? .continuous : .circular
}
}
}
@IBInspectable public var borderWidth: Double = 0 {
didSet {
layer?.borderWidth = borderWidth
}
}
@IBInspectable public var borderColor: NSColor = .clear {
didSet {
layer?.borderColor = borderColor.cgColor
}
}
@IBInspectable public var activeBorderColor: NSColor = .clear {
didSet {
if state == .on {
layer?.borderColor = activeBorderColor.cgColor
}
}
}
@IBInspectable public var backgroundColor: NSColor = .clear {
didSet {
layer?.backgroundColor = backgroundColor.cgColor
}
}
@IBInspectable public var activeBackgroundColor: NSColor = .clear {
didSet {
if state == .on {
layer?.backgroundColor = activeBackgroundColor.cgColor
}
}
}
@IBInspectable public var shadowRadius: Double = 0 {
didSet {
layer?.shadowRadius = shadowRadius
}
}
@IBInspectable public var activeShadowRadius: Double = -1 {
didSet {
if state == .on {
layer?.shadowRadius = activeShadowRadius
}
}
}
@IBInspectable public var shadowOpacity: Double = 0 {
didSet {
layer?.shadowOpacity = Float(shadowOpacity)
}
}
@IBInspectable public var activeShadowOpacity: Double = -1 {
didSet {
if state == .on {
layer?.shadowOpacity = Float(activeShadowOpacity)
}
}
}
@IBInspectable public var shadowColor: NSColor = .clear {
didSet {
layer?.shadowColor = shadowColor.cgColor
}
}
@IBInspectable public var activeShadowColor: NSColor? {
didSet {
if state == .on, let activeShadowColor = activeShadowColor {
layer?.shadowColor = activeShadowColor.cgColor
}
}
}
override public var font: NSFont? {
didSet {
setTitle()
}
}
override public var isEnabled: Bool {
didSet {
alphaValue = isEnabled ? 1 : 0.6
}
}
public convenience init() {
self.init(frame: .zero)
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
// Ensure the button doesn't draw its default contents.
override open func draw(_ dirtyRect: CGRect) {}
override open func drawFocusRingMask() {}
override open func layout() {
super.layout()
positionTitle()
}
override open func viewDidChangeBackingProperties() {
super.viewDidChangeBackingProperties()
if let scale = window?.backingScaleFactor {
layer?.contentsScale = scale
titleLayer.contentsScale = scale
}
}
private lazy var trackingArea = TrackingArea(
for: self,
options: [
.mouseEnteredAndExited,
.activeInActiveApp
]
)
override open func updateTrackingAreas() {
super.updateTrackingAreas()
trackingArea.update()
}
private func setup() {
let isOn = state == .on
wantsLayer = true
layer?.masksToBounds = false
layer?.cornerRadius = cornerRadius
layer?.borderWidth = borderWidth
layer?.shadowRadius = isOn && activeShadowRadius != -1 ? activeShadowRadius : shadowRadius
layer?.shadowOpacity = Float(isOn && activeShadowOpacity != -1 ? activeShadowOpacity : shadowOpacity)
layer?.backgroundColor = isOn ? activeBackgroundColor.cgColor : backgroundColor.cgColor
layer?.borderColor = isOn ? activeBorderColor.cgColor : borderColor.cgColor
layer?.shadowColor = isOn ? (activeShadowColor?.cgColor ?? shadowColor.cgColor) : shadowColor.cgColor
if #available(macOS 10.15, *) {
layer?.cornerCurve = hasContinuousCorners ? .continuous : .circular
}
titleLayer.alignmentMode = .center
titleLayer.contentsScale = window?.backingScaleFactor ?? 2
titleLayer.foregroundColor = isOn ? activeTextColor.cgColor : textColor.cgColor
layer?.addSublayer(titleLayer)
setTitle()
needsDisplay = true
}
public typealias ColorGenerator = () -> NSColor
private var colorGenerators = [KeyPath<CustomButton, NSColor>: ColorGenerator]()
/**
Gets or sets the color generation closure for the provided key path.
- Parameter keyPath: The key path that specifies the color related property.
*/
public subscript(colorGenerator keyPath: KeyPath<CustomButton, NSColor>) -> ColorGenerator? {
get { colorGenerators[keyPath] }
set {
colorGenerators[keyPath] = newValue
}
}
private func color(for keyPath: KeyPath<CustomButton, NSColor>) -> NSColor {
colorGenerators[keyPath]?() ?? self[keyPath: keyPath]
}
override open func updateLayer() {
animateColor()
}
private func setTitle() {
titleLayer.string = title
if let font = font {
titleLayer.font = font
titleLayer.fontSize = font.pointSize
}
needsLayout = true
}
private func positionTitle() {
let titleSize = title.size(withAttributes: [.font: font as Any])
titleLayer.frame = titleSize.centered(in: bounds).roundedOrigin()
}
private func animateColor() {
let isOn = state == .on
let duration = isOn ? 0.2 : 0.1
let backgroundColor = isOn ? color(for: \.activeBackgroundColor) : color(for: \.backgroundColor)
let textColor = isOn ? color(for: \.activeTextColor) : color(for: \.textColor)
let borderColor = isOn ? color(for: \.activeBorderColor) : color(for: \.borderColor)
let shadowColor = isOn ? (activeShadowColor ?? color(for: \.shadowColor)) : color(for: \.shadowColor)
layer?.animate(\.backgroundColor, to: backgroundColor, duration: duration)
layer?.animate(\.borderColor, to: borderColor, duration: duration)
layer?.animate(\.shadowColor, to: shadowColor, duration: duration)
titleLayer.animate(\.foregroundColor, to: textColor, duration: duration)
}
private func toggleState() {
state = state == .off ? .on : .off
animateColor()
}
override open func hitTest(_ point: CGPoint) -> NSView? {
isEnabled ? super.hitTest(point) : nil
}
override open func mouseDown(with event: NSEvent) {
isMouseDown = true
toggleState()
}
override open func mouseEntered(with event: NSEvent) {
if isHandCursor {
NSCursor.pointingHand.set()
}
if isMouseDown {
toggleState()
}
}
override open func mouseExited(with event: NSEvent) {
if isHandCursor {
NSCursor.arrow.set()
}
if isMouseDown {
toggleState()
isMouseDown = false
}
}
override open func mouseUp(with event: NSEvent) {
if isMouseDown {
isMouseDown = false
toggleState()
_ = target?.perform(action, with: self)
}
}
}
extension CustomButton: NSViewLayerContentScaleDelegate {
public func layer(_ layer: CALayer, shouldInheritContentsScale newScale: CGFloat, from window: NSWindow) -> Bool { true }
}
import Cocoa
/**
Convenience function for initializing an object and modifying its properties.
\```
let label = with(NSTextField()) {
$0.stringValue = "Foo"
$0.textColor = .systemBlue
view.addSubview($0)
}
\```
*/
@discardableResult
func with<T>(_ item: T, update: (inout T) throws -> Void) rethrows -> T {
var this = item
try update(&this)
return this
}
/**
Convenience class for adding a tracking area to a view.
\```
final class HoverView: NSView {
private lazy var trackingArea = TrackingArea(
for: self,
options: [
.mouseEnteredAndExited,
.activeInActiveApp
]
)
override func updateTrackingAreas() {
super.updateTrackingAreas()
trackingArea.update()
}
}
\```
*/
final class TrackingArea {
private weak var view: NSView?
private let rect: CGRect
private let options: NSTrackingArea.Options
private weak var trackingArea: NSTrackingArea?
/**
- Parameters:
- view: The view to add tracking to.
- rect: The area inside the view to track. Defaults to the whole view (`view.bounds`).
*/
init(
for view: NSView,
rect: CGRect? = nil,
options: NSTrackingArea.Options = []
) {
self.view = view
self.rect = rect ?? view.bounds
self.options = options
}
/**
Updates the tracking area.
- Note: This should be called in your `NSView#updateTrackingAreas()` method.
*/
func update() {
if let oldTrackingArea = trackingArea {
view?.removeTrackingArea(oldTrackingArea)
}
let newTrackingArea = NSTrackingArea(
rect: rect,
options: [
.mouseEnteredAndExited,
.activeInActiveApp
],
owner: view,
userInfo: nil
)
view?.addTrackingArea(newTrackingArea)
trackingArea = newTrackingArea
}
}
final class AnimationDelegate: NSObject, CAAnimationDelegate {
var didStopHandler: ((Bool) -> Void)?
func animationDidStop(_ animation: CAAnimation, finished flag: Bool) {
didStopHandler?(flag)
}
}
protocol LayerColorAnimation: AnyObject {}
extension CALayer: LayerColorAnimation {}
extension LayerColorAnimation where Self: CALayer {
/**
Animate colors.
*/
func animate(_ keyPath: ReferenceWritableKeyPath<Self, CGColor?>, to color: CGColor, duration: Double) {
let animation = CABasicAnimation(keyPath: keyPath.toString)
animation.fromValue = self[keyPath: keyPath]
animation.toValue = color
animation.duration = duration
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
add(animation, forKeyPath: keyPath) { [weak self] _ in
self?[keyPath: keyPath] = color
}
}
/**
Animate colors.
*/
func animate(_ keyPath: ReferenceWritableKeyPath<Self, CGColor?>, to color: NSColor, duration: Double) {
animate(keyPath, to: color.cgColor, duration: duration)
}
/**
Add color animation.
*/
func add(_ animation: CAAnimation, forKeyPath keyPath: ReferenceWritableKeyPath<Self, CGColor?>, completion: @escaping ((Bool) -> Void)) {
let animationDelegate = AnimationDelegate()
animationDelegate.didStopHandler = completion
animation.delegate = animationDelegate
add(animation, forKey: keyPath.toString)
}
}
extension CGPoint {
func rounded(_ rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero) -> Self {
Self(x: x.rounded(rule), y: y.rounded(rule))
}
}
extension CGRect {
func roundedOrigin(_ rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero) -> Self {
var rect = self
rect.origin = rect.origin.rounded(rule)
return rect
}
}
extension CGSize {
/**
Returns a CGRect with `self` centered in it.
*/
func centered(in rect: CGRect) -> CGRect {
CGRect(
x: (rect.width - width) / 2,
y: (rect.height - height) / 2,
width: width,
height: height
)
}
}
extension KeyPath where Root: NSObject {
/**
Get the string version of the key path when the root is an `NSObject`.
*/
var toString: String {
NSExpression(forKeyPath: self).keyPath
}
}
OC方法参考
《macOS开发》自定义控件之NSButton
网友评论