NSTextView inside NSTableView Blocks Scrolling - nstableview

I have an NSTableView and within each table row there is an NSTextView. I have disabled scrolling on the NSTextView in my storyboard. I can scroll my NSTableView just fine, but when my mouse cursor is over the top of the NSTextView, the scrolling stops. I assume this is because the NSTextView is intercepting the scroll behavior.
The arrow points to the NSTextView:
Keep in mind that the NSTextView is in a subclassed NSTableViewCell.
class AnnouncementsVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
#IBOutlet weak var tableView: NSTableView!
override func viewDidLoad() {
//...
}
func numberOfRows(in tableView: NSTableView) -> Int {
//...
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
//...
}
}
class AnnouncementTableCell: NSTableCellView{
#IBOutlet weak var message: NSTextView!
}
How do I make the NSTextView pass its scroll events up to its parent NSTableView? My guess is I will need scrollWheel(with event: NSEvent) but I'm unsure where it goes since I have two different classes at play here.

Here's a Swift (4.2) version that works great:
class MyScrollClass: NSScrollView{
override func scrollWheel(with event: NSEvent) {
self.nextResponder?.scrollWheel(with: event)
}
}
Just add that sub-class your NSScrollView and it should work.

One way to achieve this would be to subclass NSScrollView and implement scrollWheel: to conditionally (1) either consume scroll-events in your subclass-instance as needed, or (2) forward them to an enclosing scrollview, based on your instances' current scrollposition.
Here's a rather rudimentary implementation (Obj-C) that i've used in the past:
- (void)scrollWheel:(NSEvent *)e {
if(self.enclosingScrollView) {
NSSize
contSize = self.contentSize,
docSize = [self.documentView bounds].size,
scrollSize = NSMakeSize(MAX(docSize.width - contSize.width, 0.0f),
MAX(docSize.height - contSize.height,0.0f) );
NSPoint
scrollPos = self.documentVisibleRect.origin,
normPos = NSMakePoint(scrollSize.width ? scrollPos.x/scrollSize.width : 0.0,
scrollSize.height ? scrollPos.y/scrollSize.height : 0.0 );
if( ((NSView*)self.documentView).isFlipped ) normPos.y = 1.0 - normPos.y;
if( ( e.scrollingDeltaX>0.0f && normPos.x>0.0f)
|| ( e.scrollingDeltaX<0.0f && normPos.x<1.0f)
|| ( e.scrollingDeltaY<0.0f && normPos.y>0.0f)
|| ( e.scrollingDeltaY>0.0f && normPos.y<1.0f) ) {
[super scrollWheel:e];
}
else
[self.nextResponder scrollWheel:e];
}
else
// Don't bother when not nested inside another NSScrollView
[super scrollWheel:e];
}
It still leaves much to be desired, like handling the deltaX and deltaY components independantly, but perhaps it's sufficient for your case.

Related

Reorder and Move NSTableView Row with NSPasteboard

