Cells do not load using UITableVIewDiffableDataSource - ios

I'm trying to implement table view with new DiffableDataSource api, but the cells simply do not load:
var tableView = UITableView()
var currencyPairsArray = [String]()
lazy var fetcher = NetworkDataFetcher()
lazy var searchText = String()
lazy var searchArray = [String]()
lazy var searchController: UISearchController = {
let controller = UISearchController(searchResultsController: nil)
controller.hidesNavigationBarDuringPresentation = false
controller.obscuresBackgroundDuringPresentation = false
controller.searchBar.delegate = self
return controller
}()
fileprivate var dataSource : UITableViewDiffableDataSource<Section, String>!
var searchBarIsEmpty: Bool {
return searchController.searchBar.text?.isEmpty ?? true
}
override func viewDidLoad() {
super.viewDidLoad()
setupVC()
setupTableView()
setupDataSource()
performSearch(with: nil)
fetcher.fetchCurrencyPairs { [weak self] pairsArray in
self?.currencyPairsArray.append(contentsOf: pairsArray)
DispatchQueue.main.async {
self?.tableView.reloadData()
}
}
//tableView.reloadData()
// Do any additional setup after loading the view.
}
func setupVC() {
view.backgroundColor = .white
view.addSubview(tableView)
navigationItem.title = "Currency pairs"
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
}
func setupTableView() {
tableView.delegate = self
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.pinToSuperView()
tableView.register(UINib(nibName: "CurrencyPairCell", bundle: nil), forCellReuseIdentifier: CurrencyPairCell.reuseIdentifier)
}
func setupDataSource() {
dataSource = UITableViewDiffableDataSource<Section, String>(tableView: tableView, cellProvider: { [weak self] (tableView, indexPath, _) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: CurrencyPairCell.reuseIdentifier, for: indexPath) as! CurrencyPairCell
cell.delegate = self
let pair = self?.currencyPairsArray[indexPath.row].formattedPair()
cell.currencyPairLabel.text = pair
cell.currencyPair = self?.currencyPairsArray[indexPath.row] ?? ""
return cell
})
}
func performSearch(with filter: String?) {
var snapshot = NSDiffableDataSourceSnapshot<Section, String>()
if let filter = filter {
let filteredPairs = currencyPairsArray.filter {$0.contains(filter)}
snapshot.appendSections([.main])
snapshot.appendItems(filteredPairs, toSection: .main)
dataSource.apply(snapshot, animatingDifferences: true)
} else {
let pairs = currencyPairsArray.sorted()
snapshot.appendSections([.main])
snapshot.appendItems(pairs, toSection: .main)
dataSource.apply(snapshot)
}
}
}
extension CurrencyListViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
performSearch(with: searchText)
}
}
extension CurrencyListViewController {
fileprivate enum Section: Hashable {
case main
}
}
Also, i am getting a warning from the xcode:
[TableView] Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes. Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the debugger and see what caused this to occur, so you can avoid this action altogether if possible, or defer it until the table view has been added to a window. Table view: ; layer = ; contentOffset: {0, 0}; contentSize: {414, 0}; adjustedContentInset: {0, 0, 0, 0}; dataSource: <TtGC5UIKit29UITableViewDiffableDataSourceOC11FXTMproject26CurrencyListViewControllerP10$107a9eb7c7SectionSS: 0x600002960ca0>>

