NSCollectionView as NSCollectionViewItem - nscollectionview

Here is the window I want to achieve:
It is a NSViewController (RedViewController) with NSCollectionView (RedCollectionView) as view.
It item (BlueItem) has a NSCollectionView (BlueCollectionView) as view.
It item's item (GreenItem), has a NSImageView (GreenImageView) as view.
The RedCollectionView will scroll vertically, and its items will be selectable. However the BlueCollectionView is just for display, it won't scroll itself and won't be selectable.
Here is the code for the AppDelegate:
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
lazy var redViewController: RedViewController = {
let redViewController = RedViewController.init()
return redViewController
}()
lazy var window: NSWindow = {
let window = NSWindow.init(contentViewController: self.redViewController)
return window
}()
lazy var windowController: NSWindowController = {
let windowController = NSWindowController.init(window: self.window)
return windowController
}()
func applicationDidFinishLaunching(_ aNotification: Notification) {
self.windowController.showWindow(nil)
}
}
Here is the code for the RedViewController:
class RedViewController: NSViewController, NSCollectionViewDataSource, NSCollectionViewDelegate {
lazy var collectionViewLayout: NSCollectionViewFlowLayout = {
let collectionViewLayout = NSCollectionViewFlowLayout.init()
collectionViewLayout.itemSize = NSSize(width: 100.0, height: 100.0)
collectionViewLayout.minimumLineSpacing = 10.0
collectionViewLayout.minimumInteritemSpacing = 10.0
collectionViewLayout.sectionInset = NSEdgeInsetsMake(10.0, 10.0, 10.0, 10.0)
return collectionViewLayout
}()
class RedCollectionView: NSCollectionView { /* nothing here for now */ }
lazy var redCollectionView: RedCollectionView = {
let redCollectionView = RedCollectionView.init(frame: NSRect(x: 0.0, y: 0.0, width: 500.0, height: 500.0))
redCollectionView.collectionViewLayout = self.collectionViewLayout
redCollectionView.dataSource = self
redCollectionView.delegate = self
redCollectionView.isSelectable = true
redCollectionView.backgroundColors = [NSColor.red]
return redCollectionView
}()
override func loadView() {
let clipView = NSClipView.init()
clipView.documentView = self.redCollectionView
let scrollView = NSScrollView.init(frame: NSRect(x: 0.0, y: 0.0, width: 500.0, height: 500.0))
scrollView.contentView = clipView
self.view = scrollView
}
override func viewDidLoad() {
super.viewDidLoad()
self.redCollectionView.register(BlueItem.self, forItemWithIdentifier: NSUserInterfaceItemIdentifier("BlueItemID"))
}
func numberOfSections(in collectionView: NSCollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
return 50
}
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
let item = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "BlueItemID"), for: indexPath)
guard let blueItem = item as? BlueItem else { return item }
return blueItem
}
func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set<IndexPath>) {
indexPaths.forEach({ print($0) })
collectionView.deselectItems(at: indexPaths)
}
}
Here is the code for the BlueItem:
class BlueItem: NSCollectionViewItem, NSCollectionViewDataSource {
lazy var collectionViewLayout: NSCollectionViewFlowLayout = {
let collectionViewLayout = NSCollectionViewFlowLayout.init()
collectionViewLayout.itemSize = NSSize(width: 27.0, height: 27.0)
collectionViewLayout.minimumLineSpacing = 3.0
collectionViewLayout.minimumInteritemSpacing = 3.0
collectionViewLayout.sectionInset = NSEdgeInsetsMake(3.0, 3.0, 3.0, 3.0)
return collectionViewLayout
}()
class BlueCollectionView: NSCollectionView {
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
self.nextResponder?.mouseDown(with: event)
}
override func mouseUp(with event: NSEvent) {
super.mouseUp(with: event)
self.nextResponder?.mouseUp(with: event)
}
}
lazy var blueCollectionView: BlueCollectionView = {
let blueCollectionView = BlueCollectionView.init(frame: NSRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0))
blueCollectionView.collectionViewLayout = self.collectionViewLayout
blueCollectionView.dataSource = self
blueCollectionView.isSelectable = false
blueCollectionView.backgroundColors = [NSColor.blue]
return blueCollectionView
}()
override func loadView() {
self.view = self.blueCollectionView
}
override func viewDidLoad() {
super.viewDidLoad()
self.blueCollectionView.register(GreenItem.self, forItemWithIdentifier: NSUserInterfaceItemIdentifier("GreenItemID"))
}
func numberOfSections(in collectionView: NSCollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
return 7
}
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
let item = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "GreenItemID"), for: indexPath)
guard let greenItem = item as? GreenItem else { return item }
return greenItem
}
}
And finally here is the code for the GreenItem:
class GreenItem: NSCollectionViewItem {
class GreenImageView: NSImageView { /* nothing here for now */ }
lazy var greenImageView: GreenImageView = {
let image: NSImage = NSImage.init(imageLiteralResourceName: "greenImage")
let greenImageView = GreenImageView.init(image: image)
return greenImageView
}()
override func loadView() {
self.view = self.greenImageView
}
}
When I launch the app, I get this result:
So I have 3 issues:
1. The BlueItem with it BlueCollectionView don't get the blue background expected.
2. The BlueCollectionView flow layout tries to display all the items on the first row. It looks like it doesn't know the collection view size.
3. When I try to select a BlueItem: if I click on an empty space inside the BlueItem, the RedCollectionView delegate is triggered as expected, but if I click on a green part inside the BlueItem, the RedCollectionView delegate isn't triggered.

