Many applications have text and in this text are web hyperlinks in rounded rect. When I click them UIWebView opens. What puzzles me is that they often have custom links, for example if words starts with # it is also clickable and the application responds by opening another view. How can I do that? Is it possible with UILabel or do I need UITextView or something else?
In general, if we want to have a clickable link in text displayed by UILabel, we would need to resolve two independent tasks:
Changing the appearance of a portion of the text to look like a link
Detecting and handling touches on the link (opening an URL is a particular case)
The first one is easy. Starting from iOS 6 UILabel supports display of attributed strings. All you need to do is to create and configure an instance of NSMutableAttributedString:
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:#"String with a link" attributes:nil];
NSRange linkRange = NSMakeRange(14, 4); // for the word "link" in the string above
NSDictionary *linkAttributes = #{ NSForegroundColorAttributeName : [UIColor colorWithRed:0.05 green:0.4 blue:0.65 alpha:1.0],
NSUnderlineStyleAttributeName : #(NSUnderlineStyleSingle) };
[attributedString setAttributes:linkAttributes range:linkRange];
// Assign attributedText to UILabel
label.attributedText = attributedString;
That's it! The code above makes UILabel to display String with a link
Now we should detect touches on this link. The idea is to catch all taps within UILabel and figure out whether the location of the tap was close enough to the link. To catch touches we can add tap gesture recognizer to the label. Make sure to enable userInteraction for the label, it's turned off by default:
label.userInteractionEnabled = YES;
[label addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTapOnLabel:)]];
Now the most sophisticated stuff: finding out whether the tap was on where the link is displayed and not on any other portion of the label. If we had single-lined UILabel, this task could be solved relatively easy by hardcoding the area bounds where the link is displayed, but let's solve this problem more elegantly and for general case - multiline UILabel without preliminary knowledge about the link layout.
One of the approaches is to use capabilities of Text Kit API introduced in iOS 7:
// Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeZero];
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString];
// Configure layoutManager and textStorage
[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];
// Configure textContainer
textContainer.lineFragmentPadding = 0.0;
textContainer.lineBreakMode = label.lineBreakMode;
textContainer.maximumNumberOfLines = label.numberOfLines;
Save created and configured instances of NSLayoutManager, NSTextContainer and NSTextStorage in properties in your class (most likely UIViewController's descendant) - we'll need them in other methods.
Now, each time the label changes its frame, update textContainer's size:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.textContainer.size = self.label.bounds.size;
}
And finally, detect whether the tap was exactly on the link:
- (void)handleTapOnLabel:(UITapGestureRecognizer *)tapGesture
{
CGPoint locationOfTouchInLabel = [tapGesture locationInView:tapGesture.view];
CGSize labelSize = tapGesture.view.bounds.size;
CGRect textBoundingBox = [self.layoutManager usedRectForTextContainer:self.textContainer];
CGPoint textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
(labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
CGPoint locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
locationOfTouchInLabel.y - textContainerOffset.y);
NSInteger indexOfCharacter = [self.layoutManager characterIndexForPoint:locationOfTouchInTextContainer
inTextContainer:self.textContainer
fractionOfDistanceBetweenInsertionPoints:nil];
NSRange linkRange = NSMakeRange(14, 4); // it's better to save the range somewhere when it was originally used for marking link in attributed string
if (NSLocationInRange(indexOfCharacter, linkRange)) {
// Open an URL, or handle the tap on the link in any other way
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:#"https://stackoverflow.com/"]];
}
}
I am extending #NAlexN original detailed solution, with #zekel excellent extension of UITapGestureRecognizer, and providing in Swift.
Extending UITapGestureRecognizer
extension UITapGestureRecognizer {
func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
// Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize.zero)
let textStorage = NSTextStorage(attributedString: label.attributedText!)
// Configure layoutManager and textStorage
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
// Configure textContainer
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines
let labelSize = label.bounds.size
textContainer.size = labelSize
// Find the tapped character location and compare it to the specified range
let locationOfTouchInLabel = self.location(in: label)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
let textContainerOffset = CGPoint(
x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y
)
let locationOfTouchInTextContainer = CGPoint(
x: locationOfTouchInLabel.x - textContainerOffset.x,
y: locationOfTouchInLabel.y - textContainerOffset.y
)
let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
return NSLocationInRange(indexOfCharacter, targetRange)
}
}
Usage
Setup UIGestureRecognizer to send actions to tapLabel:, and you can detect if the target ranges is being tapped on in myLabel.
#IBAction func tapLabel(gesture: UITapGestureRecognizer) {
if gesture.didTapAttributedTextInLabel(myLabel, inRange: targetRange1) {
print("Tapped targetRange1")
} else if gesture.didTapAttributedTextInLabel(myLabel, inRange: targetRange2) {
print("Tapped targetRange2")
} else {
print("Tapped none")
}
}
IMPORTANT: The UILabel line break mode must be set to wrap by word/char. Somehow, NSTextContainer will assume that the text is single line only if the line break mode is otherwise.
Old question but if anyone can use a UITextView instead of a UILabel, then it is easy. Standard URLs, phone numbers etc will be automatically detected (and be clickable).
However, if you need custom detection, that is, if you want to be able to call any custom method after a user clicks on a particular word, you need to use NSAttributedStrings with an NSLinkAttributeName attribute that will point to a custom URL scheme(as opposed to having the http url scheme by default). Ray Wenderlich has it covered here
Quoting the code from the aforementioned link:
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:#"This is an example by #marcelofabri_"];
[attributedString addAttribute:NSLinkAttributeName
value:#"username://marcelofabri_"
range:[[attributedString string] rangeOfString:#"#marcelofabri_"]];
NSDictionary *linkAttributes = #{NSForegroundColorAttributeName: [UIColor greenColor],
NSUnderlineColorAttributeName: [UIColor lightGrayColor],
NSUnderlineStyleAttributeName: #(NSUnderlinePatternSolid)};
// assume that textView is a UITextView previously created (either by code or Interface Builder)
textView.linkTextAttributes = linkAttributes; // customizes the appearance of links
textView.attributedText = attributedString;
textView.delegate = self;
To detect those link clicks, implement this:
- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange {
if ([[URL scheme] isEqualToString:#"username"]) {
NSString *username = [URL host];
// do something with this username
// ...
return NO;
}
return YES; // let the system open this URL
}
PS: Make sure your UITextView is selectable.
(My answer builds on #NAlexN's excellent answer. I won't duplicate his detailed explanation of each step here.)
I found it most convenient and straightforward to add support for tap-able UILabel text as a category to UITapGestureRecognizer. (You don't have to use UITextView's data detectors, as some answers suggest.)
Add the following method to your UITapGestureRecognizer category:
/**
Returns YES if the tap gesture was within the specified range of the attributed text of the label.
*/
- (BOOL)didTapAttributedTextInLabel:(UILabel *)label inRange:(NSRange)targetRange {
NSParameterAssert(label != nil);
CGSize labelSize = label.bounds.size;
// create instances of NSLayoutManager, NSTextContainer and NSTextStorage
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeZero];
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:label.attributedText];
// configure layoutManager and textStorage
[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];
// configure textContainer for the label
textContainer.lineFragmentPadding = 0.0;
textContainer.lineBreakMode = label.lineBreakMode;
textContainer.maximumNumberOfLines = label.numberOfLines;
textContainer.size = labelSize;
// find the tapped character location and compare it to the specified range
CGPoint locationOfTouchInLabel = [self locationInView:label];
CGRect textBoundingBox = [layoutManager usedRectForTextContainer:textContainer];
CGPoint textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
(labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
CGPoint locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
locationOfTouchInLabel.y - textContainerOffset.y);
NSInteger indexOfCharacter = [layoutManager characterIndexForPoint:locationOfTouchInTextContainer
inTextContainer:textContainer
fractionOfDistanceBetweenInsertionPoints:nil];
if (NSLocationInRange(indexOfCharacter, targetRange)) {
return YES;
} else {
return NO;
}
}
Example Code
// (in your view controller)
// create your label, gesture recognizer, attributed text, and get the range of the "link" in your label
myLabel.userInteractionEnabled = YES;
[myLabel addGestureRecognizer:
[[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(handleTapOnLabel:)]];
// create your attributed text and keep an ivar of your "link" text range
NSAttributedString *plainText;
NSAttributedString *linkText;
plainText = [[NSMutableAttributedString alloc] initWithString:#"Add label links with UITapGestureRecognizer"
attributes:nil];
linkText = [[NSMutableAttributedString alloc] initWithString:#" Learn more..."
attributes:#{
NSForegroundColorAttributeName:[UIColor blueColor]
}];
NSMutableAttributedString *attrText = [[NSMutableAttributedString alloc] init];
[attrText appendAttributedString:plainText];
[attrText appendAttributedString:linkText];
// ivar -- keep track of the target range so you can compare in the callback
targetRange = NSMakeRange(plainText.length, linkText.length);
Gesture Callback
// handle the gesture recognizer callback and call the category method
- (void)handleTapOnLabel:(UITapGestureRecognizer *)tapGesture {
BOOL didTapLink = [tapGesture didTapAttributedTextInLabel:myLabel
inRange:targetRange];
NSLog(#"didTapLink: %d", didTapLink);
}
The UIButtonTypeCustom is a clickable label if you don't set any images for it.
Translating #samwize's Extension to Swift 4:
extension UITapGestureRecognizer {
func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
guard let attrString = label.attributedText else {
return false
}
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: .zero)
let textStorage = NSTextStorage(attributedString: attrString)
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
textContainer.lineFragmentPadding = 0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines
let labelSize = label.bounds.size
textContainer.size = labelSize
let locationOfTouchInLabel = self.location(in: label)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y)
let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, y: locationOfTouchInLabel.y - textContainerOffset.y)
let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
return NSLocationInRange(indexOfCharacter, targetRange)
}
}
To set up the recognizer (once you colored the text and stuff):
lblTermsOfUse.isUserInteractionEnabled = true
lblTermsOfUse.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTapOnLabel(_:))))
...then the gesture recognizer:
#objc func handleTapOnLabel(_ recognizer: UITapGestureRecognizer) {
guard let text = lblAgreeToTerms.attributedText?.string else {
return
}
if let range = text.range(of: NSLocalizedString("_onboarding_terms", comment: "terms")),
recognizer.didTapAttributedTextInLabel(label: lblAgreeToTerms, inRange: NSRange(range, in: text)) {
goToTermsAndConditions()
} else if let range = text.range(of: NSLocalizedString("_onboarding_privacy", comment: "privacy")),
recognizer.didTapAttributedTextInLabel(label: lblAgreeToTerms, inRange: NSRange(range, in: text)) {
goToPrivacyPolicy()
}
}
UITextView supports data-detectors in OS3.0, whereas UILabel doesn't.
If you enable the data-detectors on the UITextView and your text contains URLs, phone numbers, etc. they will appear as links.
Most simple and reliable approach is to use UITextView as Kedar Paranjape recommended. Based on answer of Karl Nosworthy I finally came up with a simple UITextView subclass:
class LinkTextView: UITextView, UITextViewDelegate {
typealias Links = [String: String]
typealias OnLinkTap = (URL) -> Bool
var onLinkTap: OnLinkTap?
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
isEditable = false
isSelectable = true
isScrollEnabled = false //to have own size and behave like a label
delegate = self
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
func addLinks(_ links: Links) {
guard attributedText.length > 0 else {
return
}
let mText = NSMutableAttributedString(attributedString: attributedText)
for (linkText, urlString) in links {
if linkText.count > 0 {
let linkRange = mText.mutableString.range(of: linkText)
mText.addAttribute(.link, value: urlString, range: linkRange)
}
}
attributedText = mText
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
return onLinkTap?(URL) ?? true
}
// to disable text selection
func textViewDidChangeSelection(_ textView: UITextView) {
textView.selectedTextRange = nil
}
}
Usage is very simple:
let linkTextView = LinkTextView()
let tu = "Terms of Use"
let pp = "Privacy Policy"
linkTextView.text = "Please read the Some Company \(tu) and \(pp)"
linkTextView.addLinks([
tu: "https://some.com/tu",
pp: "https://some.com/pp"
])
linkTextView.onLinkTap = { url in
print("url: \(url)")
return true
}
Note that isScrollEnabled is false by default, as in most cases we need small label-like view with own size and without scrolling. Just set it true if you want a scrollable text view.
Also note that UITextView unlike UILabel has default text padding. To remove it and make layout same as in UILabel just add: linkTextView.textContainerInset = .zero
Implementing onLinkTap closure is not necessary, without it URLs is automatically open by UIApplication.
As Text selection is undesirable in most cases, but it can't be turned off it is dismissed in delegate method (Thanks to Carson Vo)
Some answers didn't work for me as expected. This is Swift solution that supports also textAlignment and multiline. No subclassing needed, just this UITapGestureRecognizer extension:
import UIKit
extension UITapGestureRecognizer {
func didTapAttributedString(_ string: String, in label: UILabel) -> Bool {
guard let text = label.text else {
return false
}
let range = (text as NSString).range(of: string)
return self.didTapAttributedText(label: label, inRange: range)
}
private func didTapAttributedText(label: UILabel, inRange targetRange: NSRange) -> Bool {
guard let attributedText = label.attributedText else {
assertionFailure("attributedText must be set")
return false
}
let textContainer = createTextContainer(for: label)
let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(textContainer)
let textStorage = NSTextStorage(attributedString: attributedText)
if let font = label.font {
textStorage.addAttribute(NSAttributedString.Key.font, value: font, range: NSMakeRange(0, attributedText.length))
}
textStorage.addLayoutManager(layoutManager)
let locationOfTouchInLabel = location(in: label)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
let alignmentOffset = aligmentOffset(for: label)
let xOffset = ((label.bounds.size.width - textBoundingBox.size.width) * alignmentOffset) - textBoundingBox.origin.x
let yOffset = ((label.bounds.size.height - textBoundingBox.size.height) * alignmentOffset) - textBoundingBox.origin.y
let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - xOffset, y: locationOfTouchInLabel.y - yOffset)
let characterTapped = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
let lineTapped = Int(ceil(locationOfTouchInLabel.y / label.font.lineHeight)) - 1
let rightMostPointInLineTapped = CGPoint(x: label.bounds.size.width, y: label.font.lineHeight * CGFloat(lineTapped))
let charsInLineTapped = layoutManager.characterIndex(for: rightMostPointInLineTapped, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
return characterTapped < charsInLineTapped ? targetRange.contains(characterTapped) : false
}
private func createTextContainer(for label: UILabel) -> NSTextContainer {
let textContainer = NSTextContainer(size: label.bounds.size)
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines
return textContainer
}
private func aligmentOffset(for label: UILabel) -> CGFloat {
switch label.textAlignment {
case .left, .natural, .justified:
return 0.0
case .center:
return 0.5
case .right:
return 1.0
#unknown default:
return 0.0
}
}
}
Usage:
class ViewController: UIViewController {
#IBOutlet var label : UILabel!
let selectableString1 = "consectetur"
let selectableString2 = "cupidatat"
override func viewDidLoad() {
super.viewDidLoad()
let text = "Lorem ipsum dolor sit amet, \(selectableString1) adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat \(selectableString2) non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
label.attributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: text))
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(labelTapped))
label.addGestureRecognizer(tapGesture)
label.isUserInteractionEnabled = true
}
#objc func labelTapped(gesture: UITapGestureRecognizer) {
if gesture.didTapAttributedString(selectableString1, in: label) {
print("\(selectableString1) tapped")
} else if gesture.didTapAttributedString(selectableString2, in: label) {
print("\(selectableString2) tapped")
} else {
print("Text tapped")
}
}
}
As I mentioned in this post,
here is a light-weighted library I created specially for links in UILabel FRHyperLabel.
To achieve an effect like this:
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque quis blandit eros, sit amet vehicula justo. Nam at urna neque. Maecenas ac sem eu sem porta dictum nec vel tellus.
use code:
//Step 1: Define a normal attributed string for non-link texts
NSString *string = #"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque quis blandit eros, sit amet vehicula justo. Nam at urna neque. Maecenas ac sem eu sem porta dictum nec vel tellus.";
NSDictionary *attributes = #{NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]};
label.attributedText = [[NSAttributedString alloc]initWithString:string attributes:attributes];
//Step 2: Define a selection handler block
void(^handler)(FRHyperLabel *label, NSString *substring) = ^(FRHyperLabel *label, NSString *substring){
NSLog(#"Selected: %#", substring);
};
//Step 3: Add link substrings
[label setLinksForSubstrings:#[#"Lorem", #"Pellentesque", #"blandit", #"Maecenas"] withLinkHandler:handler];
Worked in Swift 3, pasting the entire code here
//****Make sure the textview 'Selectable' = checked, and 'Editable = Unchecked'
import UIKit
class ViewController: UIViewController, UITextViewDelegate {
#IBOutlet var theNewTextView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
//****textview = Selectable = checked, and Editable = Unchecked
theNewTextView.delegate = self
let theString = NSMutableAttributedString(string: "Agree to Terms")
let theRange = theString.mutableString.range(of: "Terms")
theString.addAttribute(NSLinkAttributeName, value: "ContactUs://", range: theRange)
let theAttribute = [NSForegroundColorAttributeName: UIColor.blue, NSUnderlineStyleAttributeName: NSUnderlineStyle.styleSingle.rawValue] as [String : Any]
theNewTextView.linkTextAttributes = theAttribute
theNewTextView.attributedText = theString
theString.setAttributes(theAttribute, range: theRange)
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
if (URL.scheme?.hasPrefix("ContactUs://"))! {
return false //interaction not allowed
}
//*** Set storyboard id same as VC name
self.navigationController!.pushViewController((self.storyboard?.instantiateViewController(withIdentifier: "TheLastViewController"))! as UIViewController, animated: true)
return true
}
}
Here is a swift version of NAlexN's answer.
class TapabbleLabel: UILabel {
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize.zero)
var textStorage = NSTextStorage() {
didSet {
textStorage.addLayoutManager(layoutManager)
}
}
var onCharacterTapped: ((label: UILabel, characterIndex: Int) -> Void)?
let tapGesture = UITapGestureRecognizer()
override var attributedText: NSAttributedString? {
didSet {
if let attributedText = attributedText {
textStorage = NSTextStorage(attributedString: attributedText)
} else {
textStorage = NSTextStorage()
}
}
}
override var lineBreakMode: NSLineBreakMode {
didSet {
textContainer.lineBreakMode = lineBreakMode
}
}
override var numberOfLines: Int {
didSet {
textContainer.maximumNumberOfLines = numberOfLines
}
}
/**
Creates a new view with the passed coder.
:param: aDecoder The a decoder
:returns: the created new view.
*/
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUp()
}
/**
Creates a new view with the passed frame.
:param: frame The frame
:returns: the created new view.
*/
override init(frame: CGRect) {
super.init(frame: frame)
setUp()
}
/**
Sets up the view.
*/
func setUp() {
userInteractionEnabled = true
layoutManager.addTextContainer(textContainer)
textContainer.lineFragmentPadding = 0
textContainer.lineBreakMode = lineBreakMode
textContainer.maximumNumberOfLines = numberOfLines
tapGesture.addTarget(self, action: #selector(TapabbleLabel.labelTapped(_:)))
addGestureRecognizer(tapGesture)
}
override func layoutSubviews() {
super.layoutSubviews()
textContainer.size = bounds.size
}
func labelTapped(gesture: UITapGestureRecognizer) {
guard gesture.state == .Ended else {
return
}
let locationOfTouch = gesture.locationInView(gesture.view)
let textBoundingBox = layoutManager.usedRectForTextContainer(textContainer)
let textContainerOffset = CGPoint(x: (bounds.width - textBoundingBox.width) / 2 - textBoundingBox.minX,
y: (bounds.height - textBoundingBox.height) / 2 - textBoundingBox.minY)
let locationOfTouchInTextContainer = CGPoint(x: locationOfTouch.x - textContainerOffset.x,
y: locationOfTouch.y - textContainerOffset.y)
let indexOfCharacter = layoutManager.characterIndexForPoint(locationOfTouchInTextContainer,
inTextContainer: textContainer,
fractionOfDistanceBetweenInsertionPoints: nil)
onCharacterTapped?(label: self, characterIndex: indexOfCharacter)
}
}
You can then create an instance of that class inside your viewDidLoad method like this:
let label = TapabbleLabel()
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[view]-|",
options: [], metrics: nil, views: ["view" : label]))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-[view]-|",
options: [], metrics: nil, views: ["view" : label]))
let attributedString = NSMutableAttributedString(string: "String with a link", attributes: nil)
let linkRange = NSMakeRange(14, 4); // for the word "link" in the string above
let linkAttributes: [String : AnyObject] = [
NSForegroundColorAttributeName : UIColor.blueColor(), NSUnderlineStyleAttributeName : NSUnderlineStyle.StyleSingle.rawValue,
NSLinkAttributeName: "http://www.apple.com"]
attributedString.setAttributes(linkAttributes, range:linkRange)
label.attributedText = attributedString
label.onCharacterTapped = { label, characterIndex in
if let attribute = label.attributedText?.attribute(NSLinkAttributeName, atIndex: characterIndex, effectiveRange: nil) as? String,
let url = NSURL(string: attribute) {
UIApplication.sharedApplication().openURL(url)
}
}
It's better to have a custom attribute to use when a character is tapped. Now, it's the NSLinkAttributeName, but could be anything and you can use that value to do other things other than opening a url, you can do any custom action.
I created UILabel subclass named ResponsiveLabel which is based on textkit API introduced in iOS 7. It uses the same approach suggested by NAlexN. It provides flexibility to specify a pattern to search in the text. One can specify styles to be applied to those patterns as well as action to be performed on tapping the patterns.
//Detects email in text
NSString *emailRegexString = #"[A-Z0-9._%+-]+#[A-Z0-9.-]+\\.[A-Z]{2,4}";
NSError *error;
NSRegularExpression *regex = [[NSRegularExpression alloc]initWithPattern:emailRegexString options:0 error:&error];
PatternDescriptor *descriptor = [[PatternDescriptor alloc]initWithRegex:regex withSearchType:PatternSearchTypeAll withPatternAttributes:#{NSForegroundColorAttributeName:[UIColor redColor]}];
[self.customLabel enablePatternDetection:descriptor];
If you want to make a string clickable, you can do this way. This code applies attributes to each occurrence of the string "text".
PatternTapResponder tapResponder = ^(NSString *string) {
NSLog(#"tapped = %#",string);
};
[self.customLabel enableStringDetection:#"text" withAttributes:#{NSForegroundColorAttributeName:[UIColor redColor],
RLTapResponderAttributeName: tapResponder}];
Here is example code to hyperlink UILabel:
Source:http://sickprogrammersarea.blogspot.in/2014/03/adding-links-to-uilabel.html
#import "ViewController.h"
#import "TTTAttributedLabel.h"
#interface ViewController ()
#end
#implementation ViewController
{
UITextField *loc;
TTTAttributedLabel *data;
}
- (void)viewDidLoad
{
[super viewDidLoad];
UILabel *lbl = [[UILabel alloc] initWithFrame:CGRectMake(5, 20, 80, 25) ];
[lbl setText:#"Text:"];
[lbl setFont:[UIFont fontWithName:#"Verdana" size:16]];
[lbl setTextColor:[UIColor grayColor]];
loc=[[UITextField alloc] initWithFrame:CGRectMake(4, 20, 300, 30)];
//loc.backgroundColor = [UIColor grayColor];
loc.borderStyle=UITextBorderStyleRoundedRect;
loc.clearButtonMode=UITextFieldViewModeWhileEditing;
//[loc setText:#"Enter Location"];
loc.clearsOnInsertion = YES;
loc.leftView=lbl;
loc.leftViewMode=UITextFieldViewModeAlways;
[loc setDelegate:self];
[self.view addSubview:loc];
[loc setRightViewMode:UITextFieldViewModeAlways];
CGRect frameimg = CGRectMake(110, 70, 70,30);
UIButton *srchButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
srchButton.frame=frameimg;
[srchButton setTitle:#"Go" forState:UIControlStateNormal];
[srchButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
srchButton.backgroundColor=[UIColor clearColor];
[srchButton addTarget:self action:#selector(go:) forControlEvents:UIControlEventTouchDown];
[self.view addSubview:srchButton];
data = [[TTTAttributedLabel alloc] initWithFrame:CGRectMake(5, 120,self.view.frame.size.width,200) ];
[data setFont:[UIFont fontWithName:#"Verdana" size:16]];
[data setTextColor:[UIColor blackColor]];
data.numberOfLines=0;
data.delegate = self;
data.enabledTextCheckingTypes=NSTextCheckingTypeLink|NSTextCheckingTypePhoneNumber;
[self.view addSubview:data];
}
- (void)attributedLabel:(TTTAttributedLabel *)label didSelectLinkWithURL:(NSURL *)url
{
NSString *val=[[NSString alloc]initWithFormat:#"%#",url];
if ([[url scheme] hasPrefix:#"mailto"]) {
NSLog(#" mail URL Selected : %#",url);
MFMailComposeViewController *comp=[[MFMailComposeViewController alloc]init];
[comp setMailComposeDelegate:self];
if([MFMailComposeViewController canSendMail])
{
NSString *recp=[[val substringToIndex:[val length]] substringFromIndex:7];
NSLog(#"Recept : %#",recp);
[comp setToRecipients:[NSArray arrayWithObjects:recp, nil]];
[comp setSubject:#"From my app"];
[comp setMessageBody:#"Hello bro" isHTML:NO];
[comp setModalTransitionStyle:UIModalTransitionStyleCrossDissolve];
[self presentViewController:comp animated:YES completion:nil];
}
}
else{
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:val]];
}
}
-(void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error{
if(error)
{
UIAlertView *alrt=[[UIAlertView alloc]initWithTitle:#"Erorr" message:#"Some error occureed" delegate:nil cancelButtonTitle:#"" otherButtonTitles:nil, nil];
[alrt show];
[self dismissViewControllerAnimated:YES completion:nil];
}
else{
[self dismissViewControllerAnimated:YES completion:nil];
}
}
- (void)attributedLabel:(TTTAttributedLabel *)label didSelectLinkWithPhoneNumber:(NSString *)phoneNumber
{
NSLog(#"Phone Number Selected : %#",phoneNumber);
UIDevice *device = [UIDevice currentDevice];
if ([[device model] isEqualToString:#"iPhone"] ) {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:[NSString stringWithFormat:#"tel:%#",phoneNumber]]];
} else {
UIAlertView *Notpermitted=[[UIAlertView alloc] initWithTitle:#"Alert" message:#"Your device doesn't support this feature." delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[Notpermitted show];
}
}
-(void)go:(id)sender
{
[data setText:loc.text];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(#"Reached");
[loc resignFirstResponder];
}
I had a hard time dealing with this... UILabel with links on it on attributed text... it is just a headache so I ended up using ZSWTappableLabel.
Here’s a Swift implementation that is about as minimal as possible that also includes touch feedback. Caveats:
You must set fonts in your NSAttributedStrings
You can only use NSAttributedStrings!
You must ensure your links cannot wrap (use non breaking spaces: "\u{a0}")
You cannot change the lineBreakMode or numberOfLines after setting the text
You create links by adding attributes with .link keys
.
public class LinkLabel: UILabel {
private var storage: NSTextStorage?
private let textContainer = NSTextContainer()
private let layoutManager = NSLayoutManager()
private var selectedBackgroundView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
textContainer.lineFragmentPadding = 0
layoutManager.addTextContainer(textContainer)
textContainer.layoutManager = layoutManager
isUserInteractionEnabled = true
selectedBackgroundView.isHidden = true
selectedBackgroundView.backgroundColor = UIColor(white: 0, alpha: 0.3333)
selectedBackgroundView.layer.cornerRadius = 4
addSubview(selectedBackgroundView)
}
public required convenience init(coder: NSCoder) {
self.init(frame: .zero)
}
public override func layoutSubviews() {
super.layoutSubviews()
textContainer.size = frame.size
}
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
setLink(for: touches)
}
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
setLink(for: touches)
}
private func setLink(for touches: Set<UITouch>) {
if let pt = touches.first?.location(in: self), let (characterRange, _) = link(at: pt) {
let glyphRange = layoutManager.glyphRange(forCharacterRange: characterRange, actualCharacterRange: nil)
selectedBackgroundView.frame = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer).insetBy(dx: -3, dy: -3)
selectedBackgroundView.isHidden = false
} else {
selectedBackgroundView.isHidden = true
}
}
public override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
selectedBackgroundView.isHidden = true
}
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
selectedBackgroundView.isHidden = true
if let pt = touches.first?.location(in: self), let (_, url) = link(at: pt) {
UIApplication.shared.open(url)
}
}
private func link(at point: CGPoint) -> (NSRange, URL)? {
let touchedGlyph = layoutManager.glyphIndex(for: point, in: textContainer)
let touchedChar = layoutManager.characterIndexForGlyph(at: touchedGlyph)
var range = NSRange()
let attrs = attributedText!.attributes(at: touchedChar, effectiveRange: &range)
if let urlstr = attrs[.link] as? String {
return (range, URL(string: urlstr)!)
} else {
return nil
}
}
public override var attributedText: NSAttributedString? {
didSet {
textContainer.maximumNumberOfLines = numberOfLines
textContainer.lineBreakMode = lineBreakMode
if let txt = attributedText {
storage = NSTextStorage(attributedString: txt)
storage!.addLayoutManager(layoutManager)
layoutManager.textStorage = storage
textContainer.size = frame.size
}
}
}
}
I follow this version,
Swift 4:
import Foundation
class AELinkedClickableUILabel: UILabel {
typealias YourCompletion = () -> Void
var linkedRange: NSRange!
var completion: YourCompletion?
#objc func linkClicked(sender: UITapGestureRecognizer){
if let completionBlock = completion {
let textView = UITextView(frame: self.frame)
textView.text = self.text
textView.attributedText = self.attributedText
let index = textView.layoutManager.characterIndex(for: sender.location(in: self),
in: textView.textContainer,
fractionOfDistanceBetweenInsertionPoints: nil)
if linkedRange.lowerBound <= index && linkedRange.upperBound >= index {
completionBlock()
}
}
}
/**
* This method will be used to set an attributed text specifying the linked text with a
* handler when the link is clicked
*/
public func setLinkedTextWithHandler(text:String, link: String, handler: #escaping ()->()) -> Bool {
let attributextText = NSMutableAttributedString(string: text)
let foundRange = attributextText.mutableString.range(of: link)
if foundRange.location != NSNotFound {
self.linkedRange = foundRange
self.completion = handler
attributextText.addAttribute(NSAttributedStringKey.link, value: text, range: foundRange)
self.isUserInteractionEnabled = true
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(linkClicked(sender:))))
return true
}
return false
}
}
Call Example:
button.setLinkedTextWithHandler(text: "This website (stackoverflow.com) is awesome", link: "stackoverflow.com")
{
// show popup or open to link
}
I found a other solution:
I find a way to detect the link in a html text that you find from the internet you transform it into nsattributeString with :
func htmlAttributedString(fontSize: CGFloat = 17.0) -> NSAttributedString? {
let fontName = UIFont.systemFont(ofSize: fontSize).fontName
let string = self.appending(String(format: "<style>body{font-family: '%#'; font-size:%fpx;}</style>", fontName, fontSize))
guard let data = string.data(using: String.Encoding.utf16, allowLossyConversion: false) else { return nil }
guard let html = try? NSMutableAttributedString (
data: data,
options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html],
documentAttributes: nil) else { return nil }
return html
}
My method allows you to detect the hyperlink without having to specify them.
first you create an extension of the tapgesturerecognizer :
extension UITapGestureRecognizer {
func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
guard let attrString = label.attributedText else {
return false
}
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: .zero)
let textStorage = NSTextStorage(attributedString: attrString)
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
textContainer.lineFragmentPadding = 0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines
let labelSize = label.bounds.size
textContainer.size = labelSize
let locationOfTouchInLabel = self.location(in: label)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y)
let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, y: locationOfTouchInLabel.y - textContainerOffset.y)
let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
return NSLocationInRange(indexOfCharacter, targetRange)
}
}
then in you view controller you created a list of url and ranges to store all the links and the range that the attribute text contain:
var listurl : [String] = []
var listURLRange : [NSRange] = []
to find the URL and the URLRange you can use :
fun findLinksAndRange(attributeString : NSAttributeString){
notification.enumerateAttribute(NSAttributedStringKey.link , in: NSMakeRange(0, notification.length), options: [.longestEffectiveRangeNotRequired]) { value, range, isStop in
if let value = value {
print("\(value) found at \(range.location)")
let stringValue = "\(value)"
listurl.append(stringValue)
listURLRange.append(range)
}
}
westlandNotifcationLabel.addGestureRecognizer(UITapGestureRecognizer(target : self, action: #selector(handleTapOnLabel(_:))))
}
then you implementing the handle tap :
#objc func handleTapOnLabel(_ recognizer: UITapGestureRecognizer) {
for index in 0..<listURLRange.count{
if recognizer.didTapAttributedTextInLabel(label: westlandNotifcationLabel, inRange: listURLRange[index]) {
goToWebsite(url : listurl[index])
}
}
}
func goToWebsite(url : String){
if let websiteUrl = URL(string: url){
if #available(iOS 10, *) {
UIApplication.shared.open(websiteUrl, options: [:],
completionHandler: {
(success) in
print("Open \(websiteUrl): \(success)")
})
} else {
let success = UIApplication.shared.openURL(websiteUrl)
print("Open \(websiteUrl): \(success)")
}
}
}
and here we go!
I hope this solution help you like it help me.
Like there is reported in earlier answer the UITextView is able to handle touches on links. This can easily be extended by making other parts of the text work as links. The AttributedTextView library is a UITextView subclass that makes it very easy to handle these. For more info see: https://github.com/evermeer/AttributedTextView
You can make any part of the text interact like this (where textView1 is a UITextView IBOutlet):
textView1.attributer =
"1. ".red
.append("This is the first test. ").green
.append("Click on ").black
.append("evict.nl").makeInteract { _ in
UIApplication.shared.open(URL(string: "http://evict.nl")!, options: [:], completionHandler: { completed in })
}.underline
.append(" for testing links. ").black
.append("Next test").underline.makeInteract { _ in
print("NEXT")
}
.all.font(UIFont(name: "SourceSansPro-Regular", size: 16))
.setLinkColor(UIColor.purple)
And for handling hashtags and mentions you can use code like this:
textView1.attributer = "#test: What #hashtags do we have in #evermeer #AtributedTextView library"
.matchHashtags.underline
.matchMentions
.makeInteract { link in
UIApplication.shared.open(URL(string: "https://twitter.com\(link.replacingOccurrences(of: "#", with: ""))")!, options: [:], completionHandler: { completed in })
}
I'd strongly recommend using a library that automatically detects URLs in text and converts them to links.
Try:
TTTAttributedLabel (pod)
ZSWTappableLabel (pod).
Both are under MIT license.
I'm extending #samwize's answer to handle multi-line UILabel and give an example on using for a UIButton
extension UITapGestureRecognizer {
func didTapAttributedTextInButton(button: UIButton, inRange targetRange: NSRange) -> Bool {
guard let label = button.titleLabel else { return false }
return didTapAttributedTextInLabel(label, inRange: targetRange)
}
func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
// Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize.zero)
let textStorage = NSTextStorage(attributedString: label.attributedText!)
// Configure layoutManager and textStorage
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
// Configure textContainer
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines
let labelSize = label.bounds.size
textContainer.size = labelSize
// Find the tapped character location and compare it to the specified range
let locationOfTouchInLabel = self.locationInView(label)
let textBoundingBox = layoutManager.usedRectForTextContainer(textContainer)
let textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
(labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
let locationOfTouchInTextContainer = CGPointMake((locationOfTouchInLabel.x - textContainerOffset.x),
0 );
// Adjust for multiple lines of text
let lineModifier = Int(ceil(locationOfTouchInLabel.y / label.font.lineHeight)) - 1
let rightMostFirstLinePoint = CGPointMake(labelSize.width, 0)
let charsPerLine = layoutManager.characterIndexForPoint(rightMostFirstLinePoint, inTextContainer: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
let indexOfCharacter = layoutManager.characterIndexForPoint(locationOfTouchInTextContainer, inTextContainer: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
let adjustedRange = indexOfCharacter + (lineModifier * charsPerLine)
return NSLocationInRange(adjustedRange, targetRange)
}
}
For fully custom links, you'll need to use a UIWebView - you can intercept the calls out, so that you can go to some other part of your app instead when a link is pressed.
based on Charles Gamble answer, this what I used (I removed some lines that confused me and gave me wrong indexed) :
- (BOOL)didTapAttributedTextInLabel:(UILabel *)label inRange:(NSRange)targetRange TapGesture:(UIGestureRecognizer*) gesture{
NSParameterAssert(label != nil);
// create instances of NSLayoutManager, NSTextContainer and NSTextStorage
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:label.attributedText];
// configure layoutManager and textStorage
[textStorage addLayoutManager:layoutManager];
// configure textContainer for the label
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(label.frame.size.width, label.frame.size.height)];
textContainer.lineFragmentPadding = 0.0;
textContainer.lineBreakMode = label.lineBreakMode;
textContainer.maximumNumberOfLines = label.numberOfLines;
// find the tapped character location and compare it to the specified range
CGPoint locationOfTouchInLabel = [gesture locationInView:label];
[layoutManager addTextContainer:textContainer]; //(move here, not sure it that matter that calling this line after textContainer is set
NSInteger indexOfCharacter = [layoutManager characterIndexForPoint:locationOfTouchInLabel
inTextContainer:textContainer
fractionOfDistanceBetweenInsertionPoints:nil];
if (NSLocationInRange(indexOfCharacter, targetRange)) {
return YES;
} else {
return NO;
}
}
Here's a drop-in Objective-C category that enables clickable links in existing UILabel.attributedText strings, exploiting the existing NSLinkAttributeName attribute.
#interface UILabel (GSBClickableLinks) <UIGestureRecognizerDelegate>
#property BOOL enableLinks;
#end
#import <objc/runtime.h>
static const void *INDEX;
static const void *TAP;
#implementation UILabel (GSBClickableLinks)
- (void)setEnableLinks:(BOOL)enableLinks
{
UITapGestureRecognizer *tap = objc_getAssociatedObject(self, &TAP); // retreive tap
if (enableLinks && !tap) { // add a gestureRegonzier to the UILabel to detect taps
tap = [UITapGestureRecognizer.alloc initWithTarget:self action:#selector(openLink)];
tap.delegate = self;
[self addGestureRecognizer:tap];
objc_setAssociatedObject(self, &TAP, tap, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // save tap
}
self.userInteractionEnabled = enableLinks; // note - when false UILAbel wont receive taps, hence disable links
}
- (BOOL)enableLinks
{
return (BOOL)objc_getAssociatedObject(self, &TAP); // ie tap != nil
}
// First check whether user tapped on a link within the attributedText of the label.
// If so, then the our label's gestureRecogizer will subsequently fire, and open the corresponding NSLinkAttributeName.
// If not, then the tap will get passed along, eg to the enclosing UITableViewCell...
// Note: save which character in the attributedText was clicked so that we dont have to redo everything again in openLink.
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if (gestureRecognizer != objc_getAssociatedObject(self, &TAP)) return YES; // dont block other gestures (eg swipe)
// Re-layout the attributedText to find out what was tapped
NSTextContainer *textContainer = [NSTextContainer.alloc initWithSize:self.frame.size];
textContainer.lineFragmentPadding = 0;
textContainer.maximumNumberOfLines = self.numberOfLines;
textContainer.lineBreakMode = self.lineBreakMode;
NSLayoutManager *layoutManager = NSLayoutManager.new;
[layoutManager addTextContainer:textContainer];
NSTextStorage *textStorage = [NSTextStorage.alloc initWithAttributedString:self.attributedText];
[textStorage addLayoutManager:layoutManager];
NSUInteger index = [layoutManager characterIndexForPoint:[gestureRecognizer locationInView:self]
inTextContainer:textContainer
fractionOfDistanceBetweenInsertionPoints:NULL];
objc_setAssociatedObject(self, &INDEX, #(index), OBJC_ASSOCIATION_RETAIN_NONATOMIC); // save index
return (BOOL)[self.attributedText attribute:NSLinkAttributeName atIndex:index effectiveRange:NULL]; // tapped on part of a link?
}
- (void)openLink
{
NSUInteger index = [objc_getAssociatedObject(self, &INDEX) unsignedIntegerValue]; // retrieve index
NSURL *url = [self.attributedText attribute:NSLinkAttributeName atIndex:index effectiveRange:NULL];
if (url && [UIApplication.sharedApplication canOpenURL:url]) [UIApplication.sharedApplication openURL:url];
}
#end
This would be a bit cleaner done via a UILabel subclass (ie none of the objc_getAssociatedObject mess), but if you are like me you prefer to avoid having to make unnecessary (3rd party) subclasses just to add some extra function to existing UIKit classes. Also, this has the beauty that it adds clickable-links to any existing UILabel, eg existing UITableViewCells!
I've tried to make it as minimally invasive as possible by using the existing NSLinkAttributeName attribute stuff already available in NSAttributedString. So its a simple as:
NSURL *myURL = [NSURL URLWithString:#"http://www.google.com"];
NSMutableAttributedString *myString = [NSMutableAttributedString.alloc initWithString:#"This string has a clickable link: "];
[myString appendAttributedString:[NSAttributedString.alloc initWithString:#"click here" attributes:#{NSLinkAttributeName:myURL}]];
...
myLabel.attributedText = myString;
myLabel.enableLinks = YES; // yes, that's all! :-)
Basically, it works by adding a UIGestureRecognizer to your UILabel. The hard work is done in gestureRecognizerShouldBegin:, which re-layouts the attributedText string to find out which character was tapped on. If this character was part of a NSLinkAttributeName then the gestureRecognizer will subsequently fire, retrieve the corresponding URL (from the NSLinkAttributeName value), and open the link per the usual [UIApplication.sharedApplication openURL:url] process.
Note - by doing all this in gestureRecognizerShouldBegin:, if you dont happen to tap on a link in the label, the event is passed along. So, for example, your UITableViewCell will capture taps on links, but otherwise behave normally (select cell, unselect, scroll, ...).
I've put this in a GitHub repository here.
Adapted from Kai Burghardt's SO posting here.
Yes this is possible albeit very confusing to figure out at first. I will go a step further and show you how you can even click on any area in the text as well.
With this method you can have UI Label tha is:
Multiline Friendly
Autoshrink Friendly
Clickable Friendly (yes, even individual characters)
Swift 5
Step 1:
Make the UILabel have the properties for Line Break of 'Truncate Tail' and set a minimum font scale.
If you are unfamiliar with font scale just remember this rule:
minimumFontSize/defaultFontSize = fontscale
In my case I wanted 7.2 to be the minimum font size and my starting font size was 36. Therefore, 7.2 / 36 = 0.2
Step 2:
If you do not care about the labels being clickable and just wanted a working multiline label you are done!
HOWEVER, if you want the labels to be clickable read on...
Add this following extension I created
extension UILabel {
func setOptimalFontSize(maxFontSize:CGFloat,text:String){
let width = self.bounds.size.width
var font_size:CGFloat = maxFontSize //Set the maximum font size.
var stringSize = NSString(string: text).size(withAttributes: [.font : self.font.withSize(font_size)])
while(stringSize.width > width){
font_size = font_size - 1
stringSize = NSString(string: text).size(withAttributes: [.font : self.font.withSize(font_size)])
}
self.font = self.font.withSize(font_size)//Forcefully change font to match what it would be graphically.
}
}
It's used like this (just replace <Label> with your actual label name):
<Label>.setOptimalFontSize(maxFontSize: 36.0, text: formula)
This extension is needed because auto shrink does NOT change the 'font' property of the label after it auto-shrinks so you have to deduce it by calculating it the same way it does by using .size(withAttributes) function which simulates what it's size would be with that particular font.
This is necessary because the solution for detecting where to click on the label requires the exact font size to be known.
Step 3:
Add the following extension:
extension UITapGestureRecognizer {
func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
// Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize.zero)
let mutableAttribString = NSMutableAttributedString(attributedString: label.attributedText!)
mutableAttribString.addAttributes([NSAttributedString.Key.font: label.font!], range: NSRange(location: 0, length: label.attributedText!.length))
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 6
paragraphStyle.lineBreakMode = .byTruncatingTail
paragraphStyle.alignment = .center
mutableAttribString.addAttributes([.paragraphStyle: paragraphStyle], range: NSMakeRange(0, mutableAttribString.string.count))
let textStorage = NSTextStorage(attributedString: mutableAttribString)
// Configure textContainer
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines
// Configure layoutManager and textStorage
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
let labelSize = label.bounds.size
textContainer.size = labelSize
// Find the tapped character location and compare it to the specified range
let locationOfTouchInLabel = self.location(in: label)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
//let textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
//(labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y)
//let locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
// locationOfTouchInLabel.y - textContainerOffset.y);
let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, y: locationOfTouchInLabel.y - textContainerOffset.y)
let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
print("IndexOfCharacter=",indexOfCharacter)
print("TargetRange=",targetRange)
return NSLocationInRange(indexOfCharacter, targetRange)
}
}
You will need to modify this extension for your particular multiline situation. In my case you will notice that I use a paragraph style.
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 6
paragraphStyle.lineBreakMode = .byTruncatingTail
paragraphStyle.alignment = .center
mutableAttribString.addAttributes([.paragraphStyle: paragraphStyle], range: NSMakeRange(0, mutableAttribString.string.count))
Make sure to change this in the extension to what you are actually using for your line spacing so that everything calculates correctly.
Step 4:
Add the gestureRecognizer to the label in viewDidLoad or where you think is appropriate like so (just replace <Label> with your label name again:
<Label>.addGestureRecognizer(UITapGestureRecognizer(target:self, action: #selector(tapLabel(gesture:))))
Here is a simplified example of my tapLabel function (just replace <Label> with your UILabel name):
#IBAction func tapLabel(gesture: UITapGestureRecognizer) {
guard let text = <Label>.attributedText?.string else {
return
}
let click_range = text.range(of: "(α/β)")
if gesture.didTapAttributedTextInLabel(label: <Label>, inRange: NSRange(click_range!, in: text)) {
print("Tapped a/b")
}else {
print("Tapped none")
}
}
Just a note in my example, my string is BED = N * d * [ RBE + ( d / (α/β) ) ], so I was just getting the range of the α/β in this case. You could add "\n" to the string to add a newline and whatever text you wanted after and test this to find a string on the next line and it will still find it and detect the click correctly!
That's it! You are done. Enjoy a multiline clickable label.
Create the class with the following .h and .m files. In the .m file there is the following function
- (void)linkAtPoint:(CGPoint)location
Inside this function we will check the ranges of substrings for which we need to give actions. Use your own logic to put your ranges.
And following is the usage of the subclass
TaggedLabel *label = [[TaggedLabel alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
[self.view addSubview:label];
label.numberOfLines = 0;
NSMutableAttributedString *attributtedString = [[NSMutableAttributedString alloc] initWithString : #"My name is #jjpp" attributes : #{ NSFontAttributeName : [UIFont systemFontOfSize:10],}];
//Do not forget to add the font attribute.. else it wont work.. it is very important
[attributtedString addAttribute:NSForegroundColorAttributeName
value:[UIColor redColor]
range:NSMakeRange(11, 5)];//you can give this range inside the .m function mentioned above
following is the .h file
#import <UIKit/UIKit.h>
#interface TaggedLabel : UILabel<NSLayoutManagerDelegate>
#property(nonatomic, strong)NSLayoutManager *layoutManager;
#property(nonatomic, strong)NSTextContainer *textContainer;
#property(nonatomic, strong)NSTextStorage *textStorage;
#property(nonatomic, strong)NSArray *tagsArray;
#property(readwrite, copy) tagTapped nameTagTapped;
#end
following is the .m file
#import "TaggedLabel.h"
#implementation TaggedLabel
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
self.userInteractionEnabled = YES;
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self)
{
self.userInteractionEnabled = YES;
}
return self;
}
- (void)setupTextSystem
{
_layoutManager = [[NSLayoutManager alloc] init];
_textContainer = [[NSTextContainer alloc] initWithSize:CGSizeZero];
_textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedText];
// Configure layoutManager and textStorage
[_layoutManager addTextContainer:_textContainer];
[_textStorage addLayoutManager:_layoutManager];
// Configure textContainer
_textContainer.lineFragmentPadding = 0.0;
_textContainer.lineBreakMode = NSLineBreakByWordWrapping;
_textContainer.maximumNumberOfLines = 0;
self.userInteractionEnabled = YES;
self.textContainer.size = self.bounds.size;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
if (!_layoutManager)
{
[self setupTextSystem];
}
// Get the info for the touched link if there is one
CGPoint touchLocation = [[touches anyObject] locationInView:self];
[self linkAtPoint:touchLocation];
}
- (void)linkAtPoint:(CGPoint)location
{
// Do nothing if we have no text
if (_textStorage.string.length == 0)
{
return;
}
// Work out the offset of the text in the view
CGPoint textOffset = [self calcGlyphsPositionInView];
// Get the touch location and use text offset to convert to text cotainer coords
location.x -= textOffset.x;
location.y -= textOffset.y;
NSUInteger touchedChar = [_layoutManager glyphIndexForPoint:location inTextContainer:_textContainer];
// If the touch is in white space after the last glyph on the line we don't
// count it as a hit on the text
NSRange lineRange;
CGRect lineRect = [_layoutManager lineFragmentUsedRectForGlyphAtIndex:touchedChar effectiveRange:&lineRange];
if (CGRectContainsPoint(lineRect, location) == NO)
{
return;
}
// Find the word that was touched and call the detection block
NSRange range = NSMakeRange(11, 5);//for this example i'm hardcoding the range here. In a real scenario it should be iterated through an array for checking all the ranges
if ((touchedChar >= range.location) && touchedChar < (range.location + range.length))
{
NSLog(#"range-->>%#",self.tagsArray[i][#"range"]);
}
}
- (CGPoint)calcGlyphsPositionInView
{
CGPoint textOffset = CGPointZero;
CGRect textBounds = [_layoutManager usedRectForTextContainer:_textContainer];
textBounds.size.width = ceil(textBounds.size.width);
textBounds.size.height = ceil(textBounds.size.height);
if (textBounds.size.height < self.bounds.size.height)
{
CGFloat paddingHeight = (self.bounds.size.height - textBounds.size.height) / 2.0;
textOffset.y = paddingHeight;
}
if (textBounds.size.width < self.bounds.size.width)
{
CGFloat paddingHeight = (self.bounds.size.width - textBounds.size.width) / 2.0;
textOffset.x = paddingHeight;
}
return textOffset;
}
#end
Drop-in solution as a category on UILabel (this assumes your UILabel uses an attributed string with some NSLinkAttributeName attributes in it):
#implementation UILabel (Support)
- (BOOL)openTappedLinkAtLocation:(CGPoint)location {
CGSize labelSize = self.bounds.size;
NSTextContainer* textContainer = [[NSTextContainer alloc] initWithSize:CGSizeZero];
textContainer.lineFragmentPadding = 0.0;
textContainer.lineBreakMode = self.lineBreakMode;
textContainer.maximumNumberOfLines = self.numberOfLines;
textContainer.size = labelSize;
NSLayoutManager* layoutManager = [[NSLayoutManager alloc] init];
[layoutManager addTextContainer:textContainer];
NSTextStorage* textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedText];
[textStorage addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, textStorage.length)];
[textStorage addLayoutManager:layoutManager];
CGRect textBoundingBox = [layoutManager usedRectForTextContainer:textContainer];
CGPoint textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
(labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
CGPoint locationOfTouchInTextContainer = CGPointMake(location.x - textContainerOffset.x, location.y - textContainerOffset.y);
NSInteger indexOfCharacter = [layoutManager characterIndexForPoint:locationOfTouchInTextContainer inTextContainer:textContainer fractionOfDistanceBetweenInsertionPoints:nullptr];
if (indexOfCharacter >= 0) {
NSURL* url = [textStorage attribute:NSLinkAttributeName atIndex:indexOfCharacter effectiveRange:nullptr];
if (url) {
[[UIApplication sharedApplication] openURL:url];
return YES;
}
}
return NO;
}
#end
This generic method works too !
func didTapAttributedTextInLabel(gesture: UITapGestureRecognizer, inRange targetRange: NSRange) -> Bool {
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize.zero)
guard let strAttributedText = self.attributedText else {
return false
}
let textStorage = NSTextStorage(attributedString: strAttributedText)
// Configure layoutManager and textStorage
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
// Configure textContainer
textContainer.lineFragmentPadding = Constants.lineFragmentPadding
textContainer.lineBreakMode = self.lineBreakMode
textContainer.maximumNumberOfLines = self.numberOfLines
let labelSize = self.bounds.size
textContainer.size = CGSize(width: labelSize.width, height: CGFloat.greatestFiniteMagnitude)
// Find the tapped character location and compare it to the specified range
let locationOfTouchInLabel = gesture.location(in: self)
let xCordLocationOfTouchInTextContainer = locationOfTouchInLabel.x
let yCordLocationOfTouchInTextContainer = locationOfTouchInLabel.y
let locOfTouch = CGPoint(x: xCordLocationOfTouchInTextContainer ,
y: yCordLocationOfTouchInTextContainer)
let indexOfCharacter = layoutManager.characterIndex(for: locOfTouch, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
guard let strLabel = text else {
return false
}
let charCountOfLabel = strLabel.count
if indexOfCharacter < (charCountOfLabel - 1) {
return NSLocationInRange(indexOfCharacter, targetRange)
} else {
return false
}
}
And you can call the method with
let text = yourLabel.text
let termsRange = (text as NSString).range(of: fullString)
if yourLabel.didTapAttributedTextInLabel(gesture: UITapGestureRecognizer, inRange: termsRange) {
showCorrespondingViewController()
}
Here is my answer based on #Luca Davanzo's answer, override the touchesBegan event instead of a tap gesture:
import UIKit
public protocol TapableLabelDelegate: NSObjectProtocol {
func tapableLabel(_ label: TapableLabel, didTapUrl url: String, atRange range: NSRange)
}
public class TapableLabel: UILabel {
private var links: [String: NSRange] = [:]
private(set) var layoutManager = NSLayoutManager()
private(set) var textContainer = NSTextContainer(size: CGSize.zero)
private(set) var textStorage = NSTextStorage() {
didSet {
textStorage.addLayoutManager(layoutManager)
}
}
public weak var delegate: TapableLabelDelegate?
public override var attributedText: NSAttributedString? {
didSet {
if let attributedText = attributedText {
textStorage = NSTextStorage(attributedString: attributedText)
} else {
textStorage = NSTextStorage()
links = [:]
}
}
}
public override var lineBreakMode: NSLineBreakMode {
didSet {
textContainer.lineBreakMode = lineBreakMode
}
}
public override var numberOfLines: Int {
didSet {
textContainer.maximumNumberOfLines = numberOfLines
}
}
public override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
public override func layoutSubviews() {
super.layoutSubviews()
textContainer.size = bounds.size
}
/// addLinks
///
/// - Parameters:
/// - text: text of link
/// - url: link url string
public func addLink(_ text: String, withURL url: String) {
guard let theText = attributedText?.string as? NSString else {
return
}
let range = theText.range(of: text)
guard range.location != NSNotFound else {
return
}
links[url] = range
}
private func setup() {
isUserInteractionEnabled = true
layoutManager.addTextContainer(textContainer)
textContainer.lineFragmentPadding = 0
textContainer.lineBreakMode = lineBreakMode
textContainer.maximumNumberOfLines = numberOfLines
}
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let locationOfTouch = touches.first?.location(in: self) else {
return
}
textContainer.size = bounds.size
let indexOfCharacter = layoutManager.glyphIndex(for: locationOfTouch, in: textContainer)
for (urlString, range) in links {
if NSLocationInRange(indexOfCharacter, range), let url = URL(string: urlString) {
delegate?.tapableLabel(self, didTapUrl: urlString, atRange: range)
}
}
}}
Modified #timbroder code to handle multiple line correctly for swift4.2
extension UITapGestureRecognizer {
func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
// Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize.zero)
let textStorage = NSTextStorage(attributedString: label.attributedText!)
// Configure layoutManager and textStorage
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
// Configure textContainer
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines
let labelSize = label.bounds.size
textContainer.size = labelSize
// Find the tapped character location and compare it to the specified range
let locationOfTouchInLabel = self.location(in: label)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
let locationOfTouchInTextContainer = CGPoint(x: (locationOfTouchInLabel.x - textContainerOffset.x),
y: 0 );
// Adjust for multiple lines of text
let lineModifier = Int(ceil(locationOfTouchInLabel.y / label.font.lineHeight)) - 1
let rightMostFirstLinePoint = CGPoint(x: labelSize.width, y: 0)
let charsPerLine = layoutManager.characterIndex(for: rightMostFirstLinePoint, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
let adjustedRange = indexOfCharacter + (lineModifier * charsPerLine)
var newTargetRange = targetRange
if lineModifier > 0 {
newTargetRange.location = targetRange.location+(lineModifier*Int(ceil(locationOfTouchInLabel.y)))
}
return NSLocationInRange(adjustedRange, newTargetRange)
}
}
UILabel Code
let tapAction = UITapGestureRecognizer(target: self, action: #selector(self.tapLabel(gesture:)))
let quote = "For full details please see our privacy policy and cookie policy."
let attributedString = NSMutableAttributedString(string: quote)
let string1: String = "privacy policy", string2: String = "cookie policy"
// privacy policy
let rangeString1 = quote.range(of: string1)!
let indexString1: Int = quote.distance(from: quote.startIndex, to: rangeString1.lowerBound)
attributedString.addAttributes(
[.font: <UIfont>,
.foregroundColor: <UI Color>,
.underlineStyle: 0, .underlineColor:UIColor.clear
], range: NSRange(location: indexString1, length: string1.count));
// cookie policy
let rangeString2 = quote.range(of: string2)!
let indexString2: Int = quote.distance(from: quote.startIndex, to: rangeString2.lowerBound )
attributedString.addAttributes(
[.font: <UIfont>,
.foregroundColor: <UI Color>,
.underlineStyle: 0, .underlineColor:UIColor.clear
], range: NSRange(location: indexString2, length: string2.count));
let label = UILabel()
label.frame = CGRect(x: 20, y: 200, width: 375, height: 100)
label.isUserInteractionEnabled = true
label.addGestureRecognizer(tapAction)
label.attributedText = attributedString
Code to recognise the Tap
#objc
func tapLabel(gesture: UITapGestureRecognizer) {
if gesture.didTapAttributedTextInLabel(label: <UILabel>, inRange: termsLabelRange {
print("Terms of service")
} else if gesture.didTapAttributedTextInLabel(label:<UILabel> inRange: privacyPolicyLabelRange) {
print("Privacy policy")
} else {
print("Tapped none")
}
}
Related
I don't want my background image to be too blury. Isn't there a property to adjust the blur intensity?
let blurEffect = UIBlurEffect(style: UIBlurEffectStyle.Light)
blurEffect.???
let effectView = UIVisualEffectView(effect: blurEffect)
effectView.frame = backgroundAlbumCover.bounds
backgroundAlbumCover.addSubview(effectView)
You can do that in super elegant way with animator
(reducing UIVisualEffectView alpha will not affect blur intensity, so we must use animator)
Usage as simple as:
let blurEffectView = BlurEffectView()
view.addSubview(blurEffectView)
BlurEffectView realisation:
class BlurEffectView: UIVisualEffectView {
var animator = UIViewPropertyAnimator(duration: 1, curve: .linear)
override func didMoveToSuperview() {
guard let superview = superview else { return }
backgroundColor = .clear
frame = superview.bounds //Or setup constraints instead
setupBlur()
}
private func setupBlur() {
animator.stopAnimation(true)
effect = nil
animator.addAnimations { [weak self] in
self?.effect = UIBlurEffect(style: .dark)
}
animator.fractionComplete = 0.1 //This is your blur intensity in range 0 - 1
}
deinit {
animator.stopAnimation(true)
}
}
Adjusting the blur itself is not possible... But, you can adjust how visible the blur view is. This can be done in a number of ways, only three of which I can think of at the moment:
1st Option: Adjust the alpha of your UIVisualEffectView instance e.g:
effectView.alpha = 0.4f;
2nd Option: Add a UIView instance to effectView at Index 0 and adjust the alpha of this UIView instance. e.g:
UIView *blurDilutionView = [[UIView alloc] initWithFrame: effectView.frame];
blurDilutionView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent: 0.5];
blurDilutionView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin|UIViewAutoresizingFlexibleBottomMargin|UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;//if not using AutoLayout
[effectView insertSubview:blurDilutionView atIndex:0];
3rd Option: use multiple UIVisualEffectView instances (I have not tried this yet, more of an idea). Apply an alpha of 0.1f on each. The more UIVisualEffectView views you have the more blurry the overall look. Once again, I have not tried this option yet!
Update:
As Axeva mentioned in the comments, Apple advises against adjusting the alpha to change the blur. So use these suggestions at your own potential peril.
Once I ran into a problem to create a blur effect that is darker than .light and lighter than .dark UIBlurEffect style.
To achieve that, put a view on the back with the color and alpha you need:
let pictureImageView = // Image that you need to blur
let backView = UIView(frame: pictureImageView.bounds)
backView.backgroundColor = UIColor(red: 100/255, green: 100/255, blue: 100/255, alpha: 0.3)
pictureImageView.addSubview(backView)
let blurEffect = UIBlurEffect(style: .light)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.frame = pictureImageView.bounds
pictureImageView.addSubview(blurEffectView)
How the result looks like:
For more details, check out this article.
UPDATE: apparently there is another nice (maybe even nicer) way to implement the Blur using CIFilter(name: "CIGaussianBlur").
It allows the make “opacity” and blur’s strengths much lower than UIBlurEffect.
Use Private API if you want. Tested on iOS 13.7, 14.8, 15.5, 16.0. Does not work with Mac Catalyst.
Sample
UIVisualEffectView+Intensity.h
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
#interface UIVisualEffectView (Intensity)
#property (nonatomic) CGFloat intensity;
#end
NS_ASSUME_NONNULL_END
UIVisualEffectView+Intensity.m
#import "UIVisualEffectView+Intensity.h"
#import <objc/message.h>
#interface UIVisualEffectView (Intensity)
#property (readonly) id backgroundHost; // _UIVisualEffectHost
#property (readonly) __kindof UIView *backdropView; // _UIVisualEffectBackdropView
#end
#implementation UIVisualEffectView (Intensity)
- (id)backgroundHost {
id backgroundHost = ((id (*)(id, SEL))objc_msgSend)(self, NSSelectorFromString(#"_backgroundHost")); // _UIVisualEffectHost
return backgroundHost;
}
- (__kindof UIView * _Nullable)backdropView {
__kindof UIView *backdropView = ((__kindof UIView * (*)(id, SEL))objc_msgSend)(self.backgroundHost, NSSelectorFromString(#"contentView")); // _UIVisualEffectBackdropView
return backdropView;
}
- (CGFloat)intensity {
__kindof UIView *backdropView = self.backdropView; // _UIVisualEffectBackdropView
__kindof CALayer *backdropLayer = ((__kindof CALayer * (*)(id, SEL))objc_msgSend)(backdropView, NSSelectorFromString(#"backdropLayer")); // UICABackdropLayer
NSArray *filters = backdropLayer.filters;
id _Nullable __block gaussianBlur = nil; // CAFilter
[filters enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (![obj respondsToSelector:NSSelectorFromString(#"type")]) return;
NSString *type = ((NSString * (*)(id, SEL))objc_msgSend)(obj, NSSelectorFromString(#"type"));
if (![type isKindOfClass:[NSString class]]) return;
if ([type isEqualToString:#"gaussianBlur"]) {
gaussianBlur = obj;
*stop = YES;
}
}];
if (gaussianBlur == nil) return 0.0f;
NSNumber * _Nullable inputRadius = [gaussianBlur valueForKeyPath:#"inputRadius"];
if ((inputRadius == nil) || (![inputRadius isKindOfClass:[NSNumber class]])) return 0.0f;
return [inputRadius floatValue];
}
- (void)setIntensity:(CGFloat)intensity {
id descriptor = ((id (*)(id, SEL, id, BOOL))objc_msgSend)(self, NSSelectorFromString(#"_effectDescriptorForEffects:usage:"), #[self.effect], YES); // _UIVisualEffectDescriptor
NSArray *filterEntries = ((NSArray * (*)(id, SEL))objc_msgSend)(descriptor, NSSelectorFromString(#"filterEntries")); // NSArray<_UIVisualEffectFilterEntry *>
id _Nullable __block gaussianBlur = nil; // _UIVisualEffectFilterEntry
[filterEntries enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSString *filterType = ((NSString * (*)(id, SEL))objc_msgSend)(obj, NSSelectorFromString(#"filterType"));
if ([filterType isEqualToString:#"gaussianBlur"]) {
gaussianBlur = obj;
*stop = YES;
}
}];
if (gaussianBlur == nil) return;
NSMutableDictionary *requestedValues = [((NSDictionary * (*)(id, SEL))objc_msgSend)(gaussianBlur, NSSelectorFromString(#"requestedValues")) mutableCopy];
if (![requestedValues.allKeys containsObject:#"inputRadius"]) {
NSLog(#"Not supported effect.");
return;
}
requestedValues[#"inputRadius"] = [NSNumber numberWithFloat:intensity];
((void (*)(id, SEL, NSDictionary *))objc_msgSend)(gaussianBlur, NSSelectorFromString(#"setRequestedValues:"), requestedValues);
((void (*)(id, SEL, id))objc_msgSend)(self.backgroundHost, NSSelectorFromString(#"setCurrentEffectDescriptor:"), descriptor);
((void (*)(id, SEL))objc_msgSend)(self.backdropView, NSSelectorFromString(#"applyRequestedFilterEffects"));
}
#end
Usage
let firstBlurView: UIVisualEffectView = .init(effect: UIBlurEffect(style: .dark))
// setter
firstBlurView.intensity = 7
// getter
print(firstBlurView.intensity) // 7.0
UIBlurEffect doesn't provide such a property. If you want another intensity, you will have to make a BlurEffect by yourself.
Here is the BlurEffectView class with public intensity setter as well as with conformance to Apple's UIView.animation functions (you can animate intensity by UIKit's animations)
BlurEffectView.swift
import UIKit
public class BlurEffectView: UIView {
public override class var layerClass: AnyClass {
return BlurIntensityLayer.self
}
#objc
#IBInspectable
public dynamic var intensity: CGFloat {
set { self.blurIntensityLayer.intensity = newValue }
get { return self.blurIntensityLayer.intensity }
}
#IBInspectable
public var effect = UIBlurEffect(style: .dark) {
didSet {
self.setupPropertyAnimator()
}
}
private let visualEffectView = UIVisualEffectView(effect: nil)
private var propertyAnimator: UIViewPropertyAnimator!
private var blurIntensityLayer: BlurIntensityLayer {
return self.layer as! BlurIntensityLayer
}
public override init(frame: CGRect) {
super.init(frame: frame)
self.setupView()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
self.setupView()
}
deinit {
self.propertyAnimator.stopAnimation(true)
}
private func setupPropertyAnimator() {
self.propertyAnimator?.stopAnimation(true)
self.propertyAnimator = UIViewPropertyAnimator(duration: 1, curve: .linear)
self.propertyAnimator.addAnimations { [weak self] in
self?.visualEffectView.effect = self?.effect
}
self.propertyAnimator.pausesOnCompletion = true
}
private func setupView() {
self.backgroundColor = .clear
self.isUserInteractionEnabled = false
self.addSubview(self.visualEffectView)
self.visualEffectView.fill(view: self)
self.setupPropertyAnimator()
}
public override func display(_ layer: CALayer) {
guard let presentationLayer = layer.presentation() as? BlurIntensityLayer else {
return
}
let clampedIntensity = max(0.0, min(1.0, presentationLayer.intensity))
self.propertyAnimator.fractionComplete = clampedIntensity
}
}
BlurIntensityLayer.swift
import QuartzCore
class BlurIntensityLayer: CALayer {
#NSManaged var intensity: CGFloat
override init(layer: Any) {
super.init(layer: layer)
if let layer = layer as? BlurIntensityLayer {
self.intensity = layer.intensity
}
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override class func needsDisplay(forKey key: String) -> Bool {
key == #keyPath(intensity) ? true : super.needsDisplay(forKey: key)
}
override func action(forKey event: String) -> CAAction? {
guard event == #keyPath(intensity) else {
return super.action(forKey: event)
}
let animation = CABasicAnimation(keyPath: event)
animation.toValue = nil
animation.fromValue = (self.presentation() ?? self).intensity
return animation
}
}
I want the header to mask the cells, but not the background.
I have a UITableView with transparent headers and cells similar to Apple's Notification Center (when you swipe down on the status bar on your iPhone). I can't figure out how to mask the cells so they don't show up underneath the header when it scrolls.
I've tried changing the contentInsets of the tableview, and I've tried changing the frame of the header View to a negative origin.
Try to make a subclass of UITableviewCell and add these methods
- (void)maskCellFromTop:(CGFloat)margin {
self.layer.mask = [self visibilityMaskWithLocation:margin/self.frame.size.height];
self.layer.masksToBounds = YES;
}
- (CAGradientLayer *)visibilityMaskWithLocation:(CGFloat)location {
CAGradientLayer *mask = [CAGradientLayer layer];
mask.frame = self.bounds;
mask.colors = [NSArray arrayWithObjects:(id)[[UIColor colorWithWhite:1 alpha:0] CGColor], (id)[[UIColor colorWithWhite:1 alpha:1] CGColor], nil];
mask.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:location], [NSNumber numberWithFloat:location], nil];
return mask;
}
and add this delegate method in UITableView
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
for (iNotifyTableViewCell *cell in self.visibleCells) {
CGFloat hiddenFrameHeight = scrollView.contentOffset.y + [iNotifyHeaderView height] - cell.frame.origin.y;
if (hiddenFrameHeight >= 0 || hiddenFrameHeight <= cell.frame.size.height) {
[cell maskCellFromTop:hiddenFrameHeight];
}
}
}
*Note that [iNotifyHeaderView height] is the height of the HeaderView. and use #import <QuartzCore/QuartzCore.h> for the custom cell.
A little edit on Alex Markman's answer, where you could skip creating a subclass for an UITableViewCell. Benefit of this approach is that you can use it for multiple different UITableViewCells.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
for (UITableViewCell *cell in self.tableView.visibleCells) {
CGFloat hiddenFrameHeight = scrollView.contentOffset.y + self.navigationController.navigationBar.frame.size.height - cell.frame.origin.y;
if (hiddenFrameHeight >= 0 || hiddenFrameHeight <= cell.frame.size.height) {
[self maskCell:cell fromTopWithMargin:hiddenFrameHeight];
}
}
}
- (void)maskCell:(UITableViewCell *)cell fromTopWithMargin:(CGFloat)margin
{
cell.layer.mask = [self visibilityMaskForCell:cell withLocation:margin/cell.frame.size.height];
cell.layer.masksToBounds = YES;
}
- (CAGradientLayer *)visibilityMaskForCell:(UITableViewCell *)cell withLocation:(CGFloat)location
{
CAGradientLayer *mask = [CAGradientLayer layer];
mask.frame = cell.bounds;
mask.colors = [NSArray arrayWithObjects:(id)[[UIColor colorWithWhite:1 alpha:0] CGColor], (id)[[UIColor colorWithWhite:1 alpha:1] CGColor], nil];
mask.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:location], [NSNumber numberWithFloat:location], nil];
return mask;
}
#Alex Markman - your answer is great and was very usefull for me, but I found that when you're scrolling on retina devices, the cells do not scroll smoothly. I found that it is caused by layer's locations parameter during the rendering process:
mask.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:location];
I slightly modified your code. Maybe someone will find it useful:
- (void)maskCellFromTop:(CGFloat)margin
{
self.layer.mask = [self visibilityMaskFromLocation:margin];
self.layer.masksToBounds = YES;
}
- (CAGradientLayer *)visibilityMaskFromLocation:(CGFloat)location
{
CAGradientLayer *mask = [CAGradientLayer layer];
mask.frame = CGRectMake(
self.bounds.origin.x,
location+self.bounds.origin.y,
self.bounds.size.width,
self.bounds.size.height-location);
mask.colors = #[
(id)[[UIColor colorWithWhite:1 alpha:1] CGColor],
(id)[[UIColor colorWithWhite:1 alpha:1] CGColor]
];
return mask;
}
Swift version
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for cell in tableView.visibleCells {
let hiddenFrameHeight = scrollView.contentOffset.y + navigationController!.navigationBar.frame.size.height - cell.frame.origin.y
if (hiddenFrameHeight >= 0 || hiddenFrameHeight <= cell.frame.size.height) {
maskCell(cell: cell, margin: Float(hiddenFrameHeight))
}
}
}
func maskCell(cell: UITableViewCell, margin: Float) {
cell.layer.mask = visibilityMaskForCell(cell: cell, location: (margin / Float(cell.frame.size.height) ))
cell.layer.masksToBounds = true
}
func visibilityMaskForCell(cell: UITableViewCell, location: Float) -> CAGradientLayer {
let mask = CAGradientLayer()
mask.frame = cell.bounds
mask.colors = [UIColor(white: 1, alpha: 0).cgColor, UIColor(white: 1, alpha: 1).cgColor]
mask.locations = [NSNumber(value: location), NSNumber(value: location)]
return mask;
}
Clean Swift 3 Version:
extension YourViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for cell in tableView.visibleCells {
let hiddenFrameHeight = scrollView.contentOffset.y + navigationController!.navigationBar.frame.size.height - cell.frame.origin.y
if (hiddenFrameHeight >= 0 || hiddenFrameHeight <= cell.frame.size.height) {
if let customCell = cell as? CustomCell {
customCell.maskCell(fromTop: hiddenFrameHeight)
}
}
}
}
}
class CustomCell: UITableViewCell {
public func maskCell(fromTop margin: CGFloat) {
layer.mask = visibilityMask(withLocation: margin / frame.size.height)
layer.masksToBounds = true
}
private func visibilityMask(withLocation location: CGFloat) -> CAGradientLayer {
let mask = CAGradientLayer()
mask.frame = bounds
mask.colors = [UIColor.white.withAlphaComponent(0).cgColor, UIColor.white.cgColor]
let num = location as NSNumber
mask.locations = [num, num]
return mask
}
}
I just used this and it works like a charm. I want to thank everyone in the post! Up votes all around!
I have two possible solutions, no code - just the idea:
not generic, should work with the settup/design apple uses at the Notification Center.
Make the Section-Header opaque, 'clone' the background-pattern of the table as background of the section-Header. Position the background-pattern depending on the section-header offset.
genereic, but probably more performance problems. Should work fine with few cells.
Add a Alpha Mask to all cell-layers. Move the Alpha Mask depending on the cell-position.
(use scrollViewDidScroll delegate method to maintain the background-pattern / Alpha-Mask offset).
I ended up setting the height of the section header to its minimum, and overriding UITableView's layoutSubviews to place the header on the tableView's superview, adjusting the frame's origin upward by its height.
The Swift 3 version didn't work for me because I added the UITableViewController as a subview. So I had to make some changes in the extension of the scrollview.
This should also work with UITableViewController that have been pushed from another ViewController (Note: not tested)
extension NavNotitionTableViewController {
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
for cell in tableView.visibleCells {
let calculatedY = cell.frame.origin.y - scrollView.contentOffset.y;
if let customCell = cell as? NavNotitionTableViewCell {
if(calculatedY < 44 && calculatedY > 0){
let hideAmount = 44 - calculatedY;
if let customCell = cell as? NavNotitionTableViewCell {
customCell.maskCell(fromTop: hideAmount)
}
}else if (calculatedY > 0){
//All other cells
customCell.maskCell(fromTop: 0)
}else if (calculatedY < 0){
customCell.maskCell(fromTop: cell.frame.height);
}
}
}
}
}
In this example, I first get the frame Y origin of the cell and distract the scollViews contentOffsetY.
The height of my custom section is 44. So I define the hideAmount value for the mask.
The Cell functions are untouched:
public func maskCell(fromTop margin: CGFloat) {
layer.mask = visibilityMask(withLocation: margin / frame.size.height)
layer.masksToBounds = true
}
private func visibilityMask(withLocation location: CGFloat) -> CAGradientLayer {
let mask = CAGradientLayer()
mask.frame = bounds
mask.colors = [UIColor.white.withAlphaComponent(0).cgColor, UIColor.white.cgColor]
let num = location as NSNumber
mask.locations = [num, num]
return mask
}
This wouldn't work if you wanted to show content behind your table view, but, since I'm only trying to create rounded headers with a plain solid colour background behind them, what solved it for me was mimicking transparency by setting the background colour of the header's background view to the background colour of the table view (or the first parent view with an opaque background).
class YourViewController {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for cell in tableView.visibleCells {
// if tableView is under navigationbar, set `systemTopInset`
cell.sectionHeaderMask(delegate: self, systemTopInset: self.navigationController?.navigationBar.frame.height ?? 0)
}
}
}
public extension UITableViewCell {
func sectionHeaderMask<T: UITableViewDelegate>(delegate: T, systemTopInset: CGFloat = 0) {
guard let tableView = self.superview as? UITableView else { return }
guard let indexPath = tableView.indexPath(for: self) else { return }
guard let heightForHeader = delegate.tableView?(tableView, heightForHeaderInSection: indexPath.section) else { return }
let hiddenFrameHeight = tableView.contentOffset.y - self.frame.origin.y + heightForHeader + tableView.contentInset.top + systemTopInset
if hiddenFrameHeight >= 0 || hiddenFrameHeight <= self.frame.size.height {
mask(margin: Float(hiddenFrameHeight))
}
}
private func mask(margin: Float) {
layer.mask = visibilityMask(location: (margin / Float(frame.size.height) ))
layer.masksToBounds = true
}
private func visibilityMask(location: Float) -> CAGradientLayer {
let mask = CAGradientLayer()
mask.frame = self.bounds
mask.colors = [UIColor(white: 1, alpha: 0).cgColor, UIColor(white: 1, alpha: 1).cgColor]
mask.locations = [NSNumber(value: location), NSNumber(value: location)]
return mask
}
}
Or, if all you need is for your UI to look nice,
you could
change your table view to not have floating section headers
In two quick and easy steps (iOS 6):
Change your UITableView style to UITableViewStyleGrouped. (You can do this from Storyboard/NIB, or via code.)
Next, set your tableview's background view to a empty view like so [in either a method such as viewDidAppear or even in the cellForRow method (though I would prefer the former)].
yourTableView.backgroundView = [[UIView alloc] initWithFrame:listTableView.bounds];
Voila, now you have your table view - but without the floating section headers. Your section headers now scroll along with the cells and your messy UI problems are solved!
Do try this out and let me know how it goes.
Happy coding :)
EDIT: for iOS 7, simply change the table view style to 'UITableViewStyleGrouped' and change the view's tint color to 'clear color'.
I'm a new learner of ios programming. I have tried to search with another example and more questions at stackoverflow but it's not my goal. I want to set an image of dot at index 0 of UIPageControl as similar as iPhone search homescreen. Have any way to do it ? Please explain me with some code or another useful link.
Thanks in advance
I have found a solution for this problem. I know it is not the way but it will work till iOS 8 will be launched in the market.
Reason for Crash:
in iOS 7 [self.subViews objectAtIndex: i] returns UIView Instead of UIImageView and setImage is not the property of UIView and the app crashes. I solve my problem using this following code.
Check Whether the subview is UIView(for iOS7) or UIImageView(for iOS6 or earlier). And If it is UIView I am going to add UIImageView as subview on that view and voila its working and not crash..!!
-(void) updateDots
{
for (int i = 0; i < [self.subviews count]; i++)
{
UIImageView * dot = [self imageViewForSubview: [self.subviews objectAtIndex: i]];
if (i == self.currentPage) dot.image = activeImage;
else dot.image = inactiveImage;
}
}
- (UIImageView *) imageViewForSubview: (UIView *) view
{
UIImageView * dot = nil;
if ([view isKindOfClass: [UIView class]])
{
for (UIView* subview in view.subviews)
{
if ([subview isKindOfClass:[UIImageView class]])
{
dot = (UIImageView *)subview;
break;
}
}
if (dot == nil)
{
dot = [[UIImageView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, view.frame.size.width, view.frame.size.height)];
[view addSubview:dot];
}
}
else
{
dot = (UIImageView *) view;
}
return dot;
}
Also, to clear the images that are already there, set the tint colors for the existing indicators to transparent:
- (void)awakeFromNib
{
self.pageIndicatorTintColor = [UIColor clearColor];
self.currentPageIndicatorTintColor = [UIColor clearColor];
}
Hope this will solve ur issue for iOS 7.
Happy coding
Try this link:-
Answer with GrayPageControl:-
Is there a way to change page indicator dots color
It is really good and reliable.I also have used this code.
You might have to do some more customization as
-(void) updateDots
{
for (int i = 0; i < [self.subviews count]; i++)
{
UIImageView* dot = [self.subviews objectAtIndex:i];
if (i == self.currentPage) {
if(i==0) {
dot.image = [UIImage imageNamed:#"activesearch.png"];
} else {
dot.image = activeImage;
}
} else {
if(i==0) {
dot.image = [UIImage imageNamed:#"inactivesearch.png"];
} else {
dot.image = inactiveImage;
}
}
}
}
Simply change the UIPageControl page indicator Color with pattern Image self.pageControl.currentPageIndicatorTintColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"circle"]];
The best compilation of the code for Swift 3 to replace the first icon of UIPageControl by a location marker:
import UIKit
class LocationPageControl: UIPageControl {
let locationArrow: UIImage = UIImage(named: "locationArrow")!
let pageCircle: UIImage = UIImage(named: "pageCircle")!
override var numberOfPages: Int {
didSet {
updateDots()
}
}
override var currentPage: Int {
didSet {
updateDots()
}
}
override func awakeFromNib() {
super.awakeFromNib()
self.pageIndicatorTintColor = UIColor.clear
self.currentPageIndicatorTintColor = UIColor.clear
self.clipsToBounds = false
}
func updateDots() {
var i = 0
for view in self.subviews {
var imageView = self.imageView(forSubview: view)
if imageView == nil {
if i == 0 {
imageView = UIImageView(image: locationArrow)
} else {
imageView = UIImageView(image: pageCircle)
}
imageView!.center = view.center
view.addSubview(imageView!)
view.clipsToBounds = false
}
if i == self.currentPage {
imageView!.alpha = 1.0
} else {
imageView!.alpha = 0.5
}
i += 1
}
}
fileprivate func imageView(forSubview view: UIView) -> UIImageView? {
var dot: UIImageView?
if let dotImageView = view as? UIImageView {
dot = dotImageView
} else {
for foundView in view.subviews {
if let imageView = foundView as? UIImageView {
dot = imageView
break
}
}
}
return dot
}
}
Attached images:
I've created a custom page controller that should function in mostly the same way without hacking into the internals of a UIPageControl or having a whole library for one small widget.
Just place an empty UIStackView in your storyboard and make its custom class this class below, and use numberOfPages and currentPage just like a normal UIPageControl. Set the spacing on the UIStackView to change how much space there is between the views.
Swift 4.2
/**
If adding via storyboard, you should not need to set a width and height constraint for this view,
just set a placeholder for each so autolayout doesnt complain and this view will size itself once its populated with pages at runtime
*/
class PageControl: UIStackView {
#IBInspectable var currentPageImage: UIImage = UIImage(named: "whiteCircleFilled")!
#IBInspectable var pageImage: UIImage = UIImage(named: "whiteCircleOutlined")!
/**
Sets how many page indicators will show
*/
var numberOfPages = 3 {
didSet {
layoutIndicators()
}
}
/**
Sets which page indicator will be highlighted with the **currentPageImage**
*/
var currentPage = 0 {
didSet {
setCurrentPageIndicator()
}
}
override func awakeFromNib() {
super.awakeFromNib()
axis = .horizontal
distribution = .equalSpacing
alignment = .center
layoutIndicators()
}
private func layoutIndicators() {
for i in 0..<numberOfPages {
let imageView: UIImageView
if i < arrangedSubviews.count {
imageView = arrangedSubviews[i] as! UIImageView // reuse subview if possible
} else {
imageView = UIImageView()
addArrangedSubview(imageView)
}
if i == currentPage {
imageView.image = currentPageImage
} else {
imageView.image = pageImage
}
}
// remove excess subviews if any
let subviewCount = arrangedSubviews.count
if numberOfPages < subviewCount {
for _ in numberOfPages..<subviewCount {
arrangedSubviews.last?.removeFromSuperview()
}
}
}
private func setCurrentPageIndicator() {
for i in 0..<arrangedSubviews.count {
let imageView = arrangedSubviews[i] as! UIImageView
if i == currentPage {
imageView.image = currentPageImage
} else {
imageView.image = pageImage
}
}
}
}
Works for my purposes but I make no guarantees
Update for Swift 3.0 ... you know if you are OK with accepting stated risk: "Modifying the subviews of an existing control is fragile".
import UIKit
class CustomImagePageControl: UIPageControl {
let activeImage:UIImage = UIImage(named: "SelectedPage")!
let inactiveImage:UIImage = UIImage(named: "UnselectedPage")!
override func awakeFromNib() {
super.awakeFromNib()
self.pageIndicatorTintColor = UIColor.clear
self.currentPageIndicatorTintColor = UIColor.clear
self.clipsToBounds = false
}
func updateDots() {
var i = 0
for view in self.subviews {
if let imageView = self.imageForSubview(view) {
if i == self.currentPage {
imageView.image = self.activeImage
} else {
imageView.image = self.inactiveImage
}
i = i + 1
} else {
var dotImage = self.inactiveImage
if i == self.currentPage {
dotImage = self.activeImage
}
view.clipsToBounds = false
view.addSubview(UIImageView(image:dotImage))
i = i + 1
}
}
}
fileprivate func imageForSubview(_ view:UIView) -> UIImageView? {
var dot:UIImageView?
if let dotImageView = view as? UIImageView {
dot = dotImageView
} else {
for foundView in view.subviews {
if let imageView = foundView as? UIImageView {
dot = imageView
break
}
}
}
return dot
}
}
You just need do it like this:
((UIImageView *)[[yourPageControl subviews] objectAtIndex:0]).image=[UIImage imageNamed:#"search.png"];
I think for this you need to customize whole UIPageControl. Please find more out at below links
How can i change the color of pagination dots of UIPageControl?
http://iphoneappcode.blogspot.in/2012/03/custom-uipagecontrol.html
From iOS 14 you can get and set the indicator image with these methods:
#available(iOS 14.0, *)
open func indicatorImage(forPage page: Int) -> UIImage?
#available(iOS 14.0, *)
open func setIndicatorImage(_ image: UIImage?, forPage page: Int)
From iProgrammer's answer
In case you want to hide the original dot
- (UIImageView *)imageViewForSubview:(UIView *)view {
UIImageView * dot = nil;
[view setBackgroundColor:[UIColor clearColor]]; << add this line
Following the previous answers I came up with the solution below.
Keep in mind that I had to add valueChanged listener that calls updateDots() as well in controller to handle taps made on UIPageControl
import UIKit
class PageControl: UIPageControl {
private struct Constants {
static let activeColor: UIColor = .white
static let inactiveColor: UIColor = .black
static let locationImage: UIImage = UIImage(named: "Location")!
}
// Update dots when currentPage changes
override var currentPage: Int {
didSet {
updateDots()
}
}
override func awakeFromNib() {
super.awakeFromNib()
self.pageIndicatorTintColor = .clear
self.currentPageIndicatorTintColor = .clear
}
func updateDots() {
for (index, view) in self.subviews.enumerated() {
// Layers will be redrawn, remove old.
view.layer.sublayers?.removeAll()
if index == 0 {
drawImage(view: view)
} else {
drawDot(index: index, view: view)
}
}
}
private func drawDot(index: Int, view: UIView) {
let dotLayer = CAShapeLayer()
dotLayer.path = UIBezierPath(ovalIn: view.bounds).cgPath
dotLayer.fillColor = getColor(index: index)
view.layer.addSublayer(dotLayer)
}
private func drawImage(view: UIView) {
let height = view.bounds.height * 2
let width = view.bounds.width * 2
let topMargin: CGFloat = -3.5
let maskLayer = CALayer()
maskLayer.frame = CGRect(x: 0, y: 0, width: width, height: height)
maskLayer.contents = Constants.locationImage.cgImage
maskLayer.contentsGravity = .resizeAspect
let imageLayer = CALayer()
imageLayer.frame = CGRect(x:0, y: topMargin, width: width, height: height)
imageLayer.mask = maskLayer
imageLayer.backgroundColor = getColor()
view.backgroundColor = .clear // Otherwise black background
view.layer.addSublayer(imageLayer)
}
private func getColor(index: Int? = 0) -> CGColor {
return currentPage == index ? Constants.activeColor.cgColor : Constants.inactiveColor.cgColor
}
}
I'm currently working on drawing vertical Chinese text in a label. Here's what I am trying to achieve, albeit with Chinese Characters:
I've been planning to draw each character, rotate each character 90 degrees to the left, then rotating the entire label via affine transformations to get the final result. However, it feels awfully complicated. Is there an easier way to draw the text without complicated CoreGraphics magic that I'm missing?
Well, You can do like below:
labelObject.numberOfLines = 0;
labelObject.lineBreakMode = NSLineBreakByCharWrapping;
and setFrame with -- height:100, width:20 It will work fine..
It works
UILabel *lbl = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 30, 100)];
lbl.transform = CGAffineTransformMakeRotation((M_PI)/2);
Tried the method offered by Simha.IC but it didn't work well for me. Some characters are thinner than others and get placed two on a line. E.g.
W
ai
ti
n
g
The solution for me was to create a method that transforms the string itself into a multiline text by adding \n after each character. Here's the method:
- (NSString *)transformStringToVertical:(NSString *)originalString
{
NSMutableString *mutableString = [NSMutableString stringWithString:originalString];
NSRange stringRange = [mutableString rangeOfString:mutableString];
for (int i = 1; i < stringRange.length*2 - 2; i+=2)
{
[mutableString insertString:#"\n" atIndex:i];
}
return mutableString;
}
Then you just setup the label like this:
label.text = [self transformStringToVertical:myString];
CGRect labelFrame = label.frame;
labelFrame.size.width = label.font.pointSize;
labelFrame.size.height = label.font.lineHeight * myString.length;
label.frame = labelFrame;
Enjoy!
If you would like to rotate the whole label (including characters), you can do so as follows:
First add the QuartzCore library to your project.
Create a label:
UILabel* label = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 30.0)];
[label setText:#"Label Text"];
Rotate the label:
[label setTransform:CGAffineTransformMakeRotation(-M_PI / 2)];
Depending on how you'd like to position the label you may need to set the anchor point. This sets the point around which a rotation occurs. Eg:
[label.layer setAnchorPoint:CGPointMake(0.0, 1.0)];
This is another way to draw vertical text, by subclassing UILabel. But it is some kind different of what the question want.
Objective-C
#implementation MyVerticalLabel
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
CGContextRef context = UIGraphicsGetCurrentContext();
CGAffineTransform transform = CGAffineTransformMakeRotation(-M_PI_2);
CGContextConcatCTM(context, transform);
CGContextTranslateCTM(context, -rect.size.height, 0);
CGRect newRect = CGRectApplyAffineTransform(rect, transform);
newRect.origin = CGPointZero;
NSMutableParagraphStyle *textStyle = [[NSMutableParagraphStyle defaultParagraphStyle] mutableCopy];
textStyle.lineBreakMode = self.lineBreakMode;
textStyle.alignment = self.textAlignment;
NSDictionary *attributeDict =
#{
NSFontAttributeName : self.font,
NSForegroundColorAttributeName : self.textColor,
NSParagraphStyleAttributeName : textStyle,
};
[self.text drawInRect:newRect withAttributes:attributeDict];
}
#end
A sample image is following:
Swift
It can put on the storyboard, and watch the result directly. Like the image, it's frame will contain the vertical text. And text attributes, like textAlignment, font, work well too.
#IBDesignable
class MyVerticalLabel: UILabel {
override func drawRect(rect: CGRect) {
guard let text = self.text else {
return
}
// Drawing code
let context = UIGraphicsGetCurrentContext()
let transform = CGAffineTransformMakeRotation( CGFloat(-M_PI_2))
CGContextConcatCTM(context, transform)
CGContextTranslateCTM(context, -rect.size.height, 0)
var newRect = CGRectApplyAffineTransform(rect, transform)
newRect.origin = CGPointZero
let textStyle = NSMutableParagraphStyle.defaultParagraphStyle().mutableCopy() as! NSMutableParagraphStyle
textStyle.lineBreakMode = self.lineBreakMode
textStyle.alignment = self.textAlignment
let attributeDict: [String:AnyObject] = [
NSFontAttributeName: self.font,
NSForegroundColorAttributeName: self.textColor,
NSParagraphStyleAttributeName: textStyle,
]
let nsStr = text as NSString
nsStr.drawInRect(newRect, withAttributes: attributeDict)
}
}
Swift 4
override func draw(_ rect: CGRect) {
guard let text = self.text else {
return
}
// Drawing code
if let context = UIGraphicsGetCurrentContext() {
let transform = CGAffineTransform( rotationAngle: CGFloat(-Double.pi/2))
context.concatenate(transform)
context.translateBy(x: -rect.size.height, y: 0)
var newRect = rect.applying(transform)
newRect.origin = CGPoint.zero
let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
textStyle.lineBreakMode = self.lineBreakMode
textStyle.alignment = self.textAlignment
let attributeDict: [NSAttributedStringKey: AnyObject] = [NSAttributedStringKey.font: self.font, NSAttributedStringKey.foregroundColor: self.textColor, NSAttributedStringKey.paragraphStyle: textStyle]
let nsStr = text as NSString
nsStr.draw(in: newRect, withAttributes: attributeDict)
}
}
Swift 5
More easy way with CGAffineTransform
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var verticalText: UILabel
override func viewDidLoad() {
verticalText.transform = CGAffineTransform(rotationAngle:CGFloat.pi/2)
}
}
import UIKit
class VerticalLabel : UILabel {
private var _text : String? = nil
override var text : String? {
get {
return _text
}
set {
self.numberOfLines = 0
self.textAlignment = .center
self.lineBreakMode = .byWordWrapping
_text = newValue
if let t = _text {
var s = ""
for c in t {
s += "\(c)\n"
}
super.text = s
}
}
}
}
I have a UILabel with some text, say "Hello World abcdefg" The label can have multiple lines, different font sizes etc.
Question: How do I find the coordinates of all letters "d" in this UILabel.
Logical first step is find the position of those characters in the string (UILabel.text), but then how do I translate that into coordinates when it's actually drawn on screen
The idea is to find those coordinates and draw something custom on top of that character (basically to cover it with a custom image)
The basic tools for measuring text on iPhone are in UIStringDrawing.h but none of them do what you need. You will basically have to iterate through substrings one character at a time measuring each. When a line wraps (the result is taller), split after the last character that did not wrap and add the line height to your y coordinate.
- (CGSize)sizeWithFont:(UIFont *)font forWidth:(CGFloat)width lineBreakMode:(UILineBreakMode)lineBreakMode;
Methods have changed since iOS 7.0 came out. Try this
- (CGFloat)charactersOffsetBeforeDayPartOfLabel {
NSRange range = [[self stringFromDate:self.currentDate] rangeOfString:[NSString stringWithFormat:#"%i",[self dayFromDate:self.currentDate]]];
NSString *chars = [[self stringFromDate:self.currentDate] substringToIndex:range.location];
NSMutableArray *arrayOfChars = [[NSMutableArray alloc]init];
[chars enumerateSubstringsInRange:NSMakeRange(0, [chars length]) options:(NSStringEnumerationByComposedCharacterSequences) usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
[arrayOfChars addObject:substring];
}];
CGFloat charsOffsetTotal = 0;
for (NSString *i in arrayOfChars){
NSDictionary *attributes = #{NSFontAttributeName: [UIFont fontWithName:#"Helvetica Neue" size:16.0f]};
charsOffsetTotal += [i sizeWithAttributes:attributes].width;
}
return charsOffsetTotal;
}
Here ya go:
fileprivate let selfSizing = UILabel()
class DualColorLabel: UILabel
{
var filled: UIColor?
var unfilled: UIColor?
var origin: String?
var widths: [CGFloat] = []
var fuckupLockup = false
override var text: String? {
didSet {
if fuckupLockup {
print ("SDBOFLAG-13822 wtf?")
}
}
}
func setupColorsAndText(filled: UIColor,
unfilled: UIColor)
{
self.filled = filled
self.unfilled = unfilled
guard let text = origin, text.count > 0 else {
assertionFailure("usage error")
return
}
guard font != nil else {
usageError()
return
}
for index in 1...text.count {
let s = String.Index(utf16Offset: 0, in: text)
let e = String.Index(utf16Offset: index, in: text)
let beginning = text[s..<e]
let p = String(beginning)
selfSizing.font = font
selfSizing.text = p
let size = selfSizing.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude))
let width = size.width
widths.append(width)
}
}
func setupfill(adjusted: CGRect)
{
assert(adjusted.origin.x <= 0, "fixed this code for fill in the middle: currently supported only fill at start")
let endOffset = adjusted.width + adjusted.origin.x
guard let font = self.font else {
usageError()
return
}
guard let origin = origin, let filled = filled,
let unfilled = unfilled else {
usageError()
return
}
var idx = String.Index(utf16Offset: origin.count, in: origin)
for (index, width) in widths.enumerated() {
if endOffset < width {
idx = String.Index(utf16Offset: index, in: origin)
print ("SDBOFLAG-13822 index \(index) for text \(origin)")
break
}
}
let total = NSMutableAttributedString()
do {
let s = String.Index(utf16Offset: 0, in: origin)
let beginning = origin[s..<idx]
let p = String(beginning)
print("SDBOFLAG-13822 filled text \(p)")
let filledAttributes:
[NSAttributedString.Key : Any] = [NSAttributedString.Key.foregroundColor:
// UIColor.yellow,
filled,
NSAttributedString.Key.font:
font
]
let filledPortion = NSAttributedString(string: p, attributes: filledAttributes)
total.append(filledPortion)
}
let unfilledAttributes:
[NSAttributedString.Key : Any] = [NSAttributedString.Key.foregroundColor:
// UIColor.blue,
unfilled,
NSAttributedString.Key.font: font]
let e = String.Index(utf16Offset: origin.count, in: origin)
let ending = origin[idx..<e]
let str = String(ending)
print("SDBOFLAG-13822 unfilled text \(str)")
let unfilledPortion = NSAttributedString(string: str, attributes: unfilledAttributes)
total.append(unfilledPortion)
self.attributedText = total
fuckupLockup = true
}
/*
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
}
*/
}
func usageError()
{
assertionFailure("usage error")
}
The width calculation for fragments goes into widths array per suggestions provided.