First of all there is a big design mistake in your code.
With UITableViewDiffableDataSource stop thinking in index paths and data source arrays. Instead think in datasource items.
In setupDataSource you get the model item of the row always from the data source array currencyPairsArray regardless whether you are going to display the filtered data or not. Forget currencyPairsArray and the index path. Take advantage of the third parameter in the closure which represents the item.
func setupDataSource() {
dataSource = UITableViewDiffableDataSource<Section, String>(tableView: tableView, cellProvider: { [weak self] (tableView, _, pair) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: CurrencyPairCell.reuseIdentifier, for: indexPath) as! CurrencyPairCell
cell.delegate = self
cell.currencyPairLabel.text = pair.formattedPair()
cell.currencyPair = pair
return cell
})
}
To get rid of the warning perform the first reload of the data without animation. Add a boolean parameter to performSearch. And rather than checking for nil check for empty string
func performSearch(with filter: String, animatingDifferences: Bool = true) {
var snapshot = NSDiffableDataSourceSnapshot<Section, String>()
let pairs : [String]
if filter.isEmpty {
pairs = currencyPairsArray.sorted()
} else {
pairs = currencyPairsArray.filter {$0.contains(filter)}
}
snapshot.appendSections([.main])
snapshot.appendItems(pairs, toSection: .main)
dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}
And never call tableView.reloadData() when using UITableViewDiffableDataSource which is most likely the reason of your issue.
Replace
performSearch(with: nil)
fetcher.fetchCurrencyPairs { [weak self] pairsArray in
self?.currencyPairsArray.append(contentsOf: pairsArray)
DispatchQueue.main.async {
self?.tableView.reloadData()
}
}
with
fetcher.fetchCurrencyPairs { [weak self] pairsArray in
self?.currencyPairsArray.append(contentsOf: pairsArray)
DispatchQueue.main.async {
self?.performSearch(with: "", animatingDifferences: false)
}
}

Related

Activating a search controller whose navigation item is not at the top of the stack | Swift

