iOS 13 Navigation Bar Large Title Issue - ios

I am trying to show a large Title in a Navigation bar, but with clear background. When scrolled up, it will be the Navigation bar with a blur effect.
This looks correct, however, when scrolling, the animation seems to be broken. Also, transition gets stuck from time to time:
My code as follows:
UINavigationController:
override func viewDidLoad() {
super.viewDidLoad()
if #available(iOS 13.0, *) {
self.navigationBar.prefersLargeTitles = true
let style = UINavigationBarAppearance()
style.configureWithDefaultBackground()
style.titleTextAttributes = [.font: UIFont.systemFont(ofSize: 18)]
self.navigationBar.standardAppearance = style
self.navigationBar.compactAppearance = style
//Configure Large Style
let largeStyle = UINavigationBarAppearance()
largeStyle.configureWithTransparentBackground()
largeStyle.largeTitleTextAttributes = [.font: UIFont.systemFont(ofSize: 28)]
self.navigationBar.scrollEdgeAppearance = largeStyle
}
}
The UITableView is inside the UINavigationController. Both are from storyboards via a segue way.

Instead of UITableview, You can try using UITableViewController. I have tried using UITableViewController and its working fine for me. Please check the following design and code.
class ViewController: UITableViewController
{
override func viewDidLoad() {
super.viewDidLoad()
if #available(iOS 13.0, *) {
self.navigationController?.navigationBar.prefersLargeTitles = true
let style = UINavigationBarAppearance()
style.configureWithDefaultBackground()
style.titleTextAttributes = [.font: UIFont.systemFont(ofSize: 18)]
self.navigationController?.navigationBar.standardAppearance = style
self.navigationController?.navigationBar.compactAppearance = style
//Configure Large Style
let largeStyle = UINavigationBarAppearance()
largeStyle.configureWithTransparentBackground()
largeStyle.largeTitleTextAttributes = [.font: UIFont.systemFont(ofSize: 28)]
self.navigationController?.navigationBar.scrollEdgeAppearance = largeStyle
}
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
// Do any additional setup after loading the view.
}
override func numberOfSections(in tableView: UITableView) -> Int {
1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = "\(indexPath.row)"
return cell
}
}
Output:-

In view debugger, check if navigation title goes beyond navigation bar bounds.
If that's the case then :
let titleHeight = UIFont.systemFont(ofSize: 28).lineHeight
if titleHeight > self.view.frame.size.height {
self.navigationController.navigationBar.frame = CGRectMake(0, 0, self.view.frame.size.width, titleHeight + topAndBottomPadding)
}

hey How are you here is your code try to remove this line
largeStyle.configureWithTransparentBackground()
you have to configure with white background
your code:
override func viewDidLoad() {
super.viewDidLoad()
if #available(iOS 13.0, *) {
self.navigationBar.prefersLargeTitles = true
let style = UINavigationBarAppearance()
style.configureWithDefaultBackground()
style.titleTextAttributes = [.font: UIFont.systemFont(ofSize: 18)]
self.navigationBar.standardAppearance = style
self.navigationBar.compactAppearance = style
//Configure Large Style
let largeStyle = UINavigationBarAppearance()
largeStyle.configureWithTransparentBackground()
largeStyle.largeTitleTextAttributes = [.font: UIFont.systemFont(ofSize: 28)]
self.navigationBar.scrollEdgeAppearance = largeStyle
}
}

changing properties via storyboard night i’ve some insights. usually when i’m stuck i reflect same
changes that i made programmatically to storyboard and the things left in code are usually the ones causing my bug. try this out if possible

Related

Expandable custom UITableViewCell for iOS 11+

