Interaction between UIPageViewController and it's Thumbnails CollectionView - swift

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.

Related

Unable to display the data into UICollection view

I am trying to display the data into UI collection view .I am following the MVVM Architecture pattern. I am following programatic approach . I put the break point , I can see onto console , The data is decoded from URL but problem is it not displaying the into view controller.
Here is the my network manager code .
import Foundation
protocol NetworkManagerProtocol{
func getModel<T: Codable>(_ model: T.Type, from url: String, completion: #escaping (Result<T, NetworkError>) -> Void)
func getData(from url: String, completion: #escaping (Result<Data, NetworkError>) -> Void)
}
class NetworkManager: NetworkManagerProtocol{
func getModel<T: Codable>(_ model: T.Type, from url: String, completion: #escaping (Result<T, NetworkError>) -> Void) {
guard let url = URL(string: url) else {
completion(.failure(.badURL))
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
do {
let response = try JSONDecoder().decode(model, from: data)
completion(.success(response))
} catch let error {
completion(.failure(.other(error)))
}
}
if let error = error {
completion(.failure(.other(error)))
}
}
.resume()
}
func getData(from url: String, completion: #escaping (Result<Data, NetworkError>) -> Void) {
guard let url = URL(string: url) else {
completion(.failure(.badURL))
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
completion(.success(data))
}
if let error = error {
completion(.failure(.other(error)))
}
}
.resume()
}
}
Here is the View Model code.
import Foundation
class PhotoViewModel {
private let networkManager: NetworkManagerProtocol
var hits = [Hit]()
private var cache = [String: Data]()
private weak var delegate: PhotoViewable?
init(networkManager: NetworkManagerProtocol) {
self.networkManager = networkManager
}
var rows: Int {
return hits.count
}
func fecthPhotoRecord(){
let networkUrl = NetworkURLs.baseURL
networkManager.getModel(Photo.self, from: networkUrl) { [weak self] result in
switch result{
case.success(let photo):
self?.hits = photo.hits
self?.delegate?.refreshUI()
case.failure(let error):
print(error)
self?.delegate?.showError()
}
}
}
func downloadImage(row: Int, completion: #escaping (Data) -> Void) {
let hit = hits[row]
let hitpath = hit.previewURL
if let data = cache[hitpath] {
completion(data)
return
}
networkManager
.getData(from: hitpath) { result in
switch result {
case .success(let data):
self.cache[hitpath] = data
DispatchQueue.main.async {
completion(data)
}
case .failure(let error):
print(error)
}
}
}
}
Here is the collection view cell ..
import UIKit
protocol ReusableView: AnyObject {
static var identifier: String { get }
}
class PhotoCollectionViewCell: UICollectionViewCell {
private lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.frame = CGRect(x:0, y:0, width:50, height:50)
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private lazy var tagLabel:UILabel = {
let label = UILabel()
label.font = UIFont.boldSystemFont(ofSize: 15)
label.textColor = .black
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
imageView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(tagLabel)
self.contentView.addSubview(imageView)
NSLayoutConstraint.activate([
tagLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor),
tagLabel.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
tagLabel.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
tagLabel.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor),
imageView.topAnchor.constraint(equalTo: self.tagLabel.topAnchor),
imageView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor),
])
self.contentView.backgroundColor = .lightGray
}
func configureCell(tagName: String) {
tagLabel.text = tagName
}
func configureImageCell(row: Int, viewModel: PhotoViewModel) {
imageView.image = nil
viewModel
.downloadImage(row: row) { [weak self] data in
let image = UIImage(data: data)
self?.imageView.image = image
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension PhotoCollectionViewCell: ReusableView {
static var identifier: String {
return String(describing: self)
}
}
Here is the code for view controller .
import UIKit
protocol PhotoViewable: AnyObject {
func refreshUI()
func showError()
}
class PhotoViewController: UIViewController {
var viewModel : PhotoViewModel
init(viewModel : PhotoViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
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 = 300.0
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var activityIndicator: UIActivityIndicatorView = {
let activityIndicator = UIActivityIndicatorView()
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
return activityIndicator
}()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
setupLayouts()
viewModel.fecthPhotoRecord()
collectionView.delegate = self
collectionView.dataSource = self
view.backgroundColor = .lightGray
collectionView.reloadData()
}
private func setupViews() {
view.backgroundColor = .white
view.addSubview(collectionView)
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(PhotoCollectionViewCell.self, forCellWithReuseIdentifier: PhotoCollectionViewCell.identifier)
}
private func setupLayouts() {
collectionView.translatesAutoresizingMaskIntoConstraints = false
// Layout constraints for `collectionView`
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
collectionView.leftAnchor.constraint(equalTo: view.leftAnchor),
collectionView.rightAnchor.constraint(equalTo: view.rightAnchor)
])
}
}
extension PhotoViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModel.hits.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotoCollectionViewCell.identifier, for: indexPath) as! PhotoCollectionViewCell
let row = indexPath.row
let hits = viewModel.hits[indexPath.row]
cell.configureCell(tagName: hits.tags)
cell.configureImageCell(row: row, viewModel: viewModel)
return cell
}
}
extension PhotoViewController: 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 PhotoViewController: PhotoViewable {
func refreshUI() {
DispatchQueue.main.async {
self.collectionView.isHidden = false
// self.activityIndicator.stopAnimating()
self.collectionView.reloadData()
}
}
func showError() {
DispatchQueue.main.async {
// self.activityIndicator.stopAnimating()
self.collectionView.reloadData()
}
}
}
Here is the console .
Here is the result of the screenshot.
The problem is you are not setting the delegate for viewModel
add the following line in viewDidLoad before calling viewModel.fecthPhotoRecord()
viewModel.delegate = self
main problem is you didn't call the delegate for your viewModel before fetching the data,
always setup you delegate before any data management in viewDidLoad
viewModel.delegate = self
viewModel.datasource = self //if needed datasource

