How to center placeholder text in UISearchBar iOS 11 - ios

With iOS 11, searchBars are defaulting to a left aligned text. While this looks good with the rest of the native changes to iOS, it doesn't really fit my design, and I would like it to be center, as it was before.
I can't find any such alignment attributes on UISearchBar. Am I missing something, or is it simply not possible? Do I have to create my own custom search bar e.g derived from a UITextField to achieve this?

I had exactly the same problem - I'm struggling to understand why the default alignment would be changed without allowing us to easily set this back to centered.
The below works for me (Swift):
let placeholderWidth = 200 // Replace with whatever value works for your placeholder text
var offset = UIOffset()
override func viewDidLoad() {
offset = UIOffset(horizontal: (searchBar.frame.width - placeholderWidth) / 2, vertical: 0)
searchBar.setPositionAdjustment(offset, for: .search)
}
func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
let noOffset = UIOffset(horizontal: 0, vertical: 0)
searchBar.setPositionAdjustment(noOffset, for: .search)
return true
}
func searchBarShouldEndEditing(_ searchBar: UISearchBar) -> Bool {
searchBar.setPositionAdjustment(offset, for: .search)
return true
}

This is the only one that worked for me, Swift 4.2:
extension UISearchBar {
func setCenteredPlaceHolder(){
let textFieldInsideSearchBar = self.value(forKey: "searchField") as? UITextField
//get the sizes
let searchBarWidth = self.frame.width
let placeholderIconWidth = textFieldInsideSearchBar?.leftView?.frame.width
let placeHolderWidth = textFieldInsideSearchBar?.attributedPlaceholder?.size().width
let offsetIconToPlaceholder: CGFloat = 8
let placeHolderWithIcon = placeholderIconWidth! + offsetIconToPlaceholder
let offset = UIOffset(horizontal: ((searchBarWidth / 2) - (placeHolderWidth! / 2) - placeHolderWithIcon), vertical: 0)
self.setPositionAdjustment(offset, for: .search)
}
}
Usage:
searchBar.setCenteredPlaceHolder()
Result:

This is a bit of a workaround.
First of all you need to get the text field of the search bar and center the text alignment:
let textFieldOfSearchBar = searchController.searchBar.value(forKey: "searchField") as? UITextField
textFieldOfSearchBar?.textAlignment = .center
After that, you need to change the placeholder's alignment. For some reason it doesn't change with the text field's alignment. You should do that by adding a padding to the left view of the textfield only when the search controller is active, so use it's delegate methods:
func presentSearchController(_ searchController: UISearchController) {
//Is active
let width: CGFloat = 100.0 //Calcualte a suitable width based on search bar width, screen size, etc..
let textfieldOfSearchBar = searchController.searchBar.value(forKey: "searchField") as? UITextField
let paddingView = UIView(x: 0, y: 0, w: width, h: searchController.searchBar.frame.size.height)
textfieldOfSearchBar?.leftView = paddingView
textfieldOfSearchBar?.leftViewMode = .unlessEditing
}
func willDismissSearchController(_ searchController: UISearchController) {
let textfieldOfSearchBar = searchController.searchBar.value(forKey: "searchField") as? UITextField
textfieldOfSearchBar?.leftView = nil
}
Good luck

There's no official way to do that. Try using UISearchBarDelegate methods and your own UILabel.
extension YourViewController: UISearchBarDelegate {
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
placeholderLabel.isHidden = true
}
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
placeholderLabel.isHidden = false
}
}
Don't forget to hide the standard left-aligned icon (use blank image):
searchBar.setImage(UIImage.imageWithColor(.clear), for: .search, state: .normal)

let placeHolderOffSet = UIOffset(horizontal: 100, vertical: 0)
setPositionAdjustment(placeHolderOffSet, for: .search)
if you want a different position while the bar is active, you'll have to reset this in the corresponding delegate method

For swift 4.0+
Well as your question doesn't really specify only placeholder I'll give an answer to both placeholder and the text. In my project, I needed both texts to be centered at all times, not just when I was not editing the textfield. You can return it to left align with some of the answers in this post by using the delegate as they stated.
Also im not using SearchController, but a SearchBar outlet, either way it can be easily fixed for your project if you use a controller. (just replace for searchController.searchBar instead of just searchBar).
So just have to call the next function where you need it.
func searchBarCenterInit(){
if let searchBarTextField = searchBar.value(forKey: "searchField") as? UITextField {
//Center search text
searchBarTextField.textAlignment = .center
//Center placeholder
let width = searchBar.frame.width / 2 - (searchBarTextField.attributedPlaceholder?.size().width)!
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: width, height: searchBar.frame.height))
searchBarTextField.leftView = paddingView
searchBarTextField.leftViewMode = .unlessEditing
}
}

