How do I zoom an image from a UICollectionView? - ios

I've created a UICollectionView in Swift with photos taken from an API : https://jsonplaceholder.typicode.com/photos
I've created a window where my images can be set to fullscreen here:
extension ViewController: UICollectionViewDataSource, UICollectionViewDelegate{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return posts.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = myCollectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCollectionViewCell
let url = URL(string: "https://via.placeholder.com/600/\(posts[indexPath.row].thumbnailUrl)")
cell.myImageView.downaloadImage(from: url!)
cell.myImageView.layer.cornerRadius = 25
cell.myLabelName.text = posts[indexPath.row].title
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
let alert = UIAlertController(title: "FullScreen", message: "Are you sure you want to see the image fullscreen?", preferredStyle: .alert)
let actionyes = UIAlertAction(title: "Yes", style: .default) { action in
cell?.frame = UIScreen.main.bounds
cell?.backgroundColor = .magenta
cell?.contentMode = .scaleAspectFit
//de schimbat imagine thumbnailURL cu url
cell?.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer(target: self, action: #selector(self.dismissFullscreenImage))
cell?.addGestureRecognizer(tap)
self.view.addSubview((cell)!)
self.navigationController?.isNavigationBarHidden = true
self.tabBarController?.tabBar.isHidden = true
}
let actionno = UIAlertAction(title: "No", style: .default) { action in
}
alert.addAction(actionno)
alert.addAction(actionyes)
present(alert, animated: true)
}
#objc func dismissFullscreenImage(sender: UITapGestureRecognizer) {
let alert2 = UIAlertController(title: "Go Back", message: "Are you sure you want to go back?", preferredStyle: .alert)
let actionyes2 = UIAlertAction(title: "Yes", style: .default) { action in
self.navigationController?.isNavigationBarHidden = false
self.tabBarController?.tabBar.isHidden = false
sender.view?.removeFromSuperview()
}
let actionno2 = UIAlertAction(title: "No", style: .default) { action in
}
alert2.addAction(actionno2)
alert2.addAction(actionyes2)
self.present(alert2, animated: true)
}
}
I'm trying to zoom the image that's in fullscreen but I don't really know where to start. My structures are here:
import Foundation
struct Post : Codable
{
let albumId : Int
let id : Int
let title : String
let url : String
let thumbnailUrl : String
}
Also, when I exit full screen my image disappears and I don't know how to keep it there. I think the problem is from here:
sender.view?.removeFromSuperview()
Can I make the image zoom from code? Or do I need something else? I've seen a lot of people using a scrollview but my images are in a collection view cell as shown here:
import UIKit
class MyCollectionViewCell: UICollectionViewCell {
#IBOutlet var myImageView: UIImageView!
#IBOutlet var myLabelName: UILabel!
}

Yes, calling sender.view?.removeFromSuperview() is bad because you are removing the collection view cell from the collection view. You must not do that.
You should not be doing anything to the collection view cell when you want to show the image fullscreen. Instead, create a new UIImageView with the image from the cell's image view, and show that new image view fullscreen. When you want to dismiss it, simply remove that image view. This will leave the original collection view cell and its image view untouched and still visible after the zoom.
It would also be a nice effect if you create the new image view with the same size and position of the cell's image view and then animate the new image view to fullscreen.
You could put the new image view into a UIScrollView if you want to allow the user to zoom in on the image but that's beyond the scope of your original question.

You can use ImageScrollView in place of ImageView in your cell.
Below is the link for the same.
https://github.com/huynguyencong/ImageScrollView

Related

Can't set the image of a imageView in a customTableViewCell

