Custom animation for UISearchController? - ios

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.

Related

How to add a straight line into UISearchBar next to right Search icon?

I already had a UISearchBar (search icon is bookmarkButton inside searchTextField) like that:
Search Bar
searchBar code
private func setupSearchBar() {
searchBar.translatesAutoresizingMaskIntoConstraints = false
searchBar.backgroundImage = UIImage()
searchBar.setImage(UIImage(), for: .search, state: .normal)
searchBar.showsBookmarkButton = true
searchBar.setImage(UIImage(systemName: "magnifyingglass")?.withTintColor(self.darklightcolor, renderingMode: .alwaysOriginal).applyingSymbolConfiguration(.init(pointSize: 22)), for: .bookmark, state: .normal)
cancelButtonSearchBar = UIBarButtonItem.appearance(whenContainedInInstancesOf: [UISearchBar.self])
cancelButtonSearchBar.tintColor = .systemPink
searchBar.layer.borderColor = darklightcolor.cgColor
searchBar.layer.borderWidth = 1.5
searchBar.layer.cornerRadius = 19
searchBar.layer.backgroundColor = UIColor.clear.cgColor
searchBar.searchTextField.backgroundColor = .clear
searchBar.delegate = self
view.addSubview(searchBar)
}
Now I want to add a gray line to searchBar next to search icon like this
But I cant find anyway to add that line. Can anyone help me?
That separator is not part of UISearchbar. You should not play with the internal view hierarchy of the searchbar since it may change and it is going to break your implementation. There is also something in your code that raises a flag, setting the appearance() for buttons when contained in UISearchbars may lead to unintended tinting of buttons in other searchbars within the app. As a practice if you want to set it for all, I would suggest applying those appearance modifications in a single place, otherwise you will end up looking everywhere in your code for the one place where you set the value.
The best approach if that design is so important would be to implement something custom, at that point, it would be up to you to make the hierarchy and you could modify the hierarchy because it was created by you.
That being said, if you still want to continue with this approach, you may add this extension:
extension UISearchBar {
var cancelButtonView: UIView? {
self.searchTextField
.superview?
.subviews
.first(where: { $0.description.contains("Button") })
}
}
And use it when setting constraints for the view you've added.

When implementing custom view controller presentations, where to apply presented view's constraints?