I have an NSTableView where I can drag and drop table rows to reorder them. This works by setting a drag type in my view controller:
#IBOutlet weak var tableView: NSTableView!
let dragType = NSPasteboard.PasteboardType(rawValue: "myapp.task")
override func viewDidLoad() {
super.viewDidLoad()
tableView.registerForDraggedTypes([dragType])
}
...and then implementing the reordering with these table delegate methods:
//Start drag
func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? {
let item = NSPasteboardItem()
item.setString(String(row), forType: dragType)
return item
}
//Verify proposed drop
func tableView(_ tableView: NSTableView, validateDrop info: NSDraggingInfo, proposedRow row: Int, proposedDropOperation dropOperation: NSTableView.DropOperation) -> NSDragOperation {
if dropOperation == .above {
return .move
}else{
return []
}
}
//Accept drop of one or multiple rows
func tableView(_ tableView: NSTableView, acceptDrop info: NSDraggingInfo, row: Int, dropOperation: NSTableView.DropOperation) -> Bool {
var oldIndexes = [Int]()
info.enumerateDraggingItems(options: [], for: tableView, classes: [NSPasteboardItem.self], searchOptions: [:]) { dragItem, _, _ in
if let str = (dragItem.item as! NSPasteboardItem).string(forType: self.dragType), let index = Int(str) {
oldIndexes.append(index)
}
}
//Do a bunch of logic to reorder the table rows...
}
Now, in addition to reordering my table rows, I want to be able to drag a row and drop it somewhere else in my app--sort of like moving the row to a different place.
I have a custom NSView set up as the drag destination for this, and I can drag a table row and the custom view reacts appropriately with a table row dragged over it:
class MyCustomView: NSView{
required init?(coder: NSCoder) {
super.init(coder: coder)
let taskDragType = NSPasteboard.PasteboardType(rawValue: "myapp.task")
registerForDraggedTypes([taskDragType])
}
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
//...
}
override func draggingExited(_ sender: NSDraggingInfo?) {
//...
}
}
But the part I'm unclear on is how to get the table row, and its associated object set as a property on the NSTableCellView, when the drop occurs:
//This is another method in MyCustomView
override func performDragOperation(_ draggingInfo: NSDraggingInfo) -> Bool {
guard let items = draggingInfo.draggingPasteboard.pasteboardItems else{ return false }
for item in items{
print("---")
print(item) //<-- NSPasteboardItem
let index = item.propertyList(forType: NSPasteboard.PasteboardType(rawValue: "myapp.task"))
print(index) //<-- Index of the table row
//How can I also get the task object associated with the row?
}
}
I can get the index of the row, but what I need is the entire object from the row's data source so I can take action on the object it represents. My suspicion is that I need to change how I'm using pasteboardWriterForRow to put my object on the pasteboard, but I'm unsure how to do that.
How can I pass both the row index and the object to the pasteboard?
Soon after posting this, I decided to try something crazy, and it turns out I found a way to make this a lot simpler. It seems that NSPasteboard is really only necessary if you need to get stuff into and out of your app. Since I am just moving something from one part of my app to another, I can use the drag and drop delegate methods as events and handle the data myself.
First, I set up a global array for adding dragged task objects:
var draggedTasks = [Task]()
Whenever a task is dragged from my NSTableView, I add them to the array in the aforementioned delegate method where dragging starts:
//Start drag
func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? {
//Queue tasks for moving to phases or projects
draggedTasks.append(tasks[row])
//Queue row for reordering
let item = NSPasteboardItem()
item.setString(String(row), forType: dragType)
return item
}
Then where I accept the drop in MyCustomView, I take action on the draggedTasks array:
//Save dropped tasks
override func performDragOperation(_ draggingInfo: NSDraggingInfo) -> Bool {
//Do stuff to draggedTasks based on the context of where they are dropped
return true
}
This is much simpler than going down the NSPasteboard route. 🙂

Detect When A Node is Visible in a Scene

I am trying to find a way to detect (or receive notification) that a Node has been added to a Scene and is visible.
I am creating Node objects off the main JavaFx thread and add them to the Stage and Scene using Platform.runLater(). However I would like the Node object to receive notification that is has been added to the Scene and is visible, for example I wish to trigger an animation to start.
I can't seem to find any property or method to add a listener to capture such an event. Any suggestions?
The third-party JavaFX library ReactFX has a mechanism for this, and this exact use case is cited in the blog. In short, you can do
Val<Boolean> showing = Val.flatMap(node.sceneProperty(), Scene::windowProperty)
.flatMap(Window::showingProperty);
and then of course
showing.addListener((obs, wasShowing, isNowShowing) -> {
if (isNowShowing) {
// node is showing
} else {
// node is not showing
}
});
The standard library has a version of this, but it is very badly written. (It is not typesafe, has no compile-time checking that the properties exist, and also pipes a lot of unnecessary warnings to standard error if any of the properties in the "chain" are null, even though the API docs indicate this is a supported use case.) If you want to do this with the standard JavaFX library, you can do
BooleanBinding showing = Bindings.selectBoolean(node.sceneProperty(), "window", "showing");
and then use the binding the same way as above.
Finally, you could do all this by hand, but it gets a bit ugly to manage the listeners properly:
BooleanProperty showing = new SimpleBooleanProperty();
ChangeListener<Window> windowListener = (obs, oldWindow, newWindow) -> {
showing.unbind();
if (newWindow != null) {
showing.bind(newWindow.showingProperty());
} else {
showing.set(false);
}
};
ChangeListener sceneListener = (obs, oldScene, newScene) -> {
showing.unbind();
if (oldScene != null) {
oldScene.windowProperty().removeListener(windowListener);
}
if (newScene == null) {
showing.set(false);
} else {
newScene.windowProperty().addListener(windowListener);
if (newScene.getWindow() == null) {
showing.set(false);
} else {
showing.bind(newScene.getWindow().showingProperty());
}
}
};
node.sceneProperty().addListener(sceneListener);
if (node.getScene() == null) {
showing.set(false);
} else {
node.getScene().windowProperty().add(windowListener);
if (node.getScene().getWindow() == null) {
showing.set(false);
} else {
showing.bind(node.getScene().getWindow().showingProperty());
}
}
You can add a listener to the children property of container node into which you are adding the new node.
grid.getChildren().addListener((ListChangeListener<? super Node>) change -> {
System.out.println(change.getList().get(0).getTypeSelector());
});
change.getList().get(0) returns the first node that is added to grid object.
After James's comment, I have looked up and yes, it is possible to do it from node's perspective as well. You can listen to parentProeprty's changes on the node. Following snippet shows the way to do it.
Button b = new Button("Test");
b.parentProperty().addListener((observable, oldValue, newValue) -> {
System.out.println("added to a container " + newValue);
});
answerPane.getChildren().add(b);

