Allow user interaction with UIButton during system-provided animation of UILabel - ios

EDIT: Gif added for clarity
I have a UITableViewCell. The cell contains two sibling elements—a button and a label. When you click the button, new text is loaded into the label.
If the new text is short, and fits on one line (as in the top cell,) all goes well—you can tap that button pretty much as fast as you want.
But if the new text is long, and has to fill two lines or more (as in the bottom cell,) UIKit automatically animates the new text in, sliding it down from the top line. While this automatic animation is happening, UIKit also disables the button so you can't interact with it.
If you try to rapidly click through a few entries in this scenario, it just feels wrong, as the button won't react to every press.
So my question is:
Is there any way to enable interaction with the button during this animation? I can turn it off completely using UIView.setAnimationsEnabled(false), but then it doesn't look as nice.
EDIT: Since it was requested, here's the code that updates the label. The text gets updated in didSet whenever the NSManagedObject model changes.
extension MasterDataSource {
private func configure(cell: UITableViewCell, for indexPath: IndexPath) {
guard let cell = cell as? MasterCell else {
fatalError("Cell Not Returned")
}
let category = categories.object(at: indexPath)
guard let exercises = category.exercises else {
return
}
guard exercises.count > 0 else {
return
}
guard let activeExercise = category.activeExercise else {
return
}
cell.model = MasterCell.Model(with: activeExercise)
}
}
class MasterCell: UITableViewCell {
#IBOutlet private weak var exerciseLabel: UILabel!
#IBOutlet private weak var nextButton: UIButton!
struct Model {
let exercise: Exercise
init(with exercise: Exercise) {
self.exercise = exercise
}
}
var model: Model? {
didSet {
guard let model = model,
let exerciseTitle = model.exercise.title else {
return
}
exerciseLabel.text = exerciseTitle
}
}
}

Related

Ios images and labels keep loading while scrolling

I'm writing a demo to show user's tweets.
The question is:
Every time I scroll to the bottom and then scroll back, the tweet's images and comments are reloaded, even the style became mess up. I know it something do with dequeue, I set Images(which is an array of UIImageView) to [] every time after dequeue, but it is not working. I'm confused and couldn't quite sleep....
Here is core code of my TableCell(property and Images set), which provide layout:
class WechatMomentListCell: UITableViewCell{
static let identifier = "WechatMomentListCell"
var content = UILabel()
var senderAvatar = UIImageView()
var senderNick = UILabel()
var images = [UIImageView()]
var comments = [UILabel()]
override func layoutSubviews() {
//there is part of Image set and comments
if images.count != 0 {
switch images.count{
case 1:
contentView.addSubview(images[0])
images[0].snp.makeConstraints{ (make) in
make.leading.equalTo(senderNick.snp.leading)
make.top.equalTo(content.snp.bottom)
make.width.equalTo(180)
make.height.equalTo(180)
}
default:
for index in 0...images.count-1 {
contentView.addSubview(images[index])
images[index].snp.makeConstraints{ (make) in
make.leading.equalTo(senderNick.snp.leading).inset(((index-1)%3)*109)
make.top.equalTo(content.snp.bottom).offset(((index-1)/3)*109)
make.width.equalTo(90)
make.height.equalTo(90)
}
}
}
}
if comments.count != 0, comments.count != 1 {
for index in 1...comments.count-1 {
comments[index].backgroundColor = UIColor.gray
contentView.addSubview(comments[index])
comments[index].snp.makeConstraints{(make) in
make.leading.equalTo(senderNick)
make.bottom.equalToSuperview().inset(index*20)
make.width.equalTo(318)
make.height.equalTo(20)
}
}
}
}
Here is my ViewController, which provide datasource:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let tweetCell = tableView.dequeueReusableCell(withIdentifier: WechatMomentListCell.identifier, for: indexPath) as? WechatMomentListCell else {
fatalError("there is no WechatMomentList")
}
let tweet = viewModel.tweetList?[indexPath.row]
for i in tweet?.images ?? [] {
let flagImage = UIImageView()
flagImage.sd_setImage(with: URL(string: i.url))
tweetCell.images.append(flagImage)
}
for i in tweet?.comments ?? [] {
let flagComment = UILabel()
flagComment.text = "\(i.sender.nick) : \(i.content)"
tweetCell.comments.append(flagComment)
}
return tweetCell
}
The Images GET request has been define at ViewModel using Alamofire.
The firsttime is correct. However, If I scroll the screen, the comments will load again and images were mess up like this.
I found the problem in your tableview cell. in cell you have two variables like this.
var images = [UIImageView()]
var comments = [UILabel()]
Every time you using this cell images and comments are getting appended. make sure you reset these arrays every time you use this cell. like setting theme empty at initialization.

