Blinking table view cell - swift

After adding a new set of data to a table view, i'd like to have the cell that displays the new data blink green for 5 seconds. I used an extension to UIView. I call the extension within "cellForRowAtIndexPath" but it turns the cell green permanently and it never turns back to default background color again. Heres the extension i am using:
extension UIView {
func blink(duration: TimeInterval = 0.5, delay: TimeInterval = 0.0, alpha: CGFloat = 0.0) {
UIView.animate(withDuration: duration, delay: delay, options: [.curveEaseInOut, .repeat, .autoreverse], animations: {
self.backgroundColor = .systemGreen
})
}
}
self.blink() is called within "cellForRowAtIndexPath"
I have no idea why the animation isnt working, does anyone have a solution?

Heres a working answer i now managed to find for my problem.
First the blinking extension:
Using .repeat .autoreverse i wasnt able to stop the animation at the exact time that it turned back to default color which didnt look nice. So I decided to write the animation myself and let the cell blink 3 times this way:
extension UIView {
func blink(duration: TimeInterval = 1.4, repetitions: Int = 3) {
var remainingReps = repetitions
UIView.animate(withDuration: duration, animations: {
self.backgroundColor = Colors.primaryAlpha
}) { (error) in
UIView.animate(withDuration: duration, animations: {
self.backgroundColor = .systemBackground
}) { (error) in
remainingReps -= 1
if remainingReps > 0 {
self.blink(duration: duration, repetitions: remainingReps)
}
}
}
}
}
You can choose yourself how many times you want the cell to blink by its "repetitions" parameter.
Now in your tableview add following code for tableView willDisplay cell:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let dataSet = myData[indexPath.row]
let myCell = cell as! YourCellType
myCell.checkIfShouldBlink(data: dataSet)
}
}
Finally within your TableViewCell Class:
func checkIfShouldBlink(data: MyDataObject){
//Here you can implement any kind of logic to have the cell blink depending
//on your dataSet. In my case for exmaple, if the timestamp of the data is
//less than 5 secs old (new Data) i have the cell blink.
if data.whatEverLogicYouWantToPutHere{
self.blink()
}
}

Related

(Automatic) reloadData on Tableview(tabbar2) once Array is updated in tabbar1 (without needing to change tabbars)

tabbar1 - I'm using core-bluetooth to connect to some peripherals. Once it connects, I update an array.
func populateFoundBTArray(peripheral: CBPeripheral, service:CBService) {
discoveredBTDevices.append(BTPeripheral.init(
id: discoveredBTDevices.count,
name: peripheral.name!,
uuid: peripheral.identifier,
desc: service.uuid.description
connected: peripheral.state == .connected ? true : false ))
}
in TabBar2 - I have some prototype (custom) cells hooked up via a XIB file via a combo of Static Cells and dynamic cells (similar to this solution - https://stackoverflow.com/a/49157374/14414215)
within TabBar2 - I have the tableView.reloadData() call which is great whereby, whenever I switch to this tabbar2, the tableview will get updated. (but I have to physically go to tabbar1 --> tabbar2 to trigger this update)
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
tableView.reloadData()
}
my goal Is would like to be able to get the tableview to reload whenever the array discoveredBTDevices gets updated.
I did try
let pop = tabbar2()
pop.tableView.reloadData() // also tried reloadSections(1 but didn't work
along with some more answers I found on SO but none of them worked. (I did not try any segues / notifications as currently I just have the discoveredBTDevices as a global variable to simplify my initial test)
I am posting my own answer as to how I solved this issue of mine. It is not a full solution but rather a workaround but I believe that it is suitable.
From the original post, the goal is to get the tableviewcell to refresh once the array discoveredBTDevices gets updated. To Recap, currently the table will only refresh once the user switch to tabbar1 and then back to tabbar2 (due to the reloadData() called in viewDidAppear)
After searching and reading through 40+ SO pages on UITableview and not finding an answer, I decided to utilise a UIButton to do manual table reload.
// https://stackoverflow.com/a/53032365/14414215
//
// https://stackoverflow.com/a/44735567/14414215
//
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == deviceSection { // this is my paired device section of the static table
let headerView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: 30.0))
let sectionHeader = UILabel(frame: CGRect(x: 8, y: 0, width: 200, height: 30.0))
sectionHeader.text = "PAIRED BLUETOOTH DEVICES"
sectionHeader.font = UIFont.systemFont(ofSize: 12)
sectionHeader.textColor = UIColor.systemGray
let reloadButton = UIButton(frame: CGRect(x: tableView.frame.width-150, y: 0, width: 150, height: 30.0))
reloadButton.setTitle("Reload", for: .normal)
reloadButton.addTarget(self, action: #selector(ReloadButtonTapped), for: .touchUpInside)
reloadButton.semanticContentAttribute = UIApplication.shared
.userInterfaceLayoutDirection == .rightToLeft ? .forceLeftToRight : .forceRightToLeft
headerView.addSubview(reloadButton)
headerView.addSubview(sectionHeader)
return headerView
}
return super.tableView(tableView, viewForHeaderInSection: section)
}
#objc func ReloadButtonTapped(_ sender : Any) {
tableView.reloadData()
}
I have linked 2 SO pages which shows the implementation and merged it slightly as using viewForHeaderInSection will remove all stylings from using Apple's native header hence the addition of the sectionHeader

