ã¯ããã«
ããã«ã¡ã¯ãæ ªåŒäŒç€Ÿã¬ã³ãã§ã¯ NXéçºæšé²éš ãããã¯ãéçºç¬¬2Gã®äžéã§ãã
æ®æ®µã¯PlayPASS PAKïŒä»¥äžPAKïŒã®éçºãæ åœããŠããŸãã
å æ¥çºè¡šãããæ¥æ¬ã²ãŒã 倧è³2026ã§ãã²ãŒã ãã¶ã€ããŒãºå€§è³ãåè³ãããæå鿝ããšããã²ãŒã ãæ°ã«ãªã£ãŠããŸãããã¹ãŠãæåã§ã§ããäžçããæåãæ¶ãããåããããããŠé²ãã²ãŒã ã§ãã
ä»åã¯ããããªã¢ãã€ã«ã¢ããªéçºã§ã®æåã«ãŸã€ããäœéšè«ãã話ãããŸãã
PAKã¢ããªã¯ãKotlin MultiplatformïŒä»¥äžKMPïŒã§éçºããŠãããAndroidãšiOSã§åŠçã®äžéšãKotlinã®å ±éã³ãŒãã«ããŠããŸãããããšãããã¬ã€ãªã¹ãåã®å ¥åæ¬ã®ãã¹ããããŠããŸããããã¬ã€ãªã¹ãåã¯ã300æåãŸã§å ¥åã§ããŸããäžéãã£ã±ããŸã§å ¥åããæ«å°Ÿã«çµµæåãå ¥ããŠä¿åãããšãããã¹ããè¡ããŸãããä¿åããåŸã®ãã¬ã€ãªã¹ãåãèŠãŠã¿ããšãiOSã§ãæ«å°Ÿã®çµµæåãæååãããŠããŸããã
å³1. äžéãã£ã±ããŸã§å ¥åããŠä¿åããåŸããã¬ã€ãªã¹ãåãèŠããšæ«å°Ÿã®çµµæåããᅵãã«å€ãã£ãŠããã
挢åãã²ãããªã ããªããäžéãŸã§å ¥åããŠãæååããããçµµæåãå«ãããšãã ãèµ·ããŠããŸãããiOSã§ã¯ãå ¥åäžã®ç»é¢ã§ã¯çµµæåãæ£ãã衚瀺ãããã«ãŠã³ã¿ãäžéã®ç¯å²ã«åãŸã£ãŠããŸãããäžæ¹ã®Androidã§ã¯ãçµµæåãå ¥ãããšã«ãŠã³ã¿ã300ãžå±ãåã«å ¥åãæ¢ãŸãããšããå¥ã®çç¶ãåºãŠããŸããã
å€ãã£ãã®ã¯ä¿åããåŸãªã®ã§ããŸãä¿ååŠçã®ã³ãŒãã確èªããŸãããä¿ååŠçã¯AndroidãšiOSã®å ±éã³ãŒãã§ããã®ãããªåŠçã«ãªã£ãŠããŸããã
// commonMain: ãã¬ã€ãªã¹ãã端æ«å
ã®ããŒã¿ããŒã¹ãžä¿åãã
class PlaylistRepositoryImpl(
private val dao: PlaylistDao,
private val idGenerator: PlaylistIdGenerator,
private val now: () -> Long,
) : PlaylistRepository {
override suspend fun savePlaylist(draft: PlaylistDraft): Result<String> {
val playlistId = idGenerator.newPlaylistId()
val title = resolveTitle(draft) // ä¿åãããã¬ã€ãªã¹ãåãæ±ºãã
val entity = PlaylistEntity(
playlistId = playlistId,
title = title,
updatedAt = now(),
)
val items = draft.items.map { /* 楜æ²ãããŒã¿ããŒã¹çšã®åœ¢ã«å€æïŒçç¥ïŒ */ }
dao.insertPlaylistWithItems(entity, items)
return Result.Success(playlistId)
}
// å
¥åæ¬ã®äžéã¯OSããšã®å®è£
ãªã®ã§ãæŒãã«åããŠä¿åã®çŽåã«ãåãè©°ãã
private fun resolveTitle(draft: PlaylistDraft): String {
return draft.title.take(PLAYLIST_TITLE_MAX_LENGTH) // äžéã¯300æå
}
}
ãã®ãã¡ããã¬ã€ãªã¹ãåã«é¢ããã®ã¯ãä¿åã®çŽåã«äžéã§åãè©°ããæ¬¡ã®éšåã§ãã
private fun resolveTitle(draft: PlaylistDraft): String {
return draft.title.take(PLAYLIST_TITLE_MAX_LENGTH) // äžéã¯300æå
}
takeã¯ãå
é ããæå®ããæ°ã ãæåãåãåºã颿°ã§ããæååãæå®ããæ°ããçããŠãäŸå€ã«ãªããªããããåãè©°ããåŠçã«åã£ãŠããŸãã
ç¹ã«åé¡ã¯ãªããããªã®ã«ããªãæååãããã®ãããããŠããªãiOSãšAndroidã§çç¶ãéã£ãã®ããä»åã¯ããã®2ã€ã®çåãé ã«è§£ããªããã人éãšã³ã³ãã¥ãŒã¿ã§æåã®æ°ãæ¹ãã©ãéãã®ãã解説ããŸãã
æ³å®èªè
- æååã®é·ããæ°ãããåãè©°ãããããåŠçãæžã人
- å ¥åæ¬ã®æåæ°ã®äžéãã«ãŠã³ã¿ãå®è£ ãã人
- çµµæåãå«ãæååã§ãæåæ°ã®ãããæååãã«å°ã£ãããšããã人
åäœç°å¢
- Kotlin 2.3.0ïŒKMPïŒ
- Xcode 26.3
- iOSïŒiPhone 17 ã·ãã¥ã¬ãŒã¿ïŒiOS 26.3.1ïŒ
- AndroidïŒPixel 10ïŒAndroid 16ïŒ
çå1ïŒãªãæååãããã®ã
æååãã¯ãçµµæåãå«ãããšãã ãèµ·ããŠããŸããããããŠä¿åã®çŽåã«ã¯ãtakeã§300æåã«åãè©°ããŠããŸããããã§ãŸãã¯ãçµµæåãã³ã³ãã¥ãŒã¿ã®äžã§ã©ãæ°ããããŠããã®ãã調ã¹ãŠãããŸãã
äººãæ°ãã1æå
ç§ãã¡ã¯æåããèŠãç®ã®ãŸãšãŸããã§æ°ããŠããŸããããããðã¯ã1ã€ã®å¡ã«èŠããã®ã§1æåã§ãã
ãã®ã人ã1æåãšæãããŸãšãŸããã«è¿ãåºåãæ¹ããUnicodeã§ã¯**æžèšçŽ ã¯ã©ã¹ã¿ïŒgrapheme clusterïŒ**ãšåŒã³ãŸãã
ã³ã³ãã¥ãŒã¿ãæ±ã2ã€ã®åäœ
äžæ¹ãã³ã³ãã¥ãŒã¿ã¯ãã®æžèšçŽ ã¯ã©ã¹ã¿ãããã«åè§£ããåœ¢ã§æåãæã£ãŠããŸããåè§£ããŠãããšãã³ãŒããã€ã³ããšã³ãŒããŠããããšãã2ã€ã®åäœãåºãŠããŸãã
å³2. æžèšçŽ ã¯ã©ã¹ã¿ããã³ãŒããã€ã³ããšã³ãŒããŠãããã«åè§£ããã
ã³ãŒããã€ã³ãã¯ãUnicodeãæåã«äžããçªå·ã§ããããããªãU+3042ãðãªãU+1F600ãæåã®æžç±çªå·ã®ãããªãã®ã§ãäžçäžã®æåã«1ã€ãã€å²ãåœãŠãããŠããŸãã
ã³ãŒããŠãããã¯ããã®çªå·ãã¡ã¢ãªããã¡ã€ã«ãžä¿åãããšããå®éã«äžŠã¶æ°å€ã®åäœã§ãã1åäœã®å€§ããã¯ä¿åã®æ¹åŒïŒç¬Šå·åæ¹åŒïŒã«ãã£ãŠéããUTF-8ãªã8ããããUTF-16ãªã16ãããã§ããKotlinã®æååã¯äžèº«ãUTF-16ã§ãlengthãtakeããã®åäœãæ°ããŸãããã®ããããã®èšäºã§ã¯UTF-16ãåæã«è©±ãé²ããŸãã
ã²ãããªãã¢ã«ãã¡ãããã¯ãåè§£ããŠãã³ãŒããã€ã³ãã§1ã€ãã³ãŒããŠãããã§ã1ã€ã®ãŸãŸã§ãããããã¯U+3042ãããã®ãŸãŸ3042ãšãã1åäœã§æãŠãŸãã
äžæ¹ã®ðã¯ãã³ãŒããã€ã³ããŸã§ã¯1ã€ã§ãããã³ãŒããŠãããã«ãªããš2ã€ã«åãããŸããçªå·ã®U+1F600ã16ãããã«åãŸããªããããD83DãšDE00ã®2åäœã«åããŠæã€ããã§ãããã®ããã«ã1ã€ã®çªå·ã2ã€ã®ã³ãŒããŠãããã§è¡šãä»çµã¿ããµãã²ãŒããã¢ãšåŒã³ãŸããçµµæåã®å€ãã¯16ãããã«åãŸããªãçªå·ãæã£ãŠããã®ã§ãã»ãšãã©ã®çµµæåã¯UTF-16ã§èŠããšæåãã2åäœã§ãã
è€æ°ã®çµµæåãçµã¿åãããä»çµã¿
åãããã®ã¯ã³ãŒããŠãããã ãã§ã¯ãããŸãããðã®ãããªã·ã³ãã«ãªçµµæåã§ããã°ãã³ãŒããã€ã³ãã¯1ã€ã§æžã¿ãŸããããããèã®è²ã»æ§å¥ã»å®¶ææ§æã»åœæã®ããã«ããªãšãŒã·ã§ã³ãæã€çµµæåã§ã¯ãã³ãŒããã€ã³ãããè€æ°ãçµã¿åãããŠäœ¿ããŸããçµã¿åããã®æ°ãå€ãããŠã1ã€ãã€çªå·ãæ¯ããããªãããã§ããå®¶ææ§æã衚ãðšâð©âð§âðŠãããã®1ã€ã§ãããã®ããã«ãè€æ°ã®éšåã䞊ã¹ãŠ1ã€ã®æåã«èŠããä»çµã¿ããããã§ã¯ãåæããšåŒã³ãŸãã
åæã®ä»£è¡šçãªä»çµã¿ãZWJïŒãŒãå¹ æ¥ååãU+200DïŒã§ããZWJã¯ååŸã®çµµæåãã€ãªããŠ1ã€ã«èŠããããã®ãå¹ ãæããªãèŠããªãæåã§ãã圹å²ãšããŠã¯åèªãã€ãªããã€ãã³ã«è¿ããðšã»ð©ã»ð§ã»ðŠãZWJã§3ç®æã€ãªããšããã®å®¶æã®çµµæåã«ãªããŸããåè§£ãããšå³3ã®ãšããã§ãã
å³3. ðšâð©âð§âðŠã3ã€ã®ã¬ãã«ã§åè§£ããå³ã
人ç©çµµæå4åã¯ãããããµãã²ãŒããã¢ã§2ãŠããããéã®ZWJã3åã§ãåãããŠ11ãèŠãç®ã®1æåã®è£ã«ã¯ã11åã®ã³ãŒããŠãããã䞊ãã§ããããã§ãã
3ã€ã®åäœã§æ°ãæ¯ã¹ã
ãããŸã§ã«åºãŠããæåãã3ã€ã®åäœã§ããããæ°ãããšæ¬¡ã®ããã«ãªããŸãã
| æå | æžèšçŽ ã¯ã©ã¹ã¿ | ã³ãŒããã€ã³ã | ã³ãŒããŠãããïŒUTF-16ïŒ |
|---|---|---|---|
| A | 1å | 1å | 1å |
| ã | 1å | 1å | 1å |
| ð | 1å | 1å | 2å |
| ðšâð©âð§âðŠ | 1å | 7å | 11å |
æåãšããŠã¯1æåã«èŠãããã®ããåäœãæ·±æããããšè€æ°ã«ãªãå¯èœæ§ããããšåãããŸãããðã®ãããªçµµæå1ã€ã§ããUTF-16ã®ã³ãŒããŠãããã§èŠããš2ã€ã«åãããŠããŸããããšãã°æåæ°ã®äžéã§åãè©°ãããšãã«ããã®2ã€ã®éã§åããŠããŸããšããµãã²ãŒããã¢ã®ååã ããæ®ããŸããååã ãã§ã¯æåãšããŠæç«ããŸããããããåé ã®ãᅵãã®æ£äœã§ã¯ãªããããšèããããŸãã
ãããããåŠçã¯èŠãç®ã®1æåã§æ°ããå¥ã®åŠçã¯ã³ãŒããŠãããã§åã£ãŠãããããã®åãæ¹ãèµ·ããŸããæåæ°ãæ±ãåŠçã¯ãæ±ãæåã«é©ããåäœã§ãã¹ãŠããããå¿ èŠããããŸããããããåŠçã©ããã®æŽåæ§ãæ¬ ããšãåãæååã§ãåŠçã«ãã£ãŠé·ãã®è§£éãå€ãã£ãŠããŸããŸãã
çå2ïŒãªãiOSãšAndroidã§çç¶ãéã£ãã®ã
ã§ã¯ãPAKã®ãã¬ã€ãªã¹ãåã¯ããã®æŽåæ§ãåããŠããã®ã§ãããããããããã¯2ã€ç®ã®çåããªãiOSãšAndroidã§çç¶ãéã£ãã®ããã確ãããŸãã
ãã¬ã€ãªã¹ãåã¯ãå ¥åãããŠããä¿åããããŸã§ã«ãæåæ°ãæ±ã次ã®3ã€ã®åŠçãéããŸãã
- ã«ãŠã³ã¿ïŒå ¥åäžã®æåæ°ãã12 / 300ãã®ããã«ç»é¢ã«åºã
- å ¥åã®äžéïŒ300æåãè¶ ããå ¥åãæ¢ãã
- ä¿åã®åãè©°ãïŒå ¥ååŽã®å¶åŸ¡æŒãã«åããŠãä¿åã®çŽåã«300æåã§åãè©°ãã
å³4. ãã¬ã€ãªã¹ãåã®æåæ°ãæ±ã3ã€ã®åŠçãã«ãŠã³ã¿ãšä¿åã®åãè©°ãã¯å ±éã³ãŒããå ¥åã®äžéã¯OSããšã«æžããŠããã
ããããããã©ã®åäœã§æ°ããŠããããé ã«èŠãŠãããŸãã
ã«ãŠã³ã¿ã®æ°ãæ¹ïŒå ±éã³ãŒãïŒ
ãã¬ã€ãªã¹ãåã®å ¥åæ¬ã«ã¯ãã12 / 300ãã®ãããªæåæ°ã«ãŠã³ã¿ãä»ããŠããŸããã«ãŠã³ã¿ã«åºãæ°ã¯ãPAKã§çšæããæ¬¡ã®é¢æ°ã§æ°ããŠããŸãã
// commonMain: 宣èšã ããå
±éã³ãŒãã«çœ®ããäž¡OSããã®é¢æ°ãåŒã¶
expect fun countGraphemes(text: String): Int
// androidMain: Androidåãã®äžèº«
private val GRAPHEME = Pattern.compile("\\X")
actual fun countGraphemes(text: String): Int {
if (text.isEmpty()) return 0
val m = GRAPHEME.matcher(text)
var count = 0
while (m.find()) count++ // ããã1å = æžèšçŽ ã¯ã©ã¹ã¿1å
return count // ðšâð©âð§âðŠ â 1
}
KMPã®å
±éã³ãŒãã§äœ¿ããKotlinã®æšæºã©ã€ãã©ãªã«ã¯ãæžèšçŽ ã¯ã©ã¹ã¿ã§æ°ãã颿°ããããŸãããããã§PAKã§ã¯ãcountGraphemesãšãã颿°ãçšæãã宣èšã ããå
±éã³ãŒãã«çœ®ããŠãäžèº«ã¯OSããšã«æžããŠããŸããã«ãŠã³ã¿ã¯ãAndroidãšiOSã®ã©ã¡ãããã®åã颿°ãåŒãã§æ°ããŠããŸãã
äžã®ã³ãŒãã¯Androidåãã®äžèº«ã§ãJavaã®æ£èŠè¡šçŸã®\Xã䜿ã£ãŠããŸãã\Xã¯ãæžèšçŽ ã¯ã©ã¹ã¿1åã«ãããããŸãããã®é¢æ°ã¯ãã®ãããåæ°ãè¿ãã®ã§ãèŠãç®ã®1æåãã€ãŸãæžèšçŽ ã¯ã©ã¹ã¿ã§æ°ããŠããããšã«ãªããŸããiOSåãã®äžèº«ã¯æžãæ¹ãéããŸãããåããæžèšçŽ ã¯ã©ã¹ã¿ã§æ°ããŸãã
ã«ãŠã³ã¿ã¯ðšâð©âð§âðŠã1æåãšæ°ããŸããã«ãŠã³ã¿ã®æ°ãæ¹ã¯ãäž¡OSãšã人éã®æ°ãæ¹ã«ããã£ãŠããŸããã
å ¥åã®äžéã®å€å®ïŒOSããšã®ã³ãŒãïŒ
次ã¯ã300æåãè¶ ããå ¥åãæ¢ããåŠçã§ãããã®åŠçã¯å ±éã³ãŒãã§ã¯ãªããiOSã¯ViewãAndroidã¯ViewModelãšãOSããšã«å¥ã ã«æžããŠããŸãããPAKã§ã¯ãç»é¢ã衚瀺ããViewãšãç»é¢ã®ç¶æ ãæã€ViewModelã¯OSããšã«æžããç»é¢ã«åºãå€ã®èšç®ãããŒã¿ã®ä¿åã¯å ±éã³ãŒãã«çœ®ããŠããŸããããŒå ¥åãåããŠæ¢ããåŠçã¯ç»é¢åŽã®ä»äºãªã®ã§ãOSããšã®å®è£ ã«ãªã£ãŠããŸããã
iOSã§ã¯ãå
¥åæ¬ã®UITextViewãæåãåãä»ããçŽåã«ã倿ŽåŸã®æååãã«ãŠã³ã¿ãšåã颿°ã§æ°ããŠå€å®ããŠããŸããã
// iosApp: 倿ŽåŸã®æåæ°ãäžéãè¶
ãããªããå
¥åãåãä»ããªã
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
let proposed = (textView.text as NSString).replacingCharacters(in: range, with: text)
// countGraphemes ã¯ãã«ãŠã³ã¿ãšåãå
±éã³ãŒãã®é¢æ°ïŒSwift ã® count ã§ã¯ãªãïŒ
let length = Int(PlaylistTitleLength_iosKt.countGraphemes(text: proposed))
return length <= Int(PlaylistConstants.shared.PLAYLIST_TITLE_MAX_LENGTH)
}
äžæ¹ã®Androidã§ã¯ãå
¥åãããæååãViewModelã§åãåããKotlinã®lengthã§å€å®ããŠãsubstringã§åãè©°ããŠããŸããã
// androidApp: ViewModel ã§ãäžéãè¶
ããŠãããåãè©°ãã
fun setTitle(title: String) {
val trimmed = if (title.length > PLAYLIST_TITLE_MAX_LENGTH) { // ã³ãŒããŠãããã§å€å®
title.substring(0, PLAYLIST_TITLE_MAX_LENGTH) // ã³ãŒããŠãããã§åã
} else {
title
}
// äžæžãã®ãã¬ã€ãªã¹ãåã trimmed ã§æŽæ°ããïŒçç¥ïŒ
}
åãã300æåãŸã§ãã§ããiOSã¯æžèšçŽ ã¯ã©ã¹ã¿ãAndroidã¯ã³ãŒããŠãããã§å€å®ããŠããããšã«ãªããŸããiOSã¯ãå
¥åã®äžéãã«ãŠã³ã¿ãšåã颿°ã䜿ã£ãŠå€å®ãããã察å¿ã§ããŠããŸããããAndroidã¯ãã®å¯Ÿå¿ãæããŠããŸããããã®ããAndroidã§ã¯ãçµµæåã2æå以äžãšæ°ããããã«ãŠã³ã¿ã300ãžå±ãåã«å
¥åãæ¢ãŸã£ãŠããŸããããŸããsubstringãã³ãŒããŠãããã§åããããåé ã®ããã«äžéã®æåã§çµµæåãå
¥ãããšãçµµæåã¯å
¥ãããããååã®ã³ãŒããŠãããã ããæååã«æ®ããŸãã
ä¿åã®åãè©°ãïŒå ±éã³ãŒãïŒ
æåŸã«ãä¿åãããšãã¯ã©ãã§ãããããåé ã§ç޹ä»ããä¿åã®ã³ãŒãã§ã¯ã次ã®1è¡ã§ãã¬ã€ãªã¹ãåãåãè©°ããŠããŸããã
// åé ã® resolveTitle ã® return æ
return draft.title.take(PLAYLIST_TITLE_MAX_LENGTH)
ä¿åã®çŽåã«äœ¿ã£ãŠããtakeããAndroidã®å
¥åã®äžéã®substringãšåãããã³ãŒããŠãããåäœã§åããŸããå
¬åŒããã¥ã¡ã³ãã§ã¯ãå
é ããnæåãè¿ãããšèª¬æãããŠããŸãããããã§ãã1æåã¯Kotlinã®Charã®ããšã§ããCharã¯UTF-16ã®ã³ãŒããŠããã1åã衚ãã®ã§ãçµµæåã¯Char2ã€ïŒãµãã²ãŒããã¢ïŒã§è¡šãããŸãã
iOSã§ã¯ãå
¥åæ¬ã«èŠãç®ã§300æåãŸã§å
¥ããŸããçµµæåãå«ãŸããŠããã°ããã®ãšãã®Charã®æ°ã¯300ãè¶
ããŸãããã®ããã300åç®ã®Charãã¡ããã©çµµæåã®éäžã«åœãããšããµãã²ãŒããã¢ã®ååã ããæ®ã£ãæååãã§ãããããŸãïŒå³5ïŒã
å³5. 299æåã®åŸãã«ðãå ¥ããŠä¿åãããšãtake(300)ãðã®éäžã§åããååã®D83Dã ããæ®ãã
ååã ãã§ã¯æåãšããŠæç«ããŸãããPAKã䜿ã£ãŠããRoomã¯ãæååãUTF-16ã®ãŸãŸç«¯æ«å ã®ããŒã¿ããŒã¹ïŒSQLiteïŒãžæž¡ããŸããSQLiteã¯æååãUTF-8ã§æã€ãããä¿åãããšãã«ã¯UTF-16ããUTF-8ãžãèªã¿åºããšãã«ã¯UTF-8ããUTF-16ãžãSQLiteèªèº«ã倿ããŸããèªã¿åºããšãã®ãã®å€æã§ãæåãšããŠæç«ããªãéšåã¯**ãᅵãïŒU+FFFDã眮ææåïŒãšãã代ããã®èšå·ã«çœ®ãæããããŸã**ãããããåé ã®ãᅵãã®æ£äœã§ããã1ã€ç®ã®çåã§èŠãããµãã²ãŒããã¢ã®ååã ããæ®ãçŸè±¡ããå®éã«èµ·ããŠããããšã«ãªããŸãã
3ã€ã®åŠçã䞊ã¹ãŠã¿ã
ãããŸã§ã®3ã€ã®åŠçããOSããšã«äžŠã¹ããšæ¬¡ã®ããã«ãªããŸãã
| åŠç | Android | iOS |
|---|---|---|
| ã«ãŠã³ã¿ | æžèšçŽ ã¯ã©ã¹ã¿ | æžèšçŽ ã¯ã©ã¹ã¿ |
| å ¥åã®äžé | ã³ãŒããŠããã | æžèšçŽ ã¯ã©ã¹ã¿ |
| ä¿åã®åãè©°ã | ã³ãŒããŠããã | ã³ãŒããŠããã |
iOSã§ã¯ãå ¥åæ¬ã¯æžèšçŽ ã¯ã©ã¹ã¿ãä¿åã¯ã³ãŒããŠããããšãå ¥åæ¬ãšä¿åãšã§æ°ããåäœãéã£ãŠããã®ã§ãã
ãã®ãããèŠãç®ã§300æåã®æååããã®ãŸãŸä¿ååŠçã«å±ããtakeãçµµæåã®éäžã§åã£ãŠããŸãããåé ã®iOSã®æååãã¯ããã®ããã§ããAndroidã¯ãå
¥åã®æç¹ã§ã³ãŒããŠãããåäœã«åãè©°ããŠããããã300ãè¶
ããæååã¯ä¿ååŠçãŸã§å±ããŸããããã ããåãç®ãçµµæåã®éäžã«åœããã°ãå
¥åã®æç¹ã§åãããã«å²ããŸãã
ããšããšã¯ã3ã€ã®åŠçãšãã³ãŒããŠãããã§æ°ããŠããŸãããããããã«ãŠã³ã¿ãæžèšçŽ ã¯ã©ã¹ã¿ã«å€ãããã®åŸiOSã®å ¥åã®äžéãã«ãŠã³ã¿ã«ããããŸãããäžæ¹ãAndroidã®å ¥åã®äžéãšä¿åã®åãè©°ãã¯ãã³ãŒããŠãããã®ãŸãŸæ®ã£ãŠããŸãããç¹ã«ä¿åã®çŽåã®åãè©°ãã¯ããžãã¯ã®å¥¥ã«ãããç»é¢ããã¯èŠããŸãããã«ãŠã³ã¿ãå ¥åæ¬ãçŽãããšãã«ãèæ ®ãããããã®ãŸãŸæ®ã£ãŠããã®ã§ãã
åäœããããã
ã§ã¯ãã©ãçŽãã°ããã§ããããããŠãŒã¶ãŒã«èŠããäžéã¯ãèŠãç®ã®æåæ°ã§ããã¹ãã§ããããã«ãæžèšçŽ ã¯ã©ã¹ã¿ã¯ãéäžã§åã£ãŠãæåãå£ããªãå¯äžã®åäœã§ããããŸããã³ãŒããŠãããã§åãã°ãµãã²ãŒããã¢ãå²ããŠãᅵãã«ãªããã³ãŒããã€ã³ãã§åã£ãŠãðšâð©âð§âðŠãðšâð©ã«ãªããªã©ãèŠãç®ãå€ãã£ãŠããŸããŸããããã§ãã³ãŒããŠãããã®ãŸãŸæ®ã£ãŠããAndroidã®å ¥åã®äžéãšä¿åã®åãè©°ãããæžèšçŽ ã¯ã©ã¹ã¿ã«ããããŸãã
countGraphemesã®é£ã«ãæžèšçŽ ã¯ã©ã¹ã¿åäœã§åãã颿°ã远å ããlengthãšsubstringãtakeã眮ãæããŸããã
// commonMain: ãåããã宣èšãå
±æãã
expect fun clampToGraphemes(text: String, limit: Int): String
// androidMain: \X ã®å¢çïŒMatcher.endïŒã limit åç®ãŸã§é²ããŠåã
actual fun clampToGraphemes(text: String, limit: Int): String {
val m = GRAPHEME.matcher(text)
var count = 0
var end = 0
while (m.find()) {
count++
end = m.end()
if (count >= limit) break
}
return text.substring(0, end) // æžèšçŽ ã¯ã©ã¹ã¿ã®éäžã§ã¯åããªã
}
// ä¿åïŒresolveTitle ã® return æ
return clampToGraphemes(draft.title, PLAYLIST_TITLE_MAX_LENGTH)
// Androidã®å
¥åã®äžéïŒsetTitle ã®å€å®ãšåãè©°ã
val trimmed = clampToGraphemes(title, PLAYLIST_TITLE_MAX_LENGTH)
çŽãåãšçŽããåŸã®3ã€ã®åŠçãæ¯ã¹ããšã次ã®ãšããã§ãã
| åŠç | çŽãåïŒAndroid / iOSïŒ | çŽããåŸïŒäž¡OSïŒ |
|---|---|---|
| ã«ãŠã³ã¿ | æžèšçŽ ã¯ã©ã¹ã¿ / æžèšçŽ ã¯ã©ã¹ã¿ | æžèšçŽ ã¯ã©ã¹ã¿ |
| å ¥åã®äžé | ã³ãŒããŠããã / æžèšçŽ ã¯ã©ã¹ã¿ | æžèšçŽ ã¯ã©ã¹ã¿ |
| ä¿åã®åãè©°ã | ã³ãŒããŠããã / ã³ãŒããŠããã | æžèšçŽ ã¯ã©ã¹ã¿ |
ããã§ã3ã€ã®åŠçã¯ã©ããæžèšçŽ ã¯ã©ã¹ã¿ã§æ°ããããã«ãªããŸããã
- ã«ãŠã³ã¿ïŒ
countGraphemesã§æ°ããïŒäž¡OSïŒ - å
¥åã®äžéïŒiOSã¯
countGraphemesã§æ°ããŠæ¢ããAndroidã¯clampToGraphemesã§åãè©°ãã - ä¿åã®åãè©°ãïŒ
clampToGraphemesã§åãïŒäž¡OSïŒ
æ°ãã颿°ãšåã颿°ã¯å
±éã³ãŒãã®1ç®æã«çœ®ãã3ã€ã®åŠçã¯ã©ãããããåŒã¶åœ¢ã«ããŠããŸããã©ãã1ç®æã«ã§ãçŽ ã®lengthãtakeãæ®ã£ãŠããã°ããŸãåããããèµ·ããããã§ãã
ãŸãšã
ðã¯ã人éãæ°ããã°1æåã§ãããKotlinã®lengthãtakeã§æ°ãããš2æåã§ããä»åã®æååãã¯ããã®ã1æåãã®åäœãåãéããããšã§èµ·ããŸããã
- åã1æåã§ããæžèšçŽ ã¯ã©ã¹ã¿ã»ã³ãŒããã€ã³ãã»ã³ãŒããŠãããã®ã©ãã§æ°ãããã«ãã£ãŠåæ°ãå€ãã
- æåæ°ãæ±ãåŠçïŒæ°ããã»å ¥åãæ¢ããã»åãïŒã¯ããã¹ãŠåãåäœã§ããããããŠãŒã¶ãŒã«èŠããæåæ°ãªããæžèšçŽ ã¯ã©ã¹ã¿ã«ãã
- æ°ãã颿°ãšåã颿°ã¯å ±éã³ãŒãã®1ç®æã«çœ®ããã©ã®åŠçãããåã颿°ãåŒã¶
ãããã«
ä»ååãäžããã®ã¯äžäŸã§ãå ¥åæ¬ã§ã®çµµæåã®æ±ãã«ã¯ã»ãã«ãèŠæŠããŸãããSwiftãšKotlinã§ããã©ã«ãã®æååã®ä»æ§ãéãããšããæ¥æ¬èªå ¥åã§å€æäžã®æåãã©ãæ±ãããªã©ã§ããAndroidãšiOSã§å ±éã®åŠçãæã€KMPãªãã§ã¯ã®é£ãããæããŸããã
äžæ¹ã§ãæ®æ®µã¯ããŸãæèããªãæåã³ãŒãã®ä»æ§ãŸã§ãã©ã£ãããšã§ãUnicodeãæååã®åŠçãžã®çè§£ãæ·±ããããŸããã
æåŸãŸã§èªãã§ããã ãããããšãããããŸããã
åèæç®
- UAX #29: Unicode Text Segmentation
- UTS #51: Unicode Emoji
- Kotlin: take
- Kotlin: Characters
- java.util.regex.PatternïŒ\Xã®å®çŸ©ïŒ
- Kotlin Docs: Expected and actual declarations
- Unicode FAQ: UTF-8, UTF-16, UTF-32 & BOMïŒãµãã²ãŒããã¢ã®ä»çµã¿ïŒ
ãã®èšäºã¯RecoChoku Engineering Blogã®èšäºã転èŒãããã®ãšãªããŸãã




