Of course! Here is the App Store Link. Also available on GitHub.
More and more Korean citizens are considering iPhones. Interestingly, the elderly Korean generation is purchasing iPhones more than ever. While many state the primary reasons for choosing a Galaxy to be call recording and Samsung Pay, my observation after my parents switched to iPhones differed.
Unexpectedly, the most significant difficulty for older generations was the keyboard. Korean customers have had no problem typing Korean with a 10-key dial pad since the very early days because there has been a powerful input method known as "천지인" (Cheon-Ji-In, which translates to "Sky, Earth, and Human") to input Hangul, the Korean characters. This was unlike Roman alphabet keyboards, which required several characters to be crammed onto a single button. Koreans had far less of a need to switch to QWERTY keyboards because of this. Many people still use Cheon-Ji-In unless they are members of Gen Z who grew up with smartphones.
The patent for Cheon-Ji-In entered the public domain in 2010. iPhone added support for Cheon-Ji-In in 2013, but its shape differed from that of the standard Cheon-Ji-In. The starkest difference was that the space button and the next character buttons were separate.
- Galaxy:
ㅇ
ᆞ
ㅡ
→ Space →ㅇ
ㅣ
ᆞ
ㄴ
→ Space →ㄴ
ᆞ
ᆞ
ㅣ
ㅇ
- iPhone:
ㅇ
ᆞ
ㅡ
→ Space →ㅇ
ㅣ
ᆞ
ㄴ
→ Next Character →ㄴ
ᆞ
ᆞ
ㅣ
ㅇ
Moreover, the size of each button was smaller, making people produce more typos than ever. For these reasons, I decided to replicate the Galaxy Cheon-Ji-In experience on iPhones.
Let's recreate the original Cheon-Ji-In for iPhones!
I also open-sourced the research notes for this project.
📜 Patents and Legal Rights
First of all, I checked the legal rights. I found that the patent holder, 조관현 (Cho Kwan-Hyeon), had donated the patent to the Korean government, and Cheon-Ji-In had become the national standard for input methods, publicizing the legal rights to the keyboard. So I confirmed these details and then moved on to the development process.
🛠 Readying the Tech
I first read through Apple's Creating a Custom Keyboard document. It was similar to creating a regular iOS app — create ViewControllers and embed the logic inside. However, I wanted to try SwiftUI since it was my first time using it. Moreover, SwiftUI Grid would be a clean approach to organizing buttons. Still, I figured that this class is more suitable for things like the Photos app, which has numerous elements to lay out, and a simple HStack and VStack (similar to display: flex on the Web ecosystem) would suffice my needs.
iPhone third-party keyboards use a unique structure known as extensions. Anything not running on the main iOS app is an extension — custom keyboards are extensions, iOS widgets are extensions, and parts of Apple Watch apps are extensions. I read through Ray Wenderlich and understood how keyboard extensions worked.
A few early prototypes
The gray background of "ㅇ" was iOS's NSRange and setMarkedText. It helped enter the text by marking the currently edited characters, but such methods seemed more suitable for Pinyin in Chinese, not Cheon-Ji-In for Hangul.
Another interesting observation was that the colors of the default iPhone keyboards differed from any default system colors provided with iOS. I had to extract the color with Color Meters one by one.
😶🌫️ But how do we make Cheon-Ji-In?
Supplementary YouTube video on how Hangul system works.
I first thought of individual cases to figure out the input logic of Cheon-Ji-In, then figured that this is tremendously difficult. For example, take:
- To input
않
, we start with안ㅅ
and pressㅅㅎ
to acquire않
. That is, we must check if the characters are "re-mergeable" with the character before. - From
앉
, when we inputㅡ
, it must be안즈
. Therefore, we must check if the last consonant is extractable from the previous character. - From
깚
, when we inputㅂㅍ
, it should result in깔ㅃ
. We must check if the consonants are extractable and switch between fortis and lenis (strong and weak sounds, like '/p/ and /b/', '/t/ and /d/', or '/k/ and /ɡ/' in English). - From
갌
, when we inputㅅㅎ
, it should result in갏
. More than switching betweenㅅ
,ㅎ
, andㅆ
, we must consider double consonant endings likeㄽ
.
These are just a few examples. Even if we used KS X 1001 Combinational Korean Encodings, it took a lot of work to consider all cases. I concluded that using a Finite State Machine required more than 20 data stacks and dozens of states. (I am unsure of this calculation because I guessed some parts of it; there may be a more straightforward implementation.) If you want to try building such an algorithm, refer to this patent's diagrams. I found some implementations online, but they were long and spaghettified. Translating them to the Swift language and understanding the codes would take significant time.
But then I came to an epiphany:
why don't I hardcode every combination?
After all, aren't keyboards supposed to input the same character, given the input sequence is the same? What if I generate all possible combinations and put them into a giant JSON file? Korean character combinations are around 11,000. Even considering previous characters, the combinations seemed to be at most 100K levels. The size of the JSON file will not exceed 2MB.
We are not living in an era where we must golf with KBs of RAM on embedded hardware. As long as Hangul coexists with the human species, someone will recreate Cheon-Ji-In in the future, making constructing the complete Hangul map worth it.
🖨️ Hwalja: The most straightforward Cheon-Ji-In implementation
Therefore, I created Hwalja: the complete map 🗺️ of Hangul, containing all such states and combinations of Cheon-Ji-In. There are around 50,000 states, and the minified JSON is about 500 KB. (Note: Hwalja means movable type in Korean.)
To implement additional higher-level features (such as removing consonants, not characters, on backspace or using timers to auto-insert "next character" buttons), we need more functional workarounds; however, the critical input logic is as simple as the following:
const type = (prev: string, Hwalja: hwalja, key: string, editing: boolean) => {
const last_two_char = prev.slice(-1)
const last_one_char = prev.slice(-2)
if (editing && last_one_char in Hwalja[key]) return prev.slice(0, -2) + Hwalja[key][last_one_char]
if (editing && last_two_char in Hwalja[key]) return prev.slice(0, -1) + Hwalja[key][last_two_char]
return prev + Hwalja[key]['']
}
I boldly claim this is the simplest implementation of Cheon-Ji-In, given its five-liner.
Some may ask how I preprocessed such large combinations; I set the 11,000 final Hangul characters as the destination and traced back what would've been the previous state and what button the user must have entered last. For example, to input 역,
the previous state must have been 여,
and the keypress must have been ㄱ.
Of course, there were many more edge cases. My work from four years ago helped a lot. The following is an interactive example of Cheon-Ji-In, made with Hwalja.
This is an interactive demo of Cheon-Ji-In, made with Hwalja.
I open-sourced Hwalja for platform-agnostic usage.
Please try out the above demo!
Hwalja is the most simplest implementation, not the lightest.
Can't we use combinatory Hangul sets and normalize the combinations to reduce the case count?
On the Hwalja project, Engineer 이성광 (Lee Sung-kwang) pointed out that using Normalization Form D and decomposing consonants will reduce the case count. I only considered Normalization Form D, but Engineer 이성광 is correct. For example, we decompose 안녕
as 안 ᄂᆞᆞㅣㅇ
and use Hwalja to gather ᆞᆞㅣ
into ㅕ
and then normalize ㄴㅕㅇ
into 녕.
I decided to maintain Hwalja's current approach because it aims for the easiest and simplest Cheon-Ji-In implementation. The current system enables developers to stick with "substring" and "replace." If I add dependencies on Normalization Form D and Unicode Normalization, the Hwalja project may be lighter, but the developers using Hwalja must add additional handlers for normalizations. I created Hwalja because using Automata and Finite State Machines had steep learning curves. Thus, requiring any learning curves to use Hwalja violates the original purpose. Also, the final minified version is already 500KB, which is manageable for a full-fledged input engine.
🤖 Implementing Keyboard Autocompletes
Cheon-Ji-In users can type at blazing speeds because of their active use of autocompleted texts (Apple QuickType). In addition, these autocompleted texts continuously learn from the user to assist with typing.
Fortunately, Apple's UIKit supports UITextChecker, which frees us from going down to Core ML and Neural Engine levels. Korean is also supported, and we can use learnWord()
and unlearnWord()
to record data on user activities.
import UIKit
let uiTextChecker = UITextChecker()
let input = "행복하"
let guesses = uiTextChecker.completions(
forPartialWordRange: NSRange(location: 0, length: input.count),
in: input,
language: "ko-KR"
)
/*
[
"행복한", "행복합니다", "행복하게", "행복할", "행복하다", "행복하고", "행복하지",
"행복하다고", "행복하다는", "행복하기", "행복하면", "행복할까", "행복하길",
"행복함을", "행복하기를", "행복함", "행복하니", "행복한테", "행복하자", "행복하네"
]
*/
I used such features to implement the autocomplete feature. Sometimes the flow feels unnatural, or the keyboard does not suggest anything, but this is a perfect implementation for an MVP.