swiftanimationuisearchcontrolleruinavigationitemtitleview

Tableview y origin not animating properly when navigationItem.titleView is hidden (Swift 5)


I’m trying to get the tableView to move up when the search bar does. Take a look at the problem:

enter image description here

I think I see what the issue is here, but I can't think of a solution. In SearchResultsUpdating I have an animation block:

func updateSearchResults(for searchController: UISearchController) {

    UIView.animateKeyframes(withDuration: 1, delay: 0, options: UIView.KeyframeAnimationOptions(rawValue: 7)) {

    self.tableView.frame = CGRect(x: 20, y: self.view.safeAreaInsets.top, width: 
    self.view.frame.size.width-40, height: self.view.frame.size.height - 
    self.view.safeAreaInsets.top)

    }
}

It seems to me that the animation block is only receiving the previous coordinates for the y origin, hence it is animating out of sync. I tried adding a target to the tableView, or navigationBar, or the searchBarTextField instead, but nothing worked.

Any help is appreciated, thanks!

EDIT: After implementing Shawn's second suggestion this was the result:

enter image description here

I can't imagine why it isn't animating smoothly now... very frustrating!

EDIT 2 - Requested Code:

class ViewController: UIViewController{

//City TableView
let cityTableView = UITableView()

let searchVC: UISearchController = {
    let searchController = UISearchController(searchResultsController: nil)
    searchController.obscuresBackgroundDuringPresentation = true
    searchController.searchBar.placeholder = "Search"
        return searchController
    }()

//viewDidLoad
override func viewDidLoad() {
    super.viewDidLoad()

    //Do any setup for the view controller here
    setupViews()
    
    //CityViewController
    setupCityViewTableView()
    
}

//setupViews
func setupViews(){
    //NAVIGATIONBAR:
    //title
    title = "Weather"
    //set to hidden because on initial load there is a scroll view layered over top of the CityViewTableView (code not shown here). This gets set to false when the scrollView alpha is set to 0 and the CityViewTableView is revealed
    navigationController?.navigationBar.isHidden = true
    navigationController?.navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
    
    //NAVIGATION ITEM:
    navigationItem.searchController = searchVC
    
    //UISEARCHBARCONTROLLER:
    searchVC.searchResultsUpdater = self
    }
}

//MARK:  -CityViewController Functions
extension ViewController{

//setUp TableView
func setupCityViewTableView(){
    
    cityTableView.translatesAutoresizingMaskIntoConstraints = false
    //set tableView delegate and dataSource
    cityTableView.delegate = self
    cityTableView.dataSource = self
    //background color
    cityTableView.backgroundColor = .black
    //separator color
    cityTableView.separatorColor = .clear
    //is transparent on initial load
    cityTableView.alpha = 0
    //set tag
    cityTableView.tag = 1000
    //hide scroll indicator
    cityTableView.showsVerticalScrollIndicator = false
    //register generic cell
    cityTableView.register(UITableViewCell.self, forCellReuseIdentifier: "cityCell")
    //add subview
    view.addSubview(cityTableView)
    //Auto Layout
    cityTableView.leadingAnchor
        .constraint(equalTo: view.leadingAnchor,
                    constant: 20).isActive = true

    cityTableView.topAnchor
        .constraint(equalTo: view.topAnchor,
                    constant: 0).isActive = true

    cityTableView.trailingAnchor
        .constraint(equalTo: view.trailingAnchor,
                    constant: -20).isActive = true

    cityTableView.bottomAnchor
        .constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor,
                    constant: 0).isActive = true
    
    }
}

//MARK: -TableView Controller
extension ViewController: UITableViewDelegate, 
UITableViewDataSource{

//number of rows
func tableView(_ tableView: UITableView, numberOfRowsInSection 
section: Int) -> Int {
    
    if tableView.tag == 1000{
        return 5
    }
    return self.models[tableView.tag].count
    
}

//cell for row
func tableView(_ tableView: UITableView, cellForRowAt indexPath: 
IndexPath) -> UITableViewCell {
    
    //CityViewController
    if tableView.tag == 1000{
        
        let cell = tableView.dequeueReusableCell(withIdentifier: 
"cityCell", for: indexPath)
        cell.textLabel?.text = "Test"
        cell.textLabel?.textAlignment = .center
        cell.backgroundColor = .systemGray
        cell.selectionStyle = .none
        cell.layer.cornerRadius = 30
        cell.layer.borderColor = UIColor.black.cgColor
        cell.layer.borderWidth = 5
        cell.layer.cornerCurve = .continuous

        return cell
    }
    
    //WeatherViewController
    //code here for scrollView tableViews
}

//Height for row
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    if tableView.tag == 1000{
        return view.frame.size.height/7
    }
    return view.frame.size.height/10
}

