state restoration working but then nullified in viewDidLoad - ios

Note code has been updated to incorporate the fixes detailed in the comments, but here is the original question text:
State restoration works on the code-based ViewController below, but then it is "undone" by a second call to viewDidLoad. My question is: how do I avoid that?
With a breakpoint at decodeRestorableState I can see that it does in fact restore the 2 parameters selectedGroup and selectedType but then it goes through viewDidLoad again and those parameters are reset to nil so the restoration is of no effect. There's no storyboard: if you associated this class with an empty ViewController it will work (I double checked this -- there are some button assets too, but they aren't needed for function). I've also included at the bottom the AppDelegate methods needed to enable state restoration.
import UIKit
class CodeStackVC2: UIViewController, FoodCellDel {
let fruit = ["Apple", "Orange", "Plum", "Qiwi", "Banana"]
let veg = ["Lettuce", "Carrot", "Celery", "Onion", "Brocolli"]
let meat = ["Beef", "Chicken", "Ham", "Lamb"]
let bread = ["Wheat", "Muffin", "Rye", "Pita"]
var foods = [[String]]()
let group = ["Fruit","Vegetable","Meat","Bread"]
var sView = UIStackView()
let cellId = "cellId"
var selectedGroup : Int?
var selectedType : Int?
override func viewDidLoad() {
super.viewDidLoad()
restorationIdentifier = "CodeStackVC2"
foods = [fruit, veg, meat, bread]
setupViews()
displaySelections()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard let index = selectedGroup, let type = selectedType else { return }
pageControl.currentPage = index
let indexPath = IndexPath(item: index, section: 0)
cView.scrollToItem(at: indexPath, at: UICollectionViewScrollPosition(), animated: true)
cView.reloadItems(at: [indexPath])
guard let cell = cView.cellForItem(at: indexPath) as? FoodCell else { return }
cell.pickerView.selectRow(type, inComponent: 0, animated: true)
}
//State restoration encodes parameters in this func
override func encodeRestorableState(with coder: NSCoder) {
if let theGroup = selectedGroup,
let theType = selectedType {
coder.encode(theGroup, forKey: "theGroup")
coder.encode(theType, forKey: "theType")
}
super.encodeRestorableState(with: coder)
}
override func decodeRestorableState(with coder: NSCoder) {
selectedGroup = coder.decodeInteger(forKey: "theGroup")
selectedType = coder.decodeInteger(forKey: "theType")
super.decodeRestorableState(with: coder)
}
override func applicationFinishedRestoringState() {
//displaySelections()
}
//MARK: Views
lazy var cView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
layout.sectionInset = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
layout.itemSize = CGSize(width: self.view.frame.width, height: 120)
let cRect = CGRect(x: 0, y: 0, width: self.view.frame.width, height: 120)
let cv = UICollectionView(frame: cRect, collectionViewLayout: layout)
cv.backgroundColor = UIColor.lightGray
cv.isPagingEnabled = true
cv.dataSource = self
cv.delegate = self
cv.isUserInteractionEnabled = true
return cv
}()
lazy var pageControl: UIPageControl = {
let pageC = UIPageControl()
pageC.numberOfPages = self.foods.count
pageC.pageIndicatorTintColor = UIColor.darkGray
pageC.currentPageIndicatorTintColor = UIColor.white
pageC.backgroundColor = .black
pageC.addTarget(self, action: #selector(changePage(sender:)), for: UIControlEvents.valueChanged)
return pageC
}()
var textView: UITextView = {
let tView = UITextView()
tView.font = UIFont.systemFont(ofSize: 40)
tView.textColor = .white
tView.backgroundColor = UIColor.lightGray
return tView
}()
func makeButton(_ tag:Int) -> UIButton{
let newButton = UIButton(type: .system)
let img = UIImage(named: group[tag])?.withRenderingMode(.alwaysTemplate)
newButton.setImage(img, for: .normal)
newButton.tag = tag // used in handleButton()
newButton.contentMode = .scaleAspectFit
newButton.addTarget(self, action: #selector(handleButton(sender:)), for: .touchUpInside)
newButton.isUserInteractionEnabled = true
newButton.backgroundColor = .clear
return newButton
}
//Make a 4-item vertical stackView containing
//cView,pageView,subStackof 4-item horiz buttons, textView
func setupViews(){
view.backgroundColor = .lightGray
cView.register(FoodCell.self, forCellWithReuseIdentifier: cellId)
//generate an array of buttons
var buttons = [UIButton]()
for i in 0...foods.count-1 {
buttons += [makeButton(i)]
}
let subStackView = UIStackView(arrangedSubviews: buttons)
subStackView.axis = .horizontal
subStackView.distribution = .fillEqually
subStackView.alignment = .center
subStackView.spacing = 20
//set up the stackView
let stackView = UIStackView(arrangedSubviews: [cView,pageControl,subStackView,textView])
stackView.axis = .vertical
stackView.distribution = .fill
stackView.alignment = .fill
stackView.spacing = 5
//Add the stackView using AutoLayout
view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 5).isActive = true
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
cView.translatesAutoresizingMaskIntoConstraints = false
textView.translatesAutoresizingMaskIntoConstraints = false
cView.heightAnchor.constraint(equalTo: textView.heightAnchor, multiplier: 0.5).isActive = true
}
// selected item returned from pickerView
func pickerSelection(_ foodType: Int) {
selectedType = foodType
displaySelections()
}
func displaySelections() {
if let theGroup = selectedGroup,
let theType = selectedType {
textView.text = "\n \n Group: \(group[theGroup]) \n \n FoodType: \(foods[theGroup][theType])"
}
}
// 3 User Actions: Button, Page, Scroll
func handleButton(sender: UIButton) {
pageControl.currentPage = sender.tag
let x = CGFloat(sender.tag) * cView.frame.size.width
cView.setContentOffset(CGPoint(x:x, y:0), animated: true)
}
func changePage(sender: AnyObject) -> () {
let x = CGFloat(pageControl.currentPage) * cView.frame.size.width
cView.setContentOffset(CGPoint(x:x, y:0), animated: true)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let index = Int(cView.contentOffset.x / view.bounds.width)
pageControl.currentPage = Int(index) //change PageControl indicator
selectedGroup = Int(index)
let indexPath = IndexPath(item: index, section: 0)
guard let cell = cView.cellForItem(at: indexPath) as? FoodCell else { return }
selectedType = cell.pickerView.selectedRow(inComponent: 0)
displaySelections()
}
//this causes cView to be recalculated when device rotates
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
cView.collectionViewLayout.invalidateLayout()
}
}
//MARK: cView extension
extension CodeStackVC2: UICollectionViewDataSource, UICollectionViewDelegate,UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return foods.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! FoodCell
cell.foodType = foods[indexPath.item]
cell.delegate = self
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: textView.frame.height * 0.4)
}
}
// *********************
protocol FoodCellDel {
func pickerSelection(_ food:Int)
}
class FoodCell:UICollectionViewCell, UIPickerViewDelegate, UIPickerViewDataSource {
var delegate: FoodCellDel?
var foodType: [String]? {
didSet {
pickerView.reloadComponent(0)
//pickerView.selectRow(0, inComponent: 0, animated: true)
}
}
lazy var pickerView: UIPickerView = {
let pView = UIPickerView()
pView.frame = CGRect(x:0,y:0,width:Int(pView.bounds.width), height:Int(pView.bounds.height))
pView.delegate = self
pView.dataSource = self
pView.backgroundColor = .lightGray
return pView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
func setupViews() {
backgroundColor = .clear
addSubview(pickerView)
addConstraintsWithFormat("H:|[v0]|", views: pickerView)
addConstraintsWithFormat("V:|[v0]|", views: pickerView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if let count = foodType?.count {
return count
} else {
return 0
}
}
func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
let pickerLabel = UILabel()
pickerLabel.font = UIFont.systemFont(ofSize: 15)
pickerLabel.textAlignment = .center
pickerLabel.adjustsFontSizeToFitWidth = true
if let foodItem = foodType?[row] {
pickerLabel.text = foodItem
pickerLabel.textColor = .white
return pickerLabel
} else {
print("chap = nil in viewForRow")
return UIView()
}
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if let actualDelegate = delegate {
actualDelegate.pickerSelection(row)
}
}
}
extension UIView {
func addConstraintsWithFormat(_ format: String, views: UIView...) {
var viewsDictionary = [String: UIView]()
for (index, view) in views.enumerated() {
let key = "v\(index)"
view.translatesAutoresizingMaskIntoConstraints = false
viewsDictionary[key] = view
}
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutFormatOptions(), metrics: nil, views: viewsDictionary))
}
}
Here are the functions in AppDelegate:
//====if set true, these 2 funcs enable state restoration
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
return true
}
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
return true
}
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
//replace the storyboard by making our own window
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
//this defines the entry point for our app
window?.rootViewController = CodeStackVC2()
return true
}