The error:
[Assert] Surprise! Activating a search controller whose navigation item is not at the top of the stack. This case needs examination in UIKit. items = (null),
search hosting item = <UINavigationItem: 0x1068473a0> title='PARTS' style=navigator leftBarButtonItems=0x282bfb8d0 rightBarButtonItems=0x282bfb890 searchController=0x110024400 hidesSearchBarWhenScrolling
Why am I getting this error and how do I fix it? This question is similar to another post, but there was only one response to it and the response was not detailed at all (therefore not helpful).
import UIKit
import SPStorkController
struct Part {
var title: String?
var location: String?
var selected: Bool? = false
}
class InspectorViewController: UIViewController, UINavigationControllerDelegate, UITableViewDataSource, UITableViewDelegate, UISearchResultsUpdating, UISearchBarDelegate {
private let initials = InspectorPartsList.getInitials() // model
private var parts = InspectorPartsList.getDamageUnrelatedParts() // model
var filteredParts: [Part] = [] // model
var searching = false
var searchController = UISearchController(searchResultsController: nil)
lazy var searchBar: UISearchBar = UISearchBar()
var isSearchBarEmpty: Bool {
return searchController.searchBar.text?.isEmpty ?? true
}
var navBar = UINavigationBar()
let inspectorTableView = UITableView() // tableView
var darkTheme = Bool()
override func viewDidLoad() {
super.viewDidLoad()
// setup the navigation bar
setupNavBar()
// add the table view
setupInspectorTableView()
// add the search controller to the navigation bar
setupSearchController()
}
func setupNavBar() {
navBar = UINavigationBar(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 100))
view.addSubview(navBar)
let navItem = UINavigationItem(title: "PARTS")
let doneItem = UIBarButtonItem(barButtonSystemItem: .done, target: nil, action: #selector(self.addBtnTapped))
let cancelItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: nil, action: #selector(self.cancelBtnTapped))
navItem.rightBarButtonItem = doneItem
navItem.leftBarButtonItem = cancelItem
navItem.searchController = searchController
navBar.setItems([navItem], animated: false)
}
#objc func cancelBtnTapped() {
// dismiss the storkView
SPStorkController.dismissWithConfirmation(controller: self, completion: nil)
}
#objc func addBtnTapped() {
// get all of the selected rows
// Update the InspectionData model with the selected items... this will allow us to update the InspectionTableView in the other view
// create an empty array for the selected parts
var selectedParts = [Part]()
// loop through every selected index and append it to the selectedParts array
for part in parts {
if part.selected! {
selectedParts.append(part)
}
}
// update the InspectionData model
if !selectedParts.isEmpty { // not empty
InspectionData.sharedInstance.partsData?.append(contentsOf: selectedParts)
// update the inspectionTableView
updateInspectionTableView()
}
// dismiss the storkView
SPStorkController.dismissWithConfirmation(controller: self, completion: nil)
}
func cancelAddPart() {
// dismiss the storkView
SPStorkController.dismissWithConfirmation(controller: self, completion: nil)
}
private func setupInspectorTableView() {
// set the data source
inspectorTableView.dataSource = self
// set the delegate
inspectorTableView.delegate = self
// add tableview to main view
view.addSubview(inspectorTableView)
// set constraints for tableview
inspectorTableView.translatesAutoresizingMaskIntoConstraints = false
// inspectorTableView.topAnchor.constraint(equalTo: fakeNavBar.bottomAnchor).isActive = true
inspectorTableView.topAnchor.constraint(equalTo: navBar.bottomAnchor).isActive = true
inspectorTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
inspectorTableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
inspectorTableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
// allow multiple selection
inspectorTableView.allowsMultipleSelection = true
inspectorTableView.allowsMultipleSelectionDuringEditing = true
// register the inspectorCell
inspectorTableView.register(CheckableTableViewCell.self, forCellReuseIdentifier: "inspectorCell")
}
func setupSearchController() {
// add the bar
searchController.searchResultsUpdater = self
searchController.searchBar.delegate = self
searchController.hidesNavigationBarDuringPresentation = false
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search by part name or location"
definesPresentationContext = true
searchController.searchBar.sizeToFit()
self.inspectorTableView.reloadData()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if searching {
return filteredParts.count
} else {
return parts.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let inspectorCell = tableView.dequeueReusableCell(withIdentifier: "inspectorCell", for: indexPath)
var content = inspectorCell.defaultContentConfiguration()
var part = Part()
if searching {
// showing the filteredParts array
part = filteredParts[indexPath.row]
if filteredParts[indexPath.row].selected! {
// selected - show checkmark
inspectorCell.accessoryType = .checkmark
} else {
// not selected
inspectorCell.accessoryType = .none
}
} else {
// showing the parts array
part = parts[indexPath.row]
if part.selected! {
// cell selected - show checkmark
inspectorCell.accessoryType = .checkmark
} else {
// not selected
inspectorCell.accessoryType = .none
}
}
content.text = part.title
content.secondaryText = part.location
inspectorCell.contentConfiguration = content
return inspectorCell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// Note: When you select or unselect a part in the filteredParts array, you must also do so in the parts array
if searching { // using filteredParts array
if filteredParts[indexPath.row].selected! { // selected
filteredParts[indexPath.row].selected = false // unselect the part
// search the parts array for the part by both the title and location, so we for sure get the correct part (there could be parts with identical titles with different locations)
if let part = parts.enumerated().first(where: { $0.element.title == filteredParts[indexPath.row].title && $0.element.location == filteredParts[indexPath.row].location}) { // exact part (with same title & location) found
parts[part.offset].selected = false // unselect the part
}
} else { // not selected
filteredParts[indexPath.row].selected = true // select the part
if let part = parts.enumerated().first(where: { $0.element.title == filteredParts[indexPath.row].title && $0.element.location == filteredParts[indexPath.row].location}) { // exact part (with same title & location) found
parts[part.offset].selected = true // select the part
}
}
} else { // using parts array
if parts[indexPath.row].selected! { // selected
parts[indexPath.row].selected = false // unselect the part
} else { // not selected
parts[indexPath.row].selected = true // select the part
}
}
inspectorTableView.reloadRows(at: [indexPath], with: .none) // reload the tableView
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
filteredParts = parts.filter { ($0.title?.lowercased().prefix(searchText.count))! == searchText.lowercased() }
searching = true
inspectorTableView.reloadData()
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searching = false
searchBar.text = ""
inspectorTableView.reloadData()
}
func updateSearchResults(for searchController: UISearchController) {
}
private func updateInspectionTableView() {
NotificationCenter.default.post(name: NSNotification.Name("updateInspectionTable"), object: nil)
}
}
// CHECKABLE UITABLEVIEWCELL
class CheckableTableViewCell: UITableViewCell {
}

UICollectionView How to make a cell size itself dynamically based on its UIHostingConfiguration?

