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
}
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.
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!?
I've never worked in WatchOS5 and want to develop a horizontal complication (Modular large) for AppleWatch, like "Heart Rate". The idea is that I would display heart rate data in a different way. Right now I want to deploy the complication on development watch.
I have created a new project with a checkbox for "complication" added. I see that this added a complications controller with timeline configuration placeholders.
There is also an storyboard with a bunch of empty screens. I'm not sure as to how much effort I need to put into an apple watch app before I can deploy it. I see this Apple doc, but it does not describe how to layout my complication. Some section seem to have missing links.
Can I provide one style of complication only (large horizontal - modular large)
Do I need to provide any iPhone app content beyond managing the
complication logic, or can I get away without having a view controller?
Do I control the appearance of my complication by adding something to the assets folder (it has a bunch of graphic slots)?
Sorry for a complete beginner project, I have not seen a project focusing specifically on the horizontal complication for watch OS 5
You should be able to deploy it immediately, though it won't do anything. Have a look at the wwdc video explaining how to create a complication: video
You can't layout the complication yourself, you can chose from a set of templates that you fill with data. The screens you are seeing are for your watch app, not the complication.
You don't have to support all complication styles.
The complication logic is part of your WatchKit Extension, so technically you don't need anything in the iOS companion app, I'm not sure how much functionality you have to provide to get past the app review though.
Adding your graphics to the asset catalog won't do anything, you have to reference them when configuring the templates.
Here's an example by Apple of how to communicate with the apple watch app. You need to painstakingly read the readme about 25 times to get all the app group identifiers changed in that project.
Your main phone app assets are not visible to the watch app
Your watch storyboard assets go in WatchKit target
Your programmatically accessed assets go into the watch extension target
Original answers:
Can I provide one style of complication only (large horizontal -
modular large) - YES
Do I need to provide any iPhone app content beyond
managing the complication logic, or can I get away without having a
view controller? YES - watch apps have computation limits imposed on them
Do I control the appearance of my complication by
adding something to the assets folder (it has a bunch of graphic
slots)? See below - it's both assets folder and placeholders
Modify the example above to create a placeholder image displayed on the watch (when you are selecting a complication while modifying the screen layout)
func getPlaceholderTemplate(for complication: CLKComplication, withHandler handler: #escaping (CLKComplicationTemplate?) -> Void) {
// Pass the template to ClockKit.
if complication.family == .graphicRectangular {
// Display a random number string on the body.
let template = CLKComplicationTemplateGraphicRectangularLargeImage()
template.textProvider = CLKSimpleTextProvider(text: "---")
let image = UIImage(named: "imageFromWatchExtensionAssets") ?? UIImage()
template.imageProvider = CLKFullColorImageProvider(fullColorImage: image)
// Pass the entry to ClockKit.
handler(template)
}else {
handler(nil);
return
}
}
sending small packets to the watch (will not send images!)
func updateHeartRate(with sample: HKQuantitySample){
let context: [String: Any] = ["title": "String from phone"]
do {
try WCSession.default.updateApplicationContext(context)
} catch {
print("Failed to transmit app context")
}
}
Transferring images and files:
func uploadImage(_ image: UIImage, name: String, title: String = "") {
let data: Data? = UIImagePNGRepresentation(image)
do {
let fileManager = FileManager.default
let documentDirectory = try fileManager.url(for: .cachesDirectory,
in: .userDomainMask,
appropriateFor:nil,
create:true)
let fileURL = try FileManager.fileURL("\(name).png")
if fileManager.fileExists(atPath: fileURL.path) {
try fileManager.removeItem(at: fileURL)
try data?.write(to: fileURL, options: Data.WritingOptions.atomic)
} else {
try data?.write(to: fileURL, options: Data.WritingOptions.atomic)
}
if WCSession.default.activationState != .activated {
print("session not activated")
}
fileTransfer = WCSession.default.transferFile(fileURL, metadata: ["name":name, "title": title])
}
catch {
print(error)
}
print("Completed transfer \(name)")
}
I'm changing my app to make it localizable/internationalizable, by adding multiple languages, so I have some texts on the storyboard and others in the swift files, so I have to make them internationalizable, but I don't know how to keep a customized font and make it internationalizable.
I have created these:
So if I understand the first strings files are for UILabel which are written in the storyboard. The second is for text in swift files also UIlabel linked in.
But I have to customize, fonts, colours... for some texts, and I don't know how to do for the UILabel in the storyboard, but for swift files I did it:
var main_string = "VOTRE EXPERT DU\nREGROUPEMENT DE CRÉDITS\nEN FRANCE DEPUIS 1998."
var string_to_color = "1998"
var string_to_bold = "REGROUPEMENT DE CRÉDITS"
var range_color = (main_string as NSString).range(of: string_to_color)
var range_bold = (main_string as NSString).range(of: string_to_bold)
let attributedString = [ NSAttributedStringKey.font: UIFont(name: "gotham-book", size: 18.0)! ]
let myString = NSMutableAttributedString(string: "VOTRE EXPERT DU\nREGROUPEMENT DE CRÉDITS\nEN FRANCE DEPUIS 1998.", attributes: attributedString)
NSLocalizedString("Welcome", comment: "")
myString.addAttribute(NSAttributedStringKey.foregroundColor, value: UIColor.orange , range: range_color)
let font = UIFont(name: "gotham-bold", size: 18.0)
myString.addAttribute(NSAttributedStringKey.font, value: font, range: range_bold)
accueilTopLabel.attributedText = myString
But how can I manage different languages with? Because I use ranges so it looks difficult. What the best way to manage it?