If viewDidLoad is being called twice it will because your view controller is being created twice.
You do not say how you are creating the view controller but I suspect your problem is that the view controller is being created first by a storyboard or in the app delegate and then a second time because you have set a restoration class.
You only need to set a restoration class if your view controller is not being created by the normal app load sequence (a restoration identifier is enough otherwise). Try removing the line in viewDidLoad where you set a restoration class and I think you will see viewDidLoad is called once followed by decodeRestorableState.
Update: Confirmed you are creating the view controller in the app delegate so you do not need to use a restoration class. That fixes the problem with viewDidLoad being called twice.
You want to do the initial root view controller creation in willFinishLaunchingWithOptions in the app delegate as that is called before state restoration takes place.
The final issue once you have the selectedGroup and selectedType values restored is to update the UI elements (page control, collection view), etc to use the restored values

In my six years of iOS programming, I don't remember ever seeing iOS calling viewDidLoad() twice on the same view controller. So it is most likely that you are instantiating CodeStackVC2 twice :)
As far as I can tell, you are creating the view hierarchy programmatically in didFinishLaunchingWithOptions. However, state restoration is invoked before this delegate method is called. So, iOS asks the view controller's restoration class for a new view controller instance, and after that your code setting up the base hierarchy is executed, creating a fresh view controller.
Try moving your code from didFinishLaunchingWithOptions to willFinishLaunchingWithOptions: (which is called before any state restoration). Then, since the view controller that iOS is trying to restore already exists, it won't call that method with the long name from the UIViewControllerRestoration protocol, and instead call decodeRestorableState(with coder:) on that view controller.
If you need a more in-depth explanation, try useyourloaf or of course the Apple docs - I have found both to be very useful in understanding the concepts behind Apple's implementation. Although I must admit, I took me several reads before I understood it myself.

Related

Simple orthogonal collection layout, but has odd behaviour on tvOS. Not sure if it's me?