I realize that many people have asked this question in various forms and the answers are all over the page, so let me summarize my specific situation in hopes of getting more specific answers. First of all, I'm building for iOS 11+ and have a relatively recent version of XCode (11+). Maybe not the latest, but recent enough.
Basically, I need a self-sizing tableview where the cells may expand and collapse at runtime when the user interacts with them. In viewDidLoad I set the rowHeight to UITableView.automaticDimension and estimatedRowHeight to some number that's bigger than the canned value of 44. But the cell is not expanding like it should, even though I seem to have tried every bit of advice in the book.
If that matters, I have a custom class for the table cell but no .XIB file for it - the UI is defined directly in the prototype. I've tried a number of other variations, but it feels like the easiest is making a UIStackView the only direct child of the prototype (the "revenue" features so to speak would all be inside it. In my case, they include a label and another tableview - I nest 3 levels deep - but that's probably beside the point) and constraining all 4 of it's edges to the parent. I've tried that, and I've tinkered with the distribution in the stack view (Fill, Fill Evenly, Fill Proportionately), but none of it seems to work. What can I do to make the cells expand properly?
In case anyone's wondering, I used to override heightForRowAt but now I don't because it's not easy to predict the height at runtime and I'm hoping the process could be automated.
Start with the basics...
Here is a vertical UIStackView with two labels:
The red outline shows the frame of the stack view.
If we tap the button, it will set bottomLabel.isHidden = true:
Notice that in addition to being hidden, the stack view removes the space it was occupying.
Now, we can do that with a stack view in a table view cell to get expand/collapse functionality.
We'll start with every-other row expanded:
Now we tap the "Collapse" button for row 1 and we get:
Not quite what we want. We successfully "collapsed" the cell content, but the table view doesn't know anything about it.
So, we can add a closure... when we tap the button, the code in the cell will show/hide the bottom label AND it will use the closure to tell the table view what happened. Our cellForRowAt func looks like this:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath) as! ExpColCell
c.setData("Top \(indexPath.row)", str2: "Bottom \(indexPath.row)\n2\n3\n4\n5", isCollapsed: isCollapsedArray[indexPath.row])
c.didChangeHeight = { [weak self] isCollapsed in
guard let self = self else { return }
// update our data source
self.isCollapsedArray[indexPath.row] = isCollapsed
// tell the tableView to re-run its layout
self.tableView.performBatchUpdates(nil, completion: nil)
}
return c
}
and we get:
Here's a complete example:
Simple "dashed outline view"
class DashedOutlineView: UIView {
#IBInspectable var dashColor: UIColor = .red
var shapeLayer: CAShapeLayer!
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
shapeLayer = self.layer as? CAShapeLayer
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = 1.0
shapeLayer.lineDashPattern = [8,8]
}
override func layoutSubviews() {
super.layoutSubviews()
shapeLayer.strokeColor = dashColor.cgColor
shapeLayer.path = UIBezierPath(rect: bounds).cgPath
}
}
The cell class
class ExpColCell: UITableViewCell {
public var didChangeHeight: ((Bool) -> ())?
private let stack = UIStackView()
private let topLabel = UILabel()
private let botLabel = UILabel()
private let toggleButton = UIButton()
private let outlineView = DashedOutlineView()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// button properties
toggleButton.translatesAutoresizingMaskIntoConstraints = false
toggleButton.backgroundColor = .systemBlue
toggleButton.setTitleColor(.white, for: .normal)
toggleButton.setTitleColor(.gray, for: .highlighted)
toggleButton.setTitle("Collapse", for: [])
// label properties
topLabel.text = "Top Label"
botLabel.text = "Bottom Label"
topLabel.font = .systemFont(ofSize: 32.0)
botLabel.font = .italicSystemFont(ofSize: 24.0)
topLabel.backgroundColor = .green
botLabel.backgroundColor = .systemTeal
botLabel.numberOfLines = 0
// outline view properties
outlineView.translatesAutoresizingMaskIntoConstraints = false
// stack view properties
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .vertical
stack.spacing = 8
// add the labels
stack.addArrangedSubview(topLabel)
stack.addArrangedSubview(botLabel)
// add outlineView, stack view and button to contentView
contentView.addSubview(outlineView)
contentView.addSubview(stack)
contentView.addSubview(toggleButton)
// we'll use the margin guide
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: g.topAnchor),
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
outlineView.topAnchor.constraint(equalTo: stack.topAnchor),
outlineView.leadingAnchor.constraint(equalTo: stack.leadingAnchor),
outlineView.trailingAnchor.constraint(equalTo: stack.trailingAnchor),
outlineView.bottomAnchor.constraint(equalTo: stack.bottomAnchor),
toggleButton.topAnchor.constraint(equalTo: g.topAnchor),
toggleButton.trailingAnchor.constraint(equalTo: g.trailingAnchor),
toggleButton.leadingAnchor.constraint(equalTo: stack.trailingAnchor, constant: 16.0),
toggleButton.widthAnchor.constraint(equalToConstant: 92.0),
])
// we set the bottomAnchor constraint like this to avoid intermediary auto-layout warnings
let c = stack.bottomAnchor.constraint(equalTo: g.bottomAnchor)
c.priority = UILayoutPriority(rawValue: 999)
c.isActive = true
// set label Hugging and Compression to prevent them from squeezing/stretching
topLabel.setContentHuggingPriority(.required, for: .vertical)
topLabel.setContentCompressionResistancePriority(.required, for: .vertical)
botLabel.setContentHuggingPriority(.required, for: .vertical)
botLabel.setContentCompressionResistancePriority(.required, for: .vertical)
contentView.clipsToBounds = true
toggleButton.addTarget(self, action: #selector(toggleButtonTapped), for: .touchUpInside)
}
func setData(_ str1: String, str2: String, isCollapsed: Bool) -> Void {
topLabel.text = str1
botLabel.text = str2
botLabel.isHidden = isCollapsed
updateButtonTitle()
}
func updateButtonTitle() -> Void {
let t = botLabel.isHidden ? "Expand" : "Collapse"
toggleButton.setTitle(t, for: [])
}
#objc func toggleButtonTapped() -> Void {
botLabel.isHidden.toggle()
updateButtonTitle()
// comment / un-comment this line to see the difference
didChangeHeight?(botLabel.isHidden)
}
}
and a table view controller to demonstrate
class ExpColTableViewController: UITableViewController {
var isCollapsedArray: [Bool] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(ExpColCell.self, forCellReuseIdentifier: "c")
// 16 "rows" start with every-other row collapsed
for i in 0..<15 {
isCollapsedArray.append(i % 2 == 0)
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return isCollapsedArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath) as! ExpColCell
c.setData("Top \(indexPath.row)", str2: "Bottom \(indexPath.row)\n2\n3\n4\n5", isCollapsed: isCollapsedArray[indexPath.row])
c.didChangeHeight = { [weak self] isCollapsed in
guard let self = self else { return }
// update our data source
self.isCollapsedArray[indexPath.row] = isCollapsed
// tell the tableView to re-run its layout
self.tableView.performBatchUpdates(nil, completion: nil)
}
return c
}
}

