Why is NSImage dragging but not dropping? - swift

MyView has user-provided text, images, etc. Goal is for user to be able to drag an image from there to wherever an image can go. Code is customized from GitHub sample. Every part of code works as expected--all the funcs from the NSDraggingSource protocol are called when expected--and the dragged image looks exactly as it should as it drags around. But there's no drop in any destination where, for example, I can drop images dragged from other apps. What am I missing that's preventing the drop?
class MyView: NSView, NSDraggingSource {
var mouseDownEvent: NSEvent?
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
}
func draggingSession(session: NSDraggingSession, sourceOperationMaskForDraggingContext context: NSDraggingContext) -> NSDragOperation {
return .Copy
}
func draggingSession(session: NSDraggingSession, endedAtPoint screenPoint: NSPoint, operation: NSDragOperation) {
}
func draggingSession(session: NSDraggingSession, willBeginAtPoint screenPoint: NSPoint) {
}
func draggingSession(session: NSDraggingSession, movedToPoint screenPoint: NSPoint) {
}
override func mouseDown(theEvent: NSEvent) {
self.mouseDownEvent = theEvent
}
override func mouseDragged(theEvent: NSEvent) {
let mouseDown = self.mouseDownEvent!.locationInWindow
var point: NSPoint = theEvent.locationInWindow
point = anotherView!.convertPoint(point, toView: nil)
if CGRectContainsPoint(someObject.rect, point) {
let image = (someObject.contents as! NSImage)
let draggingItem = NSDraggingItem(pasteboardWriter: image)
let draggingFrameOrigin = convertPoint(mouseDown, fromView: nil)
let draggingFrame = NSRect(origin: draggingFrameOrigin, size: image.size).offsetBy(dx: -image.size.width / 2, dy: -image.size.height / 2)
draggingItem.draggingFrame = draggingFrame
draggingItem.imageComponentsProvider = {
let component = NSDraggingImageComponent(key: NSDraggingImageComponentIconKey)
component.contents = image
component.frame = NSRect(origin: NSPoint(), size: draggingFrame.size)
return [component]
}
beginDraggingSessionWithItems([draggingItem], event: mouseDownEvent!, source: self)
}
}
}
Might it have to do with dragging from an NSView and not an NSImageView? I supply image to draggingItem, but is that just for the dragging visual and not the content being dropped?
Thank you!

Related

NSItemProvider[URL] - how to COPY with drag&Drop instead of MOVE?