My ViewController loads and lays out correctly. This is a pretty simple layout! Works as expected on iOS, but I have weird issues on tvOS.
So - at first all looks good. But when the user scrolls down, then scrolls back to the top - the collectionview stays partially under the titlebar. Focus then moves to the barItem, which is correct - but the top section header is under the bar. If the user then scrolls the top section right (to load new cells) - the layout gets fixed. Once this has happened the whole view works as expected.
Some code:
class ViewController: UIViewController {
var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<Int, String>!
override var preferredFocusEnvironments: [UIFocusEnvironment] {
return [collectionView]
}
override func viewDidLoad() {
super.viewDidLoad()
setup()
makeDataSource()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationItem.title = "Some Title"
navigationItem.leftBarButtonItem = .init(systemItem: .refresh)
applySnapshot()
}
// Hack..
override func viewSafeAreaInsetsDidChange() {
collectionView.contentInset.top = view.safeAreaInsets.top
collectionView.layoutMargins = .init(top: 0, left: view.safeAreaInsets.left, bottom: 0, right: view.safeAreaInsets.right)
}
init() {
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: TestLayout.makeSampleLayout() )
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.contentInsetAdjustmentBehavior = .never
collectionView.insetsLayoutMarginsFromSafeArea = false
// Some margins to move headers from sides
collectionView.layoutMargins = .init(top: 0, left: view.safeAreaInsets.left, bottom: 0, right: view.safeAreaInsets.right)
// Some (any) space on top contentInset
//collectionView.contentInset.top = 150
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
self.collectionView = collectionView
}
func applySnapshot() {
var snap = NSDiffableDataSourceSnapshot<Int, String>()
snap.appendSections([1])
snap.appendItems(["item1-1", "item1-2", "item1-3", "item1-4", "item1-5", "item1-6", "item1-7", "item1-8", "item1-9"])
snap.appendSections([2])
snap.appendItems(["item2-1", "item2-2", "item2-3", "item2-4"])
snap.appendSections([3])
snap.appendItems(["item3-1", "item3-2", "item3-3", "item3-4", "item3-5"])
snap.appendSections([4])
snap.appendItems(["item4-1", "item4-2", "item4-3", "item4-4", "item4-5"])
snap.appendSections([5])
snap.appendItems(["item5-1", "item5-2", "item5-3", "item5-4", "item5-5"])
snap.appendSections([6])
snap.appendItems(["item6-1", "item6-2", "item6-3"])
dataSource.apply(snap, animatingDifferences: false)
}
func makeDataSource() {
let textHeader = UICollectionView.SupplementaryRegistration(elementKind: UICollectionView.elementKindSectionHeader,
handler: { (cell: FixedHeaderView, string: String, indexPath: IndexPath) in
})
let testCell = UICollectionView.CellRegistration(handler: { (cell: TestCell, indexPath: IndexPath, itemIdentifier: String) in
})
// Make dataSource and supplementaryViewProvider
dataSource = UICollectionViewDiffableDataSource<Int, String> (collectionView: collectionView, cellProvider: { collectionView, indexPath, itemIdentifier in
return collectionView.dequeueConfiguredReusableCell(using: testCell, for: indexPath, item: itemIdentifier)
})
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
switch kind {
case UICollectionView.elementKindSectionHeader:
return collectionView.dequeueConfiguredReusableSupplementary(using: textHeader, for: indexPath)
default:
assertionFailure("Unknown SupplementaryView")
return nil
}
}
}
}
class TestLayout {
// Return a standard section header
static func standardSectionHeader() -> NSCollectionLayoutBoundarySupplementaryItem {
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(78) )
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize,
elementKind: UICollectionView.elementKindSectionHeader,
alignment: .top)
header.extendsBoundary = true
return header
}
static func orthogonalSectionInsets() -> UIEdgeInsets {
return .init(top: 38, left: 0, bottom: 38, right: 0)
}
static func orthogonalSectionSpacing() -> CGFloat {
return 52
}
static func orthogonalSection() -> NSCollectionLayoutSection? {
let insets = orthogonalSectionInsets()
let itemSpacing = orthogonalSectionSpacing()
let viewItems = 6 + 0.5
let heightDimension = NSCollectionLayoutDimension.estimated(1)
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1 / viewItems),
heightDimension: heightDimension)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
// iOS15
let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitem: item, count: 1)
// iOS16 - weird output?
//let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, repeatingSubitem: item, count: 1)
let section = NSCollectionLayoutSection(group: group)
// Add space to section sides, and between headers
section.contentInsets = NSDirectionalEdgeInsets(top: insets.top, leading: insets.left, bottom: insets.bottom, trailing: insets.right)
section.contentInsetsReference = .layoutMargins
// Add space between horizontal items
section.interGroupSpacing = itemSpacing
section.orthogonalScrollingBehavior = .continuous
// Header
section.boundarySupplementaryItems = [standardSectionHeader() ]
section.supplementaryContentInsetsReference = .layoutMargins
return section
}
static func makeSampleLayout() -> UICollectionViewCompositionalLayout {
let layout = UICollectionViewCompositionalLayout { (sectionNumber, env)
-> NSCollectionLayoutSection? in
return orthogonalSection()
}
// Top level layout settings
let layoutConfig = UICollectionViewCompositionalLayoutConfiguration()
layoutConfig.scrollDirection = .vertical
layout.configuration = layoutConfig
return layout
}
}
class TestCell: UICollectionViewCell {
let someView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
insetsLayoutMarginsFromSafeArea = false
someView.translatesAutoresizingMaskIntoConstraints = false
someView.backgroundColor = .red
// We shouldn't be able to see this if margins are correct
contentView.backgroundColor = .yellow
someView.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
someView.setContentHuggingPriority(.defaultHigh, for: .vertical)
contentView.addSubview(someView)
let bottom: NSLayoutConstraint = someView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
bottom.priority = .defaultHigh // .init(998)
NSLayoutConstraint.activate([
someView.topAnchor.constraint(equalTo: contentView.topAnchor),
someView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
someView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
someView.heightAnchor.constraint(equalTo: someView.widthAnchor),
bottom
])
}
// Some visible focus so we can see what is going on
override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if (context.nextFocusedView == self) {
someView.backgroundColor = .green
}
else if (context.previouslyFocusedView == self) {
someView.backgroundColor = .red
}
}
}
class FixedHeaderView: UICollectionReusableView {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
backgroundColor = .none
label.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
label.setContentHuggingPriority(.defaultHigh, for: .vertical)
label.textColor = .white
label.backgroundColor = .red
label.translatesAutoresizingMaskIntoConstraints = false
label.font = UIFont.systemFont(ofSize: 40, weight: .heavy)
label.text = "Some Header Title"
addSubview(label)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: leadingAnchor),
label.bottomAnchor.constraint(equalTo: bottomAnchor)
])
backgroundColor = .red.withAlphaComponent(0.25)
}
}
AppDelegate:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// Create the window
let newWindow = UIWindow(frame: UIScreen.main.bounds)
let nav = UINavigationController(rootViewController: ViewController() )
newWindow.rootViewController = nav
newWindow.makeKeyAndVisible()
self.window = newWindow
return true
}
I've tried using all sorts of different margin and safeArea layouts, but this issue appears to affect any layout that requires the content to sit off the top of the screen.
Sometimes it loads and works perfectly first time, making me think it's a race condition or something.
Cells are dynamic height (because I need to support a lot of cell types). If I set the height to 'absolute' values, it seems to work - but this would create a lot of work and I'd lose accessibility such as dynamic text sizing.
But I've been at this for 2 days, nudging code and reading docs. I don't do much tvOS and I'm a sole developer (so I don't have any other devs to bounce off) - would really appreciate some input!! Thanks