Swift 5 | UINavigationController not appearing until going to DetailsViewController

I'm working on rebuilding my app programmatically instead of using storyboards and running into issues with my UINavigationController. When the app first loads the root ViewController, the bar containing "carrier" and the time is a light version of the color that I've assigned and the search bar exists instead of a normal navigation bar.
When I tap/click a cell on my UITableView to get to the details screen then go back, my UINavigationBar appears correctly but the search bar disappears. I'm sure I've confused something but I'm not sure where I messed up or if I am missing something entirely. Any and all help would be greatly appreciated. I've also included an image as an example of whats going on.
Nonexistant UINavigationController > Detail screen > Back.gif
I have setup a TabBarController in the SceneDelegate
SceneDelegate
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// MARK: - Properties
var window: UIWindow?
let tabBarDelegate = TabBarDelegate()
let userAuthToken = UserDefaults.standard.string(forKey: "token")
let userKeyToken = UserDefaults.standard.string(forKey: "key")
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Disable dark mode
if #available(iOS 13.0, *) {
window?.overrideUserInterfaceStyle = .light
}
// IF USER IS LOGGED IN
if let windowScene = (scene as? UIWindowScene) {
if let _ = userAuthToken {
self.window = UIWindow(windowScene: windowScene)
// CREATE TAB BAR //
let tabController = UITabBarController()
tabController.tabBar.backgroundColor = .white
// Instantiate the storyboards
let cannabisStoryboard = UIStoryboard(name: "Cannabis", bundle: nil)
let profileStoryboard = UIStoryboard(name: "Profile", bundle: nil)
// Instantiate the view controllers to storyboards
let cannabisVC = cannabisStoryboard.instantiateViewController(withIdentifier: "Cannabis") as! CannabisViewController
let profileVC = profileStoryboard.instantiateViewController(withIdentifier: "Profile") as! ProfileViewController
// Displays the items in below order in tab bar
let vcData: [(UIViewController, UIImage, UIImage)] = [
(cannabisVC, UIImage(named: "Cannabis_icon")!, UIImage(named: "Cannabis_icon_selected")!),
(profileVC, UIImage(named: "Profile_icon")!, UIImage(named: "Profile_icon_selected")!),
]
let vcs = vcData.map { (vc, defaultImage, selectedImage) -> UINavigationController in
let nav = UINavigationController(rootViewController: vc)
nav.tabBarItem.image = defaultImage
nav.tabBarItem.selectedImage = selectedImage
return nav
}
// Assign to tab bar controller
tabController.viewControllers = vcs
tabController.tabBar.isTranslucent = false
tabController.delegate = tabBarDelegate
// Disables rendering for tab bar images
if let items = tabController.tabBar.items {
for item in items {
if let image = item.image {
item.image = image.withRenderingMode(UIImage.RenderingMode.alwaysOriginal)
}
if let selectedImage = item.selectedImage {
item.selectedImage = selectedImage.withRenderingMode(UIImage.RenderingMode.alwaysOriginal)
}
// Hides title
item.imageInsets = UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0)
}
}
// Customize Navigation bar
UINavigationBar.appearance().backgroundColor = UIColor(rgb: 0x00ffcc)
// Disable dark mode
window!.overrideUserInterfaceStyle = .light
window?.rootViewController = tabController
window?.makeKeyAndVisible()
} else {
// let loginStoryboard = UIStoryboard(name: "Login", bundle: nil)
// let loginViewController = loginStoryboard.instantiateViewController(withIdentifier: "Login") as! LoginViewController
// Disable dark mode
window!.overrideUserInterfaceStyle = .light
// window?.rootViewController = loginViewController
window?.rootViewController = LoginViewController()
window?.makeKeyAndVisible()
}
window?.windowScene = windowScene
}
}
Root ViewController (stripped down to relevant info)
class CannabisViewController: UIViewController {
// MARK:- Outlets
let tableView = UITableView()
// MARK:- Properties
var cannabisDetailViewController: CannabisDetailsViewController? = nil
// Search
let searchController = UISearchController(searchResultsController: nil)
var isSearchBarEmpty: Bool { return searchController.searchBar.text?.isEmpty ?? true }
var filteredStrains = [Cannabis]()
var isFiltering: Bool { return searchController.isActive && !isSearchBarEmpty }
// MARK: - ViewWillLayoutSubViews
override func viewWillLayoutSubviews() {
let navigationBar: UINavigationBar = UINavigationBar(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 44))
// navigationController?.setViewControllers([CannabisViewController()], animated: true)
self.view.addSubview(navigationBar)
}
// MARK: - ViewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
configurePage()
// MARK: Self sizing table view cell
tableView.estimatedRowHeight = CGFloat(88.0)
tableView.rowHeight = UITableView.automaticDimension
// MARK: DataSource/Delegate
tableView.dataSource = self
tableView.delegate = self
// Removes default lines from table views
tableView.tableFooterView = UIView()
tableView.separatorStyle = .none
// MARK: Navigation: logo in center
let logoHeader = UIImageView(image: UIImage(named: "logoHeader"))
self.navigationItem.titleView = logoHeader
// MARK: API
getCannabisList()
// MARK: Search bar controller
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search for a strain!"
navigationItem.searchController = searchController
definesPresentationContext = true
}
// Configure TableView
func configurePage() {
// Configure Tableview
view.addSubview(tableView)
tableView.anchor(top: view.topAnchor, left: view.leftAnchor,
bottom: view.bottomAnchor, right: view.rightAnchor)
}
}
SearchController (Still in the root ViewController)
func updateSearchResults(for searchController: UISearchController) {
let searchBar = searchController.searchBar
let userSearch = searchBar.text!.trimmingCharacters(in: .whitespaces)
search(searchText: userSearch)
}
I can see a couple of issues here, but first to initialize your window in the SceneDelegate you would use the UIWindowScene:
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
self.window = window
let rootViewController = RootViewController()
window.rootViewController = UINavigationController(rootViewController: rootViewController)
window.makeKeyAndVisible()
}
If you want to disable Dark Mode globally in your app, simply add a key UIUserInterfaceStyle to your Info.plist and set its value to Dark (or Light). By doing this, you won't need to update each view controller because it will overwrite the global app default style. I highly encourage you to add support for Dark Mode though!
If you want to change your navigation bar appearance:
if #available(iOS 13.0, *) {
let navigationAppearance = UINavigationBarAppearance()
navigationAppearance.configureWithOpaqueBackground()
navigationAppearance.backgroundColor = .white
navigationAppearance.titleTextAttributes = // ...
navigationAppearance.largeTitleTextAttributes = // ...
UINavigationBar.appearance().tintColor = .systemBlue
UINavigationBar.appearance().barTintColor = .white
UINavigationBar.appearance().standardAppearance = navigationAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navigationAppearance
} else {
UINavigationBar.appearance().backgroundColor = .white
UINavigationBar.appearance().barTintColor = .white
UINavigationBar.appearance().tintColor = .systemBlue
UINavigationBar.appearance().titleTextAttributes = // ...
UINavigationBar.appearance().largeTitleTextAttributes = // ...
}
I made a minimal project for you to see a working example with the search bar, in which the flickering of the status bar doesn't happen, and the UISearchBar's hide/show animation works properly when pushing/popping your DetailViewController:
RootViewController:
import UIKit
class RootViewController: UIViewController {
private let reuseIdentifier = "reuseIdentifier"
lazy var tableView: UITableView = {
$0.delegate = self
$0.dataSource = self
$0.register(UITableViewCell.self, forCellReuseIdentifier: reuseIdentifier)
return $0
}(UITableView(frame: .zero, style: .grouped))
private lazy var searchController: UISearchController = {
$0.searchResultsUpdater = self
$0.delegate = self
$0.searchBar.delegate = self
$0.obscuresBackgroundDuringPresentation = false
$0.hidesNavigationBarDuringPresentation = false
$0.searchBar.backgroundColor = .white
$0.searchBar.tintColor = .systemBlue
return $0
}(UISearchController(searchResultsController: nil))
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
setupConstraints()
}
func setupViews() {
title = "Source"
view.backgroundColor = .white
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
view.addSubview(tableView)
// ...
}
func setupConstraints() {
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
}
}
extension RootViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let detailViewController = DetailViewController()
navigationController?.pushViewController(detailViewController, animated: true)
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return nil
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return CGFloat.leastNonzeroMagnitude
}
}
extension RootViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath)
cell.textLabel?.text = "Cell at indexPath \(indexPath)"
return cell
}
}
extension RootViewController: UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate {
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {}
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {}
func updateSearchResults(for searchController: UISearchController) {}
}
DetailViewController:
class DetailViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
title = "Detail"
view.backgroundColor = .systemGray6
}
}