Pictures getting Mixed when scrolling in UITable View Swift

I have a UITable View in my program with dequeueReusableCells
I should load several images from server and show them in slide show
I have a custom cell and in configuring each cell I download the images in DispatchQueue.global(qos: .userInitiated).async and in DispatchQueue.main.async I add the downloaded pic to the slide show images
but when I start scrolling some of the cells that shouldn't have any pictures , have the repeated pics of another cell
Do you have any idea what has caused this ?!
I'm using swift and also ImageSlideShow pod for the slide show in each cell
Here is some parts of my code :
In my news cell class I have below part for getting images:
class NewsCell: UITableViewCell{
#IBOutlet weak var Images: ImageSlideshow!
#IBOutlet weak var SubjectLbl: UILabel!
#IBOutlet weak var NewsBodyLbl: UILabel!
func configureCell(news: OneNews) {
self.SubjectLbl.text = news.Subject
self.NewsBodyLbl.text = news.Content
if news.ImagesId.count==0{
self.Images.setImageInputs([ImageSource(image: UIImage(named: "ImagePlaceholderIcon")!)])
}
else{
for imgId in news.ImagesId {
let Url = URL(string: "\(BASE_URL)\(NEWS_PATH)/\(imgId)/pictures")
DispatchQueue.global(qos: .userInitiated).async {
let data = try? Data(contentsOf: Url!)
DispatchQueue.main.async {
if let d = data {
let img = UIImage(data: data!)!
imageSrc.append(ImageSource(image: img))
self.Images.setImageInputs(imageSrc);
}
}
}
}
}
self.Images.slideshowInterval = 3
}
And this is cellForRow method:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = generalNewsTableView.dequeueReusableCell(withIdentifier: "NewsCell" , for: indexPath) as! NewsCell
if let news = NewsInfo.sharedInstance.getGeneralNews(){
cell.configureCell(news: news[indexPath.row])
}
return cell
}
getGeneralNews() is a getter that returns an array of news
so what I'm doing in cellForRowAt is that I get the news in the given index path and configure my cell with it .
class NewsInfo {
static var sharedInstance = NewsInfo()
private init(){}
private (set) var generalNews:[OneNews]!{
didSet{
NotificationCenter.default.post(name:
NSNotification.Name(rawValue: "GeneralNewsIsSet"), object: nil)
}
}
func setGeneralNews(allGeneralNews:[OneNews]){
self.generalNews = allGeneralNews
}
func getGeneralNews() -> [OneNews]!{
return self.generalNews
}
}
Each news contains an array of the picture Ids
These are the fields in my OneNews class
var Subject :String!
var Content:String!
var ImagesId:[Int]!
Thanks !
UITableViewCell are reused as you scroll. When a cell goes off the top of the screen, it will be reused for another row appearing at the bottom of the screen.
UITableViewCell has a method prepareForReuse you can override. You can use that method to clear out iamgeViews or any other state that should be reset or cancel downloading of images.
In your case, you probably shouldn't use Data(contentsOf:) since it doesn't give you a way to cancel it. URLSessionDataTask would be a better option since it lets you cancel the request before it finishes.
You can try something like this. The main idea of this code is giving a unique number to check if the cell is reused.
I have renamed many properties in your code, as Capitalized identifiers for non-types make the code hard to read. You cannot just replace whole definition of your original NewsCell.
There was no declaration for imageSrc in the original definition. I assumed it was a local variable. If it was a global variable, it might lead other problems and you should avoid.
(Important lines marked with ###.)
class NewsCell: UITableViewCell {
#IBOutlet weak var images: ImageSlideshow!
#IBOutlet weak var subjectLbl: UILabel!
#IBOutlet weak var newsBodyLbl: UILabel!
//### An instance property, which holds a unique value for each cellForRowAt call
var uniqueNum: UInt32 = 0
func configureCell(news: OneNews) {
self.subjectLbl.text = news.subject
self.newsBodyLbl.text = news.content
let refNum = arc4random() //### The output from `arc4random()` is very probably unique.
self.uniqueNum = refNum //### Assign a unique number to check if this cell is reused
if news.imagesId.count==0 {
self.images.setImageInputs([ImageSource(image: UIImage(named: "ImagePlaceholderIcon")!)])
} else {
var imageSrc: [ImageSource] = [] //###
for imgId in news.imagesId {
let Url = URL(string: "\(BASE_URL)\(NEWS_PATH)/\(imgId)/pictures")
DispatchQueue.global(qos: .userInitiated).async {
let data = try? Data(contentsOf: Url!)
DispatchQueue.main.async {
//### At this point `self` may be reused, so check its `uniqueNum` is the same as `refNum`
if self.uniqueNum == refNum, let d = data {
let img = UIImage(data: d)!
imageSrc.append(ImageSource(image: img))
self.images.setImageInputs(imageSrc)
}
}
}
}
}
self.images.slideshowInterval = 3
}
}
Please remember, the order of images may be different than the order of imagesId in your OneNews (as described in Duncan C's comment).
Please try.
If you want to give a try with this small code fix, without overriding the prepareForReuse of the cell, just change in configure cell:
if news.ImagesId.count==0{
self.Images.setImageInputs([ImageSource(image: UIImage(named: "ImagePlaceholderIcon")!)])
}
else{
// STUFF
}
in
self.Images.setImageInputs([ImageSource(image: UIImage(named: "ImagePlaceholderIcon")!)])
if news.ImagesId.count > 0{
// STUFF
}
so every cell will start with the placeholderIcon even when reused

Need help setting UISwitch in custom cell (XIB, Swift 4, Xcode 9)

Successes so far: I have a remote data source. Data gets pulled dynamically into a View Controller. The data is used to name a .title and .subtitle on each of the reusable custom cells. Also, each custom cell has a UISwitch, which I have been able to get functional for sending out both a “subscribe” signal for push notifications (for a given group identified by the cell’s title/subtitle) and an “unsubscribe” signal as well.
My one remaining issue: Whenever the user "revisits" the settings VC, while my code is "resetting" the UISwitches, it causes the following warnings in Xcode 9.2:
UISwitch.on must be used from main thread
UISwitch.setOn(_:animated:) must be used from main thread only
-[UISwitch setOn:animated:notifyingVisualElement:] must be used from main thread
The code below "works" -- however the desired result happens rather slowly (the UISwitches that are indeed supposed to be "on" take a good while to finally flip to "on").
More details:
What is needed: Whenever the VC is either shown or "re-shown," I need to "reset" the custom cell’s UISwitch to "on" if the user is subscribed to the given group, and to "off" if the user is not subscribed. Ideally, each time the VC is displayed, something should reach out and touch the OneSignal server and find out that user’s “subscribe state” for each group, using the OneSignal.getTags() function. I have that part working. This code is in the VC. But I need to do it the right way, to suit proper protocols regarding threading.
VC file, “ViewController_13_Settings.swift” holds a Table View with the reusable custom cell.
Table View file is named "CustomTableViewCell.swift"
The custom cell is called "customCell" (I know, my names are all really creative).
The custom cell (designed in XIB) has only three items inside it:
Title – A displayed “friendly name” of a “group” to be subscribed to or unsubscribed from. Set from the remote data source
Subtitle – A hidden “database name” of the aforementioned group. Hidden from the user. Set from the remote data source.
UISwitch - named "switchMinistryGroupList"
How do I properly set the UISwitch programmatically?
Here is the code in ViewController_13_Settings.swift that seems pertinent:
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as! CustomTableViewCell
// set cell's title and subtitle
cell.textLabelMinistryGroupList?.text = MinistryGroupArray[indexPath.row]
cell.textHiddenUserTagName?.text = OneSignalUserTagArray[indexPath.row]
// set the custom cell's UISwitch.
OneSignal.getTags({ tags in
print("tags - \(tags!)")
self.OneSignalUserTags = String(describing: tags)
print("OneSignalUserTags, from within the OneSignal func, = \(self.OneSignalUserTags)")
if self.OneSignalUserTags.range(of: cell.textHiddenUserTagName.text!) != nil {
print("The \(cell.textHiddenUserTagName.text!) UserTag exists for this device.")
cell.switchMinistryGroupList.isOn = true
} else {
cell.switchMinistryGroupList.isOn = false
}
}, onFailure: { error in
print("Error getting tags - \(String(describing: error?.localizedDescription))")
// errorWithDomain - OneSignalError
// code - HTTP error code from the OneSignal server
// userInfo - JSON OneSignal responded with
})
viewWillAppear(true)
return cell
}
}
In the above portion of the VC code, this part (below) is what is functioning but apparently not in a way the uses threading properly:
if OneSignalUserTags.range(of: cell.textHiddenUserTagName.text!) != nil {
print("The \(cell.textHiddenUserTagName.text!) UserTag exists for this device.")
cell.switchMinistryGroupList.isOn = true
} else {
cell.switchMinistryGroupList.isOn = false
}
It's not entirely clear what your code is doing, but there seems to be a few things that need sorting out, that will help you solve your problem.
1) Improve the naming of your objects. This helps others see what's going on when asking questions.
Don't call your cell CustomTableViewCell - call it, say, MinistryCell or something that represents the data its displaying. Rather than textLabelMinistryGroupList and textHiddenUserTagName tree ministryGroup and userTagName etc.
2) Let the cell populate itself. Make your IBOutlets in your cell private so you can't assign to them directly in your view controller. This is a bad habit!
3) Create an object (Ministry, say) that corresponds to the data you're assigning to the cell. Assign this to the cell and let the cell assign to its Outlets.
4) Never call viewWillAppear, or anything like it! These are called by the system.
You'll end up with something like this:
In your view controller
struct Ministry {
let group: String
let userTag: String
var tagExists: Bool?
}
You should create an array var ministries: [Ministry] and populate it at the start, rather than dealing with MinistryGroupArray and OneSignalUserTagArray separately.
In your cell
class MinistryCell: UITableViewCell {
#IBOutlet private weak var ministryGroup: UILabel!
#IBOutlet private weak var userTagName: UILabel!
#IBOutlet private weak var switch: UISwitch!
var ministry: Ministry? {
didSet {
ministryGroup.text = ministry?.group
userTagName.text = ministry?.userTag
if let tagExists = ministry?.tagExists {
switch.isEnabled = false
switch.isOn = tagExists
} else {
// We don't know the current state - disable the switch?
switch.isEnabled = false
}
}
}
}
Then you dataSource method will look like…
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as! MinistryCell
let ministry = ministries[indexPath.row]
cell.ministry = ministry
if ministry.tagExists == nil {
OneSignal.getTags { tags in
// Success - so update the corresponding ministry.tagExists
// then reload the cell at this indexPath
}, onFailure: { error in
print("Error")
})
}
return cell
}

