I have made a UITableView with some cells containing UISliders. Every UISlider has a unique tag - the number of a value it represents.
When isContinuous value is set to true for them, the first and the last sliders affect each other - dragging one of them makes the same change in value on the other.
Here is how those cells are made:
class CellWithSliderValues: UITableViewCell{
#IBOutlet var slider: UISlider! = {
let ctrl = UISlider()
ctrl.backgroundColor = Constants.SETTINGS_CELL_COLOR
return ctrl
}()
// some labels
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
// layout
}
override func prepareForReuse() {
super.prepareForReuse()
slider.tag = -10
minLabel.text = nil
maxLabel.text = nil
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
internal func makeSignalLevelCell(\*some args*\) -> UITableViewCell{
let tag: Int
let currValue: Float
let minValue, maxValue: Float
switch(kind){
\\ set all the values above
}
if (numInSection == 0){
// other cells
} else{
let cell = mainScreen.table.dequeueReusableCell(withIdentifier: "CellWithSliderValues") as! CellWithSliderValues
cell.slider.maximumValue = maxValue
cell.slider.minimumValue = minValue
cell.slider.value = currValue
cell.minLabel.text = Int(minValue).description
cell.maxLabel.text = Int(maxValue).description
cell.slider.tag = tag
cell.slider.addTarget(self, action: #selector(sliderChange), for: .valueChanged)
return cell
}
#objc func sliderChange(sender: UISlider){
guard let valueToAdjustNumber = HeatmapSettings.RangedValueOption(rawValue: sender.tag) else {print("Non-understood slider"); return}
print (valueToAdjustNumber)
let newValueGiven = sender.value
switch(valueToAdjustNumber){
\\check for validity and save changes to a different variable
}
sender.resignFirstResponder()
mainScreen.table.reloadData() // beacuse we need to refresh the number in text box
}
The first and the last sliders in the table affect each other - when one has its value adjusted, the other changes its value too. With some debug outputs - see the
print (valueToAdjustNumber)
line above - it looks like valueToAdjustNumber alternates between the one expected for the slider and the one affected by the glitch.
It is not the problem of cell reuse - I tried genereting new cells instead of reuse, and also reusing cells but recreating sliders, it did not help.
When .isContinuous is set to false for the sliders, the problem disappears.
Bug disappeared after reloading only one section at a time.
mainScreen.table.reloadData()
Is made into
mainScreen.table.reloadSections([sectionNumberToReload(value: valueToAdjustNumber)], with: .none)
Related
I am attempting to pass data from a the UITableView function cellForRowAt to a custom UITableViewCell. The cell constructs a UIStackView with n amount of UIViews inside of it. n is a count of items in an array and is dependent on the data that is suppose to be transferred (a count of items in that array). Something very confusing happens to me here. I have checked in the VC with the tableView that the data has successfully passed by using the following snippet of code
print("SELECTED EXERCISES: ", self.selectedExercises)
cell.randomSelectedExercises = self.selectedExercises
print("PRINTING FROM CELL: ", cell.randomSelectedExercise)
I can confirm that both of these print statements return non-empty arrays. So to me, this means that the custom cell has the data I require it to have. But, when I try to print out the very same array in the UITableViewCell swift file (randomSelectedExercises) , it returns empty to me. How is this possible? From what I understand, the cell works on creating the property initializers first, then 'self' becomes available. I had a previous error telling me this and to fix it, I turned my UIStackView initializer to lazy, but this is how I ended up with my current problem.
Here is the code in beginning that is relevant to the question that pertains to the table view. I have decided to present this code incase the issue is not in my cell but in my table view code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RoutineTableViewCell") as! RoutineTableViewCell
let workout = selectedWorkouts[indexPath.row]
//get the information we need - just the name at this point
let name = workout["name"] as! String
var randomInts = [Int]()
//perform a query
let query = PFQuery(className: "Exercise")
query.includeKey("associatedWorkout")
//filter by associated workout
query.whereKey("associatedWorkout", equalTo: workout)
query.findObjectsInBackground{ (exercises, error) in
if exercises != nil {
//Created an array of random integers... this code is irrelevant to the question
//Picking items from parent array. selectedExercises is a subarray
for num in randomInts {
//allExercises just contains every possible item to pick from
self.selectedExercises.append(self.allExercises[num-1])
}
//now we have our selected workouts
//Both print statements successfully print out correct information
print("SELECTED EXERCISES: ", self.selectedExercises)
cell.randomSelectedExercises = self.selectedExercises
print("PRINTING FROM CELL: ", cell.randomSelectedExercises)
//clear the arrays so we have fresh ones through each iteration
self.selectedExercises.removeAll(keepingCapacity: false)
self.allExercises.removeAll(keepingCapacity: false)
} else {
print("COULD NOT FIND WORKOUT")
}
}
//***This works as expected - workoutName is visible in cell***
cell.workoutName.text = name
//clear the used arrays
self.allExercises.removeAll(keepingCapacity: false)
self.selectedExercises.removeAll(keepingCapacity: false)
return cell
}
Below is the code that gives me a problem in the cell swift file. the randomSelectedExercise does not have any data in it when I enter this area. This is an issue because in my for loop I am iterating from 1 to randomSelectedExercise.count. If this value is 0, I receive an error. The issue is focused in the UIStackView initializer:
import UIKit
import Parse
//Constants
let constantHeight = 50
//dynamic height number
var heightConstantConstraint: CGFloat = 10
class RoutineTableViewCell: UITableViewCell {
//will hold the randomly selected exercises that need to be displayed
//***This is where I thought the data would be saved, but it is not... Why???***
var randomSelectedExercises = [PFObject]()
static var reuseIdentifier: String {
return String(describing: self)
}
// MARK: Overrides
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
contentView.addSubview(containerView)
containerView.addSubview(workoutName)
containerView.addSubview(stackView)
NSLayoutConstraint.activate(staticConstraints(heightConstantConstraint: heightConstantConstraint))
//reset value
heightConstantConstraint = 10
}
//MARK: Elements
//Initializing workoutName UILabel...
let workoutName: UILabel = {...}()
//***I RECEIVE AN EMPTY ARRAY IN THE PRINT STATEMENT HERE SO NUM WILL BE 0 AND I WILL RECEIVE AN ERROR IN THE FOR LOOP***
lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.backgroundColor = .gray
stackView.translatesAutoresizingMaskIntoConstraints = false
//not capturing the data here
print("rSE array:", randomSelectedExercises)
var num = randomSelectedExercises.count
for i in 1...num {
let newView = UIView(frame: CGRect(x: 0, y: (i*50)-50, width: 100, height: constantHeight))
heightConstantConstraint += CGFloat(constantHeight)
newView.backgroundColor = .purple
let newLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
newLabel.text = "Hello World"
newView.addSubview(newLabel)
stackView.addSubview(newView)
}
return stackView
}()
//initializing containerView UIView ...
let containerView: UIView = {...}()
//Setting the constraints for each component...
private func staticConstraints(heightConstantConstraint: CGFloat) -> [NSLayoutConstraint] {...}
}
Why is my data not properly transferring? How do I make my data transfer properly?
Here is what you're doing in your cellForRowAt func...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// get a cell instance
let cell = tableView.dequeueReusableCell(withIdentifier: "RoutineTableViewCell") as! RoutineTableViewCell
// ... misc stuff
// start a background process
query.findObjectsInBackground { (exercises, error) in
// nothing here will happen yet... it happens in the background
}
// ... misc stuff
return cell
// at this point, you have returned the cell
// and your background query is doing its work
}
So you instantiate the cell, set the text of a label, and return it.
The cell creates the stack view (with an empty randomSelectedExercises) and does its other init /setup tasks...
And then - after your background find objects task completes, you set the randomSelectedExercises.
What you most likely want to do is run the queries while you are generating your array of "workout" objects.
Then you will already have your "random exercises" array as part of the "workout" object in cellForRowAt.
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.
I run into the following. I have created a custom UIView using XIB file. I connected a VC that inherits from UIView and add it to the File Owner property. Connect two buttons with #IBOutlet and connected both buttons to the same #IBAction method, that will switch the background color, to get a boolean effect. By default no button is selected.
In my ViewController.swift file, that is connected with a View Controller in Storyboard, I add it as subview inside my UICollectionView cells.
Based on my two model classes (Messages and PermissionMessage), I determine what the text of the buttons should be.
My wish is to add functions to the buttons, so when button one is clicked, it will fire function A and if button two is selected, function B.
At this point I don't know where I should do this logic, to determine which function should be fired off. Also because I can create different instances of the same BooleanView.xib so I can't add the functions to the file owners class of that XIB file, am I right?
I am trying to get the button click inside the didSelectItemAt method of UICollectionView, but I can't get access to that programmatically added custom UIView called BooleanView().
How and what should be the correct way to add functions to each instance buttons, based on the button that is clicked?
In my example I want to determine for the current instance if the selected button is Manual or GPS. If Manual, it will do something, like print a text in my console, if GPS is selected, I would like to call the initializer for the LocationManager that I stored in a custom separate class/handler.
This function is placed inside the cellForItemAt method of UICollectionView:
if message is PermissionMessage {
let bool = BooleanView()
bool.frame = CGRect(x: 60, y: estimatedFrame.height + 15, width: estimatedFrame.width + 15 + 8, height: bool.frame.height)
let permissionType = (message as! PermissionMessage).getTypeOfPermission()
switch permissionType {
case .camera:
print("Case: camera")
bool.leftButton.setTitle("Yes", for: .normal)
bool.rightButton.setTitle("No", for: .normal)
cell.addSubview(bool)
case .location:
print("Case: location")
bool.leftButton.setTitle("Manual", for: .normal)
bool.rightButton.setTitle("GPS", for: .normal)
cell.addSubview(bool)
}
}
Note: the message variable is the selected array entry of indexPath.row.
The custom UIView class that is added to my BooleanView.xib:
class BooleanView: UIView {
#IBOutlet var contentView: UIView!
#IBOutlet weak var leftButton: UIButton!
#IBOutlet weak var rightButton: UIButton!
//MARK: Initialization
override init(frame: CGRect) {
super.init(frame: frame)
// Load the nib named 'CardView' into memory, finding it in the main bundle.
Bundle.main.loadNibNamed("BooleanView", owner: self, options: nil)
self.frame = contentView.frame
addSubview(contentView)
// Style the custom view
contentView.backgroundColor = UIColor.clear
leftButton.layer.cornerRadius = (leftButton.frame.height / 2)
rightButton.layer.cornerRadius = (rightButton.frame.height / 2)
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
#IBAction func pressedButton(_ sender: UIButton) {
if sender.backgroundColor == Constants.BOOL_BUTTON.enabled {
return
}
sender.backgroundColor = Constants.BOOL_BUTTON.enabled
sender.titleLabel?.font = UIFont(name: Constants.FONTS.bold, size: (sender.titleLabel?.font.pointSize)!)
if sender.tag == 0 {
self.rightButton.backgroundColor = Constants.BOOL_BUTTON.disabled
self.rightButton.titleLabel?.font = UIFont(name: Constants.FONTS.regular, size: (self.rightButton.titleLabel?.font.pointSize)!)
} else {
self.leftButton.backgroundColor = Constants.BOOL_BUTTON.disabled
self.leftButton.titleLabel?.font = UIFont(name: Constants.FONTS.regular, size: (self.leftButton.titleLabel?.font.pointSize)!)
}
}
}
First you need a way to communicate which, if either, button has been pressed, an enum will work for that:
enum BooleanViewState {
case none
case left
case right
}
Next your BooleanView needs to be able to communicate its state to interested parties. Given that you only care about the state of a given boolean view at specific moments in time the easiest is to use a computed property. Also, your current method of reaching into the view to set button titles isn't the best of practices so we will add an alternative to that as well:
class BooleanView: UIView {
var boolViewState: BooleanViewState {
get {
if leftButton.backgroundColor == Constants.BOOL_BUTTON.enabled {
return .left
} else if rightButton.backgroundColor == Constants.BOOL_BUTTON.enabled {
return .right
} else {
return .none
}
}
}
var buttonTitles = (leftTitle: "", rightTitle: "") {
didSet {
leftButton.setTitle(buttonTitles.leftTitle, for: .normal)
rightButton.setTitle(buttonTitles.rightTitle, for: .normal)
}
}
// The rest of you existing class
}
It is possible you will need to add a didSet to the outlets for the two buttons that gets the titles. The titles might get set before the buttons exist.
Next you need to switch from using a default UICollectionViewCell to a custom subclass that can hold a BooleanView and pass things back and forth:
class BooleanViewCell: UICollectionViewCell {
#IBOutlet private var boolView: BooleanView!
var boolViewState: BooleanViewState {
get {
return boolView.boolViewState
}
}
var buttonTitles = (leftTitle: "", rightTitle: "") {
didSet {
boolView.buttonTitles = buttonTitles
}
}
}
You'll need to change some code in your cellForItemAt method
if message is PermissionMessage {
let permissionType = (message as! PermissionMessage).getTypeOfPermission()
switch permissionType {
case .camera:
print("Case: camera")
cell.buttonTitles = (leftTitle: "Yes", rightTitle: "No")
case .location:
print("Case: location")
cell.buttonTitles = (leftTitle: "Manual", rightTitle: "GPS")
}
}
The last thing to do is read boolState from the cell in didSelectItemAt
guard let cell = collectionView.cellForItem(at: indexPath) as? BooleanViewCell else { return }
let theState = cell.boolState
// React to the state as needed
This does mean that you will need to add a generic UIView to your prototype collection view cell in the storyboard, set its class to BooleanView and connect it to the outlet in BooleanViewCell.
That should take care of it all for you though I did all of this with no compiler so I make no guarantees I haven't mistyped or missed something all together.
I am having a problem creating UILabels to insert into my tableview cells. They insert fine and display fine, the problem comes in when I go to scroll. The ordering of the row's goes haywire and looses it's initial order. For instance, at the beginning, the idLabel's cell text, goes from 0 > 14, in sequential order. After scrolling down and back up, the order could be anything, 5, 0, 10, 3 etc.
Any ideas on why this is happening? I am presuming my attempts to update the label's after cell creation is wrong.
var idArr: NSMutableArray!
// In init function
self.idArr = NSMutableArray()
for ind in 0...14 {
self.idArr[ind] = ind
}
// Create the tableview cells
internal func tableView( tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath ) -> UITableViewCell {
if cell == nil {
let id = self.idArr[indexPath.row] as Int
cell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "cell" )
// Id
let idLabel: UILabel = UILabel( frame: CGRectZero )
idLabel.text = String( id )
idLabel.textColor = UIColor.blackColor()
idLabel.tag = indexPath.row
idLabel.sizeToFit()
idLabel.layer.anchorPoint = CGPointMake( 0, 0 )
idLabel.layer.position.x = 15
idLabel.layer.position.y = 10
idLabel.textAlignment = NSTextAlignment.Center
cell?.addSubview( idLabel )
}
// Update any labels text string
for obj: AnyObject in cell!.subviews {
if var view = obj as? UILabel {
if view.isKindOfClass( UILabel ) {
if view.tag == indexPath.row {
view.text = String( id )
}
}
}
}
// Return the cell
return cell!
}
Thanks for any assistance, I can't see the wood for the trees anymore.
The logic in your cell updating looks off. You are checking that a tag you set at initialization is equal to the index path's row but because the cells should be reused this will not always be the case. If you just remove that tag checking line in the for loop it should work fine.
Also, I'm not sure why you are checking ifKindOfClass after you have optionally cast the view as a label anyway.
To prevent accidentally updating Apple's views you would be better off adding a constant tag for the label and pulling the label by that tag instead of looping through all of the subviews.
This is clearly not the actual code you are running since the id variable is not in the proper scope and there is no definition of cell. I've added proper dequeueing and move the id variable to something that works. I'm assuming you are doing those in your actual code.
internal func tableView( tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath ) -> UITableViewCell {
let labelTag = 2500
let id = self.idArr[indexPath.row] as Int
var cell = tableView.dequeueReusableCellWithIdentifier("cell")
if cell == nil {
cell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "cell" )
// Id
let idLabel: UILabel = UILabel( frame: CGRectZero )
idLabel.text = String( id )
idLabel.textColor = UIColor.blackColor()
idLabel.tag = labelTag
idLabel.sizeToFit()
idLabel.layer.anchorPoint = CGPointMake( 0, 0 )
idLabel.layer.position.x = 15
idLabel.layer.position.y = 10
idLabel.textAlignment = NSTextAlignment.Center
cell?.addSubview( idLabel )
}
// Update any labels text string
if let view = cell?.viewWithTag(labelTag) as? UILabel {
view.text = String( id )
}
// Return the cell
return cell!
}
I got a strange issue (in my opinion;)) considering a subclass of uitableview cell. The code for my subclass is:
class SubLevelTableCell: UITableViewCell {
var subLevelLabel:UILabel
var subLevelBack:UIView
var subLevelScore:UIImageView
override init(style: UITableViewCellStyle, reuseIdentifier: String!)
{
self.subLevelBack = UIView()
self.subLevelLabel = UILabel()
self.subLevelScore = UIImageView()
super.init(style: UITableViewCellStyle.Value1, reuseIdentifier: reuseIdentifier)
self.subLevelLabel.textColor = whiteColor
self.subLevelLabel.font = UIFont(name: subLevelLabel.font.fontName, size: 20)
self.addSubview(self.subLevelBack)
self.subLevelBack.addSubview(self.subLevelLabel)
self.subLevelBack.addSubview(self.subLevelScore)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
self.subLevelBack.frame = CGRectMake(10, 10, self.bounds.size.width-20, self.bounds.size.height-20)
self.subLevelLabel.frame = CGRectMake(5, 0, subLevelBack.frame.size.width/2-10, subLevelBack.frame.size.height)
self.subLevelScore.frame = CGRectMake(subLevelBack.frame.size.width-120, 15, 100, subLevelBack.frame.size.height-30)
}
}
Now I create these cells in my view controller as follows:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:SubLevelTableCell? = SubLevelTable?.dequeueReusableCellWithIdentifier("Cell") as? SubLevelTableCell
if (cell == nil)
{
cell = SubLevelTableCell(style: UITableViewCellStyle.Subtitle,
reuseIdentifier: "Cell")
cell!.backgroundColor = grayColor
}
if (indexPath.row == 0){nextLevelCanBePlayed = true}
if(nextLevelCanBePlayed){
canBePlayedArray[indexPath.row] = true
cell!.subLevelBack.backgroundColor = blueColor
}else{
canBePlayedArray[indexPath.row] = false
cell!.subLevelBack.backgroundColor = redColor
}
cell!.subLevelLabel.text = "Stage \(indexPath.row+1)"
let data = db.query("SELECT * FROM LEVEL_DETAIL WHERE MAIN_LEVEL =\(MainLevelNr!) AND SUB_LEVEL = \(indexPath.row+1)")[0]
var levelScore: Int = 0
if let columnValue = data["COMPLETED"]{
levelScore = columnValue.asInt()
if (levelScore > 0){nextLevelCanBePlayed = true}else{nextLevelCanBePlayed = false}
cell!.subLevelScore.image = UIImage(named: "\(levelScore)Stars.png")
}
return cell!
}
My problem is that when the view loads for the first time, the subLevelBack.backgroundColor is set properly, ie. the subview's color is correct.
Though when I start scrolling, it becomes a bit of a mess with different cells having incorrect background colors, and I don't know how to solve this issue. I don't have this same issue with the image displayed in the UIImageView btw.
I hope someone will point me in the right direction. Kind regards, Sander
if (indexPath.row == 0){nextLevelCanBePlayed = true}
if(nextLevelCanBePlayed){ ...
The above code seems incorrect. You're trying to keep track of how many levels have been played, but each time the first cell gets re-rendered, nextLevelCanBePlayed will be reset. Perhaps try just if(nextLevelCanBePlayed || indexPath.row == 0){.
The other thing is you're making the assumption that the table view will re-render its cells near the viewport in order, but isn't documented anywhere and very likely isn't reliably true, and is especially likely if you're scrolling in the reverse direction. So when the table view re-builds cell 4 (nextLevelCanBePlayed calculates and sets to false) and maybe goes back to cell 3 next, and even if 3 is actually playable, nextLevelCanBePlayed will be false and 3 will show up incorrect. It then starts making sense that your colors start erratically changing as you scroll up and down.
My suggestion is to properly use your UITableViewDataSource methods and use those to work with the db and return proper data objects that represent the state and data of your cells.