Complex Image based Button Layout - swift

I'm fairly new to programming in general, I'm trying to figure out how to create numerous buttons in a complex layout. My layout is basically a transparent PNG of a body cut into about 24 sections and I want each segment of the body to be a separate button.
I've tried a few layouts in the view controller. Setting up a ton of buttons overlaying an image (couldn't keep the layout straight when launching in the simulator) and I've tried giving the buttons images, I've tried large image sized buttons but I could only use the top most button.
Is there any way to do this, or is it going to need code to be doable?

UICollectionViewController or UICollectionView is a better choice for you. You can set the UICollectionViews background image and regard the cells as your buttons.
Here is a quick example.
import UIKit
class Collection: UICollectionViewController {
private let cellId = "cell"
override init(collectionViewLayout layout: UICollectionViewLayout) {
if let l = layout as? UICollectionViewFlowLayout{
l.minimumInteritemSpacing = 0
l.sectionInset = UIEdgeInsetsZero
l.scrollDirection = .Vertical
l.footerReferenceSize = CGSize.zero
l.headerReferenceSize = CGSize.zero
let screenWidth = UIScreen.mainScreen().bounds.width
let itemWidth = CGFloat(Int(screenWidth/4))
l.itemSize = CGSize(width: itemWidth, height: itemWidth)
}
super.init(collectionViewLayout: layout)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.whiteColor()
collectionView?.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: cellId)
}
}
extension Collection{
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 4
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 4
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellId, forIndexPath: indexPath)
cell.backgroundColor = UIColor.redColor()
return cell
}
}
If you want to add touch action to a cell.
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
}

Related

Updating height constraint of UICollectionView not working

I have this collection view inside my collection view cell, of which I will populate with more cells. I'm currently trying to update the height of the UICollectionView upon any editing events to the inner cells text field.
This currently works when I load the page, click into the field and then scroll. However, if I load the page, scroll first and then click into the field, it doesn't work but I'm unsure why. I can't see anything that is running differently.
Any help would be appreciated. Thanks.
import UIKit
class MyCollectionViewCell: UICollectionViewCell {
var heightAnchor: NSLayoutConstraint?
var collectionView: UICollectionView!
static let reuseIdentifier: String = "MyCollectionViewCell"
override func awakeFromNib() {
super.awakeFromNib()
var layout: UICollectionViewLayout = UICollectionViewLayout()
collectionView = UICollectionView(frame: contentView, collectionViewLayout: layout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.isScrollEnabled = false
self.contentView.addSubview(collectionView)
heightAnchor = collectionView.heightAnchor.constraint(equalToConstant: 600)
heightAnchor?.isActive = true
collectionView.anchorViewPosition(parent: self.contentView, leading: .zero, top: .zero, bottom: .zero)
collectionView.register(UINib(nibName: "MyInnerCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: MyInnerCollectionViewCell.reuseIdentifier)
}
override func layoutSubviews() {
super.layoutSubviews()
collectionview.widthAnchor.constraint(equalToConstant: 288).isActive = true
}
#objc func updateCollectionViewSize() {
heightAnchor?.constant = 200
}
}
extension MyCollectionViewCell: UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyInnerCollectionViewCell", for: indexPath) as? MyInnerCollectionViewCell
cell?.field.addTarget(self, action: #selector(updateCollectionViewSize()), for: .allEditingEvents)
return cell!
}
}

UICollectionViewCell changes cell contents when scrolling

I have a custom UICollectionView. I am using a UIView instead of a UICollectionViewCell as I'm wanting to use the UIView elsewhere.
The UICollectionView scrolls only horizontally and has 14 cells; each with the UIView as its contents.
Inside this UIView is another UICollectionView which houses a UICollectionViewCell which is just an image of a dice.
So:
Outer UICollectionView with Cell
The cell has a UIView
The UIView has a UICollectionView and UICollectionViewCell
The UICollectionViewCell is just an imageView of a dice.
The problem I'm having is, when the UICollectionView scrolls horizontally, the contents of the cell are changing when they should remain static; sometimes it randomly moves the contents, or completely removes the contents.
I'm not sure what is causing this to happen.
How can I ensure that upon scrolling that the UICollectionView keeps its contents intact?
Outer UICollectionView:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return allLocos?.count ?? 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell:OuterCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "OuterCollectionViewCell", for: indexPath) as! OuterCollectionViewCell
if cell.cardView == nil {
let arr = UINib(nibName: "CardView", bundle: nil).instantiate(withOwner: nil, options: nil)
let view = arr[0] as! CardView
cell.contentView.addSubview(view)
cell.cardView = view
}
if let locos = self.allLocos {
let loco:EYLocomotive = locos[indexPath.row]
print(indexPath.row, loco.name, loco.orders)
cell.engineCardView?.setup(loco:loco)
}
cell.layoutIfNeeded()
return cell
}
I think the issue is being caused by this line:
cell.engineCardView?.setup(loco:loco)
My UIView (The cardView) has this code:
#IBOutlet weak var diceCollectionView: UICollectionView!
var dice:[Int] = [Int]()
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func setup(loco:EYLocomotive) {
self.diceCollectionView.delegate = self
self.diceCollectionView.dataSource = self
self.diceCollectionView.allowsSelection = false
self.diceCollectionView.allowsMultipleSelection = false
self.diceCollectionView.isUserInteractionEnabled = false
self.diceCollectionView.showsVerticalScrollIndicator = false
self.diceCollectionView.showsHorizontalScrollIndicator = false
self.diceCollectionView.register(UINib(nibName: "EYDiceCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "EYDiceCollectionViewCell")
// add dummy dice for testing purposes only
for var _ in 1...3 {
let die = Die().roll
dice.append(die)
}
}
The diceCollectionView is the holder of the Dice Imagery.
When we get to to the UICollectionViewDelegate; in the same file;
// MARK: Collection View Delegate
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dice.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: EYDiceCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "EYDiceCollectionViewCell", for: indexPath) as! EYDiceCollectionViewCell
let dieValue = self.dice[indexPath.row] as Int
let imageFile:String = "die-face-\(dieValue)"
cell.imageView.image = UIImage(named: imageFile)
cell.layoutIfNeeded()
return cell
}
The UICollectionView works fine, the problem comes when I start scrolling horizontally out of the viewport and then scroll back.
Contents of the cell change.
Contents of the cell sometimes change to a different one to the one I'm expecting
Contents of the cell sometimes disappear upon scrolling
What I'd like to know is how can I ensure the contents do not change?
With thanks