I have implemented function that returns NSItemProvider
func dragOutsideWnd(url: URL?) -> NSItemProvider {
if let url = url {
TheApp.appDelegate.hideMainWnd()
let provider = NSItemProvider(item: url as NSSecureCoding?, typeIdentifier: UTType.fileURL.identifier as String)
provider.suggestedName = url.lastPathComponent
//provider.copy()// This doesn't work :)
//DispatchQueue.main.async {
// TheApp.appDelegate.hideMainWnd()
//}
return provider
}
return NSItemProvider()
}
and I have use it this way:
.onDrag {
return dragOutsideWnd(url: itm.url)
}
This drag&drop action performs file MOVE action to any place of FINDER/HDD.
But how to perform COPY action?
Remember Drag&Drop is actually implemented with NSPasteboard.
I have written an example for you:
GitHub
Now the key to your questions:
To control dragging behavior(your window is the source):
Draggable objects conform to the NSDraggingSource protocol, so check the first method of the protocol:
#MainActor func draggingSession(
_ session: NSDraggingSession,
sourceOperationMaskFor context: NSDraggingContext
) -> NSDragOperation
As the method docsuggests, return different NSDragOperation in this delegation method. That includes: "Copy","Move", "Link", etc.
To control dropping behavior(your window is the destination):
NSView that accepts drop conforms to the NSDraggingDestination protocol, so you need to override the draggingEntered(_:) method by adding this code inside the DestinationView class implementation:
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation
{
var allow = true
//.copy .move, see more options in NSDragOperation, up to you.
return allow ? .copy : NSDragOperation()
}
More info form Apple's Documentation
For swiftUI, a simple show case SwiftUI Showcase
Further Reading: RayWenderlich.com has a detailed tutorial Drag and Drop Tutorial for macOS tutorial for you(needs a little swift upgrade).
Thanks a lot to answer of kakaiikaka!
The following solution works in swiftUI:
import Foundation
import SwiftUI
extension View {
func asDragable(url: URL, tapAction: #escaping () -> () , dTapAction: #escaping () -> ()) -> some View {
self.background {
DragDropView(url: url, tapAction: tapAction, dTapAction: dTapAction)
}
}
}
struct DragDropView: NSViewRepresentable {
let url: URL
let tapAction: () -> ()
let dTapAction: () -> ()
func makeNSView(context: Context) -> NSView {
return DragDropNSView(url: url, tapAction: tapAction, dTapAction: dTapAction)
}
func updateNSView(_ nsView: NSView, context: Context) { }
}
class DragDropNSView: NSView, NSDraggingSource {
let url: URL
let tapAction: () -> ()
let dTapAction: () -> ()
let imgMove: NSImage = NSImage(named: "arrow.down.doc.fill_cust")!
init(url: URL, tapAction: #escaping () -> (), dTapAction: #escaping () -> ()) {
self.url = url
self.tapAction = tapAction
self.dTapAction = dTapAction
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
return mustBeMoveAction ? .move : .copy
}
}
extension DragDropNSView: NSPasteboardItemDataProvider {
func pasteboard(_ pasteboard: NSPasteboard?, item: NSPasteboardItem, provideDataForType type: NSPasteboard.PasteboardType) {
// If the desired data type is fileURL, you load an file inside the bundle.
if let pasteboard = pasteboard, type == NSPasteboard.PasteboardType.fileURL {
pasteboard.setData(url.dataRepresentation, forType:type)
}
}
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
tapAction()
if event.clickCount == 2 {
dTapAction()
}
}
override func mouseDragged(with event: NSEvent) {
//1. Creates an NSPasteboardItem and sets this class as its data provider. A NSPasteboardItem is the box that carries the info about the item being dragged. The NSPasteboardItemDataProvider provides data upon request. In this case a file url
let pasteboardItem = NSPasteboardItem()
pasteboardItem.setDataProvider(self, forTypes: [NSPasteboard.PasteboardType.fileURL])
var rect = imgMove.alignmentRect
rect.size = NSSize(width: imgMove.size.width/2, height: imgMove.size.height/2)
//2. Creates a NSDraggingItem and assigns the pasteboard item to it
let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem)
draggingItem.setDraggingFrame(rect, contents: imgMove) // `contents` is the preview image when dragging happens.
//3. Starts the dragging session. Here you trigger the dragging image to start following your mouse until you drop it.
beginDraggingSession(with: [draggingItem], event: event, source: self)
}
}
////////////////////////////////////////
///HELPERS
///////////////////////////////////////
extension DragDropNSView {
var dragGoingOutsideWindow: Bool {
guard let currEvent = NSApplication.shared.currentEvent else { return false }
if let rect = self.window?.contentView?.visibleRect,
rect.contains(currEvent.locationInWindow)
{
return false
}
return true
}
var mustBeMoveAction: Bool {
guard let currEvent = NSApplication.shared.currentEvent else { return false }
if currEvent.modifierFlags.check(equals: [.command]) {
return true
}
return false
}
}
extension NSEvent.ModifierFlags {
func check(equals: [NSEvent.ModifierFlags] ) -> Bool {
var notEquals: [NSEvent.ModifierFlags] = [.shift, .command, .control, .option]
equals.forEach{ val in notEquals.removeFirst(where: { $0 == val }) }
var result = true
equals.forEach{ val in
if result {
result = self.contains(val)
}
}
notEquals.forEach{ val in
if result {
result = !self.contains(val)
}
}
return result
}
}
usage:
FileIcon()
.asDragable( url: recent.url, tapAction: {}, dTapAction: {})
this element will be draggable and perform MOVE in case .command key pressed.
And will perform COPY in another case
Also it performs drag action only outside widndow. But it's easy to change.

Xcode Swift MacOS App, drag and drop file into NSTextField