Related

Update Segmented Progress Bar When CollectionView Index is Changed Swift

Working Example Video
I am trying to create an onboarding view such as Instagram stories with a progress bar on top of the view.
So far I was able to animate to the second or third index in the given time. This animation also changes stack view progress bar. But when I try to scroll to the next or previous collection view index I can not show this action inside the progress bar.
I can read the current page index with page control but can not reflect this reading to progress bar.
class OnboardingViewController: UIViewController, SegmentedProgressBarDelegate {
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var stackView: UIStackView!
private var spb: SegmentedProgressBar!
var currentPage = 0
lazy var pageControl: UIPageControl = {
let pageControl = UIPageControl()
pageControl.numberOfPages = slides.count
return pageControl
}()
//Burası Presenter'dan gelecek.
var slides: [OnboardingSlide] = [
OnboardingSlide(title: "Delicious Dishes", image: #imageLiteral(resourceName: "1")),
OnboardingSlide(title: "World-Class Chefs", image: #imageLiteral(resourceName: "2")),
OnboardingSlide(title: "Instant World-Wide Delivery", image: #imageLiteral(resourceName: "3"))
]
override func viewDidLoad() {
super.viewDidLoad()
spb = SegmentedProgressBar(numberOfSegments: slides.count, duration: 3)
spb.frame = CGRect(x: 15, y: 56, width: collectionView.frame.width - 30, height: 4)
spb.delegate = self
spb.topColor = UIColor.white
spb.bottomColor = UIColor.white.withAlphaComponent(0.25)
spb.padding = 2
collectionView.delegate = self
collectionView.dataSource = self
stackView.addArrangedSubview(spb)
spb.startAnimation()
collectionView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tappedView)))
}
func segmentedProgressBarChangedIndex(index: Int) {
updateView(index: index)
}
override var prefersStatusBarHidden: Bool {
return true
}
func segmentedProgressBarFinished() {
print("Finished!")
}
#objc private func tappedView() {
spb.isPaused = !spb.isPaused
}
private func updateView(index: Int) {
print("index: \(index)")
let indexPath = IndexPath(row: index, section: 0)
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
#IBAction func loginButtonTapped(_ sender: UIButton) {
}
#IBAction func signupButtonTapped(_ sender: UIButton) {
}
}
extension OnboardingViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return slides.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: OnboardingCollectionViewCell.identifier, for: indexPath) as! OnboardingCollectionViewCell
cell.setup(slides[indexPath.row])
return cell
}
}
extension OnboardingViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width, height: collectionView.frame.height)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let width = scrollView.frame.width
currentPage = Int(scrollView.contentOffset.x / width)
pageControl.currentPage = currentPage
updateView(index: currentPage)
print("current page: \(currentPage)")
}
}
I have used
https://github.com/D-32/SegmentedProgressBar
as the segmentedProgressBar
class OnboardingCollectionViewCell: UICollectionViewCell {
static let identifier = String(describing: OnboardingCollectionViewCell.self)
#IBOutlet weak var slideImageView: UIImageView!
#IBOutlet weak var slideTitleLabel: UILabel!
func setup(_ slide: OnboardingSlide) {
slideImageView.image = slide.image
slideTitleLabel.text = slide.title
}
}
struct OnboardingSlide {
let title: String
let image: UIImage
}
extension OnboardingViewController: UICollectionViewDelegateFlowLayout {
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
if scrollView.panGestureRecognizer .translation(in: view).x > 0 {
spb.rewind()
} else {
spb.skip()
}
}
}
with this, I was able to understand if collection view was scrolled left or right. Thus calling related functions inside the spb does the trick.