How to reload Section in UICollection view

So i'm trying to implement a searchbar to search over different categories. After looking online a bit, I took a tutorial for UITableView and adapted it to my UICollectionView.
The searching works fine, as I am able to print the search results which go into the filteredCategories array.
Where I am having trouble is reloading the UICollectionView using the new filteredCategories array. I tried collectionView.reloadData() as well as collectionView.reloadSections(NSIndexSet(index: 1) as IndexSet) but with no success.
It seems as if the values are passed only once when the section is being built, and that I cannot change it.
Here is my code:
import UIKit
class ViewController: UIViewController {
var collectionView: UICollectionView!
let categories: [String] = ["Technology", "Science", "Entertainment", "Business", "Health", "Sports"]
var filteredCategories: [String] = ["none"]
var count = 0
var filteredCount = 6
var ready: [String] = ["none"]
var readyCount = 0
func filterIt() {
if isFiltering {
ready = filteredCategories
readyCount = filteredCategories.count
} else {
ready = categories
readyCount = categories.count
}
print("second", ready)
}
lazy var sections: [Section] = [
TitleSection(title: "NewsStand"),
// SearchSection(),
BasicGridSection(categories: ready, count: readyCount)
]
lazy var collectionViewLayout: UICollectionViewLayout = {
var sections = self.sections
let layout = UICollectionViewCompositionalLayout { (sectionIndex, environment) -> NSCollectionLayoutSection? in
return sections[sectionIndex].layoutSection()
}
return layout
}()
let searchController = UISearchController(searchResultsController: nil)
var isSearchBarEmpty: Bool {
return searchController.searchBar.text?.isEmpty ?? true
}
func filterContentForSearchText(_ searchText: String) {
filteredCategories = categories.filter { (category: String) -> Bool in
print("filtered", filteredCategories)
return category.lowercased().contains(searchText.lowercased())
}
filterIt()
collectionView.reloadData()
setupCollectionView()
collectionView.reloadSections(NSIndexSet(index: 1) as IndexSet)
}
var isFiltering: Bool {
return searchController.isActive && !isSearchBarEmpty
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Compositional Layout"
self.view.backgroundColor = UIColor.white
self.title = "NewsStand"
setupCollectionView()
filterIt()
// 1
searchController.searchResultsUpdater = self
// 2
searchController.obscuresBackgroundDuringPresentation = false
// 3
searchController.searchBar.placeholder = "Search"
// 4
navigationItem.searchController = searchController
// 5
definesPresentationContext = true
}
func setupCollectionView() {
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: collectionViewLayout)
collectionView.dataSource = self
collectionView.delegate = self
collectionView.backgroundColor = UIColor.white
collectionView.register(UINib(nibName: "TitleCell", bundle: .main), forCellWithReuseIdentifier: TitleCell.identifier)
collectionView.register(UINib(nibName: "GridCell", bundle: .main), forCellWithReuseIdentifier: GridCell.identifier)
collectionView.register(UINib(nibName: "SearchCell", bundle: .main), forCellWithReuseIdentifier: SearchCell.identifier)
self.view.addSubview(collectionView)
collectionView.reloadData()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
collectionView.reloadData()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
collectionView.reloadData()
}
}
extension ViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
sections.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
sections[section].numberOfItems
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
sections[indexPath.section].configureCell(collectionView: collectionView, indexPath: indexPath)
}
}
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let articlesView = storyboard?.instantiateViewController(withIdentifier: "articleViewController") as? ArticleViewController else {
print("error")
return
}
// set the post id for the comments
articlesView.set(index: indexPath.item)
// present(articlesView, animated: true, completion: nil)
self.navigationController!.pushViewController(articlesView, animated: true)
}
}
extension ViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
let searchBar = searchController.searchBar
filterContentForSearchText(searchBar.text!)
}
}
and Section:
struct BasicGridSection: Section {
// TODO: create a constant for the title of the header of type String
var categories: [String]
var numberOfItems: Int
// TODO: create an initializer to set the title
init(categories: [String], count: Int) {
self.categories = categories
self.numberOfItems = count
}
func layoutSection() -> NSCollectionLayoutSection? {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.1), heightDimension: .fractionalHeight(0.8))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(0.4))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 2)
let section = NSCollectionLayoutSection(group: group)
return section
}
func configureCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: GridCell.self), for: indexPath) as? GridCell
cell!.set(label:categories[indexPath.row])
return cell!
}
}
Thanks!

