How to create dynamic form in iOS? - ios

I have to create one dynamic form which contains question answer set and questions are different for every user. There are 5 questions out of this user have to answer at least 3 questions. In the set of 5 questions, the first user may get the set question which required 1 textfield 2 drops down and 2 radio buttons in the same way the second user may get 3 textfield 1 drop down and 1 radio button. What approach I should have to follow to achieve this?
I tried to create a table view. In the cell, I specified one label to render a question and one blank view which will be filled later on the basis where the question required text field or radio button or drop down. But with this case, I am not able to maintain which question I get answered because if I make textfield or checkbox's user interaction unable the didselectrowatindexpath method is not getting called even tried to mapping with delegate but this one is also not suitable for me because I have so many cases to manage as an answer field.

There might be different implementations. What I would do is:-
Distinguish the Question Types based on the UI(as per your case)
Add a questionType enum in QuestionModel class
Will create a different cell for each question type
Use that particular cell for each question type
Even in future any new Qustion Type has come, add one more case in enum and create one more cell
My Implementation will go something like this for QuestionModel class:-
Class Question {
enum Type {
case type1, type2 .....
func cellIdentifier() -> String {
switch self {
case type1:
return "type1"
//Handle all cases
}
}
}
var type: Type
}
Controller class will be something like this:-
Class Controller: UIViewController {
var questions = [Question]()
//Table view delegate method
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let questionAtIndexPath = questions[indexPath.row]
let cell = tableView.dequeReusableCell(withIdentifier: questionAtIndexPath.type.cellIdentifier(), for: indexPath)
cell.configureWith(questionAtIndexPath)
return cell
}
}

Well, the main task you have is to create a dynamic form where you have different objects depending on the required question. So, do this first:
Create individual tableCells like one containing a label for the question and a textfield for the answer or checkbox's to answer the question. The label will be common for every tableCell.
Connect the required delegates or IBOutlets with the objects
Create closure in table cells and handle the data when user completed answering the question.
Finally save the data collected by the closures call backs in TableViewcellforRowAtIndexpath and use them as per required.

Related

Pass the result to a Button within a UITableViewCell after executing an on tap closure?

I'm looking for any possible way of passing the result of an networking update to a button in a UITableViewCell as a closure.
I have some UITableViewCells that are products. In these cells, I have an 'Add to Cart' button. I set a buttonTap closure in my UITableViewCell cellforRowAtIndexPath method, setup a touch handler within the cell for the button, and when that handler is called, execute the buttonTap closure. I handle my cart updating on a cart object which lives on the main controller.
The result of the cart update action returns true if they can add more items to their cart. Then, I update the button accordingly. I like this approach because I don't have to deal with delegates and I can keep all of the cart logic itself far far away from the cell; the cell just knows how to make a button enabled/disabled/loading/etc.
/// Buttom tap callback callback.
public typealias Selection = () -> Bool
class MealTableViewCell: UITableViewCell {
var buttonTap: Selection?
// Runs when tapping the button
func didTapAdd() {
if let buttonBlock = buttonTap {
self.button.isLoading = true
// Simulate loading
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
self.button.enabled = buttonBlock()
self.button.isLoading = false
}
}
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = .... (fetch cell)
cell.buttonTap = {
// returns true if the user can add more items to their cart
return self.cart.update(product: product, quantity: 1)
}
}
My question is that currently, my cart is all local with no API calls. I'm currently switching the cart over to an API driven one, with network calls to add and remove items. That means I can no longer return a BOOL, or return at all, from my cart.update(product:,quantity:) method as it is now an async call.
So, I can do something like rewrite that method signature to be
self.cart.update(product: product, quantity: 1, success: { canAddMore in
// API call succeeded
}, failure: { error in
// fall failed
})
The question is that how can I pass canAddMore to the tableViewCell? If I redefine what Selection means to take in a block that takes a bool as a param, I can't pass that param in from the controller as it would only be passed in when the block is executed on the cell itself.
How can I do something like
cell.buttonTap = {
cell.buttonTap = {
self.cart.update(product: product, quantity: 1), success: { canAddMore in
// !!!! What can I call here to pass canAddMore to the cell.
}, failure: { error in
}
}
}
canAddMore can be any value really, a BOOL is just this example. My big goal is to avoid coupling any knowledge of the cell's loading and buttons to the controller itself. If I use delegates, I would have to have a two way delegate makes the cell a delegate of the controller, and I've always felt that's the sort of wrong direction to approach this. I'm not positive it's really possible to pass the result of a closure back to the cell, but I am hoping there is!
EDIT: The big question I'm really trying to answer is if it is at all possible to pass data back to the cell (or any object) that originally called closure through that closure. There's a million ways to do data modifications in a table view, but that's sort of the main thing I'm trying to address.
I'm also "looking" to avoid storing the canAddMore state (e.g. the quantity remaining for that product) in the main 'products' array that powers the tableview. The initial state is set there, returned from a /products endpoint, but after that, inventory being available or not is returned by the carts API action.
I don't think you want to do what you think you want to do :)
In a nutshell, instead of trying to "talk back" to the cell that called the closure, you probably want to track the "canAddMore" state of each product in your Products data array, and then update the table row(s) when the state changes. So...
User taps "Add to Cart"
Give visual feedback in that row to show that you are processing the tap (gray out the button, or show a spinner, whatever looks good)
Call back to the closure to start the Add-to-cart API call
When the API call returns, update your local Products data to indicate the "canAddMore" state
reload the row(s) in the table to update the Button (make it active, inactive, change the title, whatever)
You almost certainly need to be doing something similar anyway, so the Buttons in each row will be updated when the user scrolls and the cells are reused.
A general approach is to update the cell's content with tableView.reloadRows(at: [indexPath], with: true) in your callback. This will call func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell for the specified cell. Don't forget that this has to be done in the main queue.