Trying to get button to spin in WatchKit

the code i'm using works just fine in swift for iPhone apps but not in the WatchKit 7.0 beta. the outlets and actions are different. I'm not sure what needs to change to make it work in WatchKit. please help!
import WatchKit
import Foundation
class InterfaceController: WKInterfaceController {
#IBOutlet var spinButton: WKInterfaceButton!
var isRotating = false
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
// Configure interface objects here.
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
#IBAction func spinAction() {
if !isRotating {
// create a spin animation
let spinAnimation = CABasicAnimation()
// starts from 0
spinAnimation.fromValue = 0
// goes to 360 ( 2 * π )
spinAnimation.toValue = M_PI*2
// define how long it will take to complete a 360
spinAnimation.duration = 1
// make it spin infinitely
spinAnimation.repeatCount = Float.infinity
// do not remove when completed
spinAnimation.removedOnCompletion = false
// specify the fill mode
spinAnimation.fillMode = kCAFillModeForwards
// and the animation acceleration
spinAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
// add the animation to the button layer
spinButton.layer.addAnimation(spinAnimation, forKey: "transform.rotation.z")
} else {
// remove the animation
spinButton.layer.removeAllAnimations()
}
// toggle its state
isRotating = !isRotating
}
}
You are limited to a subset of all the APIs available on iOS when developing for the watchOS.
If you want to do basic animations try out a WKInterfacePicker and change images when the digital crown is moved.
IBOutlet WKInterfacePicker *myPicker;
- (void)willActivate {
[super willActivate];
WKPickerItem *item1 = [[WKPickerItem alloc] init];
item1.contentImage = [WKImage imageWithImageName:#"Unknown.png"];
WKPickerItem *item2 = [[WKPickerItem alloc] init];
item2.contentImage = [WKImage imageWithImageName:#"Unknown-2.png"];
[self.myPicker setItems:array];
}
When the value exceeds the array count start over from index 0.
- (IBAction)myPickerAction:(NSInteger)value {
if (value % 2 == 0) {
[self.myPicker setSelectedItemIndex:-1];
}
}
This will make the WKInterfacePicker change between your images when the digital crown is rotated.

Adding observer for KVO without pointers using Swift

In Objective-C, I would normally use something like this:
static NSString *kViewTransformChanged = #"view transform changed";
// or
static const void *kViewTransformChanged = &kViewTransformChanged;
[clearContentView addObserver:self
forKeyPath:#"transform"
options:NSKeyValueObservingOptionNew
context:&kViewTransformChanged];
I have two overloaded methods to choose from to add an observer for KVO with the only difference being the context argument:
clearContentView.addObserver(observer: NSObject?, forKeyPath: String?, options: NSKeyValueObservingOptions, context: CMutableVoidPointer)
clearContentView.addObserver(observer: NSObject?, forKeyPath: String?, options: NSKeyValueObservingOptions, kvoContext: KVOContext)
With Swift not using pointers, I'm not sure how to dereference a pointer to use the first method.
If I create my own KVOContext constant for use with the second method, I wind up with it asking for this:
let test:KVOContext = KVOContext.fromVoidContext(context: CMutableVoidPointer)
EDIT: What is the difference between CMutableVoidPointer and KVOContext? Can someone give me an example how how to use them both and when I would use one over the other?
EDIT #2: A dev at Apple just posted this to the forums: KVOContext is going away; using a global reference as your context is the way to go right now.
There is now a technique officially recommended in the documentation, which is to create a private mutable variable and use its address as the context.
(Updated for Swift 3 on 2017-01-09)
// Set up non-zero-sized storage. We don't intend to mutate this variable,
// but it needs to be `var` so we can pass its address in as UnsafeMutablePointer.
private static var myContext = 0
// NOTE: `static` is not necessary if you want it to be a global variable
observee.addObserver(self, forKeyPath: …, options: [], context: &MyClass.myContext)
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
if context == &myContext {
…
}
else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
Now that KVOContext is gone in Xcode 6 beta 3, you can do the following. Define a global (i.e. not a class property) like so:
let myContext = UnsafePointer<()>()
Add an observer:
observee.addObserver(observer, forKeyPath: …, options: nil, context: myContext)
In the observer:
override func observeValueForKeyPath(keyPath: String!, ofObject object: AnyObject!, change: [NSObject : AnyObject]!, context: UnsafePointer<()>) {
if context == myContext {
…
} else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}
Swift 4 - observing contentSize change on UITableViewController popover to fix incorrect size
I had been searching for an answer to change to a block based KVO because I was getting a swiftlint warning and it took me piecing quite a few different answers together to get to the right solution. Swiftlint warning:
Block Based KVO Violation: Prefer the new block based KVO API with keypaths when using Swift 3.2 or later. (block_based_kvo).
My use case was to present a popover controller attached to a button in a Nav bar in a view controller and then resize the popover once it's showing - otherwise it would be too big and not fitting the contents of the popover. The popover itself was a UITableViewController that contained static cells, and it was displayed via a Storyboard segue with style popover.
To setup the block based observer, you need the following code inside your popover UITableViewController:
// class level variable to store the statusObserver
private var statusObserver: NSKeyValueObservation?
// Create the observer inside viewWillAppear
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
statusObserver = tableView.observe(\UITableView.contentSize,
changeHandler: { [ weak self ] (theTableView, _) in self?.popoverPresentationController?.presentedViewController.preferredContentSize = theTableView.contentSize
})
}
// Don't forget to remove the observer when the popover is dismissed.
override func viewDidDisappear(_ animated: Bool) {
if let observer = statusObserver {
observer.invalidate()
statusObserver = nil
}
super.viewDidDisappear(animated)
}
I didn't need the previous value when the observer was triggered, so left out the options: [.new, .old] when creating the observer.
Update for Swift 4
Context is not required for block-based observer function and existing #keyPath() syntax is replaced with smart keypath to achieve swift type safety.
class EventOvserverDemo {
var statusObserver:NSKeyValueObservation?
var objectToObserve:UIView?
func registerAddObserver() -> Void {
statusObserver = objectToObserve?.observe(\UIView.tag, options: [.new, .old], changeHandler: {[weak self] (player, change) in
if let tag = change.newValue {
// observed changed value and do the task here on change.
}
})
}
func unregisterObserver() -> Void {
if let sObserver = statusObserver {
sObserver.invalidate()
statusObserver = nil
}
}
}
Complete example using Swift:
//
// AppDelegate.swift
// Photos-MediaFramework-swift
//
// Created by Phurg on 11/11/16.
//
// Displays URLs for all photos in Photos Library
//
// #see http://stackoverflow.com/questions/30144547/programmatic-access-to-the-photos-library-on-mac-os-x-photokit-photos-framewo
//
import Cocoa
import MediaLibrary
// For KVO: https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/AdoptingCocoaDesignPatterns.html#//apple_ref/doc/uid/TP40014216-CH7-ID12
private var mediaLibraryLoaded = 1
private var rootMediaGroupLoaded = 2
private var mediaObjectsLoaded = 3
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
var mediaLibrary : MLMediaLibrary!
var allPhotosAlbum : MLMediaGroup!
func applicationDidFinishLaunching(_ aNotification: Notification) {
NSLog("applicationDidFinishLaunching:");
let options:[String:Any] = [
MLMediaLoadSourceTypesKey: MLMediaSourceType.image.rawValue, // Can't be Swift enum
MLMediaLoadIncludeSourcesKey: [MLMediaSourcePhotosIdentifier], // Array
]
self.mediaLibrary = MLMediaLibrary(options:options)
NSLog("applicationDidFinishLaunching: mediaLibrary=%#", self.mediaLibrary);
self.mediaLibrary.addObserver(self, forKeyPath:"mediaSources", options:[], context:&mediaLibraryLoaded)
NSLog("applicationDidFinishLaunching: added mediaSources observer");
// Force load
self.mediaLibrary.mediaSources?[MLMediaSourcePhotosIdentifier]
NSLog("applicationDidFinishLaunching: done");
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
NSLog("observeValue: keyPath=%#", keyPath!)
let mediaSource:MLMediaSource = self.mediaLibrary.mediaSources![MLMediaSourcePhotosIdentifier]!
if (context == &mediaLibraryLoaded) {
NSLog("observeValue: mediaLibraryLoaded")
mediaSource.addObserver(self, forKeyPath:"rootMediaGroup", options:[], context:&rootMediaGroupLoaded)
// Force load
mediaSource.rootMediaGroup
} else if (context == &rootMediaGroupLoaded) {
NSLog("observeValue: rootMediaGroupLoaded")
let albums:MLMediaGroup = mediaSource.mediaGroup(forIdentifier:"TopLevelAlbums")!
for album in albums.childGroups! {
let albumIdentifier:String = album.attributes["identifier"] as! String
if (albumIdentifier == "allPhotosAlbum") {
self.allPhotosAlbum = album
album.addObserver(self, forKeyPath:"mediaObjects", options:[], context:&mediaObjectsLoaded)
// Force load
album.mediaObjects
}
}
} else if (context == &mediaObjectsLoaded) {
NSLog("observeValue: mediaObjectsLoaded")
let mediaObjects:[MLMediaObject] = self.allPhotosAlbum.mediaObjects!
for mediaObject in mediaObjects {
let url:URL? = mediaObject.url
// URL does not extend NSObject, so can't be passed to NSLog; use string interpolation
NSLog("%#", "\(url)")
}
}
}
}

