Implementing AI-Powered Writing Assistant in Mobile Applications
A Writing Assistant in mobile isn't just a text field with an "improve" button. It's an editor that understands context: knows document type (email, post, report), supports streaming, doesn't reset cursor on insertion, survives background mode without state loss.
Most implementations fail on these details.
Editor architecture with AI
First choice: use native UITextView/EditText or build custom. For most cases, native is right—but AI features add complexity: inserting generated text without destroying cursor position or selection.
// iOS: insert AI text via NSTextStorage without resetting cursor
func insertAIText(_ text: String, at range: NSRange) {
guard let textView = self.textView else { return }
let storage = textView.textStorage
// Save cursor position
let cursorOffset = textView.selectedRange.location
storage.beginEditing()
storage.replaceCharacters(
in: range,
with: NSAttributedString(string: text, attributes: defaultTypingAttributes)
)
storage.endEditing()
// Restore cursor after insertion
let newOffset = cursorOffset + (text.count - range.length)
textView.selectedRange = NSRange(location: max(0, newOffset), length: 0)
}
Android equivalent via Editable.replace() + preserve SelectionStart/SelectionEnd through android.text.Selection.
Streaming and progressive insertion
Writing Assistant must stream—users see AI typing. Technically it's AsyncStream<String> (iOS) or Flow<String> (Android), each chunk appended to the active paragraph.
Common issue: fast streaming (> 20 chars/sec) causes UITextView lag on long texts. Reason: textStorage triggers layout pass on every change. Solution: batch updates:
private var streamBuffer = ""
private var streamTimer: Timer?
func appendStreamChunk(_ chunk: String) {
streamBuffer += chunk
if streamTimer == nil {
streamTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: false) { [weak self] _ in
guard let self else { return }
self.textView.textStorage.beginEditing()
self.textView.textStorage.append(NSAttributedString(string: self.streamBuffer))
self.textView.textStorage.endEditing()
self.streamBuffer = ""
self.streamTimer = nil
}
}
}
One layout pass every 50 ms instead of 20. On iPhone SE 2nd gen, the difference is visible.
Assistant modes: avoid UI clutter
Writing Assistant typically offers: continue text, rewrite selection, change tone, shorten, expand. Show all buttons and UI becomes chaos.
Right pattern: context menu appears only with selection (for "rewrite," "change tone"), floating button at paragraph end (for "continue"). Two triggers, two UX patterns.
// Android Compose—floating assistant button
@Composable
fun WritingAssistantOverlay(
textFieldState: TextFieldState,
onContinue: () -> Unit,
onRewrite: (String) -> Unit
) {
val hasSelection = textFieldState.selection.length > 0
AnimatedVisibility(visible = !hasSelection) {
FloatingActionButton(
onClick = onContinue,
modifier = Modifier.align(Alignment.BottomEnd)
) {
Icon(Icons.Default.AutoAwesome, "Continue")
}
}
AnimatedVisibility(visible = hasSelection) {
ContextualMenu(
items = listOf("Rewrite", "Change tone", "Shorten"),
onSelect = { action ->
val selected = textFieldState.text.substring(textFieldState.selection)
onRewrite("$action: $selected")
}
)
}
}
Prompts for each action
Each action is a separate prompt; can't generalize to one universal. Working templates:
Continue text:
Continue the following text naturally, maintaining the same style, language, and tone.
Write 1-3 sentences only. Do not repeat what was already written.
Text: {last_500_chars}
Rewrite selected:
Rewrite the following text. Keep the core meaning but improve clarity and flow.
Language: {detected_language}. Style: {business|casual|formal}.
Text: {selected_text}
Change tone:
Rewrite this text in a {formal|casual|empathetic|assertive} tone.
Preserve all key information. Output only the rewritten text.
Text: {selected_text}
Detect language via NLLanguageRecognizer (iOS) or TextClassifier from ML Kit (Android). Don't rely on Locale.current—user might write in different language.
State during background mode
If user backgrounds the app during generation, iOS sends task to URLSession with background config or cancels. Save prompt and status to UserDefaults/SharedPreferences and restore on return.
For long generations (> 15 sec for large texts), use Background Tasks API on iOS or WorkManager on Android—streaming in background isn't possible, but get result via push notification.
Timeline estimates
Basic assistant with "improve" button via OpenAI—3–5 days. Full editor with streaming, context menu, modes, and state persistence—3–4 weeks. Offline support with on-device model (CoreML/TFLite)—separate 2+ weeks.