I have made an UICollectionView in which you can double tap a cell to resize it.
I'm using a CompositionalLayout, a DiffableDataSource and the new UIHostingConfiguration hosting a SwiftUI View which depends on an ObservableObject.
The resizing is triggered by updating the height property of the ObservableObject. That causes the SwiftUI View to change its frame which leads to the collectionView automatically resizing the cell. The caveat is that it does so immediately without animation only jumping between the old and the new frame of the view.
The ideal end-goal would be to be able to add a .animation() modifier to the SwiftUI View that then determines animation for both view and cell. Doing so now without additional setup makes the SwiftUI View animate but not the cell.
Is there a way to make the cell (orange) follow the size of the view (green) dynamically?
The proper way to manipulate the cell animation (as far as I known) is to override initialLayoutAttributesForAppearingItem() and finalLayoutAttributesForDisappearingItem() but since the cell just changes and doesn't appear/disappear they don't have an effect.
One could also think of Auto Layout constraints to archive this but I don’t think they are usable with UIHostingConfiguration?
I've also tried:
subclassing UICollectionViewCell and overriding apply(_ layoutAttributes: UICollectionViewLayoutAttributes) but it only effects the orange cell-background on initial appearance.
to put layout.invalidateLayout() or collectionView.layoutIfNeeded() inside UIView.animate() but it does not seem to have an effect on the size change.
Any thoughts, hints, ideas are greatly appreciated ✌️ Cheers!
Here is the code I used for the first gif:
struct CellContentModel {
var height: CGFloat? = 100
}
class CellContentController: ObservableObject, Identifiable {
let id = UUID()
#Published var cellContentModel: CellContentModel
init(cellContentModel: CellContentModel) {
self.cellContentModel = cellContentModel
}
}
class DataStore {
var data: [CellContentController]
var dataById: [CellContentController.ID: CellContentController]
init(data: [CellContentController]) {
self.data = data
self.dataById = Dictionary(uniqueKeysWithValues: data.map { ($0.id, $0) } )
}
static let testData = [
CellContentController(cellContentModel: CellContentModel()),
CellContentController(cellContentModel: CellContentModel(height: 80)),
CellContentController(cellContentModel: CellContentModel())
]
}
class CollectionViewController: UIViewController {
enum Section {
case first
}
var dataStore = DataStore(data: DataStore.testData)
private var layout: UICollectionViewCompositionalLayout!
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, CellContentController.ID>!
override func loadView() {
createLayout()
createCollectionView()
createDataSource()
view = collectionView
}
}
// - MARK: Layout
extension CollectionViewController {
func createLayout() {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(50))
let Item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.8), heightDimension: .estimated(300))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [Item])
let section = NSCollectionLayoutSection(group: group)
layout = .init(section: section)
}
}
// - MARK: CollectionView
extension CollectionViewController {
func createCollectionView() {
collectionView = .init(frame: .zero, collectionViewLayout: layout)
let doubleTapGestureRecognizer = DoubleTapGestureRecognizer()
doubleTapGestureRecognizer.doubleTapAction = { [unowned self] touch, _ in
let touchLocation = touch.location(in: collectionView)
guard let touchedIndexPath = collectionView.indexPathForItem(at: touchLocation) else { return }
let touchedItemIdentifier = dataSource.itemIdentifier(for: touchedIndexPath)!
dataStore.dataById[touchedItemIdentifier]!.cellContentModel.height = dataStore.dataById[touchedItemIdentifier]!.cellContentModel.height == 100 ? nil : 100
}
collectionView.addGestureRecognizer(doubleTapGestureRecognizer)
}
}
// - MARK: DataSource
extension CollectionViewController {
func createDataSource() {
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewCell, CellContentController.ID>() { cell, indexPath, itemIdentifier in
let cellContentController = self.dataStore.dataById[itemIdentifier]!
cell.contentConfiguration = UIHostingConfiguration {
TextView(cellContentController: cellContentController)
}
.background(.orange)
}
dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
}
var initialSnapshot = NSDiffableDataSourceSnapshot<Section, CellContentController.ID>()
initialSnapshot.appendSections([Section.first])
initialSnapshot.appendItems(dataStore.data.map{ $0.id }, toSection: Section.first)
dataSource.applySnapshotUsingReloadData(initialSnapshot)
}
}
class DoubleTapGestureRecognizer: UITapGestureRecognizer {
var doubleTapAction: ((UITouch, UIEvent) -> Void)?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
if touches.first!.tapCount == 2 {
doubleTapAction?(touches.first!, event)
}
}
}
struct TextView: View {
#StateObject var cellContentController: CellContentController
var body: some View {
Text(cellContentController.cellContentModel.height?.description ?? "nil")
.frame(height: cellContentController.cellContentModel.height, alignment: .top)
.background(.green)
}
}

