I have a cell with a button on it. When I press the button, I start an animation indicating something is preparing. I do this within the #IBAction function like so: (this is in my custom tableViewCell function).
#IBAction func playNowTapped(_ sender: UIButton) {
let loadingShape = CAShapeLayer()
//Some animating of the shape
}
I define this shape within the #IBAction as the process should repeat if I press the button again
However, because only the necessary cells displayed on the device are loaded in one chunk in the cellForRowAt function for the tableView, my animation repeats every few cells if I scroll down while the animation is loading.
What I have done so far is append to a list of all the previously pressed buttons in my by defining a function and calling it in the #IBAction function of the button like so:
func findCell() {
//Iterate tableView list and compare to current cellText
for value in list {
if value == cellText.text {
//If found, checking if value is already stored in pressedBefore
for selected in pressedBefore {
if selected == value { return }
}
alreadyPlay.append(song: cellText.text!)
}
}
}
Then, in my cellForRowAt function, I simply make a reverse operation, which checks if the current index in the list is the same as any value in the already selected ones.
Having all of these filtered out, I now only have a list of the non-selected ones.However, I do not know what to do now.
Weirdly, cell.bringSubview(tofront: cell.cellText)
cell.bringSubview(tofront: cell.buttonText) does NOT change the order of the subviews. What would do I do now? Is it possible that CAShapeLayer() is not regarded as a subview, but only a layer?
Thank you in advance!
Weirdly, cell.bringSubview(tofront: cell.cellText)
cell.bringSubview(tofront: cell.buttonText) does NOT change the order of the subviews. What would do I do now?
bringSubview(tofront:) only works with direct subviews. Traditionally your cellText and buttonText are subviews to cell.contentView.
so try
cell.contentView.bringSubview(tofront: cell.buttonText)
Is it possible that CAShapeLayer() is not regarded as a subview, but
only a layer?
Yes, CAShapeLayer inherits from CALayer and only regarded as a layer to its view and will likely need to be updated via layoutSubviews() or draw()
Saw those nested for loops and if statements, thought I'd offer a way to clean that up a bit.
func findCell() {
//find list elements that match cell's text and ensure it hasn't been pressed before
list.filter { $0 == cellText.text && !pressedBefore.contains($0) }.forEach {
alreadyPlay.append(alreadyPlayed(song: LeLabelOne.text!, artist: leLabelThree.text!))
}
}
// alternative using Sets
func findCell() {
let cellTextSet = Set(list).intersection([cellText.text])
// find entries in cellTextSet that haven't been pressed before
cellTextSet.subtract(Set(pressedBefore)).forEach {
alreadyPlay.append(alreadyPlayed(song: LeLabelOne.text!, artist: leLabelThree.text!))
}
}
Related
To set the stage, I have a 4x5 grid of UIImageViews that I would like to flip and show a new image upon tap. I'd like to accomplish this with a single #IBaction. I'm simply having a bit of trouble referencing the selected UIIMageView when a tap is recognized. I'm sure it's something very simple. I'm just now starting to work with UITapGestureRecognizer, so I don't know all the ins and outs just yet. Here's the #IBAction I'm trying to use:
#IBAction func tileTapped(recognizer: UITapGestureRecognizer, _ sender: UIImageView) {
print("Tile Tapped: \(sender.tag)")
}
My print statement is giving me the following no matter what UIImageView is tapped:
Tile Tapped: 0
My tags are set up to reference the row and column they fall in. For example, the tags for my first row are:
00, 01, 02, 03
My biggest challenge is simply retrieving the tag for the appropriate UIIMageView. Once I figure that out, I should be solid.
Thanks in advance!
That is not a valid function for a gesture recognizer. See the documentation for UIGestureRecognizer on the possible signatures.
To access the view associated with the gesture, access the gesture's view property.
#IBAction func tileTapped(_ recognizer: UITapGestureRecognizer) {
if let view = recognizer.view as? UIImageView {
print("Tile Tapped: \(view.tag)")
}
}
I have messages screen and implement custom tableviewcell for the display message. A message should be text or image and some case I need to display boxes with information(see image sender and receiver). it's working fine but some time messages view cut off(see image messages). I have used many stackViews to hiding and show some views.
Please find the code here for more understanding.
The possible cause for such a behaviour is setting up the layers of the view in cell, I can see in your cell, you are adding the corner radius to the background. I could be able to fix it in my app by using the following approach.
Define a optional data varibale in your cell.
var currentData: MessageModel?
set that value in the method you are calling to provide the data to cell.
func loadData(_ data:MessageModel) -> Void {
currentData = data
// YOUR EXISTING CODE GOES HERE.
// Move your code to the function which do the setup of corner radius.
// Call this method.
setupCornerRadius()
}
Add the following methods to your cells
open override func layoutIfNeeded() {
super.layoutIfNeeded()
setupCornerRadius()
}
open override func layoutSubviews() {
super.layoutSubviews()
setupCornerRadius()
}
func setupCornerRadius() {
if let data = currentData {
let strMsg = data.body ?? ""
lblMsgBody.text = strMsg
if strMsg != "" {
viewBG.backgroundColor = UIColor.primaryGreen
if strMsg.count > 5 {
viewBG.layer.cornerRadius = 18.0
}else{
viewBG.layer.cornerRadius = 12.0
}
}
else{
viewBG.backgroundColor = UIColor.clear
}
}
}
Try the above approach.
Along with that what I did was, I removed the stackView from the cell and managed to implement the required UI by setting up the constraints.
Label with numberOfLines = 0 and setting Leading, Trailing, Bottom and Top constraints. with value = 8 (You can set it as per your margin and spacing you needs.)
Try and share the results.
Use UI debugger and see what exactly is going on.
I created a segmented control in my storyboard that looks like this:
Then I created an IBAction for when the control's value changes:
#IBAction func segmentValChanged(_ sender: UISegmentedControl) {
print("touched")
if (sender.selectedSegmentIndex == 0) {
sender.selectedSegmentIndex = 1;
}
else{
sender.selectedSegmentIndex = 0;
}
}
The only issue is that this function is never called!
When I click the Jokes (Beta) button, nothing happens.
However, when I created an IBOutlet for the control and tried to change the selected segment in my viewDidLoad(), it worked:
segmentedController.selectedSegmentIndex = 1
I know I probably missed something obvious, since I've never used a segmented control before.
Thanks so much if anyone can point out what this is.
Cheers!
import UIKit
import Alamofire
class ViewController: UIViewController {
var currentImage: CurrentImage!
var parameters: Parameters!
#IBOutlet weak var segmentedController: UISegmentedControl!
#IBAction func segmentValChanged(_ sender: UISegmentedControl) {
print("touched")
if (sender.selectedSegmentIndex == 0) { // switch to 1
sender.selectedSegmentIndex = 1;
}
else{
sender.selectedSegmentIndex = 0;
}
}
override func viewDidLoad() {
super.viewDidLoad()
segmentedController.selectedSegmentIndex = 1
self.segmentedController.addTarget(self, action: #selector(segmentValChanged(_:)), for: .valueChanged)
}
}
And this is what it looks like when I right click the view:
Whenever I see this it is almost always the same problem... If the segmented control is being drawn outside of its parent view, then it will not respond to taps. This is most likely the problem you are having. (This can also happen with UIButtons.)
Ways to test if this is the problem:
set clipsToBounds of the segmented control's parent to true. If the control no longer shows on the screen, then it is being drawn outside of it's parent view. segmentedControl.superview?.clipsToBounds = true
add a border to the segmented control's parent. If the control is being drawn outside the border then there you go. segmentedControl.superview?.layer.borderWidth = 1
Solve the problem by expanding the size of the parent view so the control is being drawn within it.
On a side note, your IBAction is incorrect. When the value changed action is called, the segmented control will already have changed its value and the new segment will be highlighted. You don't have to do it manually. This is another indicator that your control isn't being drawn in the bounds of its parent view.
I have a method which generates several buttons, all with the same properties. The buttons are arranged vertically, like that:
What I need is that when each of them is clicked, it is capable of recognizing if it is the first one from the top, or the second one etc., printing "I'am the first/second/etc one".
How can I do it considering that the method they have is the same?
If all you need is an integer, use the tag property in UIView.
If you have an array of buttons…
var buttons: [UIButton]
you could sort them by their position in their superview to determine their order:
var sortedButtons: [UIButton] {
return buttons.sorted(by: { $0.frame.origin.y < $1.frame.origin.y })
}
and then get the index of a button (and vice-versa):
func index(of button: UIButton) -> Int {
return sortedButtons.index(of: button)
}
func button(at index: Int) -> UIButton? {
guard index < buttons.count else { return nil }
return sortedButtons[index]
}
Then…
#IBAction func buttonClicked(_ sender: UIButton) {
guard let index = index(of: sender) else { return }
sender.setTitle("I am \(index)", for: .normal)
}
This would also work if you added your buttons to an Outlet Collection in a storyboard:
#IBoutlet var buttons: [UIButton]!
Quick and dirty approach:
When dragging the outlet for the button, for the connection type select "Outlet Collection". This will create an outlet "to" a button array. Then you can drag and drop the rest of your buttons to this array. The order you make the drag n drop will be the order of your buttons in the array.
Then you can easily access them by index.
Caution: Easily breakable if re-drag n dropping the collection.
How to redraw non-visible UICollectionViewCell's ready for when reuse occurs???
One approach I thought of was per the code in the Layout Cell prepareForReuse function, however whilst it works it non-optimal as it causes more re-drawing then required.
Background: Need to trigger drawRect for cells after an orientation change that are not current visible, but pop up to be used and haven't been redraw, so so far I can only see that prepareForReuse would be appropriate. Issue is I'm re-drawing all "reuse" cells, whereas I really only want to redraw those that initially pop up that were created during the previous orientation position of the device.
ADDITIONAL INFO: So currently I'm doing this:
In ViewController:
override func viewWillLayoutSubviews() {
// Clear cached layout attributes (to ensure new positions are calculated)
(self.cal.collectionViewLayout as! GCCalendarLayout).resetCache()
self.cal.collectionViewLayout.invalidateLayout()
// Trigger cells to redraw themselves (to get new widths etc)
for cell in self.cal?.visibleCells() as! [GCCalendarCell] {
cell.setNeedsDisplay()
}
// Not sure how to "setNeedsDisplay" on non visible cells here?
}
In Layout Cell class:
override func prepareForReuse() {
super.prepareForReuse()
// Ensure "drawRect" is called (only way I could see to handle change in orientation
self.setNeedsDisplay()
// ISSUE: It does this also for subsequent "prepareForReuse" after all
// non-visible cells have been re-used and re-drawn, so really
// not optimal
}
Example of what happens without the code in prepareForReuse above. Snapshot taken after an orientation change, and just after scrolling up a little bit:
I think I have it now here:
import UIKit
#IBDesignable class GCCalendarCell: UICollectionViewCell {
var prevBounds : CGRect?
override func layoutSubviews() {
if let prevBounds = prevBounds {
if !( (prevBounds.width == bounds.width) && (prevBounds.height == bounds.height) ) {
self.setNeedsDisplay()
}
}
}
override func drawRect(rect: CGRect) {
// Do Stuff
self.prevBounds = self.bounds
}
}
Noted this check didn't work in "prepareForReuse" as at this time the cell had not had the rotation applied. Seems to work in "layoutSubviews" however.
You can implement some kind of communication between the cells and the view controller holding the collection view ( protocol and delegate or passed block or even direct reference to the VC ). Then You can ask the view controller for rotation changes.
Its a bit messy, but if You have some kind of rotation tracking in Your view controller You can filter the setNeedsDisplay with a simple if statement.
I had similar challenged updating cells that were already displayed and off the screen. While cycling through ALLL cells may not be possible - refreshing / looping through non-visible ones is.
IF this is your use case - then read on. Pre - Warning - if you're adding this sort of code - explain why you're doing it. It's kind of anti pattern - but can help fix that bug and help ship your app albeit adding needless complexity. Don't use this in multiple spots in app.
Any collectionviewcell that's de-initialized (off the screen and being recylced) should be unsubscribed automatically.
Notification Pattern
let kUpdateButtonBarCell = NSNotification.Name("kUpdateButtonBarCell")
class Notificator {
static func fireNotification(notificationName: NSNotification.Name) {
NotificationCenter.default.post(name: notificationName, object: nil)
}
}
extension UICollectionViewCell{
func listenForBackgroundChanges(){
NotificationCenter.default.removeObserver(self, name: kUpdateButtonBarCell, object: nil)
NotificationCenter.default.addObserver(forName:kUpdateButtonBarCell, object: nil, queue: OperationQueue.main, using: { (note) in
print( " contentView: ",self.contentView)
})
}
}
override func collectionView(collectionView: UICollectionView!, cellForItemAtIndexPath indexPath: NSIndexPath!) -> UICollectionViewCell! {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("die", forIndexPath: indexPath) as UICollectionViewCell
cell.listenForBackgroundChanges()
return cell
}
// Where appropriate broadcast notification to hook into all cells past and present
Notificator.fireNotification(notificationName: kUpdateButtonBarCell)
Delegate Pattern
It's possible to simplify this.... an exercise for the reader. just do not retain the cells (use a weak link) - otherwise you'll have memory leaks.