RXSwift + UITableView with animated Cell ends up in flashing reloads - ios

I have the following issue. I use RxSwift to bind my datasource to an UITableView like this:
self.presenter.posts
.bind(to: self.tableView.rx.items(cellIdentifier: "ListPostsCell")) { index, model, cell in
if let cell = cell as? ImagePostTableViewCell {
cell.viewModel = model
cell.actionButton.rx.action = self.action(for: model.postId, index: index)
}
}.disposed(by: self.bag)
When the tableview reaches the end of the datasource count I request more data and update the datasource (infinite scroll) like this:
self.tableView.rx.willDisplayCell
.map { $0.indexPath.item }
.distinctUntilChanged()
.withLatestFrom(self.presenter.posts) { (item, posts) -> Bool in
return item == posts.count - 3
}
.filter { $0 }
.withLatestFrom(self.presenter.posts)
.map { UsersPosts.Request(after: $0.last?.postId) }
.bind(to: self.usersPostsInteractor.receiveUsersPosts.inputs)
.disposed(by: self.bag)
This works fine and I can scroll infinite as expected. The problem is within my Cell:
class ImagePostTableViewCell: UITableViewCell {
#IBOutlet weak var postImageView: UIImageView!
#IBOutlet weak var haiScoreLabel: UILabel!
#IBOutlet weak var lastVoteLabel: UILabel!
#IBOutlet weak var actionButton: UIButton!
#IBOutlet weak var activtiyIndicator: UIActivityIndicatorView!
var bag: DisposeBag?
var viewModel: ImagePostTableViewCellViewModel? {
didSet {
let bag = DisposeBag()
guard let vm = viewModel else { return }
vm.image.asDriver(onErrorJustReturn: UIImage())
.drive(onNext: { image in
self.updateAppearanceFor(image)
}).disposed(by: bag)
self.haiScoreLabel.text = vm.haiScoreText
self.lastVoteLabel.text = vm.lastVoteText
self.actionButton.setTitle(vm.actionLabelText, for: .normal)
self.activtiyIndicator.startAnimating()
self.bag = bag
}
}
func updateAppearanceFor(_ image: UIImage?, animated: Bool = true) {
DispatchQueue.main.async {
if animated {
UIView.animate(withDuration: 0.5) {
self.displayImage(image)
}
} else {
self.displayImage(image)
}
}
}
private func displayImage(_ image: UIImage?) {
if let image = image {
self.show(image: image)
} else {
self.hide()
}
}
private func show(image: UIImage) {
self.postImageView.image = image
self.postImageView.alpha = 1.0
self.haiScoreLabel.alpha = 1.0
self.lastVoteLabel.alpha = 1.0
self.activtiyIndicator.stopAnimating()
}
private func hide() {
self.postImageView.image = nil
self.postImageView.alpha = 0
self.haiScoreLabel.alpha = 0
self.lastVoteLabel.alpha = 0
self.activtiyIndicator.startAnimating()
}
override public func prepareForReuse() {
super.prepareForReuse()
self.displayImage(.none)
self.viewModel = nil
self.bag = nil
}
}
As you can see there is an animation which ends up in an super smooth scrolling behaviour - but because of the datasource update, with every reload the animation starts and unchanged content flashes because of the animation.
In the end I want to avoid the reload for the visible cell but I really desperate here.
Any hints to follow? Thanks in advance!!!

Related

Update text values of TableViewCell content by using UIButton action inside TableViewCell

