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()
Related
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
}
}
}
I am using MVVM pattern with RxSwift, RxCocoa, RxDataSources.
I have successfully populated the UITableView with array of PaletteViewModel present in ListViewModel by using RxDataSource but it's one way binding.
I want to achieve what I have shown in the picture i.e. I want to bind the UITextField from UITableViewCell to the Observable which is present at some index in the array in ListViewModel
I want to do 2 way binding with the UITextField and answer property of the PaletteViewModel. If the user changes the text in the textField it should change the value in the answer property present at particular index and vice versa.
How Can I achieve something complex like this using MVVM pattern using ReactiveX frameworks?
What if the UITableViewCell at some IndexPath is removed from the memory as it's not visible and the observable's value is changed will it result in crash as the UITextField at that IndexPath will return nil?
A UITextField is an input element. You don't need a two way binding to it because you shouldn't be dynamically changing it. The most you should do is initialize it and you don't need a binding for that.
You don't mention what the final output will be for this input so the answer may be different than the below. This particular solution assumes that you need to push all the answers as a group to a server or database. Maybe when a button is tapped.
There is a lot of code below, but it compiles as it stands (with the proper imports.) You can subscribe to ListViewModel.answers to see all the answers collected together.
class ViewController: UIViewController {
#IBOutlet weak var myTableView: UITableView!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let answersSubject = PublishSubject<(PaletteID, String)>()
let viewModel = ListViewModel(answersIn: answersSubject.asObservable())
viewModel.paletteViewModels
.bind(to: myTableView.rx.items(cellIdentifier: "Cell", cellType: MyCell.self)) { index, element, cell in
cell.answerTextField.text = element.initialAnswer
cell.answerTextField.rx.text.orEmpty
.map { (element.id, $0) }
.bind(to: answersSubject)
.disposed(by: cell.bag)
}
.disposed(by: bag)
}
}
class MyCell: UITableViewCell {
#IBOutlet weak var answerTextField: UITextField!
let bag = DisposeBag()
}
struct ListViewModel {
let paletteViewModels: Observable<[PaletteViewModel]>
let answers: Observable<[PaletteID: String]>
init(answersIn: Observable<(PaletteID, String)>) {
paletteViewModels = Observable.just([])
answers = answersIn
.scan(into: [PaletteID: String]()) { current, new in
current[new.0] = new.1
}
}
}
struct PaletteViewModel {
let id: PaletteID
let initialAnswer: String
}
struct PaletteID: RawRepresentable, Hashable {
let rawValue: String
}
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
So I have TableViewCell's that are being populated from 2 Dicitionaries in my view controller.
var categories : [Int : [String : Any]]!
var assignments : [Int: [String : Any]]!
I have a UiTextField in my cell that the user is supposed to be able to edit. I then want to be able to change the values of certain keys in that dictionary-based off what the user changes and re-display the table with those changes. My main problem is that I don't know how I will be able to access theese variables from within my cell. I have a method in my view controller that takes the row that the text field is in, along with the value of the textField, and updates the dictionaries. What I need is to be able to instantiate the view controller that the cell is in but I need the original instance that already has values loaded into the categories and assignments Dictionaries. If you have any other ideas on how I could accomplish this please post.
You can use delegate for sharing cell-data to your VC:
protocol YourCellDelegate() {
func pickedString(str: String)
}
class YourCell: UITableViewCell {
var delegate: YourCellDelegate! = nil
#IBOutlet weak var textField: UITextField!
.....//some set-method, where you can handle a text
func textHandle() {
guard let del = delegate else { return }
del.pickedString(textField.text)
}
.....
}
And usage in your VC: When you create cell, set its delegate self:
...
cell.delegate = self
...
and sure you VC supported your Delegate Protocol:
class YourVC: UIViewController, YourCellDelegate {
}
And now, you MUST implement protocol method:
class YourVC: UIViewController, YourCellDelegate {
....
func pickedString(str: String) {
}
....
}
All times, when you use textHandle() in your cell, pickedString(str: String) activates in yourVC with string from textField.
Enjoy!
I have created a tableviewcontroller, with a dynamic prototype cell. Within this view the user has an option to type in a review of a location from a button press as well as can rate the location on a scale of 1 to 10 with a UI Slider. The review is done with a UIAlertController within the tableviewcontroller - but the UISlider is within the cell itself. I am trying to save both pieces of data to core data within the tableviewcontroller. But the UISlider rating value is not available within the tableviewcontroller - is there a way to reference it in tableview from the cell or do I need to have two separate save functions? Here is some of my code thus far - within the tableview controller it doesn't recognize the variable assigned to the UISLider value in the prototype cell. Thanks in advance for any help!
In my tableviewcell:
class WineryInfoTableViewCell: UITableViewCell {
#IBOutlet weak var ratingLabel: UILabel!
#IBOutlet weak var sliderbutton: UISlider!
#IBAction func sliderValueChanged(sender: UISlider) {
var ratingValue = Float(sender.value)
var roundedRatingValue = roundf(ratingValue/0.5)*0.5
ratingLabel.text = "\(roundedRatingValue)"
}
in my tableviewcontroller
#IBAction func save() {
if let managedObjectContext = (UIApplication.sharedApplication().delegate as AppDelegate).managedObjectContext {
myRatingData = NSEntityDescription.insertNewObjectForEntityForName("WineryReview", inManagedObjectContext: managedObjectContext) as wineryReview
myRatingData.wineryName = wineryNames
//myRatingData.rating = ratingLabel.text how do I call this for saving?
myRatingData.review = myRatingEntry
var e:NSError?
if managedObjectContext.save(&e) != true {
println("insert error: \(e!.localizedDescription)")
return
}
}
// If all fields are correctly filled in, extract the field value
println("Winery: " + myRatingData.wineryName)
println("Review: " + myRatingData.review)
//println("Rating: " + ratingLabel.text!) how do I call this for saving?
}
I'm still new at Swift, but I think I may have an answer to a part of your dilemma. To reference the UISlider in your table cell, you can use 'tags' to get a reference to it.
For example:
let cell = tableView.dequeueReusableCellWithIdentifier(TableViewCellIdentifiers.someCell, forIndexPath: indexPath) as UITableViewCell
let slider = cell.viewWithTag(100) as UISlider
In the above example, I would have used assigned the UISlider with a tag of 100, so I am able to get a reference to it using the cell.viewWithTag(100).
Forgive me if this doesn't work!
You could pass a WineryReview object into the cell and set its rating directly from sliderValueChanged.
In cellForRowAtIndex:
cell.wineryReview = myReviewData
In TableViewCell:
#IBOutlet weak var ratingLabel: UILabel!
#IBOutlet weak var sliderbutton: UISlider!
var wineryReview: WineryReview?
#IBAction func sliderValueChanged(sender: UISlider) {
var ratingValue = Float(sender.value)
var roundedRatingValue = roundf(ratingValue/0.5)*0.5
wineryReview.rating = roundedRatingValue
ratingLabel.text = "\(roundedRatingValue)"
}
You could call save on the context from the TableViewController if you like. I did something very similar to this in a project.