I did some modification in #Danny182 answer. Here is the updated version and it will work on all OS versions.
extension UISearchBar {
func setPlaceHolder(text: String?) {
self.layoutIfNeeded()
self.placeholder = text
var textFieldInsideSearchBar:UITextField?
if #available(iOS 13.0, *) {
textFieldInsideSearchBar = self.searchTextField
} else {
for view : UIView in (self.subviews[0]).subviews {
if let textField = view as? UITextField {
textFieldInsideSearchBar = textField
}
}
}
//get the sizes
let searchBarWidth = self.frame.width
let placeholderIconWidth = textFieldInsideSearchBar?.leftView?.frame.width
let placeHolderWidth = textFieldInsideSearchBar?.attributedPlaceholder?.size().width
let offsetIconToPlaceholder: CGFloat = 8
let placeHolderWithIcon = placeholderIconWidth! + offsetIconToPlaceholder
let offset = UIOffset(horizontal: ((searchBarWidth / 2) - (placeHolderWidth! / 2) - placeHolderWithIcon), vertical: 0)
self.setPositionAdjustment(offset, for: .search)
}
}
You just need to call:
searchBar.setPlaceHolder(text: "Search \(name)")

Related

Firebaseui on iOS: can't set background color on customized subclassed login screen

I'm subclassing the login screen of Firebaseui with:
import UIKit
import FirebaseUI
class LoginViewControllerCustom: FUIAuthPickerViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .red
let arr = Bundle.main.loadNibNamed("LoginText", owner: nil)!
let v = arr[0] as! UIView
self.view.addSubview(v)
}
}
My implementation works as I see the xib LoginText loaded on login screen.
But the background color is royally ignored.
How to enforce a bg color on the login screen from that subclass?
Edit: if I apply the answer below with view.insertSubview(imageViewBackground, at: 0)
Here is what I get:
As you can see the image gets inserted under the view that holds the login button. If I set "at: 1" it completely cover the buttons and they can't be used.
I resolved the problem in an unexpected way.
On the delegate method that would load this controller, I changed:
func authPickerViewController(forAuthUI authUI: FUIAuth) -> FUIAuthPickerViewController {
return LoginViewControllerCustom(authUI: authUI)
}
to
func authPickerViewController(forAuthUI authUI: FUIAuth) -> FUIAuthPickerViewController {
return LoginViewControllerCustom(nibName: nil, bundle: Bundle.main, authUI: authUI)
}
The addition of Bundle.main solved the issue, and replaced the original controller by mine, which was several levels deeper until that.
Not sure exactly why, but this did solve the issue.
you can try to put "fake" image background:
override func viewDidLoad() {
super.viewDidLoad()
let width = UIScreen.main.bounds.size.width
let height = UIScreen.main.bounds.size.height
let imageViewBackground = UIImageView(frame: CGRect(x: 0, y: 0, width:
width, height: height))
imageViewBackground.backgroundColor = .red
view.insertSubview(imageViewBackground, at: 0)
let arr = Bundle.main.loadNibNamed("LoginText", owner: nil)!
let v = arr[0] as! UIView
self.view.addSubview(v)
}
Edit: try this it's not elegant but it solves the problem.
override func viewDidLoad() {
let scrollView = view.subviews[0]
scrollView.backgroundColor = .clear
let contentView = scrollView.subviews[0]
contentView.backgroundColor = .red
let width = UIScreen.main.bounds.size.width
let height = UIScreen.main.bounds.size.height
let backgroundImage = UIImageView(frame: CGRect(x: 0, y: -1, width: width, height: height))
view.backgroundColor = .red
backgroundImage.contentMode = UIView.ContentMode.scaleAspectFill
view.insertSubview(backgroundImage, at: 0)
}

Auto-size view with dynamic font in enclosed textview