I am working on an app in which i am using UITableviewCell to display feed data. In Cell i am using multiple UIButtons to perform actions.
The main issue arise when i perform action through buttons the method is not called for the very first time(TableViewCell content is not updated). But when i press button second time it works fine.
Here is my code
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell_Reels", for: indexPath) as! Cell_Reels
cell.delegate = self
DispatchQueue.main.asyncAfter(deadline: .now()+0.1) { [self] in
UIView.animate(withDuration: 12.0, delay: 1, options: ([.curveLinear, .repeat]), animations: {() -> Void in
cell.marqueeLabel.center = CGPoint.init(x: 35 - cell.marqueeLabel.bounds.size.width / 2, y: cell.marqueeLabel.center.y)
}, completion: { _ in })
cell.likebutton.tag = indexPath.row
cell.FollowButton.tag = indexPath.row
cell.shareBtn.tag = indexPath.row
cell.shareBtn.addTarget(self, action: #selector(buttonShare), for: .touchUpInside)
let VideoStr = self.videos[indexPath.row]["media"].string ?? ""
self.fullVideoUrl = "\(KeyConstant.KBaseImageURL)\(VideoStr)"
//print("======", self.fullVideoUrl)
cell.configureCell( videoUrl: self.fullVideoUrl)
self.pausePlayeVideos()
cell.totalViews.text = "\(self.videos[indexPath.row]["count_view"].string ?? "") Views"
cell.nameLbl.text = self.videos[indexPath.row]["full_name"].string ?? ""
cell.descLbl.text = self.videos[indexPath.row]["title"].string ?? ""
cell.marqueeLabel.text = self.videos[indexPath.row]["title"].string ?? ""
cell.idLbl.text = self.videos[indexPath.row]["id"].string ?? ""
cell.likedLbl.text = self.videos[indexPath.row]["like_status"].string ?? ""
cell.followedLbl.text = self.videos[indexPath.row]["follow_status"].string ?? ""
self.currentLikesStr = self.videos[indexPath.row]["count_like"].string ?? ""
cell.likesCountLabel.text = "\(self.currentLikesStr) Likes"
self.followedStr = self.videos[indexPath.row]["follow_status"].string ?? ""
print("followedStr ppppppppppppppppppppppppp",followedStr, cell.idLbl.text!)
if self.followedStr == "0"
{
cell.FollowButton .setTitle("Follow", for: UIControl.State.normal)
}
else
{
cell.FollowButton .setTitle("Following", for: UIControl.State.normal)
}
self.likedStr = self.videos[indexPath.row]["like_status"].string ?? ""
print("likedStr -=-==-===-=-=-",likedStr, cell.idLbl.text!)
if likedStr == "0"
{
let selectedImage = UIImage(named: "heart_y2_30x30") as UIImage?
cell.likebutton.setImage(selectedImage, for: UIControl.State.normal)
}
else
{
let selectedImage = UIImage(named: "heart_y_30x30") as UIImage?
cell.likebutton.setImage(selectedImage, for: UIControl.State.normal)
}
cell.CommentsCountLabel.text = "Comments"
}
return cell
}
Button method decleration
func addFollowed(cell: Cell_Reels)
{
followedStr = cell.followedLbl.text!
let idstr = cell.idLbl.text
print(userId, idstr ?? "", followedStr)
let VideoidStr = cell.idLbl.text
print("=====", VideoidStr!)
let deviceid = "12345"
//print(videoId)
WebServiceHelper.sharedInstanceAPI.hitPostAPI(urlString: KeyConstant.APIFollow, params: ["user_id":userId, "device_id": deviceid, "device_type" : "iOS", "video_id":VideoidStr!], completionHandler: { (result: [String:Any], err:Error?) in
//print(result)
DispatchQueue.main.async {
//MBProgress().hideIndicator(view: self.view)
}
if(!(err == nil))
{
return
}
let json = JSON(result)
let statusCode = json["status"].string
//print(json)
if(statusCode == "success")
{
DispatchQueue.main.asyncAfter(deadline: .now()+0.1) { [self] in
self.followedStr = json["follow_status"].string ?? ""
cell.followedLbl.text = "\(self.followedStr)"
print(self.followedStr)
print(";;;;;;;;;;;;;;;;;;", self.followedStr)
if self.followedStr == "0"
{
cell.FollowButton .setTitle("Follow", for: UIControl.State.normal)
self.followedStr = "1"
self.arrayTempData[cell.FollowButton.tag] = 1
}
else
{
cell.FollowButton .setTitle("Following", for: UIControl.State.normal)
self.followedStr = "0"
self.arrayTempData[cell.FollowButton.tag] = 0
}
}
}
})
self.tblInstaReels.reloadData()
self.followedStr = ""
}
This i defined in TableViewCell class
protocol TableViewCellDelegate
{
func addTapped(cell: Cell_Reels, Tlikes : String)
func addFollowed(cell: Cell_Reels)
}
class Cell_Reels: UITableViewCell, ASAutoPlayVideoLayerContainer
{
var delegate: TableViewCellDelegate?
#IBOutlet weak var likedLbl : UILabel!
#IBOutlet weak var followedLbl : UILabel!
#IBOutlet weak var idLbl : UILabel!
#IBOutlet weak var nameLbl : UILabel!
#IBOutlet weak var likesCountLabel : UILabel!
#IBOutlet weak var CommentsCountLabel : UILabel!
#IBOutlet weak var marqueeLabel: UILabel!
#IBOutlet weak var imgReels: UIImageView!
#IBOutlet weak var Bkview: UIView!
#IBOutlet weak var PauseBtn: UIButton!
#IBOutlet weak var descLbl: UILabel!
#IBOutlet weak var shareBtn : UIButton!
#IBOutlet weak var likebutton : UIButton!
#IBOutlet weak var FollowButton : UIButton!
#IBOutlet weak var totalViews : UILabel!
var playerController: ASVideoPlayerController?
var videoLayer: AVPlayerLayer = AVPlayerLayer()
var likestr = ""
private(set) var liked = false
private(set) var followed = false
var VideoId = ""
#IBAction func followed(_ sender: Any)
{
delegate?.addFollowed(cell: self)
}
#IBAction func like(_ sender: Any)
{
delegate?.addTapped(cell: self, Tlikes: self.likedLbl.text!)
}
func likeVideo()
{
print("==========", likestr)
}
var videoURL: String?
{
didSet {
if let videoURL = videoURL
{
ASVideoPlayerController.sharedVideoPlayer.setupVideoFor(url: videoURL)
}
videoLayer.isHidden = videoURL == nil
}
}
override func awakeFromNib()
{
super.awakeFromNib()
imgReels.layer.addSublayer(videoLayer)
selectionStyle = .none
}
func configureCell(videoUrl: String?)
{
self.videoURL = videoUrl
}
override func prepareForReuse()
{
imgReels.imageURL = nil
super.prepareForReuse()
}
override func layoutSubviews()
{
super.layoutSubviews()
let horizontalMargin: CGFloat = 20
let width: CGFloat = bounds.size.width - horizontalMargin * 2
let height: CGFloat = (width * 0.9).rounded(.up)
videoLayer.frame = CGRect(x: 0, y: 0, width: imgReels.layer.frame.width, height: imgReels.layer.frame.height)
}
func visibleVideoHeight() -> CGFloat
{
return imgReels.frame.size.height
let videoFrameInParentSuperView: CGRect? = self.superview?.superview?.convert(imgReels.frame, from: imgReels)
guard let videoFrame = videoFrameInParentSuperView,
let superViewFrame = imgReels?.frame else {
return 0
}
let visibleVideoFrame = videoFrame.intersection(superViewFrame)
return visibleVideoFrame.size.height
}
}
in place of calling a separate method, use the addTarget method.
if you update anything in table view then perform all operations in cellForRowAt else after scrolling it reused the cell and it will create ambiguity
reload the table view in the main thread.