I'm implementing my first app for MacOS and the user should input a file path to be processed.
I have a NSTextField on my NSViewController app and I'd like to let the user just drag and drop one file there so I could get the file path, open it and put on the NSTextField some text wi the info about the file.
Can you please help me? I saw that if I make the NSTextField editable I can drop the file but I don't want the NSTextField to be editable (just selectable to copy paste info)
Thanks!
First of all, you need to read this guide.
Second, I post here some code that I use to do something similar to what you are asking.
However, my strategy is not to subclass NSTextField but rather place this field inside an NSBox, which I subclass. This has the advantage of providing to the user some visual feedback using a focus ring.
Pay attention to performDragOperation where the string value is set via the window's controller, which then forwards it to the text field to set its string value to the path to the dropped file.
You can filter what you can accept by prepareForDragOperation. Check that too.
class DropBox: NSBox
{
let dragType = NSPasteboard.PasteboardType(kUTTypeFileURL as String)
var doHighlight = false
// ---------------------------------------------------------------------------------
// awakeFromNib
// ---------------------------------------------------------------------------------
override func awakeFromNib()
{
registerForDraggedTypes([dragType])
}
// ---------------------------------------------------------------------------------
// acceptsFirstMouse
// ---------------------------------------------------------------------------------
// Accept activation click as click in window, so source doesn't have to be the
// active window
override func acceptsFirstMouse(for event: NSEvent?) -> Bool
{
return true
}
// ---------------------------------------------------------------------------------
// draggingEntered
// ---------------------------------------------------------------------------------
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation
{
let pasteboard = sender.draggingPasteboard
let mask = sender.draggingSourceOperationMask
if let types = pasteboard.types, types.contains(dragType)
{
if mask.contains(.link)
{
doHighlight = true
needsDisplay = true
return .link
}
}
return []
}
// ---------------------------------------------------------------------------------
// draggingExited
// ---------------------------------------------------------------------------------
override func draggingExited(_ sender: NSDraggingInfo?)
{
doHighlight = false
needsDisplay = true
}
// ---------------------------------------------------------------------------------
// drawRect
// ---------------------------------------------------------------------------------
override func draw(_ dirtyRect: NSRect)
{
super.draw(dirtyRect)
if doHighlight {
let rect = NSRect(x: dirtyRect.origin.x,
y: dirtyRect.origin.y,
width: NSWidth(dirtyRect),
height: NSHeight(dirtyRect) - NSHeight(titleRect) + 1.0)
NSFocusRingPlacement.only.set()
let contentRect = rect.insetBy(dx: 4, dy: 4)
NSBezierPath(rect: contentRect).fill()
}
}
// ---------------------------------------------------------------------------------
// performDragOperation
// ---------------------------------------------------------------------------------
// Method to handle drop data
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool
{
if let source = sender.draggingSource as? NSBox {
if source === self {
return false
}
}
let pasteboard = sender.draggingPasteboard
let options = [NSPasteboard.ReadingOptionKey.urlReadingFileURLsOnly:true]
if let urls = pasteboard.readObjects(forClasses: [NSURL.self], options: options) as? [URL],
let controller = self.window?.delegate as? WindowController
{
for url in urls {
if SchISCoreFileUtilities.isValid(url.path) {
controller.setApplicationPath(url.path)
return true
}
}
}
return false
}
// ---------------------------------------------------------------------------------
// prepareForDragOperation
// ---------------------------------------------------------------------------------
// Method to determine if we can accept the drop (filter for urls to apps)
override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool
{
doHighlight = false
needsDisplay = true
let pasteboard = sender.draggingPasteboard
if let types = pasteboard.types, types.contains(dragType)
{
let options = [NSPasteboard.ReadingOptionKey.urlReadingFileURLsOnly:true]
if let urls = pasteboard.readObjects(forClasses: [NSURL.self], options: options) as? [URL]
{
for url in urls {
if url.pathExtension == "app" {
return true
}
}
}
}
return false
}
}

iCarousel shows images only after resize

