Subclassing UIView from Kotlin Native - ios

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?

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) {
}
}

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.

Related

Inserting UIKit content into a SwiftUI view hierarchy

I'm sorry if the title is confusing, i'm kind of new to the whole thingy.
I'm trying to integrate PassBase ID verification to my app, which is built using SwiftUI, their documentation offers instructions using Swift and view Controllers.
My question is, is there a way to insert the Swift code part into my SwiftUI view?
The code example from their Documentation:
import Passbase
import UIKit
class ViewController: UIViewController, PassbaseDelegate {
override func viewDidLoad() {
super.viewDidLoad()
PassbaseSDK.delegate = self
// Optional - You can prefill the email to skip that step.
Passbase.prefillUserEmail = "testuser#yourproject.com"
let button = PassbaseButton(frame: CGRect(x: 40, y: 90, width: 300, height: 60))
self.view.addSubview(button)
}
func onFinish(identityAccessKey: String) {
print("onFinish with identityAccessKey \(identityAccessKey)")
}
func onSubmitted(identityAccessKey: String) {
print("onSubmitted with identityAccessKey \(identityAccessKey)")
}
func onError(errorCode: String) {
print("onError with code \(errorCode)")
}
func onStart() {
print("onStart")
}
}
As i understand this part of code should create a button in a VC.
My goal is to add this button with functionality to my SwiftUI view.
Full Documentation: https://docs.passbase.com/ios#general
Thank you all in advance for the help!
The basic strategy is to use a View that represents the content you want to bring in from a UIViewController. Your View is going to conform to UIViewCotrollerRepresentable and use the functions of that protocol to create and manage the UIKit content.
The UIViewControllerRepresentable documentation is here
And, as was commented on your original post by vadian, there is A tutorial with sample code
With the sample code above, I would rename "ViewController" to be something like PassBaseViewController or PBViewController, then you would create a View that derives from UIViewControllerRepresentable
You end up with a file called PBViewController.swift that has your code from above:
import Passbase
import UIKit
class PBViewController: UIViewController, PassbaseDelegate {
override func viewDidLoad() {
super.viewDidLoad()
PassbaseSDK.delegate = self
// Optional - You can prefill the email to skip that step.
Passbase.prefillUserEmail = "testuser#yourproject.com"
let button = PassbaseButton(frame: CGRect(x: 40, y: 90, width: 300, height: 60))
self.view.addSubview(button)
}
... and the rest of the code from your question here ...
Then (probably in another file, but not necessarily) you could create the SwiftUIView that uses that view controller:
struct PassBaseView : UIViewControllerRepresentable {
typealias UIViewControllerType = PBViewController
func makeUIViewController(context: Context) -> PBViewController {
return PBViewController()
}
func updateUIViewController(_ uiViewController: PBViewController, context: Context) {
/* code here to make changes to the view controller if necessary when this view is updated*/
}
}

How to add protocol type as subview

So I wrote a simple protocol:
protocol PopupMessageType{
var cancelButton: UIButton {get set}
func cancel()
}
and have a customView:
class XYZMessageView: UIView, PopupMessageType {
...
}
and then I currently have:
class PopUpViewController: UIViewController {
//code...
var messageView : CCPopupMessageView!
private func setupUI(){
view.addSubview(messageView)
}
}
But what I want to do is:
class PopUpViewController: UIViewController {
//code...
var messageView : PopupMessageType!
private func setupUI(){
view.addSubview(messageView) // ERROR
}
}
ERROR I get:
Cannot convert value of type 'PopupMessageType!' to expected argument
type 'UIView'
EDIT:
I'm on Swift 2.3!
Change the type of property messageView to (UIView & PopupMessageType)!
I mean
class PopUpViewController: UIViewController {
//code...
var messageView : (UIView & PopupMessageType)!
private func setupUI(){
view.addSubview(messageView) // ERROR
}
}
In Swift 4 you can do this:
typealias PopupMessageViewType = UIView & PopupMessageType
And then use PopupMessageViewType as the type of the variable.
DISCLAIMER: I do not have the swift 2.3 compiler anymore since swift 4 is the new normal for iOS development. The following code may possibly need tweaks to get it working in swift 2.3
Essentially we will be making a 2x1 mux where the two inputs are the same object. The output depends on whether you set the mux to choose the first or the second one.
// The given protocol
protocol PopupMessageType{
var cancelButton: UIButton {get set}
func cancel()
}
// The object that conforms to that protocol
class XYZMessageView: UIView, PopupMessageType {
var cancelButton: UIButton = UIButton()
func cancel() {
}
}
// The mux that lets you choose the UIView subclass or the PopupMessageType
struct ObjectPopupMessageTypeProtocolMux<VIEW_TYPE: UIView> {
let view: VIEW_TYPE
let popupMessage: PopupMessageType
}
// A class that holds and instance to the ObjectPopupMessageTypeProtocolMux
class PopUpViewController: UIViewController {
var messageWrapper : ObjectPopupMessageTypeProtocolMux<UIView>!
private func setupUI(){
view.addSubview(messageWrapper.view)
}
}
//...
let vc = PopUpViewController() // create the view controller
let inputView = XYZMessageView() // create desired view
// create the ObjectPopupMessageTypeProtocolMux
vc.messageWrapper = ObjectPopupMessageTypeProtocolMux(view: inputView, popupMessage: inputView) //<-- 1
vc.messageWrapper.view // retreive the view
vc.messageWrapper.popupMessage.cancel() // access the protocol's methods
vc.messageWrapper.popupMessage.cancelButton // get the button
1) I input the "inputView" twice for the initializer of ObjectPopupMessageTypeProtocolMux. They are the same class instance, but they get casted to different types.
I hope this helps you get to where you wanna go in swift 2.3

