Case Study: Pan Gesture Recognizer with the Intervention UIScrollView

As we all know, UIScrollView converts the Pan Gesture signal into a scrollViewDidXXX: message and sends it to its delegate, and most of the time you just need to understand the relationship between the two and listen for those messages in the Delegate. But what if you’re interfering with the Pan Gesture recognizer’s work? I mean, interfering with the recognition of a pan gesture.

In this case, if we do not choose to modify the internal mechanics of UIScrollView, we will have to choose to create a subclass.

Since the Pan Gesture recognizer of UIScrollView has consolidated his delegate into the UIScrollView that recognises this gesture recognizer, If you set its delegate to another “middleman”, you will get a runtime exception. At this point, most people would think to create a subclass. But what if you expect this change to affect other subclasses of UIScrollView as well?

In the object-oriented programming paradigm we do not encourage modification of the internal mechanics of an existing class. Because object-oriented programming is based on constantly making assertions about what it is — that what a class does makes it what it is, one of the core concepts of object-oriented programming is “extending rather than modifying.” Modifying the internals of existing classes breaks this paradigm. If you choose to modify, the “what” assertion falls apart, and the foundation of the software architecture begins to shake.

So we should never be fanatics of any particular coding sect. This time you need section-oriented programming. With this, you can interfere with the Pan Gesture recognizer’s work without having to create a new class. This can also affect subclasses that inherit from UIScrollView.

Introduction to section steering programming

Profile-oriented programming is probably the most overexplained term in the programming world.

The most similar concept to section-oriented programming I think is plant grafting.

Grafting means that a branch or bud of one plant is attached to the deep surface of the trunk or stem of another living plant, so that the branch or bud can receive nutrients from the living plant and continue to grow.

Plant grafting

Profile-guided programming is really similar to grafting.

Graft v.s. AOP

As shown in the figure above, profiler programming is concerned with three things:

  • New code added
  • profile
  • The object being operated on

We can compare the newly added code of section-guided programming to the branch or bud of a plant in grafting, the section to the deep plane, and the object being manipulated to the living plant. Thus, profil-guided programming is the process of pinning all three together.

Cross-sectional guided programming already exists in Objective-C and Swift

There is a misconception about cross-sectional programming in Objective-C: Apple does not officially support cross-sectional programming.

Isn’t.

Key-value Observation in Objective-C is an AD hoc profit-oriented programming framework, and it is an official feature brought by Apple. Key-value observations can be substituted into the previous plant grafting model:

  • The property change event trigger of the object being observed by key-value is the branch or bud of the plant (new code added)
  • The properties that can be observed by key-value are the depth plane (section)
  • The object being observed by the key-value is the living plant (the object being manipulated)

So we can see that key-value Observation is profil-guided programming, but this “profile” is “AD hoc”. What Apple doesn’t officially support is profiler oriented programming that supports a “universal” profile.

The profile guiding programming situation is more complicated in Swift. Swift supports key-value Observation by default with the help of Objective-C. But because the distribution of function calls can be determined at compile time and written into the compile product, and key-value observations are generated at run time, it is possible that the compile product will never know how to call the run-time generated code. So you need the @objc property on the properties tag that will be observed. This forces the compiler to generate code that the runtime determines is distributed by the function.

Like Objective-C, there is no support in Swift for profit-oriented programming that supports “generic” profiles.

All right. Apple made a nice framework and we were all very happy, but you still couldn’t quite achieve the intent of the Pan Gesture recognizer to intervene with UIScrollView — is that the end of the story?

No.

Implementation of profile guidance programming to support general profile

Simple path

In Objective-C, the easiest way to modify the behavior of an instance of a class without subclassing is method Swizzling. There’s a lot of material online on how to do Method swizzling in Objective-C and Swift, so I don’t want to go over it here. I want to talk about the downside of this approach.

First, Method Swizzling works on classes. If we swizzle UIScrollView, then all instances of UIScrollView and its subclasses will get the same behavior.

However, just because we’re doing profit-oriented programming doesn’t mean we’ve given up on making “what” assertions. The act of making “what” assertions is a critical step in drawing the boundaries of component responsibility and a cornerstone of any programming paradigm. Method Swizzling is an anonymous approach to modification that bypasses the “what is” assertion, easily shakes the foundation of software architecture, and is difficult to detect and track.

Furthermore, since Swift does not support the class Func Load () method for overloading Objective-C bridge classes, many articles suggest that you put your Swizzle code in class Func Initialize (). Because the app only calls an overload of Class Func Initialize () at startup for each class of each module, So you have to put all the Swizzle code from the same class into one file — otherwise you won’t know which class Func Initialize () overload will be called at startup. This ultimately leads to potential confusion in code management for Method Swizzling.

Maturation pathway