So here's one I just can't seem to find a matching case for in searching on here.
I have a small UIView that contains a UITextView, and the UIView needs to auto-size around the TextView for presentation over another view. Basically the TextView needs to fully fill the UIView, and the UIView should only be big enough to contain the TextView.
The TextView just contains a couple sentences that are meant to stay on the screen until an external thing happens, and certain values change.
Everything is great when I used a fixed-size font.
But hey... I'm an old guy, and I have the text size jacked up a bit on my phone. Testing it on my device shows where I must be missing something.
When using the dynamic font style "Title 2" in the textview properties, and turning on "Automatically adjust font" in the TextView properties, and having the text larger than the default, it seems as if I'm not properly capturing the size of the TextView's growth (with the bigger text) when creating the new bounding rect to toss at the frame. It's returning values that look a lot like the smaller, default-size text values rather than the increased text size.
Code is below, the view's class code as well as the calling code (made super explicit for posting here). I figure I'm either missing something silly like capturing the size after something happens to the fonts, but even moving this code to a new function and explicitly calling it after the controls fully draw doesn't seem to do it.
I hope this make sense.
Thanks, all.
Calling code:
let noWView:NoWitnessesYetView = (Bundle.main.loadNibNamed("NoWitnessesYetView", owner: nil, options: nil)!.first as! NoWitnessesYetView)
//if nil != noWView {
let leftGutter:CGFloat = 20.0
let bottomGutter:CGFloat = 24.0
let newWidth = self.view.frame.width - ( leftGutter + leftGutter )
let newTop = (eventMap.frame.minY + eventMap.frame.height) - ( noWView.frame.height + bottomGutter ) // I suspect here is the issue
// I suspect that loading without drawing is maybe not allowing
// the fonts to properly draw and the
// TextView to figure out the size...?
noWView.frame = CGRect(x: 20, y: newTop, width: newWidth, height: noWView.frame.height)
self.view.addSubview(noWView)
//}
Class code:
import UIKit
class NoWitnessesYetView: UIView {
#IBOutlet weak var textView: EyeneedRoundedTextView!
override func draw(_ rect: CGRect) {
let newWidth = self.frame.width
// form up a dummy size just to get the proper height for the popup
let workingSize:CGSize = self.textView.sizeThatFits(CGSize(width: newWidth, height: CGFloat(MAXFLOAT)))
// then build the real newSize value
let newSize = CGSize(width: newWidth, height: workingSize.height)
textView.frame.size = newSize
self.textView.isHidden = false
}
override func awakeFromNib() {
super.awakeFromNib()
self.backgroundColor = UIColor.clear // .blue
self.layer.cornerRadius = 10
}
}
This perfect way to do it the content comes from : https://www.youtube.com/watch?v=0Jb29c22xu8 .
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// let's create our text view
let textView = UITextView()
textView.frame = CGRect(x: 0, y: 0, width: 200, height: 100)
textView.backgroundColor = .lightGray
textView.text = "Here is some default text that we want to show and it might be a couple of lines that are word wrapped"
view.addSubview(textView)
// use auto layout to set my textview frame...kinda
textView.translatesAutoresizingMaskIntoConstraints = false
[
textView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
textView.heightAnchor.constraint(equalToConstant: 50)
].forEach{ $0.isActive = true }
textView.font = UIFont.preferredFont(forTextStyle: .headline)
textView.delegate = self
textView.isScrollEnabled = false
textViewDidChange(textView)
}
}
extension ViewController: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
print(textView.text)
let size = CGSize(width: view.frame.width, height: .infinity)
let estimatedSize = textView.sizeThatFits(size)
textView.constraints.forEach { (constraint) in
if constraint.firstAttribute == .height {
constraint.constant = estimatedSize.height
}
}
}
}

iOS swift: add kern space in the tab bar text