Self-sizing cells with UICollectionViewCompositionalLayout

I have a view controller that displays a collection view with self-sizing cells. The collection view has one section that scrolls horizontally. It looks like this:
Problem
The collection view behaves unexpectedly when the view controller is presented using the default pageSheet style on iOS 13+.
When pulling upward on the sheet, cells may appear to resize like the cell labeled "Rectify" below:
When pulling upward on the sheet, the content may shift horizontally. Sometimes, cells may disappear too:
Question
Is there a way to fix this behavior while still using UICollectionViewCompositionalLayout and the pageSheet presentation style?
Code Summary
The code is pretty straightforward. Just 3 classes, which can be dropped into the ViewController.swift file using the Single View App project template in Xcode.
A UICollectionViewCell class called Cell. The cell has a UILabel and overrides sizeThatFits(_:).
A UIViewController called ViewController used only to present BugViewController.
BugViewController, which configures the data source and presents the collection view. This is where the problem occurs.
Code
import UIKit
// MARK: - Cell -
final class Cell: UICollectionViewCell {
static let reuseIdentifier = "Cell"
lazy var label: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.frame.size = contentView.bounds.size
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(label)
contentView.backgroundColor = .tertiarySystemFill
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
.init(width: label.sizeThatFits(size).width + 32, height: 32)
}
}
// MARK: - ViewController -
final class ViewController: UIViewController {
private let button: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Tap Me!".uppercased(), for: .normal)
button.addTarget(self, action: #selector(presentBugViewController), for: .touchUpInside)
button.sizeToFit()
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(button)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
button.center = view.center
}
#objc func presentBugViewController() {
present(BugViewController(), animated: true)
}
}
// MARK: - BugViewController -
final class BugViewController: UIViewController {
private let models = [
"Better Call Saul",
"Mad Men",
"Rectify",
"Tiger King: Murder, Mayhem, and Madness",
"Master of None",
"BoJack Horseman"
]
private lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createCollectionViewLayout())
collectionView.register(Cell.self, forCellWithReuseIdentifier: Cell.reuseIdentifier)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.contentInset.top = 44
collectionView.backgroundColor = .white
return collectionView
}()
private lazy var dataSource = UICollectionViewDiffableDataSource<Int, String>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.reuseIdentifier, for: indexPath) as? Cell else { fatalError() }
cell.label.text = itemIdentifier
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
var snapshot = NSDiffableDataSourceSnapshot<Int, String>()
snapshot.appendSections([0])
snapshot.appendItems(models)
dataSource.apply(snapshot)
}
private func createCollectionViewLayout() -> UICollectionViewCompositionalLayout {
let layoutSize = NSCollectionLayoutSize.init(
widthDimension: .estimated(200),
heightDimension: .absolute(32)
)
let section = NSCollectionLayoutSection(group:
.horizontal(
layoutSize: layoutSize,
subitems: [.init(layoutSize: layoutSize)]
)
)
section.interGroupSpacing = 8
section.orthogonalScrollingBehavior = .continuous
return .init(section: section)
}
}
Notes
The collection view in my app actually has many sections and scrolls vertically. That is why I'm using a vertically scrolling collection view and a section with orthogonalScrollingBehavior in the example code.
Failed Attempts
I've tried using Auto Layout constraints instead of sizeThatFits(_:).
I've tried not using UICollectionViewDiffableDataSource.
Workarounds
Modifying the cell with a child scroll view and passing in an array of strings (as opposed to one at a time) does avoid this problem. But, it's a dirty hack that I'd like to avoid if possible.