Trying to hook up Compositional Layout CollectionView with PageControl. visibleItemsInvalidationHandler is not calling

I want to implement paging on a welcome screen in iOS app (iOS 13, swift 5.2, xcode 11.5).
For this purpose, I use a UICollectionView and UIPageControl. Now I need to bind pageControl to Collection view.
At first, I tried to use UIScrollViewDelegate but soon found out that it does not work with the compositional layout.
Then I discovered visibleItemsInvalidationHandler which is available for compositional layout sections.
I tried different options like this:
section.visibleItemsInvalidationHandler = { (visibleItems, point, env) -> Void in
self.pageControl.currentPage = visibleItems.last?.indexPath.row ?? 0
}
and like this:
section.visibleItemsInvalidationHandler = { items, contentOffset, environment in
let currentPage = Int(max(0, round(contentOffset.x / environment.container.contentSize.width)))
self.pageControl.currentPage = currentPage
}
but nothing works...
Seems like that callback is not triggering at all. If I place a print statement inside of it it is not executed.
Please find below the whole code:
import Foundation
import UIKit
class WelcomeVC: UIViewController, UICollectionViewDelegate {
//MARK: - PROPERTIES
var cardsDataSource: UICollectionViewDiffableDataSource<Int, WalkthroughCard>! = nil
var cardsSnapshot = NSDiffableDataSourceSnapshot<Int, WalkthroughCard>()
var cards: [WalkthroughCard]!
var currentPage = 0
//MARK: - OUTLETS
#IBOutlet weak var signInButton: PrimaryButton!
#IBOutlet weak var walkthroughCollectionView: UICollectionView!
#IBOutlet weak var pageControl: UIPageControl!
//MARK: - VIEW DID LOAD
override func viewDidLoad() {
super.viewDidLoad()
walkthroughCollectionView.isScrollEnabled = true
walkthroughCollectionView.delegate = self
setupCards()
pageControl.numberOfPages = cards.count
configureCardsDataSource()
configureCardsLayout()
}
//MARK: - SETUP CARDS
func setupCards() {
cards = [
WalkthroughCard(title: "Welcome to abc", image: "Hello", description: "abc is an assistant to your xyz account at asdf"),
WalkthroughCard(title: "Have all asdf projects at your fingertips", image: "Graphs", description: "Enjoy all project related data whithin a few taps. Even offline")
]
}
//MARK: - COLLECTION VIEW DIFFABLE DATA SOURCE
private func configureCardsDataSource() {
cardsDataSource = UICollectionViewDiffableDataSource<Int, WalkthroughCard>(collectionView: walkthroughCollectionView) {
(collectionView: UICollectionView, indexPath: IndexPath, card: WalkthroughCard) -> UICollectionViewCell? in
// Create cell
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: "WalkthroughCollectionViewCell",
for: indexPath) as? WalkthroughCollectionViewCell else { fatalError("Cannot create new cell") }
//cell.layer.cornerRadius = 15
cell.walkthroughImage.image = UIImage(named: card.image)
cell.walkthroughTitle.text = card.title
cell.walkthroughDescription.text = card.description
return cell
}
setupCardsSnapshot()
}
private func setupCardsSnapshot() {
cardsSnapshot = NSDiffableDataSourceSnapshot<Int, WalkthroughCard>()
cardsSnapshot.appendSections([0])
cardsSnapshot.appendItems(cards)
cardsDataSource.apply(self.cardsSnapshot, animatingDifferences: true)
}
//MARK: - CONFIGURE COLLECTION VIEW LAYOUT
func configureCardsLayout() {
walkthroughCollectionView.collectionViewLayout = generateCardsLayout()
}
func generateCardsLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let fullPhotoItem = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: groupSize,
subitem: fullPhotoItem,
count: 1
)
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPagingCentered
let layout = UICollectionViewCompositionalLayout(section: section)
//setup pageControl
section.visibleItemsInvalidationHandler = { (items, offset, env) -> Void in
self.pageControl.currentPage = items.last?.indexPath.row ?? 0
}
return layout
}
}
It should be working if you set up the invalidation handler for the section before passing the section to the layout:
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPagingCentered
section.visibleItemsInvalidationHandler = { (items, offset, env) -> Void in
self.pageControl.currentPage = items.last?.indexPath.row ?? 0
}
return UICollectionViewCompositionalLayout(section: section)
I think you should give a chance to UIPageViewController. I'm attaching its implementation so that you can easily integrate with your code.
You can design your slider items in a separate UIViewController and use wherever area you want within your main view for UIPageViewController.
Cheers!
import UIKit
import SnapKit
class WelcomeViewController: BaseViewController<WelcomeViewModel> {
#IBOutlet weak var pageControl: UIPageControl!
#IBOutlet weak var viewForPageController: UIView!
private var pageViewController: UIPageViewController!
private var introductionSliderViewControllers: [UIViewController] = []
override func viewDidLoad() {
super.viewDidLoad()
viewModel.welcomeSlidersLoaded = { [weak self] sliders in
self?.loadSliders(sliders: sliders)
}
}
private func loadSliders(sliders: [IntroductionSliderViewModel]) {
guard sliders.count > 0 else { return }
self.introductionSliderViewControllers = sliders.map{
IntroductionSliderViewController(viewModel: $0)
}
self.initializePageViewController()
}
private func initializePageViewController(){
self.pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
self.viewForPageController.addSubview(self.pageViewController.view)
self.addChild(pageViewController)
self.pageViewController.view.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
}
self.pageViewController.delegate = self
self.pageViewController.dataSource = self
self.pageViewController.setViewControllers([introductionSliderViewControllers[0]], direction: .forward, animated: false, completion: nil)
self.pageControl.numberOfPages = introductionSliderViewControllers.count
self.pageControl.currentPage = 0
}
}
extension WelcomeViewController: UIPageViewControllerDataSource{
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let index = self.introductionSliderViewControllers.firstIndex(of: viewController),
index > 0 else{return nil}
return self.introductionSliderViewControllers[index - 1]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let index = self.introductionSliderViewControllers.firstIndex(of: viewController),
index < self.introductionSliderViewControllers.count - 1 else{return nil}
return self.introductionSliderViewControllers[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if let vc = pageViewController.viewControllers?.first, let index = self.introductionSliderViewControllers.firstIndex(of: vc){
pageControl.currentPage = index
}
}
}
This should work
section.visibleItemsInvalidationHandler = { items, contentOffset, environment in
let currentPage = Int(max(0, round(contentOffset.x / environment.container.contentSize.width)))
self.pageControl.currentPage = currentPage
}
I know this question is old, but I think it can help someone:
section.visibleItemsInvalidationHandler = { [weak self] (items, offset, env) -> Void in
guard let self = self,
let itemWidth = items.last?.bounds.width else { return }
// This offset is different from a scrollView. It increases by the item width + the spacing between items.
// So we need to divide the offset by the sum of them.
let page = round(offset.x / (itemWidth + section.interGroupSpacing))
self.didChangeCollectionViewPage(to: Int(page))
}
As I commented in the code snippet, the offset here is different, it sums the item width and the section spacing, so instead of dividing the offset by the content width, you need to divide it by the item width and the intergroup spacing.
It may not help you if you have different item widths, but I'm my case, where all the items have the same width, it works.
Maybe this solution might helps.
It's pretty simple and it worked smoothly for me
section.visibleItemsInvalidationHandler = { [weak self] _, offset, environment in
guard let self else { return }
let pageWidth = environment.container.contentSize.width
let currentPage = Int((offset.x / pageWidth).rounded())
self.pageControl.currentPage = currentPage
}