Animating UIView always goes to top left, even when the center is somewhere else

I am trying to animate a UIView shrinking and moving to a different center point. It starts when the user taps a certain UICollectionViewCell, a new UIView is made, centred on the cell, and starts expanding until it fills the entire screen. This works fine. I store the original center point of the cell in the new UIView (custom class with property originCenter).
let expandingCellView = SlideOverView(frame: cell.bounds)
expandingCellView.center = collectionView.convert(cell.center, to: self)
expandingCellView.textLabel.text = cell.textLabel.text
expandingCellView.originWidth = cell.bounds.width
expandingCellView.originHeight = cell.bounds.height
expandingCellView.originCenter = self.convert(expandingCellView.center, to: self)
expandingCellView.originView = self
self.addSubview(expandingCellView)
expandingCellView.widthConstraint.constant = self.frame.width
expandingCellView.heightConstraint.constant = self.frame.height
UIView.animate(withDuration: 0.3, animations: {
expandingCellView.center = self.center
expandingCellView.layoutIfNeeded()
})
This code works perfectly fine. Now I have added a button to that expanded view, which executes the following code:
widthConstraint.constant = originWidth
heightConstraint.constant = originHeight
print(self.convert(self.originCenter, to: nil))
UIView.animate(withDuration: 0.5, animations: {
self.center = self.convert(self.originCenter, to: nil)
self.layoutIfNeeded()
}, completion: { (done) in
// self.removeFromSuperview()
})
The shrinking of the view to its original size works fine. The only thing that doesn't work is the centering. I take the original cell centerpoint, and convert it to this views coordinate system. This produces coordinates which I think are correct. However all the views just move to the top left of the screen.
Below is a link to a screen recording. The first UIView prints its new centerpoint as (208.75, 411.75) and the second UIView I open prints its center as (567.25, 411.75). These values seem correct to me, however they don't move to this point, as you can see in the video. Any way I can fix this?
Even when setting the new center to for instance CGPoint(x: 500, y: 500), the view still moves to x = 149.5 and y = 149.5
Video: https://streamable.com/pxemc
Create a var that has a weak reference to the cell
weak var selectedCell: UICollectionViewCell!
assign the selected cell in CollectionView delegate didSelectItemAt call
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedCell = collectionView.cellForItemAtIndexPath(indexPath)
}
after add expandingCellView as subview do this to make it go from cell size to full screen
// make expanding view the same size as cell
expandingCellView.frame = selectedCell.frame
// animate
UIView.animate(withDuration: 0.5, animations: {
self.expandingCellView.layoutIfNeeded()
self.expandingCellView.frame = self.view.frame
}, completion: { (_) in
self.expandingCellView.layoutIfNeeded()
})
just reverse it to make it small again like the cell size
// animate
UIView.animate(withDuration: 0.5, animations: {
self.expandingCellView.layoutIfNeeded()
self.expandingCellView.frame = self.selectedCell.frame
}, completion: { (_) in
self.expandingCellView.layoutIfNeeded()
})
frame use global position.
frame vs bounds

