Why NotificationCenter doesn't pass NSPoint properly? - swift

I'm trying to pass NSPoint via NotificationCenter. I have build a NSView subclass with defined three methods:
override func mouseDragged(with event: NSEvent) {
post(event: event)
}
override func mouseDown(with event: NSEvent) {
post(event: event)
}
func post(event: NSEvent) {
let point = convert(event.locationInWindow, from: nil)
print ("sending \(point)")
NotificationCenter.default.post (
name: NSNotification.Name.mousePositionChanged,
object: point )
}
I registered observer as:
NotificationCenter.default.addObserver(
self,
selector: #selector(self.mouseMovedTo(_:)),
name: NSNotification.Name.mousePositionChanged,
object: nil)
...and function to receive:
#objc func mouseMovedTo(_ point:NSPoint) {
print ("receive \(point)")
}
Notification works — when I press mouse button AppDelegate knows about it and fires mouseMovedTo but it receives only Point(0,0):
sending (0.350715866416309, 0.308759973404255)
receive (0.0, 0.0)
sending (0.350187768240343, 0.30802384118541)
receive (0.0, 0.0)
sending (0.349039364270386, 0.306409099544073)
receive (0.0, 0.0)
.....
How to pass NSPoint proper values? Is it bug?
I tried to pass NSEvent, but I had an errors when I tried to convert(event.locationInWindow, from: someView) inside AppDelegate. Some ideas?

You need
#objc func mouseMovedTo(_ notification:NSNotification) {
print ("receive \(notification.object)")
}

Related

Custom MapView Class Swift

I'm having some major issues and could use some help. I'm using some code from the company What3Words. They created a custom class:
open class MapView: MKMapView, MKMapViewDelegate, UIGestureRecognizerDelegate, CLLocationManagerDelegate {
//all their code is here
}
My goal is to use a tap gesture in order to acquire a lat/long from the map. I can do this incredibly easily when using a MapViewKit drop in and I make the object conform to MKMapView!
#IBOutlet weak var map: MKMapView!
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func tapped(_ sender: UITapGestureRecognizer) {
print("hey")
let tap = sender
let coord = tap.location(in: map)
print(coord)
print(tap.location(in: self))
print(tap.location(in: map))
map.convert(coord, to: map)
print(map.convert(coord, to: map))
}
}
But, if I conform it MKMapView! I can't access any of the specialty code within the open class.
and if I conform it to the open class I don't know how to use my tap gesture.
I'm supposed to call this code below in whatever VC I want the map to appear in:
super.viewDidLoad()
let map = MapViewController(frame: view.frame)
map.set(api: api)
map.set(center: "school.truck.lunch", latitudeSpan: 0.05, longitudeSpan: 0.05)
map.showsUserLocation = true
map.onError = { error in
self.showError(error: error)
}
self.view = map
}
// MARK: Show an Error
/// display an error using a UIAlertController, error messages conform to CustomStringConvertible
func showError(error: Error) {
DispatchQueue.main.async {
let alert = UIAlertController(title: "Error", message: String(describing: error), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Dismiss", style: .cancel, handler: nil))
self.present(alert, animated: true)
}
}
Any idea how I can get a tap gesture to work while still using this special class? I feel like I've tried everything. UGH!! Please assist.
The simplest way to do it is to add the following to your ViewController:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let location = touches.first?.location(in: map) {
map.screenCoordsToWords(point: location) { square in
print(square.words ?? "")
}
}
}
And in your MapView class add this:
func screenCoordsToWords(point: CGPoint, completion: #escaping (W3WSquare) -> () ) {
let coordinates = convert(point, toCoordinateFrom: self)
self.api?.convertTo3wa(coordinates: coordinates, language: "en") { square, error in
if let s = square {
completion(s)
}
}
}
But doing it this way can lead to confusion between MKMapView's touch event handler and your view controller. Depending on what you're trying to accomplish this might be okay, or not okay.
If it's an issue, you can instead attach a gesture recognizer inside your MapView, but there is a trick to prevent overriding the MKMapView's built in double tap recogniser:
func addGesture() {
/// when the user taps the map this is called and it gets the square info
let tap = UITapGestureRecognizer(target: self, action: #selector(tapped))
tap.numberOfTapsRequired = 1
tap.numberOfTouchesRequired = 1
// A kind of tricky thing to make sure double tap doesn't trigger single tap in MKMapView
let doubleTap = UITapGestureRecognizer(target: self, action:nil)
doubleTap.numberOfTapsRequired = 2
addGestureRecognizer(doubleTap)
tap.require(toFail: doubleTap)
tap.delegate = self
addGestureRecognizer(tap)
}
#objc func tapped(_ gestureRecognizer : UITapGestureRecognizer) {
let location = gestureRecognizer.location(in: self)
let coordinates = convert(location, toCoordinateFrom: self)
api?.convertTo3wa(coordinates: coordinates, language: "en") { square, error in
if let s = square {
print(s.words ?? "")
}
}
}
Then in your ViewController's viewDidLoad, set it up:
map.addGesture()