Unable to simultaneously satisfy constraints. Adaptive cell height using UICollectionViewFlowLayout

I'm trying to make a news feed like app using UICollectionView with adaptive cell height. I found this tutorial and used the option "2. Solution for iOS 11+" for my code, which works perfectly fine. However, whenever I try to add more subviews to the cell and layout them as required, I get this error: "Unable to simultaneously satisfy constraints. Probably at least one of the constraints in the following list is one you don't want."
Below is my code which gives me an error.
NewsFeedViewController:
import UIKit
class NewsFeedViewController: UIViewController {
var friends: [Friend] = []
var allPosts: [Post?] = []
var shuffledPosts: [Post?] = []
var collectionView: UICollectionView = {
let layout = NewsFeedFlowLayout()
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize // new
layout.scrollDirection = .vertical
let collection = UICollectionView(frame: .zero, collectionViewLayout: layout)
collection.backgroundColor = UIColor.systemRed
collection.isScrollEnabled = true
collection.contentInsetAdjustmentBehavior = .always
collection.translatesAutoresizingMaskIntoConstraints = false
return collection
}()
let cellID = "NewsFeedCollectionViewCell"
override func viewDidLoad() {
super.viewDidLoad()
getPosts()
view.addSubview(collectionView)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(NewsFeedCollectionViewCell.self, forCellWithReuseIdentifier: cellID)
collectionView.pin(to: view)
}
func getPosts() {
friends = FriendFactory().friends
allPosts = friends.flatMap{$0.posts}
var tempPosts = allPosts
for _ in allPosts {
if let index = tempPosts.indices.randomElement() {
let post = tempPosts[index]
shuffledPosts.append(post)
tempPosts.remove(at: index)
}
}
}
}
extension NewsFeedViewController: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return shuffledPosts.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath) as! NewsFeedCollectionViewCell
let post = shuffledPosts[indexPath.row]
let friend = friends.first(where: {$0.identifier == post?.authorID})
let text = post?.postText
cell.configurePostText(postText: text!)
return cell
}
}
NewsFeedCollectionViewCell:
import UIKit
class NewsFeedCollectionViewCell: UICollectionViewCell {
var selectedPost = Post()
var likeBarView: LikeBarView = {
let view = LikeBarView()
view.backgroundColor = .systemIndigo
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
var postTextLabel: UILabel = {
let label = UILabel()
label.font = label.font.withSize(20)
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.backgroundColor = .systemYellow
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
addViews()
setupConstraints()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func addViews() {
addSubview(likeBarView)
addSubview(postTextLabel)
}
func configurePostText(postText: String) {
postTextLabel.text = postText
}
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
let layoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
layoutIfNeeded()
layoutAttributes.frame.size = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
return layoutAttributes
}
func setupConstraints() {
NSLayoutConstraint.activate([
likeBarView.topAnchor.constraint(equalTo: topAnchor, constant: 20),
likeBarView.leadingAnchor.constraint(equalTo: leadingAnchor),
likeBarView.trailingAnchor.constraint(equalTo: trailingAnchor),
likeBarView.heightAnchor.constraint(equalToConstant: 100),
postTextLabel.topAnchor.constraint(equalTo: likeBarView.bottomAnchor, constant: 20),
postTextLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
postTextLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
postTextLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20),
])
}
}
NewsFeedFlowLayout:
import UIKit
final class NewsFeedFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributesObjects = super.layoutAttributesForElements(in: rect)?.map{ $0.copy() } as? [UICollectionViewLayoutAttributes]
layoutAttributesObjects?.forEach({ layoutAttributes in
if layoutAttributes.representedElementCategory == .cell {
if let newFrame = layoutAttributesForItem(at: layoutAttributes.indexPath)?.frame {
layoutAttributes.frame = newFrame
}
}
})
return layoutAttributesObjects
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let collectionView = collectionView else {
fatalError()
}
guard let layoutAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes else {
return nil
}
layoutAttributes.frame.origin.x = sectionInset.left
layoutAttributes.frame.size.width = collectionView.safeAreaLayoutGuide.layoutFrame.width - sectionInset.left - sectionInset.right
return layoutAttributes
}
}
I enclose the screenshot of the collection view I'm getting and the screenshot of the analysis of the error done here, which is saying that the height of the cell is not dynamic if I'm getting it right.
What am I doing wrong?
Adding this line of code to NewsFeedViewController silenced the error:
layout.estimatedItemSize = CGSize(width: 375, height: 200)

