Large Navigation Bar Text With Multiple Colors - ios

iOS 11 introduces the option for larger text in the Navigation Bar. I would like to have a title that uses multiple colors. For example:
It's fairly easy to set the title, and even to change the color of the entire title:
[[self navigationItem] setTitle: #"Colors"];
[[[self navigationController] navigationBar] setLargeTitleTextAttributes: #{NSForegroundColorAttributeName: [UIColor colorFromHex: redColor]}];
What I can't figure out is how to change just part of the title. For example, a way to select the range like this – NSRangeMake(0, 1) – so that I could apply a color to it.
This must be possible, right?

There is no public API to set your own attributed text for the large title.
The solution is to navigate down the view hierarchy. You specifically mentioned you wanted to avoid this, but it's the only way to modify the colors while getting the rest of the UINavigationBar behavior for free.
Of course, you can always create your own UILabel and set its attributedText, but you will have to re-create any navigation bar animations and other behavior yourself.
Honestly the simplest solution is to modify your design so it doesn't require a multi-colored large title, as this is currently not supported.
I took a dive down the "spelunking" path, and there are a variety of visual issues with animations snapping back to the original text color.
Here is the code I used if it's useful for anyone trying to achieve a similar effect:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
applyColorStyle(toLabels: findTitleLabels())
}
private func applyColorStyle(toLabels labels: [UILabel]) {
for titleLabel in labels {
let attributedString = NSMutableAttributedString(string: titleLabel.text ?? "")
let fullRange = NSRange(location: 0, length: attributedString.length)
attributedString.addAttribute(NSAttributedStringKey.font, value: titleLabel.font, range: fullRange)
let colors = [UIColor.red, UIColor.orange, UIColor.yellow, UIColor.green, UIColor.blue, UIColor.purple]
for (index, color) in colors.enumerated() {
attributedString.addAttribute(NSAttributedStringKey.foregroundColor, value: color, range: NSRange(location: index, length: 1))
}
titleLabel.attributedText = attributedString
}
}
private func findTitleLabels() -> [UILabel] {
guard let navigationController = navigationController else { return [] }
var labels = [UILabel]()
for view in navigationController.navigationBar.subviews {
for subview in view.subviews {
if let label = subview as? UILabel {
if label.text == title { labels.append(label) }
}
}
}
return labels
}
The downside of the "spelunking" approach is it's not a supported API, meaning that it could easily break in a future update or not work as intended in various edge cases.

That's no easy feat unfortunately, it's not officially supported. Here's a few ways I'd consider:
• Override layoutSubiews() in a navigation bar subclass. Traverse the view hierarchy, and mess with the UILabel to apply attributes. I wouldn't recommend this, I've moved away from doing something similar - the iOS 11 navigation bar has a seemingly complex and unpredictable behavior. Things like interactive gestures don't always play nice with whatever you tack on. Your case is a little more simple than mine was though, so there's a chance this may work fine.
• Create your own navigation bar style view from scratch - one that doesn't inherit from UINavigationBar. This is the route I ended up taking when doing something similar. It's more work, and won't mirror future changes Apple make to the visual style - but on the other hand, your app will look the same even across older iOS versions, and you have complete control.
• Hide the nav bar separator, revert it to a non-large size, and place your own view underneath it mimicking the look (the background is a visual effect view with a .extraLight blur style) - you can place a custom label inside this view.

Related

Is UISegmentedControl just not usable currently?

Our application has a "dark" palette, with mostly black or charcoal backgrounds. This is creating a major problem in Apple controls that ignore (or don't even offer) control over text and background color.
UISegmentedControl is a particularly good example. It's drawn with often illegible, seemingly arbitrary text/background combinations. These controls are all set up with the exact same properties in IB, and yet you never know if they'll be legible from one view controller to the next.
Most of these are OK in "dark" mode in our app, but "light" mode is shambolic. I've spent a day experimenting with themes, UIAppearance, and setting appearance in IB and programmatically. I'm fed up with it. Does anyone actually know how to guarantee legibility in these things?
Try this,
let seg:UISegmentedControl = {
let seg = UISegmentedControl()
seg.insertSegment(withTitle: "tab 1", at: 0, animated: true)
seg.insertSegment(withTitle: "tab 2", at: 1, animated: true)
seg.selectedSegmentTintColor = .red //you can replace the colours you want
seg.backgroundColor = .lightGray //you can replace the colours you want
return seg
}()
Result
Dark Mode
Light Mode

Is it possible to set the alignment of segmented Control titles to the left?

I have been looking around for a way to set the alignment of the segmented control titles to the left but I don't seem to be able to achieve what I want.
I have created this little function to change the frame of the subviews of the segment control.
It works at first.
func modifyFrameOfSegment() {
for segment in segmentedControl.subviews {
guard segment.subviews.isNotEmpty else { return }
segment.contentMode = .left
for label in segment.subviews where label is UILabel {
label.frame = CGRect(x: 0, y: label.frame.origin.y, width: label.frame.size.width, height: label.frame.size.height)
(label as! UILabel).textAlignment = .left
}
}
}
But everytime I select a new segment it resets the frames of all the subviews and center align all the titles again.
Is there a way to achieve a permanent left alignment for the segment titles in a segmented control?
Any tips or advice would be greatly appreciated.
Thank you for your time.
Let's use this method
self.segmentedControl.setContentPositionAdjustment(UIOffset(horizontal: -20, vertical: 0), forSegmentType: .left, barMetrics: .default)
And you can do what you want (Of course, you can change the horizontal & vertical value by your needs). Here is the result:
Update:
There's apparently no way to set the alignment of the items, but you can fake it by adjusting the position of each individual item using setContentOffset(_ offset: CGSize, forSegmentAt segment: Int). Here's a kludgy example:
class LeftSegmentedControl: UISegmentedControl {
var margin : CGFloat = 10
override func layoutSubviews() {
super.layoutSubviews()
leftJustifyItems()
}
func leftJustifyItems() {
let fontAttributes = titleTextAttributes(for: .normal)
let segments = numberOfSegments - 1
let controlWidth = frame.size.width
let segmentWidth = controlWidth / CGFloat(numberOfSegments)
for segment in 0...segments {
let title = titleForSegment(at: segment)
setWidth(segmentWidth, forSegmentAt: segment)
if let t = title {
let titleSize = t.size(withAttributes: fontAttributes)
let offset = (segmentWidth - titleSize.width) / 2 - margin
self.setContentOffset(CGSize(width: -offset, height: 0), forSegmentAt: segment)
}
}
}
}
Here's what it looks like:
There are a few caveats:
This version sets the segments to all have equal width, which might not be what you want.
I used a fixed left margin of 10px because it seems unlikely that you'd want to vary that, but you can obviously change it or make it a settable property.
Just because you can do this doesn't mean you should. Personally, I don't think it looks great, and it suffers in the usability department too. Users expect segmented control items to be centered, and left-justifying the items will make it harder for them to know where to tap to hit the segment. That seems particularly true for short items like the one labelled "3rd" in the example. It's not terrible, it just seems a little weird.
Original answer:
UIControl (of which UISegmentedControl is a subclass) has a contentHorizontalAlignment property that's supposed to tell the control to align its content a certain way, so the logical thing to do would be to set it like this:
let segmented = UISegmentedControl(items: ["Yes", "No", "Maybe"])
segmented.frame = CGRect(x:75, y:250, width:250, height:35)
segmented.contentHorizontalAlignment = .left
But that doesn't work — you still get the labels centered. If you've got a compelling use case for left-aligned segments, you should send the request to Apple.
One way you could work around this problem is to render your labels into images and then use the images as the segment labels instead of plain strings. Starting from the code in How to convert a UIView to an image, you could easily subclass UISegmentedControl to create images from the item strings.

Custom animation for UISearchController?

I'm presenting a UISearchController from my controller embedded in a navigation controller. The default animation occurs, where the search box drops down from the top on the navigation bar.
This isn't a good UX in my case because I present the search when a user taps into a UITextField in the middle of the screen. What I'd like to do is have the UITextField float to the top and morph into the search box, but I can't figure how to do this.
This is what I have:
class PlacesSearchController: UISearchController, UISearchBarDelegate {
convenience init(delegate: PlacesAutocompleteViewControllerDelegate) {
let tableViewController = PlacesAutocompleteContainer(
delegate: delegate
)
self.init(searchResultsController: tableViewController)
self.searchResultsUpdater = tableViewController
self.hidesNavigationBarDuringPresentation = false
self.definesPresentationContext = true
self.searchBar.placeholder = searchBarPlaceholder
}
}
private extension ShowAddressViewController {
#objc func streetAddressTextFieldEditingDidBegin() {
present(placesSearchController, animated: true, completion: nil)
}
}
Instead of the search dropping down from the top, I'm hoping to get the text field fly up to the nav bar. What I’m after is the same effect that’s on the iOS 11 File app:
It has a text field in the middle of the screen then animated up to the navigation bar when you tap on it. In my case though, the text field is way lower in the screen and not originally part of the navigation bar.
UISearchController
UISearchController is a component that highly difficult to customize. From my experience I can say, that it is better to use it as is without any drastic or significant customization. Otherwise, customization could result in messy code, global state variables, runtime tricks with UIView hierarchy etc.
If specific behavior still needed, it is better to implement search controller from scratch or use third party one.
Default implementation
Looks like UISearchController was designed to be used in couple with UITableView and UISearchBar installed in the table header view. Even apple official code samples and documentation provides such example of usage (see UISearchController). Attempt to install UISearchBar somewhere else often results in numerous ugly side effects with search bar frame, position, twitching animations, orientation changes etc.
Starting with iOS 11, UINavigationItem got searchController property. It allows to integrate search controller into your navigation interface, so search will look exactly like iOS Files or iOS AppStore app. UISearchController's behavior still coupled with another component, but I believe it is better than coupling with UITableView all the time.
Possible solutions
From my perspective there are several possible solutions for your problem. I will provide them in order of increasing effort, which is needed for implementation:
If you still want to use UISearchController, consider to use it as is without significant customizations. Apple provides sample code, that demonstrates how to use UISearchController (see Table Search with UISearchController).
There are several third party libraries which may be more flexible with lots of additional features. For example: YNSearch, PYSearch. Please, have a look.
Along with UISearchController presentation, you could try to move UITextField up with changing alpha from 1 to 0. This will create a feeling that UITextField is smoothly transforming to UISearchBar. This approach described in article that was provided by Chris Slowik (see comment to your original post). The only thing I would improve is animations using transition coordinators (see example here), it will make animations smoother with synchronized timing. Final implementation also will be much cleaner.
As an option, you could try to design your own search controller using only UISearchBar or even plain UITextField.
You could subclass UISearchController and add UITextField object. UISearchController conforms to UIViewControllerAnimatedTransitioning and UIViewControllerTransitioningDelegate protocols, where UITextFiled could be removed or added along with transition animations.
I hope this helps.
Update:
I have implemented approach I described under point 3. Here is how it works:
You can find code snippet here. Please note, that it is only code example, there are might be situations which are not handled. Check it twice then.
Create one UIView in XIB. name it searchView.
Add UIButton inside above UIView in same xib and name it btnSearch. Like below

Setup search controller in ViewDidLoad as below:
func setupSearchController() {
self.searchController = UISearchController(searchResultsController: nil)
self.searchController.delegate = self
self.searchController.searchBar.delegate = self
definesPresentationContext = true
self.searchController.dimsBackgroundDuringPresentation = false
self.searchController.searchBar.sizeToFit()
searchView.addSubview(self.searchController.searchBar)
self.searchController.searchBar.isHidden = true
self.searchController.searchBar.tintColor = UIColor.white
self.searchController.searchBar.showsCancelButton = false
UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).tintColor = session?.makeColor(fromHexString: TYPE_COLOR, alpha: 1.0)
self.searchController.searchBar.isTranslucent = false
self.searchController.searchBar.barTintColor = UIColor(red: 239.0 / 255.0, green: 135.0 / 255.0, blue: 1.0 / 255.0, alpha: 1.0)
}
This method will setup searchcontroller programatically inside searchview.
Now you just need to show searchcontroller programatically. add On click method of button in step 2. Call this below method name showsearchAnimation:
func ShowSeachAnimation() {
searchFrame = searchView.frame (add one global variable "searchFrame" in controller which saves searchView.frame so it will be used when cancel button clicked on search)
self.btnSearch.isHidden = true
var yAxis : CGFloat = 20
if #available(iOS 11 , *) {
yAxis = 8
} else {
yAxis = 20
}
searchView.translatesAutoresizingMaskIntoConstraints = true
searchView.frame = CGRect(x: 0, y: yAxis, width: view.frame.size.width, height: view.frame.size.height - yAxis)
self.searchController.searchBar.isHidden = false
self.searchController.searchBar.becomeFirstResponder()
self.searchController.searchBar.showsCancelButton = true
self.searchBar(self.searchController.searchBar, textDidChange: "")
searchView.layoutIfNeeded()
}
For hide searchbar, Add search hide method named "searchbarcancelbuttonclicked" in searchcontroller delegate:
func searchViewHideAnimation() {
self.removeNavigationBar()
self.navigationController?.isNavigationBarHidden = false
self.searchController.searchBar.text = " "
self.searchController.searchBar.isHidden = true
UIView.animate(withDuration: 0.3, animations: {() -> Void in
self.searchView.frame = self.searchFrame!
self.btnSearch.isHidden = false
}, completion: {(_ finished: Bool) -> Void in
self.searchView.layoutIfNeeded()
})
}
you can try to use my https://github.com/Blackjacx/SHSearchBar Which essentially will be your text field. You can add constraints to the left, right and top and adjust the top constraint when the text field editing begins. At the same time, you hide the navigation bar animated and gray out the background by using an overlay view. This way you have maximized control over your animations and this appüroach is not so difficult as it might sound.

