Mouse over in a NSView subview - objective-c

i have a subclass of NSView that handles Mouse events, inside that NSView i have a subview (which is another subclass of NSView). How can i handle Mouse Events for both NSViews.
What i want to achieve is the following:
A NSView where i got a character, when i move my mouse around inside that view the character rotate to follow the mouse. inside the same there are some Items, when the mouse hover over an item i want to display some information... how can achieve this?
basically: two classes receive and respond to mouse over.
Best Regards
Kristian

Here is how we did in Swift 5:
class TrackingAreaView: NSView {
private var isMouseOverTheView = false {
didSet {
backgroundColor = isMouseOverTheView ? .red : .green
}
}
private lazy var area = makeTrackingArea()
private var backgroundColor: NSColor? {
didSet {
setNeedsDisplay(bounds)
}
}
init() {
super.init(frame: NSRect()) // Zero frame. Assuming that we are in autolayout environment.
isMouseOverTheView = false
}
required init?(coder: NSCoder) {
fatalError()
}
public override func updateTrackingAreas() {
removeTrackingArea(area)
area = makeTrackingArea()
addTrackingArea(area)
}
public override func mouseEntered(with event: NSEvent) {
isMouseOverTheView = true
}
public override func mouseExited(with event: NSEvent) {
isMouseOverTheView = false
}
private func makeTrackingArea() -> NSTrackingArea {
return NSTrackingArea(rect: bounds, options: [.mouseEnteredAndExited, .activeInKeyWindow], owner: self, userInfo: nil)
}
open override func draw(_ dirtyRect: NSRect) {
if let backgroundColor = backgroundColor {
backgroundColor.setFill()
dirtyRect.fill()
} else {
super.draw(dirtyRect)
}
}
}

i guess, you should play with CreateMouse Region and handle Mouse event like mouseenter , mouse exit on it,
refer following method of NSView
addTrackingRect : provide Rect where you would like to capture mouse event
for that region you would get following event,
mouseDown
mouseUp
mouseEntered
mouseExited
and so on

Related

Is it possible to disable "funk" error sound on global keyboard events in SwiftUI (macOS)?