So in my app I currently have a tableView with custom cells that have an imageView in them. Here is an image for reference to see what It look like and here is the code that defines the custom cell:
class GearComponentTableViewCell: UITableViewCell {
var delegate:Stepper?
var tableViewCellPosition: Int! = nil
// Image
#IBOutlet weak var itemImage: UIImageView!
// Name
#IBOutlet weak var itemName: UILabel!
// Weight
#IBOutlet weak var itemWeight1: UILabel!
#IBOutlet weak var itemWeight2: UILabel!
// Quanity
#IBOutlet weak var itemQuanity: UILabel!
#IBAction func stepperPressed (_ sender: UIStepper!) {
if (sender.value == 1) {
sender.value = 0
delegate?.stepperWasPressed(didIncrease: true, namePassed: itemName.text!, userindexPath: tableViewCellPosition)
} else if (sender.value == -1) {
sender.value = 0
delegate?.stepperWasPressed(didIncrease: false, namePassed: itemName.text!, userindexPath: tableViewCellPosition)
}
}
// Notes
#IBOutlet weak var itemNotes: UILabel!
}
These cells are tappable and when tapped they present an alert with options for adding an image to the cell you chose:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("User selected item: \(indexPath.row) and name is \(itemArray[indexPath.row].name)")
showPopUp(itemChosen: indexPath)
}
Here is the logic for the pop up/Alert:
func showPopUp(itemChosen: IndexPath) {
let alert = UIAlertController(title: "Add Photo To Item", message: .none, preferredStyle: .actionSheet)
// alert actions
let action1 = UIAlertAction(title: "Take Photo", style: .default) { action in
self.takePhoto()
}
let action2 = UIAlertAction(title: "Import Photo", style: .default) { action in
self.importPhoto(position: itemChosen)
}
let action3 = UIAlertAction(title: "Cancel", style: .destructive)
alert.addAction(action1)
alert.addAction(action2)
alert.addAction(action3)
present(alert, animated: true,completion: nil)
}
Ignore the takePhoto() function because it isn't being used yet, but right below that I am calling the importPhoto() function that takes in the position of the item you selected:
func importPhoto(position: IndexPath) {
print("User chose to import photo")
let picker = UIImagePickerController()
picker.allowsEditing = true
picker.delegate = self
present(picker, animated: true) {
print("cell tapped \(position)")
self.cellChosen = position
}
}
It then sets a global variable to the item you selected (I know this isn't a good practice vs. passing the actual indexPath) that I use to pass into the imagePickerControllerDelegate function:
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let image = info[.editedImage] as? UIImage else { return }
dismiss(animated: true)
if let cell = tableView(gearTableView, cellForRowAt: cellChosen) as? GearComponentTableViewCell {
cell.itemImage.image = image
self.updateUI()
print("item image set for the item at position: \(cellChosen.row)")
}else {
print("did not set item image")
}
}
This is where I am running into issues now. When that delegate function gets called it successfully prints the true-case in the if/else statement and the photo isn't actually being set and I'm not receiving any errors at the time it is set. The only error I receive is "Changing the translatesAutoresizingMaskIntoConstraints property of a UICollectionViewCell that is managed by a UICollectionView is not supported, and will result in incorrect self-sizing." when I tap the right bar-button item from the first photo in my post to add a new cell to the tableView. I require some info in the form of another Alert with textfields for some info about the item you are creating before the cell is actually created to I'm not sure if that error is actually relevant to what I am currently working on.
Any ideas as to how I can set the image of the imageView in my cell? I've already tried changing the Layout of my imageView from Inferred (constraints) to Auto-resizing mask and any new cells stopped appearing...
Here is a link to my repository/the viewController I was working in.
It's because you're not updating the image on the data source of the table view. You update when you dequeue the cell, but the you call updateUI that calls reloadData().
For instance, your method imagePickerController can work like this:
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let image = info[.editedImage] as? UIImage else { return }
dismiss(animated: true)
let item = itemArray[cellChosen.row]
let userSubmittedItem = GearItem(itemName: item.name,
itemImage: image,
itemWeight1: item.weight1,
itemWeight2: item.weight2,
itemQuantity: item.quantity,
itemNotes: item.notes)
itemArray[cellChosen.row] = userSubmittedItem
self.updateUI()
}
Change your implementation of method imagePickerController(_ picker:didFinishPickingMediaWithInfo:) to
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let image = info[.editedImage] as? UIImage else { return }
picker.dismiss(animated: true)
itemArray[cellChosen.row].image = image
gearTableView.reloadRows(at: [cellChosen], with: .none)
}
When you need to update an image of a data source model. You update the model and call reload row.
About why your code doesn't work, set a breakpoint at func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell, and you will find your imageView's image is beening reset in above method.

