Paste Formatted Text, Not Images or HTML - ios

I am trying to emulate the pasteboard behavior of the iOS Pages and Keynote apps. In short, allowing basic NSAttributedString text formatting (i.e. BIU) to be pasted into a UITextView, but not images, HTML, etc.
BEHAVIOR SUMMARY
If you copy formatted text from the Notes app, Evernote, or text and images from a web site, Pages will only paste the plain text string
If you copy formatted text from within Pages or Keynote, it will paste the formatted text elsewhere in Pages, Keynote, etc.
An undesired consequence, but perhaps important to acknowledge, is that neither Notes app or Evernote will paste formatted text copied from Pages or Keynote. I am speculating that the discrepancy between apps is the use of NSAttributedStrings, versus HTML?
How is this accomplished? On Mac OS, it appears you can ask the pasteboard to return different types of itself, have it provide both a rich text and a string representation, and use rich text as preferred. Unfortunately, the readObjectsForClasses doesn't appear to exist for iOS. That said, I can see via log that iOS does have an RTF related type of pasteboard, thanks to this post. I can't however, find a way to request an NSAttributedString version of pasteboard contents so I can prioritize it for pasting.
BACKGROUND
I have an app that allows basic NSAttributedString user editable formatting (i.e. bold, italic, underline) of text in UITextViews. Users want to copy text from other apps (e.g. web page in Safari, text in Notes app), to paste into a UITextView in my app. Allowing pasteboard to operate as default means I may end up with background colors, images, fonts, etc. that my app isn't intended to handle. Example below shows text how copied text with a background color looks when pasted into my app's UITextView.
I can overcome 1 by subclassing UITextView
- (void)paste:(id)sender
{
UIPasteboard *pasteBoard = [UIPasteboard generalPasteboard];
NSString *string = pasteBoard.string;
NSLog(#"Pasteboard string: %#", string);
[self insertText:string];
}
The unintended consequence is, losing the ability to retain formatting of text that's copied from within my app. Users may want to copy text from one UITextView in my app, and paste it to another UITextView in my app. They will expect formatting (i.e. bold, italics, underline) to be retained.
Insight and suggestions appreciated.

Some copy/paste goodies and ideas, for you :)
// Setup code in overridden UITextView.copy/paste
let pb = UIPasteboard.generalPasteboard()
let selectedRange = self.selectedRange
let selectedText = self.attributedText.attributedSubstringFromRange(selectedRange)
// UTI List
let utf8StringType = "public.utf8-plain-text"
let rtfdStringType = "com.apple.flat-rtfd"
let myType = "com.my-domain.my-type"
Override UITextView copy: and use your custom pasteboard type
pb.setValue(selectedText.string, forPasteboardType: myType)
To allow rich text copy (in copy:):
// Try custom copy
do {
// Convert attributedString to rtfd data
let fullRange = NSRange(location: 0, length: selectedText.string.characters.count)
let data:NSData? = try selectedText.dataFromRange(fullRange, documentAttributes: [NSDocumentTypeDocumentAttribute: NSRTFDTextDocumentType])
if let data = data {
// Set pasteboard values (rtfd and plain text fallback)
pb.items = [[rtfdStringType: data], [utf8StringType: selectedText.string]]
return
}
} catch { print("Couldn't copy") }
// If custom copy not available;
// Copy as usual
super.copy(sender)
To allow rich text paste (in paste:):
// Custom handling for rtfd type pasteboard data
if let data = pb.dataForPasteboardType(rtfdStringType) {
do {
// Convert rtfd data to attributedString
let attStr = try NSAttributedString(data: data, options: [NSDocumentTypeDocumentAttribute: NSRTFDTextDocumentType], documentAttributes: nil)
// Bonus: Possibly strip all unwanted attributes here.
// Insert it into textview
replaceSelection(attStr)
return
} catch {print("Couldn't convert pasted rtfd")}
}
// Default handling otherwise (plain-text)
else { super.paste(sender) }
Even better then using a custom pasteboard type, white-list all possibly wanted tags, loop through and strip away all other on paste.
(Bonus: help UX in other apps by stripping away unnecessary attributes you've added, on copy (like font and fg-color))
Also worth noting, the textView might not want to allow pasting when the pasteboard contains a specific type, to fix that:
// Allow all sort of paste (might want to use a white list to check pb.items agains here)
override func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool {
if action == #selector(UITextView.paste(_:)) {
return true
}
return super.canPerformAction(action, withSender: sender)
}
Furthermore, cut: could be nice to implement as well. Basically just copy: and replaceSelection(emptyString)
For your convenience:
// Helper to insert attributed text at current selection/cursor position
func replaceSelection(attributedString: NSAttributedString) {
var selectedRange = self.selectedRange
let m = NSMutableAttributedString(attributedString: self.attributedText)
m.replaceCharactersInRange(self.selectedRange, withAttributedString: attributedString)
selectedRange.location += attributedString.string.characters.count
selectedRange.length = 0
self.attributedText = m
self.selectedRange = selectedRange
}
Good luck!
Refs:
Uniform Type Identifiers Reference