I've started a SwiftUI project (it is a macOS tray application) that relies on global keyboard events (even when my application is minimized). Specifically i care about the F3 and F4 keys. While the keyboard events are registered correctly and my application is fully functional it is always playing that error "funk" sound when a key is pressed. Does anyone know how to fix this?
MyApp.swift
import SwiftUI
#main
struct MyApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var delegate;
var body: some Scene {
Settings {
ContentView()
}
}
}
class AppDelegate: NSObject,NSApplicationDelegate {
var statusItem: NSStatusItem!
var popOver: NSPopover!
func applicationDidFinishLaunching(_ notification: Notification){
let contentView = ContentView()
let popOver = NSPopover();
popOver.behavior = .transient
popOver.animates = true
popOver.contentViewController = NSHostingController(rootView: contentView)
popOver.setValue(true, forKeyPath: "shouldHideAnchor")
self.popOver = popOver
self.statusItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String : true]
let accessEnabled = AXIsProcessTrustedWithOptions(options)
if !accessEnabled {
print("Access Not Enabled")
}
// Here is where the global keypress event is registered
NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { (event) in
if (event.keyCode == 99) {
// do smth
}else if (event.keyCode == 118) {
// do smth else
}
}
}
if let MenuButton = self.statusItem.button {
MenuButton.image = NSImage(systemSymbolName: "display.2", accessibilityDescription: nil)
MenuButton.action = #selector(MenuButtonToggle)
}
}
#objc func MenuButtonToggle(_ sender: AnyObject){
if let button = self.statusItem.button {
if self.popOver.isShown{
self.popOver.performClose(sender)
}else {
self.popOver.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
self.popOver.contentViewController?.view.window?.makeKey()
}
}
}
}
It looks like you can achieve this by assigning the keys through the view directly at least, perhaps this can work on your AppDelegate also but you need to override.
Here is a working example:
struct KeyEventHandling: NSViewRepresentable {
class KeyView: NSView {
func isManagedByThisView(_ event: NSEvent) -> Bool {
//...
return true
}
override var acceptsFirstResponder: Bool { true }
override func keyDown(with event: NSEvent) {
if isManagedByThisView(event) {
print(">> key \(event.keyCode)")
} else {
super.keyDown(with: event)
}
}
}
func makeNSView(context: Context) -> NSView {
let view = KeyView()
DispatchQueue.main.async { // wait till next event cycle
view.window?.makeFirstResponder(view)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
}
}
struct ContentView: View {
var body: some View {
KeyEventHandling()
}
}
According to Documentation: "When you call super.keyDown(with: event), the event goes up through the responder chain and if no other responders process it, causes beep sound." Good Luck!
struct DisableBeepsView: NSViewRepresentable {
class KeyView: NSView {
func isManagedByThisView(_ event: NSEvent) -> Bool {
return true
}
override var acceptsFirstResponder: Bool { true }
override func keyDown(with event: NSEvent) {
if isManagedByThisView(event) {
// print(">> key \(event.keyCode)")
} else {
super.keyDown(with: event)
}
}
}
func makeNSView(context: Context) -> NSView {
let view = KeyView()
DispatchQueue.main.async { // wait till next event cycle
view.window?.makeFirstResponder(view)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}
just locate DisableBeepsView() inside your View where you need to disable beep sound

Swift - UIStackView - hide if height of all items is below threshold

I have UIStackView in vertical mode filled with UIButtons. I have dynamic screen resize and if all buttons in stack view have height under some threshold, I want to hide them. How to achive this automatically?
I have tried to extend UIButton and add:
override func layoutSubviews() {
super.layoutSubviews()
self.isHidden = (self.frame.height < 20)
}
which works, but once the button is hidden it will never re-appear and layoutSubviews is never called back (even if the height should be again larger).
It's not clear what all you are doing, or why you say it would be problematic to set the buttons' .alpha property, but here are two approaches, both using a UIStackView subclass and handling the show/hide in layoutSubviews().
1: calculate what the button heights will be and set .isHidden property:
class MyStackView: UIStackView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
axis = .vertical
distribution = .fillEqually
spacing = 8
}
override func layoutSubviews() {
super.layoutSubviews()
// approach 1
// setting .isHidden
let numViews = arrangedSubviews.count
let numSpaces = numViews - 1
let h = (bounds.height - (spacing * CGFloat(numSpaces))) / CGFloat(numViews)
let bHide = h < 20
arrangedSubviews.forEach { v in
v.isHidden = bHide
}
}
}
set .isHidden property based on what the button heights are (much simpler):
class MyStackView: UIStackView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
axis = .vertical
distribution = .fillEqually
spacing = 8
}
override func layoutSubviews() {
super.layoutSubviews()
// approach 2
// setting .alpha
arrangedSubviews.forEach { v in
v.alpha = v.frame.height < 20 ? 0.0 : 1.0
}
}
}
And here's a sample controller to see it in use. Tapping anywhere will toggle the height of the stack view between 300 and 100 (buttons will have less-than 20-pts height at 100):
class ConditionalStackViewController: UIViewController {
let stackView: MyStackView = {
let v = MyStackView()
// so we can see the stack view frame
v.backgroundColor = .systemYellow
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
var stackHeight: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
for i in 1...6 {
let b = UIButton()
b.setTitle("Button \(i)", for: [])
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
b.backgroundColor = .red
stackView.addArrangedSubview(b)
}
view.addSubview(stackView)
let g = view.safeAreaLayoutGuide
stackHeight = stackView.heightAnchor.constraint(equalToConstant: 300.0)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
stackHeight,
])
let t = UITapGestureRecognizer(target: self, action: #selector(gotTap(_:)))
view.addGestureRecognizer(t)
}
#objc func gotTap(_ g: UITapGestureRecognizer) -> Void {
stackHeight.constant = stackHeight.constant == 300 ? 100 : 300
}
}

Mkmap iOS11 clusters doesn't split up after max zoom, how to set it up?

