In my last post I wrote about the Multi-touch support for Android and iOS under Delphi XE5. The article received more attention then I anticipated, so I was surprised as it seems that this is something that people need. I was happy with the Android support but far from content on the iOS. Patching official Delphi units is always bad. You have to re-patch each and every new version that comes out. Even updates can cause problems as you still use the old patched units and so you can miss the fixes in the original.

That bothered me and when something bothers me I try to fix that. I am the kind of person that does not quit easily when I set my mind upon something. So I went back to the drawing board and tried to think of something that would let me enter through the back door. I did not believe that objectiveC is so badly designed that all you have is delegation to implement interfaces with actual classes and objects. As it turned out after digging around the internet and inside EMB code objectiveC is quite elegant, far more then Delphi in that respect. I found out that not only it supports subclassing (to no surprise) but it also supports dynamic method adding and more important a technique called swizzling. This is a tool that enables you to make detours from the original method, to your own, and back to the original. Exactly what I needed. Here I have to tell you, that I never programmed in objectiveC, I never even looked at objectiveC code. So this was completely new to me with no prior knowledge. All I had was the internet and Delphis own source code.

I put together something, that should in theory capture all touch calls in every UIView in the application, so that I could intercept all touch events in a single place. It looked ok, it compiled, but then it failed with exception in runtime when my detour methods were called. After a lot of documentation reading and experimentation I was no closer to a working solution and had no clue where I was making a mistake. So I tried to ask what is wrong on StackOverflow. But as it always happens when I ask a hard question, there was no answer. SO is great for mainstream simple questions, but ask a hard one and you can bet there will be no answer. It happened to me now to many times. The next step was to try official EMB forums. As I expected again no luck. As a last resort I turned to Marco Cantu for help. I saw they use this technique in one or two places in FMX code. So I hoped someone inside EMB could help with some advice. After all they would help the community this way. I received a fast reply from Marco who told me he will get my mail to the correct person inside.

Ok so I waited. But as there was no response I went back to my code. I am very persistent 🙂 To shorten this, after a lot more digging around I found where the problem was. My parameters should be pointers and later converted to correct interfaces with “Wrapping”. Simple mistake, but hard to detect if you are not at home in objectiveC. From there on it was a smooth ride. I proved to myself it can be done. Let me show you how. You may find this useful to extend FMX code for iOS.

First you have to extend the original class with your own detour methods. Then you have to get both methods and finally you exchange their implementation. This is called swizzling.

constructor TTouchEventListener_IOS.Create;
var
  ViewClass: Pointer;
begin
  inherited;
 
  // get the UIView class
  ViewClass := objc_getClass('UIView');
 
  // get the touchesBegan and hook it
  class_addMethod(ViewClass, sel_getUid('touchesBeganDetour:withEvent:'), @touchesBeganDetour, 'v@:@@');
  touchesBeganOrig := class_getInstanceMethod(ViewClass, sel_getUid('touchesBegan:withEvent:'));
  touchesBeganRepl := class_getInstanceMethod(ViewClass, sel_getUid('touchesBeganDetour:withEvent:'));
  method_exchangeImplementations(touchesBeganOrig, touchesBeganRepl);
 
  class_addMethod(ViewClass, sel_getUid('touchesEndedDetour:withEvent:'), @touchesEndedDetour, 'v@:@@');
  touchesEndedOrig := class_getInstanceMethod(ViewClass, sel_getUid('touchesEnded:withEvent:'));
  touchesEndedRepl := class_getInstanceMethod(ViewClass, sel_getUid('touchesEndedDetour:withEvent:'));
  method_exchangeImplementations(touchesEndedOrig, touchesEndedRepl);
 
  class_addMethod(ViewClass, sel_getUid('touchesMovedDetour:withEvent:'), @touchesMovedDetour, 'v@:@@');
  touchesMovedOrig := class_getInstanceMethod(ViewClass, sel_getUid('touchesMoved:withEvent:'));
  touchesMovedRepl := class_getInstanceMethod(ViewClass, sel_getUid('touchesMovedDetour:withEvent:'));
  method_exchangeImplementations(touchesMovedOrig, touchesMovedRepl);
 
  class_addMethod(ViewClass, sel_getUid('touchesCancelledDetour:withEvent:'), @touchesCancelledDetour, 'v@:@@');
  touchesCancelledOrig := class_getInstanceMethod(ViewClass, sel_getUid('touchesCancelled:withEvent:'));
  touchesCancelledRepl := class_getInstanceMethod(ViewClass, sel_getUid('touchesCancelledDetour:withEvent:'));
  method_exchangeImplementations(touchesCancelledOrig, touchesCancelledRepl);
 
  {
  class_addMethod(ViewClass, sel_getUid('loadViewDetour:'), @loadViewDetour, 'v@:');
  loadViewOrig := class_getInstanceMethod(ViewClass, sel_getUid('loadView:'));
  loadViewRepl := class_getInstanceMethod(ViewClass, sel_getUid('loadViewDetour:'));
  method_exchangeImplementations(loadViewOrig, loadViewRepl);
  }