This should be a comment on Leonard Pauli's answer, but I don't have enough reputation to make comments yet.
Instead of:
selectedRange.location += attributedString.string.characters.count
(or attributedString.string.count as it is in more recent versions of Swift)
It's best to use:
selectedRange.location += attributedString.length
Otherwise when you paste text that contains emoji that cause attributedString.length and attributedString.string.count to differ, the selection will end up in the wrong place.

Related

Put two words in a same line if they fit, if not put them in a new line

So I have the following question:
I have attributed string contained in a UITextView. Attributed string is contained of two parts - the normal one is ordinary text, and the second part has NSURL in it - so for better visualisation it would look like:
For more information READ HERE
The problem that I am having is that if the text fits in the one line I must keep it in one line, and if "HERE" falls in second line I must put READ also in the second line.
So, the first case - if all fits
For more information READ HERE
All other cases -
For more information
READ HERE
I tried to do it with checking if size of the screen is bigger than textfield bounds but it didn't work:
if (label.bounds.size.width < size.width) ...
I tried also other similar solutions but i think they are all addable on normal UILabels and not modified attributed texts.
If you have any idea how to deal with this I would appreciate it.
Thanks :)
There are multiple solution to resolve this.
First one is to use non-breakable space
simply add "\u{00a0}" in between click here like CLICK\u{00a0}HERE
here is the link
Second one is instead of adding space you can add "_" into it.
e.g.
"CLICK_HERE" and you can replace the color of "_" with clear.
here is the code
class ViewController: UIViewController {
#IBOutlet weak var temp : UILabel!
override func viewDidLoad() {
let myString = "For more detail information READ_HERE"
let attributedString: NSMutableAttributedString = NSMutableAttributedString(string: myString)
attributedString.setColorForText(textForAttribute: "_", withColor: .clear)
temp.attributedText = attributedString
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
extension NSMutableAttributedString {
func setColorForText(textForAttribute: String, withColor color: UIColor) {
let range: NSRange = self.mutableString.range(of: textForAttribute, options: .caseInsensitive)
self.addAttribute(NSAttributedString.Key.foregroundColor, value: color, range: range)
}
}

How to convert smileys to emoticon?

Is there a way to convert ":)" to 😊 like : detection and conversion, I have a UITextView in a chat application which should convert the smiley to the respective emoticon.
Get the offical list: emoji
Just an example: yourTextView.text = "your smiley: \u{1f642}"
If you want to convert emoticons to emoji on the fly either you need to specify it by yourself and analyze the input string or using a 3rd-party lib e.g. pods for converting and watching input string through text change events e.g.: docs
You can use the logic in this package npm, you find also a map of smile and the respective emoji:
https://www.npmjs.com/package/smile2emoji
I have created this simple class based on the npm package suggested by #emish89 https://www.npmjs.com/package/smile2emoji.
https://gist.github.com/lorenzOliveto/f20a89e9f68276cae21497a177ad8a4c
Swift 5
You should implement delegate textViewDidChange for your UITextView and find all need substrings in its text then replace them inside with textStorage property:
extension ViewController : UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
guard let text = textView.text else { return }
var index = text.startIndex
while let range = text.range(of: ":)", range: index..<text.endIndex) {
index = range.upperBound
textView.textStorage.replaceCharacters(in: NSRange(range, in: text), with: "😀")
}
}
}
It works while editing or paste text.

How to use NSAttributedStrings in iOS with Swift?

I would like to have a label with an attributed string that changes dynamically at runtime. Something like this
Is there a better way to dynamically change the price value without hardcoding the attributed keys dictionaries and manually building the NSAttributedString?
The best way to approach Attributed Strings on iOS is by using the built-in Attributed Text editor in the interface builder and avoid uneccessary hardcoding NSAtrributedStringKeys in your source files.
You can later dynamically replace placehoderls at runtime by using this extension:
extension NSAttributedString {
func replacing(placeholder:String, with valueString:String) -> NSAttributedString {
if let range = self.string.range(of:placeholder) {
let nsRange = NSRange(range,in:valueString)
let mutableText = NSMutableAttributedString(attributedString: self)
mutableText.replaceCharacters(in: nsRange, with: valueString)
return mutableText as NSAttributedString
}
return self
}
}
Add a storyboard label with attributed text looking like this.
Then you simply update the value each time you need like this:
label.attributedText = initalAttributedString.replacing(placeholder: "<price>", with: newValue)
Make sure to save into initalAttributedString the original value.
You can better understand this approach by reading this article:
https://medium.com/mobile-appetite/text-attributes-on-ios-the-effortless-approach-ff086588173e
May not know, this is what you're looking for but will solve your problem.
You may use labels for each and update amount easily without touching attributed string.
Here is result:
Sample code, I've tried for this demo:
#IBOutlet weak var lblAmount: UILabel!
func pricelabel() -> Void {
var amount = 0
Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) { (timer) in
amount += 10
self.lblAmount.text = "\(amount)"
}.fire()
}