Swift Protocol of Custom View Always nil

I have a custom view CustomSegmentedControl in my app which have its protocol with changed index function as below,
protocol SegmentControllerDelegate:class {
func indexChanged(index : Int)
}
class CustomSegmentedControl: UIView {
weak var delegate : SegmentControllerDelegate?
#objc func buttonAction(sender:UIButton) {
for (buttonIndex, btn) in buttons.enumerated() {
btn.setTitleColor(textColor, for: .normal)
if btn == sender {
let selectorPosition = frame.width/CGFloat(buttonTitles.count) * CGFloat(buttonIndex)
selectedIndex = buttonIndex
//delegate?.changeToIndex(index: buttonIndex)
if(delegate != nil){
delegate?.indexChanged(index: buttonIndex)
}else{
print(buttonIndex)
}
UIView.animate(withDuration: 0.3) {
self.selectorView.frame.origin.x = selectorPosition
}
btn.setTitleColor(selectorTextColor, for: .normal)
}
}
}
I added a view to my ViewController Mail and set the class to my custom view and make outlet for it and use the protocol as well like below,
import UIKit
class Mail: UIViewController, SegmentControllerDelegate {
func indexChanged(index: Int) {
print(index)
switch index {
case 0:
container.bringSubviewToFront(inbox)
break
case 1:
container.bringSubviewToFront(outbox)
break
default:
break
}
}
#IBOutlet weak var segment: CustomSegmentedControl!{
didSet{
segment.setButtonTitles(buttonTitles: ["First","Second"])
segment.selectorTextColor = .orange
segment.selectorViewColor = .orange
}
}
#IBOutlet weak var container: UIView!
var inbox : UIView!
var outbox : UIView!
override func viewDidLoad() {
super.viewDidLoad()
segment.delegate = self
inbox = Inbox().view
outbox = GPS().view
container.addSubview(inbox)
container.addSubview(outbox)
}
But the protocol is always nil and the function never be called !?, What Im missing here ?
Any help will be much appreciated