ViewWillDisappear not getting called searchcontroller

When I'm in the middle of a search and then switch UItabs, ViewWillDisappear does not get called. Any idea as to why ViewWillDisappear does not get called when I have filtered results displaying and switch tabs?
func updateSearchResultsForSearchController(searchController: UISearchController) {
if self.searchController?.searchBar.text.lengthOfBytesUsingEncoding(NSUTF32StringEncoding) > 0 {
if let results = self.results {
results.removeAllObjects()
} else {
results = NSMutableArray(capacity: MyVariables.dictionary.keys.array.count)
}
let searchBarText = self.searchController!.searchBar.text
let predicate = NSPredicate(block: { (city: AnyObject!, b: [NSObject : AnyObject]!) -> Bool in
var range: NSRange = NSMakeRange(0, 0)
if city is NSString {
range = city.rangeOfString(searchBarText, options: NSStringCompareOptions.CaseInsensitiveSearch)
}
return range.location != NSNotFound
})
// Get results from predicate and add them to the appropriate array.
let filteredArray = (MyVariables.dictionary.keys.array as NSArray).filteredArrayUsingPredicate(predicate)
self.results?.addObjectsFromArray(filteredArray)
// Reload a table with results.
self.searchResultsController?.tableView.reloadData()
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(self.identifier) as! UITableViewCell
var text: String?
var imgtext:AnyObject?
if tableView == self.searchResultsController?.tableView {
if let results = self.results {
text = self.results!.objectAtIndex(indexPath.row) as? String
imgtext = MyVariables.dictionary[text!]
let decodedData = NSData(base64EncodedString: imgtext! as! String, options: NSDataBase64DecodingOptions(rawValue: 0) )
var decodedimage = UIImage(data: decodedData!)
cell.imageView?.image = decodedimage
}
} else {
text = MyVariables.dictionary.keys.array[indexPath.row] as String
}
cell.textLabel!.text = text
return cell
}
On the Load
override func viewDidLoad() {
super.viewDidLoad()
let resultsTableView = UITableView(frame: self.tableView.frame)
self.searchResultsController = UITableViewController()
self.searchResultsController?.tableView = resultsTableView
self.searchResultsController?.tableView.dataSource = self
self.searchResultsController?.tableView.delegate = self
// Register cell class for the identifier.
self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: self.identifier)
self.searchResultsController?.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: self.identifier)
self.searchController = UISearchController(searchResultsController: self.searchResultsController!)
self.searchController?.searchResultsUpdater = self
self.searchController?.delegate = self
self.searchController?.searchBar.sizeToFit()
self.searchController?.hidesNavigationBarDuringPresentation = false;
self.tableView.tableHeaderView = self.searchController?.searchBar
self.definesPresentationContext = true
}
Had the same issue. viewWillDisappear is not called on the UITableViewController, but it is called in the UISearchController.
So I subclassed UISearchController and overrode the viewWillDisappear method. In my case I just needed to deactivate the search controller.
class SearchController: UISearchController {
override func viewWillDisappear(_ animated: Bool) {
// to avoid black screen when switching tabs while searching
isActive = false
}
}
Similar to #user5130344 I found that subclassing resolved my issue, although I found that isActive = false cleared the search bar where I wanted the search query to remain on returning to the view.
Here's my subclass instead - this fixed my issue with iOS 13 dismissing the parent view:
class MySearchController: UISearchController {
override func viewWillDisappear(_ animated: Bool) {
// to avoid black screen when switching tabs while searching
self.dismiss(animated: true)
}
}
I think its the problem with the xcode . Try to close it and reopen the project once again and try to run again

