Mobile Development 16 min read

Understanding Swift Thunk Functions and SIL: Why UITableViewDelegate Methods May Not Be Invoked

This article examines a real iOS project case where a UITableView delegate method does not fire, explores Swift Intermediate Language (SIL) and thunk functions, explains the role of @objc and dynamic dispatch, and demonstrates how generic base classes affect Objective‑C visibility of delegate methods.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Understanding Swift Thunk Functions and SIL: Why UITableViewDelegate Methods May Not Be Invoked

Introduction

This article originates from a real case encountered in a project. Using a concrete UITableView implementation example, it investigates the iOS framework details through Swift Intermediate Language (SIL). The discussion also covers thunk functions, Swift message dispatch, and iterative code modifications to verify hypotheses.

1. Problem Introduction

Below is a typical UITableView usage scenario:

protocol ListDataProtocol {}

class BaseViewController
: UIViewController, UITableViewDelegate, UITableViewDataSource {
    var tableView: UITableView
    var presenter: P?
    override func viewDidLoad() {
        // omitted non‑essential details
    }
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        // omitted non‑essential details
    }
    required init?(coder: NSCoder) {
        // omitted non‑essential details
    }
    // MARK: UITableViewDataSource
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // omitted non‑essential details
    }
    // MARK: UITableViewDelegate
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 44.0
    }
}

class ListData: NSObject, ListDataProtocol {}

class ViewController: BaseViewController
{
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
    // Called when a cell is selected
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print(indexPath.row)
    }
}

The base class implements the required UITableViewDataSource and UITableViewDelegate methods, while the subclass implements table(_:didSelectRowAt:) to handle cell selection.

The question is: when the code runs and a cell is tapped, will print(indexPath.row) be executed as expected?

Answer: No, the table(_:didSelectRowAt:) method is not called.

Because the underlying Foundation framework does not provide documentation for this behavior, the author turned to SIL to uncover low‑level details.

2. Generating SIL

swiftc -emit-silgen -target x86_64-apple-ios13.0-simulator -sdk $(xcrun --show-sdk-path --sdk iphonesimulator) -Onone test.swift > test.swift.sil

Explanation:

The -sdk flag points to the iOS SDK because UIKit is required.

-target specifies the compilation target.

-Onone disables optimizations.

3. SIL Analysis

Two SIL functions are generated for BaseViewController.viewDidLoad :

// BaseViewController.viewDidLoad()
sil hidden [ossa] @$s4test18BaseViewControllerC11viewDidLoadyyF : $@convention(method)
(@guaranteed BaseViewController
) -> () {
    // ... omitted ...
}

// @objc BaseViewController.viewDidLoad()
sil hidden [thunk] [ossa] @$s4test18BaseViewControllerC11viewDidLoadyyFTo : $@convention(objc_method)
(BaseViewController
) -> () {
    // ... omitted ...
    %3 = function_ref @$s4test18BaseViewControllerC11viewDidLoadyyF : $@convention(method) <τ_0_0 where τ_0_0 : ListDataProtocol> (@guaranteed BaseViewController<τ_0_0>) -> ()
    %4 = apply %3
(%2) : $@convention(method) <τ_0_0 where τ_0_0 : ListDataProtocol> (@guaranteed BaseViewController<τ_0_0>) -> ()
}

The first is a native Swift function; the second is a thunk that makes the method visible to Objective‑C.

Thunk Functions

A thunk is a small wrapper generated by the compiler to adapt one calling convention to another. In Swift, thunk functions are created when a Swift method must be exposed to Objective‑C (e.g., when marked with @objc ).

4. Swift Message Dispatch

Static dispatch : Direct call to the method implementation; allows inlining and compile‑time optimizations. Used for value types.

Dynamic dispatch : Runtime table lookup to find the method implementation; cannot be inlined.

Swift’s “Objective‑C dispatch” is essentially a thunk generated for methods marked @objc (and optionally dynamic ). The @objc attribute makes the symbol visible to the Objective‑C runtime; dynamic forces dynamic dispatch.

Since Swift 4, the compiler no longer infers @objc automatically for classes inheriting from NSObject ; developers must add it explicitly.

5. Answer Exploration

The focus is on the table(_:didSelectRowAt:) selector. The SIL for the subclass implementation is:

// ViewController.tableView(_:didSelectRowAt:)
sil hidden [ossa] @$s4test14ViewControllerC05tableB0_14didSelectRowAtySo07UITableB0C_10Foundation9IndexPathVtF : $@convention(method) (@guaranteed UITableView, @in_guaranteed IndexPath, @guaranteed ViewController) -> () {
    // ... omitted ...
}

This function lacks both @objc and the [thunk] attribute, meaning the compiler did not generate an Objective‑C‑visible wrapper. Consequently, UIKit’s delegate mechanism (which uses Objective‑C messaging) cannot call it, so the method is never invoked.

Why the Compiler Does Not Generate a Thunk

The compiler only creates a thunk when the method is required to be Objective‑C visible. In a generic base class, the compiler assumes the generic subclass will inherit the protocol conformance without needing a separate thunk unless the base class already provides an @objc implementation.

6. Experiments

Test 1 : Removing the generic ListDataProtocol and related code makes the subclass method output the index as expected because the compiler now generates a thunk for the non‑generic class.

Test 2 : Adding override to the subclass implementation while keeping the generic base class also results in correct output, because the overridden method is now part of the vtable with an associated thunk.

These experiments confirm that the presence of generics and the lack of an explicit @objc on the subclass method prevent thunk generation.

7. Summary

When a derived class conforms to a protocol via inheritance, Swift represents this as an inherited protocol conformance that simply references the base class’s implementation. Therefore, for a delegate method to be callable from UIKit, the base class must already expose a thunk; otherwise the subclass’s implementation remains invisible to Objective‑C.

8. Speculation

The compiler’s design likely favors compilation speed: generating thunks only when necessary avoids traversing all subclasses and creating redundant wrappers, which would be costly for large hierarchies.

References

https://en.wikipedia.org/wiki/Thunk_(programming)

http://www.ruanyifeng.com/blog/2015/05/thunk.html

https://github.com/apple/swift/blob/master/docs/SIL.rst

https://swiftunboxed.com/interop/objc-dynamic/

iOSSwiftObjective‑CSILUITableViewMessage Dispatchthunk
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.