UIMenuItem #selector method crash in wkwebview - crash

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

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

Creating Overlay / Polygon Mapkit Swift 5

I've tried to make an Overlay but nothing show on the map.
I want to make a 4 lines that make a shape like a square, that will show on the map (I added 4 CLLocationCoord).
What am I doing wrong?
Should I add some code?
I tried to add mapView.delegate = self but I don't know why it is doesn't work.
import UIKit
import MapKit
import CoreLocation
class ViewController: UIViewController {
#IBOutlet weak var mapView: MKMapView!
let locationManager = CLLocationManager()
let regionInMeters: Double = 1000
override func viewDidLoad() {
super.viewDidLoad()
checkLocationServices()
//calling the method
addBoundry()
}
func addBoundry(){ //creation of a polygon
var points = [CLLocationCoordinate2DMake(52.284428, 20.989394),
CLLocationCoordinate2DMake(52.224534, 21.044326),
CLLocationCoordinate2DMake(52.209182, 20.948024),
CLLocationCoordinate2DMake(52.247143, 20.918842),]
let polygon = MKPolygon(coordinates: &points, count: points.count)
mapView.addOverlay(polygon)
}
func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer {
if overlay is MKPolygon {
let polygonView = MKPolygonRenderer(overlay: overlay)
polygonView.strokeColor = .magenta
return polygonView
}
return MKOverlayRenderer()
}
func setupLocationManager(){
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
}
func centerViewOnUserLocation () {
if let location = locationManager.location?.coordinate {
let region = MKCoordinateRegion.init(center: location, latitudinalMeters: regionInMeters, longitudinalMeters: regionInMeters)
mapView.setRegion(region, animated: true)
}
}
func checkLocationServices() {
if CLLocationManager.locationServicesEnabled() {
setupLocationManager()
checkLocationAuthorization()
} else {
// Show alert letting the user know they have to turn this on.
}
}
func checkLocationAuthorization() {
switch CLLocationManager.authorizationStatus() {
case .authorizedWhenInUse:
mapView.showsUserLocation = true
centerViewOnUserLocation()
locationManager.startUpdatingLocation()
break
case .denied:
// Show alert instructing them how to turn on perm
break
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
break
case .restricted:
// Show an alert letting them know what's up
break
case .authorizedAlways:
break
}
}
}
extension ViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else {return}
let center = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
let region = MKCoordinateRegion.init(center: center, latitudinalMeters: regionInMeters, longitudinalMeters: regionInMeters)
mapView.setRegion(region, animated: true)
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
checkLocationAuthorization()
}
}
mapView.delegate = self
works when your class implements MKMapViewDelegate.
something like
class ViewController: UIViewController, MKMapViewDelegate {

Is possibile to open a PDF in SwiftUI using Quicklook?

I am developing an application that integrates a button that must refer to display a pdf file. Is it possible to do this using QuickLook?
Best Regards,
Stefano
here's some sample code.
import SwiftUI
import QuickLook
struct PreviewController: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context: Context) -> UINavigationController {
let controller = QLPreviewController()
controller.dataSource = context.coordinator
let navigationController = UINavigationController(rootViewController: controller)
return navigationController
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
class Coordinator: NSObject, QLPreviewControllerDelegate, QLPreviewControllerDataSource {
let parent: PreviewController
init(parent: PreviewController) {
self.parent = parent
}
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
return parent.url as NSURL
}
}
}

How to pass variables from one View Controller to another in WatchOS 2 & Swift

