While I haven’t deployed a KMP app on iOS just yet (no mac), I could help with general KMP / CMP stuff if needed!
Thank you for mentioning this.
I was going to go with KMP but after careful consideration I decided to go native with SwiftUI. KMP is an interesting platform that I haven’t fully explored, however, I did see that there are two options to choose from – use Compose for both platforms or write native code individually for each while sharing the business logic.
Going with SwiftUI ensures I have full compatibility and I can do things the iOS way rather than being tied to Compose and the way of doing things there. Besides, I intend to have the iOS version be entirely (almost) different from the Android sibling implementation wise.
Yeah that definitely makes sense! SwiftUI will feel a lot more at home and take advantage of platform capabilities a lot better than Compose Multiplatform (which functions kinda like Flutter).
However, there’s definitely still a lot of value in using KMP, at least for the business logic. That includes database stuff, API calls, models, etc… and sharing it enables you to focus on what really matters to the iOS experience (the UI, state management, etc…). The interop is pretty seamless (and constantly improving), and there’s lots of community support for iOS use-cases. The KMP scene on iOS is a lot more thriving than even desktop, since a lot of the focus tends to go to mobile.
And shared UI with Compose can also allow you to ship an iOS version much sooner than a full re-write, with just a few adaptations here and there. From there, you can start re-writing the view layer into SwiftUI at your own pace, whether it’s screen by screen, component by component, or all at once.
Whatever end up doing, wishing you best of luck!
Oooh this is already out!
I also didn’t notice it until now ![]()
You make very good points and I considered them before deciding on SwiftUI. While KMP has its advantages, I think going with SwiftUI was the right call. Besides, the work is underway and can’t go back now.
The natural choice would have been KMP if I just wanted to keep things the way they look on Android but that is not the case.
Thank you!
Haha! You didn’t notice too? Now, I am not sure whether that feature was needed
.
Oh no it’s actually a game changer for fast reviews! ![]()
To be fair, I was using the app very sparingly recently as I was on a trip, and probably didn’t get updates for a while due to storage (stacked a little too many videos hehe), so… there’s that ![]()
Hi again!
This felt pretty weird to me, because the current input loss should be impossible with the new text fields (maybe even the old ones), so I tried to play with it a bit.
First thing I noticed is that Wanakana-kt’s toKanaIME function is not suitable for use with Compose’s transformations, at all. All it provides is the final text, in addition to the final selection mapping, which is far from enough information to help compose map the underlying text to the transformed text (for purposes of selection, deletion, etc…).
The only thing you can do with it is a replacement of the whole text:
outputTransformation = {
val imeText = ImeText(toString(), selection.start, selection.end)
val result = Wanakana.toKanaIme(imeText)
replace(0, length, result.text)
selection = TextRange(result.selection.start, result.selection.endInclusive)
}
While this works (and there’s no state loss), it also makes Compose understand that the entire original text maps to the entire transformed text, which… isn’t particularly a useful mapping. So pressing “delete”, Compose sees only one chunk, so everything gets deleted. Same with selection, you can only select the full text, or nothing.
This leaves me puzzled as to how Wani does this now
The only way I assume is that it uses the transformation to change the value of the state variable, that’ll always result in lost state (the transformation shall only transform the TextFieldBuffer receiver it gets, without any side effects!), but I have no clue.
In any case, the only way to make Compose aware of the mapping with the new text fields, is to run a separate replace per each character transformed. That way Compose maps each set of original characters to the final transformed result. This can’t be currently done with Wanakana’s own IME affordances.
However, taking a quick look at toKanaIME, it’s pretty straightforward. It gets the full Romaji-to-Kana mapping, creates a list of ConversionTokens, which are mappings of every individual kana transformation, then concatenates the results into a string…
Wait, those ConversionTokens are actually exactly what we need, but instead of concatenating them into a string, we’ll instead iterate over them and use replace on our TextFieldBuffer.
Now, most functions within toKanaIME are private/internal to Wanakana’s package, so you’ll have to copy the wanakana-core library’s contents into the project to access them directly.
Once that’s done, I was able to add this function to the same file where toKanaIME is defined. It looks almost identical, and simply replaces the concatenation with direct replace calls on the TextFieldBuffer receiver:
fun TextFieldBuffer.transformToKana(
input: ImeText = ImeText(toString(), selection.start, selection.end),
imeMode: IMEMode = IMEMode.ENABLED,
useObsoleteKana: Boolean = false,
) {
if (input.text.isEmpty()) return
val map = createRomajiToKanaMap(imeMode, useObsoleteKana)
val enforceHiragana = imeMode == IMEMode.TO_HIRAGANA
val enforceKatakanaMode = imeMode == IMEMode.TO_KATAKANA
// If we have a cursor that is not at the edge of the text, we need to split the input to avoid
// converting a single n too early.
val needToSplit = input.selection.first == input.selection.last &&
input.selection.first > 0 && input.selection.first != input.text.length
val tokens: List<ConversionToken> = if (!needToSplit) {
splitIntoConvertedKana(input.text, map, imeMode)
} else {
val cursor = input.selection.first
val inputStart = input.text.take(cursor)
val inputEnd = input.text.drop(cursor)
val startTokens = splitIntoConvertedKana(inputStart, map, imeMode)
val endTokens = splitIntoConvertedKana(inputEnd, map, imeMode)
.map { token -> token.shift(cursor) }
startTokens + endTokens
}
// DON'T throw away the substring index information
// use it to map the tokens using replace
for (token in tokens.reversed()) {
val kana = token.value
val replacement = if (kana == null) {
// haven't converted the end of the string, since we are in IME mode
input.text.slice(token.range)
} else {
val enforceKatakana = enforceKatakanaMode
|| input.text.slice(token.range).all { it.isEnglishUpperCase() }
if (enforceHiragana || !enforceKatakana) kana else hiraganaToKatakana(kana)
}
replace(token.start, token.end, replacement)
}
}
The usage is simple:
val state = rememberTextFieldState()
BasicTextField(
state = state,
outputTransformation = { transformToKana() }
)
No state loss, works perfectly. Hope this helps!
I will be temporarily removing the app from Play Store as I need to make a couple of changes to the app to avoid confusion that could cause people to think this is an official app.
In doing so, I will be renaming the app. As always I will communicate when the new app becomes available.
If you are a graphics designer and don’t mind making a few graphics for the app especially the logo please let me know. This will be very much appreciated.
You’re welcome to chime in on what the new name should be.
Thank you!
I have now updated the app name with a new app icon (work in progress). The new name for the app is manakame. On installing the new update, you should see a new launcher icon, additionally, every WaniKani artwork has been removed. The app listing has also been updated to reflect these changes.
It seems like my friends who weren’t beta testers and are just getting started with Wanikani aren’t able to see the app on the Play Store - your top post says it should be available to everyone now, but do people still need to be added by email?
Hello! Not sure how involved this would be but I’d love an option to hide the English translation on the context sentences. One of the web user scripts I use puts spoiler tags on the English line so you can reveal on hover or click.
Something similar here would be amazing to have.
Thanks!
Unfortunately I had to take the app down for a design overhaul. This would take some time before it is made available again. The stage I am at is creating the new designs/wireframes. Once concluded, I’d have to begin bringing the designs to life.
On another note, I might just end up making it a new app to distinguish it from the existing one.
For your friends, I could make the current app available via a link so they can download it but not through Play Store. Let me know if they’d want that.
Sure, this can be done. I will add it in a future release.
Thank you!!
A download link would be great, if it’s not too much trouble! Thanks!
For the last released version on Play Store before unpublishing use this link to download the app.
Thankyou so much! I’ve passed it on <3
One of the friends I passed the app onto is having a weird issue - when she goes to start lessons, the app just transitions out to a blank screen. At first it seems like the problem was that the “max lessons per day” setting didn’t initialize correctly and was set to 0, and fixing that got things running for a day, but now it’s set to 15 and the issue is back. Everything on the home screen looks correct (it says 10 lessons available after doing the first 5).
Any ideas what could be causing this / anything we can do to help debug it? I understand of course that patching this version of the app is probably not your focus though!
Could you upload a video of the issue and a screenshot of the lessons settings screen? This you can do on the GitHub issue page for the app.