Detecting UITableView scroll with viewDidLayoutSubviews causes excessive callbacks

I have a tableView which is a subclass of a Parallax table view I found on Github. Done all table view set up as normal (Deleted some of it since its irrelevant).
class EventDetailTableViewController: ParallaxTableViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Set the image:
self.imageView.image = eventImage
self.navigationController?.navigationBar.setBackgroundImage(UIImage(), forBarMetrics: .Default)
self.navigationController?.navigationBar.shadowImage = UIImage()
self.navigationController?.navigationBar.translucent = true
}
// MARK: - UITableViewDelegate
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
override
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 4
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// SET UP CELL
}
#IBAction func backButtonPressed(sender: AnyObject) {
print("backButtonPressed")
self.navigationController?.navigationBar.translucent = false
self.navigationController?.navigationBar.barTintColor = UIColor(red: 227/255, green: 38/255, blue: 54/255, alpha: 1.0)
self.navigationController?.popViewControllerAnimated(true)
}
} // END OF VIEWCONTROLLER CLASS
Important things to note here is I have made the navigationBar transparent. When backButtonPressed is called, I wish to change back the navigationBar.Color to the color of the previous View Controller (Red). So I have coded that in the backButtonPressed()
Here is the Parallax code I have. Please view the last func moveImage(). I have the simple effect of when the tableview scrolls down far enough to the description the navigationBar appears with the title "Event Description"
class ParallaxTableViewController: UITableViewController {
// Create the UIView
//var bottomFloatingView = UIView()
// Create the UIImageView
let imageView = UIImageView()
// Set the factor for the parallaxEffect. This is overwritable.
var parallaxFactor:CGFloat = 2
// Set the default height for the image on the top.
var imageHeight:CGFloat = 320 {
didSet {
moveImage()
}
}
// Initialize the scrollOffset varaible.
var scrollOffset:CGFloat = 0 {
didSet {
moveImage()
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Set the contentInset to make room for the image.
self.tableView.contentInset = UIEdgeInsets(top: imageHeight, left: 0, bottom: 0, right: 0)
// Change the contentMode for the ImageView.
imageView.contentMode = UIViewContentMode.ScaleAspectFill
// Add the imageView to the TableView and send it to the back.
view.addSubview(imageView)
view.sendSubviewToBack(imageView)
}
override func viewDidLayoutSubviews() {
// Update the image position after layout changes.
moveImage()
}
// Define method for image location changes.
func moveImage() {
let imageOffset = (scrollOffset >= 0) ? scrollOffset / parallaxFactor : 0
let imageHeight = (scrollOffset >= 0) ? self.imageHeight : self.imageHeight - scrollOffset
imageView.frame = CGRect(x: 0, y: -imageHeight + imageOffset, width: view.bounds.width, height: imageHeight)
if imageOffset > 150 {
print("imageOffSet")
self.navigationItem.title = "Event Description"
self.navigationController?.navigationBar.translucent = false
self.navigationController?.navigationBar.barTintColor = UIColor.whiteColor()
self.navigationController?.navigationBar.shadowImage = nil
} else {
print("else")
self.navigationItem.title = ""
self.navigationController?.navigationBar.shadowImage = UIImage()
self.navigationController?.navigationBar.translucent = true
// This is being called after the back button is being pressed. Which means this else function overrides my backButtonPressed() therefore making my navigation bar in the new VC not the correct color.
}
}
}
As you can see in my notes, this else function is being called again after the backButtonPressed() is called. Therefore it does not give me the desired red in the new VC but it leaves it translucent instead. How can I stop this else function being called when the backButtonPressed is being called?
viewDidLayoutSubviews should never be involved in responding to user events. It is an internal lifecycle method that is rarely used unless you're adding or modifying programmatic NSLayoutConstraints to those already added in Storyboard. It is called quite frequently and apparent redundancy.
Use table delegate methods instead. In particular UITableViewDelegate inherits from UIScrollViewDelegate so reference:
UITableView Scroll event
Then query the tableView itself for visibility information e.g. indexPathsForVisibleRows:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableView_Class/#//apple_ref/occ/instp/UITableView/indexPathsForVisibleRows
You should not use viewDidLayoutSubviews() like this as BaseZen has explained in his answer.
You can use scrollViewDidScroll to achieve what you want.
Replace this
#IBAction func backButtonPressed(sender: AnyObject) {
print("backButtonPressed")
self.navigationController?.navigationBar.translucent = false
self.navigationController?.navigationBar.barTintColor = UIColor(red: 227/255, green: 38/255, blue: 54/255, alpha: 1.0)
self.navigationController?.popViewControllerAnimated(true)
}
With this
var backButtonWasPressed = false
#IBAction func backPressed(sender: AnyObject) {
print("backButtonPressed")
backButtonWasPressed = true
self.navigationController?.navigationBar.translucent = false
self.navigationController?.navigationBar.barTintColor = UIColor(red: 227/255, green: 38/255, blue: 54/255, alpha: 1.0)
self.navigationController?.popViewControllerAnimated(true)
}
And replace this
override func viewDidLayoutSubviews() {
// Update the image position after layout changes.
moveImage()
}
With this
override func scrollViewDidScroll(scrollView: UIScrollView) {
if !backButtonWasPressed {
moveImage()
}
}
And put backButtonWasPressed = false in your viewDidLoad()