UIImageView.appearance is overriding UISegmentedControl.appearance

I've been trying to use the appearance proxy API to apply some default colors to some controls, but I've run into a problem.
When I apply a tint color to UISegmentedControl using something like...
UISegmentedControl.appearance().tintColor = UIColor.red
It generates this...
All good, but when I add...
UIImageView.appearance().tintColor = UIColor.green
it changes to...
Just to be clear, I have BOTH this lines in my code
UISegmentedControl.appearance().tintColor = UIColor.red
UIImageView.appearance().tintColor = UIColor.green
It doesn't matter in what order I call them, the result is the same, the UIImageView properties override the UISegmentedControls
I've spent over half a day trying to find a solution to this problem but can't seem to find anything that works.
Running Xcode 8.2, iOS 10, Swift 3
What am I doing wrong and how can I fix?
I am not sure about this, but I guess, UISegmentedControl uses UIImageView to create segments, i.e. the segments we see inside segmented control are UIImageViews and not UIViews. UISegmentedControl even has methods to setImage for a particular segment.
If above is true, we can use appearanceWhenContainedIn API of UIAppearance to set image view tint colour like this:
UIImageView.appearance(whenContainedInInstancesOf: [UISegmentedControl.self]).tintColor = UIColor.red
UIImageView.appearance().tintColor = UIColor.green