Tap button to change UICollectionView Cell size?

I have a collection view on my HomeController (UICollectionView) Class and my PostCell (UICollectionViewCell) class. HomeController has just 4 different cells. I also have a button inside my PostCell that when touched, handles a handleChangeSize function. When I touch that button, I want that specific cells size to change. Preferably change in height, similar to Instagrams "show more" feature on their cells. Any help will be highly, highly appreciated. Thank you... Here's my code:
class HomeController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
let cellId = "cellId"
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.backgroundColor = UIColor(white: 0.90, alpha: 1.0)
collectionView?.register(PostCell.self, forCellWithReuseIdentifier: cellId)
collectionView?.showsVerticalScrollIndicator = false
collectionView?.alwaysBounceVertical = true
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 4
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! PostCell
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let height = (view.frame.width) * 9 / 16
return CGSize(width: view.frame.width, height: height + 50 + 50)
}
}
class PostCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
lazy var myButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Change Size", for: .normal)
button.addTarget(self, action: #selector(handleChangeSize), for: .touchUpInside)
return button
}()
func handleChangeSize() {
}
func setupViews() {
addSubview(myButton)
myButton.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
myButton.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I have done this in tableView. follow the same for collectionView.
var expandedCells = [Int]()
here is the cellForRowAtIndexPath method
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RequestTableViewCell") as! RequestTableViewCell
cell.selectionStyle = .none
cell.optionButton.tag = indexPath.row
cell.optionButton?.addTarget(self, action: #selector(RequestsViewController.deleteAction), for: UIControlEvents.touchUpInside)
return cell
}
and the action for option button in tableViewCell is
func deleteAction(_ sender: UIButton){
if let button = sender as? UIButton {
// If the array contains the button that was pressed, then remove that button from the array
if expandedCells.contains(sender.tag) {
expandedCells = expandedCells.filter { $0 != sender.tag }
}
// Otherwise, add the button to the array
else {
expandedCells.removeAll()
expandedCells.append(sender.tag)
}
// Reload the tableView data anytime a button is pressed
self.requestTableView.beginUpdates()
self.requestTableView.endUpdates()
}
}
and the heightForRowAtIndexPath method is
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// Set the row height based on whether or not the Int associated with that row is contained in the expandedCells array
if expandedCells.contains(indexPath.row) {
return 112
} else {
return 67
}
}
The solution to this problem is quite easy. First, you need to specify what height you want to set for the particular cell and perform that change in handleChangeSize() method (this could be done by simple frame.size.height = ...).
After that, you probably need to resize the whole collection view, otherwise, there are going to be some nasty surprises for you. Perform the calculation (to get the new height) after you have resized the cell (you might call a notification or something that triggers whenever a to resize is necessary).