UISlider not updating values

apologies if this is a stupid question. I can't seem to get my slider to update its value as its being interacted with. (I'm going to point everyone to the very last method in this long code)
class CustomSlider: UISlider {
override func trackRect(forBounds bounds: CGRect) -> CGRect {
var rect = super.trackRect(forBounds: bounds)
rect.size.height = 7
return rect
}
}
class FactionButton: CustomSlider {
var factionSlider = CustomSlider(frame: CGRect(x: 15, y: 542, width: 386, height: 57))
func factionBalanceSlider(){
factionSlider.minimumValueImage = #imageLiteral(resourceName: "Alliance Slider")
factionSlider.maximumValueImage = #imageLiteral(resourceName: "Horde Slider")
factionSlider.setThumbImage(#imageLiteral(resourceName: "Thumb Image"), for: .normal)
factionSlider.minimumTrackTintColor = UIColor(red:0.08, green:0.33, blue:0.69, alpha:0.8)
factionSlider.maximumTrackTintColor = UIColor(red:1.00, green:0.00, blue:0.00, alpha:0.59)
factionSlider.setValue(0.5, animated: true)
factionSlider.isContinuous = false
factionSlider.addTarget(self, action: #selector(recordFactionBalance(sender:)), for: .valueChanged)
}
func getSlider() -> CustomSlider {
return factionSlider
}
override func trackRect(forBounds bounds: CGRect) -> CGRect {
var customBounds = super.trackRect(forBounds: bounds)
customBounds.size.height = 10
return customBounds
}
#objc func recordFactionBalance(sender: CustomSlider){
//also calculates balance and adds it into the quiz data
print("hi")
print(sender.value) //It's this part that doesn't work
}
}
It's this bit nearest to the bottom that has the issue. (Everything else is fine) The action function doesn't seem to be triggered at all, even when I'm interacting with it. Neither print statements are being executed. Any ideas why?
Cheers
From the getSlider(), i can guess you are using this class as a utility to get the CustomSlider. So, i suspect you are adding the slider to the view as below,
let container = FactionButton()
container.factionBalanceSlider()
let slider = container.getSlider()
self.view.addSubview(slider)
If you will not add the container to the view which is set as the receiver for .valueChange event so it will not get any event. To receive events you also need to add the container in the view as below,
self.view.addSubview(container)

In auto scrolling UICollectionView cellForItemAtIndexPath not triggered by contentOffset

I am attempting to create an auto scrolling UICollectionView (each cell has an image and a string). The scrolling works fine using contentOffset but since cellForItem is never triggered new/non-visible cells never load. I do not want to use scrollToItem...contentOffset allows for a slow scrolling effect. I also can't use anything that requires a duration because I want this to run until the view is changed by the user. Here is the code I'm using:
func configAutoScrollTimer() {
signInTimer = Timer.scheduledTimer(timeInterval: 0.03, target: self, selector: #selector(autoScrollView), userInfo: nil, repeats: true)
}
func deconfigAutoScrollTimer() {
signInTimer.invalidate()
scrollX = 0
}
#objc func autoScrollView() {
scrollX += 1
let offsetPoint = CGPoint(x: scrollX, y: 0)
collectionView.contentOffset = offsetPoint
collectionView.layoutIfNeeded()
}
configureAutoSrollTimer() is called when the view is loaded. Any ideas on how to get the non-visible cells to load?
Hook the width constraint of the collectionView as IBOutlet
and do this in viewDidLayoutSubviews
self.collectionViewWithCon.constant = numOfCells*cellWidth
Edit:
#objc func autoScrollView() {
self.collectionViewLeadCon.constant -= 1.0
self.collectionViewWithCon.constant += cellWidth
collectionView.layoutIfNeeded()
}

Keep active cell in collectionview in one place