SKNode scale from the touched point

I have added UIPinchGestureRecognizer to my scene.view to scale my content. I actually scale the parent node where all my visible contents reside. But I have problem though with scaling point. The thing is node scale from the lower-left corner. It's definitely not what I want. Do I have to write lots of code to be able to scale from the point where pinching occurs? Could you please give some hints as to what way to follow.
I have been working on the same problem and my solution is shown below. Not sure if it is the best way to do it, but so far it seems to work. I'm using this code to zoom in and out of an SKNode that has several SKSpriteNode children. The children all move and scale with the SKNode as desired. The anchor point for the scaling is the location of the pinch gesture. The parent SKScene and other SKNodes in the scene are not affected. All of the work takes place during recognizer.state == UIGestureRecognizerStateChanged.
// instance variables of MyScene.
SKNode *_mySkNode;
UIPinchGestureRecognizer *_pinchGestureRecognizer;
- (void)didMoveToView:(SKView *)view
{
_pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(handleZoomFrom:)];
[[self view] addGestureRecognizer:_pinchGestureRecognizer];
}
// Method that is called by my UIPinchGestureRecognizer.
- (void)handleZoomFrom:(UIPinchGestureRecognizer *)recognizer
{
CGPoint anchorPoint = [recognizer locationInView:recognizer.view];
anchorPoint = [self convertPointFromView:anchorPoint];
if (recognizer.state == UIGestureRecognizerStateBegan) {
// No code needed for zooming...
} else if (recognizer.state == UIGestureRecognizerStateChanged) {
CGPoint anchorPointInMySkNode = [_mySkNode convertPoint:anchorPoint fromNode:self];
[_mySkNode setScale:(_mySkNode.xScale * recognizer.scale)];
CGPoint mySkNodeAnchorPointInScene = [self convertPoint:anchorPointInMySkNode fromNode:_mySkNode];
CGPoint translationOfAnchorInScene = CGPointSubtract(anchorPoint, mySkNodeAnchorPointInScene);
_mySkNode.position = CGPointAdd(_mySkNode.position, translationOfAnchorInScene);
recognizer.scale = 1.0;
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
// No code needed here for zooming...
}
}
The following are helper functions that were used above. They are from the Ray Wenderlich book on Sprite Kit.
SKT_INLINE CGPoint CGPointAdd(CGPoint point1, CGPoint point2) {
return CGPointMake(point1.x + point2.x, point1.y + point2.y);
}
SKT_INLINE CGPoint CGPointSubtract(CGPoint point1, CGPoint point2) {
return CGPointMake(point1.x - point2.x, point1.y - point2.y);
}
SKT_INLINE GLKVector2 GLKVector2FromCGPoint(CGPoint point) {
return GLKVector2Make(point.x, point.y);
}
SKT_INLINE CGPoint CGPointFromGLKVector2(GLKVector2 vector) {
return CGPointMake(vector.x, vector.y);
}
SKT_INLINE CGPoint CGPointMultiplyScalar(CGPoint point, CGFloat value) {
return CGPointFromGLKVector2(GLKVector2MultiplyScalar(GLKVector2FromCGPoint(point), value));
}
I have translated ninefifteen's solution for Swift and Pinch Gestures. I spent a couple days trying to get this to work on my own. Thank goodness for ninefifteen's Obj-C post! Here is the Swift version that appears to be working for me.
func scaleExperiment(_ sender: UIPinchGestureRecognizer) {
var anchorPoint = sender.location(in: sender.view)
anchorPoint = self.convertPoint(fromView: anchorPoint)
let anchorPointInMySkNode = _mySkNode.convert(anchorPoint, from: self)
_mySkNode.setScale(_mySkNode.xScale * sender.scale)
let mySkNodeAnchorPointInScene = self.convert(anchorPointInMySkNode, from: _mySkNode)
let translationOfAnchorInScene = (x: anchorPoint.x - mySkNodeAnchorPointInScene.x, y: anchorPoint.y - mySkNodeAnchorPointInScene.y)
_mySkNode.position = CGPoint(x: _mySkNode.position.x + translationOfAnchorInScene.x, y: _mySkNode.position.y + translationOfAnchorInScene.y)
sender.scale = 1.0
}
Can't zoom I don't know why but the main problem is those SKT_INLINE. I've googled them and didn't found anything about 'em... The problem is when I copy/paste them in my project the compiler tells me I have to add an ";" right after them. I wonder if that's the reason that I can zoom.
In Swift 4, my SKScene adds the UIPinchGestureRecognizer to the view but passes handling of the pinch gesture off to one of its SKNode children that is created in the scene's init(), due to some reasons not relevant here. Anyhow, this is ninefifteen's answer from the perspective of what s/he calls _mySkNode. It also includes a little code to limit the zoom and does not use the convenience functions listed at the bottom of his post. The #objc part of the declaration allows the function to be used in #selector().
Here is what is in my SKScene:
override func didMove(to view: SKView) {
let pinchRecognizer: UIPinchGestureRecognizer = UIPinchGestureRecognizer(target: self.grid, action: #selector(self.grid.pinchZoomGrid))
self.view!.addGestureRecognizer(pinchRecognizer)
}
And this is the relevant section in my SKNode:
// Pinch Management
#objc func pinchZoomGrid(_ recognizer: UIPinchGestureRecognizer){
var anchorPoint: CGPoint = recognizer.location(in: recognizer.view)
anchorPoint = self.scene!.convertPoint(fromView: anchorPoint)
if recognizer.state == .began {
// No zoom code
} else if recognizer.state == .changed {
let anchorPointInGrid = self.convert(anchorPoint, from: self.scene!)
// Start section that limits the zoom
if recognizer.scale < 1.0 {
if self.xScale * recognizer.scale < 0.6 {
self.setScale(0.6)
} else {
self.setScale(self.xScale * recognizer.scale)
}
} else if recognizer.scale > 1.0 {
if self.xScale * recognizer.scale > 1.5 {
self.setScale(1.5)
} else {
self.setScale(self.xScale * recognizer.scale)
}
}
// End section that limits the zoom
let gridAnchorPointInScene = self.scene!.convert(anchorPointInGrid, from: self)
let translationOfAnchorPointInScene = CGPoint(x:anchorPoint.x - gridAnchorPointInScene.x,
y:anchorPoint.y - gridAnchorPointInScene.y)
self.position = CGPoint(x:self.position.x + translationOfAnchorPointInScene.x,
y:self.position.y + translationOfAnchorPointInScene.y)
recognizer.scale = 1.0
} else if recognizer.state == .ended {
// No zoom code
}
}

Resources