Data exchange between UIViewControllers, Service and Views

Top brick problem I face every time is a passing data between component.
I can separate this problem in few sub-problems:
Do we need to pass data to views? Do we need to change model directly in view?
Can we use some service call directly from views, or we need to pass back all operation to UIViewConrtoller and only then controller will request appropriate service.
Specifying indexes instead of real models.
So the first question demonstrate case when we create UITableViewCell and pass to it data directly. So we now have ability to modify some properties of this data object. Let's say we have PlayListViewController that implement UITableView datasource. If you see bellow I set data model directly into view.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = viewController.theTableView.dequeueReusableCellWithIdentifier(String(SongTableViewCell.self)) as! SongTableViewCell
cell.song = songs[indexPath.row]
return cell
}
Second one for example, when you want to trigger something in your service directly from your SongTableViewCell without using any delegate calls. Let's say in SongTableViewCell I want to play my song and on this cell I have play button. So then it is a simple solution - I can bind cell with UIButton action touch up inside for example and invoke needed operation in my service:
#IBAction func onTappedPlayButton(sender: AnyObject)
{
MusicService.playSong(song)
}
But usually I do another things. I store delegate instance in my cell that pass back any action to a controller and controller decides what to do:
#IBAction func onTappedPlayButton(sender: AnyObject)
{
delegate?.didTappedPlayButton()
}
In this case SongTableViewCell looks like this:
class SongTableViewCell: UITabeViewCell
{
weak var delegate:SongTableViewCellDelegate?
...
#IBAction func onTappedPlayButton(sender: AnyObject)
{
delegate?.didTappedPlayButton(index)
}
}
and my view controller implements this didTappedPlayButton method where it calls MusicService.playSong(song). Here is 3rd problem if we have not pushed model object into UITableviewCell then we need to say somehow to view controller that it needs to play some appropriate song from array. So I use index that I set into the UITableviewCell which is sometimes can tangle other developers. I don't know if it's better to use index or data model. I understood advantage of changeability but index say nothing for developers and data model object says a lot.
I know it's more architecture questions, but maybe we can outline some props and cons of these 3 approaches/problems.
I found a post and it says
Classes=more complexity. Values=less complexity. Not sure it can fit all my subquestion, but maybe values it much better then transit entire class.