Deinit not called where blocks used in swift 3.0

I have used blocks in cell for getting switch value but now my problem is that deinit not called where i used the blocks. it is completely working previously but in swift 3.0 it is not working.
My switch cell :
import UIKit
class CellSwitch: UITableViewCell {
#IBOutlet weak var objSwitch: UISwitch!
#IBOutlet weak var btnInfo: UIButton!
#IBOutlet weak var lblTitle: UILabel!
var blockSwitch_Change : ((_ isOn:Bool) -> Void)!
var blockBtn_Clicked : (() -> Void)!
override func awakeFromNib() {
super.awakeFromNib()
self.lblTitle.font = Font.init(Font.FontType.custom(Font.FontName.NotoSans_Regular), size: Font.FontSize.standard(Font.StandardSize.Regular)).instance
// Initialization code
}
//MARK:- switch object change
#IBAction func switch_ValChanged(_ obj:UISwitch){
self.blockSwitch_Change?(obj.isOn)
}
//MARK:- button clicked
#IBAction func btnInfo_Clicked(_ sender: UIButton) {
self.blockBtn_Clicked?()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
Uses of this cell
let cell = tableView.dequeueReusableCell(withIdentifier: CellSwitch.identifier) as? CellSwitch
cell?.lblTitle.textColor = Color.custom(hexString: objModel.titleLblColor, alpha: 1.0).value
cell?.lblTitle.text = objModel.strTitle
cell?.objSwitch.isOn = objModel.isOn
cell?.btnInfo.isHidden = !objModel.isInfoBtn
cell?.blockBtn_Clicked = {
print("button clicked")
}
cell?.blockSwitch_Change = { (isOn) in
print("switch value changed \(isOn)")
}
if objModel.isEnable == false
{
cell?.isUserInteractionEnabled = false
cell?.contentView.alpha = 0.5
}
else
{
cell?.isUserInteractionEnabled = true
cell?.contentView.alpha = 1.0
}
return cell!
Also if i comment this two blocks then my deinit will called.
It sounds like you are creating a retain cycle by referencing the view controller strongly inside the blocks. Instead you should create a weak reference to the vc to use. This is my preferred method
cell?.blockBtn_Clicked = { [weak self]
print("button clicked")
self?.viewModel.//do something
}
cell?.blockSwitch_Change = { [weak self] (isOn) in
print("switch value changed \(isOn)")
self?.viewModel.//do something
}
The [weak self] portion will pass a weak reference of self into the block, although note this reference is now an optional. You can then use optional chaining or unwrap it after that.