A glance at the key-value Observations of the officially supported profit-guided programming framework reveals none of the above shortcomings. How did Apple do it?

In fact, Apple uses a technique called IS-a Swizzling to implement this cross-sectional programming framework.

Is-a swizzling Is very simple, even reflected in the code — setting an IS-A pointer to one object to another class.

Foo * foo = [[Foo alloc] init];
object_setClass(foo, [Bar class]);
Copy the code

Key-value Observation is a class that creates a subclass of the object being observed, and then sets the IS-A pointer to that object to be the IS-A pointer to the newly created class. The entire process is shown in the following code:

@interface Foo: NSObject
// ...
@end

@interface NSKVONotifying_Foo: Foo
// ...
@end

NSKVONotifying_Foo * foo = [[NSKVONotifying_Foo alloc] init];
object_setClass(foo, [NSKVONotifying_Foo class]);
Copy the code

Since Apple has come up with a mature solution for “AD hoc” section-oriented programming, it should make sense to create a subclass of an object’s class and then set its IS-A pointer to that object. But when we do system design, the most important question is: Why should it work?

KVO design analysis

Open Swift Playground and type the following code:

import Cocoa

class Foo: NSObject {
    @objc var intValue: Int = 0
}

class Observer: NSObject {}let foo = Foo(a)let observer = Observer(a)We need to use `object_getClass` to check the real is-a pointer.

print(NSStringFromClass(object_getClass(foo)!) )print(NSStringFromClass(object_getClass(observer)!) ) foo.addObserver(observer, forKeyPath:"intValue", options: .new, context: nil)

print(NSStringFromClass(object_getClass(foo)!) )print(NSStringFromClass(object_getClass(observer)!) )Copy the code

You should then see the following output:

__lldb_expr_2.Foo
__lldb_expr_2.Observer
NSKVONotifying___lldb_expr_2.Foo
__lldb_expr_2.Observer
Copy the code

__lldb_exPR_2 is the name of the module generated by Swift Playground and added by the Swift compiler when bridging the Swift class to Objective-C. NSKVONotifying_ is the protective prefix generated by KVO. Foo and Observer are the class names we use in our code.

From a peek inside KVO, we can see that KVO creates a new class for the object being observed. But is it enough? I mean, is it enough to create a subclass of a class of the object being observed?

Since KVO is a mature framework, we can certainly answer “yes” intuitively. But if we do, we miss an opportunity to learn why.

In fact, because KVO looks at the properties of an object, all variables are in the observer’s event handler: [NSObject – observeValueForKeyPath: ofObject: change: context:], on the other hand, again because of the observed objects only need to send events, mechanical observed objects are fixed. This means that it is perfectly sufficient to create a subclass of an observed object’s class — because the observed objects of the same class work exactly the same.

Replace the code in Swift Playground with the following:

import Cocoa

class Foo: NSObject {
    @objc var intValue: Int = 0
}

class Observer: NSObject {}let foo = Foo(a)let observer = Observer(a)func dumpObjCClassMethods(class: AnyClass) {
    let className = NSStringFromClass(`class`)

    var methodCount: UInt32 = 0;
    let methods = class_copyMethodList(`class`, &methodCount);

    print("Found\ [methodCount) methods on\ [className) ");for i in0.. <methodCount {
        letmethod = methods! [numericCast(i)]

        let methodName = NSStringFromSelector(method_getName(method))
        let encoding = String(cString: method_getTypeEncoding(method)!)

        print("\t\(className) has method named '\(methodName)' of encoding '\(encoding)'")
    }

    free(methods)
}

foo.addObserver(observer, forKeyPath: "intValue", options: .new, context: nil)