UICollectionView cells randomly disappear on scroll up or reload functions - iOS 9/Swift

After going through the solutions here on STO and other blogs, none seem to truly fix the UICollectionView issues others definitely seem to still have to this day!
Avoiding the reload functions initially loads the CollectionVC with no issues and scrolling down to the bottom no issues as well, but the moment I scroll up, random cells go missing. Also, noticed that the delegate method below gets called multiple times as I am scrolling, is this normal?
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
Using any of the reload functions results in random cells .backgroundView disappearing:
self.collectionView?.reloadSections(NSIndexSet.init(index: 0))
self.collectionView?.reloadData()
self.collectionView?.reloadItemsAtIndexPaths(cellIndexPathArray)
self.collectionView?.reloadInputViews()
Subclassing UICollectionViewFlowLayout with different override options seems to have no effect.
class CustomCollectionFlowLayout: UICollectionViewFlowLayout {
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
//self.scrollDirection = UICollectionViewScrollDirection.Vertical
return true
//invalidateLayoutWithContext(invalidationContextForBoundsChange(newBounds))
//return super.shouldInvalidateLayoutForBoundsChange(newBounds)
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
/*
let attributes = super.layoutAttributesForElementsInRect(rect)
print(attributes)
let contentSize = collectionViewContentSize()
return attributes?.filter { $0.frame.maxX <= contentSize.width && $0.frame.maxY < contentSize.height }
*/
let attributes = super.layoutAttributesForElementsInRect(rect)
return attributes
}
}
CollectionVC.swift
private let reuseIdentifier = "cell"
class CollectionVC: UICollectionViewController {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
override func viewDidLoad() {
super.viewDidLoad()
//Register cell class
self.collectionView!.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
}
////////////////////////////////
//UICollectionViewDataSource
////////////////////////////////
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of items
return appDelegate.chosenImageArray.count
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
// Configure the cell
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath)
cell.backgroundColor = UIColor.clearColor()
cell.layer.borderColor = UIColor.whiteColor().CGColor
cell.layer.borderWidth = 1
cell.layer.cornerRadius = 8
let bgView = appDelegate.chosenImageArray.objectAtIndex(indexPath.row) as? UIView
if bgView != nil {
cell.backgroundView = bgView
cell.hidden = false
cell.backgroundView?.hidden = false
}
return cell
}
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return appDelegate.deviceSize
//Tried with different sizes CGSize(100,100) - (200,200) and 0 on insets, no difference
}
////////////////////////////////
//UICollectionViewDelegate
////////////////////////////////
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath)
//do various stuff with selected cell and segue to next view, no issues here
}
}
Hope someone can tell me it's just a few settings I need to input, as this issue seems random and maybe just a bug with UICollectionView? Anyway, thank you very much in advance for any help.
Yes this is normal every time you scroll the collection view cell and it becomes visible on screen again the delegates get called.
It is getting re used.
In cellForItemAtIndexPath you have
let bgView = appDelegate.chosenImageArray.objectAtIndex(indexPath.row) as? UIView
this above line.
I guess in appDelegate.chosenImageArray you have all the UIViews.
Possible fix
Do not store the UIView directly on array create a NSObject model class and create required properties in it.
-Try to create a NSObject model class which should have all the relevant properties like UIImage , name etc... as per your requirement.
Store that NSObject class in array. Here NSObject class is your model class which you are going to store in your array.
Now in delegate cellForItemAtIndexPath -
let bgViewModel = appDelegate.yourModelClassArray.objectAtIndex(indexPath.row) as? yourModelClass
cell.backgroundView = bgViewModel.bgView
remove cell.hidden = false & cell.backgroundView?.hidden = false it is not required.

Back button on UICollectionView