How to change button title every time checkmark selected within custom cell

I have a viewcontroller that has a tableview and a button at the bottom. Within each cell is a radio button as a tapGesture. I would like to updated the button with the number of cells selected. If my gesture is in the custom cell and my button is in my viewcontroller how can I get the two to work together?
Custom cell:
class SearchTalentCell: UITableViewCell {
#IBOutlet weak var userProfileImage: UIImageView!
#IBOutlet weak var talentUserName: UILabel!
#IBOutlet weak var selectedImg: UIImageView!
#IBOutlet weak var inviteSentImg: UIImageView!
var prospectRef: FIRDatabaseReference!
var currentTalent: UserType!
func setTalent(talent: UserType) {
currentTalent = talent
currentTalent.userKey = talent.userKey
}
override func awakeFromNib() {
super.awakeFromNib()
let tap = UITapGestureRecognizer(target: self, action: #selector(selectTapped))
tap.numberOfTapsRequired = 1
selectedImg.addGestureRecognizer(tap)
selectedImg.isUserInteractionEnabled = true
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func configureCell(user: UserType, img: UIImage? = nil) {
prospectRef = Cast.REF_PRE_PRODUCTION_CASTING_POSITION.child(ProjectDetailVC.currentProject).child(FIRDataCast.prospect.rawValue).child(CastingDetailVC.positionName).child(user.userKey)
setTalent(talent: user)
self.talentUserName.text = "\(user.firstName) \(user.lastName)"
prospectRef.observeSingleEvent(of: .value, with: { (snapshot) in
if let _ = snapshot.value as? NSNull {
self.inviteSentImg.isHidden = true
} else {
self.inviteSentImg.image = UIImage(named: "inviteSent")
self.inviteSentImg.isHidden = false
}
})
if UserType.selectedTalentForSearch.contains(currentTalent.userKey) {
selectedImg.image = UIImage(named: "radioSelected")
} else {
selectedImg.image = UIImage(named: "radioUnselected")
}
//Image Caching
if img != nil {
self.userProfileImage.image = img
} else {
if let imageURL = user.profileImage {
let ref = FIRStorage.storage().reference(forURL: imageURL)
ref.data(withMaxSize: 2 * 1024 * 1024, completion: { (data, error) in
if error != nil {
print("ZACK: Unable to download image from Firebase Storage")
} else {
print("ZACK: Image downloaded from Firebase Storage")
if let imgData = data {
if let img = UIImage(data: imgData) {
self.userProfileImage.image = img
SearchTalentVC.userProfileImageCache.setObject(img, forKey: imageURL as NSString)
}
}
}
})
}
}
}
#objc func selectTapped(sender: UITapGestureRecognizer) {
if UserType.selectedTalentForSearch.contains(currentTalent.userKey) {
selectedImg.image = UIImage(named: "radioUnselected")
UserType.selectedTalentForSearch = UserType.selectedTalentForSearch.filter{$0 != currentTalent.userKey}
print("Keys: \(UserType.selectedTalentForSearch)")
} else {
selectedImg.image = UIImage(named: "radioSelected")
UserType.selectedTalentForSearch.append(currentTalent.userKey)
print("Keys: \(UserType.selectedTalentForSearch)")
}
}
}
ViewController:
class SearchTalentVC: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var sendInviteButton: UIButton!
var searchingRole = [Cast]()
var unfilteredTalent = [UserType]()
var filteredTalent = [UserType]()
var selectedTalent = [UserType]()
var matchingTalentUserKeys = [String]()
var isFiltered = false
var selectedCounter = [String]()
var prospectRef: FIRDatabaseReference!
static var userProfileImageCache: NSCache<NSString, UIImage> = NSCache()
let searchController = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
super.viewDidLoad()
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search Talent"
searchController.searchBar.barStyle = .black
navigationItem.searchController = searchController
definesPresentationContext = true
searchController.searchBar.scopeButtonTitles = ["All", "Role Specific"]
searchController.searchBar.tintColor = UIColor.white
searchController.searchBar.delegate = self
searchController.searchResultsUpdater = self
self.sendInviteButton.setTitle("Send Invite to \(UserType.selectedTalentForSearch.count) Prospects", for: .normal)
getTalentProfiles()
}
Thank you for any help!
I'm not sure why you are using the cell selection inside the cell, as opposed to the tableview delegate function func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)?
If you use didSelectRowAt, you could have an array of selected rows and you can include the selectedRows.count into your button text.
Hope that helps!

