So I create a UICollectionViewController class along with a custom UICollectionViewCell. I am presenting a new UIViewController from the CollectionView and then dismissing it in the UIViewController in order to return to the CollectionView. Everything works except for the fact that after dismissing the UIViewController it still remains in memory which is not what I want. I would like to completely destroy the UIViewController once it is dismiss but cannot figure out how do it.
Am I doing anything wrong? Is it normal for the dismiss ViewController to remain in memory after it's dismissal?
// UICollectionViewController class
class MyCollection: UICollectionViewController, UICollectionViewDelegateFlowLayout {
let cellId = "cellId"
override func viewDidLoad() {
collectionView.register(CustomCell.self, forCellWithReuseIdentifier: cellId)
}
let viewControllers:[UIViewController] = [ViewController1()]
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewControllers.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let activityCell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! CustomCell
return activityCell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let vc = viewControllers[indexPath.item]
present(vc, animated: true, completion: nil)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: 90)
}
}
// Custom UICollectionViewCell class
class CustomCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .red
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class ViewController1: UIViewController {
lazy var dismissButton: UIButton = {
let newButton = UIButton(type: .system)
newButton.setTitle("Dismiss", for: .normal)
newButton.backgroundColor = .red
newButton.addTarget(self, action: #selector(dismissView), for: .touchUpInside)
newButton.translatesAutoresizingMaskIntoConstraints = false
return newButton
}()
#objc func dismissView() {
dismiss(animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(dismissButton)
NSLayoutConstraint.activate([
dissmissButton.centerXAnchor.constraint(equalTo: view.centerXAnchor)
dissmissButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
}
Reason why ViewController1 instance is not destroyed completely?
Even after the viewController is dismissed, you're still holding the reference to it inside MyCollection's viewControllers array.
Solution:
In case, you want a brand new instance of the controller everytime a cell is tapped, there is no need to store the controller in viewControllers array.
Simply update didSelectItemAt method to,
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let vc = ViewController1() //here......
present(vc, animated: true, completion: nil)
}
No you are not doing it wrong. There is no strong retain cycle within your code.
The only problem is, even after you dismiss your view controller, it still resides in here
let viewControllers:[UIViewController] = [ViewController1()]
If you want the instance to be destroyed completely you need to remove it from the array as well.
It's not a good choice to make a global variable of a viewController.Once you still hold it,you'll be unable to destroy it.Do it like this:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let vc:UIViewController?
switch indexPath.row {
case 0:
vc = ViewController1()
case 1:
vc = ViewController2()
default:
vc = ViewController0()
}
present(vc!, animated: true, completion: nil)
}
And if there are many viewControllers, maybe it will be better to create a Array of Class like this:
guard let nameSpace = Bundle.main.infoDictionary?["CFBundleName"] as? String else { return }
let clsName = String(format: "%#.%#", nameSpace, cList[indexPath.row])
let cls = (NSClassFromString(clsName) as? UIViewController.Type)!
let vc = cls.init()
present(vc, animated: true, completion: nil)
clist:let cList = ["FirstController","SecondController"]
PS:
Of course I would not use this route if I get 50 ViewControllers.I just think that we can just use the most convenient way to solve the problem.
Hope this will help you.
Related
#TusharMordiya Please check this image
I have created a tableView inside a CollectionView.The contents of the view are UIImage and a UILabel.I want to design the cell in which when I click on a cell the image and label must go to another ViewController.
import UIKit
class ExploreTableViewCell: UITableViewCell, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout{
#IBOutlet var collectionView: UICollectionView!
var namearr = ["images-1", "images-2", "images-3", "images-4", "images-5"]
override func awakeFromNib() {
super.awakeFromNib()
collectionView.delegate = self
collectionView.dataSource = self
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 132, height: 134)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let vc = UIStoryboard(name: "DetailViewController", bundle: nil).instantiateViewController(withIdentifier:"DetailViewController") as? DetailViewController
vc?.name = namearr[indexPath.row]
vc?.img.image = UIImage(named: namearr[indexPath.row])
//self.navigationController?.pushViewController(vc, animated: true)
// showing error in here and while declaring object of viewcontroller
}
}
Please try this.
Add function for get top most view controller
extension UIApplication {
class func topViewController(_ viewController: UIViewController? = UIApplication.shared.connectedScenes
.filter({$0.activationState == .foregroundActive})
.compactMap({$0 as? UIWindowScene})
.first?.windows
.filter({$0.isKeyWindow}).first?.rootViewController) -> UIViewController? {
if let nav = viewController as? UINavigationController {
return topViewController(nav.visibleViewController)
}
if let tab = viewController as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(selected)
}
}
if let presented = viewController?.presentedViewController {
return topViewController(presented)
}
return viewController
}
}
Use this code in your didSelectItemAt method
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
if let vc = storyBoard.instantiateViewController(withIdentifier:"DetailViewController") as? DetailViewController {
vc.name = namearr[indexPath.row]
print("You tapped the cell\(indexPath) with car name \(namearr[indexPath.row]) ")
UIApplication.topViewController()?.navigationController?.pushViewController(vc, animated: true)
}
}
In your Detail Screen
class DetailViewController: UIViewController {
#IBOutlet var lbl: UILabel!
#IBOutlet var img: UIImageView!
var name = ""
override func viewDidLoad() {
super.viewDidLoad()
lbl.text = name
img.image = UIImage(named: name)
}
}
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier:"DetailViewController") as? DetailViewController
change storyBoard name to Main
I have a collectionView and I am using a custom cell that shows an Image and label. I am populating the view with an array. When a cell is selected, I want a new activity to open and the name of the class to be passed through.
Here is my code:
class CollectionViewController: UICollectionViewController {
let classes = ["English","Math","Science","Social Studies","Other","Technology"]
let class_images : [UIImage] = [
UIImage(named: "English")!,
UIImage(named: "Math")!,
UIImage(named: "Science")!,
UIImage(named: "Social Studies")!,
UIImage(named: "Other")!,
UIImage(named: "Technology")!
]
override func viewDidLoad() {
super.viewDidLoad()
var layout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
layout.sectionInset = UIEdgeInsets(top: 22, left: 22, bottom: 22, right: 22)
layout.minimumInteritemSpacing = 22
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of items
return classes.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "class_cell", for: indexPath) as! custom_class_cell
cell.class_name.text = classes[indexPath.item]
cell.class_image.image = class_images[indexPath.item]
// Configure the cell
cell.layer.borderWidth = 1.0
cell.layer.borderColor = UIColor.black.cgColor
return cell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
//This isn't the right code, but an example of what I want to do!
if (indexPath=1){
let vc = self.storyboard?.instantiateViewController(withIdentifier:
"classes")
self.present(vc!, animated: true, completion: nil)
//I want to pass this string to the class
let class_name2 = "English"
}
else if(indexPath=2){
let vc = self.storyboard?.instantiateViewController(withIdentifier:
"classes")
self.present(vc!, animated: true, completion: nil)
//I want to pass this string to the class
let class_name2 = "Math"
//it keeps going through the technology cell
}
}
In the didSelectItemAt method, there is an example of what I am trying to do, but the code isn't right. I want to do this for all cells English to Technology. Thank you in advance and let me know if you have any questions!
easiest way:
in dest controller (let's say is a DetailController instance)
you should have:
class DetailController...
... var myInfo : MyInfo?
(MyInfo should contain ALL data You want to pass.)
and in prepare for segue:
vc.myInfo = Info(class_name2)
in viewDidLoad populate your UI:
override func viewDidLoad() {
super.viewDidLoad()
self.detail.text = self. myInfo....
It is actually a really simple solution, I just over complicated it!
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let valueToPass = classes[indexPath.row]
That gets you name of each class when the cell is clicked. After that, just do the prepare for segue method.
Very new to code and making a project programmatically. I have my collection view all set up but i cant figure out how to tell a specific cell to navigate to a new collection view or detail view. Very confused and frustrated. can anyone help me or at least point me in the right direction. please dumb it down haha.
this is my main View controller
import UIKit
class HomeController: UICollectionViewController,
UICollectionViewDelegateFlowLayout {
var Legends: [Legend] = {
var select1 = Legend()
select1.thumbnailImageName = "select1thumbnail"
var select2 = Legend()
select2.thumbnailImageName = "select2humbnail"
return[select1, select2]
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Choose Selection"
collectionView.backgroundView = UIImageView(image: UIImage(named: "backgroundlogo"))
collectionView?.register(VideoCell.self, forCellWithReuseIdentifier: "cellId")
collectionView.dataSource = self
collectionView.delegate = self
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return Legends.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! VideoCell
cell.legend = Legends[indexPath.item]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width:view.frame.height, height: 150)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let select2VC = UIViewController()
navigationController?.navigationBar.tintColor = UIColor.white
navigationController?.pushViewController(select2VC, animated: true)
print("selcected")
}
}
this is the swift file i have for my collection view
import UIKit
class VideoCell: UICollectionViewCell {
var legend: Legend? {
didSet {
thumbnailImageView.image = UIImage(named: (legend?.thumbnailImageName)!)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
let thumbnailImageView: UIImageView = {
let imageView = UIImageView()
imageView.backgroundColor = UIColor.darkGray
imageView.image = UIImage(named:"bgcolor")
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
func setupViews() {
addSubview(thumbnailImageView)
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-16-[v0]-16-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["v0": thumbnailImageView]))
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-1-[v0]-0-|", options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: ["v0": thumbnailImageView]))
}
}
in a 3rd file i have just this bit of code
import UIKit
class Legend: NSObject {
var thumbnailImageName: String?
}
the bottom code of the main view controller does print out a "selected" but it prints it for every cell... I assume each cell will need its own viewController.swift file? then tell that cell to that swift file to show the contents?? thank you for your time.
No you don't need to create its own viewController.swift file for each cell. You just need to create one detail page screen (one DetailViewController.swift) and you need to pass the detail of selected cell with variables declared on detail view controller.
Like I have done HERE in demo project with your code and result will be:
What I have done is I have added detailViewController in storyboard and also added class file. and didSelectItemAt will look like:
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: "DetailViewController") as! DetailViewController
controller.selectedIndex = indexPath.row //pass selected cell index to next view.
self.navigationController?.pushViewController(controller, animated: true)
}
Here you can pass data with controller.selectedIndex = indexPath.row (Herer I am passing selected index) because selectedIndex is a property of DetailViewController because I have declared it in DetailViewController.swift like
var selectedIndex = 0
And same way you can pass other data as well which is related with selected index.
For more info refer demo project.
If you want check what is sell pressed. You can check what is type cell pressed:
if let cell = tableView.cellForRow(at: indexPath) as? VideoCell {
//check some property and go to new UIViewController or UICollectionViewController
//and transfer data
}
I'm working with a UITableViewController and when clicking a cell i would like to have a UICollectionViewController be popped up.
I've been successful in presenting the UICollectionViewController from a UITableViewController. However, I'm not able to figure out on how to present it like a pop-up only taking portion of the screen. It currently feels the entire view.
I searched on stackoverflow but couldn't find a working solution.
UITableViewController
class SearchViewController: UITableViewController, UISearchBarDelegate {
let cellId = "cellId"
var filteredArray = [String]()
let books = Model().books
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
navigationItem.titleView = navSearchBar
setupView()
}
// Views.
let navSearchBar = NavSearchBar()
func setupView() {
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellId)
tableView.delegate = self
tableView.dataSource = self
navSearchBar.delegate = self
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { // Cancel button is touched.
self.dismiss(animated: true, completion: nil)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return books.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
book = books[indexPath.row]
}
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
cell.textLabel?.text = book
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)
let cellLabelContent = cell!.textLabel!.text // Gets cell name.
let cellLabelIndex = bibleBooks.firstIndex(of: cellLabelContent!) // searches books array to get correct index of cell name.
print("Book name:", cellLabelContent!+",", "index:", cellLabelIndex!)
let notificName = Notification.Name(rawValue: searchedBookIndex)
NotificationCenter.default.post(name: notificName, object: cellLabelIndex)
dismiss(animated: true, completion: nil)
**HERE is where i present the collectionView**
let layout = UICollectionViewFlowLayout()
var topVC = UIApplication.shared.keyWindow?.rootViewController
while((topVC!.presentedViewController) != nil) {
topVC = topVC!.presentedViewController
let navController = UINavigationController(rootViewController: ChapterNumbersCollectionView(collectionViewLayout: layout))
topVC?.present(navController, animated: true, completion: nil)
}
}
UICollectionViewController
class ChapterNumbersCollectionView: UICollectionViewController {
let reuseIdentifier = "CellId"
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
setupNavigationItem()
}
private func setupCollectionView() {
collectionView.backgroundColor = .yellow
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
}
private func setupNavigationItem() {
let button = UIButton(type: .system)
button.setTitle("cancel", for: [])
button.addTarget(self, action: #selector(handleDismiss), for: .touchUpInside)
button.frame = CGRect(x: 0, y: 0, width: 34, height: 34)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: button)
}
#objc func handleDismiss() {
dismiss(animated: true, completion: nil)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 200
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
cell.backgroundColor = .red
return cell
}
One approach would be to use view controller containment. You'd instantiate ChapterNumbersCollectionView in didSelectRow, but instead of presenting, add it as a child view controller.
Then, having added its view as a subview, animate it in using animateWithDuration.
I got a HomeController of type UICollectionViewController which handles some PictureCells (contains a picture and a label).
Now I am sending one PictureCell to another ViewController to edit the label. This all works perfectly. I could send it back to the HVC using a protocol but instead of going back I want to go one step further and Display the edited PictureCell in a new ViewController.
Instead of creating a completely new one, I am subclassing the existing HomeController to reuse his CollectionView. All works fine and the view shows up but the edited PictureCell is not showing up at all, even tho I can show it's layer, the Cell itself with its content doesn't show up.
(Messy) Class-Diagram looks like this:
ClassDiagram
class HomeController: UICollectionViewController, UICollectionViewDelegateFlowLayout, MyProtocol {
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.backgroundColor = UIColor.black
collectionView?.register(PictureCell.self, forCellWithReuseIdentifier: Constants.cellId)
}
//MARK: Get value from second VC
var valueSentFromSecondViewController: String?
var cellSentFromSecondViewController: PictureCell?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
//Acting with Protocol here
}
//HERE ARE THE COLLECTIONVIEWFUNCTIONS
}
class PictureEditViewController: UIViewController, UITextFieldDelegate {
var delegate:MyProtocol?
var myCell: PictureCell?
let collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = UIColor.white
return cv
}()
init(pictureCellInit: PictureCell?) {
self.myCell = pictureCellInit
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
showThings()
}
#objc func showNextVC() {
let newViewController = PictureShowViewController(withCell: self.myCell)
newViewController.modalPresentationStyle = .overCurrentContext
present(newViewController, animated: true) //with dismiss
}
#objc func showThings() {
view.addSubview(collectionView)
self.collectionView.frame = CGRect(x: x, y: y, width: width, height: height)
self.setupViews()
}
func setupViews() {
//ADDING THE SUBVIEWS
}
func confBounds() {
//LAYOUT
}
#objc func handleDismiss() {
self.dismiss(animated: true, completion: nil)
}
class PictureShowViewController: HomeController {
//MARK: Variable/Constant declaration
var myCellToShow: PictureCell?
init(withCell: PictureCell?) {
let layoutUsing = UICollectionViewFlowLayout()
myCellToShow = withCell
super.init(collectionViewLayout: layoutUsing)
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.backgroundColor = .white
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
collectionView?.addSubview(myCellToShow!)
self.collectionView?.reloadData()
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = myCellToShow {
cell.layer.borderColor = UIColor.red.cgColor
cell.layer.borderWidth = 2
return cell
}
else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Constants.cellId, for: indexPath) as! PictureCell
return cell
}
}
//Size of Cell
override func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let cellSize = CGFloat(view.frame.width)
return CGSize(width: cellSize, height: cellSize)
}
}
Does anyone have an idea where I went wrong?
It is a bad idea to use PictureCell to move your data around view controllers. UICollectionView reuses instances of cells so the content of the cell is bound to change anytime.
So instead of using your cell, hand over the underlying data and insert the data in to the newly dequeued collection view cell.
In the end,
var cellSentFromSecondViewController: PictureCell?
should be var pictureFromSecondViewController: YourPictureData
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Constants.cellId, for: indexPath) as! PictureCell
//Set your data
cell.picture = pictureFromSecondViewController
return cell
}