1

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()

}
jmsapps
  • 388
  • 2
  • 17
  • When you launch the search and the search bar appears at the top, the table does not animate to a new position. So what is the purpose of animating its position when you dismiss the search bar given that it is still in its original position ? I am just trying to understand the end goal. – Shawn Frank Feb 16 '22 at 03:22
  • I’m trying to get it to animate to the top when the search bar goes up, thats the issue I’m having. I want to replicate how it animates on the native iOS weather app, so you can see that for reference of what my aim is. – jmsapps Feb 16 '22 at 04:22

1 Answers1

1

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:

Shawn Frank
  • 4,381
  • 2
  • 19
  • 29
  • I went with the second option because I'm a little too far into the project to implement a UITableViewController now (I think?), but it didn't seem to work out like you showed. I'll post a gif as an answer so you can see what I mean. EDIT: Actually, I'll edit my original post instead. – jmsapps Feb 16 '22 at 14:34
  • @jmsapps - could you update your code changes as well so I can have a look ? Better yet if I can see a minimal version of your project to test on my side, I can help trying to debug your isues. – Shawn Frank Feb 16 '22 at 15:52
  • Oh yeah, of course. I’m not sure how much of the other code is relevant. I have other subViews with apha = 0. When I select on a cell in this tableView it opens up a scrollView with 5 other tableViews with different tags. Do you want to see all of that too or just everything relevant to this tableView and searchController? EDIT: in the mean time ill just post everything relevant to this tableView up above – jmsapps Feb 16 '22 at 16:16
  • @jmsapps - just the tableview stuff should be enough, I'll have a look in a bit and If I figure something out, I'll answer. – Shawn Frank Feb 16 '22 at 17:03
  • Thanks, even if you can’t figure it out I appreciate all the help you’ve given me. – jmsapps Feb 16 '22 at 17:06
  • @jmsapps - have a look at the update I made in my answer towards the end – Shawn Frank Feb 16 '22 at 17:08
  • Oh, my bad I forgot to change that back. I tried view.topAnchor first and it didnt work, so that isn’t the solution – jmsapps Feb 16 '22 at 17:14
  • I'm stumped. I have added the link to my implementation in my answer, check if you notice any clues. In addition, can you try some ideas from here if it helps: https://stackoverflow.com/questions/49098249/uitableview-jumps-between-positions-when-activating-and-deactivating-uisearchcon – Shawn Frank Feb 16 '22 at 17:41
  • @jmsapps If nothing works, you could share your code for this view controller's set up and I could try and run it at my end to see if I get any insights if you wish. – Shawn Frank Feb 16 '22 at 18:01
  • I added a third edit to my post, take a look! It seems I found the buggy area, but don't know exactly whats causing the issue. – jmsapps Feb 16 '22 at 18:53
  • I got it, all I had to do to get it to work was move setupCityViewTableViews() to before setupViews() in viewDidLoad. I'm not sure why that worked, I guess it has something to do with the stacking order of views. Either way I'm glad thats behind me now lol. – jmsapps Feb 16 '22 at 22:12
  • @jmsapps Nice ! It could have something to do with hiding the nav bar or maybe the tableview need to be added before the scrollview so yes probably the stacking order of the views makes a difference. – Shawn Frank Feb 17 '22 at 03:42