Реализация Clipboard-интеграции в мобильном приложении
Clipboard — один из тех API, который выглядит тривиально, пока не сталкиваешься с его поведением на разных версиях iOS и Android. Начиная с iOS 14 каждый доступ к UIPasteboard.general без согласия пользователя показывает системный баннер «[App] вставила из буфера обмена». На Android 12+ аналогичный тост. Это не баг — это намеренное поведение платформ.
iOS: UIPasteboard и ограничения
UIPasteboard.general.string — синхронное чтение. Но с iOS 14 приложение получает предупреждение при каждом чтении, если оно не инициировано явным действием пользователя (нажатием кнопки «Вставить»).
Правильный подход — использовать UIPasteControl (iOS 16+): системная кнопка, которая читает буфер обмена без предупреждения, потому что пользователь явно нажал на неё:
let pasteControl = UIPasteControl(configuration: UIPasteControl.Configuration())
pasteControl.target = self
// реализуем UIPasteConfigurationSupporting
override func paste(itemProviders: [NSItemProvider]) {
for provider in itemProviders {
if provider.canLoadObject(ofClass: NSString.self) {
provider.loadObject(ofClass: NSString.self) { string, _ in
DispatchQueue.main.async {
self.handlePastedText(string as? String)
}
}
}
}
}
Для iOS 14-15, где UIPasteControl недоступен, единственный способ не показывать баннер — читать буфер только в applicationDidBecomeActive или при явном tap-действии. Чтение в viewDidLoad или в фоне — гарантированный баннер.
Запись в буфер обмена — без ограничений: UIPasteboard.general.string = "text". Но если записываем сложный контент (изображение + текст), используем setItems([["public.plain-text": text, "public.png": imageData]]) с явными UTI-типами.
Android: ClipboardManager
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
// Запись
val clip = ClipData.newPlainText("label", "text to copy")
clipboard.setPrimaryClip(clip)
// Чтение
val text = clipboard.primaryClip?.getItemAt(0)?.coerceToText(context)
На Android 13+ (API 33) ClipboardManager.getPrimaryClip() возвращает данные только для приложения на переднем плане или приложения, которое записало данные. Фоновое чтение чужих данных — SecurityException. Это изменение сломало несколько менеджеров паролей при обновлении.
Android 13 также добавил визуальное подтверждение при копировании — системный тост с превью скопированного текста. Приложение может отключить его для конкретной операции: ClipData.newPlainText("label", text).apply { description.extras = PersistableBundle().apply { putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) } } — для чувствительных данных (пароли) тост не показывается.
Flutter и React Native
В Flutter — flutter/services package, Clipboard.getData(Clipboard.kTextPlain) и Clipboard.setData(). Возвращает Future — важно не вызывать в initState без mounted-проверки, иначе setState после dispose даёт assertion error.
В React Native — @react-native-clipboard/clipboard. На iOS под капотом тот же UIPasteboard, на Android — ClipboardManager. Проблема: пакет не абстрагирует iOS 16 UIPasteControl — при необходимости нужен нативный модуль.
Реализация clipboard-интеграции с учётом iOS 14+ и Android 13+: 1 день. Стоимость рассчитывается индивидуально.







