I’m trying to get the tableView to move up when the search bar does. Take a look at the problem:
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:
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()
}
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:
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
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
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:
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: