I am working on an app that uses TableView for showing feeds to users using ViewModel and my ViewModel contains a variable that contains data of all cells and ViewModel also contains other data as well, what I am doing is passing the whole ViewModel reference and indexPath to cell, here you can see:
func configureCell(feedsViewModelObj feedsViewModel: FeedsViewModel, cellIndexPath: IndexPath, presentingVC: UIViewController){
//Assigning on global variables
self.feedsViewModel = feedsViewModel
self.cellIndexPath = cellIndexPath
self.presentingVC = presentingVC
let postData = feedsViewModel.feedsData!.data[cellIndexPath.row]
//Populate
nameLabel.text = postData.userDetails.name
userImageView.sd_setImage(with: URL(string: postData.userDetails.photo), placeholderImage: UIImage(named: "profile-image-placeholder"))
updateTimeAgo()
postTextLabel.text = postData.description
upvoteBtn.setTitle(postData.totalBull.toString(), for: .normal)
upvoteBtn.setSelected(selected: postData.isClickedBull, isAnimated: false)
downvoteBtn.setSelected(selected: postData.isClickedBear, isAnimated: false)
downvoteBtn.setTitle(postData.totalBear.toString(), for: .normal)
commentbtn.setTitle(postData.totalComments.toString(), for: .normal)
optionsBtn.isHidden = !(postData.canEdit && postData.canDelete)
populateMedia(mediaData: postData.files)
}
so, is it the right or good way to pass full ViewModel reference and index to cell, and then each cell access its data from the data array? thanks.
The accepted solution is good, but not great.
This method's logic in particular needs to be improved:
func initialiseOutlet(_feedsData: feedsData) {
nameLabel.text = _feedsData.userDetails.name
userImageView.sd_setImage(with: URL(string: _feedsData.userDetails.photo), placeholderImage: UIImage(named: "profile-image-placeholder"))
updateTimeAgo()
postTextLabel.text = _feedsData.description
upvoteBtn.setTitle(_feedsData.totalBull.toString(), for: .normal)
upvoteBtn.setSelected(selected: _feedsData.isClickedBull, isAnimated: false)
downvoteBtn.setSelected(selected: _feedsData.isClickedBear, isAnimated: false)
downvoteBtn.setTitle(_feedsData.totalBear.toString(), for: .normal)
commentbtn.setTitle(_feedsData.totalComments.toString(), for: .normal)
optionsBtn.isHidden = !(_feedsData.canEdit && postData.canDelete)
}
to something like this:
func configure(with viewModel: PostCellViewModel) {
nameLabel.text = viewModel.username
userImageView.sd_setImage(with: viewModel.userPhotoURL, placeholderImage: UIImage(named: "profile-image-placeholder"))
updateTimeAgo()
postTextLabel.text = viewModel.description
upvoteBtn.setTitle(viewModel.totalBull, for: .normal)
upvoteBtn.setSelected(selected: viewModel.isClickedBull, isAnimated: false)
downvoteBtn.setSelected(selected: viewModel.isClickedBear, isAnimated: false)
downvoteBtn.setTitle(viewModel.totalBear, for: .normal)
commentbtn.setTitle(viewModel.totalComments, for: .normal)
optionsBtn.isHidden = viewModel.isHidden
}
You are currently referencing postData and _feedsData (part of the Model) from Table View Cell - which is technically incorrect in the context of MVVM paradigm since View would have direct dependencies of Model...
Note that PostCellViewModel is the ViewModel struct (or class) you have to implement and it should look like this:
struct PostCellViewModel {
private(set) var nameLabel: String
private(set) var userImageURL: URL?
// ...
private(set) var postDescription: String
private(set) var isHidden: Bool
init(model: FeedItem) {
nameLabel = model.userDetails.name
userImageURL = URL(string: model.userDetails.photo)
// ...
postDescription = model.description
isHidden = !(model.canEdit && model.post.canDelete)
}
}
Depending on the project/team/coding standards, you may want to also use a protocol:
protocol PostCellViewModelType {
var nameLabel: String { get }
var userImageURL: URL? { get }
// ...
var postDescription: String { get }
var isHidden: Bool { get }
init(model: FeedItem)
}
And then implement it:
struct PostCellViewModel: PostCellViewModelType {
private(set) var nameLabel: String
private(set) var userImageURL: URL?
// ...
private(set) var postDescription: String
private(set) var isHidden: Bool
init(model: FeedItem) {
// ...
}
}
Also note that sd_setImage uses a library/pod/dependency, which on its turn uses functionality of the Networking/Service Layer. So probably it's better not to make Cell/View dependent on it.
For cells' images in particular - you can add those calls inside cellForRow(at:), even if the method is implemented inside a dedicated UITableViewDatasource subclass and not inside the UIViewController directly.
For the UITableViewDatasource subclass, which is technically some controller/mediator type (since it depends on View and Model or ViewModel) - it's ok to interact with dependencies from other layers (Networking in case of image downloads). Views/Cells should care less if the image is to be downloaded or to to be fetched from local cache.
In general, if the images are too big and you want to implement a scalable architecture - you may want to create a custom ImageLoader class to take care of loading images only when needed, as well as canceling remote image requests if the cell disappears while the image download is in progress.
Have a look here for such a solution:
https://www.donnywals.com/efficiently-loading-images-in-table-views-and-collection-views/
Also see how Apple recommends to implement a solution for a similar use-case:
https://developer.apple.com/documentation/uikit/views_and_controls/table_views/asynchronously_loading_images_into_table_and_collection_views
*Passing whole ViewModel reference and indexPath to cell is not necessary. Call back after receiving data:
ViewController -> ViewModel -> TableViewDatasource -> TableViewCell.*
ViewController
class ViewController: UIViewController {
var viewModel: ViewModel?
override func viewDidLoad() {
super.viewDidLoad()
TaxiDetailsViewModelCall()
}
func TaxiDetailsViewModelCall() {
viewModel = ViewModel()
viewModel?.fetchFeedsData(completion: {
self?.tableViewDatasource = TableViewDatasource(_feedsData:modelview?.feedsData ?? [FeedsData]())
DispatchQueue.main.async {
self.tableView.dataSource = self.tableViewDatasource
self.tableView.reloadData()
}
})
}
}
View Model
class ViewModel {
var feedsData = [FeedsData]()
func fetchFeedsData(completion: () -> ()) {
let _manager = NetworkManager()
_manager.networkRequest(_url: url, _modelType: FeedsData.self, _sucessData: { data in
self.feedsData.accept(data)
completion()
})
}
}
TableView Datasource
class TableViewDatasource: NSObject,UITableViewDataSource {
var feedsData: [FeedsData]?
init(_feedsData: [FeedsData]) {
feedsData = _feedsData
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return feedsData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withReuseIdentifier: "TableViewCellName", for: indexPath) as? TableViewViewCell else {
return TableViewViewCell()
}
cell.initialiseOutlet(_feedsData: feedsData[indexPath.row])
return cell
}
}
TableView Cell
class TableViewCell: UITableViewCell {
#IBOutlet weak var nameLabel : UILabel!
#IBOutlet weak var userImageView : UIImageView!
#IBOutlet weak var postTextLabel : UILabel!
#IBOutlet weak var upvoteBtn : UIButton!
#IBOutlet weak var downvoteBtn : UIButton!
#IBOutlet weak var commentbtn : UIButton!
#IBOutlet weak var optionsBtn : UIButton!
override func awakeFromNib() {
super.awakeFromNib()
}
/*
Passing feedsData Object from TableViewDatasource
*/
func initialiseOutlet(_feedsData: feedsData) {
nameLabel.text = _feedsData.userDetails.name
userImageView.sd_setImage(with: URL(string: _feedsData.userDetails.photo), placeholderImage: UIImage(named: "profile-image-placeholder"))
updateTimeAgo()
postTextLabel.text = _feedsData.description
upvoteBtn.setTitle(_feedsData.totalBull.toString(), for: .normal)
upvoteBtn.setSelected(selected: _feedsData.isClickedBull, isAnimated: false)
downvoteBtn.setSelected(selected: _feedsData.isClickedBear, isAnimated: false)
downvoteBtn.setTitle(_feedsData.totalBear.toString(), for: .normal)
commentbtn.setTitle(_feedsData.totalComments.toString(), for: .normal)
optionsBtn.isHidden = !(_feedsData.canEdit && postData.canDelete)
}
}
Related
I'm a Swift beginner and I'm trying to make a simple app for ordering food. The user could add a new order by setting food name, price and serving. After adding an order, that order will be shown on the tableView as a FoodTableViewCell, and the user could change the serving with an UIStepper called stepper in each cell. Each order is a FoodItem stored in an array called foodList, and you can see all orders listed in a tableView in ShoppingListVC.
My problem is: When I press "+" or "-" button on stepper, my servingLabel doesn't change to corresponding value. I tried to use NotificationCenter to pass serving value to stepper, and store new value back to food.serving after stepperValueChanged with delegate pattern. However, there still seems to be some bugs. I've been kind of confused after browsing lots of solutions on the Internet. Any help is appreciated.
Update
I removed NotificationCenter and addTarget related methods as #Tarun Tyagi 's suggestion. Now my UIStepper value turns back to 1 whereas the servingLabels are showing different numbers of serving. Since NotificationCenter doesn't help, how can I connect the label and stepper value together? Is it recommended to implement another delegate?
Here are my codes(Updated on July 8):
FoodItem
class FoodItem: Equatable {
static func == (lhs: FoodItem, rhs: FoodItem) -> Bool {
return lhs === rhs
}
var name: String
var price: Int
var serving: Int
var foodID: String
init(name: String, price: Int, serving: Int) {
self.name = name
self.price = price
self.serving = serving
self.foodID = UUID().uuidString
}
}
ViewController
import UIKit
class ShoppingListVC: UIViewController, UITableViewDataSource, UITableViewDelegate {
var foodList = [FoodItem]()
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
...
for i in 1...5 {
let testItem = FoodItem(name: "Food\(i)", price: Int.random(in: 60...100), serving: Int.random(in: 1...10))
self.foodList.append(testItem)
}
}
// MARK: - Table view data source
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "foodCell", for: indexPath) as! FoodTableViewCell
let food = foodList[indexPath.row]
cell.nameLabel.text = food.name
cell.priceLabel.text = "$\(String(food.price)) / serving"
cell.servingLabel.text = "\(String(food.serving)) serving"
cell.stepper.tag = indexPath.row
cell.delegate = self
return cell
}
}
// MARK: - FoodTableViewCellDelegate Method.
extension ShoppingListVC: FoodTableViewCellDelegate {
func stepper(_ stepper: UIStepper, at index: Int, didChangeValueTo newValue: Double) {
let indexPath = IndexPath(item: index, section: 0)
guard let cell = tableView.cellForRow(at: indexPath) as? FoodTableViewCell else { return }
let foodToBeUpdated = foodList[indexPath.row]
print("foodToBeUpdated.serving: \(foodToBeUpdated.serving)")
foodToBeUpdated.serving = Int(newValue)
print("Value changed in VC: \(newValue)")
cell.servingLabel.text = "\(String(format: "%.0f", newValue)) serving"
}
}
TableViewCell
import UIKit
protocol FoodTableViewCellDelegate: AnyObject {
func stepper(_ stepper: UIStepper, at index: Int, didChangeValueTo newValue: Double)
}
class FoodTableViewCell: UITableViewCell {
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var priceLabel: UILabel!
#IBOutlet weak var servingLabel: UILabel!
#IBOutlet weak var stepper: UIStepper!
weak var delegate: FoodTableViewCellDelegate?
#IBAction func stepperValueChanged(_ sender: UIStepper) {
sender.minimumValue = 1
servingLabel.text = "\(String(format: "%.0f", sender.value)) serving"
// Pass the new value to ShoppingListVC and notify which cell to update using tag.
print("sender.value: \(sender.value)")
delegate?.stepper(stepper, at: stepper.tag, didChangeValueTo: sender.value)
}
override func awakeFromNib() {
super.awakeFromNib()
print(stepper.value)
}
}
Initially FoodTableViewCell is the ONLY target for UIStepper value changed (looking at #IBAction inside FoodTableViewCell).
When you dequeue a cell to display on screen, you call -
cell.stepper.addTarget(self, action: #selector(stepperValueChanged(_:)), for: .valueChanged)
which causes your ShoppingListVC instance to be added as an additional target every time a cellForRow call is executed.
Things to fix :
Remove all of your NotificationCenter related code from both classes.
Remove cell.stepper.addTarget() line as well.
This would give you a better idea of why it is happening this way. Update your question with these changes in case you still don't have what you want.
UPDATE
// Inside cellForRow
cell.stepper.value = food.serving
Cell Config:
protocol FoodTableViewCellDelegate: AnyObject {
func stepper(sender: FoodTableViewCell)
}
#IBAction func stepperButtonTapped(sender: UIStepper) {
delegate?.stepperButton(sender: self)
stepperLabel.text = "\(Int(countStepper.value))"
}
Controller Config:
cellForRow:
cell.countStepper.value = Double(foodList[indexPath.row].serving);
cell.stepperLabel.text = "\(Int(cell.countStepper.value))"
Delegate Method:
func stepperButton(sender: FoodTableViewCell) {
if let indexPath = tableView.indexPath(for: sender){
print(indexPath)
foodList[sender.tag].serving = Int(sender.countStepper.value)
}
}
Please check value stepper pod it will help you: Value stepper
Integrate value stepper pod and use below code for basic implementation.
import ValueStepper
let valueStepper: ValueStepper = {
let stepper = ValueStepper()
stepper.tintColor = .whiteColor()
stepper.minimumValue = 0
stepper.maximumValue = 1000
stepper.stepValue = 100
return stepper
}()
override func viewDidLoad() {
super.viewDidLoad()
valueStepper.addTarget(self, action: "valueChanged:", forControlEvents: .ValueChanged)
}
#IBAction func valueChanged1(sender: ValueStepper) {
// Use sender.value to do whatever you want
}
Its simplify custom stepper implantation.Take outlet of value stepper view in table tableview and use it.
I have a custom view(tagListView) inside a custom tableview cell.
When I call addTag to cell.tagListView inside "cellForRowAt", it adds a tag for every cell.
How do I add tag only for that cell? I tried to keep a count so I only add tags to those which don't have tags. But it appears that this count is the same for all cells? I know this has something to do with reusable cell.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = topicListTableView.dequeueReusableCell(withIdentifier: "TopicListTableCell", for: indexPath)
if let myCell = cell as? TopicListTableCell {
if let model=topicListModel{
let t=model.topics[(indexPath.row)]
if let subject=t.subject{
let (tags,title)=util().getTag(subject: subject)
myCell.subject.text=title
if(myCell.tagCount==0){
myCell.tagList.addTags(tags)
myCell.tagCount+=1
}
}
myCell.content.text=t.content
myCell.authorTime.text=t.author
if let replynum=t.replies{
myCell.commentNum.text=String(replynum)
}
if let upvoteNum=t.score{
myCell.upVote.text=String(upvoteNum)
}
if indexPath.row%2==1{
myCell.background.backgroundColor=util().lightyellow
}else{
myCell.background.backgroundColor=util().darkeryellow
}
}
}
return cell
}
code for cell:
class TopicListTableCell: UITableViewCell{
#IBOutlet weak var content: UILabel!
#IBOutlet weak var upVote: UILabel!
#IBOutlet weak var commentNum: UILabel!
#IBOutlet weak var subject: UILabel!
#IBOutlet weak var authorTime: UILabel!
#IBOutlet weak var background: UIView!
#IBOutlet weak var tagList: TagListView!
var tagCount = 0
}
As dfd already stated, your approach isn't the right one because of the way UITableViews are working in iOS.
You should have one dataSource for your tableView (some array or dictionary) which contains all the information needed to present the cells the way you want.
So I would make a struct which contains all information I need to fill one cell and then make an array of these structs to fill all cells with the information they need.
Something like this:
// In your model:
struct TopicItem {
let topic: Topic
let tags: [Tag]
let title: String
}
var topicItems = [TopicItem]()
// This is function is only a draft, so you get a better idea what i mean
func updateTopicItems() {
topicItems.removeAll()
for topic in topics {
let tags: [Tag]
let title: String
if let subject = topic.subject {
// I would refactor this function, it is called getTag() but it returns tags and title. Kinda confusing
(tags,title) = util().getTag(subject: subject)
} else {
tags = [Tag]()
title = ""
}
topicItems.append(TopicItem(topic: topic, tags: tags, title: title))
}
}
I also have added some refactoring and comments which will hopefully help you to keep your code clean and maintainable in the future :)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// cell can be force unwrapped, it will never be something different than a TopicListTableCell, unless you change your code
// and even then you want a crash, so you find the error fast (crash will only occur during development)
let cell = topicListTableView.dequeueReusableCell(withIdentifier: "TopicListTableCell", for: indexPath) as! TopicListTableCell
guard let model = topicListModel else { return cell } // If there is no model, there is nothing to do
let topicItem = model.topicItems[(indexPath.row)]
let t = topicItem.topic
cell.subject.text = topicItem.title
// You should replace the addTags() function with a setTags() function, otherwise you will get more and more tags every time you present the cell
cell.tagList.addTags(topicItem.tags)
cell.content.text = t.content
cell.authorTime.text = t.author
if let replynum = t.replies {
cell.commentNum.text = String(replynum)
} else {
// Because cells are getting reused, you need to always set all fields, otherwise you get cells with content from other cells
cell.commentNum.text = ""
}
if let upvoteNum = t.score {
cell.upVote.text = String(upvoteNum)
} else {
// Again always set all fields
cell.upVote.text = ""
}
if indexPath.row %2 == 1 {
cell.background.backgroundColor=util().lightyellow
} else {
cell.background.backgroundColor=util().darkeryellow
}
return cell
}
I am new to iOS and Swift.
I am not able to get the data which I manually entered in multiple UITextFiled present in a cell in my UICollectionView
I want to get the data from each text field continuously as soon as the user starts editing the text and then push it inside the variable in the object.
Cells Sample - There will be multiple cells
For example, the image provided in the above link is a sample of a cell, this cell holds multiple textfields, labels and buttons
Now I want to get all the data from each cell and store it in an array of object
My ViewController
extension SampleViewController:UICollectionViewDelegate,UICollectionViewDataSource, UICollectionViewDelegateFlowLayout{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell=collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! SampleCollectionViewCell;
return cell;
}
}
My model object class
class Singleton{
static let shared = Singleton()
var list = [CellFields].self
}
class CellFields {
var button1:bool
var button2:bool
var dropdown:String
var field1:String
var field2:String
var field3:String
var label1:String
var label2:String
}
My UICollectionViewCell
class MySampleCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var dropdown: DropDown!
#IBOutlet weak var field1: UITextField!
#IBOutlet weak var field2: UITextField!
#IBOutlet weak var field3: UITextField!
#IBOutlet weak var label1: UILabel!
#IBOutlet weak var label2: UILabel!
#IBOutlet weak var button1: UIButton!
#IBOutlet weak var button2: UIButton!
}
I have tried all the delegate methods yet still I am not able to achieve what I want.
The result I want to achieve looks something similar to this
Singleton.shared.list
[0]
dropdown = "Dropdown"
field1 = "Something"
field2 = "Random"
field3 = "Another"
label1 = "Label"
label2 = "Label2"
button1 = true
button2 = false
[1]
dropdown = "Dropdown1"
field1 = "Something1"
field2 = "Random2"
field3 = "Another3"
label1 = "Label4"
label2 = "Label3"
button1 = false
button2 = true
...
...
...
Create something similar to the following:
// create your own delegate type for the cell
protocol MyCellDelegate {
func myTextFieldChanged(_ tf: UITextField) // call when the textfield changes
func myOtherTextFieldChanged(_ tf: UITextField) // call when the other textfield changes
func myToggleChanged(_ sw: UISwitch) // call when the switch changes
}
class MyCell: UICollectionViewCell {
#IBOutlet private var myTextField: UITextField!
#IBOutlet private var myOtherTextField: UITextField!
#IBOutlet private var myToggle: UISwitch!
private var _delegate: MyCellDelegate? // instance of above protocol type, this will generally be your VC
func initialize(withDelegate delegate: MyCellDelegate) {
myTextField.delegate = self
myOtherTextField.delegate = self
self._delegate = delegate // a textfield uses a delegate pattern
self.myToggle.addTarget(self, action: #selector(toggleValueChanged(_:)), for: .valueChanged) // a switch uses this target/selector pattern
}
}
// I like putting delegate implementations in extensions
extension MyCell: UITextFieldDelegate {
// called when a textfield changes
func textFieldDidEndEditing(_ textField: UITextField) {
if textField == myTextField {
_delegate?.myTextFieldChanged(textField) // call the protocol's method to signal the VC of the change
}
if textField == myOtherTextField {
_delegate?.myOtherTextFieldChanged(textField) // call the protocol's method to signal the VC of the change
}
}
}
extension MyCell {
// #objc is required for the target/selector pattern
#objc func toggleValueChanged(_ toggle: UISwitch) {
if toggle == myToggle {
_delegate?.myToggleChanged(toggle)
}
}
}
Then in your VC's cellForItemAt:
let cell=collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! SampleCollectionViewCell;
cell.initialize(withDelegate: self)
return cell;
And also in your VC, make it a MyCellDelegate with:
extension SampleViewController: MyCellDelegate{
func myTextFieldChanged(_ tf: UITextField) {
// save new textfield value
}
func myOtherTextFieldChanged(_ tf: UITextField) {
// save new other textfield value
}
func myToggleChanged(_ sw: UISwitch) {
// save new toggle value
}
}
Ideally, you would create a single method that updates your entire form at once, but that really depends on what kind of data you have and what is optional and whatnot, I'll leave that as a challenge. But at least from this you should be able to get your form working and understand what is going on with all the delegate stuff.
You don't need a singleton to create an array of class objects:
let classFieldSets: [ClassFields] = []
Create an init method in your model class:
Pass them to the cell via cellForItemAt in your SampleViewController extension.
I can't seem to make my custom collectionView Cell visible. I know that the CollectionView shows up because I changed the background color on the attributes inspector, but the custom cell is not showing up. I set the constraints in the attributes inspector and have a horizontal stack view under the image.
I followed the direction from a Udemy course exactly but it doesn't seem to work. This is how I have configured the cell to look:
The code is below:
import UIKit
class ListVC: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
#IBOutlet weak var productsCollection: UICollectionView!
private(set) public var products = [Product]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
productsCollection.dataSource = self
productsCollection.delegate = self
}
func initProducts(Product: Product) {
products = DataService.instance.getGroceryOptions()
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return products.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ProductCell", for: indexPath) as? ProductCell {
let product = products[indexPath.row]
cell.updateViews(product: product)
return cell
}
return ProductCell()
}
}
Here is the ProductCell implementation:
class ProductCell: UICollectionViewCell {
#IBOutlet weak var itemImage: UIImageView!
#IBOutlet weak var productName: UILabel!
#IBOutlet weak var productPrice: UILabel!
#IBOutlet weak var calCount: UILabel!
func updateViews(product: Product){
itemImage.image = UIImage(named: product.imageName)
productName.text = product.title
calCount.text = product.cal
productPrice.text = product.price
}
}
Here is the DataService implementation:
class DataService {
static let instance = DataService()
private let item = [
Product(title: "Eggs", price: "4.94", imageName: "eggs.jpeg", cal: "340"),
]
private let Groceries = [Product]()
func getGroceryOptions() -> [Product] {
return Products
}
}
Here is the Product implementation:
struct Product {
private(set) public var title: String
private(set) public var price: String
private(set) public var imageName: String
private(set) public var cal: String
init(title: String, price: String, imageName: String, cal: String) {
self.title = title
self.price = price
self.imageName = imageName
self.cal = cal
}
}
1) Your initProducts method is not called anywhere in the code.
2) You have to call collectionView.reloadData() after you update your data source. Which means, you have to call this method after updating products property.
3) Also, as other people in their answers mentioned, seems that your getGroceryOptions() method should actually return items instead of Products (what's that, btw?).
4) Please, follow swift style guide for writing your code. It will be easier for you to distinguish classes/struct from variables. You can learn more about it here and also here.
Your initProducts method is not called. Because of this products array stays empty and you see no cells in your collection.
You should call initProducts (e.g. in viewDidLoad) to fill products array with elements which will be shown in your collection.
Also consider removing parameters from this function (you're not using parameter within this method, so you don't need it there). It should look like:
func initProducts() {
products = DataService.instance.getGroceryOptions()
}
Plus you should correct getGroceryOptions method. It should return item as was pointed out in comments.
Do you have a numberOfSectionsInCollectionView section that goes before the other collectionView functions?
Take a look at this example:
func numberOfSectionsInCollectionView(collectionView:UICollectionView) -> Int {
return 1
}
Although I've found similar questions asked, I'm not able to comprehend the answers for it.
How would we read the changes in UISwitch or for that matter any element while in a UITableViewCell? Tried using a protocol, but the custom cell class complains about no initialisers. Used, a delegate, which seems to not conform to the view controller.
protocol SwitchTableViewCellDelegate {
func didChangeSwitchValue(value: Bool)
}
class SwitchTableViewCell: UITableViewCell {
var delegate: SwitchTableViewCellDelegate
var value: Bool = true
#IBOutlet weak var switchCellLabel: UILabel!
#IBOutlet weak var switchCellSwitch: UISwitch!
#IBAction func changedSwitchValue(sender: UISwitch) {
self.value = sender.on
delegate.didChangeSwitchValue(value)
}
In cellForRowAtIndexPath,
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! SwitchTableViewCell
cell.delegate = self
cell.switchCellLabel?.text = "Show Cloud Music"
cell.switchCellSwitch.on = userDefaults.boolForKey(cloudMusicKey)
Any suggestions, on how to implement this?
I would suggest using a Swift closure for this. Use the following code in your cell class:
class SwitchTableViewCell: UITableViewCell {
var callback: ((switch: UISwitch) -> Void)?
var value: Bool = true
#IBOutlet weak var switchCellLabel: UILabel!
#IBOutlet weak var switchCellSwitch: UISwitch!
#IBAction func changedSwitchValue(sender: UISwitch) {
self.value = sender.on
callback?(switch: sender)
}
Then this code in your cellForRowAtIndexPath:
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! SwitchTableViewCell
cell.callback = { (switch) -> Void in
// DO stuff here.
}
cell.switchCellLabel?.text = "Show Cloud Music"
cell.switchCellSwitch.on = userDefaults.boolForKey(cloudMusicKey)
Firstly, since there can be many cells sharing the same delegate, the delegate should know which cell calls it. Hence, your protocol method should provide the cell itself, not just its switch value. In fact, we can omit the switch value parameter since it can be queried from the cell.
protocol SwitchTableViewCellDelegate {
func switchTableViewCellDidChangeSwitchValue(cell: SwitchTableViewCell)
}
In your delegate's implementation of the protocol method, you can access the switch value like this:
func switchTableViewCellDidChangeSwitchValue(cell: SwitchTableViewCell) {
let switchValue = cell.value
}
Secondly, the delegate property can be nil so its type must be an Optional.
var delegate: SwitchTableViewCellDelegate?
To call the delegate when value changes:
delegate?.switchTableViewCellDidChangeSwitchValue(self)