Refresh tableView appearance

I want to refresh tableView appearance after I press "Dark mode".But it looks like this:
Before press"Dark mode"
After press"Dark mode"
How can I refresh this tableView after I changed it's appearance
Mycode:
#IBAction func setDarkMode(sender: UISwitch) {
UIView.animateWithDuration(0.5, delay:0,options:UIViewAnimationOptions.BeginFromCurrentState, animations: { () -> Void in
self.setStyleMode()
}) { (finish: Bool) -> Void in
}
}
func setStyleMode() {
if isDarkMode {
view.backgroundColor = UIColor(red:0.1922, green:0.1922, blue:0.1922, alpha:1.0)
self.tableView.backgroundView?.backgroundColor = UIColor(red:0.1922, green:0.1922, blue:0.1922, alpha:1.0)
tableView.separatorColor = UIColor(red:0.3137, green:0.3137, blue:0.3137, alpha:1.0)
self.navigationController?.navigationBar.barTintColor = UIColor(red:0.1451, green:0.1451, blue:0.1451, alpha:1.0)
self.navigationController?.navigationBar.titleTextAttributes = [NSForegroundColorAttributeName: UIColor(red:0.6549, green:0.6549, blue:0.6549, alpha:1.0)]
self.navigationController?.navigationBar.tintColor = UIColor(red:0.9412, green:0.3412, blue:0.302, alpha:1.0)
for sectionIndex in 0...tableView.numberOfSections - 1 {
for rowIndex in 0...tableView.numberOfRowsInSection(sectionIndex) - 1 {
let cellPath = NSIndexPath(forRow: rowIndex, inSection: sectionIndex)
let cell = tableView.cellForRowAtIndexPath(cellPath)
cell?.backgroundColor = UIColor(red:0.1451, green:0.1451, blue:0.1451, alpha:1.0)
}
}
for aLabel in labels {
aLabel.textColor = UIColor(red:0.6549, green:0.6549, blue:0.6549, alpha:1.0)
}
} else {
view.backgroundColor = UIColor.whiteColor()
tableView.backgroundColor = UIColor(red:0.9529, green:0.9529, blue:0.9529, alpha:1.0)
tableView.separatorColor = UIColor(red:0.7372, green:0.7371, blue:0.7372, alpha:1.0)
self.navigationController?.navigationBar.barTintColor = UIColor.whiteColor()
self.navigationController?.navigationBar.titleTextAttributes = [NSForegroundColorAttributeName: UIColor.blackColor()]
self.navigationController?.navigationBar.tintColor = UIColor(red:0.9412, green:0.3412, blue:0.302, alpha:1.0)
for sectionIndex in 0...tableView.numberOfSections - 1 {
for rowIndex in 0...tableView.numberOfRowsInSection(sectionIndex) - 1 {
let cellPath = NSIndexPath(forRow: rowIndex, inSection: sectionIndex)
let cell = tableView.cellForRowAtIndexPath(cellPath)
cell?.backgroundColor = UIColor.whiteColor()
//do stuff with 'cell'
}
}
for aLabel in labels {
aLabel.textColor = UIColor.blackColor()
}
}
}
(This "settings" tableView is in a "tableviewController " and embed in a ContainerView)
Looks like you have not called reloadData()
Once your style changes are over call call execute the following code
tableView.reloadData()
Another thing is, styling your tableViewCellshould be done inside
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
Not inside setStyleMode() function. You can make the style changes based on some bool value.
Hope this will resolve your issue
You're setting the cell's background color, but not the cell's contentView's background color.
You should do:
let cell = tableView.cellForRowAtIndexPath(cellPath)
cell?.backgroundColor = ....
cell?.contentView.backgroundColor = ....
You might have to dig deeper if your cell has more wrapper views in the hierarchy.
A couple of observations, though:
It would be a better pattern to define a method within the UITableViewCell subclass to define a dark and light mode. In complex cell structures, you might not easily be able to access all the subviews you need. Besides, what about encapsulation?
I would also much prefer the reload way. Calling a UITableViewDatasource method manually seems like an anti-pattern.
There's a lot of duplication in your code. You could significantly reduce the dependency by, for instance, using ternary operators on isDarkMode
Need change background of "headerView" and "footerView" in "setStyleMode()"
let header = tableView.headerViewForSection(sectionIndex)
let footer = tableView.footerViewForSection(sectionIndex)
header?.contentView.backgroundColor = UIColor(red:0.1922, green:0.1922, blue:0.1922, alpha:1.0)
footer?.contentView.backgroundColor = UIColor(red:0.1922, green:0.1922, blue:0.1922, alpha:1.0)

Resources