Subclassing UIView from Kotlin Native
Asked Answered
E

2

6

UIKit is designed to be used through subclasses and overridden methods.

Typically, the drawRect objective-C method of UIView is implemented like this in SWIFT:

import UIKit
import Foundation

class SmileView: UIView {
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        
        let smile = ":)" as NSString
        smile.draw(in: rect, withAttributes: nil)
    }
}

Unfortunately, the UIKit import in Kotlin defines these functions as extensions function that cannot be overridden.

Did anybody succeed in subclassing an UIView from Kotlin through a custom cinterop configuration?

Eolian answered 21/9, 2021 at 9:3 Comment(0)
E
7

So we managed to make it work.

1. Add a cinterop configuration task in the build.gradle.kts

kotlin {
    android()
    ios {
        binaries {
            framework {
                baseName = "shared"
            }
        }
        compilations.getByName("main") {
            val uikit by cinterops.creating {
            }

        }
    }

2. Add a `src/nativeinterop/cinterop/uikit.def` file.

package = demo.cinterop
language = Objective-C
---

#import <Foundation/Foundation.h>
#import <UIKit/UIView.h>

@protocol UIViewWithOverrides
- (void) drawRect:(CGRect)aRect;
- (void) layoutSubviews;
@end

3. Create a custom UIView class

The class extends the UIView from UIKit and implements the previously created UIViewWithOverridesProtocol (the suffix is automatically added)

package demo

import demo.cinterop.UIViewWithOverridesProtocol
import kotlinx.cinterop.*
import platform.CoreGraphics.*
import platform.UIKit.*

@ExportObjCClass
class MyView() : UIView(frame = CGRectMake(.0, .0, .0, .0)), UIViewWithOverridesProtocol {

    override fun layoutSubviews() {
        println("layoutSubviews")
        setNeedsDisplay()
    }

    override fun drawRect(aRect: CValue<CGRect>) {
        val rectAsString = aRect.useContents {
            "" + this.origin.x + ", " + this.origin.y + ", " + (this.origin.x +this.size.width) + ", " + (this.origin.y +this.size.height)
        }
        println("drawRect:: Rect[$rectAsString]")

        val context: CPointer<CGContext>? = UIGraphicsGetCurrentContext()
        CGContextSetLineWidth(context, 2.0)
        val components = cValuesOf(0.0, 0.0, 1.0, 1.0)
        CGContextSetFillColor(context, components)
        val square = CGRectMake(100.0, 100.0, 200.0, 200.0)
        CGContextFillRect(context, square)

    }

}

fun createMyView(): UIView = MyView()

4. Use it from Swift

struct ChartView: View {

    var body: some View {

        VStack {
            Text("Chart View")
            MyView()
                .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
        }
    }

}

struct ChartView_Previews: PreviewProvider {
    static var previews: some View {
        ChartView()
    }
}

struct MyView: UIViewRepresentable {

    func makeUIView(context: Context) -> UIView {
        UIChartViewKt.createMyView()
    }

    func updateUIView(_ uiView: UIView, context: Context) {
    }

}

Eolian answered 22/9, 2021 at 9:10 Comment(3)
Just a little notice, you can use cValue { CGRectZero } instead of CGRectMake(.0, .0, .0, .0)Poleax
Wouldnt it been easier to just annotate layoutSubviews with @ObjCAction annotation?Joellajoelle
You can't call super methods with this approach though. Might sometimes be desirable for layoutSubviews for example.West
P
0

The above answer is awesome, and it served me pretty well until I needed to override updateConstraints() - which has to call super.updateConstraints(). Without that, I was getting runtime errors, and I found no way how to do that call via the Kotlin <-> Swift interop (and now I'm reasonably sure it's really not possible).

So instead, I gave up on trying to subclass the custom UIView in Swift, and only focused on actually instantiating it from Kotlin/Native (so that it is easy to pass it the data it needs):

class CustomView : UIView {

    /* Data we need to use from the Kotlin Code */
    lazy var kotlinClass: KotlinClass? = nil

    ... init etc. ...

    override func updateConstraints() {
        ... my stuff ...
        super.updateConstraints()
    }

    override func draw(_ rect: CGRect) {
        ... call the kotlinClass' methods as you need ...
    }
}

And implemented a factory function to instantiate it:

func customViewFactory(kotlinClass: KotlinClass) -> UIView {
    return CustomView(kotlinClass: kotlinClass)
}

Then early during the app startup, I pass this factory function to the Kotlin/Native code like this:

KotlinClass.Companion.shared.setCustomViewFactory(factory: customViewFactory(kotlinClass:))

In the Kotlin part of the project (that is actually compiled before the Swift part), it looks like this:

class KotlinClass {

    companion object {
        /* To be used where I want to instantiate the custom UIView from the Kotlin code. */
        lateinit var customViewFactory: (kotlinClass: KotlinClass) -> UIView

        /* To be used early during the startup of the app from the Swift code. */
        fun setCustomViewFactory(factory: (kotlinClass: KotlinClass) -> UIView) {
            customViewFactory = factory
        }
    }

When I want to instantiate the custom UIView in the Kotlin code, I just call:

val customView = customViewFactory(this)

And then I can work with this customView as I need in the Kotlin part, even though the Kotlin part is compiled first.

Pharmacy answered 28/1, 2022 at 0:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.