end;

Then you declare your own detour methods and write the implementation

procedure DoNotifyTouchEvent(const touches: NSSet;
                             const withEvent: UIEvent;
                             const EventType: TTouchEventType);
var
  I: Integer;
  Touch: UITouch;
  Event: TTouchEvent;
begin
  // do the event begin notify
  if TouchEventListener <> nil then
  begin
    SetLength(Event.Points, touches.allObjects.count);
    Event.EventType := EventType;
 
    // notify our global touch handler
    for I := 0 to touches.allObjects.count - 1 do
    begin
      Touch := TUITouch.Wrap(touches.allObjects.objectAtIndex(I));
      Event.Points[I].ID := Integer(touches.allObjects.objectAtIndex(I));
      Event.Points[I].Position.X := Touch.locationInView(Touch.View).x;
      Event.Points[I].Position.Y := Touch.locationInView(Touch.View).y;
 
      SetLength(Event.Points[I].History, 1);
      Event.Points[I].History[0].X := Touch.previousLocationInView(Touch.View).x;
      Event.Points[I].History[0].Y := Touch.previousLocationInView(Touch.View).y;
    end;
 
    TouchEventListener.Notify(Event);
  end;
end;
 
procedure touchesBeganDetour(self: id; _cmd: SEL; touches: Pointer; withEvent: Pointer); cdecl;
begin
  DoNotifyTouchEvent(TNSSet.Wrap(touches), TUIEvent.Wrap(withEvent), teDown);
end;
 
procedure touchesEndedDetour(self: id; _cmd: SEL; touches: Pointer; withEvent: Pointer); cdecl;
begin
  DoNotifyTouchEvent(TNSSet.Wrap(touches), TUIEvent.Wrap(withEvent), teUp);
end;
 
procedure touchesMovedDetour(self: id; _cmd: SEL; touches: Pointer; withEvent: Pointer); cdecl;
begin
  DoNotifyTouchEvent(TNSSet.Wrap(touches), TUIEvent.Wrap(withEvent), teMove);
end;
 
procedure touchesCancelledDetour(self: id; _cmd: SEL; touches: Pointer; withEvent: Pointer); cdecl;
begin
  DoNotifyTouchEvent(TNSSet.Wrap(touches), TUIEvent.Wrap(withEvent), teCanceled);
end;
 
procedure loadViewDetour(self: id; _cmd: SEL); cdecl;
begin
  UIView(TUIView.Wrap(self)).setMultipleTouchEnabled(True);
end;

This is basically all there is to it. I am not sure if I have to call the original methods explicitly. It seems they get called anyway. I suspect the “method_exchangeImplementations” takes care of that. You also have to enable multi-touch support for each view you use under iOS, as it is disabled by default. I tried to do that with hooking “loadView”, but I cannot get the method. This returns nil:

  loadViewOrig := class_getInstanceMethod(ViewClass, sel_getUid('loadView:'));

If anyone knows how the selector is correctly written let me know. For now you have to enable multi-touch for each form yourselves.

procedure TfMain.FormShow(Sender: TObject);
{$IFDEF IOS}
var
  V: UIView;
{$ENDIF}
begin
{$IFDEF IOS}
  V := WindowHandleToPlatform(Handle).View;
  V.setMultipleTouchEnabled(True);
{$ENDIF}
end;

Anyway I updated the demo and all the units on my blog. You can download the files from the same location as last time. No need for patching anymore. For iOS you now enable it the same way as you do for Android. There is still some work to be done on letting the use know which view or component fired the touch event and to distinguish between global and in-component coordinates. But you can also play with that yourselves. Maybe I will update the code some more, if there is enough demand. Windows implementation is not planned for now as I have no need for it and I have no Windows enabled multi-touch device. If there is interest and I am provided with the device I can try to support that to.

Multitouch