Segmented control to switch collection views

Unable to load viewControllers.
There is no initial selection in segmented controller
when my view loads I want my firstCVC be by default the first viewController
Here is my parent viewController
class covidVC: UIViewController {
private let segmentedController: UISegmentedControl = {
let labelArr = ["Covid19","Symptoms","WHO","Vaccine"]
let seg = UISegmentedControl(items: labelArr)
seg.translatesAutoresizingMaskIntoConstraints = false
seg.tintColor = .gray
seg.addTarget(self, action:#selector(segAction(_:)), for: .valueChanged)
return seg
}()
func viewLoader(){
addChild(FirstCVC())
addChild(SecondCVC())
addChild(ThirdCVC())
addChild(FourthCVC())
self.view.addSubview(FirstCVC().view)
self.view.addSubview(SecondCVC().view)
self.view.addSubview(ThirdCVC().view)
self.view.addSubview(FourthCVC().view)
FirstCVC().didMove(toParent: self)
SecondCVC().didMove(toParent: self)
ThirdCVC().didMove(toParent: self)
FourthCVC().didMove(toParent: self)
FirstCVC().view.frame = self.view.bounds
SecondCVC().view.frame = self.view.bounds
ThirdCVC().view.frame = self.view.bounds
FourthCVC().view.frame = self.view.bounds
}
#objc func segAction(_ segmentedControll: UISegmentedControl){
FirstCVC().view.isHidden = true
SecondCVC().view.isHidden = true
ThirdCVC().view.isHidden = true
FourthCVC().view.isHidden = true
switch segmentedControll.selectedSegmentIndex {
case 0:
FirstCVC().view.isHidden = false
case 1:
SecondCVC().view.isHidden = false
case 2:
ThirdCVC().view.isHidden = false
case 3:
FourthCVC().view.isHidden = false
default:
FirstCVC().view.isHidden = false
}
}
override func viewDidLayoutSubviews() {
view.addSubview(segmentedController)
segmentedController.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
segmentedController.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
}
override func viewDidLoad() {
super.viewDidLoad()
viewLoader()
}
}
All four Child view are collection VC. I want them to be independent VCs
I don't want to relaod collection views as they have different rows & cols
class FirstCVC: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UICollectionViewDelegate{
// primary horizantal scrollVIew
private let myCollectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.itemSize = CGSize(width: 365 , height: 300)
layout.sectionInset = UIEdgeInsets(top: 10, left: 5, bottom: 10, right: 5)
let view = UICollectionView(frame: .zero, collectionViewLayout:layout)
view.showsVerticalScrollIndicator = false
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
return view
}()
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FirstCustomCell", for: indexPath) as! FirstCustomCell
cell.backgroundColor = .red
return cell
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
view.addSubview(myCollectionView)
myCollectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
myCollectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
myCollectionView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor).isActive = true
}
override func viewDidLoad() {
super.viewDidLoad()
myCollectionView.dataSource = self
myCollectionView.delegate = self
myCollectionView.register(FirstCustomCell.self, forCellWithReuseIdentifier: FirstCustomCell.identifier)
}
}
The Image output I'm getting
You create a different instance each line
addChild(FirstCVC())
addChild(SecondCVC())
addChild(ThirdCVC())
addChild(FourthCVC())
self.view.addSubview(FirstCVC().view)
self.view.addSubview(SecondCVC().view)
self.view.addSubview(ThirdCVC().view)
self.view.addSubview(FourthCVC().view)
FirstCVC().didMove(toParent: self)
SecondCVC().didMove(toParent: self)
ThirdCVC().didMove(toParent: self)
FourthCVC().didMove(toParent: self)
FirstCVC().view.frame = self.view.bounds
SecondCVC().view.frame = self.view.bounds
ThirdCVC().view.frame = self.view.bounds
FourthCVC().view.frame = self.view.bounds
while it should be
let vc1 = FirstCVC()
let vc2 = SecondCVC()
let vc3 = ThirdCVC()
let vc4 = FourthCVC()
addChild(vc1)
addChild(vc2)
addChild(vc3)
addChild(vc4)
self.view.addSubview(vc1.view)
self.view.addSubview(vc2.view)
self.view.addSubview(vc3.view)
self.view.addSubview(vc4.view)
vc1.didMove(toParent: self)
vc2.didMove(toParent: self)
vc3.didMove(toParent: self)
vc4.didMove(toParent: self)
vc1.view.frame = self.view.bounds
vc2.view.frame = self.view.bounds
vc3.view.frame = self.view.bounds
vc4.view.frame = self.view.bounds
Tip : Also you better create an extension instead of repeating the 4 lines for each vc