I'm not able to add kern space into the tab bar attributed text.
The UITabBar in question is a custom tabBar, you can find the code below.
I'm using the "attributed key" dictionary to add attributes to the items title, but I'm having an issue with the kern space.
class ProfileTabBar: UITabBar {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setStyle()
}
required override init(frame: CGRect) {
super.init(frame: frame)
self.setStyle()
}
func setStyle() {
self.tintColor = Style.shared.primary1
// Disable the default border
self.layer.borderWidth = 0.0
self.clipsToBounds = true
// Create a new bottom border
let bottomLine = CALayer()
let screenWidth = UIScreen.main.bounds.width
//let viewForFrame = self.superview ?? self
//let screenWidth = viewForFrame.bounds.width
bottomLine.frame = CGRect(x: 0.0, y: self.frame.height - 1, width: screenWidth, height: 2.0)
bottomLine.backgroundColor = UIColor(red: 235.0/255, green: 235.0/255, blue: 235.0/255, alpha: 1.0).cgColor
self.layer.addSublayer(bottomLine)
// Get the size of a single item
let markerSize = CGSize(width: screenWidth/CGFloat(self.items!.count), height: self.frame.height)
// Create the selection indicator
self.selectionIndicatorImage = UIImage().createSelectionIndicator(color: self.tintColor, size: markerSize , lineWidth: 3.0)
// Customizing the items
if let items = self.items {
for item in items {
item.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: -15)
let attributes: [NSAttributedStringKey : Any] = [
NSAttributedStringKey.font: UIFont(name: Style.shared.fontBold.fontName, size: 14) as Any,
NSAttributedStringKey.kern: NSNumber(value: 1.0)
]
item.setTitleTextAttributes(attributes, for: .normal)
}
}
}
All the attributes works except for the kern. What I'm doing wrong?
This question is old and there is an even older answer here. It appears that UITabBarItem appearance ignores NSAttributedString.Key.kern. That leaves us with a few options.
Subclass UITabBarItem this isn't easy because UITabBarItem inherits from UIBarItem which is an NSObject not a UIView.
Subclass UITabBar this can be done, but involves a decent amount of work for just some kern. You'll have to use UIButton instead of UITabBarItem so that the kern is applied.
You can add spacing using unicode characters in your title. This is really easy and can probably achieve the spacing you're looking for with just a few lines of code.
Unicode spacing:
U+0020 1/4 em
U+2004 1/3 em
U+2005 1/4 em
U+2006 1/6 em
U+2008 The width of a period “.”
U+2009 1/5 em (or sometimes 1/6 em)
You can use a unicode character in a String in Swift like this "\u{2006}". That means we can insert a small space between all the characters in our tabBarItem title. Like this:
extension String {
var withOneSixthEmSpacing: String {
let letters = Array(self)
return letters.map { String($0) + "\u{2006}" }.joined()
}
Using this for our tabBarItems:
self.navigationController.tabBarItem = UITabBarItem(
title: "Home".withOneSixthEmSpacing,
image: homeImage,
selectedImage: homeSelectedImage
)
Visually we end up with:
Instead of:
Another workaround is to subclass UITabBarController, and set the kerning in viewDidLayoutSubviews.
class FooTabBarController: UITabBarController {
private var tabBarButtons: [UIControl] {
tabBar.subviews.compactMap { $0 as? UIControl }
}
private var tabBarButtonLabels: [UILabel] {
tabBarButtons.compactMap { $0.subviews.first { $0 is UILabel } as? UILabel }
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.tabBarButtonLabels.forEach {
if let attributedText = $0.attributedText {
let mutable = NSMutableAttributedString(attributedString: attributedText)
mutable.addAttribute(.kern, value: 0.5, range: .init(location: 0, length: mutable.length))
$0.attributedText = mutable
$0.sizeToFit()
}
}
}
}
The caveats to this solution are:
It is somewhat fragile. It can break if Apple changes the view structure in the tab bar, ie if they stop using UIControl, or if they change the subview heirarchy.
It isn't all that efficient because the kerning has to be set every layout cycle.
I loved #DoesData's answer, it really helped me out a lot.
Here's a more "swifty" version of it I came up with if it helps anyone:
extension String {
var withAddedSpacing: String {
Array(self)
.compactMap { String($0) }
.joined(separator: "\u{2006}")
}
}

How to lock caret y position in UITextView to create typewriter effect?

I'm trying to force UITextView to keep caret always on the same fixed height, for example in the 1/4 of screen.
I should behave similar to old typewriters - when user presses enter (or reaches end of line) text should scroll one line up and caret should stay in the same y position and jump to the begining of new line.
I was trying to do it like so, but it behaves unexpectedly, caret jumps randomly sometimes and scrolling is visible, it scrolls itself down and then I scroll it up again with scrollRectToVisible, this do not seem like ideal way of doing it.
How can I achieve such effect? Any library or pod with similar functionality would also be much appreciated.
func setScrollToMiddle() {
if let selectedRange = textView.selectedTextRange {
let caretRect = textView.caretRect(for: selectedRange.start)
let middleOfCaretHeight = caretRect.origin.y + (caretRect.height / 2)
let screenHeight = UIScreen.main.bounds.height
guard let kbSize = self.keyboardSize else { return }
let keyboardHeight = kbSize.height
let visibleTextAreaHeight = screenHeight - keyboardHeight - topMenuView.frame.height
let finalRectY = middleOfCaretHeight - topMenuView.frame.height - (visibleTextAreaHeight / 2)
let finalRect = CGRect(x: 0.0, y: finalRectY, width: textView.frame.width, height: visibleTextAreaHeight)
textView.scrollRectToVisible(finalRect, animated: false)
}
}
Here is what I would do:
First set up these in viewDid load
override func viewDidLoad() {
super.viewDidLoad()
textView.delegate = self
textView.isEditable = true
textView.isScrollEnabled = false
}
Then add this extension:
extension ViewController: UITextViewDelegate {
func trackCaret(_ textView:UITextView){
if let selectedRange = textView.selectedTextRange {
let caretRect = textView.caretRect(for: selectedRange.start)
let yPos = caretRect.origin.y - (textView.frame.height/2)
let xPos = caretRect.origin.x - (textView.frame.width/2)
textView.bounds.origin = CGPoint(x: xPos, y: yPos)
}
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
trackCaret(textView)
return true
}
func textViewDidBeginEditing(_ textView: UITextView) {
trackCaret(textView)
}
func textViewDidEndEditing(_ textView: UITextView) {
// If you don't need to move back to the original position you can leave this out
// textView.bounds.origin = CGPoint.zero
//or if you want smooth animated scroll back then
textView.scrollRectToVisible(CGRect(x:0,
y:0,
width: textView.bounds.width,
height: textView.bounds.height),
animated: true)
}
}
With this you get the typewriter effect without jumping all over the place. The didendEditing method is only there to scroll back to origin 0,0. If you don't need to do it just remove it.

Swift: how to make search bar animate when focused? Used to work?

Ok, I feel as though a few versions of Xcode ago this was the default, and I have it working in a past project, just do not know why I cant get this search bar animation to happen with this search bar now in Xcode 9.
What I want to happen is when the search bar ISNT focused, the placeholder text is centered:
Then when its tapped and becomes first responder, the text is left aligned (the transition between the two seems to animate automatically):
I swear search bars used to do this without any customization. How can I achieve this? Right now in my tableview custom header I set up my search bar programmatically like this:
searchBar.placeholder = "Find an Episode."
searchBar.backgroundColor = UIColor.clear
searchBar.searchBarStyle = .minimal
searchBar.layer.shadowOpacity = 0.0
searchBar.barTintColor = thirdColorStr
searchBar.tintColor = thirdColorStr
searchBar.frame = CGRect(x: 0, y: 0, width: self.bounds.width, height: screenSize.height * (40/screenSize.height))
self.addSubview(searchBar)
// searchBar.center = CGPoint(x: self.bounds.width/2, y: (searchBar.bounds.height/2))
searchBar.center = CGPoint(x: self.bounds.width/2, y: (searchBar.bounds.height)*0.8)
searchBar.delegate = listVC
for subView in searchBar.subviews {
for subViewOne in subView.subviews {
if let textField = subViewOne as? UITextField {
subViewOne.backgroundColor = secondColorStr
//print("HEREE:", textField.bounds.width/screenSize.width)
//use the code below if you want to change the color of placeholder
if let t = textField.value(forKey: "placeholderLabel") as? UILabel
{
t.textColor = thirdColorStr
t.font = iphoneFontThin
t.tintColor = thirdColorStr
t.backgroundColor = UIColor.clear
}
// if let clearButton = textField.value(forKey: "_clearButton") as? UIButton {
// // Create a template copy of the original button image
// let img = UIImage(named: "clear")
// clearButton.setImage(img, for: .normal)
//
// // Finally, set the image color
// //clearButton.tintColor = .red
// }
if let t = searchBar.value(forKey: "searchField") as? UITextField
{
t.textColor = barColorStr
t.font = iphoneFontThin
t.backgroundColor = secondColorStr
Then
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
self.searchBar.showsCancelButton = true
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
//searchBar.showsCancelButton = true
resetCategory(newCat: overallCategory)
searchBar.text = ""
searchBar.resignFirstResponder() //kills keyboard
}
I cant find any differences between the code that produces and animation and the one above that does not.

Resources