I'm working on a tvOS application where I want the active cell of a collectionView always in the same position. In other words, I don't want the active cell to go through my collectionView but I want the collectionView to go through my active cell.
Example in the following image. The active label is always in the center of the screen while you can scroll the collectionView to get a different active label over there.
Basically a pickerview with a custom UI. But the pickerview of iOS is unfortunately not provided on tvOS...
I've already looked into UICollectionViewScrollPosition.verticallyCentered. That partially gets me there but it isn't enough. If I scroll really fast, the active item jumps further down the list and when I pause it scrolls up all the way to the center. I really want the active item to be in center of the screen at all times.
Any ideas on how to do this?
Update based on #HMHero answer
Okay, I tried to do what you told me, but can't get the scrolling to work properly. This is perhaps due to my calculation of the offset, or because (like you said) setContentOffset(, animated: ) doesn't work.
Right now I did the following and not sure where to go from here;
Disable scrolling and center last and first label
override func viewDidLoad() {
super.viewDidLoad()
//Disable scrolling
self.collectionView.isScrollEnabled = false
//Place the first and last label in the middle of the screen
self.countryCollectionView.contentInset.top = collectionView.frame.height * 0.5 - 45
self.countryCollectionView.contentInset.bottom = collectionView.frame.height * 0.5 - 45
}
Getting the position of a label to retrieve the distance from the center of the screen (offset)
func collectionView(_ collectionView: UICollectionView, didUpdateFocusIn context: UICollectionViewFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if let indexPath = context.nextFocusedIndexPath,
let cell = countryCollectionView.cellForItem(at: indexPath) {
//Get the center of the next focused cell, and convert that to position in the visible part of the (tv) screen itself
let cellCenter = countryCollectionView.convert(cell.center, to: countryCollectionView.superview)
//Get the center of the collectionView
let centerView = countryCollectionView.frame.midY
//Calculate how far the next focused cell y position is from the centerview. (Offset)
let offset = cellCenter.y - centerView
}
}
The offset returns incrementals of 100 when printing. The labels' height is 90, and there is a spacing of 10. So I thought that would be correct although it runs through all the way up to 2400 (last label).
Any ideas on where to go from here?
It's been more than a year since I worked on tvOS but as I remember it should be fairly simple with a simple trick. There might be a better way to do this with updated api but this is what I did.
1) Disable to scroll on collection view isScrollEnabled = false
2) In delegate, optional public func collectionView(_ collectionView: UICollectionView, didUpdateFocusIn context: UICollectionViewFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator), get nextFocusedIndexPath and calculate the offset of the cell to be at the center of the collection view.
3) With the offset, animate the scroll manually in the delegate callback.
I found the old post where I got an idea.
How to center UICollectionViewCell that was focused?
I ended up doing somewhat different from the answer in the thread but it was a good hint.
Okay, along with some pointers of #HMHero I've achieved the desired effect. It involved animating the contentOffset with a custom animation. This is my code;
func collectionView(_ collectionView: UICollectionView, didUpdateFocusIn context: UICollectionViewFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if let previousIndexPath = context.previouslyFocusedIndexPath,
let cell = countryCollectionView.cellForItem(at: previousIndexPath) {
UIView.animate(withDuration: 0.3, delay: 0, options: UIViewAnimationOptions.curveEaseOut, animations: {
cell.contentView.alpha = 0.3
cell.contentView.transform = CGAffineTransform(scaleX: 0.7, y: 0.7)
})
}
if let indexPath = context.nextFocusedIndexPath,
let cell = countryCollectionView.cellForItem(at: indexPath) {
let cellCenter = CGPoint(x: cell.bounds.origin.x + cell.bounds.size.width / 2, y: cell.bounds.origin.y + cell.bounds.size.height / 2)
let cellLocation = cell.convert(cellCenter, to: self.countryCollectionView)
let centerView = countryCollectionView.frame.midY
let contentYOffset = cellLocation.y - centerView
UIView.animate(withDuration: 0.3, delay: 0, options: UIViewAnimationOptions.curveEaseOut, animations: {
collectionView.contentOffset.y = contentYOffset
cell.contentView.alpha = 1.0
cell.contentView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
self.countryCollectionView.layoutIfNeeded()
})
}
}