dumpObjCClassMethods(class: object_getClass(foo)!Copy the code

So you get:

Found 4 methods on NSKVONotifying___lldb_expr_1.Foo
	NSKVONotifying___lldb_expr_1.Foo has method named 'setIntValue:' of encoding 'v24@0:8q16'
	NSKVONotifying___lldb_expr_1.Foo has method named 'class' of encoding '# 16 @ 0:8'
	NSKVONotifying___lldb_expr_1.Foo has method named 'dealloc' of encoding 'v16@0:8'
	NSKVONotifying___lldb_expr_1.Foo has method named '_isKVOA' of encoding 'c16@0:8'
Copy the code

By dumping the methods of the class created by KVO, we can notice that it overloads some methods. The purpose of overloading setIntValue: is straightforward — we’ve told the framework to observe the intValue property, so the framework overloading this method to add notification code; Class overloading must return a pseudo-IS-A pointer to the object’s original class; The purpose of the dealloc overload should be to release garbage. From the naming conventions of Cocoa, we can guess that the new method _isKVOA should be a method that returns a Boolean. We can add the following code to Swift Playground:

let isKVOA = foo.perform(NSSelectorFromString("_isKVOA"))! .toOpaque()print("isKVOA: \(isKVOA)")
Copy the code

Then we will get:

isKVOA: 0x0000000000000001
Copy the code

Since in objective-C practice, Boolean truth is stored in memory as 1, we can confirm that _isKVOA is a method that returns a Boolean value. Obviously, we can assume that _isKVOA is used to indicate whether the class is A kVO-generated class (although we don’t know what the trailing A means).

Our system

Our system is very different from KVO’s.

First, our goal was to design a profiler oriented programming system that provided “universal” profiler support. This means that you can inject custom implementations into any method of any object. This also makes it no longer appropriate to create a subclass of an injected object’s class to accommodate all changes.

Second, we want a “named” approach to code injection rather than an “anonymous,” or “anonymous,” approach. “Name by name” allows us to draw boundaries of responsibility for things, and these boundaries are the foundation of clean software architecture.

Third, we hope that the system will not introduce any mechanics that will “scare” developers.

By referring to the design of KVO, we can give the following design

  • An object should contain the target method to be injected
  • A protocol to represent the profile that defines the target injection method (forcing developers to give a specific name for this)
  • A class that implements this profile in named form. It will provide the method implementation to inject.
  • When an object is injected into a custom implementation, the system creates a subclass for it. The distinction between subclasses takes into account all injections that have been done and injections that will be done. The IS-A pointer of the object is then set to the IS-A pointer of the new subclass.
Mechanism diagram

You may have noticed that the name of the class created by our system contains the string “->”. This is an illegal character in source code. However, in an Objective-C runtime environment, these characters are allowed in class names. These characters create a guaranteed fence between system-created classes and user-created classes.

The implementation process is fairly simple until you get to the inheritance hierarchy of parsing protocol: Which methods should I inject?

Consider the following code:

@protocol Foo<NSObject>
- (void)bar;
@end
Copy the code

Since Foo inherits from the NSObject Protocol, the declaration of the method -iskindofClass: must also be included in Foo’s inheritance hierarchy. When we treat the protocol as a profile, should we inject the method -iskindofClass: into the object as well?

Obviously not.

Because the profile is a proposal for method injection, and the class provides the implementation to be injected, I have set a limit here: the system will only inject methods that have concrete implementations at the cotyledon level of the class that provides the custom implementation. This means that methods such as -iskindofClass: will not be injected if you do not provide a concrete implementation at the cotyledon level of the class that provides the custom implementation; You can, in turn, inject such methods by providing concrete implementations at the cotyledon level of classes that provide custom implementations.

Finally, this is the code repository. Then the API looks like this:

API graphic

Finally, the pan Gesture recognizer sample code for the UIScrollView intervention:

MyUIScrollViewAspect.h

@protocol MyUIScrollViewAspect<NSObject>
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
@end
Copy the code

MyUIScrollView.h

#import <UIKitUIKit.h>
@interface MyUIScrollView: UIScrollView<MyUIScrollViewAspect>
@end
Copy the code

MyUIScrollView.m

#import "MyUIScrollView.h"

@implementation MyUIScrollView
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
{
	// Do what you wanna do.
	return [super gestureRecognizerShouldBegin: gestureRecognizer];
}
@end
Copy the code

MyViewController.m

// ...
UIScrollView * scrollView = [UIScrollView alloc] init];
object_graftImplemenationOfProtocolFromClass(scrollView, @protocol(MyUIScrollViewAspect),MyUIScrollView class]);
// ...
Copy the code

After a day to talk about

I designed this framework in 2017. I had no experience at the time designing a framework that would really help ease the pain of software development, and one of the things I thought about most was drawing the boundaries of responsibility so that we could build cleaner software architectures. But software development is a process. This design may give clear software architecture a chance, but forcing developers to name a profile from the start slows development down.

Name can be named, very name.

— Lao Tzu

We name things for a purpose. If the purpose changes, the name changes with it. The composition of a pig, for example, is different from what a butcher sees from what a biologist sees. In software development, this purpose comes from how we define and interpret problems. This in turn changes as the software development process evolves. So a good framework that really helps ease the pain of building software should have part of an API that uses anonymous functions, or you could call them closures in Swift, blocks in Objective-C. This prevents us from giving a name to something before we have a full understanding of it. But since the framework was designed in 2017 and I wasn’t aware of what I mentioned above, it doesn’t support anonymous functions.

I need more research to get the framework to support anonymous functions. At least from my preliminary observation, Swift’s function reference size is as long as two words, while C’s is one. Swift compile time resolution is also a very troublesome problem. Obviously it takes a lot of work and I don’t have the time right now. But at some point in the future, it will be a reality.


The code repository mentioned in this article


Originally published on my blog (English)

This paper uses OpenCC for simplified conversion