I am having a lot of problems trying to get a couple of variables from one View Controller to the next. How can I do it properly?
Here's my code below. This is the view controller where I want to be able to send the variables RedScoreW and BlueScoreW to the next window. I am asking on HOW TO DO THIS using SWIFT language and specially for WATCHOS apps.
class InterfaceController2: WKInterfaceController {
var RedScoreW = 0
var BlueScoreW = 0
#IBOutlet var WatchRedScoreLabel: WKInterfaceLabel!
#IBOutlet var WatchBlueScoreLabel: WKInterfaceLabel!
#IBAction func RedScorePlus() {
if RedScoreW == 999 {
RedScoreW = 0
WatchRedScoreLabel.setText("0")
}else {
RedScoreW += 1
WatchRedScoreLabel.setText(String(RedScoreW))
}
}
#IBAction func RedScoreMinus() {
if RedScoreW == 0 {
RedScoreW = 999
WatchRedScoreLabel.setText("999")
}
else {
RedScoreW -= 1
WatchRedScoreLabel.setText(String(RedScoreW))
}
}
#IBAction func BlueScorePlus() {
if BlueScoreW == 999 {
BlueScoreW = 0
WatchBlueScoreLabel.setText("0")
} else{
BlueScoreW += 1
WatchBlueScoreLabel.setText(String(BlueScoreW))
}
}
#IBAction func BlueScoreMinus() {
if BlueScoreW == 0 {
BlueScoreW = 999
WatchBlueScoreLabel.setText("999")
}
else {
BlueScoreW -= 1
WatchBlueScoreLabel.setText(String(BlueScoreW))
}
}
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
WatchRedScoreLabel.setText(String(RedScoreW))
WatchBlueScoreLabel.setText(String(BlueScoreW))
// 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()
}
}
And this is the Destination View Controller where I want to be able to use RedScoreW and BlueScoreW variables.
class InterfaceController3: WKInterfaceController {
#IBOutlet var finalRedScoreLabel: WKInterfaceLabel!
#IBOutlet var finalBlueScoreLabel: WKInterfaceLabel!
#IBAction func DoneAndResetButton() {
self.popToRootController()
}
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()
}
}
* EDIT *
I am trying to do it this way, this is the code where I send it, check:
#IBAction func FinishButtonPushVariables() {
arrayofScores[0] = RedScoreW
arrayofScores[1] = BlueScoreW
pushControllerWithName("LastScreen", context: arrayofScores)
}
And this is where I receive it... and it doesn't work. LOL
#IBOutlet var finalRedScoreLabel: WKInterfaceLabel!
#IBOutlet var finalBlueScoreLabel: WKInterfaceLabel!
#IBAction func DoneAndResetButton() {
self.popToRootController()
}
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
let finalarrayofScores = context as? InterfaceController2
finalBlueScoreLabel.setText(String(finalarrayofScores!.arrayofScores[1]))
finalRedScoreLabel.setText(String(finalarrayofScores!.arrayofScores[0]))
// Configure interface objects here.
}
In iOS apps, we use prepareForSegue to do this. On watchOS apps, we use contextForSegueWithIdentifier to pass a context from one interfaceController to another.
Here is a link to the class reference that will detail more about this. But here are the basics:
There are two different methods that can be used. One is for going from one interface controller to another:
func contextForSegueWithIdentifier(_ segueIdentifier: String) -> AnyObject?
The other is for going from a one interface controller to another when a row in a table is tapped:
func contextForSegueWithIdentifier(_ segueIdentifier: String, inTable table: WKInterfaceTable, rowIndex rowIndex: Int) -> AnyObject?
So one of these two methods will go in the interfaceController that is sending the context, and you will receive that context in the awakeWithContext method of the receiving interfaceController.
Here is a link to a tutorial that will show an application of this process.
EDIT
Here is a specific solution to your problem.
In the interface controller where you send it, put this code:
override func contextForSegueWithIdentifier(segueIdentifier: String) -> AnyObject? {
arrayofScores[0] = RedScoreW
arrayofScores[1] = BlueScoreW
return arrayOfScores
}
Then in your destination interface controller, put this code:
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
let finalArrayOfScores = context as? [Int]
if let f = finalArrayOfScores {
finalBlueScoreLabel.setText(String(f[1]))
finalRedScoreLabel.setText(String(f[0]))
}
}
You need to set up variables to hold your variable first.
class YourSecondViewController: UIViewController {
var yourVariable:Double?
}
Then have your button trigger your custom segue. Use your variable as the argument for sender.
class YourFirstViewController: UIViewController {
#IBAction func buttonTapped(sender: AnyObject) {
self.performSegueWithIdentifier("segue", sender: yourVariable)
}
}
Then pass the sender data by overriding the prepareForSegue method:
override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
if (segue.identifier = "segue") {
let secondViewController = segue.destinationViewController as YourSecondViewController
let yourVariable = sender as Double
secondViewController.duration = yourVariable
}
}
I guess your problem is that you are passing an array to the context and you cast it as WKIntefaceController.
Try replacing this line
let finalarrayofScores = context as? InterfaceController2
by
let finalarrayofScores = context as? [Int]

UIKeyboardDidShowNotification is called twice

In my UIViewController there are two UITextField at the bottom. So on keyboard appear I'm trying to move then above. Below is my code for handling keyboard
override func viewWillAppear(animated: Bool) {
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardShown:", name: UIKeyboardDidShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardHidden:", name: UIKeyboardDidHideNotification, object: nil)
}
override func viewWillDisappear(animated: Bool) {
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardDidShowNotification, object: nil)
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardDidHideNotification, object: nil)
}
// MARK: - Handling Notification
func keyboardShown(notification: NSNotification){
if let initialFrame = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.CGRectValue() {
let convertedFrame = self.view.convertRect(initialFrame, fromView: nil)
iY = viewLogin.frame.origin.y
var currentFrame = viewLogin.frame
currentFrame.origin.y = convertedFrame.origin.y - 140
self.viewLogin.frame = currentFrame
UIView.animateWithDuration(0.2, animations: { () -> Void in
})
}
}
func keyboardHidden(notification: NSNotification){
var currentFrame = viewLogin.frame
currentFrame.origin.y = iY
UIView.animateWithDuration(0.2, animations: { () -> Void in
self.viewLogin.frame = currentFrame
})
}
It's working fine for first UITextField. But, when I try to move to second UITextField then again keyboardShown() is called and my view move back to bottom. I'm not able to detect the actual cause. Please let me know if I'm missing anything.
Thanks in advance
Issue was related to the AutoLayout. I solved it as below
// MARK: - Handling Notification
func keyboardShown(notification: NSNotification){
self.bottomConstraint.constant += 125
UIView.animateWithDuration(0.3, animations: { () -> Void in
self.viewLogin.layoutIfNeeded()
})
}
func keyboardHidden(notification: NSNotification){
self.bottomConstraint.constant -= 125
UIView.animateWithDuration(0.3, animations: { () -> Void in
self.viewLogin.layoutIfNeeded()
})
}
We need to change the property of constraint, if we had used AutoLayout.