We're implementing KMM Bridge with Swift Package M...
# touchlab-tools
a
We're implementing KMM Bridge with Swift Package Manager (SPM) for our iOS integration. Given that Swift Package Manager (SPM) only supports semantic versioning without pre-release tags (like 1.3.0-alpha.2 or 1.3.0-beta.1), how should we structure our development workflow when using KMMBridge to iteratively build and tag SPM packages for feature development? Specifically, what’s the best practice for managing intermediate versions during the dev cycle, and how can we ensure a smooth final release?
d
Not using KMMBridge, but we’re also shipping an SPM framework. We have a (manually triggered) automation that can make a build for a branch. It uses the GitHub Tag action for versioning: • If it’s run for the main branch, it will increment the version number without adding a pre-release tag. It’ll increment either the minor or patch version, depending on the prefixes on the merged PR titles. Example:
0.23.1
• If it’s run for any other branch, it increments the version from the latest one on the main branch and adds a pre-release tag based on the branch name (
0.23.2-cool-new-feature.0
for the first build of that branch,
0.23.2-cool-new-feature.1
for the next one).
a
and where you host the xcframework ? also If I am not wrong SPM only supports semantic version, so when you create version like
0.23.2-cool-new-feature.1
does this create any problem or how you deal with it ?
d
SemVer includes support for these kinds of tags, and SPM supports them:
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
(semver.org) We host the XCFramework on S3, but you could use any other storage that you can get SPM to pull from via HTTPS.
I also want to highlight that you might be able to do this with KMMBridge, I just don’t know because I’m not very familiar with it. Our automation predates KMMBridge and we haven’t seen the need yet to look into a potential migration.
k
We'll need to update some guidance if SPM actually supports those kinds of labels. We had trouble doing anything like that previously, but maybe didn't do it correctly. It'll still "work" in Xcode and SPM, but you need to clear caches, etc.
how should we structure our development workflow when using KMMBridge to iteratively build and tag SPM packages for feature development?
This is a different discussion. By "feature development" you mean architecture and direct app dev? This is a major focus on mine on scaling KMP. KMMBridge is a library publishing tool. Trying to do "feature dev" using a library model doesn't scale well (depending on team size) https://touchlab.co/kmp-teams-piloting-vs-scaling
Also, depending on what you're doing, there's another tool for library publishing, but with source: https://touchlab.co/kmp-teams-use-source
d
We’ll need to update some guidance if SPM actually supports those kinds of labels.
We’ve been doing this for almost two years now and never had any issues with it. Happy to answer any questions you have!
gratitude thank you 1
a
@kpgalligan Thank you for the resources. I’ve gone through the blogs on the library mode and the recommended bidirectional approach using GitPortal. I have a few questions, though they’re not directly related to this topic: 1. When using the bidirectional approach, I noticed that the XCFramework is integrated directly into Xcode. However, we use SPM to modularize our codebase, and since SPM doesn’t have direct access to the main app target, it can’t access the API from the framework. How should the flow be structured in this case? 2. If we decide to proceed with the library mode for now, I’m looking for a tagging strategy similar to what @Daniel Seither is using. For example, for any feature branch, the tag might be something like
0.2.3-some-feature.1
, and when merged to main, it should create a tag like
1.0.0
. How can I configure this in KMMBridge?
k
Last first.
How can I configure this in KMMBridge?
There's a new version of KMMBridge coming out soon. You don't have to use it. You can configure the current version if you want. I assume you're using the GitHub actions workflow with "autoversion". That's all going away. It adds too much complication. Now, it's important to understand that in the 0.5.x versions of KMMBridge, which I assume you're on, the autoversion stuff was all done in GitHub actions (in the 0.3.x KMMBridge versions, Gradle versioning was manipulated inside the plugin, which is a terrible idea). Anyway, you can just ignore the autoversion provided by GitHub actions by changing your build Gradle file. In the 0.5.x version template, the version is applied this way
Copy code
val autoVersion = project.property(
    if (project.hasProperty("AUTO_VERSION")) {
        "AUTO_VERSION"
    } else {
        "LIBRARY_VERSION"
    }
) as String

subprojects {
    val GROUP: String by project
    group = GROUP
    version = autoVersion
}
Just ignore that and do something like this:
Copy code
val buildVersion = project.property("BUILD_VERSION") as String