Swift UITableView messing up after reloadData()

I'm developing an iOS app where I want to reload information of a UITableView from a different controller. In order to do that, I have a reference of the controller MainViewController with the UITableView in another controller called (AirlinesController).
I have an issue reloading the data of the UITableView of the first controller where it messes up pretty much:
MainViewController
Each cell of the table you can see leads to AirlinesController:
AirlinesController
So after the user clicks on the "Apply" button, the UITableView of MainViewController reloads using mainView.reloadData(). In this example I would want to see in the MainViewController that the cell with the title "Embraer" has a green label with the text "Completed" and the cell with the title "Airbus" has a yellow label with the title "Employee" but this is the result I get after reloading the table:
Why did the last cell of the table change one of its labels color to yellow?
This is the code I'm using:
MainViewController
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "itemCell", for: indexPath) as! CellController
let items = self.gameView.data!.levels
cell.model.text = items[indexPath.row].name
if items[indexPath.row].id < self.gameView.game!.level.id {
cell.cost.text = "Complete"
cell.cost.textColor = UIColor.systemGreen
cell.accessoryType = .none
cell.isUserInteractionEnabled = false
} else if items[indexPath.row].id == self.gameView.game!.level.id {
cell.cost.text = "Employee"
cell.cost.textColor = UIColor.systemYellow
cell.accessoryType = .none
cell.isUserInteractionEnabled = false
} else {
cell.cost.text = "\(items[indexPath.row].XP) XP"
}
return cell
}
AirlinesController
#IBAction func apply(_ sender: Any) {
if (mainView.game.XP >= level.XP) {
let new_salary = Int(Float(mainView.game.salary) * level.salaryMultiplier)
let new_XP = Int(Float(mainView.game.XPSalary) * level.XPMultiplier)
mainView.game.salary = new_salary
mainView.game.XPSalary = new_XP
mainView.salaryLabel.text = "\(new_salary) $"
mainView.XPLabel.text = "\(new_XP) XP"
mainView.workingFor.text = "Currently working for \(level.name)"
mainView.game.level = Level(id: level.id, name: level.name, XP: 0, XPMultiplier: 1, salaryMultiplier: 1, aircrafts: [Aircraft]())
mainView.saveGame()
DispatchQueue.main.async {
self.mainView.levelItems.reloadData()
self.navigationController?.popViewController(animated: true)
}
} else {
let alert = UIAlertController(title: "Error", message: "Not enough experience to apply!", preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.cancel, handler: nil))
self.present(alert, animated: true)
}
}
How can I fix this?
This is because of the reuse of cells.
Set the default color in else condition.
} else {
cell.cost.textColor = UIColor.gray
cell.cost.text = "\(items[indexPath.row].XP) XP"
}
Another way, you can set the default style property inside the prepareForReuse method of UITableViewCell
class TableViewCell: UITableViewCell {
override func prepareForReuse() {
super.prepareForReuse()
// Set default cell style
}
}
TableViewCell are reused all the time. My advice is that you have to set the default color every time.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "itemCell", for: indexPath) as! CellController
cell.cost.textColor = ...your default text color....
...
return cell

Changing navigation Left Bar button title while selecting collection cell inside a container view