Swift NSNotification Observers not working

I have 2 view controllers, one with a switch that when toggled should post the following notification. In the other view controller I have Observers which should trigger the following function which just toggles a boolean. I am not able to get the observers to work and make that function call, am I doing something wrong here? I have another Notification (Doesn't trigger with user input) that is being sent in the opposite direction which works fine.
#IBAction func switchAction(_ sender: Any) {
if switchUI.isOn {
print("Collecting Data ")
NotificationCenter.default.post(name:NSNotification.Name(rawValue: "Collect"), object: self)
}
else
{
print("Not Collecting Data")
NotificationCenter.default.post(name:NSNotification.Name(rawValue: "Do Not Collect"), object: self)
}
}
func collectDataObserver () {
//Add an Observer
NotificationCenter.default.addObserver(self, selector: #selector(CentralViewController.toggleData(notification:)), name: Notification.Name(rawValue: "Collect"), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(CentralViewController.toggleData(notification:)), name: Notification.Name(rawValue: "Do Not Collect"), object: nil)
}
#objc func toggleData(notification: NSNotification) {
let isCollectData = notification.name.rawValue == "Collect"
if(isCollectData){
IsCollectingData = true
}
else
{
IsCollectingData = false
}
}
You need to call collectDataObserver() in viewDidLoad() of CentralViewController, i.e.
override func viewDidLoad() {
super.viewDidLoad()
collectDataObserver()
}

About the callback of SKStoreReviewController.requestReview()

If the review popup initiated from a view controller shows up, there isn't a way to switch the window focus back to the view controller when the popup is dismissed due to lack of callback function of SKStoreReviewController.requestReview().
I would like to make a call to becomeFirstResponder() when the review popup is dismissed. Any idea?
Is there a way to extend the SKStoreReviewController and add a callback somehow?
Warning this will probably break at some point.
Step 1: add this code to your didFinishLaunchingWithOptions
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let windowClass: AnyClass = UIWindow.self
let originalSelector: Selector = #selector(setter: UIWindow.windowLevel)
let swizzledSelector: Selector = #selector(UIWindow.setWindowLevel_startMonitor(_:))
let originalMethod = class_getInstanceMethod(windowClass, originalSelector)
let swizzledMethod = class_getInstanceMethod(windowClass, swizzledSelector)
let didAddMethod = class_addMethod(windowClass, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!))
if didAddMethod {
class_replaceMethod(windowClass, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!))
} else {
method_exchangeImplementations(originalMethod!, swizzledMethod!)
}
return true
}
Step 2: add this class
class MonitorObject: NSObject {
weak var owner: UIWindow?
init(_ owner: UIWindow?) {
super.init()
self.owner = owner
NotificationCenter.default.post(name: UIWindow.didBecomeVisibleNotification, object: self)
}
deinit {
NotificationCenter.default.post(name: UIWindow.didBecomeHiddenNotification, object: self)
}
}
Step 3: Add this UIWindow extension
private var monitorObjectKey = "monitorKey"
private var partialDescForStoreReviewWindow = "SKStore"
extension UIWindow {
// MARK: - Method Swizzling
#objc func setWindowLevel_startMonitor(_ level: Int) {
setWindowLevel_startMonitor(level)
if description.contains(partialDescForStoreReviewWindow) {
let monObj = MonitorObject(self)
objc_setAssociatedObject(self, &monitorObjectKey, monObj, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
Step 4: add this to ViewDidLoad of your controller where you want this
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
NotificationCenter.default.addObserver(self, selector: #selector(windowDidBecomeHiddenNotification(_:)), name: UIWindow.didBecomeHiddenNotification, object: nil)
}
Step 5: add the callback for the notification and check that the associated object is a match
#objc func windowDidBecomeHiddenNotification(_ notification: Notification?) {
if notification?.object is MonitorObject {
print("hello")
}
}
Now when a review dialog is closed the notification is triggered and 'print("hello") will be called.
Sometimes iOS app is losing the responder chain, like in the above example of showing StoreKit prompt. What we can do is to detect such events in UIApplication.sendAction and reactivate the first responder chain via becomeFirstResponder. UIKit will reestablish the first responder chain and we can resend the same event.
class MyApplication: UIApplication {
func reactivateResponderChainWhenFirstResponderEventWasNotHandled() {
becomeFirstResponder()
}
override func sendAction(_ action: Selector, to target: Any?, from sender: Any?, for event: UIEvent?) -> Bool {
let wasHandled = super.sendAction(action, to: target, from: sender, for: event)
if wasHandled == false, target == nil {
reactivateResponderChainWhenFirstResponderEventWasNotHandled()
return super.sendAction(action, to: target, from: sender, for: event)
}
return wasHandled
}
}
This works for me on iOS 13 and does not require any private API access.

swift animation not work in UIKeyboardWillShowNotification event

I don't know why animation work fine in UIKeyboardDidShowNotification event but not work in UIKeyboardWillShowNotification event.
Code looks like this:
anim in the comment code not work good, color will change but duration = 4 is not right
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillShow:"), name:UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillHide:"), name:UIKeyboardWillHideNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardDidShow:"), name:UIKeyboardDidShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardDidHide:"), name:UIKeyboardDidHideNotification, object: nil)
}
deinit {NSNotificationCenter.defaultCenter().removeObserver(self)}
func keyboardDidShow(notification: NSNotification) {
showAnima1()
}
func keyboardDidHide(notification: NSNotification) {
showAnima2()
}
func keyboardWillShow(notification: NSNotification) {
// showAnima1() // animate not work here
}
func keyboardWillHide(notification: NSNotification) {
// showAnima2() // animate not work here
}
func showAnima1() {
UIView.animateWithDuration(4, delay: 0, options: UIViewAnimationOptions(rawValue: 7), animations: { () -> Void in
self.view.backgroundColor = UIColor.greenColor()
}) { (finish) -> Void in
print("animate state = \(finish)")
}
}
func showAnima2() {
UIView.animateWithDuration(4, delay: 0, options: UIViewAnimationOptions(rawValue: 7), animations: { () -> Void in
self.view.backgroundColor = UIColor.whiteColor()
}) { (finish) -> Void in
print("animate state = \(finish)")
}
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {self.view.endEditing(false)}
}
Sorry, i see comment in source code says:
// Each notification includes a nil object and a userInfo dictionary containing the
// begining and ending keyboard frame in screen coordinates. Use the various UIView and
// UIWindow convertRect facilities to get the frame in the desired coordinate system.
// Animation key/value pairs are only available for the "will" family of notification.
The last comment maybe it means "did" family of notification???
I had the same problem.
It looks like the animate function has to be called from the main thread. Wrapping the animation in DispatchQueue.main.async worked for me:
#objc func keyboardWillHide(notification: NSNotification) {
DispatchQueue.main.async {
UIView.animate(withDuration: 2.0) {
self.view.frame.origin.y = 0
}
}
}