subprojects {
    val GROUP: String by project
    group = GROUP
    version = buildVersion
}
Then just make sure you set
BUILD_VERSION
to whatever you want in Gradle properties (either in source or passed to the build). Essentially, KMMBridge is getting out of the "version numbering business".
When using the bidirectional approach, I noticed that the XCFramework is integrated directly into Xcode.
That would be my default, but it's not required. You are building from source locally, though.
However, we use SPM to modularize our codebase
I'm writing a blog post about this now. You're thinking of KMP as a library. It's not. When you're writing feature code, it's part of your app code. If you modularize you Swift feature code in frameworks and use SPM to pull that in, OK, but that seems inefficient. Unless you mean you've configured SPM to build everything, including local Swift code, and Xcode isn't really managing "builds"
since SPM doesn’t have direct access to the main app target, it can’t access the API from the framework.
I'm not sure what you mean here. Can you expand on this?
a
Thank you for the explanation. I’m still in the process of exploring KMMBridge, so I don’t have a complete understanding of it yet. Could you share some resources or guide me on how to achieve my desired result? Specifically, do I need to add the following lines:
Copy code
val buildVersion = project.property("BUILD_VERSION") as String

subprojects {
    val GROUP: String by project
    group = GROUP
    version = buildVersion
}
in my shared module’s Gradle file, or is this something that needs to be configured for the GitHub Actions? My primary goal is to generate the SPM package, so I plan to use the iosPublish action.
> I'm not sure what you mean here. Can you expand on this? In our main app, we use separate SPM modules for each feature, encapsulating its data, domain, and presentation layers. This approach helps modularize our codebase, speeds up build times, and allows different developers to work on various features in parallel. For instance, we have distinct modules for Feature A, Feature B, etc., all of which are added locally to the main app. However, since these modules are separate, they cannot access any files from the main app target. For example, if we add an XCFramework to our main app, the SPM modules won’t have a reference to it. To address this, we would need to host the XCFramework somewhere and then add it as a dependency to the SPM modules so they can reference it. Given this setup, how would things work with the bidirectional approach?
k
In our main app, we use separate SPM modules for each feature, encapsulating its data, domain, and presentation layers
You can essentially have one KMP XCFramework, so using multiple modules in this way with KMP probably wouldn't work, unless I'm not understanding this completely.
Given this setup, how would things work with the bidirectional approach?
If you need to use SPM in that way, then you'll need to have the KMP/SPM module as a dependency of each of the feature modules that use the KMP (Feature A, Feature B, etc). If you're strictly talking the bidirectional approach, that implies you're building from source locally. SPM has horrible integrations outside of what's stock to SPM. For example, there's no way that I know of to trigger Gradle to build the KMP from SPM. We have a local SPM dev flow. Confusingly, it's part of KMMBridge currently, although we should probably separate that out. Anyway, you run Gradle on the command line to build your KMP binary, then run SPM with that as a dependency. So I understand better, the modules Feature A, Feature B, are they all Swift/Objc built by SPM? Does all of that live in an app repo, or are you pulling those features liked versioned libraries from somewhere?
a
No, All the modules lives locally in the same repo , we do not share them , all of them just contain pure swift code
k
OK. Yeah, anyway, SPM really doesn't make doing some things easy (although the status may have changed. We'd need to dig in again). I mean, if we could tell SPM to run that Gradle command, then point SPM at the local binary, that would work, but SPM really doesn't like inheriting env vars, etc. So, you build KMP locally, then SPM will be pointing at that local binary. See SPM Local Dev Flow. Unfortunately, this is currently attached to KMMBridge for historical reasons. JetBrains hasn't released official SPM dev support. I'm not sure they will anytime soon, so we should probably separate local SPM dev from KMMBridge.
👍 1
d
We have a very similar SPM setup for the modularization of the app’s code, without any dependencies of the feature modules on KMP, it’s all injected from the main app target. If we wouldn’t constrain the use of Kotlin types to a very small integration layer, we’d have much slower SwiftUI previews, and we’d have to suffer the KMP limitations in the whole code base, such as all the limitations that come with the intermediate Objective-C layer, but also Kotlin data classes being reference types while SwiftUI liking value types for state. We do use the KMP framework both as an external library (as a default) and with local builds (while iterating on the Kotlin and Swift part side by side). When including the external library, we’re doing the common thing in our project’s Package.swift in the dependencies section:
.package(url: "<https://github.com/org/lib>", exact: "0.11.22-somebranch.0")
. When doing local KMP development, we can comment this out and replace it with a local package:
.package(path: "../../lib")
(checkouts are side by side). The local Package.swift looks like this:
Copy code
// swift-tools-version: 5.6