how can I refresh a single button in UITableViewCell instead of refreshing whole table or whole cell?

In my swift app I have a UITableView with one static cell and many dynamic cells.
Static cell contains different fields, such as labels, map (taken from MapKit) and a button, that indicates whether user voted up or not.
Now, when user presses the button, I want to change its color, possibly without refreshing anything else.
So far my code looks like this:
var currentUserVote:Int = 0
func tableView(_ tview: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if (indexPath as NSIndexPath).row == 0 {
let cell = tview.dequeueReusableCell(withIdentifier: "cellStatic") as! VideoDetailsCell
fetchScore(cell.score)
let voteUpImage = UIImage(named: "voteUp");
let tintedVoteUpImage = voteUpImage?.withRenderingMode(UIImageRenderingMode.alwaysTemplate)
cell.voteUpButton.setImage(tintedVoteUpImage, for: UIControlState())
checkUsersVote() { responseObject in
if(responseObject == 1) {
cell.voteUpButton.tintColor = orangeColor
} else if (responseObject == -1){
cell.voteUpButton.tintColor = greyColor
} else {
cell.voteUpButton.tintColor = greyColor
}
self.currentUserVote = responseObject
}
//map handling:
let regionRadius: CLLocationDistance = 1000
let initialLocation = CLLocation(latitude: latitude, longitude: longitude)
centerMapOnLocation(initialLocation, map: cell.mapView, radius: regionRadius)
//cell.mapView.isScrollEnabled = false
cell.mapView.delegate = self
.
.
.
return cell
} else {
//handle dynamic cells
}
}
So in the method above I'm checking if user voted already and based on that I'm setting different color on the button. I'm also centering the map on a specific point.
Now, since it's a static cell, I connected IBAction outlet to that button:
#IBAction func voteUpButtonAction(_ sender: AnyObject) {
if(currentUserVote == 1) {
self.vote(0)
}else if (currentUserVote == -1){
self.vote(1)
} else {
self.vote(1)
}
}
and the vote method works as follows:
func vote(_ vote: Int){
let indexPath = IndexPath(row: 0, section: 0)
let cell = tview.dequeueReusableCell(withIdentifier: "cellStatic") as! VideoDetailsCell
switch(vote) {
case 1:
cell.voteUpButton.tintColor = orangeColor
case 0:
cell.voteUpButton.tintColor = greyColor
case -1:
cell.voteUpButton.tintColor = greyColor
default:
cell.voteUpButton.tintColor = greyColor
}
tview.beginUpdates()
tview.reloadRows(at: [indexPath], with: .automatic)
tview.endUpdates()
currentUserVote = vote
//sending vote to my backend
}
My problem is, that when user taps the button, he invokes the method vote, then - based on the vote, the button changes color, but immediately after that method cellForRow is called and it changes the color of the button again. Also, it refreshes the map that's inside of it.
What I want to achieve is that when user taps the button, it should immediately change its color and that's it. Map stays untouched and the button is not changed again from cellForRow.
Is there a way of refreshing only that particular button without calling again cellForRow?
First of all, you confuse static and dynamic cells. You can use static cells only in the UITableViewController and you can't use static and dynamic cell at the same time.
I strongly recommend you not to use cell for storing map and button. All elements from the cell will be released after scrolling it beyond the screen.
I can advise you use TableViewHeaderView for this task. In this case you will be able set button and map view as #IBOutlet.
(See more about adding tableview headerView. You can also set it from interface builder.)
Another way is change tableView.contentInset and set your view with map and button as subview to tableView. This method is used when you need create Stretchy Headers.
It should be quite easy, simply do it in your button handler. The sender argument there is the button object that caused the action. When you were connecting it from IB there was a dropdown to select sender type, you may have missed it and the whole thing would have been obvious with UIButton type there.
Simply change your handler like this :
#IBAction func voteUpButtonAction(_ sender: UIButton) {
if(currentUserVote == 1) {
self.vote(0)
}else if (currentUserVote == -1){
self.vote(1)
} else {
self.vote(1)
}
sender.backgroundColor = yourFavouriteColor
}
Another approach would be to create an IBOutlet for your button, since its from a static cell, and then you would be able to reference it from any place in your view controller.
In this call:
func tableView(_ tview: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
I see it calls checkUsersVote() which I'm guessing should get the updated value set in the vote() call. Could the problem be that you aren't doing this
currentUserVote = vote
until after reloadRows() is called?

Moving data between custom cells in a dynamic table in Swift

Firstly, apologies but I'm new to all of this (iOS Dev & Swift).
I have a number of custom cells that I dynamically load into a tableview. One of my cells is a data picker that, when the date is changed by the user, I want to send the updated data to one of the other cells but I'm stumped. Help please.
Assuming the cells are loaded and visible, you can pass a reference of one cell to another, but you'll need to create a couple of methods within your custom cells.
In my case I have two custom cells, a cell named CellBirthday that contains a label named birthDateLabel, and a cell that contains a DatePicker named CellDatePicker. I want to update birthDateLabel every time the DataPicker value changes.
I'll first load the cells and store a reference of CellBirthday inside CellDatePicker, then when the date picker changes, I'll update the label value inside CellBirthday. Here is the relevant code fragment to load the two cells. In this example, I use the same name for both the cell tag and class name, for example CellBirthday is both the cell tag and the class name specified in the storyboard:
let birthdayCell = tableView.dequeueReusableCell(withIdentifier: "CellBirthday") as! CellBirthday
let datePickerCell = tableView.dequeueReusableCell(withIdentifier: "CellDatePicker") as! CellDatePicker
datePickerCell.setBirthdayCell(BirthdayCell: birthdayCell)
And here are the custom classes:
class CellBirthday: UITableViewCell {
#IBOutlet fileprivate weak var birthDateLabel: UILabel!
var birthdayText: String? {
didSet {
birthDateLabel.text = birthdayText
}
}
}
class CellDatePicker: UITableViewCell {
#IBOutlet fileprivate weak var datePicker: UIDatePicker!
var birthdayCell: CellBirthday?
func setBirthdayCell(BirthdayCell birthdayCell: CellBirthday) {
self.birthdayCell = birthdayCell
}
func getDateString(FromPicker picker: UIDatePicker? = nil) -> String {
var dateText: String = ""
if picker != nil {
let dateFormatter = DateFormatter()
dateFormatter.setLocalizedDateFormatFromTemplate("MMMMdy")
dateText = dateFormatter.string(from: picker!.date)
}
return dateText
}
#IBAction func datePickerValueChange(_ sender: UIDatePicker) {
if birthdayCell != nil {
birthdayCell!.birthdayText = getDateString(FromPicker: sender)
}
}
}
Since your cells are dynamically loaded into the table, it is not possible to address a specific cell directly. You should trying changing the underlying data source when the user chooses a date, and call table.reloadData()

Resources