Change variable based on Table View Cell

I need an event, that changes a variable based on which TableViewCell I click. But unlike an action connected to a button, there is no action indicator for table view cells at all. So my question is:
I want to make a TableView that contains items of an array. Based on which item I click, I want to change my variable so that the result on the next ViewController depends on which button you click.
So to make things easier, here is an example what I want the app to look like:
On the first TableViewController I have a list based on an array and on the second ViewController I have a label that shows text based on the variable.
I have a nameArray = ["Mum", "Brother", "Me"] and a weightArray = [140, 160, 120] and a variable weight = 0. The label on the second ViewController tells the var weight. So when you click on "Mum" in the TableView I want the next ViewController to say 140, when I click on "Brother" then 160 and so on...
Until here everything works just fine and I have no problems with anything but changing the var based on what I click.
Long story, short sense:
I want an Action for the TableViewCell that changes the var like in an Action connected to a Button, but there is no Action outlet for Cells at all.
Use this method. Use indexPath.row to find what row number you selected
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var cell : UITableViewCell = tableView.cellForRowAtIndexPath(indexPath)!
switch cell.labelSubView.text as! String {
case "Mum":
self.weight = weightArray[0]
case "Brother"
self.weight = weightArray[1]
and so on..
..
default:
statements
}
}
Note A better alternative
I also considered a case where you have too many entries in nameArray and switch statement might not be good. In that case you can get the text inside the selected row by cell.labelSubView.text as! String
next you can check if the nameArray contains the cell text and get the index of the name that matches the cell text. Next you can get the required weight at the same index in weightArray. And then do self.weight = weightArray[requiredIndex]
Hope this helps.
Update : My experienced friend #Duncan mentioned down below that switch statement in this case is a bad coding practice . I am not going to delete it because it is a lesson for me and also my fellow programmers who are relatively new to programming. So i have put it in a yellow box, stating that it is not a good code
A better option for this would be :
As Duncan mentions, creating an array of dictionary is a good option
Second option is the option in my answer after my Note
You need to maintain array of dictionaries , those dictionaries have keys like "person", and "weight", then you can easily get weight value after selecting the cell by using table view delegate method UITableViewDelegate's tableView:didSelectRowAtIndexPath:
Create an instance variable in your view controller (a var at the top level after the class definition) for the selected cell.
class MyTableViewController: UIViewController
var selectedRow: Int
override func tableView(tableView: UITableView,
didSelectRowAtIndexPath indexPath: NSIndexPath)
{
selectedRow = indexPath.row
//invoke a segue if desired
performSegueWithIdentifier("someSegue");
}
override func prepareForSegue(segue: UIStoryboardSegue,
sender: AnyObject?)
{
if segue.identifier == "someSegue"
{
//Cast the destination view controller to the appropriate class
let destVC = DestVCClass(segue.destinationViewController)
destVC.selectedRow = selectedRow
}
}
As Andey says in his answer, it's probably better to create a single array of data objects (dictionaries, structs, or custom data objects). Then when the user taps a cell, instead of passing the index of the selected row to the next view controller, you could pass the whole data object to the destination view controller. Then the destination view controller could extract whatever data it needed. (Weight, in your example.)

How to pass variable reference between viewcontrollers? [duplicate]