I have imported the iCarousel provided by nicklockwood into a macOs app.
https://github.com/nicklockwood/iCarousel
The app is written in Swift.
After managing to get everything working (importing .m and .h files, adding Bridging Header) there is one minor thing.
Once the app is started and the NSViewController is activated I see a blank ViewController. Only if I start resizing the view I see the iCarousel with all loaded picture.
My code looks like the following:
class CarouselViewController: NSViewController, iCarouselDataSource, iCarouselDelegate {
var images = [NSImage]()
#IBOutlet var carousel: iCarousel!
override func awakeFromNib() {
super.awakeFromNib()
loadImages()
}
override func viewDidLoad() {
super.viewDidLoad()
self.carousel.type = iCarouselType.coverFlow
self.carousel.dataSource = self
self.carousel.delegate = self
carousel.reloadData()
}
func loadImages() {
let filePath = "/pics/"
let paths = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.picturesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).first
let path = paths?.appending(filePath)
//
let fileManager = FileManager.default
let enumerator:FileManager.DirectoryEnumerator = fileManager.enumerator(atPath: path!)!
while let element = enumerator.nextObject() as? String {
if element.hasSuffix("jpg") || element.hasSuffix("png") {
if let image = NSImage(contentsOfFile: path! + element) {
print("File: \(path!)\(element)")
self.images.append(image)
}
}
}
}
func numberOfItems(in carousel: iCarousel) -> Int {
return images.count
}
func carousel(_ carousel: iCarousel, viewForItemAt index: Int, reusing view: NSView?) -> NSView {
let imageView = NSImageView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
imageView.image = self.images[index]
imageView.imageScaling = .scaleAxesIndependently
return imageView
}
func carouselItemWidth(_ carousel: iCarousel) -> CGFloat {
return 200.0
}
}
How can I manage to display my iCarousel without resizing first?
Thank you
I guess I've found one solution:
The drawing of the actual images in the carousel is triggered by layOutItemViews or layoutSubviews. One function that is public accessible is setType which allows to set the carousel type.
If I set the type after assigning dataSource and after loading the data, the images will be displayed fine.
So the most simple answer is to change viewDidLayout like the following:
override func viewDidLoad() {
super.viewDidLoad()
self.carousel.dataSource = self
self.carousel.delegate = self
carousel.reloadData()
// Triggers layOutItemView and thus will render all images.
self.carousel.type = iCarouselType.coverFlow
}
Now it is also possible to assign datasource via storyboard and the Connection inspector.

Creation of buttons for SpriteKit

I am creating the main menu for a sprite kit application I am building. Throughout my entire project, I have used SKScenes to hold my levels and the actual gameplay. However, now I need a main menu, which holds buttons like "Play," "Levels," "Shop," etc... However, I don't feel really comfortable the way I am adding buttons now, which is like this:
let currentButton = SKSpriteNode(imageNamed: button) // Create the SKSpriteNode that holds the button
self.addChild(currentButton) // Add that SKSpriteNode to the SKScene
And I check for the touch of the button like this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let touchLocation = touch!.location(in: self)
for node in self.nodes(at: touchLocation) {
guard let nodeName = node.name else {
continue
}
if nodeName == ButtonLabel.Play.rawValue {
DispatchQueue.main.asyncAfter(deadline: .now()) {
let transition = SKTransition.reveal(with: .left, duration: 1)
self.view?.presentScene(self.initialLevel, transition: transition)
self.initialLevel.loadStartingLevel()
}
return
}
if nodeName == ButtonLabel.Levels.rawValue {
slideOut()
}
}
}
However, I don't know if this is considered efficient. I was thinking of using UIButtons instead, but for that would I have to use an UIView?
Or can I add UIButtons to an SKView (I don't really get the difference between an SKView, SKScene, and UIView) What is recommended for menus?
I totally agree with #Whirlwind here, create a separate class for your button that handles the work for you. I do not think the advice from #ElTomato is the right advice. If you create one image with buttons included you have no flexibility on placement, size, look and button state for those buttons.
Here is a very simple button class that is a subclass of SKSpriteNode. It uses delegation to send information back to the parent (such as which button has been pushed), and gives you a simple state change (gets smaller when you click it, back to normal size when released)
import Foundation
import SpriteKit
protocol ButtonDelegate: class {
func buttonClicked(sender: Button)
}
class Button: SKSpriteNode {
//weak so that you don't create a strong circular reference with the parent
weak var delegate: ButtonDelegate!
override init(texture: SKTexture?, color: SKColor, size: CGSize) {
super.init(texture: texture, color: color, size: size)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
isUserInteractionEnabled = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
setScale(0.9)
self.delegate.buttonClicked(sender: self)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
setScale(1.0)
}
}
This button can be instantiated 2 ways. You can create an instance of it in the Scene editor, or create an instance in code.
class MenuScene: SKScene, ButtonDelegate {
private var button = Button()
override func didMove(to view: SKView) {
if let button = self.childNode(withName: "button") as? Button {
self.button = button
button.delegate = self
}
let button2 = Button(texture: nil, color: .magenta, size: CGSize(width: 200, height: 100))
button2.name = "button2"
button2.position = CGPoint(x: 0, y: 300)
button2.delegate = self
addChild(button2)
}
}
func buttonClicked(sender: Button) {
print("you clicked the button named \(sender.name!)")
}
You have to remember to make the scene conform to the delegate
class MenuScene: SKScene, ButtonDelegate
func buttonClicked(sender: Button) {
print("you clicked the button named \(sender.name!)")
}
For simple scenes what you are doing is fine, and actually preferred because you can use the .SKS file.
However, if you have a complex scene what I like to do is subclass a Sprite and then override that node's touchesBegan.
Here is a node that I use in all of my projects... It is a simple "on off" button. I use a "pointer" to a Boolean via the custom Reference class I made, so that way this node doesn't need to be concerned with your other scenes, nodes, etc--it simply changes the value of the Bool for the other bits of code to do with what they want:
public final class Reference<T> { var value: T; init(_ value: T) { self.value = value } }
// MARK: - Toggler:
public final class Toggler: SKLabelNode {
private var refBool: Reference<Bool>
var value: Bool { return refBool.value }
var labelName: String
/*
var offText = ""
var onText = ""
*/
func toggleOn() {
refBool.value = true
text = labelName + ": on"
}
func toggleOff() {
refBool.value = false
text = labelName + ": off"
}
/*init(offText: String, onText: String, refBool: Reference<Bool>) {
ref = refBool
super.init(fontNamed: "Chalkduster")
if refBool.value { toggleOn() } else { toggleOff() }
isUserInteractionEnabled = true
}
*/
init(labelName: String, refBool: Reference<Bool>) {
self.refBool = refBool
self.labelName = labelName
super.init(fontNamed: "Chalkduster")
isUserInteractionEnabled = true
self.refBool = refBool
self.labelName = labelName
if refBool.value { toggleOn() } else { toggleOff() }
}
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if refBool.value { toggleOff() } else { toggleOn() }
}
public required init?(coder aDecoder: NSCoder) { fatalError("") }
override init() {
self.refBool = Reference<Bool>(false)
self.labelName = "ERROR"
super.init()
}
};
This is a more elaborate button than say something that just runs a bit of code when you click it.
The important thing here is that if you go this route, then you need to make sure to set the node's .isUserInteractionEnabled to true or it will not receive touch input.
Another suggestion along the lines of what you are doing, is to separate the logic from the action:
// Outside of touches func:
func touchPlay() {
// Play code
}
func touchExit() {
// Exit code
}
// In touches began:
guard let name = node.name else { return }
switch name {
case "play": touchPlay()
case "exit": touchExit()
default:()
}
PS:
Here is a very basic example of how to use Toggler:
class Scene: SKScene {
let spinnyNode = SKSpriteNode(color: .blue, size: CGSize(width: 50, height: 50))
// This is the reference type instance that will be stored inside of our Toggler instance:
var shouldSpin = Reference<Bool>(true)
override func didMove(to view: SKView) {
addChild(spinnyNode)
spinnyNode.run(.repeatForever(.rotate(byAngle: 3, duration: 1)))
let toggleSpin = Toggler(labelName: "Toggle Spin", refBool: shouldSpin)
addChild(toggleSpin)
toggleSpin.position.y += 100
}
override func update(_ currentTime: TimeInterval) {
if shouldSpin.value == true {
spinnyNode.isPaused = false
} else if shouldSpin.value == false {
spinnyNode.isPaused = true
}
}
}