How to create constraint on protocol

I tried to create protocol which can be only implemented by classes which inherit from UIView, what was my surprise when this code compiles without errors (in Swift 3.0):
protocol TestsProtocol {
func test()
}
extension TestsProtocol where Self: UIView { }
class FooClass: TestsProtocol {
func test() {
}
}
We can see that FooClass don't inherit from UIView, using protocol extension I wan't to force that only classes which inherit from UIView can implement it.
As far as I remember this would not compile in Swift 2.1
You cannot do this in Swift. The extension syntax does something else:
extension TestsProtocol where Self: UIView {
func useful() {
// do something useful
}
}
now any class which implements TestsProtocol and is a UIView (or subclass) also has the useful() function.
You can do that easily by limit protocol from be extendable from any type other than UIView :
protocol TestsProtocol:UIView {
func test()
}
class FooClass: TestsProtocol {
func test() {
}
}
So this will cause compile error
'TestsProtocol' requires that 'FooClass' inherit from 'UIView'

Can't conform to obj-c protocol in Swift

I'm creating an iOS app with Swift. I discovered an animation I'd like to implement in my table view, but the code is in Objective-C.
Repository: https://github.com/recruit-mp/RMPZoomTransitionAnimator
I have successfully bridged Obj-C code to Swift but can't seem to conform to a required protocol.
The protocol:
#protocol RMPZoomTransitionAnimating <NSObject>
#required
- (UIImageView *)transitionSourceImageView;
- (UIColor *)transitionSourceBackgroundColor;
- (CGRect)transitionDestinationImageViewFrame;
#end
My Swift implementation:
First class that implements the protocol:
class ChallengeViewController: UIViewController, RMPZoomTransitionAnimating
func transitionSourceImageView() -> UIImageView {
return imageView
}
func transitionSourceBackgroundColor() -> UIColor {
return UIColor.whiteColor()
}
func transitionDestinationImageViewFrame() -> CGRect {
return imageView.frame
}
Second class:
class ChallengeTableViewController: UITableViewController, RMPZoomTransitionAnimating
func transitionSourceImageView() -> UIImageView {
return imageForTransition!
}
func transitionSourceBackgroundColor() -> UIColor {
return UIColor.whiteColor()
}
func transitionDestinationImageViewFrame() -> CGRect {
return imageFrame!
}
This check that occurs before animating always fails:
Protocol *animating = #protocol(RMPZoomTransitionAnimating);
BOOL doesNotConfirmProtocol = ![self.sourceTransition conformsToProtocol:animating] || ![self.destinationTransition conformsToProtocol:animating];
I've read this topic How to create class methods that conform to a protocol shared between Swift and Objective-C? but didn't found any help
Any clues would be really appreciated
Swift classes by themselves are not (by default) Objective-C Compatible.
You get compatibility by either inheriting from NSObject or adding #objc in front of your class. I suspect this "may" be your issues - but I unfortunately can't test it at the moment.
You may also have to add a few initializers like in your case one from NSCoder or something - I don't unfortunately recall off the top of my head - and I don't have access to Xcode right now.
https://developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html
Try:
#objc
class ChallengeViewController: UIViewController, RMPZoomTransitionAnimating
func transitionSourceImageView() -> UIImageView {
return imageView
}
func transitionSourceBackgroundColor() -> UIColor {
return UIColor.whiteColor()
}
func transitionDestinationImageViewFrame() -> CGRect {
return imageView.frame
}
This will tell the compiler your class is objective-c compatible

How to make an array of objects that confirm a certain protocol and have a different parent class?

I have a design related question.
There is a class inherited from CAShapeLayer and one another class inherited from CATextLayer and both of them confirm to a certain protocol like below.
protocol HogeProtocol {
func aFunction()
}
class A: CAShapeLayer, HogeProtocol {
func aFunction() {
print("I am Class A")
}
}
class B: CATextLayer, HogeProtocol {
func aFunction() {
print("I am Class B")
}
}
A subclass of UIView has an array of objects that confirm this protocol:
class CustomView: UIView {
var customLayers = [HogeProtocol]()
func callTheFunctionAndAddSublayer() {
// some implementation
}
}
What I am trying to do here is to call aFunction() of the customLayers and add them to them to the layer of this custom UIView.
class CustomView: UIView {
var customLayers = [HogeProtocol]()
func callTheFunctionAndAddSublayer() {
for customLayer in customLayers {
customLayer.aFunction() // can call
layer.addSublayer(customLayer) // cannot call..
}
}
}
In this case, the elements is confirming protocol but cannot be added to sublayers because they are not inherited from CALayer. I wish I could make an array of an object inherited from CALayer (which is the common parent class for CAShapeLayer and CATextLayer) AND confirming to the protocol but swift doesn't allow me to do so (as far as I know...)
It seems very a simple problem and guess there might be a solution already but I couldn't find any answers after hours of google researching...
Any ideas?
You'll need to downcast the array object with as? or as! to a CALayer.
Try this:
for customLayer in customLayers {
customLayer.aFunction()
layer.addSublayer(customLayer as! CALayer) // would crash if customLayer is not of CALayer
}
You can cast the object to CALayer and check if it is really a kind of CALayer.
class CustomView: UIView
{
var customLayers = [CALayer]()
func callTheFunctionAndAddSublayer()
{
for customLayer in customLayers
{
customLayer.aFunction() // can call
if let layer = customLayer as? CALayer where layer.isKindOfClass(CALayer)
{
layer.addSublayer(layer)
}
}
}
}

Resources