//Should Highlight Row
func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
    if tableView.tag == 1000{
        return true
    }
    return false
}

//Did select row
func tableView(_ tableView: UITableView, didSelectRowAt 
indexPath: IndexPath) {
    
    //calls function for segue to Weather Scroll View (not shown)
    if tableView.tag == 1000{
        segueToWeatherView(indexPath: indexPath)
        
        }
    
    }

}

EDIT 3: When I comment out another function it finally works, but I'm not sure exactly why, or how to fix it. This is the function in question, addSubViews()

//setup viewController
func addSubViews(){
    //add weatherView as subView of ViewController
    view.addSubview(weatherView)
    //add subviews to weatherView
    weatherView.addSubview(scrollView)
    weatherView.addSubview(pageControl)
    weatherView.addSubview(segueToCityViewButton)
    weatherView.addSubview(segueToMapViewButton)
    
}

Specifically, it works when I comment out this line:

view.addSubview(weatherView)

Here is all the code concerning the setting up of the weatherView and all of its subViews:

//Any additional setup goes here
private func setupViews(){
    
    //VIEWCONTROLLER:
    //title
    title = "Weather"
    //Background color of view Controller
    view.backgroundColor = .darkGray
    
    //WEATHERVIEW:
    //Background color of weather view Controller
    weatherView.backgroundColor = .clear
    //weatherView frame
    weatherView.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height)
    
    //SCROLLVIEW:
    //background color of scroll view
    scrollView.backgroundColor = .clear
    //scrollView frame
    scrollView.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height)
    //changed
    
    //PAGECONTROL:
    //page control frame
    pageControl.frame = CGRect(x: 0, y: view.frame.height-view.frame.size.height/14, width: view.frame.width, height: view.frame.size.height/14)
    
    //TRANSITIONVIEW:
    //TransitionView frame
    transitionView.frame = CGRect(x: 20, y: 0, width: view.frame.size.width-40, height: view.frame.size.height)
    
    //BUTTONS:
    //segue to CityView
    segueToCityViewButton.frame = CGRect(x: (weatherView.frame.width/5*4)-20, y: weatherView.frame.height-weatherView.frame.size.height/14, width: weatherView.frame.width/5, height: pageControl.frame.height)
    //segue to MapView:
    segueToMapViewButton.frame = CGRect(x: 20, y: weatherView.frame.height-weatherView.frame.size.height/14, width: weatherView.frame.width/5, height: pageControl.frame.height)
    
    //LABELS:
    transitionViewLabel.frame = transitionView.bounds
    
    //NAVIGATIONBAR:
    //set to hidden on initial load
    navigationController?.navigationBar.isHidden = true
    navigationController?.navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
    
    //NAVIGATION ITEM:
    navigationItem.searchController = searchVC
    
    //UISEARCHBARCONTROLLER:
    searchVC.searchResultsUpdater = self
    
    
}

For the sake of being thorough, here is the full viewDidLoad() Function:

override func viewDidLoad() {
    super.viewDidLoad()

    //MARK: View Controller
    
    //These two will eventually be moved to the DispatchQueue in APICalls.swift
    configureScrollView()
    pageControl.numberOfPages = models.count
    
    
    //Do any setup for the view controller here
    setupViews()
    
    //setup ViewController
    addSubViews()

    //Add Target for the pageControl
    addTargetForPageControl()
    
    //MARK: CityViewController
    setupCityViewTableViews()
    
}

EDIT 4: With the following changes in viewDidLoad(), I finally got it to work!

override func viewDidLoad() {
super.viewDidLoad()

//MARK: CityViewController
//Moved to a position before setting up the other views
setupCityViewTableViews()

//MARK: View Controller

//These two will eventually be moved to the DispatchQueue in APICalls.swift
configureScrollView()
pageControl.numberOfPages = models.count

//Do any setup for the view controller here
setupViews()

//setup ViewController
addSubViews()

//Add Target for the pageControl
addTargetForPageControl()

}