Swift Add different actions to cells on a UICollectionView

I don't know where to start, I have a UICollectionView with multiple cells filled with a UIImage. I want each cell/UIImage to do a different action on a fingertap. Can someone pinpoint me in the right direction here?
The action that I have is a #IBAction from a UIButton, I know want have that action on a cell in the UICollectionView..
( I Guess I have to do something with the 'let cell = countryCollectionView.dequeueReusableCell' ?
import UIKit
class ViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
#IBOutlet weak var soundsCollectionView: UICollectionView!
lazy var cv: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
var cc = UICollectionView(frame: .zero, collectionViewLayout: layout)
cc.translatesAutoresizingMaskIntoConstraints = false
cc.register(CustomCell.self, forCellWithReuseIdentifier: "CustomCell")
cc.delegate = self
cc.dataSource = self
cc.backgroundColor = .white
return cc
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.addSubview(cv)
cv.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
cv.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
cv.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
cv.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCell
cell.tag = indexPath.row
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: 100)
}
}
class CustomCell: UICollectionViewCell {
lazy var centerImageView: UIImageView = {
var img = UIImageView()
img.translatesAutoresizingMaskIntoConstraints = false
img.image = UIImage(named: "1")
img.clipsToBounds = true
img.isUserInteractionEnabled = true
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handler(_:)))
tapGesture.numberOfTapsRequired = 1
img.addGestureRecognizer(tapGesture)
return img
}()
#objc private func handler(_ sender: UITapGestureRecognizer) {
print("tapped tag > ", self.tag)
}
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(centerImageView)
centerImageView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
centerImageView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
centerImageView.heightAnchor.constraint(equalToConstant: 80).isActive = true
centerImageView.widthAnchor.constraint(equalToConstant: 80).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I edit as new programmatically example for your problem solution.
class ViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
lazy var cv: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
var cc = UICollectionView(frame: .zero, collectionViewLayout: layout)
cc.translatesAutoresizingMaskIntoConstraints = false
cc.register(CustomCell.self, forCellWithReuseIdentifier: "CustomCell")
cc.delegate = self
cc.dataSource = self
cc.backgroundColor = .white
return cc
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(cv)
cv.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
cv.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
cv.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
cv.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCell
cell.tag = indexPath.row
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: 100)
}
}
class CustomCell: UICollectionViewCell {
lazy var centerImageView: UIImageView = {
var img = UIImageView()
img.translatesAutoresizingMaskIntoConstraints = false
img.image = UIImage(named: "1")
img.clipsToBounds = true
img.isUserInteractionEnabled = true
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handler(_:)))
tapGesture.numberOfTapsRequired = 1
img.addGestureRecognizer(tapGesture)
return img
}()
#objc private func handler(_ sender: UITapGestureRecognizer) {
print("tapped tag > ", self.tag)
}
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(centerImageView)
centerImageView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
centerImageView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
centerImageView.heightAnchor.constraint(equalToConstant: 80).isActive = true
centerImageView.widthAnchor.constraint(equalToConstant: 80).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
In this scenario, I would avoid using buttons on your cells. Instead you should hook into the didSelectItemAt delegate method and just add the necessary information into a data model:
struct Country {
let imageName: String
let sound: Sound // You didn't specify what type the sound1 object is but you get the gist
}
So your countries array will now contain this new struct instead of just raw Strings:
let countries = [
Country("country1", sound1),
Country("country2", sound2),
...
]
Then you can get the exact sound you want to play from the indexPath passed into didSelectItemAt:
let sound = self.countries[indexPath.row].sound
sound.play()
You'll also need to adjust how you're setting the cell's image in cellForItemAt:
let imageName = self.countries[indexPath.row].imageName
cell.countryImageView.image = UIImage(named: imageName)

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