Why isn't view available?

Why do I get:
fatal error: unexpectedly found nil while unwrapping an Optional value
while switching to the next scene and back?
// Main Menu Scene
class MainMenuScene: SKScene {
override func didMoveToView(view: SKView) {
// self.backgroundColor = UIColor.whiteColor()
var timer = NSTimer.scheduledTimerWithTimeInterval(5.0, target: self, selector: "switchScene", userInfo: nil, repeats: true)
println("loaded MainMenuScene")
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
}
}
func switchScene() {
println("switching to GameScene")
var game = GameScene(size: CGSizeMake(2048, 1536))
game.scaleMode = .AspectFill
if self.view == nil {
println("no self.view")
} else {
self.view!.presentScene(game)
}
}
}
// Game Scene
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
// self.backgroundColor = UIColor.whiteColor()
var timer = NSTimer.scheduledTimerWithTimeInterval(5.0, target: self, selector: "switchScene", userInfo: nil, repeats: true)
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
}
}
func switchScene() {
// make var a member variable or this won't work
timer.invalidate()
var menu = MainMenuScene(size: CGSizeMake(2048, 1536))
menu.scaleMode = .AspectFill
self.view!.presentScene(menu)
}
}
First, change timer variable to a member.
Second, invalidate the timer.
Everything runs as expected.
From the SKScene docs:
To present a scene, you call the presentScene: method or presentScene:transition: method on the SKView class. If the scene is not currently presented, this property holds nil.
view is nil because the your GameScene (self) is not currently presented when this code runs. Your app crashes because you are force-unwrapping something that's nil.
Actually, Aaron was right. If you put remove the timer, or don't add enough time, then the view will be nil. In this instance, with the timer, by time the timer trigged, view did have a view initially.
What I was forgetting to do was invalidate() my timer before presenting the new SKScene. This kept the timer running, and when it triggers, view! is really nil because the object was removed.