This question already has answers here:
How do you share data between view controllers and other objects in Swift?
(9 answers)
Closed 7 years ago.
I am writing an application with a customizable color palette. I have a settings class (settings) which stores these colors, and a settings viewcontroller (themeVC) which lets the user modify them, and another to list the possible values (colorsVC).
I keep a reference to the settings in both view controllers:
let settings = Settings.sharedInstance
In the themeVC I list categories like background, text and so on in a tableview like this:
let cell = tableView.dequeueReusableCellWithIdentifier("right detail with disclosure", forIndexPath: indexPath) as UITableViewCell
switch indexPath.row {
case 0:
cell.textLabel?.text = "Backgrounds"
cell.detailTextLabel?.text = settings.backgroundColor.name()
case 1:
....
default:
cell.textLabel?.text = "Texts"
cell.detailTextLabel?.text = settings.textColor.name()
}
return cell
I am trying to pass the settings value to the colorsVC, but this way it is just copied:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if (segue.identifier == "showColorsViewController") {
let detailVC = segue.destinationViewController as! ColorsViewController
detailVC.setting = settings.backgroundColor
}
}
ColorsVC lets the user pick a color and then modifies the corresponding value in settings based on which row was selected in themeVC.
How can I pass a different value for every cell so the receiving colorsVC can read AND modify the corresponding setting?
I am looking for something like the inout keyword, but sadly I couldn't use it in this scenario.
Classes are reference types:
Unlike value types, reference types are not copied when they are assigned to a variable or constant, or when they are passed to a function. Rather than a copy, a reference to the same existing instance is used instead.
In the case above I think you would be best setting detailVC.setting to setting and then accessing/modifying the object properties on the object directly instead of passing the value of a class property, Does this help?
See Classes are Reference Types in the documentation
EDIT:
I believe you are using the colorsVC to set a color on the themeVC, so you segue to the colorsVC, select a color and this is returned to the themeVC table cell, correct?
If so there are a couple of ways to achieve this, I think the best way would be to pass the cell's row to the colorsVC so that it can update it as needed.

Working with Multiple Segues | How to use Cell data to perform specific Segues

I am displaying data in a collection view, I know how to pass the data on with prepareForSegue function but am trying to have the app determine which segue to use depending on the cell property data. (Each segue goes to a different view controller to display relevant information.)
For e.g.
If the cell.type is equal to "1" then perform segueOne if it is of type "2" then perform segueTwo.
I was trying to do something like this;
func collectionView(collectionView: UICollectionView, shouldSelectItemAtIndexPath indexPath: NSIndexPath) -> Bool {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! CollectionViewCell
if cell[indexPath].type = "1" {
performSegueWithIdentifier("showPage1", sender: self)
} else if self.cell[indexPath].type = "2" {
performSegueWithIdentifier("showPage2", sender: self)
} else { println("error when selecting cell to segue") }
}
However with this I get an error;
'CollectionViewCell' does not have a member named Subscript
Has anybody got any ideas ?
Assuming the items in your collection view can be re-arranged (or might be some time in the future), the indexPath will not be sufficient to give you the information which cell was selected. Thus, IMO your idea to give the cell a property is a feasible one.
The easiest "quick and dirty" way is to simply hardcode the segue identifier string into your cell. This is not the best design because you are introducing dependencies between app elements that should know of each other.
class MyCell : UICollectionViewCell {
var segue = "DefaultSegue"
}
Now calling the appropriate segue is really easy in didSelectItemAtIndexPath...
self.performSegueWithIdentifier(cell.segue, sender:cell)
It would of course be preferable to use an enum. Safer, more readable and better maintainable.
enum Segue : String {
case ToInfo = "SegueToInfo"
case ToLogin = "SegueToLogin"
// etc.
}
The ivar for MyCell would now be var : Segue = SomeDefaultValue and you can call it the same way.
BTW: Regarding your original question please note the following: as has been pointed out, you cannot subscript a cell. (UICollectionViewCell is not a Dictionary, so cell["key"] does not make sense.) Also, I am not a fan of dequeueing the cell in more than one place - instead you could call cellForItemAtIndexPath or do the work in that method in the first place, as I have suggested.
You're trying to index into a UICollectionViewCell, but of course that class is not an array, so you can't 'subscript' it.
My suggestion is to refactor your code. Whatever data you're storing in your cell you can presumably get from your data model, because that's where it originally came from. You are probably putting that in your cell in cellForIndexPath.
If that is the case, then there is no reason you can't get the same data from the same place in your func ... shouldSelectItemAtIndexPath ... -> Bool. I'd suggest doing it there. Your cell should only contain the data it needs to properly render itself to the screen.
See if that helps.

Resources