Update CoreData Value with UISwitch in UITableViewCell

Just learning so I am sure there are much better ways to code what I have so far. Anyways, I have a simple task list that is saved to core data. You can segue to another VC where you can edit or create new task. I wanted to be able to put a uiswitch in the custom cell that would allow you to set the task as complete and then reload the tableview. I have searched all over and found this Select UITableView's row when tapping on its UISwitch in Swift but can't seem to translate a solution. Any help would be great.
enter image description here
I am not sure if I am supposed to code the save within the custom cell or just or pass delegate.
Here is my custom cell
protocol SettingCellDelegate : class {
func didChangeSwitchState(sender: TaskCell, isOn: Bool)
}
class TaskCell: UITableViewCell {
var delegate: SettingCellDelegate!
#IBOutlet weak var taskImage: UIImageView!
#IBOutlet weak var arrowImage: UIImageView!
#IBOutlet weak var dueDateLabel: UILabel!
#IBOutlet weak var dueDateValue: UILabel!
#IBOutlet weak var dueTimeLabel: UILabel!
#IBOutlet weak var dueTimeValue: UILabel!
#IBOutlet var taskCellStatus: UITextField!
#IBOutlet var taskCellSwitchDisplay: UISwitch!
#IBAction func taskCellSwitchAction(sender: AnyObject) {
if (taskCellSwitchDisplay.on) {
taskCellStatus.text = "Completed"
} else {
taskCellStatus.text = "Active"
}
}
#IBOutlet weak var taskNameValue: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
delegate = self
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
taskCellSwitchDisplay.addTarget(self, action: "switchChanged:", forControlEvents: UIControlEvents.ValueChanged)
}
func configureCell(task: Tasks) {
taskImage.image = UIImage(named: "Icon.png")
arrowImage.image = UIImage(named: "arrow_filled.png")
dueDateLabel.text = "Due Date:"
dueDateValue.text = task.taskDate
dueTimeLabel.text = "Due Time:"
dueTimeValue.text = task.taskTime
taskNameValue.text = task.taskName
taskCellStatus.text = task.status
//taskCellStatus.hidden = true
if (taskCellStatus.text == "Completed") {
taskCellSwitchDisplay.on = true
} else {
taskCellSwitchDisplay.on = false
}
}
func switchChanged(taskCellSwitchDisplay: UISwitch) {
_ = taskCellSwitchDisplay.on
if taskCellSwitchDisplay.on == true {
let switchValue = "Completed"
print("Switch Variable: \(switchValue)")
delegate.didChangeSwitchState(self, isOn: true)
} else {
let switchValue = "Active"
print("Switch Variable: \(switchValue)")
delegate.didChangeSwitchState(self, isOn: false)
}
}
Here is where I am getting the delegate in my tableview
extension TaskCell: SettingCellDelegate {
func didChangeSwitchState(sender: TaskCell, isOn: Bool) {
if isOn == true {
print("true")
} else {
print("false")
}
}

Resources