UICollectionView Drag and Drop cell

Hi I want to do drag drop using uicollectionview. When performing drag and drop it is moving contents i want to do it like in the photo. I want the box to carry itself. For example; when I drag the photo to 4 I should leave the red area with full measurements. Swap photo 6 to photo 1 like taking photo 3 to the left. I have researched in uicollectionview so much but I can’t find anything like this. Please help me
import UIKit
final class ViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
var cellIds = ["image 1","image 2","image 3","image 4","image 5","6","7"]
override func viewDidLoad() {
super.viewDidLoad()
let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
flowLayout.estimatedItemSize = CGSize(width: 200, height: 10)
let gestureRecognizer = UILongPressGestureRecognizer(target: self,
action: #selector(self.handleLongPress(gestureRecognizer:)))
collectionView.addGestureRecognizer(gestureRecognizer)
}
#objc func handleLongPress(gestureRecognizer: UILongPressGestureRecognizer) {
guard let view = gestureRecognizer.view else { return }
let location = gestureRecognizer.location(in: view)
switch gestureRecognizer.state {
case .began:
guard let selectedIndexPath = collectionView.indexPathForItem(at: location) else { break }
collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
case .changed:
collectionView.updateInteractiveMovementTargetPosition(location)
case .ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}
}
extension ViewController: UICollectionViewDataSource,
UICollectionViewDelegateFlowLayout
{
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return cellIds.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SampleCell", for: indexPath) as! SampleCell
let text = cellIds[indexPath.item]
cell.label.text = text
return cell
}
func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
return true
}
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let text = cellIds.remove(at: sourceIndexPath.item)
cellIds.insert(text, at: destinationIndexPath.item)
collectionView.reloadData()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if (indexPath.row==0)
{
return CGSize(width: 190, height: 100)
}
if (indexPath.row==1)
{
return CGSize(width: 190, height: 100)
}
if (indexPath.row==2)
{
return CGSize(width: 190, height: 400)
}
if (indexPath.row==3)
{
return CGSize(width: 400, height: 200)
}
return CGSize(width: 0, height: 0)
}
}
final class SampleCell: UICollectionViewCell {
#IBOutlet weak var label: UILabel!
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
setNeedsLayout()
layoutIfNeeded()
let size = self.systemLayoutSizeFitting(layoutAttributes.size)
var newFrame = layoutAttributes.frame
// note: don't change the width
newFrame.size.height = ceil(size.height)
layoutAttributes.frame = newFrame
return layoutAttributes
}
}
picture
You can use UICollectionViewDragDelegate,
For multiple sections, in order to drag to the end item, we should add an extra item when dragging.
Sample Code:
ViewController:
import UIKit
enum CellModel {
case simple(text: String)
case availableToDropAtEnd
}
class SecondController: UIViewController {
private lazy var cellIdentifier = "cellIdentifier"
private lazy var supplementaryViewIdentifier = "supplementaryViewIdentifier"
private lazy var sections = 10
private lazy var itemsInSection = 2
private lazy var numberOfElementsInRow = 3
private lazy var data: [[CellModel]] = {
var count = 0
return (0 ..< sections).map { _ in
return (0 ..< itemsInSection).map { _ -> CellModel in
count += 1
return .simple(text: "cell \(count)")
}
}
}()
override func viewDidLoad() {
super.viewDidLoad()
let collectionViewFlowLayout = UICollectionViewFlowLayout()
collectionViewFlowLayout.minimumLineSpacing = 5
collectionViewFlowLayout.minimumInteritemSpacing = 5
let _numberOfElementsInRow = CGFloat(numberOfElementsInRow)
let allWidthBetwenCells = _numberOfElementsInRow == 0 ? 0 : collectionViewFlowLayout.minimumInteritemSpacing*(_numberOfElementsInRow-1)
let width = (view.frame.width - allWidthBetwenCells)/_numberOfElementsInRow
collectionViewFlowLayout.itemSize = CGSize(width: width, height: width)
collectionViewFlowLayout.headerReferenceSize = CGSize(width: 0, height: 40)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewFlowLayout)
collectionView.backgroundColor = .white
view.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
collectionView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true
collectionView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true
collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: cellIdentifier)
collectionView.register(SupplementaryView.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: supplementaryViewIdentifier)
collectionView.dragInteractionEnabled = true
collectionView.reorderingCadence = .fast
collectionView.dropDelegate = self
collectionView.dragDelegate = self
collectionView.delegate = self
collectionView.dataSource = self
}
}
extension SecondController: UICollectionViewDelegate { }
extension SecondController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return data.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data[section].count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! CollectionViewCell
switch data[indexPath.section][indexPath.item] {
case .simple(let text):
cell.label?.text = text
cell.backgroundColor = .gray
case .availableToDropAtEnd:
cell.backgroundColor = UIColor.green.withAlphaComponent(0.3)
}
return cell
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
return collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: supplementaryViewIdentifier, for: indexPath)
}
}
extension SecondController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
let itemProvider = NSItemProvider(object: "\(indexPath)" as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = data[indexPath.section][indexPath.row]
return [dragItem]
}
func collectionView(_ collectionView: UICollectionView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem] {
let itemProvider = NSItemProvider(object: "\(indexPath)" as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = data[indexPath.section][indexPath.row]
return [dragItem]
}
func collectionView(_ collectionView: UICollectionView, dragSessionWillBegin session: UIDragSession) {
var itemsToInsert = [IndexPath]()
(0 ..< data.count).forEach {
itemsToInsert.append(IndexPath(item: data[$0].count, section: $0))
data[$0].append(.availableToDropAtEnd)
}
collectionView.insertItems(at: itemsToInsert)
}
func collectionView(_ collectionView: UICollectionView, dragSessionDidEnd session: UIDragSession) {
var removeItems = [IndexPath]()
for section in 0..<data.count {
for item in 0..<data[section].count {
switch data[section][item] {
case .availableToDropAtEnd:
removeItems.append(IndexPath(item: item, section: section))
case .simple:
break
}
}
}
removeItems.forEach { data[$0.section].remove(at: $0.item) }
collectionView.deleteItems(at: removeItems)
}
}
extension SecondController: UICollectionViewDropDelegate {
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
let destinationIndexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath {
destinationIndexPath = indexPath
} else {
// useless, just in case
let section = collectionView.numberOfSections - 1
let row = collectionView.numberOfItems(inSection: section)
destinationIndexPath = IndexPath(row: row, section: section)
}
switch coordinator.proposal.operation {
case .move:
reorderItems(coordinator: coordinator, destinationIndexPath:destinationIndexPath, collectionView: collectionView)
default:
break
}
}
func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool {
return true
}
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
// made the above logic useless
if collectionView.hasActiveDrag, destinationIndexPath != nil {
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
else {
return UICollectionViewDropProposal(operation: .forbidden)
}
}
private
func reorderItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView) {
let items = coordinator.items
if items.count == 1, let item = items.first,
let sourceIndexPath = item.sourceIndexPath,
let localObject = item.dragItem.localObject as? CellModel {
collectionView.performBatchUpdates ({
data[sourceIndexPath.section].remove(at: sourceIndexPath.item)
data[destinationIndexPath.section].insert(localObject, at: destinationIndexPath.item)
collectionView.deleteItems(at: [sourceIndexPath])
collectionView.insertItems(at: [destinationIndexPath])
})
}
}
}
View:
import UIKit
class CollectionViewCell: UICollectionViewCell {
weak var label: UILabel?
override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = true
let label = UILabel(frame: .zero)
label.contentMode = .scaleAspectFill
addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.topAnchor.constraint(equalTo: topAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
label.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
label.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
label.textAlignment = .center
label.textColor = .white
self.label = label
layer.borderWidth = 1
layer.borderColor = UIColor.white.cgColor
backgroundColor = .white
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func prepareForReuse() {
super.prepareForReuse()
label?.text = nil
backgroundColor = .white
}
}
class SupplementaryView: UICollectionReusableView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.blue.withAlphaComponent(0.7)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
github link

Interaction between UIPageViewController and it's Thumbnails CollectionView

In my app using UIPageViewController to display full size images, and at the bottom of screen have thumbnail CollectionView each thumbnail is representing UIPageViewController full size image.My question is how to achieve paging of UIpageViewController when user select different thumbnail collection view cell?
if anyone know how to achieve this functionality , please help me with code or advice or links. Thanks
I believe I have to work with UICollectionView’s didSelectItemAtIndexPath method ,but not too sure how best to do it and would really appreciate help, please.
My Collection View File
protocol ThumbnailViewDelegate: class {
func didSelectThumbnail(at index: Int)
}
class ThumbnailView: UIView, UICollectionViewDelegate, UICollectionViewDataSource,UICollectionViewDelegateFlowLayout, PhotoCollectionViewModelPresenter{
weak var delegate: ThumbnailViewDelegate?
var assets: [PHAsset] = []
private let viewModel = PhotoCollectionViewModel()
//private var myViewModel: DetailViewModel!
private let imageDownloader = ImageDownloader(targetSize: CGSize(width: 80, height: 80 ))
//var selectedImage: UIImage?
lazy var thumbnailCollectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let view = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout)
view.translatesAutoresizingMaskIntoConstraints = false
view.isPagingEnabled = true
view.delegate = self
view.dataSource = self
view.isPrefetchingEnabled = false
view.register(ThumbnailCollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
let cellImageView = ThumbnailCollectionViewCell()
view.addSubview(cellImageView.imageView)
view.backgroundColor = UIColor.white
return view
}()
let imageView: ThumbnailCollectionViewCell = {
let view = ThumbnailCollectionViewCell()
view.contentMode = .scaleAspectFit
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(thumbnailCollectionView)
thumbnailCollectionView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
thumbnailCollectionView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
thumbnailCollectionView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
thumbnailCollectionView.heightAnchor.constraint(equalToConstant: 100).isActive = true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func propagateReload() {
thumbnailCollectionView.reloadData()
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return assets.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath as IndexPath) as! ThumbnailCollectionViewCell
cell.layer.masksToBounds = true
cell.layer.cornerRadius = 8
let image = assets[indexPath.item]
imageDownloader.download(asset: image) { [weak self] (image) in
self?.configureCell(indexPath: indexPath, image: image)
}
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
return CGSize(width: 80.0, height: 80.0)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
delegate?.didSelectThumbnail(at: indexPath.item)
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
}
func configureCell(indexPath: IndexPath, image: UIImage) {
guard let cell = thumbnailCollectionView.cellForItem(at: indexPath) as? ThumbnailCollectionViewCell else {
return
}
cell.configure(image: image)
}
}
PageViewController File
class MainPageViewController: UIPageViewController {
var mainImageIndex: Int?
var pageViewModels: [DetailPageViewModel]!
var viewModel: DetailViewModel!
var collectionAssets: [PHAsset] = []
var selectedAsset: UIImage?
private var pendingIndex: Int?
// Collection View
lazy var collectionView: ThumbnailView = {
let view = ThumbnailView()
view.delegate = self
view.assets = self.collectionAssets
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
//MARK: - Create VC
lazy var pageViewControllersArray: [PageViewController] = {
return pageViewModels.map {
return PageViewController(viewModel: $0)
}
}()
var currentIndex:Int {
get {
return pageViewControllersArray.index(of: self.viewControllers!.first! as! PageViewController)!
}
set {
guard newValue >= 0,
newValue < pageViewControllersArray.count else {
return
}
let vc = pageViewControllersArray[newValue]
let direction:UIPageViewController.NavigationDirection = newValue > currentIndex ? .forward : .reverse
self.setViewControllers([vc], direction: direction, animated: true, completion: nil)
}
}
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self
delegate = self
view.backgroundColor = UIColor.white
setViewControllers([pageViewControllersArray[mainImageIndex ?? 0]], direction: .forward, animated: true, completion: nil)
self.view.addSubview(collectionView)
setupCollectioViewAutoLayout()
}
func setupCollectioViewAutoLayout(){
collectionView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
collectionView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
collectionView.heightAnchor.constraint(equalToConstant: 100).isActive = true
}
func viewWillAppear() {
super.viewWillAppear(true)
}
}
extension MainPageViewController: UIPageViewControllerDelegate, UIPageViewControllerDataSource, ThumbnailViewDelegate {
func didSelectThumbnail(at index: Int) {
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewController = viewController as? PageViewController else {return nil}
if let index = pageViewControllersArray.index(of: viewController){
if index > 0{
return pageViewControllersArray[index - 1]
}
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewController = viewController as? PageViewController else {return nil}
if let index = pageViewControllersArray.index(of: viewController){
if index < pageViewControllersArray.count - 1{
return pageViewControllersArray[index + 1]
}
}
return nil
}
func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int {
return pageViewControllersArray.count
}
func presentationIndexForPageViewController(pageViewController: UIPageViewController) -> Int {
return currentIndex
}
}
When the select method is called, show another page view or page view controller on the whole screen.

Transition between two UICollectionView layouts causes inconsistency error

I have a navigation controller and on viewDidAppear it pushes a UICollectionViewController called SingleBlueSquareCollectionViewController. There is a single blue square UICollectionViewCell positioned center and towards the bottom of the collectionView. When you tap the blue cell I push another CollectionViewController, TriangleCollectionViewController, which places the blue square at the top and center and adds two red cells at either corner of the viewController. This is what I expected to happen but I get an NSInconsistencyError instead: UICollectionView received layout attributes for a cell with an index path that does not exist.
The error makes sense. One view controller has a single element but another view controller has 3. But I don't want the triangleViewController to get information from SingleBlue. Is there a way around this? And also the bigger question is Can you transition between two layouts with different number of elements?
If so, what am I doing wrong? How should I actually be handling this. Is there an animation object to provide my collectionViews? This use case is a test for something I want to try in another app which has a view controller with a few complex layouts (so I totally understand that this use case could be done with some UIViews and animating things like the blue square around easily).
Here's some source code:
class SingleBlueSquare: UICollectionViewLayout {
var cache : [UICollectionViewLayoutAttributes] = [UICollectionViewLayoutAttributes]()
override func prepare() {
configureCache()
}
override var collectionViewContentSize: CGSize { return CGSize(width: collectionView!.frame.width,
height: collectionView!.frame.height) }
override func prepareForTransition(from oldLayout: UICollectionViewLayout) {
configureCache()
}
func configureCache() {
guard let collection = collectionView else {return}
let first = IndexPath(item: 0, section: 0)
let blue = UICollectionViewLayoutAttributes(forCellWith: first)
let size : CGSize = .init(width: 50, height: 50)
blue.frame = CGRect(origin: .init(x: collection.frame.width/2,
y: collection.frame.height - size.height),
size: size)
cache = [blue]
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return cache
}
TriangleLayout
class TriangleLayout: UICollectionViewLayout {
var cache : [UICollectionViewLayoutAttributes] = [UICollectionViewLayoutAttributes]()
override func prepare() {
configureCache()
}
override func prepareForTransition(to newLayout: UICollectionViewLayout) {
cache.removeAll()
}
override var collectionViewContentSize: CGSize { return CGSize(width: collectionView!.frame.width,
height: collectionView!.frame.height) }
override func prepareForTransition(from oldLayout: UICollectionViewLayout) {
configureCache()
}
func configureCache() {
guard let collection = collectionView else {return}
cache.removeAll()
let first = IndexPath(item: 0, section: 0)
let second = IndexPath(item: 1, section: 0)
let third = IndexPath(item: 2, section: 0)
let blue = UICollectionViewLayoutAttributes(forCellWith: first)
let red = UICollectionViewLayoutAttributes(forCellWith: second)
let red2 = UICollectionViewLayoutAttributes(forCellWith: third)
let size : CGSize = .init(width: 50, height: 50)
blue.frame = CGRect(origin: .init(x: collection.frame.width/2,
y: 0),
size: size)
red.frame = CGRect(origin: .init(x: 0,
y: collection.frame.height - 50),
size: size)
red2.frame = CGRect(origin: .init(x: collection.frame.width - 50,
y: collection.frame.height - 50),
size: size)
cache = [blue, red, red2]
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return cache
}
}
SingleCollectionViewController
private let reuseIdentifier = "blue"
class SingleBlueCollectionViewController: UICollectionViewController {
init() {
super.init(collectionViewLayout: SingleBlueSquare())
useLayoutToLayoutNavigationTransitions = false
}
override init(collectionViewLayout layout: UICollectionViewLayout) {
super.init(collectionViewLayout: layout)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView!.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.collectionView?.reloadData()
self.collectionView?.layoutIfNeeded()
}
// MARK: UICollectionViewDataSource
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
cell.backgroundColor = indexPath.item == 0 ? UIColor.blue : UIColor.red
return cell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let cell = collectionView.cellForItem(at: indexPath), cell.backgroundColor == UIColor.blue else {return}
collectionView.collectionViewLayout.invalidateLayout()
navigationController?.pushViewController(TriangleCollectionViewController(), animated: true)
}
}
Here's the triangle view controller
private let reuseIdentifier = "triangle"
class TriangleCollectionViewController : SingleBlueCollectionViewController {
override init() {
super.init(collectionViewLayout: TriangleLayout())
useLayoutToLayoutNavigationTransitions = true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func viewDidLoad() {
super.viewDidLoad()
// Register cell classes
self.collectionView!.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
self.collectionView?.reloadData()
self.collectionView?.collectionViewLayout.invalidateLayout()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: UICollectionViewDataSource
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
cell.backgroundColor = indexPath.item == 0 ? UIColor.blue : UIColor.red
return cell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let cell = collectionView.cellForItem(at: indexPath), cell.backgroundColor == UIColor.blue else {return}
navigationController?.popViewController(animated: true)
}
}

Swift 3: How to get text from individual textfields that are in different collectionView cells?

I have a simple UICollectionView that has 3 cells. The cell has it's own class and all 3 cells use this class. The cell class just has a UITextField that is the cells size. I want to know how to be able to get hold of whatever is entered in each of the 3 cells UITextFields and pass it into my SecondController. How do I achieve this? Thank you guys. Code is below:
import UIKit
class FirstCell: BaseCell {
var secondController: SecondController?
let textField: UITextField = {
let tv = UITextField()
tv.textColor = .white
return tv
}()
override func setupViews() {
super.setupViews()
backgroundColor = .lightGray
addSubview(textField)
textField.frame = CGRect(x: 12, y: 0, width: self.frame.width, height: self.frame.height)
}
}
private let reuseIdentifier = "Cell"
class SecondController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView?.backgroundColor = .white
self.collectionView?.alwaysBounceVertical = true
self.collectionView?.register(FirstCell.self, forCellWithReuseIdentifier: reuseIdentifier)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Print", style: .plain, target: self, action: #selector(handlePrint))
}
func handlePrint() {
// print here...
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: 50)
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as? FirstCell
cell?.secondController = self
if indexPath.item == 0 {
cell?.textField.placeholder = "First Textfield"
}
if indexPath.item == 1 {
cell?.textField.placeholder = "Second Textfield"
}
if indexPath.item == 2 {
cell?.textField.placeholder = "ThirdTextfield"
}
return cell!
}
}
create protocol
protocol MyCellTextFieldDelegate {
func didChangeTextTo(text: String)
}
then
extension SecondController: MyCellTextFieldDelegate {
func didChangeTextTo(text: String) {
//doStuff
}
}
inside your cell change
var secondController: SecondController?
to
weak var delegate: MyCellTextFieldDelegate?
and change your method to be like
override func setupViews() {
super.setupViews()
backgroundColor = .lightGray
addSubview(textField)
textField.delegate = self //this line
textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged) //and this line
textField.frame = CGRect(x: 12, y: 0, width: self.frame.width, height: self.frame.height)
}
and your cell class
class FirstCell: BaseCell, UITextFieldDelegate {
//other code
private func textFieldDidChange() {
self.delegate?.didChangeTextTo(text: (self.textField.text ?? ""))
}
}
and inside cellForItemAt
cell?.delegate = self
Edit: if you would like to get UICollectionViewCell that calls this, you should add
var tag: Int = 0
inside your cell and inside cellForItemAt
cell?.tag = indexPath.row
and then change your MyCellTextFieldDelegate func to pass not only text but tag as well.