The problem:
I have a master UIPageViewController (MainPageVC) with three imbedded page views (A, B, & C) that are accessible both with swipe gestures and by pressing the appropriate locations in a custom page indicator* in the MainPageVC (*not a true UIPageControl but comprised of three ToggleButtons - a simple reimplementation of UIButton to become a toggle-button). My setup is as follows:
Previous reading:
Reliable way to track Page Index in a UIPageViewController - Swift, A reliable way to get UIPageViewController current index, and UIPageViewController: return the current visible view
indicated that the best way to do this was with didFinishAnimating calls, and manually keep track of the current page index, but I'm finding that this does not deal with certain edge cases.
I have been trying to produce a safe way of keeping track of the current page index (with didFinishAnimating and willTransitionTo methods) but am having trouble with the edge case where a user is in view A, and then swipes all the way across to C (without lifting up their finger), and then beyond C, and then releasing their finger... in this instance didFinishAnimating isn't called and the app still believes it is in A (i.e. A toggle button is still pressed and pageIndex is not updated correctly by the viewControllerBefore and viewControllerAfter methods).
My code:
#IBOutlet weak var pagerView: UIView!
#IBOutlet weak var aButton: ToggleButton!
#IBOutlet weak var bButton: ToggleButton!
#IBOutlet weak var cButton: ToggleButton!
let viewControllerNames = ["aVC", "bVC", "cVC"]
lazy var buttonsArray = {
[aButton, bButton, cButton]
}()
var previousPage = "aVC"
var pageVC: UIPageViewController?
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
print("TESTING - will transition to")
let currentViewControllerClass = String(describing: pageViewController.viewControllers![0].classForCoder);
let viewControllerIndex = viewControllerNames.index(of: currentViewControllerClass);
if currentViewControllerClass == previousPage {
return
}
let pastIndex = viewControllerNames.index(of: previousPage)
if buttonsArray[pastIndex!]?.isOn == true {
buttonsArray[pastIndex!]?.buttonPressed()
}
if let newPageButton = buttonsArray[viewControllerIndex!] {
newPageButton.buttonPressed()
}
self.previousPage = currentViewControllerClass
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
print("TESTING - did finish animating")
let currentViewControllerClass = String(describing: pageViewController.viewControllers![0].classForCoder)
let viewControllerIndex = viewControllerNames.index(of: currentViewControllerClass)
if currentViewControllerClass == previousPage {
return
}
let pastIndex = viewControllerNames.index(of: previousPage)
if buttonsArray[pastIndex!]?.isOn == true {
buttonsArray[pastIndex!]?.buttonPressed()
}
if let newPageButton = buttonsArray[viewControllerIndex!] {
newPageButton.buttonPressed()
}
self.previousPage = currentViewControllerClass
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
let onboardingViewControllerClass = String(describing: viewController.classForCoder)
let viewControllerIndex = viewControllerNames.index(of: onboardingViewControllerClass)
let newViewControllerIndex = viewControllerIndex! - 1
if(newViewControllerIndex < 0) {
return nil
} else {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: viewControllerNames[newViewControllerIndex])
if let vc = vc as? BaseTabVC {
vc.mainPageVC = self
vc.intendedCollectionViewHeight = pagerViewHeight
}
return vc
}
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
let onboardingViewControllerClass = String(describing: viewController.classForCoder)
let viewControllerIndex = viewControllerNames.index(of: onboardingViewControllerClass)
let newViewControllerIndex = viewControllerIndex! + 1
if(newViewControllerIndex > viewControllerNames.count - 1) {
return nil
} else {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: viewControllerNames[newViewControllerIndex])
if let vc = vc as? BaseTabVC {
vc.mainPageVC = self
vc.intendedCollectionViewHeight = pagerViewHeight
}
return vc
}
}
I'm at a loss as to how to deal with this edge case, the problem is that it can lead to fatal crashes of the app if the user then tries to press something in C that should otherwise be guaranteed to exist, and an unexpected nil or indexOutOfBounds error is thrown.
Very well written question. Especially for a newbie. (Voted.) You clearly state the problem you're having, including illustrations and your current code.
The solution I proposed in another thread was to subclass UIPageControl and have it implement a didSet on its currentPage property. You can then have the page control notify the view controller of the current page index. (By giving your custom subclass a delegate property, by sending a notification center message, or whatever method best fits your needs.)
(I did a simple test of this approach and it worked. I didn't test exhaustively however.)
The fact that the UIPageViewController reliably updates the page control but that there's no reliable, obvious way to figure out the current page index seems like an oversight in the design of this class.
Own Solution
I found the solution to this: don't use a UIPageView(Controller), use a CollectionView(Controller) instead. It is MUCH easier to keep track of the position of a collection view than to try and manually keep track of the current page in a UIPageViewController.
The solution is as follows:
Method
Refactor MainPagerVC as a CollectionView(Controller) (or as a regular VC that conforms to the UICollectionViewDelegate UICollectionViewDataSource protocols).
Set each page (aVC, bVC, and cVC) as a UICollectionViewCell subclass (MainCell).
Set each of these pages to fill the MainPagerVC.collectionView within the screen's bounds - CGSize(width: view.frame.width, height: collectionView.bounds.height).
Refactor the toggle-buttons at the top (A, B, and C) as three UICollectionViewCell subclasses (MenuCell) in a MenuController (itself a UICollectionViewController.
As collection views inherit from UIScrollView you can implement scrollViewDidScroll, scrollViewDidEndScrollingAnimation and scrollViewWillEndDragging methods, along with delegation (with didSelectItemAt indexPath) to couple the MainPagerVC and MenuController collection views.
Code
class MainPagerVC: UIViewController, UICollectionViewDelegateFlowLayout {
fileprivate let menuController = MenuVC(collectionViewLayout: UICollectionViewFlowLayout())
fileprivate let cellId = "cellId"
fileprivate let pages = ["aVC", "bVC", "cVC"]
let collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = 0
layout.scrollDirection = .horizontal
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .white
cv.showsVerticalScrollIndicator = false
cv.showsHorizontalScrollIndicator = false
return cv
}()
override func viewDidLoad() {
super.viewDidLoad()
menuController.delegate = self
setupLayout()
}
fileprivate func setupLayout() {
guard let menuView = menuController.view else { return }
view.addSubview(menuView)
view.addSubview(collectionView)
collectionView.dataSource = self
collectionView.delegate = self
//Setup constraints (placing the menuView above the collectionView
collectionView.register(MainCell.self, forCellWithReuseIdentifier: cellId)
//Make the collection view behave like a pager view (no overscroll, paging enabled)
collectionView.isPagingEnabled = true
collectionView.bounces = false
collectionView.allowsSelection = true
menuController.collectionView.selectItem(at: [0, 0], animated: true, scrollPosition: .centeredHorizontally)
}
}
extension MainPagerVC: MenuVCDelegate {
// Delegate method implementation (scroll to the right page when the corresponding Menu "Button"(Item) is pressed
func didTapMenuItem(indexPath: IndexPath) {
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
}
extension MainPagerVC: UICollectionViewDelegate, UICollectionViewDataSource {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let x = scrollView.contentOffset.x
let offset = x / pages.count
menuController.menuBar.transform = CGAffineTransform(translationX: offset, y: 0)
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
let item = Int(scrollView.contentOffset.x / view.frame.width)
let indexPath = IndexPath(item: item, section: 0)
collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .bottom)
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let x = targetContentOffset.pointee.x
let item = Int(x / view.frame.width)
let indexPath = IndexPath(item: item, section: 0)
menuController.collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return pages.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MainCell
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return .init(width: view.frame.width, height: collectionView.bounds.height)
}
}
class MainCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
// Custom UIColor extension to return a random colour (to check that everything is working)
backgroundColor = UIColor().random()
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
}
protocol MenuVCDelegate {
func didTapMenuItem(indexPath: IndexPath)
}
class MenuVC: UICollectionViewController, UICollectionViewDelegateFlowLayout {
fileprivate let cellId = "cellId"
fileprivate let menuItems = ["A", "B", "C"]
var delegate: MenuVCDelegate?
//Sliding bar indicator (slightly different from original question - like Reddit)
let menuBar: UIView = {
let v = UIView()
v.backgroundColor = .red
return v
}()
//1px view to visually separate MenuBar region from "pager"-views
let menuSeparator: UIView = {
let v = UIView()
v.backgroundColor = .gray
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
collectionView.backgroundColor = .white
collectionView.allowsSelection = true
collectionView.register(MenuCell.self, forCellWithReuseIdentifier: cellId)
if let layout = collectionViewLayout as? UICollectionViewFlowLayout {
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
}
//Add views and setup constraints for collection view, separator view and "selection indicator" view - the menuBar
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
delegate?.didTapMenuItem(indexPath: indexPath)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return menuItems.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MenuCell
cell.label.text = menuItems[indexPath.item]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = view.frame.width
return .init(width: width/CGFloat(menuItems.count), height: view.frame.height)
}
}
class MenuCell: UICollectionViewCell {
let label: UILabel = {
let l = UILabel()
l.text = "Menu Item"
l.textAlignment = .center
l.textColor = .gray
return l
}()
override var isSelected: Bool {
didSet {
label.textColor = isSelected ? .black : .gray
}
}
override init(frame: CGRect) {
super.init(frame: frame)
//Add label to view and setup constraints to fill Cell
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
}
References
A "Lets Build That App" YouTube Video: "We Made It on /r/iosprogramming! Live coding swiping pages feature"
Related
I am trying to create a custom transition, similar to the one that is on the App Store. I used these tutorials:
https://eric-dockery283.medium.com/custom-view-transitions-like-the-new-app-store-a2a1181229b6
https://www.raywenderlich.com/2925473-ios-animation-tutorial-custom-view-controller-presentation-transitions
I have also been experimenting with using the load view to have the root view to create the view, similar to what is used in the answer used here: does loadView get called even if we don't override it in viewcontroller like the other ViewController lifecycle methods?
now what I am doing is that I am using a collectionview, which I want to keep, and when an item is pressed it animate transitions to a new view controller with a larger image, simple.
the problem is that when I combine it with using the loadView with view = rootView() it did not work in some scenarios.
For example:
when I use the modalPresentationStyle = .fullScreen, when I press an item in the collection view the screen just goes black, and then when I press the view hierarchy it shows this:
when I remove the modalPresentationStyle = .fullScreen it does animate but it is the popover transition, which I do not want.
What I want is to have a full screen transition that is animated.
here is the view controller:
class ViewController: UIViewController {
let data = createData()
var transition = PopAnimator()
var rootView = MainView()
private let collectionView: UICollectionView = {
let viewLayout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: viewLayout)
collectionView.backgroundColor = .white
return collectionView
}()
private enum LayoutConstant {
static let spacing: CGFloat = 16.0
static let itemHeight: CGFloat = 200.0
}
override func loadView() {
view = rootView
rootView.collectionView.delegate = self
rootView.collectionView.dataSource = self
}
}
class MainView: UIView {
let collectionView: UICollectionView = {
let viewLayout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: viewLayout)
collectionView.backgroundColor = .white
return collectionView
}()
init() {
super.init(frame: .zero)
setupLayouts()
}
private func setupLayouts() {
backgroundColor = .white
addSubview(collectionView)
collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: CollectionViewCell.identifier)
collectionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor),
collectionView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor),
collectionView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CollectionViewCell.identifier, for: indexPath) as! CollectionViewCell
cell.dogImg.image = UIImage(named: data[indexPath.row].image)
return cell
}
}
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let selected = data[indexPath.row]
let vc = OtherViewController()
vc.transitioningDelegate = self
// vc.modalPresentationStyle = .fullScreen
vc.data = selected
self.present(vc, animated: true)
print(selected)
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = itemWidth(for: view.frame.width, spacing: LayoutConstant.spacing)
return CGSize(width: width, height: LayoutConstant.itemHeight)
}
func itemWidth(for width: CGFloat, spacing: CGFloat) -> CGFloat {
let itemsInRow: CGFloat = 2
let totalSpacing: CGFloat = 2 * spacing + (itemsInRow - 1) * spacing
let finalWidth = (width - totalSpacing) / itemsInRow
return floor(finalWidth)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: LayoutConstant.spacing, left: LayoutConstant.spacing, bottom: LayoutConstant.spacing, right: LayoutConstant.spacing)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return LayoutConstant.spacing
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return LayoutConstant.spacing
}
}
extension ViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
guard let selectedIndexPathCell = self.collectionView.indexPathsForSelectedItems?.first,
let selectedItem = self.collectionView.cellForItem(at: selectedIndexPathCell) as? CollectionViewCell
else { return nil }
guard let originFrame = selectedItem.superview?.convert(selectedItem.frame, to: nil) else {
return transition
}
transition.originFrame = originFrame
transition.presenting = true
return transition
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.presenting = false
return transition
}
}
this is the other view that is presented to.
class OtherViewController: UIViewController, UIViewControllerTransitioningDelegate {
var data: DogArr?
var rootView = testView1()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .systemBackground
}
override func loadView() {
view = rootView
//rootView.inputViewController?.transitioningDelegate = self
rootView.carImg.image = UIImage(named: data?.image ?? "")
rootView.dismissBtn.addTarget(self, action: #selector(dismissingBtn), for: .touchUpInside)
}
#objc func dismissingBtn() {
self.dismiss(animated: true)
}
}
class testView1: UIView {
var carImg: UIImageView = {
let img = UIImageView()
img.contentMode = .scaleAspectFit
img.translatesAutoresizingMaskIntoConstraints = false
return img
}()
var dismissBtn: UIButton = {
let btn = UIButton()
btn.translatesAutoresizingMaskIntoConstraints = false
btn.setTitle("Done", for: .normal)
btn.titleLabel?.font = UIFont.systemFont(ofSize: 25)
btn.titleLabel?.textAlignment = .center
btn.setTitleColor(.label, for: .normal)
btn.contentMode = .scaleAspectFill
return btn
}()
init() {
super.init(frame: .zero)
self.addSubview(carImg)
self.addSubview(dismissBtn)
carImg.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = .systemBackground
NSLayoutConstraint.activate([
carImg.topAnchor.constraint(equalTo: self.topAnchor),
carImg.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.5),
carImg.widthAnchor.constraint(equalTo: carImg.heightAnchor),
dismissBtn.topAnchor.constraint(equalTo: self.carImg.bottomAnchor, constant: 20),
dismissBtn.centerXAnchor.constraint(equalTo: self.dismissBtn.centerXAnchor),
dismissBtn.widthAnchor.constraint(equalToConstant: 150),
dismissBtn.heightAnchor.constraint(equalToConstant: 75)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I noticed that when I remove the root view and just created everything in the view controller, it seems to work, the thing is that this is just a test for now. The root view in a future project may be very large so I think keeping to a rootview may be a good idea. If there are any other methods similar, please share.
If there is anything I can answer please ask,
Thank you
I have a viewController with a collectionView inside of it. I'm pretty sure that I have configured everything right yet it is not rendering any cells. I have added the appropriate delegates and data sources and double checked to see if the data is loading and it is but cells are not being populated
import UIKit
import UIKit
import Alamofire
import AlamofireNetworkActivityIndicator
import SwiftLocation
import CoreLocation
import AMScrollingNavbar
class NewHomeFeedControllerViewController: UIViewController {
let detailView = EventDetailViewController()
var allEvents = [Event]()
let customCellIdentifier1 = "customCellIdentifier1"
var grideLayout = GridLayout(numberOfColumns: 2)
let refreshControl = UIRefreshControl()
var newHomeFeed: NewHomeFeedControllerViewController?
let paginationHelper = PaginationHelper<Event>(serviceMethod: PostService.showEvent)
lazy var dropDownLauncer : DropDownLauncher = {
let launcer = DropDownLauncher()
launcer.newHomeFeed = self
return launcer
}()
// 1 IGListKit uses IGListCollectionView, which is a subclass of UICollectionView, which patches some functionality and prevents others.
let collectionView: UICollectionView = {
// 2 This starts with a zero-sized rect since the view isn’t created yet. It uses the UICollectionViewFlowLayout just as the ClassicFeedViewController did.
let view = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout())
// 3 The background color is set to white
view.backgroundColor = UIColor.white
return view
}()
func handleDropDownMenu(){
dropDownLauncer.showDropDown()
}
func configureCollectionView() {
// add pull to refresh
refreshControl.addTarget(self, action: #selector(reloadHomeFeed), for: .valueChanged)
collectionView.addSubview(refreshControl)
}
func reloadHomeFeed() {
self.paginationHelper.reloadData(completion: { [unowned self] (events) in
self.allEvents = events
if self.refreshControl.isRefreshing {
self.refreshControl.endRefreshing()
}
DispatchQueue.main.async {
self.collectionView.reloadData()
}
})
}
func categoryFetch(dropDown: DropDown){
navigationItem.title = dropDown.name
paginationHelper.category = dropDown.name
configureCollectionView()
reloadHomeFeed()
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
collectionView.contentInset = UIEdgeInsetsMake(15, 0, 0, 0)
navigationItem.title = "Home"
collectionView.dataSource = self
collectionView.delegate = self
collectionView.collectionViewLayout = grideLayout
collectionView.reloadData()
collectionView.register(CustomCell.self, forCellWithReuseIdentifier: customCellIdentifier1)
// self.navigationItem.hidesBackButton = true
let backButton = UIBarButtonItem(image: UIImage(named: "menu"), style: .plain, target: self, action: #selector(handleDropDownMenu))
self.navigationItem.leftBarButtonItem = backButton
configureCollectionView()
reloadHomeFeed()
// Do any additional setup after loading the view.
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let navigationController = self.navigationController as? ScrollingNavigationController {
navigationController.followScrollView(self.collectionView, delay: 50.0)
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if let navigationController = navigationController as? ScrollingNavigationController {
navigationController.stopFollowingScrollView()
}
}
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
if let navigationController = navigationController as? ScrollingNavigationController {
navigationController.showNavbar(animated: true)
}
return true
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
grideLayout.invalidateLayout()
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
extension NewHomeFeedControllerViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return allEvents.count
}
// The cell that is returned must be retrieved from a call to -dequeueReusableCellWithReuseIdentifier:forIndexPath:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let customCell = collectionView.dequeueReusableCell(withReuseIdentifier: customCellIdentifier1, for: indexPath) as! CustomCell
let imageURL = URL(string: allEvents[indexPath.item].currentEventImage)
print(imageURL ?? "")
customCell.sampleImage.af_setImage(withURL: imageURL!)
return customCell
}
}
extension NewHomeFeedControllerViewController: UICollectionViewDelegateFlowLayout{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if indexPath.item == 0 || indexPath.item == 1 {
return CGSize(width: view.frame.width, height: grideLayout.itemSize.height)
}else{
return grideLayout.itemSize
}
}
}
Any idea what could be going on?
Sorry you guys forgot to add this
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView.frame = view.bounds
}
Haha thanks for the help
I have a collection in the ViewController1, if i click that it goes to ViewController2 where i can change a image; when i push back button on the navigation controller to go back in the ViewController1 i should see the image i changed in the ViewController2. My problem is that i need to reload the data of the CollectionView but i can't do it! I already tried to put CollectionView.reloaddata() in the **ViewWillAppear**, but nothing happened! How can i do this?
import UIKit
private let reuseIdentifier = "Cell2"
class CollectionViewControllerStatiVegetarian: UICollectionViewController {
let baza1 = Baza()
#IBOutlet var CollectionViewOut: UICollectionView!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
CollectionViewOut.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView!.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
let backround = CAGradientLayer().turquoiseColor()
backround.frame = self.view.bounds
self.collectionView?.backgroundView = UIView()
self.collectionView?.backgroundView?.layer.insertSublayer(backround, at: 0)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let size2 = baza1.superTuples(name: "2")
let x = Mirror(reflecting: size2).children.count //
return Int(x+1)
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell2", for: indexPath) as! CollectionViewCell_VegetarianStaty
if indexPath.row != 0 {
cell.shapka_stati_vegetarian.image = nil
let superTupl = baza1.superTuplesShapka(Nomer_tupl: (indexPath.row-1))
cell.label.text = superTupl.5
let tupl = baza1.superTuplesShapka(Nomer_tupl: (indexPath.row-1))
if (tupl.2 == 1) {
cell.shapka_stati_vegetarian.image = nil
cell.shapka_stati_vegetarian.image = UIImage(named: "fon_galochka.png")
} else {}
} else {
cell.shapka_stati_vegetarian.image = UIImage(named: "shapkastaty")
cell.label.text = ""
}
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let screenWidth = UIScreen.main.fixedCoordinateSpace.bounds.width
let height = screenWidth*550/900+20
var size = CGSize(width: screenWidth, height: 73)
if indexPath.row==0 {
size = CGSize(width: screenWidth, height: height)
}
return size
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.row != 0 {
numb_cell = indexPath.row
let bazaSh = Baza()
let f = bazaSh.superTuplesShapka(Nomer_tupl: (indexPath.row-1) )
let vc = storyboard?.instantiateViewController(withIdentifier: "ViewStaty") as! ViewController
vc.obr = f.3
self.navigationController?.pushViewController(vc, animated: true)
}
}
}
Views are loaded only once in the lifetime of a view controller, so viewDidLoad is only run once.
One way to do this is to reload the data in viewWillAppear which is fired when the view appears, but this might run many times.
Another way is to have a delegate method of vc2 that is implemented by vc1. This delegate method is run when the data is changed in vc2 and since vc1 implements the delegate, it can then choose to reload the view.
Yet another way, and one that I prefer, is to use something like Core Data as a model. That way when vc2 changes the data, vc1 can be observing the state of objects it is interested in and react to changes in the model through the NSFetchedResultsControllerDelegate methods.
You could choose to use Realm as a persistence mechanism, and I'm sure there is a similar way to observe the model and react to changes.
inn order to reloadData in background thread, you need to use
DispatchQueue.main.async { self.collectionView.reloadData() }
implement your vc from
UICollectionViewDelegate , UICollectionViewDataSource
then in
viewDidLoad()
self.collectionView.delegate = self
self.collectionView.dataSource = self
I need to reload the collection view while navigating back to that particular view.
I tried implementing all the following things but its not working
1.calling self.collectionView.reloadData() in viewDidAppear()
2.calling self.collectionView.reloadData() in viewWillAppear()
even i tried calling viewDidLoad() in viewDidAppear but yet it is not working.
And checked the other same type of questions but it is not working...
the only way it works is by performing segue action but I dont think so it is efficent can anyone suggest me any idea??
enter code here class RoomDeviceListVC: UIViewController , UICollectionViewDelegate , UICollectionViewDataSource , UICollectionViewDelegateFlowLayout ,segueAction{
let functions = DataFunctionalities()
#IBOutlet weak var collectionView1: UICollectionView!
#IBOutlet weak var collectionView2: UICollectionView!
var horizontalBarLeadingConstraint : NSLayoutConstraint?
static var isVisible : Bool?
static var CollectionViewSection : Int!
static var reloadFlag : Int!
static var delegates : CollectionViewRotation? = nil
var uiFunctions = UIFunctions()
var selectedIndex = IndexPath(item: 0, section: 0)
var constant : CGFloat?
let headerView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
uiFunctions.setViewBackground(view: self.view)
self.navigationItem.hidesBackButton = true
setupHeader()
setupConstraints()
setupHorizontalBar()
collectionView2.clipsToBounds = true
RoomDeviceListVC.CollectionViewSection = selectedIndex.section
BottomViewRoomCell.delegates = self
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
// self.collectionView2.reloadData()
}
override func viewDidAppear(_ animated: Bool) {
super.viewWillAppear(true)
self.collectionView2.reloadData()
RoomDeviceListVC.isVisible = true
}
override func viewDidDisappear(_ animated: Bool) {
RoomDeviceListVC.isVisible = false
}
func setupHorizontalBar(){
let horizontalBar = UIView()
self.view.addSubview(horizontalBar)
horizontalBar.translatesAutoresizingMaskIntoConstraints = false
horizontalBar.backgroundColor = .white
horizontalBarLeadingConstraint = horizontalBar.leadingAnchor.constraint(equalTo: collectionView2.leadingAnchor)
horizontalBarLeadingConstraint?.isActive = true
horizontalBar.heightAnchor.constraint(equalToConstant: 3).isActive = true
NSLayoutConstraint(item: horizontalBar, attribute: .width, relatedBy: .equal, toItem: collectionView1, attribute: .width, multiplier: 0.5, constant: 1).isActive = true
horizontalBar.bottomAnchor.constraint(equalTo: collectionView2.topAnchor).isActive = true
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let menuName = ["Rooms" , "DeviceList"]
let color :[UIColor] = [.green , .blue]
if collectionView == collectionView1{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MenuBarCell", for: indexPath) as! MenuBarCell
cell.menuLabel.text = menuName[indexPath.section]
cell.menuLabel.textColor = UIColor.darkGray
DispatchQueue.main.async {
self.collectionView1.selectItem(at: self.selectedIndex, animated: false, scrollPosition: .centeredHorizontally)
}
return cell
}
else {
if indexPath.section == 0{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "BottomViewRoomCell", for: indexPath) as! BottomViewRoomCell
cell.backgroundColor = .clear
cell.layer.cornerRadius = 9
cell.layer.masksToBounds = true
return cell
}
else{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "BottomViewDeviceCell", for: indexPath) as! BottomViewDeviceCell
cell.backgroundColor = .clear
cell.layer.cornerRadius = 9
cell.layer.masksToBounds = true
return cell
}
}
}
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize{
if collectionView == collectionView1{
return CGSize(width: self.view.frame.width / 2, height: collectionView1.frame.height)
}
else{
return CGSize(width: self.view.frame.width, height: collectionView2.frame.height)
}
}
you may use UINavigationControllerDelegate to handle this situation.
add UINavigationControllerDelegate, sign self to the delegate of your collectionView's navigationController, and then put your personal function inside "willShow/didShow" delegate method. your personal function will be called when you pop back to the collectionView.
some method about cell display is not shown
class HomeCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, UINavigationControllerDelegate{
private let cellId = "cellId"
override func viewDidLoad() {
super.viewDidLoad()
// sign navigationController's delegate to collectionViewController itself.
navigationController?.delegate = self
}
// push view controller when cell is selected
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let vc = CustomViewController()
navigationController?.pushViewController(vc, animated: true)
}
// call your personal function when this collection view controller shows back
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
if viewController == self {
print("will show self")
}
}
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
if viewController == self {
print("did show self")
}
}
}
I am new to iOS development. I want to create collectionView in two different view controllers with same UI.I want to create only one UICollectionView and resuse it on different view controller instead of create separate collectionViews . On approach i can follow is to create the UICollectionViewController subclass and add this on my viewcontrollers as a childviewcontroller, but not sure if this is the correct approach do not know how addChildViewcontroller works and how to pass data between child and parent viewcontrollers. It would be great if someone can help on this. If any sample code is available to achive this please let me know.
Any help is much appreciated.
You can pass around the same collection view controller instance. Add it in viewWillAppear and remove in viewDidDisappear in first and second classes. Here is a sample code that you could use,
extension UIColor {
class func randomColor() -> UIColor {
let red = CGFloat(arc4random_uniform(255)) / 255.0
let green = CGFloat(arc4random_uniform(255)) / 255.0
let blue = CGFloat(arc4random_uniform(255)) / 255.0
return UIColor(red: red, green: green, blue: blue, alpha: 1.0)
}
}
class MyCollectionViewController: UICollectionViewController {
let data: [UIColor]
init(data: [UIColor]) {
self.data = data
let layout = UICollectionViewFlowLayout()
layout.itemSize = CGSizeMake(100, 100)
layout.scrollDirection = UICollectionViewScrollDirection.Vertical
super.init(collectionViewLayout: layout)
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath)
cell.backgroundColor = data[indexPath.item]
return cell
}
}
class FirstViewController: UIViewController {
lazy var myData:[UIColor] = {
var allData = [UIColor]()
for i in 0 ..< 20 {
allData.append(UIColor.randomColor())
}
return allData
}()
var collectionViewController: MyCollectionViewController!
override func viewDidLoad() {
super.viewDidLoad()
collectionViewController = MyCollectionViewController(data: self.myData)
let barButton = UIBarButtonItem(title: "Show next", style: .Plain, target: self, action: "showNext:")
navigationItem.rightBarButtonItem = barButton
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
let collectionView = collectionViewController.view
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
addChildViewController(collectionViewController)
collectionView.topAnchor.constraintEqualToAnchor(view.topAnchor).active = true
collectionView.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor).active = true
collectionView.leftAnchor.constraintEqualToAnchor(view.leftAnchor).active = true
collectionView.rightAnchor.constraintEqualToAnchor(view.rightAnchor).active = true
collectionViewController.didMoveToParentViewController(self)
}
override func viewWillDisappear(animated: Bool) {
super.viewDidDisappear(animated)
collectionViewController.willMoveToParentViewController(nil)
collectionViewController.view.removeFromSuperview()
collectionViewController.removeFromParentViewController()
}
func showNext(sender: AnyObject) {
let secondViewController = SecondViewController(collectionViewController: collectionViewController)
navigationController?.pushViewController(secondViewController, animated: true)
}
}
class SecondViewController: UIViewController {
var collectionViewController: MyCollectionViewController!
init(collectionViewController: MyCollectionViewController) {
self.collectionViewController = collectionViewController
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
let collectionView = collectionViewController.view
view.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
addChildViewController(collectionViewController)
collectionView.topAnchor.constraintEqualToAnchor(view.topAnchor).active = true
collectionView.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor).active = true
collectionView.leftAnchor.constraintEqualToAnchor(view.leftAnchor).active = true
collectionView.rightAnchor.constraintEqualToAnchor(view.rightAnchor).active = true
collectionViewController.didMoveToParentViewController(self)
}
override func viewWillDisappear(animated: Bool) {
super.viewDidDisappear(animated)
collectionViewController.willMoveToParentViewController(nil)
collectionViewController.view.removeFromSuperview()
collectionViewController.removeFromParentViewController()
}
}
I have a Set of Answers you can use,
My source Code is ParentViewController and ChildViewController are
same viewController to be declared.
First you create the ParentViewController and add the
UICollectionView then set the Cell size in ParentViewController.
Second you create UICollectionViewCell in same parentViewController,
then add what u need Label or Buttons to be declare.
In ParentViewController class declare 'UICollectionViewDelegate',
Ex: class MyViewController: UIViewController, UICollectionViewDelegate
Then Create UICollectionViewDelegate methods and i have put my
methods below,
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return arrayvalue.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("reuseIdentifier", forIndexPath: indexPath)
// Configure the cell
let baseView = cell.viewWithTag(101)
let titleLabel = baseView?.viewWithTag(102) as! UILabel
titleLabel.text = arrayvalue[indexPath.row] as String
return cell
}
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSizeMake(CellSize)
}
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath)
{
collectionView.deselectItemAtIndexPath(indexPath, animated:true)
let storyBoard = UIStoryboard(name: "storyboardName", bundle: nil)
let name: classname = storyBoard.instantiateViewControllerWithIdentifier("reuseIdentifier") as! AnotherViewController
self.navigationController?.pushViewController(name, animated: true)
}
Very Important to give storyboard 'reuseIdentifier' value and also
give inside the class cellForItemAtIndexPath reuseIdentifier, example this line
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("reuseIdentifier", forIndexPath: indexPath)
This code was working for me...