I have a collection view inside a container view. In collection view I have two parts as, UICollectionReusableView and UICollectionViewCell . Inside the UICollectionReusableView again I have a collection view and while selecting a cell from that cell I have to change the LeftNavigationBarTitle. I tried the below code in didSelectItem of collection view but nothing is happen.
navigationItem.leftBarButtonItem = UIBarButtonItem(image: UIImage(named: "cancel-music")?.withRenderingMode(.alwaysOriginal), style: .plain, target: self, action: #selector(self.closeViewTapped)).
I used the above code but it didn't change the navigationButton.
This my storyBoard design.
Is any mistake in my code, please help me to get the exact one. Thankyou.
Create the protocol method for view controller which contains collection view.
Inside HomeFilterVC
protocol HomeFilterVCDelegate: class {
func collectionViewDidTapped()
}
Then, declare delegate variable like as follow, which will assign to your view controller which contains UIContainerView.
class HomeFilterVC: UIViewController {
weak var delegate : HomeFilterVCDelegate?
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let selfDelegate = self.delegate {
selfDelegate.collectionViewDidTapped()
}
}
}
Implement prepare(for segue:, sender:) and HomeFilterVCDelegate methods in HomeVC and update your code as follow.
HomeVC:
class HomeVC: UIViewController {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "HomeFilterVC" {
let collectionVC = segue.destination as! HomeFilterVC
collectionVC.delegate = self
}
}
}
extension HomeVC: HomeFilterVCDelegate {
func collectionViewDidTapped() {
navigationItem.leftBarButtonItem = UIBarButtonItem(image: UIImage(named: "cancel-music")?.withRenderingMode(.alwaysOriginal), style: .plain, target: self, action: #selector(self.closeViewTapped)).
}
}
I hope this will fix your issue.
First of all, create a property for leftItem in your ViewController that will save the reference to leftBarButtonItem i.e.
class VC: UIViewController, UICollectionViewDelegate {
lazy var leftItem: UIBarButtonItem = {
let item = UIBarButtonItem(image: UIImage(named: "Initial Title")?.withRenderingMode(.alwaysOriginal), style: .plain, target: self, action: #selector(closeViewTapped))
return item
}()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationItem.leftBarButtonItem = self.leftItem
}
}
In the above code, I've added the leftBarButtonItem initially in viewWillAppear(_:). Since we're saving the reference to leftBarButtonItem in leftItem, we can make any changes to it using leftItem.
Then in your collectionView(_:didSelectItemAt:) use the same property to change your leftBarButtonItem's title, i.e.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.leftItem.title = "Title at Index - \(indexPath.row)"
}
You can change button title like below :
func changeBarButtonTitle() {
let leftitem = self.navigationItem.leftBarButtonItem!
let leftbutton = leftitem.customView as! UIButton
leftbutton.setTitle("<YOUR-TITLE>", for: .normal)
}
Also if you know that a button already there and just want to change title use below function.
func changeBarButtonTitle() {
if let leftitem = self.navigationItem.leftBarButtonItem {
print("Left Button")
leftitem.title = "test-left"
}
if let rightitem = self.navigationItem.rightBarButtonItem {
print("Right Button")
rightitem.title = "test-right"
}
}
Hope this will helps to change bar button title!

Table view with button that changes label based on user input