wait until scrollToRowAtIndexPath is done in swift

I have a UITableView with more cells than fit on screen. When I get a notification from my data model I want to jump to a specific row and show a very basic animation.
My code is:
func animateBackgroundColor(indexPath: NSIndexPath) {
dispatch_async(dispatch_get_main_queue()) {
NSLog("table should be at the right position")
if let cell = self.tableView.cellForRowAtIndexPath(indexPath) as? BasicCardCell {
var actColor = cell.backgroundColor
self.manager.vibrate()
UIView.animateWithDuration(0.2, animations: { cell.backgroundColor = UIColor.redColor() }, completion: {
_ in
UIView.animateWithDuration(0.2, animations: { cell.backgroundColor = actColor }, completion: { _ in
self.readNotificationCount--
if self.readNotificationCount >= 0 {
var legicCard = self.legicCards[indexPath.section]
legicCard.wasRead = false
self.reloadTableViewData()
} else {
self.animateBackgroundColor(indexPath)
}
})
})
}
}
}
func cardWasRead(notification: NSNotification) {
readNotificationCount++
NSLog("\(readNotificationCount)")
if let userInfo = notification.userInfo as? [String : AnyObject], let index = userInfo["Index"] as? Int {
dispatch_sync(dispatch_get_main_queue()){
self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: index), atScrollPosition: .None, animated: true)
self.tableView.layoutIfNeeded()
NSLog("table should scroll to selected row")
}
self.animateBackgroundColor(NSIndexPath(forRow: 0, inSection: index))
}
}
I hoped that the dispatch_sync part would delay the execution of my animateBackgroundColor method until the scrolling is done. Unfortunately that is not the case so that animateBackgroundColor gets called when the row is not visible yet -> cellForRowAtIndexPath returns nil and my animation won't happen. If no scrolling is needed the animation works without problem.
Can anyone tell my how to delay the execution of my animateBackgroundColor function until the scrolling is done?
Thank you very much and kind regards
Delaying animation does not seem to be a good solution for this since scrollToRowAtIndexPath animation duration is set based on distance from current list item to specified item. To solve this you need to execute animateBackgroudColor after scrollToRowAtIndexPath animation is completed by implementing scrollViewDidEndScrollingAnimation UITableViewDelegate method. The tricky part here is to get indexPath at which tableview did scroll. A possible workaround:
var indexPath:NSIndexpath?
func cardWasRead(notification: NSNotification) {
readNotificationCount++
NSLog("\(readNotificationCount)")
if let userInfo = notification.userInfo as? [String : AnyObject], let index = userInfo["Index"] as? Int{
dispatch_sync(dispatch_get_main_queue()){
self.indexPath = NSIndexPath(forRow: 0, inSection: index)
self.tableView.scrollToRowAtIndexPath(self.indexPath, atScrollPosition: .None, animated: true)
self.tableView.layoutIfNeeded()
NSLog("table should scroll to selected row")
}
}
}
func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) {
self.animateBackgroundColor(self.indexPath)
indexPath = nil
}
Here are my solution
1) Create a .swift file, and copy code below into it:
typealias SwagScrollCallback = (_ finish: Bool) -> Void
class UICollectionViewBase: NSObject, UICollectionViewDelegate {
static var shared = UICollectionViewBase()
var tempDelegate: UIScrollViewDelegate?
var callback: SwagScrollCallback?
func startCheckScrollAnimation(scroll: UIScrollView, callback: SwagScrollCallback?){
if let dele = scroll.delegate {
self.tempDelegate = dele
}
self.callback = callback
scroll.delegate = self
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
callback?(true)
if let dele = self.tempDelegate {
scrollView.delegate = dele
tempDelegate = nil
}
}
}
extension UICollectionView {
func scrollToItem(at indexPath: IndexPath, at scrollPosition: UICollectionView.ScrollPosition, _ callback: SwagScrollCallback?){
UICollectionViewBase.shared.startCheckScrollAnimation(scroll: self, callback: callback)
self.scrollToItem(at: indexPath, at: scrollPosition, animated: true)
}
}
2) Example:
#IBAction func onBtnShow(){
let index = IndexPath.init(item: 58, section: 0)
self.clv.scrollToItem(at: index, at: .centeredVertically) { [weak self] (finish) in
guard let `self` = self else { return }
// Change color temporarily
if let cell = self.clv.cellForItem(at: index) as? MyCell {
cell.backgroundColor = .yellow
cell.lbl.textColor = .red
}
// Reset
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.clv.reloadData()
}
}
}
3) My github code example: github here
I has similar problem. I just do this.
UIView.animateWithDuration(0.2,delay:0.0,options: nil,animations:{
self.tableView.scrollToRowAtIndexPath(self.indexPath!, atScrollPosition: .Middle, animated: true)},
completion: { finished in UIView.animateWithDuration(0.5, animations:{
self.animateBackgroundColor(self.indexPath)})})}

Resources