When presenting a view controller using a custom animation, none of Apple's documentation or example code mentions or includes constraints, beyond the following:
// Always add the "to" view to the container.
// And it doesn't hurt to set its start frame.
[containerView addSubview:toView];
toView.frame = toViewStartFrame;
The problem is that the double-height status bar is not recognized by custom-presented view controllers (view controllers that use non-custom presentations don't have this problem). The presented view controller is owned by the transition's container view, which is a temporary view provided by UIKit that we have next to no dominion over. If we anchor the presented view to that transient container, it only works on certain OS versions; not to mention, Apple has never suggested doing this.
UPDATE 1: There is no way to consistently handle a double-height status bar with custom modal presentations. I think Apple botched it here and I suspect they will eventually phase it out.
UPDATE 2: The double-height status bar has been phased out and no longer exists on non-edge-to-edge devices.
My answer is: You should not use constraints in case of custom modal presentations
Therefore I know your pain, so I will try to help you to save time and effort by providing some hints which I suddenly revealed.
Example case:
Card UI animation like follows:
Terms for further use:
Parent - UIViewController with "Detail" bar button item
Child - UIViewController with "Another"
Troubles you mentioned began, when my animation involved size change along with the movement. It causes different kinds of effects including:
Parent's under-status-bar area appeared and disappeared
Parent's subviews were animated poorly - jumps, duplication and other glitches.
After few days of debugging and searching I came up with the following solution (sorry for some magic numbers ;)):
UIView.animate(withDuration: transitionDuration(using: transitionContext),
delay: 0,
usingSpringWithDamping: 1,
initialSpringVelocity: 0.4,
options: .curveEaseIn, animations: {
toVC.view.transform = CGAffineTransform(translationX: 0, y: self.finalFrame.minY)
toVC.view.frame = self.finalFrame
toVC.view.layer.cornerRadius = self.cornerRadius
fromVC.view.layer.cornerRadius = self.cornerRadius
var transform = CATransform3DIdentity
transform = CATransform3DScale(transform, scale, scale, 1.0)
transform = CATransform3DTranslate(transform, 0, wdiff, 0)
fromVC.view.layer.transform = transform
fromVC.view.alpha = 0.6
}) { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
Main point here is, that You have to use CGAffineTransform3D to avoid animation problems and problems with subviews animation (2D Transforms are not working for unknown reasons).
This approach fixes, I hope, all your problems without using constraints.
Feel free to ask questions.
UPD: According to In-Call status bar
After hours of all possible experiments and examining similar projects like this and this and stackoverflow questions like this, this (it's actually fun, OPs answer is there) and similar I am totally confused. Seems like my solution handles Double status bar on UIKit level (it adjusts properly), but the same movement is ignoring previous transformations. The reason is unknown.
Code samples:
You can see the working solution here on Github
P.S. I'm not sure if it's ok to post a GitHub link in the answer. I'd appreciate for an advice how to post 100-300 lines code In the answer.
I've been struggling with double-height statusBar in my current project and I was able to solve almost every issue (the last remaining one is a very strange transformation issue when the presentingViewController is embedded inside a UITabBarController).
When the height of the status bar changes, a notification is posted.
Your UIPresentationController subclass should subscribe to that particular notification and adjust the frame of the containerView and its subviews:
UIApplication.willChangeStatusBarFrameNotification
Here is an example of code I'm using:
final class MyCustomPresentationController: UIPresentationController {
// MARK: - StatusBar
private func subscribeToStatusBarNotifications() {
let notificationName = UIApplication.willChangeStatusBarFrameNotification
NotificationCenter.default.addObserver(self, selector: #selector(statusBarWillChangeFrame(notification:)), name: notificationName, object: nil)
}
#objc private func statusBarWillChangeFrame(notification: Notification?) {
if let newFrame = notification?.userInfo?[UIApplication.statusBarFrameUserInfoKey] as? CGRect {
statusBarWillChangeFrame(to: newFrame)
} else {
statusBarWillChangeFrame(to: .zero)
}
}
func statusBarWillChangeFrame(to newFrame: CGRect) {
layoutContainerView(animated: true)
}
// MARK: - Object Lifecycle
deinit {
// Unsubscribe from all notifications
NotificationCenter.default.removeObserver(self)
}
// MARK: - Layout
/// Called when the status-bar is about to change its frame.
/// Relayout the containerView and its subviews
private func layoutContainerView(animated: Bool) {
guard let containerView = self.containerView else { return }
// Retrieve informations about status-bar
let statusBarHeight = UIApplication.shared.statusBarFrame.height
let normalStatusBarHeight = Constants.Number.statusBarNormalHeight // 20
let isStatusBarNormal = statusBarHeight ==~ normalStatusBarHeight
if animated {
containerView.frame = …
updatePresentedViewFrame(animated: true)
} else {
// Update containerView frame
containerView.frame = …
updatePresentedViewFrame(animated: false)
}
}
func updatePresentedViewFrame(animated: Bool) {
self.presentedView?.frame = …
}
}

Large Navigation Bar Text With Multiple Colors

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.

Search bar as header in tableview - appear and disappear

I need to put a search bar at the top of my tableview. I am making a network call and when the results are greater than 100 I want to search bar to appear and when they are less than 100 I don't want to search bar to appear. The tableview is on the right side of the VC and does not take up the whole view controller. I want the search bar to be at the top of the table view as a header.
I cannot use a search controller because in iOS 11, using a search controller makes the search bar pop to the top of the VC when it is active.
I tried to set the tableviewheader to nil to get it to disappear. But I can't get it back obviously because I made the header nil.
self.rightDetailFilterTableView.tableHeaderView = nil
self.rightDetailFilterTableView.sectionHeaderHeight = 0
I have put the search bar into the storyboard as seen in the image below. Is this the right way to add the search bar as a header?
What is the best way to get it to appear and disappear in the tableview? I have tried a bunch of different methods. They all either leave a blank header or do something else that causes problems. I also tried using the header delegate methods but that still did not work.
I am not using a tableview controller, I am using a normal VC. I am also not using a search bar controller because of issues it causes in iOS 11.
Here's what I've done in one of my recent project. First, laid out my views like so:
That is, the Search Bar was added to the parent view rather than the table view. This allows me to hide/show it as needed.
Next, I've defined two optional layout constraint, one ensuring that the tableview is aligned to the top of the safe area, priority 750; the other aligning the top of the search bar to the top of the safe area; priority lower than 750 to hide it below the nav bar or priority higher than 750 to reveal it and push the table view down.
In code, I created a #IBOutlet to the layout constraint for the search bar to the top of the safe area, and I change its priority as needed:
#IBAction
func toggleSearchBar(_ sender: Any?) {
if searchBarVisibleLayoutConstraint.priority.rawValue > 750.0 {
searchBarVisibleLayoutConstraint.priority = UILayoutPriority(rawValue: 1.0)
searchBar?.endEditing(true)
} else {
searchBarVisibleLayoutConstraint.priority = UILayoutPriority(rawValue: 999.0)
}
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
In my case, the navigation bar is opaque and the search bar is not visible behind it. Your case may be different so you may also want to either clip the parent view or alpha fade the search bar when it is not visible.
Good luck!
Please check :
Created IBOutlet for my SearchBar.
#IBOutlet weak var testbar: UISearchBar!
And in my viewDidLoad :
override func viewDidLoad() {
var contentOffset = tableView.contentOffset
let showSearchBar = (results.count > 100)
self.tableView.tableHeaderView?.isHidden = !(showSearchBar)
if showSearchBar {
contentOffset.y -= testbar.frame.size.height
} else {
contentOffset.y += testbar.frame.size.height
}
tableView.contentOffset = contentOffset
}
Here is my tableview storyboard

Swift - Custom SearchController and SearchBar

I am trying to recreate the search field as seen in the Yahoo Finance app. I followed an online tutorial for customizing the UISearchBar and UISearchController, however I still have some problems. If any of you could open up my project and see where Im going wrong / where I need to add these lines with even one of these that would be really great.
My attempted solution project can be found here: https://github.com/jordanw421/yahoofinance
1) How to get the search bar active (with text to the left, and typing indicator blinking) as soon as view presents itself? In the link below, you can see what I am talking about, when the button is pressed the search view is loaded and the search bar is instantly active.
https://youtu.be/tRtXm-m1hX0
I tried using:
customSearchController.definesPresentationContext = true
customSearchController.isActive = true
customSearchController.searchBar.becomeFirstResponder()
but that didn't work. Should I be setting these in the initial view controller with prepareForSegue?
2) How to set the keyboard appearance (to dark), and to add a keyboard toolbar with a button?
I was able to get this working for a non-custom search bar, but for some reason these don't work now:
customSearchController.searchBar.keyboardAppearance = .dark
and
func addKeyboardButton() {
let keyboardToolbar = UIToolbar()
keyboardToolbar.sizeToFit()
keyboardToolbar.isTranslucent = false
keyboardToolbar.barTintColor = UIColor.blue
let addButton = UIButton(type: .custom)
addButton.frame = CGRect(x: keyboardToolbar.frame.size.width / 2, y: keyboardToolbar.frame.size.height / 2, width: 50, height: 30)
addButton.addTarget(self, action: #selector(clickMe), for: .touchUpInside)
let item = UIBarButtonItem(customView: addButton)
keyboardToolbar.items = [item]
customSearchController.searchBar.inputAccessoryView = keyboardToolbar
}
and calling,
addKeyboardButton()
in the search bar configure function.
3) How to prevent the search bar / table view header from scrolling, but still allow the tableView to scroll?
If you look at my attempted solution you can see that for some reason the tableview header scrolls with the table view. When I use a non-custom search bar the header remains static.
I know there are a lot of questions here, but I've been stuck on this for awhile and could really use some help. Thank you.

Resources