How to change collectionview cells color based on device theme (following my color scheme)

Overview:
I'm building a keyboard Extension using collectionviews. I want the cells to change color based on the device theme (light/dark). At the moment, when I set the color scheme for my collectionview cells they don't work. I'm marking the problematic parts of my code with a "///" comment.
Resources:
I found this RayWenderlich project and I liked how they handled the color changing stuff so I copied it.
My code:
I have 3 classes:
KeyboardViewController
Custom View containing keyboard buttons
Custom collectionview cells
CollectionView cell
class KeyboardKeys: UICollectionViewCell {
var defaultColor = UIColor.white
var highlighColor = UIColor.lightGray.withAlphaComponent(0.6)
let label: UILabel = {
let iv = UILabel()
iv.translatesAutoresizingMaskIntoConstraints = false
iv.contentMode = .scaleAspectFit
iv.font = UIFont.systemFont(ofSize: 20)
iv.clipsToBounds = true
iv.numberOfLines = 1
iv.textAlignment = .center
return iv
}()
override init(frame: CGRect) {
super.init(frame: .zero)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
contentView.addSubview(label)
label.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
label.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true
label.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
}
override func layoutSubviews() {
super.layoutSubviews()
backgroundColor = isHighlighted ? highlighColor : defaultColor
}
}
Custom View
class lettersKeyboard: UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout{
var keyView: UICollectionView!
let letters = ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"]
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit(){
//If you find some errors it's because this is way different in my code. This is just a regulare collection view anyway
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
keyView = UICollectionView(frame: CGRect(x: 0.0, y: 0.0 , width: frame.width, height: 280), collectionViewLayout: layout)
keyView.setCollectionViewLayout(layout, animated: true)
keyView.isScrollEnabled = false
keyView.register(KeyboardKeys.self, forCellWithReuseIdentifier: "collectionCellId")
keyView.delegate = self
keyView.dataSource = self
addSubview(keyView)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = keyView.dequeueReusableCell(withReuseIdentifier: "collectionCellId", for: indexPath) as! KeyboardKeys
cell.label.text = letters[indexPath.row]
return cell
}
///I guess something is wrong here
func setColorScheme(_ colorScheme: ColorScheme) {
let colorScheme = CColors(colorScheme: colorScheme)
for view in subviews {
if let cell = view as? KeyboardKeys {
cell.tintColor = colorScheme.buttonTextColor
cell.defaultColor = colorScheme.keysDefaultColor
cell.highlighColor = colorScheme.keysHighlightColor
}
}
}
}
Color scheme struct
enum ColorScheme {
case dark
case light
}
struct CColors {
let keysDefaultColor: UIColor
let keysHighlightColor: UIColor
let buttonTextColor: UIColor
init(colorScheme: ColorScheme) {
switch colorScheme {
case .light:
keysDefaultColor = .systemRed
//UIColor.white
keysHighlightColor = UIColor.lightGray.withAlphaComponent(0.6)
buttonTextColor = .black
case .dark:
keysDefaultColor = .systemBlue
// UIColor.gray.withAlphaComponent(0.5)
keysHighlightColor = UIColor.lightGray.withAlphaComponent(0.5)
buttonTextColor = .white
}
}
}
KeyboardViewController
class KeyboardViewController: UIInputViewController {
var letters : lettersKeyboard = {
let m = lettersKeyboard(frame: .zero)
m.translatesAutoresizingMaskIntoConstraints = false
m.backgroundColor = .clear
return m
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(letters)
letters.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
letters.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
letters.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
letters.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
//The rest is the default inputvc stuff
///Or here
override func textDidChange(_ textInput: UITextInput?) {
// The app has just changed the document's contents, the document context has been updated.
let colorScheme: ColorScheme
let proxy = self.textDocumentProxy
if proxy.keyboardAppearance == UIKeyboardAppearance.dark {
colorScheme = .dark
} else {
colorScheme = .light
}
letters.setColorScheme(colorScheme)
}
}
Question:
I don't know what I'm doing wrong since my code works with everything except for collectionview cells. I guess another way of doing this stuff exists. So how do I change my collectionView cells' color based on the device's theme following my color scheme?
You should really be reloading the collection view, rather than trying to find the subviews that are the keys, and updating those.
Pass in the colorScheme model to each cell and have the colors be set as a result of a reload.
A very kind guy helped me out and found this solution. The problem here is that I forgot the view's hierarchy.
CollectionView cell
override func layoutSubviews() {
super.layoutSubviews()
setupBackGround()
}
func setupBackGround(){
backgroundColor = isHighlighted ? highlighColor : defaultColor
}
KeyboardViewController
func setColorScheme(_ colorScheme: ColorScheme) {
let colorScheme = CColors(colorScheme: colorScheme)
for view in subviews {
func setToRootView(view: UIView) {
if let cell = view as? KeyboardKeys {
cell.tintColor = colorScheme.buttonTextColor
cell.defaultColor = colorScheme.keysDefaultColor
cell.highlighColor = colorScheme.keysHighlightColor
cell.setBackground()
return
}
guard view.subviews.count > 0 else {
return
}
view.subviews.forEach(setToRootView(view:))
}
setToRootView(view: self)
}

Slide Menu (Hamburger Menu) iOS Swift

I need create slide menu with transition from right to left, I have succeeded to create it but when I press icon of menu show me slide menu without animation!! and I can’t hide it with press outside of slide menu!!
How can I create animation with transition from right to left??
How can I set hide when press outside of slide menu??
SlideInMenu Class:
import UIKit
class SlideInMenu: NSObject, UIViewControllerAnimatedTransitioning {
var isPresenting = false
let dimmingView = UIView()
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toVC = transitionContext.viewController(forKey: .to),
let fromVC = transitionContext.viewController(forKey: .from) else { return }
let containerView = transitionContext.containerView
let finalWidth = toVC.view.bounds.width * 0.8375
let finalHeight = toVC.view.bounds.height
if (isPresenting) {
dimmingView.backgroundColor = UIColor(red: 32/255, green: 70/255, blue: 86/255, alpha: 0.8)
containerView.addSubview(dimmingView)
dimmingView.frame = containerView.bounds
containerView.addSubview(toVC.view)
toVC.view.frame = CGRect(x: -(finalWidth - toVC.view.bounds.width), y: 0, width: finalWidth, height: finalHeight)
}
let transform = {
self.dimmingView.alpha = 0.9
toVC.view.transform = CGAffineTransform(translationX: finalWidth - toVC.view.bounds.width, y: 0)
}
let identiy = {
self.dimmingView.alpha = 0.9
fromVC.view.transform = .identity
}
let duration = transitionDuration(using: transitionContext)
let isCancelled = transitionContext.transitionWasCancelled
UIView.animate(withDuration: duration, animations: {
self.isPresenting ? transform () : identiy()
}) { (_) in
transitionContext.completeTransition(!isCancelled)
}
}
}
ViewController Class:
import UIKit
class ViewController: UIViewController {
let transition = SlideInMenu()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func tapBtnMenu(_ sender: Any) {
guard let menuVC = storyboard?.instantiateViewController(withIdentifier: "MenuViewController") else { return }
menuVC.modalPresentationStyle = .overCurrentContext
menuVC.transitioningDelegate = self
present(menuVC, animated: true)
}
}
extension ViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.isPresenting = true
return transition
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.isPresenting = false
return transition
}
}
Output:
You can copy paste the below code in see it a in live action.
Explanation:
ViewController:
A button is added to ViewController for the opening of Sidebar
It confirms to the delegate so that it can listen to when the sidebar gets opened and when it gets closed and whether the user has selected any option of not
Sidebar:
This does the heavy lifting of main work.
The balck transparent View is close button and the other part is NavigationViewController
In show method, the close button is set to 60 width and navigation vc view is aligned next to it using constraints. Note that the x position of navigation vc is set to the width of screenwidth.
With the help of UIView.animate, the X position is set to 0 thus it animates from right to left.
on click on any option or clicking on outside of nav vc i.e. close button, the closeSidebar method is called.
closeSidebar again has a UIViw.animate block which is setting X position to screenwidth again thus left to right animation. and when it gets completed it is removed from the view hierarchy.
The ViewController is confirming to SidebarDelegate in this delegate's sidebarDidClose method, you will get to know that side bar has been closed and if the user has selected any option or not.
ViewController:
import UIKit
class ViewController: UIViewController {
public let button: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.setTitle("Open Menu", for: .normal)
v.setTitleColor(.blue, for: .normal)
v.setTitleColor(UIColor.blue.withAlphaComponent(0.5), for: .highlighted)
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(button)
let constrains = [
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
]
NSLayoutConstraint.activate(constrains)
button.addTarget(self, action: #selector(didSelect(_:)), for: .touchUpInside)
}
#objc func didSelect(_ sender: UIButton){
SidebarLauncher.init(delegate: self).show()
}
}
extension ViewController: SidebarDelegate{
func sidbarDidOpen() {
}
func sidebarDidClose(with item: NavigationModel?) {
guard let item = item else {return}
switch item.type {
case .home:
print("called home")
case .star:
print("called star")
case .about:
print("called about")
case .facebook:
print("called facebook")
case .instagram:
print("instagram")
}
}
}
SideBar:
import UIKit
protocol SidebarDelegate {
func sidbarDidOpen()
func sidebarDidClose(with item: NavigationModel?)
}
class SidebarLauncher: NSObject{
var view: UIView?
var delegate: SidebarDelegate?
var vc: NavigationViewController?
init(delegate: SidebarDelegate) {
super.init()
self.delegate = delegate
}
public let closeButton: UIButton = {
let v = UIButton()
v.backgroundColor = UIColor.black.withAlphaComponent(0.5)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
func show(){
let bounds = UIScreen.main.bounds
let v = UIView(frame: CGRect(x: bounds.width, y: 0, width: bounds.width, height: bounds.height))
v.backgroundColor = .clear
let vc = NavigationViewController()
vc.delegate = self
v.addSubview(vc.view)
v.addSubview(closeButton)
vc.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
closeButton.topAnchor.constraint(equalTo: v.topAnchor),
closeButton.leadingAnchor.constraint(equalTo: v.leadingAnchor),
closeButton.bottomAnchor.constraint(equalTo: v.bottomAnchor),
closeButton.widthAnchor.constraint(equalToConstant: 60),
vc.view.topAnchor.constraint(equalTo: v.topAnchor),
vc.view.leadingAnchor.constraint(equalTo: closeButton.trailingAnchor),
vc.view.bottomAnchor.constraint(equalTo: v.bottomAnchor),
vc.view.trailingAnchor.constraint(equalTo: v.trailingAnchor, constant: 0)
])
closeButton.addTarget(self, action: #selector(close(_:)), for: .touchUpInside)
self.view = v
self.vc = vc
UIApplication.shared.keyWindow?.addSubview(v)
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: [.curveEaseOut], animations: {
self.view?.frame = CGRect(x: 0, y: 0, width: self.view!.frame.width, height: self.view!.frame.height)
self.view?.backgroundColor = UIColor.black.withAlphaComponent(0.5)
}, completion: {completed in
self.delegate?.sidbarDidOpen()
})
}
#objc func close(_ sender: UIButton){
closeSidebar(option: nil)
}
func closeSidebar(option: NavigationModel?){
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: [.curveEaseOut], animations: {
if let view = self.view{
view.frame = CGRect(x: view.frame.width, y: 0, width: view.frame.width, height: view.frame.height)
self.view?.backgroundColor = .clear
}
}, completion: {completed in
self.view?.removeFromSuperview()
self.view = nil
self.vc = nil
self.delegate?.sidebarDidClose(with: option)
})
}
}
extension SidebarLauncher: NavigationDelegate{
func navigation(didSelect: NavigationModel?) {
closeSidebar(option: didSelect)
}
}
For the completeness of the code
NavigationView
import Foundation
import UIKit
class NavigationView: UIView{
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit(){
addSubview(mainView)
mainView.addSubview(collectionView)
setConstraints()
}
func setConstraints() {
let constraints = [
mainView.topAnchor.constraint(equalTo: topAnchor),
mainView.leadingAnchor.constraint(equalTo: leadingAnchor),
mainView.bottomAnchor.constraint(equalTo: bottomAnchor),
mainView.trailingAnchor.constraint(equalTo: trailingAnchor),
collectionView.topAnchor.constraint(equalTo: mainView.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor),
collectionView.bottomAnchor.constraint(equalTo: mainView.bottomAnchor),
collectionView.trailingAnchor.constraint(equalTo: mainView.trailingAnchor)
]
NSLayoutConstraint.activate(constraints)
}
public let collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
let v = UICollectionView(frame: .zero, collectionViewLayout: layout)
v.translatesAutoresizingMaskIntoConstraints = false
v.register(NavigationCell.self, forCellWithReuseIdentifier: "NavigationCell")
v.backgroundColor = .clear
return v
}()
public let mainView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .blue
return v
}()
}
NavigationViewController plus Navigation Model
import Foundation
import UIKit
protocol NavigationDelegate{
func navigation(didSelect: NavigationModel?)
}
enum NavigationTypes{
case home,star,about,facebook,instagram
}
struct NavigationModel {
let name: String
let type: NavigationTypes
}
class NavigationViewController: UIViewController{
var myView: NavigationView {return view as! NavigationView}
unowned var collectionView: UICollectionView {return myView.collectionView}
var delegate: NavigationDelegate?
var list = [NavigationModel]()
override func loadView() {
view = NavigationView()
}
override func viewDidLoad() {
super.viewDidLoad()
setupList()
collectionView.delegate = self
collectionView.dataSource = self
}
func setupList(){
list.append(NavigationModel(name: "Home", type: .home))
list.append(NavigationModel(name: "Star", type: .star))
list.append(NavigationModel(name: "About", type: .about))
list.append(NavigationModel(name: "Facebook", type: .facebook))
list.append(NavigationModel(name: "Instagram", type: .instagram))
}
}
extension NavigationViewController: UICollectionViewDataSource{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return list.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "NavigationCell", for: indexPath) as! NavigationCell
let model = list[indexPath.item]
cell.label.text = model.name
return cell
}
}
extension NavigationViewController: UICollectionViewDelegate{
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
delegate?.navigation(didSelect: list[indexPath.item])
}
}
extension NavigationViewController: UICollectionViewDelegateFlowLayout{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 45)
}
}
NavigationCell
import Foundation
import UIKit
class NavigationCell: UICollectionViewCell{
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit(){
[label,divider].forEach{contentView.addSubview($0)}
setConstraints()
}
func setConstraints() {
let constraints = [
label.centerYAnchor.constraint(equalTo: centerYAnchor),
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
divider.leadingAnchor.constraint(equalTo: leadingAnchor),
divider.bottomAnchor.constraint(equalTo: bottomAnchor),
divider.trailingAnchor.constraint(equalTo: trailingAnchor),
divider.heightAnchor.constraint(equalToConstant: 1)
]
NSLayoutConstraint.activate(constraints)
}
public let label: UILabel = {
let v = UILabel()
v.text = "Label Text"
v.textColor = .white
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
public let divider: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .white
return v
}()
}

UIRefreshControl endRefresh jumps when used with Large Title enabled

I'm trying to use UIRefreshControl, but when I call endRefreshing() it jumps the UINavigationBar. The problem only happens when I use UIRefreshControl along with large titles.
Looking at some similar issues (UIRefreshControl glitching in combination with custom TableViewCell) reported here, I tried to refresh only after dragging ends, nevertheless, the bug still occurs. Also tried to use
self.navigationController?.navigationBar.isTranslucent = false and self.extendedLayoutIncludesOpaqueBars = true
But, none of the solutions found on other questions seems to resolve the problem, it still not smooth.
The video of what is happening:
https://www.youtube.com/watch?v=2BBRnZ444bE
The app delegate
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let window = UIWindow(frame: UIScreen.main.bounds)
window.makeKeyAndVisible()
let nav = UINavigationController()
nav.title = "My Nav"
nav.navigationBar.prefersLargeTitles = true
nav.viewControllers = [ViewController()]
window.rootViewController = nav
self.window = window
return true
}
}
Observe that I'm using large titles:
let nav = UINavigationController()
nav.title = "My Nav"
nav.navigationBar.prefersLargeTitles = true
The ViewController:
import UIKit
import Foundation
final class ViewController: UICollectionViewController {
let randomHeight = Int.random(in: 100..<300)
init() {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.estimatedItemSize = CGSize(width: 20, height: 20)
super.init(collectionViewLayout: layout)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Try to refresh"
self.navigationController?.navigationBar.isTranslucent = false
self.extendedLayoutIncludesOpaqueBars = true
collectionView.backgroundColor = .white
registerCells()
setupRefreshControl()
}
private func registerCells() {
self.collectionView.register(
Cell.self,
forCellWithReuseIdentifier: "Cell"
)
}
private func setupRefreshControl() {
let refreshControl = UIRefreshControl()
refreshControl.addTarget(
self,
action: #selector(refreshControlDidFire),
for: .valueChanged
)
self.collectionView.refreshControl = refreshControl
}
#objc private func refreshControlDidFire(_ sender: Any?) {
if let sender = sender as? UIRefreshControl, sender.isRefreshing {
refresh()
}
}
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if collectionView.refreshControl!.isRefreshing {
refresh()
}
}
private func refresh() {
if !collectionView.isDragging {
collectionView.refreshControl!.endRefreshing()
collectionView.perform(#selector(collectionView.reloadData), with: nil, afterDelay: 0.05)
}
}
}
extension ViewController {
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(
_ collectionView: UICollectionView,
numberOfItemsInSection section: Int
) -> Int {
return 10
}
override func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: "Cell", for: indexPath
) as? Cell else {
return UICollectionViewCell()
}
cell.label.text = "Text number \(indexPath.row), with height \(randomHeight)"
cell.heightAnchorConstraint.constant = CGFloat(randomHeight)
return cell
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
}
}
final class Cell: UICollectionViewCell {
private let shadowView = UIView()
private let containerView = UIView()
private let content = UIView()
let label = UILabel()
var heightAnchorConstraint: NSLayoutConstraint!
override init(frame: CGRect = .zero) {
super.init(frame: frame)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
insertSubview(shadowView, at: 0)
addSubview(containerView)
containerView.addSubview(label)
containerView.addSubview(content)
activateConstraints()
}
private func activateConstraints() {
self.translatesAutoresizingMaskIntoConstraints = false
shadowView.translatesAutoresizingMaskIntoConstraints = false
containerView.translatesAutoresizingMaskIntoConstraints = false
label.translatesAutoresizingMaskIntoConstraints = false
content.translatesAutoresizingMaskIntoConstraints = false
shadowView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
shadowView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
shadowView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
shadowView.bottomAnchor
.constraint(equalTo: self.bottomAnchor).isActive = true
containerView.backgroundColor = .white
containerView.layer.cornerRadius = 14
containerView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
containerView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
containerView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
let widthAnchorConstraint = containerView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width - 20)
widthAnchorConstraint.identifier = "Width ContainerView"
widthAnchorConstraint.priority = .defaultHigh
widthAnchorConstraint.isActive = true
label.numberOfLines = 0
label.textAlignment = .center
label.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).isActive = true
label.centerYAnchor.constraint(equalTo: containerView.centerYAnchor).isActive = true
label.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
label.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
content.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 20).isActive = true
content.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 10).isActive = true
content.bottomAnchor.constraint(lessThanOrEqualTo: containerView.bottomAnchor, constant: -10).isActive = true
heightAnchorConstraint = content.heightAnchor.constraint(greaterThanOrEqualToConstant: 220)
heightAnchorConstraint.identifier = "Height Content"
heightAnchorConstraint.priority = .defaultHigh
heightAnchorConstraint.isActive = true
content.widthAnchor.constraint(equalToConstant: 40).isActive = true
content.backgroundColor = .red
}
override func layoutSubviews() {
super.layoutSubviews()
applyShadow(width: 0.20, height: -0.064)
}
private func applyShadow(width: CGFloat, height: CGFloat) {
let shadowPath = UIBezierPath(roundedRect: shadowView.bounds, cornerRadius: 14.0)
shadowView.layer.masksToBounds = false
shadowView.layer.shadowRadius = 8.0
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOffset = CGSize(width: width, height: height)
shadowView.layer.shadowOpacity = 0.3
shadowView.layer.shadowPath = shadowPath.cgPath
}
}
The problem is related to layout.estimatedItemSize = CGSize(width: 20, height: 20)
When we use AutoLayout to resize the cell, it creates a bug with UIRefreshControl and navigation bar large title. So, if you use layout.estimatedItemSize with an equal or greater size than we expected. So the bug will not happen and the glitch will not happen.
Basically, the problem is when we call updateData but the cell is bigger than we expect and each cell of the UICollectinView will resize to a bigger size then the UICollectionViewController will glitches.
So, I try your code and all works fine.
But I ran it on iPhone 7 - no large title there.
I think, it is largeTitle issue.
You can try use this code snippet:
self.navigationController?.navigationBar.prefersLargeTitles = false
self.navigationController?.navigationBar.prefersLargeTitles = true

Resources