Solution

  • Doing it the way you are doing it right now is a way to do it but I think it is the most challenging way to do it for several reasons:

    1. You don't have much control and access to the implementation of the search controller animation within the navigation bar so getting the right coordinates might be a task

    2. Even if you did manage to get the right coordinates, trying to synchronize your animation frames and timing to look in sync and seamless with the search animation on the nav bar will be tricky

    I suggest the 2 following alternatives to what you are currently doing where you will get the news experience pretty much for free out of the box.

    Option 1: Use a UITableViewController instead of a UIViewController

    This is all the code using a UITableViewController and adding a UISearchController to the navigation bar.

    class NewsTableViewVC: UITableViewController
    {
        private let searchController: UISearchController = {
            let sc = UISearchController(searchResultsController: nil)
            sc.obscuresBackgroundDuringPresentation = false
            sc.searchBar.placeholder = "Search"
            sc.searchBar.autocapitalizationType = .allCharacters
            return sc
        }()
        
        override func viewDidLoad()
        {
            super.viewDidLoad()
            
            view.backgroundColor = .black
            
            title = "Weather"
            
            // Ignore this as you have you own custom cell class
            tableView.register(CustomCell.self,
                               forCellReuseIdentifier: CustomCell.identifier)
            
            setUpNavigationBar()
        }
        
        private func setUpNavigationBar()
        {
            navigationItem.searchController = searchController
        }
    }
    

    This is the experience you can expect

    UITableViewController search controller animation search bar news app iOS swift

    Option 2: Use auto layouts rather than frames to configure your UITableView

    If you don't want to use a UITableViewController, configure your UITableView using auto layout rather than frames which has a little more work but not too much:

    class NewsTableViewVC: UIViewController, UITableViewDataSource, UITableViewDelegate
    {
        private let searchController: UISearchController = {
            let sc = UISearchController(searchResultsController: nil)
            sc.obscuresBackgroundDuringPresentation = false
            sc.searchBar.placeholder = "Search"
            sc.searchBar.autocapitalizationType = .allCharacters
            return sc
        }()
        
        private let tableView = UITableView()
        
        override func viewDidLoad()
        {
            super.viewDidLoad()
            
            // Just to show it's different from the first
            view.backgroundColor = .purple
            
            title = "Weather"
            
            setUpNavigationBar()
            setUpTableView()
        }
        
        private func setUpNavigationBar()
        {
            navigationItem.searchController = searchController
        }
        
        private func setUpTableView()
        {
            tableView.translatesAutoresizingMaskIntoConstraints = false
            tableView.register(CustomCell.self,
                               forCellReuseIdentifier: CustomCell.identifier)
            tableView.dataSource = self
            tableView.delegate = self
            tableView.backgroundColor = .clear
            view.addSubview(tableView)
            
            // Auto Layout
            tableView.leadingAnchor
                .constraint(equalTo: view.leadingAnchor,
                            constant: 0).isActive = true
            
            // This important, configure it to the top of the view
            // NOT the safe area margins to get the desired result
            tableView.topAnchor
                .constraint(equalTo: view.topAnchor,
                            constant: 0).isActive = true
            
            tableView.trailingAnchor
                .constraint(equalTo: view.trailingAnchor,
                            constant: 0).isActive = true
            
            tableView.bottomAnchor
                .constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor,
                            constant: 0).isActive = true
        }
    }
    

    You can expect the following experience:

    UITableView search bar animation search controller news app iOS swift

    Update

    This is based on your updated code, you missed one small detail which might be impacting the results you see and this is the top constraint of the UITableView.

    You added the constraint to the safeAreaLayoutGuide top anchor:

    cityTableView.topAnchor
            .constraint(equalTo: view.safeAreaLayoutGuide.topAnchor,
                        constant: 0).isActive = true
    

    My recommendation from the code above if you notice is to add it to the view top constraint

    // This important, configure it to the top of the view
    // NOT the safe area margins to get the desired result
    cityTableView.topAnchor
        .constraint(equalTo: view.topAnchor,
                    constant: 0).isActive = true
    

    Give this a go and see if you come close to getting what you expect ?

    Here is a link to the complete code of my implementation if it helps: