After a holiday related nightmare with version control, I can't work out why the below is no longer working.
I'm trying to save attributed text to an .rtf file which is then read later.
This was working but now isn't for some reason I can't understand after a couple of hours of going through line by line.
I'm trying to save the contents of a textview to a .rtf file with its attributes like this :
func saveExistingScene() {
let textView = textViewOutlet
let fileURL = getFileURL() //shared function to get the URL of an existing RTF file: there’s a different one for “setFileURL” when saving a new scene
textView?.attributedText = NSAttributedString(string: (textView?.text)!, attributes: nil)
if let attributedText = textView?.attributedText {
let documentAttributes: [NSAttributedString.DocumentAttributeKey: Any] = [.documentType: NSAttributedString.DocumentType.rtf]
do {
let rtfData = try attributedText.data(from: NSRange(location: 0, length: attributedText.length), documentAttributes: documentAttributes)
let rtfString = String(data: rtfData, encoding: .utf8) ?? ""
try rtfString.write(to: fileURL, atomically: true, encoding: .utf8)
print("Saved an update")
} catch {
print("failed to save an update with error: \(error)")
}
}
The text can be changed by the user to bold and italics when they press CMD+B or CMD+I as per here (haven't put them all as don't think there is a need)
let makeBold: [String : Any] = [
NSAttributedStringKey.font.rawValue: UIFont(name: "AvenirNext-Bold", size: 15.0)!, NSAttributedStringKey.foregroundColor.rawValue: UIColor.white
]
which is enabled via key commands. This works fine and is reflected in the UITextView properly.
The generic colour of the text in the textview is set as white in viewDidLoad by
textViewOutlet.UIColour = white
The file is saving fine and creating an .rtf file but the attributes are all being stripped away and leaves it in helvetica, in black with no bold or italics.
This is so frustrating because it was working at some point in the few days before going on holiday! Help appreciated!
UPDATE : I've added a print statement below the 'if let' and it is printing out in RTF format but is defaulting to Helvetica size 12 rather than the attributes that are actually in the textview (white, avenir, mix of bold/italic/normal etc). Does that help anyone!?
Related
Let's assume I have this code:
let string = "This is a <b>bold</b> text"
let data = Data(string.utf8)
let attributedText = try! NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil)
textField.attributedText = attributedText
Result: This is a bold text
Now my textField is editable, and let's say I have my cursor here:
This is a bold tex|t
You can see my cursor between x and t
I have a variable called cursorPosition where I store the cursor position anytime it changes and in this current case, the value of cursorPosition will be 18
Now my question: Is there a possible way I can get all the NSAttributedString using NSRange. So I want to get all NSAttributedString before my cursor position using 0..<cursorPosition and the result will be:
This is a <b>bold</b> tex
And not
This is a bold tex
You can get an attributed substring of an NSAttributedString using the attributedSubstring method.
let attrStr: NSAttributedString = ... // some attributed string
let range = NSRange(location: 0, length: someLength)
let attrSubStr = attrStr.attributedSubstring(from: range)
But this is not going to give you an HTML string. This is going to give you another NSAttributedString representing the desired range. If you want HTML then you need to take extra steps to convert the attributed string into HTML.
I tried to do like this, but it does not work, the text is not copied
if let urlScheme = URL(string: "instagram-stories://share") {
if UIApplication.shared.canOpenURL(urlScheme) {
let imageData: Data = UIImage(systemName:"pencil.circle.fill")!.pngData()!
let items:[String: Any] = ["public.utf8-plain-text": "text","com.instagram.sharedSticker.backgroundImage": imageData]
UIPasteboard.general.setItems([items])
UIApplication.shared.open(urlScheme, options: [:], completionHandler: nil)
}}
I would really appreciate any advice
2 things I can think of:
First, I am not sure the below data in your array can be properly handled by pastebin
let items:[String: Any] = ["public.utf8-plain-text": "text","com.instagram.sharedSticker.backgroundImage": imageData]
Next it seems that the activity of sharing causes data in the PasteBoard to be lost so I can offer the solution to put valid data into the PasteBoard (I am using string for example, you can use something else" from the completion handler of your sharing action, something like this might solve it:
UIApplication.shared.open(urlScheme, options: [:]) { (_) in
UIPasteboard.general.string =
"click on the screen until the paste button appears: https://google.com"
}
EDIT
It seems your set up was right and on reading the docs, IG stories should handle the Paste automatically as it seems to check the pasteboard when you execute this url scheme: instagram-stories://share - so it seems IG checks the pasteboard and performs a paste programmatically and that is why the pasteboard gets cleared.
Maybe because the image you choose is black on the black instagram background, it seems nothing is shared but with some proper image the result seems fine.
The other thing I noticed after reading their docs, they do not allow you to set captions anymore, I cannot find this key anymore public.utf8-plain-text
Another idea I can offer to share text is to convert text into an image and add it as a sticker as the sticker layer comes on top of the background image layer.
You can find multiple ways to convert text to an image and it is not relevant to your solution, here is one way I used
So bringing the code together, I have this:
// Just an example to convert text to UIImage
// from https://stackoverflow.com/a/54991797/1619193
extension String {
/// Generates a `UIImage` instance from this string using a specified
/// attributes and size.
///
/// - Parameters:
/// - attributes: to draw this string with. Default is `nil`.
/// - size: of the image to return.
/// - Returns: a `UIImage` instance from this string using a specified
/// attributes and size, or `nil` if the operation fails.
func image(withAttributes attributes: [NSAttributedString.Key: Any]? = nil, size: CGSize? = nil) -> UIImage? {
let size = size ?? (self as NSString).size(withAttributes: attributes)
return UIGraphicsImageRenderer(size: size).image { _ in
(self as NSString).draw(in: CGRect(origin: .zero, size: size),
withAttributes: attributes)
}
}
}
// Then inside some function of yours
func someFunction() {
if let urlScheme = URL(string: "instagram-stories://share") {
if UIApplication.shared.canOpenURL(urlScheme) {
let imageData: Data = UIImage(named: "bg")!.pngData()!
let textImage: Data = "Shawn Test".image(withAttributes: [.foregroundColor: UIColor.red,
.font: UIFont.systemFont(ofSize: 30.0)],
size: CGSize(width: 300.0, height: 80.0))!.pngData()!
let items = ["com.instagram.sharedSticker.stickerImage": textImage,
"com.instagram.sharedSticker.backgroundImage": imageData]
UIPasteboard.general.setItems([items])
UIApplication.shared.open(urlScheme, options: [:], completionHandler: nil)
}
}
}
I then see this in IG stories with correct background and text as sticker which can be moved.
Only downside of using the sticker is you cannot edit the text in Instagram.
Regarding the research looks like the only one workaround to have a text/link copied in the Pasteboard when IG Story is opened is to use:
UIPasteboard.general.string = "your link here"
but you need to do it with a delay - like:
UIApplication.shared.open(instagramStoryURL, options: [:]) { success in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
UIPasteboard.general.string = "your link here"
}
}
to try to be sure the it won't override:
UIPasteboard.general.items
that contains, for example, "com.instagram.sharedSticker.stickerImage"
Also, please be careful with a delay - as iOS has some privacy restrictions to allow copy data to UIPasteboard when the App is in background (based on the tests we have less than 1 second to do that.
It means that you could try to copy the link this way:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.willResignActiveNotification, object: nil)
}
#objc func appMovedToBackground() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) {
UIPasteboard.general.string = "your link here"
}
}
Anyway, there is one obvious inconvenience.
Each time you try to call "instagram-stories://share" API the first time - you face a system popup that asks for the permisson to allow to open Instagram and also to allow paste the data.
In this case we'll lose, for example, "com.instagram.sharedSticker.stickerImage" data as by the delay it will be overrided by UIPasteboard.general.string.
But we could to make it expected for users by any UI/UX solution with instructions/guide.
I am trying to replace an image in an NSAttributedString with a scaled copy of itself (to fit a UITextView's width), however, in many (not all) cases, the replaced image is a mask of the original. I assume that there is something in the other attributes of the string which is doing this as both the image extraction and the creation of a new attributed string from the image work perfectly.
Note: answers like this how to resize an image or done as a NSAttributedString NSTextAttachment (or set its initital size) show how to construct a string with images (which I can do) or how to modify the size of a text attachment (again, I can do that) prior to creating the string, but not how to modify in place.
Update
Thanks to suggestions by commentator #Larme I have now solved my original problem of resizing an image in-place by changing the original NSTextAttachment.bounds - note, not replacing it, changing it in situ. I'm not going to post that as an answer to my own question, as it doesn't answer the wider question of replacing an image.
Any tips as to how to proceed gratefully accepted.
Here's the basis:
extension NSAttributedString {
func resizeAttachments(maxWidth: CGFloat) -> NSAttributedString {
var replacement = NSMutableAttributedString(attributedString: self)
func replace(_ image: UIImage, in range: NSRange, dict: [NSAttributedString.Key : Any]) {
// ... see below for variants of this
}
self.enumerateAttributes(in: NSMakeRange(0, self.length),
options: [],
using: {dict, range, stop in
for (key, value) in dict {
if key == .attachment, let attachment = value as? NSTextAttachment {
if
let fw = attachment.fileWrapper, fw.isRegularFile,
let d = fw.regularFileContents,
let image = UIImage(data: d)?.resized(toWidth: maxWidth) {
// removing the resize above makes no difference to the issue
// i.e. replacing the image with itself still causes the same problem
replace(image, in: range, dict: dict)
return // Exit the block. Can't have 2 .attachments.
}
}
}
})
return replacement
}
}
So far, a straight forward enumeration on self, modifying the mutable copy string replacement when an image is encountered. Here's what I get:
From this unscaled original:
So, what have I tried in the replace function?
func replace(_ image: UIImage, in range: NSRange, dict: [NSAttributedString.Key : Any]) {
let newAttachment = NSTextAttachment(image: image)
let newCharacter = NSMutableAttributedString(attachment: newAttachment)
// NB if I replace the entire string with `newCharacter` the image is perfect (but obviously the rest of the string has gone)
// Option 1
replacement.replaceCharacters(in: range, with: newCharacter)
// Option 2
var newDict: [NSAttributedString.Key : Any] = dict
newDict[.attachment] = newAttachment
replacement.addAttributes(newDict, range: range)
// Option 3
replacement.removeAttribute(.attachment, range: range)
replacement.addAttribute(.attachment, value: newAttachment, range: range)
}
I can't find anything anywhere about actually replacing images. As I said, the extraction of the image works fine - if I just return a new attributed string with only the extracted image then it's perfect. If I append the image to the end, it works. It's modifying it in place that seems to be the issue. Has anyone ever done this successfully?
When I add an image attachment to an UITextView with a foreground colour set, the image is blanked out with the set colour:
let attrString = NSMutableAttributedString(string: rawText, attributes: [.font: UIFont.systemFont(ofSize: 17), .foregroundColor: UIColor.black])
let attachment = NSTextAttachment(image: image)
let imgStr = NSMutableAttributedString(attachment: attachment)
attrString.append(imgStr)
textview.attributedText = attrString
When I removed .foregroundColor: UIColor.black, the image is displayed correctly, but I need to be able to set the attributed text colour.
I tried to explicitly remove the .foregroundColor attribute after adding the image attachment with no luck. I also tried to remove the .foregroundColor attribute from most of the text and it still wouldn't work, only removing the attribute from the entire string works:
attrString.removeAttribute(.foregroundColor, range: NSRange(location: attrString.length-1, length: 1)) // does not work
// -------
attrString.removeAttribute(.foregroundColor, range: NSRange(location: 1, length: attrString.length-1)) // does not work
// -------
attrString.removeAttribute(.foregroundColor, range: NSRange(location: 0, length: attrString.length)) // works but no text colour
This is developed on Xcode 11.0, iOS 13. Is this a UITextView/iOS bug or an expected behaviour (which, I don't think is likely)? How do I display the image correctly with the text colour set?
It looks like there is a bug with the NSTextAttachment(image:) constructor (on iOS 13, at the time of this answer), the following image attachment construction works correctly:
let attachment = NSTextAttachment()
attachment.image = image
I am building a RSS app, and am working on displaying all of the content inside the app, not in a WebView. I am trying to figure out how to create custom styles for each HTML element in a UILabel.
HTML text in UILabel with Attributed String:
I know that this can be done with a NSAttributedString. I've been able to display the HTML text in the UILabel (see screenshot) through an attributed string:
do {
let attrStr = try NSAttributedString(
data: (content.data(using: String.Encoding.unicode, allowLossyConversion: true)!),
options: [ NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
documentAttributes: nil)
bodyLabel.attributedText = attrStr
} catch let error {
print(error)
}
However I can not figure out how to set the text in between each tag to have the specific style associated with that tag. Here is an example of the HTML code I am trying to style:
<p>Back in 2006, when the iPhone was a mere rumor, Palm CEO Ed Colligan was asked if he
was worried:</p>
<blockquote>
<p>“We’ve learned and struggled for a few years here figuring out how to
make a decent phone,” he said. “PC guys are not going to just figure this
out. They’re not going to just walk in.” What if Steve Jobs’
company did bring an iPod phone to market? Well, it would probably use WiFi
technology and could be distributed through the Apple stores and not the carriers
like Verizon or Cingular, Colligan theorized.</p>
</blockquote>
<p>I was reminded of this quote after Amazon <a href=
"http://phx.corporate-ir.net/phoenix.zhtml?c=176060&p=irol-newsArticle&ID=2281414">
announced an agreement to buy Whole Foods</a> for $13.7 billion; after all, it was only
<a href=
"https://www.bloomberg.com/news/articles/2015-01-29/in-shift-whole-foods-to-compete-with-price-cuts-loyalty-app">
two years ago</a> that Whole Foods founder and CEO John Mackey predicted that groceries
would be Amazon’s Waterloo. And while Colligan’s prediction was far worse
— Apple simply left Palm in the dust, unable to compete — it is Mackey who
has to call Amazon founder and CEO Jeff Bezos, the Napoleon of this little morality
play, boss.</p>
EDIT:
So I've made some progress. The original problem of isolating the inside of specific HTML elements for styling still persists, but I have gotten to the point where this is the only problem.
if let htmlText = content.htmlAttributedString {
let attributes = [NSFontAttributeName: UIFont.systemFont(ofSize: 19.0)]
let attributedText = NSMutableAttributedString(string: htmlText.string, attributes: attributes)
let htmlString = htmlText.string as NSString
let range = htmlString.range(of: title)
attributedText.addAttribute(NSFontAttributeName, value: UIFont.boldSystemFont(ofSize: 24.0), range: range)
bodyLabel.attributedText = attributedText
}
var htmlAttributedString: NSAttributedString? {
do {
let attrStr = try NSAttributedString(
data: (self.data(using: String.Encoding.unicode, allowLossyConversion: true)!),
options: [ NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
documentAttributes: nil)
return attrStr
} catch let error {
print(error)
}
return nil
}