I am trying to add a back/ return button to my UICollectionView, I have this code so far to implement the button:
import UIKit
class EmojiPopup: UIView,UICollectionViewDataSource,UICollectionViewDelegate
{
var collocationView : UICollectionView!
var arrImagesList:NSMutableArray!
var blur:UIBlurEffect = UIBlurEffect()
override init(frame: CGRect)
{
super.init(frame: frame)
arrImagesList = NSMutableArray()
self.backgroundColor = UIColor.purpleColor().colorWithAlphaComponent(0.2)
let layout = UICollectionViewFlowLayout()
//header gap
layout.headerReferenceSize = CGSizeMake(20,20)
//collection view item size
layout.itemSize = CGSizeMake(70, 70)
layout.minimumInteritemSpacing = 25
layout.minimumLineSpacing = 25
collocationView = UICollectionView(frame: CGRectMake(50,50,UIScreen.mainScreen().bounds.screenWidth - 100,UIScreen.mainScreen().bounds.screenHeight - 100), collectionViewLayout: layout)
self.addSubview(collocationView)
// Create the blurEffect and apply to view
let blurEffect = UIBlurEffect(style: UIBlurEffectStyle.ExtraLight)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.alpha = 0.7
blurEffectView.frame = self.bounds
self.addSubview(blurEffectView)
collocationView.backgroundColor = UIColor.purpleColor().colorWithAlphaComponent(0.002)
collocationView.dataSource = self
collocationView.delegate = self
collocationView.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: "cellIdentifier")
//hide scrollbar
self.collocationView.showsVerticalScrollIndicator = false
//back button
let btnBack = UIButton(frame:TCRectMake(x:138 ,y:523,width:45,height:45))
btnBack.setImage(UIImage(named:"back"), forState: UIControlState.Normal)
btnBack.addTarget(self, action:"btnBackClick", forControlEvents: UIControlEvents.TouchUpInside)
self.addSubview(btnBack)
//back button func
func btnBackClick()
{
}
let fm = NSFileManager.defaultManager()
let path = NSBundle.mainBundle().resourcePath!
let items = try! fm.contentsOfDirectoryAtPath(path)
for item in items
{
if item.hasSuffix("png") && item.containsString("#") == false && item.containsString("AppIcon") == false && item.containsString("tick_blue") == false && item.containsString("video_camera") == false
{
arrImagesList.addObject(item)
}
}
}
var completeHandler:((String)->())?
func showDetails(viewParent:UIView,doneButtonClick:((String)->())?)
{
completeHandler = doneButtonClick
viewParent.addSubview(self)
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return arrImagesList.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
{
let identifier="ImageCell\(indexPath.section)\(indexPath.row)"
collectionView.registerClass(ImageViewCell.self, forCellWithReuseIdentifier:identifier)
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(identifier, forIndexPath: indexPath) as! ImageViewCell
cell.backgroundColor = UIColor(white:1, alpha:0)
cell.imgView.image = UIImage(named:arrImagesList[indexPath.row] as! String)
cell.imgView.backgroundColor = UIColor.clearColor()
cell.imgView.opaque = false
cell.imgView.contentMode = .ScaleAspectFit
//keeps blur to background
self.bringSubviewToFront(collocationView)
return cell
}
// func collectionView(collectionView: UICollectionView,
// layout collectionViewLayout: UICollectionViewLayout,
// sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
// {
// let width=UIScreen.mainScreen().bounds.size.width-50
// return CGSize(width:width/3, height:width/3)
// }
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath)
{
//let cell=collectionView.cellForItemAtIndexPath(indexPath) as! ImageViewCell
UIView.animateWithDuration(0.3, animations:{
self.collocationView.alpha=0
}, completion: { finished in
if self.completeHandler != nil
{
self.completeHandler!(self.arrImagesList[indexPath.row] as! String)
}
self.removeFromSuperview()
})
}
func showDetails(viewParent:UIView,dictData : [String:String],index:Int,doneButtonClick:(()->())?,cancelBUttonClick:(()->())?)
{
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I want the collection view to be closed if the user presses the back button, but I am not sure what to enter in the back button function. I want the user to be returned back to the main view controller (mapview) if possible
I will assume you are talking about a UICollectionViewController and not about a UICollectionView. A UICollectionViewController has a UICollectionView inside. You can "close" (dismiss) a UICollectionViewController but not a UICollectionView. You can even dismiss a UIViewController that has a UICollectionView inside.
You have two options:
Put you collection view controller (and the main view controller) inside a navigation controller so you can use the default back button already implemented by the navigation controller.
You can present the collection view controller modally from the main view controller. Then you need to add a close button (Not back button) that dismiss the controller (The main view controller will stay "behind" so when you dismiss the UICollectionViewController it will become visible again.
It's a long way. I suggest you read this getting started guide from Apple, there you can figure it out how navigation controllers and what they do. This is something you need to learn when developing with Swift. I suggest you go further and read the whole tutorial. After reading that chapter you should understand the navigation flow of an iOS application and implement the back-button navigation.
If you find any trouble following that tutorial, let me know.