First, my code is perfectly running.
I have well set up and mapView.register my annotation markers and cluster.
When I zoom out the annotations fusion as expected in my cluster views,
when I zoom in, same good result, except at a certain point. When too many annotations are too close from each other, the cluster view doesn't split up into my two annotation views anymore.
So I search a way to be able to setup this "zoom level" that will makes appear my two annotations even if there are really close from each other.
Here are my cluster views with a high zoom on the map:
Here if I zoom at the maximum:
Well, one of the cluster views split into two, but doesn't reveal the 4 annotations.
I also try to setup the displayPriority to be higher for my two annotations, than the cluster view, but the result is still the same.
Any ideas ?
You will need to keep track of the zoom level of the map, and reload your annotations when you cross a zoom level that you specify.
private let maxZoomLevel = 9
private var previousZoomLevel: Int?
private var currentZoomLevel: Int? {
willSet {
self.previousZoomLevel = self.currentZoomLevel
}
didSet {
// if we have crossed the max zoom level, request a refresh
// so that all annotations are redrawn with clustering enabled/disabled
guard let currentZoomLevel = self.currentZoomLevel else { return }
guard let previousZoomLevel = self.previousZoomLevel else { return }
var refreshRequired = false
if currentZoomLevel > self.maxZoomLevel && previousZoomLevel <= self.maxZoomLevel {
refreshRequired = true
}
if currentZoomLevel <= self.maxZoomLevel && previousZoomLevel > self.maxZoomLevel {
refreshRequired = true
}
if refreshRequired {
// remove the annotations and re-add them, eg
let annotations = self.mapView.annotations
self.mapView.removeAnnotations(annotations)
self.mapView.addAnnotations(annotations)
}
}
}
private var shouldCluster: Bool {
if let zoomLevel = self.currentZoomLevel, zoomLevel <= maxZoomLevel {
return false
}
return true
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
// https://stackoverflow.com/a/40616239/883413
let zoomWidth = mapView.visibleMapRect.size.width
let zoomLevel = Int(log2(zoomWidth))
self.currentZoomLevel = zoomLevel
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
// for me, annotation reuse doesn't work with clustering
let annotationView = CustomAnnotationView(annotation: annotation)
if self.shouldCluster {
annotationView.clusteringIdentifier = "custom-id"
} else {
annotationView.clusteringIdentifier = nil
}
return annotationView
}
In my case, ! EVERY TIME ! I didn't update the clusteringIdentifier
in "func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation)"
When the MKAnnotationView is reused by the mapView.dequeueReusableAnnotationView(withIdentifier: "identifier", for: annotation), the clusteringIdentifier will be nil. (reset)
That's the reason why the clusters doesn't work.
AnnotationView.swift
import MapKit
// MARK: - Define
struct AnnotationViewInfo {
static let identifier = "AnnotationView"
}
final class AnnotationView: MKAnnotationView {
// MARK: - Initializer
override init(annotation: MKAnnotation!, reuseIdentifier: String!) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
setView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setView()
}
// MARK: - Value
// MARK: Public
override var annotation: MKAnnotation? {
willSet { update(annotation: newValue) }
}
// MARK: - Function
// MARK: Private
private func setView() {
if #available(iOS 11.0, *) {
collisionMode = .rectangle
clusteringIdentifier = AnnotationViewInfo.identifier
}
canShowCallout = true
image = #imageLiteral(resourceName: "pin01").resizedImage(size: CGSize(width: #imageLiteral(resourceName: "pin01").size.width/4.0, height: #imageLiteral(resourceName: "pin01").size.height/4.0), scale: 1.0)
}
private func update(annotation: MKAnnotation?) {
if #available(iOS 11.0, *) {
clusteringIdentifier = AnnotationViewInfo.identifier
}
// TODO: Update the annotationView
}
}
MKMapViewDelegate
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if #available(iOS 11.0, *) {
switch annotation {
case is PointAnnotation: return mapView.dequeueReusableAnnotationView(withIdentifier: AnnotationView1Info.identifier, for: annotation)
case is MKClusterAnnotation: return mapView.dequeueReusableAnnotationView(withIdentifier: ClusterAnnotationViewInfo.identifier, for: annotation)
case is MKUserLocation: return nil
default: return nil
}
} else {
return nil
}
}
Key Point (You must update the "clusteringIdentifier" every time.)
private func update(annotation: MKAnnotation?) {
if #available(iOS 11.0, *) {
clusteringIdentifier = AnnotationViewInfo.identifier
}
// TODO: Update the annotationView
}
}
Sample Project Here

UIMenuItem #selector method crash in wkwebview