Update segmented control in ios without change interface

when I update segmented control text, the interface (segment's width) changed and cut some letters.
[segmentedcontoll setTitle:#"test" forSegmentAtIndex:1];
segmentedcontoll.apportionsSegmentWidthsByContent = YES;
How can I solve this ?
EDIT:
It looks like your content has outgrown the dimensions of the standard UISegmentedControl.
If you are okay with smaller font, it's possible to set the entire control to have a smaller font point size, seen here.
Another option is to configure the segments the other supported way.. With images. It's a little bit of a hack, but you can create images on the fly with the UIView Snapshotting API of views/labels configured however you want and set images for each segment instead of using text. This would allow you to create 2 line labels with fixed widths and set images for each section to be images generated from the label as the content changes. More work, but you would still be using the standard class.
The last option, which might work the best for you, is to create some other custom control that does what you would like. After all, UISegmentedControl really is just a nice button container. And it does somewhat seem like you are using the control in a non-standard way - both as a control and an input form section.
Others have gone this route before and created alternatives that you can use.
You can create a separate class as below,
class CustomSegmentedControl: UISegmentedControl {
//code for creating multi line
override func didMoveToSuperview()
{
for segment in subviews
{
for subview in segment.subviews
{
if let segmentLabel = subview as? UILabel
{
segmentLabel.numberOfLines = 0 //just change here the number of lines and check it.
}
}
}
}
}
and create an outlet in your viewcontroller as,
// Initialize
let items = ["Purple", "Green", "New Segment"]
let customSC = CustomSegmentedControl(items: items)
use customSC and do what ever you want to do, similar to segmentedControl object.

Resources