Callback for MSSticker Peels in iOS 10 iMessage sticker app

I'm experimenting with sticker iMessage apps in iOS 10 and I'm running into an issue with the override func didStartSending(_ message: MSMessage, conversation: MSConversation) method in MSMessagesAppViewController. When "peeling" a sticker from an MSStickerView, I would expect to receive some sort of callback on the didStartSending method. But it appears this is not the case. Does anyone know if this is the expected behavior and/or if there's another way to subscribe to callbacks for when these stickers are peeled, dragged, and dropped into the MSConversation? I realize that didStartSending is reserved for when the user taps the send button, but surely there should be some way of knowing when users drag MSStickers without hacking together some UIView dragging/rect-reading heuristic.
Messages View Controller:
class MessagesViewController: MSMessagesAppViewController {
var nYCStickersBroswerViewController: NYCStickersBroswerViewController!
override func viewDidLoad() {
super.viewDidLoad()
nYCStickersBroswerViewController = NYCStickersBroswerViewController(stickerSize: .regular)
nYCStickersBroswerViewController.view.frame = self.view.frame
self.addChildViewController(nYCStickersBroswerViewController)
nYCStickersBroswerViewController.didMove(toParentViewController: self)
self.view.addSubview(nYCStickersBroswerViewController.view)
nYCStickersBroswerViewController.loadStickers()
nYCStickersBroswerViewController.stickerBrowserView.reloadData()
}
...
override func didStartSending(_ message: MSMessage, conversation: MSConversation) {
// Called when the user taps the send button.
print(message) // should this not contain the sticker that is peeled, dragged, and dropped into the conversation?
}
}
Sticker Browser:
import Foundation
import UIKit
import Messages
class ASSticker: MSSticker {
var identifier: String?
}
class NYCStickersBroswerViewController: MSStickerBrowserViewController {
var stickers = [ASSticker]()
override func viewDidLoad() {
super.viewDidLoad()
}
func changeBrowswerViewBackgroundColor(color: UIColor) {
stickerBrowserView.backgroundColor = color
}
func loadStickers() {
createSticker(name: "brooklyn", localizedDescription: "Brooklyn Bridge Sticker")
createSticker(name: "liberty", localizedDescription: "Statue of Liberty Sticker")
createSticker(name: "love", localizedDescription: "I Love New York Sticker")
createSticker(name: "mets", localizedDescription: "New York Mets Sticker")
createSticker(name: "rangers", localizedDescription: "New York Rangers Sticker")
createSticker(name: "subway", localizedDescription: "New York City MTA Subway Train Sticker")
}
func createSticker(name: String, localizedDescription: String) {
guard let stickerPath = Bundle.main.pathForResource(name, ofType: "png") else {
print("Call ae cab, you're intoxicated.")
return
}
let stickerURL = URL(fileURLWithPath: stickerPath)
let sticker: ASSticker
do {
try sticker = ASSticker(contentsOfFileURL: stickerURL, localizedDescription: localizedDescription)
sticker.identifier = "something unique"
stickers.append(sticker)
} catch {
print("Call a cab, you're intoxicated.")
}
}
override func numberOfStickers(in stickerBrowserView: MSStickerBrowserView) -> Int {
return self.stickers.count
}
override func stickerBrowserView(_ stickerBrowserView: MSStickerBrowserView, stickerAt index: Int) -> MSSticker {
return self.stickers[index]
}
}
Here's a subclass and delegate that will tie into the tap and long press gesture recognizers that MSStickerView is using for select and peel interactions. If the implementation of MSStickerView changes this may no longer provide events, but shouldn't crash.
import UIKit
import Messages
protocol InstrumentedStickerViewDelegate: class {
func stickerViewDidSelect(stickerView: MSStickerView)
func stickerViewDidPeel(stickerView: MSStickerView)
}
class InstrumentedStickerView: MSStickerView {
weak var delegate: InstrumentedStickerViewDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
for gestureRecognizer in gestureRecognizers ?? [] {
if let tapGestureRecognizer = gestureRecognizer as? UITapGestureRecognizer {
tapGestureRecognizer.addTarget(self, action: #selector(didTap))
} else if let longPressGestureRecognizer = gestureRecognizer as? UILongPressGestureRecognizer {
longPressGestureRecognizer.addTarget(self, action: #selector(didLongPress))
}
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func didTap(tapGestureRecognizer: UITapGestureRecognizer) {
if tapGestureRecognizer.state == .Recognized {
delegate?.stickerViewDidSelect(self)
}
}
func didLongPress(longPressGestureRecognizer: UILongPressGestureRecognizer) {
if longPressGestureRecognizer.state == .Began {
delegate?.stickerViewDidPeel(self)
}
}
}
This is a workaround for sticker peeled and tapped events, it is not guaranteed that a particular sticker will be inserted but an approximation of such data points - you will have to use your subclass of MSStickerView. This solution is not futureproof, but works for the time being IMHO, therefore I welcome other ideas.
import UIKit
import Messages
class CustomStickerView : MSStickerView{
class GestureRecognizerReceiver : NSObject, UIGestureRecognizerDelegate{
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
let _recognizerDelegate = GestureRecognizerReceiver()
weak var _recognizer: UITapGestureRecognizer? = nil
func setupTapRecognizer(){
if _recognizer == nil {
let r = UITapGestureRecognizer(target: self, action: #selector(_customTapReceived))
r.cancelsTouchesInView = false
r.delegate = _recognizerDelegate
addGestureRecognizer(r)
_recognizer = r
}
}
func _customTapReceived(){
if let s = sticker{
Analytics.shared.reportEvent(name: "Sticker Inserted", description: s.localizedDescription)
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
if let s = sticker{
Analytics.shared.reportEvent(name: "Sticker Peeled", description: s.localizedDescription)
}
}
}
Usage:
let sv = CustomStickerView(frame: _stickerViewHolder.bounds, sticker: sticker)
sv.setupTapRecognizer()
_stickerViewHolder.addSubview(sv)
sv.startAnimating()