import PackageDescription

let package = Package(
    name: "SomeLib",
    products: [
        .library(
            name: "SomeLib",
            targets: ["SomeLib"]
        ),
    ],
    targets: [
        .binaryTarget(
            name: "SomeLib",
            path: "build/XCFrameworks/debug/SomeLib.xcframework"
        ),
    ]
)
The downside of this setup: we need to build the XCFramework manually after each change because building in Xcode doesn’t trigger a Kotlin build. But to be honest, we like building the lib against unit tests anyhow and only then consume it in the apps, which reduces the amount of back and forth between both worlds. We also only share business logic, not view models etc., so we don’t need as much parallel development of lib and app.
k
If we wouldn’t constrain the use of Kotlin types to a very small integration layer, we’d have much slower SwiftUI previews
Why?
such as all the limitations that come with the intermediate Objective-C layer
Any specifics on which limitations? Have you tried SKIE, or is that not what you're talking about? Not that SKIE solves everything, but I'm curious. Objc isn't so much the problem. Kotlin and Swift are very different.
but also Kotlin data classes being reference types while SwiftUI liking value types for state.
Why not observable objects or similar?
d
SwiftUI previews For SwiftUI previews to work reliably and with quick update cycles after code changes, the target that contains the preview needs to be relatively small. More code in the target (and presumably more dependencies of the target) quickly get previews to a point where building them times out frequently, so for medium to large iOS apps it comes natural to build features in small modules with few dependencies. The main app target’s job is then to wire up the features and inject dependencies. We haven’t benchmarked the impact of adding dependencies to the modules, but we also didn’t look further because we already had other compelling reasons for restricting the use of Kotlin types to a small integration layer, some of which I’ll get into below. Limitations of the Objective-C layer in between Kotlin and Swift The biggest annoyance that we’ve run into is the extremely limited support for generics. We frequently run into the case where we do something in Kotlin that feels natural but leads to lots of “Any” types in Swift because Objective-C just doesn’t support generic interfaces, for example, and then have to dumb the Kotlin interface down to a level that works with Objective-C, even though Swift would be totally happy with what we’re doing in Kotlin. I’m hoping for the direct Kotlin to Swift export that JetBrains is working on, even though the first version that will ship with 2.1 won’t support generics at all, AFAIR. We do use SKIE, and it is excellent in the areas that it tackles (thank you!), but generics is not something it can currently help with. Why not observable objects or similar? When starting out with SwiftUI and building small feature modules, we experimented with a couple of approaches, some of them relying on observable objects, but eventually converged on sticking with value types in
@State
vars as much as possible, which worked out better for us and seems to be a general trend in the SwiftUI space. In sync with the general direction of the Swift community, we default to value types (structs/enums) for modeling state and try to steer clear of inheritance. Unfortunately, Kotlin and Objective-C don’t really support value types, so we translate between the worlds in the integration layer. In summary, there are already so many considerations that are going into design decisions for our feature modules that we don’t want to add any more constraints by factoring KMP into the mix. We’re much happier with isolating KMP to the integration layer.
k
The biggest annoyance that we’ve run into is the extremely limited support for generics
I'm familiar (I added them). Wait till Swift export arrives. It'll be worse (unless there's a magic solution). Objc generics are only on classes, but you can cast around variance issues. Swift generics do not work that way.
even though Swift would be totally happy with what we’re doing in Kotlin
You'd be surprised.
🙈 1
I’m hoping for the direct Kotlin to Swift export that JetBrains is working on, even though the first version that will ship with 2.1 won’t support generics at all, AFAIR
Swift is very strict on variance. I wrote the ObjC implementation (the initial one, anyway, not sure how much it's been updated). I've been saying for years. Swift interop is necessary for KMP's long term, but Swift and Kotlin are very different languages. Generics will be brutal.
thanks the for the feedback. Will add it to the KB internally. There is very little experienced iOS feedback with KMP. This is helpful. The SKIE engineers are working on some interesting stuff for SwiftUI. Not really SKIE related (although there's parallel work on that too). Structs/enums, though. Swift export won't help with structs. There simply is no "value type" in Kotlin. You could translate a data class into a struct, but after some POC work on that, it's just not worth it (generally). SKIE outputs proper Swift enums, but that's just a partial solution. I do think Swift Export will have better enum support. SKIE can't add enums to generics because the base is
AnyClass
rather than
Any
for generics (I think, my names may be off). SKIE doesn't generate a full Swift wrapper interface. It's partial. The team wanted to add that, but I kind of shut that down because of effort and the time value, considering Swift Export is coming, although I suspect it'll a lot longer than we'd all hope for the features out of scope to be in scope. That's a long discussion in itself. Again, Swift and Kotlin look similar, but they're really, really different languages. Generics, Flows, default params, sealed classes, (lack of) structs, and so much more. Swift interop is not the same as "Swift fluency".
Swift Export does generate a full Swift wrapper, around a C interface generated by the Kotlin compiler (I believe). That's why enums should be easier (long story). Generics will require some weird solutions. Swift needed to interop with ObjC, so ObjC gets special treatment. You can cast around generics issues if the Swift compiler thinks you're talking to ObjC. Swift won't allow this is it's talking to "Swift", because Swift to Swift is much more strict. Swift Export will be "Swift" as far as the Swift compiler is concerned. built-in Swift types have exceptions on variance (it's been a while, but I think array, map, etc have generics with support for variance), you your code can't.
d
Thanks for these insights! Don’t get me wrong, I still think that KMP is in the sweet spot of all the cross-platform code sharing solutions I’ve seen, but I’m now even more convinced that it’s a good idea to isolate KMP from the feature code, to constrain any interop considerations to a much smaller part of the code base. It’s not all that hard to write that glue code manually, and the upside is that the hand-written types that we feed into the feature modules feel really natural and don’t have to be a compromise between the three platforms we’re shipping to (Android, iOS, web), and between Kotlin, Objective-C, and Swift.
k
it’s a good idea to isolate KMP from the feature code, to constrain any interop considerations to a much smaller part of the code base
The trick there is KMP being relegated to small portions of the code, unless I'm not understanding what you're say. It reduces the value of KMP. Either less code in KMP, or significantly reduced dev efficiency, or both. For KMP to really be valuable while sharing code (not doing Compose UI, etc), KMP needs to be used for a significant portion of development, and it needs to do so efficiently. I'm obsessed with that. Otherwise it'll be another "also ran/tried that" cross-platform thing. https://touchlab.co/kmp-teams-piloting-vs-scaling
d
KMP is (or will be, we’re already using it for some features but are currently scaling it up) used for a significant part of the code, there’s just a clear separation between business logic (KMP) and UI code (native), with a pretty small interface in between, making it possible to keep the worlds largely separated. Business logic can be developed in separation by programming against unit tests, while UI code can be developed in separation because feature modules don’t depend on KMP and can be fed dummy data (necessary anyhow for previews). We’ve been sharing code (mostly business logic) between apps with a native UI with a library development model for the better part of a decade now, and it’s a success in general as it allowed us to build an offline-first app with some complex calculations without duplicating all that syncing and calculation logic, even though the underlying tech (a home-grown JS solution) didn’t hold up to growing requirements well, and we learned that we overdid the code sharing when we tried letting the library drive the native UI in some parts of the app. With KMP, we’re doubling down on letting the apps use the architecture and UI that works best for their platforms, but only write the complex business logic once.
k
We’ve been sharing code (mostly business logic) between apps with a native UI with a library development model for the better part of a decade now ... and we learned that we overdid the code sharing when we tried letting the library drive the native UI in some parts of the app.
Yeah, you can. We were heavily into J2ObjC before KMP, and started with KMP when it first was possible. It's just that, for most teams, they simply won't change or invest the way yours did. Also, if you really want to use KMP for direct UI architecture, not just business logic (depending on how you define "business logic"), the library model is terrible. Not for small teams so much, although it's not fun. If you had 10 android and 10 iOS devs, trying to do that would be worse than just writing everything native. That's the problem I'm trying to figure out.
d
Also, if you really want to use KMP for direct UI architecture, not just business logic (depending on how you define “business logic”), the library model is terrible.
💯
k
Yeah, that's what I'm trying to figure out. Some apps have lots of "business logic", and a lot of development to write it. In those cases, my complex "scaling" problems aren't problems. "Scaling" KMP in general means using it for significant portions of your dev efforts, efficiently. If that is the business logic, OK. Not a big problem. For the average app, most of the dev time is messing around with UI and UI-associated code. If that can't easily be KMP, then they're not getting much from KMP. I've talked to a lot of teams in that situation.
136 Views