JetBrains / kotlin-native

Kotlin/Native infrastructure
Apache License 2.0
7.02k stars 566 forks source link

UITabBarController crash in iOS related to Objective-C subclass properties #3488

Closed Ti-m closed 4 years ago

Ti-m commented 4 years ago

Hey, when I run the following code I get a crash:

import platform.UIKit.*

class TabBarBug : UITabBarController(nibName = null, bundle = null) {
    val innerNavDashboard = UINavigationController(rootViewController = UIViewController())

    override fun viewDidLoad() {
        super.viewDidLoad()

        val tabDashboard = UITabBarItem(title = "tabbaritem-dashboard", image= null, selectedImage = null)

        this.innerNavDashboard.tabBarItem = tabDashboard

        this.viewControllers = listOf(this.innerNavDashboard) //This line crashes with -[NSNull _setViewHostsLayoutEngine:]: unrecognized selector sent to instance 0x7fff805f0330

    }
}

I think this is a bug because:

So the issue seems related to properties.


2019-10-23 14:39:33.667636+0200 MyProject[37446:1296203] -[NSNull _setViewHostsLayoutEngine:]: unrecognized selector sent to instance 0x7fff805f0330
2019-10-23 14:39:33.702761+0200 MyProject[37446:1296203] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSNull _setViewHostsLayoutEngine:]: unrecognized selector sent to instance 0x7fff805f0330'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff23baa1ee __exceptionPreprocess + 350
    1   libobjc.A.dylib                     0x00007fff50864b20 objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff23bcb154 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
    3   CoreFoundation                      0x00007fff23baef6c ___forwarding___ + 1436
    4   CoreFoundation                      0x00007fff23bb10f8 _CF_forwarding_prep_0 + 120
    5   UIKitCore                           0x00007fff46e3aa34 -[UITabBarController _setViewControllers:animated:] + 242
    6   UIKitCore                           0x00007fff46e3b76b -[UITabBarController setViewControllers:animated:] + 119
    7   my_project                      0x000000010f1db57f kfun:my.bundle.TabBarBug.objc:viewDidLoad + 1471
    8   my_project                      0x000000010f1db9de _knbridge353 + 142
    9   UIKitCore                           0x00007fff46e356cc -[UITabBarController initWithNibName:bundle:] + 237
olonho commented 4 years ago

See https://github.com/JetBrains/kotlin-native/blob/838cccf275b121878003d9c8c2f7600d78b10989/samples/uikit/src/iosMain/kotlin/ViewController.kt#L16 note @ExportObjCClass annotation.

SvyatoslavScherbina commented 4 years ago

If I convert the code to swift and run it directly in Xcode it works.

Actually this is not exactly true. Kotlin code

class TabBarBug : UITabBarController {
    val innerNavDashboard = UINavigationController(rootViewController = UIViewController())
// ...

is not equivalent to Swift code

class TabBarBug : UITabBarController {
    let innerNavDashboard = UINavigationController(rootViewController: UIViewController())
// ...

The difference is that innerNavDashboard in Kotlin is initialized after super initializer, while innerNavDashboard in Swift is initialized before super initializer. And the problem here is that viewDidLoad is called by super initializer, so in Kotlin innerNavDashboard is not initialized by this moment.

The following Swift code is (roughly) equivalent to your Kotlin reproducer:

import UIKit

class TabBarBug : UITabBarController {
    var innerNavDashboard: UINavigationController? = nil

    init() {
        super.init(nibName: nil, bundle: nil)
        self.innerNavDashboard = UINavigationController(rootViewController: UIViewController())
    }

    // Required by compiler:
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let tabDashboard = UITabBarItem(title: "tabbaritem-dashboard", image: nil, selectedImage: nil)

        self.innerNavDashboard!.tabBarItem = tabDashboard

        self.viewControllers = [self.innerNavDashboard!]
    }
}

It indeed fails.

Ti-m commented 4 years ago

See

https://github.com/JetBrains/kotlin-native/blob/838cccf275b121878003d9c8c2f7600d78b10989/samples/uikit/src/iosMain/kotlin/ViewController.kt#L16

note @ExportObjCClass annotation.

you mean this note right? https://github.com/JetBrains/kotlin-native/blob/54881b421cbdc784102d0023d93528f6523984b8/Interop/Runtime/src/native/kotlin/kotlinx/cinterop/ObjectiveCUtils.kt#L58

  • Note: runtime lookup can be forced even when the class is referenced statically from
  • Objective-C source code by adding __attribute__((objc_runtime_visible)) to its @interface.

I am not sure how to do this. Should this annotation appear in my Framework header if I add @ExportObjCClass to my class? Or do I have to add it by hand in my code?

————————————

The following Swift code is (roughly) equivalent to your Kotlin reproducer:

When I run your swift code it already crashes at:

self.innerNavDashboard!.tabBarItem = tabDashboard // Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

The line with the crash in my first post is not even reached. So you mean my crash is caused by an uninitialized value? innerNavDashboard Is declared as an non-nullable reference. So this shouldn’t be possible?

From my first post:

this.innerNavDashboard.tabBarItem = tabDashboard //In case of null reference, it should crash already here? this.viewControllers = listOf(this.innerNavDashboard) //This line crashes with -[NSNull _setViewHostsLayoutEngine:]: unrecognized selector sent to instance 0x7fff805f0330

In my real code I first tried to pass the reference for the UINavigationController as a constructor argument like this:

class TabBarBugKotlin(private val navDashboard: UINavigationController) : UITabBarController(nibName = null, bundle = null) {

    override fun viewDidLoad() {
        super.viewDidLoad()
        val tabDashboard = UITabBarItem(title = "tabbaritem-dashboard", image= null, selectedImage = null)
        this.navDashboard.tabBarItem = tabDashboard
        this.viewControllers = listOf(this.navDashboard) //crashes here
   }
 }

In this code the crash is happening too.

SvyatoslavScherbina commented 4 years ago

I am not sure how to do this. Should this annotation appear in my Framework header if I add @ExportObjCClass to my class? Or do I have to add it by hand in my code?

@ExportObjCClass is likely not related to your issue and thus not actually required here.

The line with the crash in my first post is not even reached. So you mean my crash is caused by an uninitialized value?

Yes.

innerNavDashboard Is declared as an non-nullable reference. So this shouldn’t be possible?

In fact it is possible. Uninitialized values usually cause such problems.

In this code the crash is happening too.

Exactly, because val navDashboard: UINavigationController is initialized after super initializer too.

For example, see the snippet in pure Kotlin showing this behaviour:

open class Base {
    open fun foo() {}

    init {
        foo()
    }
}

class Derived(val x: Any) : Base() {
    override fun foo() {
        println(x)
    }
}

fun main() {
    Derived(Any())
}

It prints null on JVM too.

Ti-m commented 4 years ago

Ah you're right. The compiler even generates a warning in the base class. Of course I don't have the base class in this case. ;-) I replaced viewDidLoad with viewWillAppear and it works. Thank you very much.