25

tl;dr- How can I programmatically perform a native touch in specific point on the screen?

Within the web view there is HTML that contains an Iframe, so the web view code and elements are not accessible and sanding massages to JS is not possible either. There is a button in the web view on specific coordinates. How can I press it programmatically?

JAL
  • 41,701
  • 23
  • 172
  • 300
Luda
  • 7,282
  • 12
  • 79
  • 139
  • Can you please provide more info on what you want and what you have tried now ? – Mayank Jain Jun 28 '16 at 12:05
  • 1
    Without private API, I think that you can't. Except that you call directly the function (or delegate) that handles the touch. – Duyen-Hoa Jun 28 '16 at 12:13
  • 2
    And unless what you want to do is in a Test scheme. In that case, you can use some framework like KIF to do that – Duyen-Hoa Jun 28 '16 at 12:20
  • @MayankModi, I have button on a webview. I know where the button is specifically located and I need to programmatically press it, but I can't access the webview code. So I want to simulate a touch. (Updated the question) – Luda Jun 28 '16 at 13:15
  • Why could you not scan the subviews of the page and look for one at the appropriate coordinate? – Feldur Jun 28 '16 at 14:18
  • Why do you need to detect button ? you can use delegate methods ? Can you please describe what exact you want ? – Mayank Jain Jun 29 '16 at 06:06
  • 1
    Try [this](http://stackoverflow.com/a/4034034/5654848). This is the swift version if you want: `buttonObj.sendActionsForControlEvents(.TouchUpInside)` . – Mtoklitz113 Jun 30 '16 at 17:03
  • Did you find the solution? I am looking for it with no luck. Please elaborate if your problem solved. – Masoud Dadashi May 24 '17 at 14:55

4 Answers4

6

If you don't want to simulate the touch on the JavaScript level, you can use this extension on UITouch and UIEvent, just bridge it to Swift:

func performTouchInView(view: UIView) {
    let touch = UITouch(inView: view)
    let eventDown = UIEvent(touch: touch)

    touch.view!.touchesBegan(eventDown.allTouches()!, withEvent: eventDown)

    touch.setPhase(.Ended)
    let eventUp = UIEvent(touch: touch)

    touch.view!.touchesEnded(eventUp.allTouches()!, withEvent: eventUp)
}

Your syntax may vary depending on the version of Swift you use. I'm doing some forced-unwrapping here but you should get the general idea of what is happening.

JAL
  • 41,701
  • 23
  • 172
  • 300
  • @Luda did you add the extension to your application through a bridging header? – JAL Jun 30 '16 at 13:33
  • @Luda Looking at the [UITouch private header](https://github.com/nst/iOS-Runtime-Headers/blob/master/Frameworks/UIKit.framework/UITouch.h), I see that there are ivars that handle the position of the touch: `_locationInWindow `, `_preciseLocationInWindow`, etc. You could try overriding `_setLocation:preciseLocation:inWindowResetPreviousLocation:` or `_setLocationInWindow:resetPrevious:` to explicitly set a touch point. Just expose those interfaces in the Objective-C file. – JAL Jun 30 '16 at 13:46
  • @Luda I didn't translate the extension. The extension adds additional methods on UITouch and UIEvent. If you bridge to Swift, that is what the implementation of the extension would look like. To override the other methods I mentioned, just expose them in the interface extension. – JAL Jun 30 '16 at 13:58
  • 1
    @Luda you added both the `TouchSynthesis.h` and `TouchSynthesis.m` files to your project? I can't reproduce the issue, I just created an empty Swift project and was able to compile the code in my answer no problem. – JAL Jun 30 '16 at 14:27
  • What is `performTouchLeftInView`? Oh I see, that's a class method on `UIEvent`, not `UITouch`. Use `UIEvent.performTouchLeftInView(someView)`. – JAL Jun 30 '16 at 14:28
  • @Luda I have and am not experieincing any issues. Here's what I did: 1. Create a new single view Swift application. 2. Add a bridging header 3. Add `TouchSynthesis.h` and `TouchSynthesis.m` to the project 4. add `#import "TouchSynthesis.h"` to bridging header. 5. Add Swift code in my answer to a view controller. – JAL Jun 30 '16 at 14:45
  • @Luda I should also note that I am using Xcode 7.3.1 with Swift 2.2. – JAL Jun 30 '16 at 15:11
  • Jal, I tried to reproduce your steps till 4 and built. 1. Create a new single view Swift application. 2. Add a bridging header 3. Add TouchSynthesis.h and TouchSynthesis.m to the project 4. add #import "TouchSynthesis.h" to bridging header.. Then I built it and it didn't compile. – Luda Jul 03 '16 at 13:21
  • There is a reference to this code here http://stackoverflow.com/a/1975901/1341180 and you can see people saying it is not compiling as well. Maybe you can upload your sample protect to GitHub – Luda Jul 03 '16 at 13:22
  • @Luda you're right. I was using this in a cocoapods project and while it was compiling, it was crashing at runtime. Tried adding this to an empty project and got a bunch of compilation errors. Not sure why. Still investigating... – JAL Jul 06 '16 at 13:42
  • 4
    Would an app using this get through Apple's app submission process though? – Geoff H Mar 12 '19 at 07:04
3

To simulate the touch you will need to write a javascript function in your web page that can click the button on your behalf.

Let's assume the button on the website loaded in the iFrame is coded as the following:

<a href="#" id="MagicButton" onclick="targetFunction();">Click Me!</a>

And the iFrame is coded like so in your webpage:

<iframe id="MyIFrame" src="http://www.stackoverflow.com" width="200" height="200"></iframe>

In your web page add the following javascript function to call the click event on the embedded button:

<script>
    function myJavaScriptFunction(){
        //get handle for the iFrame element
        var iFrame = document.getElementById('MyIFrame');
        var htmlDoc = iFrame.contentDocument ? iFrame.contentDocument : iFrame.contentWindow.document;
        var magicButton = htmlDoc.getElementById('MagicButton');
        magicButton.click();
    }
</script>

Going back to your Swift code, you will need to use the following API:

func evaluateJavaScript(_ javaScriptString: String,
      completionHandler completionHandler: ((AnyObject?,
                                 NSError?) -> Void)?) 

You can execute your javascript function by calling the following function after the page has loaded:

   myWKWebview.evaluateJavaScript("myJavaScriptFunction(argument1, argument2);",
                                                               completion:{ _ in })
Myles Eynon
  • 156
  • 3
  • 4
    I don't have access to the JS at all. It is encoded for security reasons. The solution has to be iOS native only – Luda Jun 30 '16 at 13:36
2

So the objective was to simulate a touch screen event on a webView.

The structure is

ViewController-
  self.glassView-|
     self.webView-|
       self.view.

I've write this class but I don't yet test:

class ViewController: UIViewController,UIWebViewDelegate {

    @IBOutlet weak var webView: UIWebView!
    @IBOutlet weak var glassView: UIView!
    var portionRect: CGRect!

    override func viewDidLoad() {
        super.viewDidLoad()
        self.webView.delegate = self
        let url = NSURL (string: "http://www.tiscali.it");
        let requestObj = NSURLRequest(URL: url!)
        self.webView.loadRequest(requestObj)
        self.view.userInteractionEnabled = true
    }

    ////////////////////////////////////////////////
    // UIWebViewDelegate

    func webViewDidStartLoad(webView: UIWebView) {
        UIApplication.sharedApplication().networkActivityIndicatorVisible = true
    }


    func webViewDidFinishLoad(webView: UIWebView) {
        UIApplication.sharedApplication().networkActivityIndicatorVisible = false
        if self.webView.loading {
            return
        } else {
            simulateTap()
        }
    }

    func webView(webView: UIWebView, didFailLoadWithError error: NSError?) {
        UIApplication.sharedApplication().networkActivityIndicatorVisible = false
    }

    func simulateTap() {
        let pointOnTheScreen = CGPointMake(156.0, 506.6)
         self.glassView.hitTest(pointOnTheScreen, withEvent: nil)
         self.glassView.overlapHitTest(pointOnTheScreen, withEvent: nil)            
         print("tap on screen")
    }

    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        if let touch = touches.first {
            let touchLocation = touch.locationInView(self.view)
            print("point touched: \(touchLocation)")
        }
        super.touchesBegan(touches, withEvent:event)
    }
}

extension UIView {
    func overlapHitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        // 1
        if !self.userInteractionEnabled || self.hidden || self.alpha == 0 {
            return nil
        }
        //2
        var hitView: UIView? = self
        if !self.pointInside(point, withEvent: event) {
            if self.clipsToBounds {
                return nil
            } else {
                hitView = nil
            }
        }
        //3
        for subview in self.subviews.reverse() {
            let insideSubview = self.convertPoint(point, toView: subview)
            if let sview = subview.overlapHitTest(insideSubview, withEvent: event) {
                return sview
            }
        }
        return hitView
    }
}
Alessandro Ornano
  • 34,887
  • 11
  • 106
  • 133
0

You also try hit test:

    let pointOnTheScreen = CGPointMake(50, 50)
    view.hitTest(pointOnTheScreen, withEvent: nil)
eNeF
  • 3,241
  • 2
  • 18
  • 41
  • Found this explanation: http://stackoverflow.com/a/4961484/1341180 Not sure how this can help me... – Luda Jul 06 '16 at 10:49