I'm trying to create a table view with just 1 label and 1 button in the prototype cell.
When you click the button, it asks the user for text input, and then replaces the label in the same cell with that text. I have been able to create a version of this where pressing the button updates the corresponding label with pre-determined text, but not input text.
The problems are:
(a) Can't seem to run an alert asking for user input in the TableViewCell class I created - must be in the ViewController to do that it seems?
(b) Have set up a TableCellDelegate protocol, and can detect a button press, then pass back to the ViewController to run the alert, but can't find a way to send the text input back to the TableViewCell to be updated.
Any help would be appreciated.
Here is the ViewController code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
extension ViewController: TableCellDelegate {
func didTapButton(label: String) {
print(label)
}
}
extension ViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell") as! TableViewCell
let label = "Label " + String(indexPath.row)
cell.setLabel(label: label)
cell.delegate = self
return cell
}
}
And here is the TableViewCell (without the alert coding):
import UIKit
protocol TableCellDelegate {
func didTapButton (label: String)
}
class TableViewCell: UITableViewCell {
#IBOutlet weak var tableCellLabel: UILabel!
var delegate: TableCellDelegate?
func setLabel(label: String) {
tableCellLabel.text = label
}
#IBAction func buttonTapped(_ sender: Any) {
delegate?.didTapButton(label: tableCellLabel.text!)
tableCellLabel.text = "pressed"
}
}
Finally, here is the alert code I am trying to insert in place of the tableCellLabel.text = "pressed" code above:
// Create the alert window
let buttonAlert = UIAlertController(title: "Label", message: "Enter Label", preferredStyle: UIAlertControllerStyle.alert)
// Add text input field to alert controller
buttonAlert.addTextField { (buttonLabel:UITextField!) -> Void in
buttonLabel.placeholder = "Enter Label"
}
// Create alert action for OK button
let okButtonAction = UIAlertAction.init(title: "OK", style: UIAlertActionStyle.default) { (alert:UIAlertAction!) -> Void in
// Get the button name from the alert text field
let buttonLabel = buttonAlert.textFields![0]
let buttonLabelString = buttonLabel.text
self.tableCellLabel.text = buttonLabelString
// Dismiss alert if ok pressed
buttonAlert.dismiss(animated: true, completion: nil)
}
// Add ok Button alert actions to alert controller
buttonAlert.addAction(okButtonAction)
// Create alert action for a cancel button
let cancelAction = UIAlertAction.init(title: "Cancel", style: UIAlertActionStyle.cancel) { (alert:UIAlertAction!) -> Void in
// Dismiss alert if cancel pressed
buttonAlert.dismiss(animated: true, completion: nil)
}
// Add Cancel Button alert actions to alert controller
buttonAlert.addAction(cancelAction)
// Display the alert window
self.present(buttonAlert, animated: true, completion: nil)
Where this goes wrong is the self.present(buttonAlert, animated: true, completion: nil), which it doesn't appear you can call from a table cell.
Depending on how you have the UItableview set up, it sounds like you're going to want to take the id of the data of the cell that is registering the alert and then updating the button text in that cell based on that within the table view render statement. Remember that setting the label of a button has an animation, it can cause some odd things to happen if you're animating other things at the same time.
You'll probably want to look at reload rows in the link below in your table view. Be careful though, the indexPath is not always the same as when the user interaction is called if you're using dequeueReusableCell.
UITableView Reference
See UIButton Reference for swift implementation, you'll have to set it for the current state of the button.

Presenting ActionSheet Alert on UICollectionViewController

I have custom CollectionViewCell called DiaryCell in my project. I configure my cells using that class. Each cell has this menu button, and when it's clicked I want UICollectionviewcontroller to present actionsheet, but it gives me the following error.
'NSInvalidArgumentException', reason: 'UICollectionView must be initialized with a non-nil layout parameter'
Here is the code to handle that action when menu button is pressed in the cell. This action is handled in UICollectionViewController. To sum it up, my tap gesture is defined in customCell and when tapped it redirects the code to controller to handle the actions.
func menuButtonPressed(_ gesture: UITapGestureRecognizer) {
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
let editAction = UIAlertAction(title: "Edit", style: .default) { (action) in
}
let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { (action) in
}
let alert = UIAlertController(title: "Pick Your Adventure 😜", message: "You have the following options", preferredStyle: .actionSheet)
alert.addAction(cancelAction)
alert.addAction(editAction)
alert.addAction(deleteAction)
present(alert, animated: true, completion: nil)
}
I believe it has something to do with view layers. Since a cell is tapped inside UICollectionview, it doesn't know how and where to present actionsheet. I could be wrong. Thank you for reading my question.
EDIT: Code responsible for redirection of action to controller.
var homeCollection = HomeCollectionViewController()
let menuImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = #imageLiteral(resourceName: "menu_image")
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
imageView.isUserInteractionEnabled = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
func setUpMenuItemGesture() {
let tapGesture = UITapGestureRecognizer(target: self.homeCollection, action: #selector(self.homeCollection.menuButtonPressed(_:)))
tapGesture.numberOfTapsRequired = 1
tapGesture.delegate = homeCollection
menuImageView.addGestureRecognizer(tapGesture)
}
CODE FOR LAYOUT IN MY UICollectionViewController:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = collectionView.frame.size.width
return CGSize(width: width, height: width + (width*(0.15)))
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
My guess is that the problem is this line:
var homeCollection = HomeCollectionViewController()
That causes a completely new, separate HomeCollectionViewController to be created. It was never properly initialized and thus is invalid; more important, it is not the HomeCollectionViewController you want. You want a reference to the existing HomeCollectionViewController that you are already using, not a new and different HomeCollectionViewController.

Resources