UIMenuItem selector method crashes in iOS 11 beta SDK.
-[WKContentView highlightText]: unrecognized selector sent to instance 0x7f85df8f3200
Method Definition:
func highlightText()
{
//
}
I try to add UIMenuItem in WKWebView,
let menuItemHighlight = UIMenuItem.init(title: "Highlight", action: #selector(ContentWebkitView.highlightText))
UIMenuController.shared.menuItems = [menuItemHighlight]
I was also getting this error when I was overriding canPerformAction and checking for my custom selector. In my case I wanted to remove all menu items except for my custom one and the following made this work for me.
class ViewController: UIViewController {
#IBOutlet weak var webView: MyWebView!
override func viewDidLoad() {
super.viewDidLoad()
loadWebView()
setupCustomMenu()
}
func loadWebView() {
let url = URL(string: "http://www.google.com")
let request = URLRequest(url: url!)
webView.load(request)
}
func setupCustomMenu() {
let customMenuItem = UIMenuItem(title: "Foo", action:
#selector(ViewController.customMenuTapped))
UIMenuController.shared.menuItems = [customMenuItem]
UIMenuController.shared.update()
}
#objc func customMenuTapped() {
let yay = "🤪🤪🤪🤪🤪🤪🤪🤪🤪🤪🤪🤪"
let alertView = UIAlertController(title: "Yay!!", message: yay, preferredStyle: .alert)
alertView.addAction(UIAlertAction(title: "cool", style: .default, handler: nil))
present(alertView, animated: true, completion: nil)
}
}
class MyWebView: WKWebView {
// turn off all other menu items
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false
}
}
OK, we finally made it work for Swift 4:
In your WKWebView subclass, add the following property and method:
// MARK: - Swizzling to avoid responder chain crash
var wkContentView: UIView? {
return self.subviewWithClassName("WKContentView")
}
private func swizzleResponderChainAction() {
wkContentView?.swizzlePerformAction()
}
Then, add an extension to UIView (I put it in the same file as my WKWebView subclass, you can make it fileprivate if you'd like)
// MARK: - Extension used for the swizzling part linked to wkContentView
extension UIView {
/// Find a subview corresponding to the className parameter, recursively.
func subviewWithClassName(_ className: String) -> UIView? {
if NSStringFromClass(type(of: self)) == className {
return self
} else {
for subview in subviews {
return subview.subviewWithClassName(className)
}
}
return nil
}
func swizzlePerformAction() {
swizzleMethod(#selector(canPerformAction), withSelector: #selector(swizzledCanPerformAction))
}
private func swizzleMethod(_ currentSelector: Selector, withSelector newSelector: Selector) {
if let currentMethod = self.instanceMethod(for: currentSelector),
let newMethod = self.instanceMethod(for:newSelector) {
let newImplementation = method_getImplementation(newMethod)
method_setImplementation(currentMethod, newImplementation)
} else {
print("Could not find originalSelector")
}
}
private func instanceMethod(for selector: Selector) -> Method? {
let classType = type(of: self)
return class_getInstanceMethod(classType, selector)
}
#objc private func swizzledCanPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false
}
}
Now the UIMenuItem works as expected:
But honestly, this really feels like a hack, and I would love Apple to fix this issue :-/
Thanks for Stephan Heilner for his answer: https://stackoverflow.com/a/42985441/4670400

Need help getting IBDesignable working

I cannot get my custom button to live preview basic examples and this is putting me off using what would otherwise be a great help to my development (IBDesignable).
My custom button code is as follows:
import Cocoa
#IBDesignable class MyButton: NSButton {
#IBInspectable var name:String = "Bob"{
didSet{
setup()
}
}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override func prepareForInterfaceBuilder() {
setup()
}
func setup(){
self.title = name
self.setNeedsDisplay()
}
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
// Drawing code here.
}
}
I then drag either a Custom view OR a NSButton onto my canvas (mainmenu.xib) and adjust its class type in the inspector window to MyButton. The inspectable field pops up and there are no errors BUT my custom button does NOT change its name when I change its value in the property panel!
Further, when I drag a custom view onto the canvas all I get is a blank/transparent rectangle in place of a button (after changing the class to MyButton).
Any assistance would be greatly appreciated. This has been driving me nuts!
I had to put the button inside a NSView:
#IBDesignable
public class MyButton: NSView {
#IBInspectable var name: String = "Bob" {
didSet{
button?.title = name
}
}
public var touchUpHandler: (() -> Void)?
private weak var button: NSButton!
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
configureView()
}
required public init?(coder: NSCoder) {
super.init(coder: coder)
configureView()
}
private func configureView() {
let button = NSButton(title: name, target: self, action: #selector(didTapButton(_:)))
button.bezelStyle = .regularSquare
button.translatesAutoresizingMaskIntoConstraints = false
addSubview(button)
NSLayoutConstraint.activate([
button.topAnchor.constraint(equalTo: topAnchor),
button.leftAnchor.constraint(equalTo: leftAnchor),
button.rightAnchor.constraint(equalTo: rightAnchor),
button.bottomAnchor.constraint(equalTo: bottomAnchor)
])
self.button = button
}
func didTapButton(_ sender: NSButton) {
touchUpHandler?()
}
}
And then I used it like so:
#IBOutlet weak var myButton: MyButton!
override func viewDidLoad() {
super.viewDidLoad()
myButton.touchUpHandler = { [unowned self] in
self.performSomeAction()
}
}