Get the current text inside of a textfield for IOS custom keyboard

I am developing a IOS custom keyboard. I was wondering if there was a way to fetch the current text inside of the text field and how it would work.
For example, we can use textDocumentProxy.hasText() to see if the textfield has text inside but I want to know the exact string that is inside the textfield.
The closest things would be textDocumentProxy.documentContextBeforeInput and textDocumentProxy.documentContextAfterInput. These will respect sentences and such, which means if the value is a paragraph, you will only get the current sentence. Users have been known to retrieve the entire string by repositioning the cursor multiple times until everything is retrieved.
Of course, you generally do not have to worry about this if the field expects a single value like a username, email, id number, etc. Combining the values of both before and after input contexts should suffice.
Sample Code
For the single phrase value, you would do:
let value = (textDocumentProxy.documentContextBeforeInput ?? "") + (textDocumentProxy.documentContextAfterInput ?? "")
For values that might contain sentence ending punctuation, it will be a little more complicated as you need to run it on a separate thread. Because of this, and the fact that you have to move the input cursor to get the full text, the cursor will visibly move. It is also unknown whether this will be accepted into the AppStore (after all, Apple probably did not add an easy way to get the full text on purpose in order to prevent official custom keyboards from invading a user's privacy).
Note: the below code is based off of this Stack Overflow answer except modified for Swift, removed unnecessary sleeps, uses strings with no custom categories, and uses a more efficient movement process.
func foo() {
dispatch_async(dispatch_queue_create("com.example.test", DISPATCH_QUEUE_SERIAL)) { () -> Void in
let string = self.fullDocumentContext()
}
}
func fullDocumentContext() {
let textDocumentProxy = self.textDocumentProxy
var before = textDocumentProxy.documentContextBeforeInput
var completePriorString = "";
// Grab everything before the cursor
while (before != nil && !before!.isEmpty) {
completePriorString = before! + completePriorString
let length = before!.lengthOfBytesUsingEncoding(NSUTF8StringEncoding)
textDocumentProxy.adjustTextPositionByCharacterOffset(-length)
NSThread.sleepForTimeInterval(0.01)
before = textDocumentProxy.documentContextBeforeInput
}
// Move the cursor back to the original position
self.textDocumentProxy.adjustTextPositionByCharacterOffset(completePriorString.characters.count)
NSThread.sleepForTimeInterval(0.01)
var after = textDocumentProxy.documentContextAfterInput
var completeAfterString = "";
// Grab everything after the cursor
while (after != nil && !after!.isEmpty) {
completeAfterString += after!
let length = after!.lengthOfBytesUsingEncoding(NSUTF8StringEncoding)
textDocumentProxy.adjustTextPositionByCharacterOffset(length)
NSThread.sleepForTimeInterval(0.01)
after = textDocumentProxy.documentContextAfterInput
}
// Go back to the original cursor position
self.textDocumentProxy.adjustTextPositionByCharacterOffset(-(completeAfterString.characters.count))
let completeString = completePriorString + completeAfterString
print(completeString)
return completeString
}

How to copy text to clipboard/pasteboard with Swift

I'm looking for a clean example of how to copy text to iOS clipboard that can then be used/pasted in other apps.
The benefit of this function is that the text can be copied quickly, without the standard text highlighting functions of the traditional text copying.
I am assuming that the key classes are in UIPasteboard, but can't find the relevant areas in the code example they supply.
If all you want is plain text, you can just use the string property. It's both readable and writable:
// write to clipboard
UIPasteboard.general.string = "Hello world"
// read from clipboard
let content = UIPasteboard.general.string
(When reading from the clipboard, the UIPasteboard documentation also suggests you might want to first check hasStrings, "to avoid causing the system to needlessly attempt to fetch data before it is needed or when the data might not be present", such as when using Handoff.)
Since copying and pasting is usually done in pairs, this is supplemental answer to #jtbandes good, concise answer. I originally came here looking how to paste.
iOS makes this easy because the general pasteboard can be used like a variable. Just get and set UIPasteboard.general.string.
Here is an example showing both being used with a UITextField:
Copy
UIPasteboard.general.string = myTextField.text
Paste
if let myString = UIPasteboard.general.string {
myTextField.insertText(myString)
}
Note that the pasteboard string is an Optional, so it has to be unwrapped first.
Copying text from the app to the clipboard:
let pasteboard = UIPasteboard.general
pasteboard.string = employee.phoneNumber
SWIFT 4
UIPasteboard.general.string = "TEXT"
in Swift 5 i can copy text to clipboard using
UIPasteboard.general.string = "Hello world"
then you can paste the text anywhere of your device
Write below the code where you want to Copying String or Text
UIPasteboard.general.string = "Dhaval Gevariya" // Put your String here
this is for read String from clipboard.
var readString = UIPasteboard.general.string
import UIKit.UIPasteboard
extension UIPasteboard {
static func pasteToClipboard(_ content: String) {
self.general.string = content
}
static func readFromClipboard() -> String? {
return self.general.string
}
}

Resources