In MVC pattern, should a View contain the Model it is presenting?
For example, I have an Item model, ItemsListView & ItemRowView to display it.
In order to display the list, should I pass the Item model to ItemRowView (1) or I can pass Item.title, Item.details, Item.image, etc to the ItemRowView (2)
In (1), it is violate the MVC design pattern where View does not talk directly to the Model, and the View can also call Model methods
In (2), when make the View dummy, If in another ViewController, we use the ItemsList a gain, we have to duplicate the passing parameters again, and what happend if we need to pass one more properties to the user?
There another way, using ViewModel to wrap around the Model to handle UI Logic, but that will create many classes, and everytime I get a list of models, I also have to map it to ViewModels
example:
class ItemModel {
var name: String = ""
var price: Double = 9000
}
Should I assign the model to UITableViewCell like this: (so the cell can update the data itself whenever new data come)
class ItemTableViewCell: UITableViewCell {
#IBOutlet var titleLabel: UILabel!
var item: ItemModel! {
didSet {
self.updateUI()
}
}
func updateUI() {
titleLabel.text = item.name
}
}
Or should I keep the cell dummy, and do the update UI in ViewController:
class ItemTableViewCell: UITableViewCell {
#IBOutlet var titleLabel: UILabel!
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: ItemTableViewCell = tableView.dequeueReusableCell(withIdentifier: "ItemTableViewCell", for: indexPath)
var item = items[indexPath.row]
cell.titleLabel.text = item.name
return cell
}
Yes. As it enables you to use the base methods of the model itself instead of creating wrapper classes and handling conversion based on design change.
The Rule of thumb nowadays is to have a view-model for each view if that view requires more than one object.This allows you to handle design and data changes without touching the controller.
Related
I am going try be as specific as possible.
What I am trying to achieve is to have one view controller which has a collection view in it and when the user clicks on of the collection view cells, it then sends the user to another view controller which has another collection view however the items displayed in the second collection view will change depending on which collection view cell the user previously tapped. I am also using CoreData to do this. With CoreData, the task is an entity and has attributes like name etc. Will I have to change this or have a relationship between the 2 collection views?
The reason for me wanting to do this is because I am creating a productivity app for iOS and the first view controller with a collection view will be where the user can create projects, within these projects which will be displayed in a collection view, the user can then tap on one of the cells and go to the next view controller and begin to create tasks specific to that project.
How can I keep the tasks stored in a specific collection view cell and have the user create different tasks in other projects. It is sort of like Wunderlist. If anybody is confused at what I am trying to do, I can ellaborate more.
This is one of my view controllers where the data from the 'create item view controller' gets sent to via CoreData and displays it in a collection view, hope it helps:
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
#IBOutlet weak var myCollView: UICollectionView!
#IBAction func addCore(_ sender: UIButton) {
}
var tasks : [Task] = []
override func viewDidLoad() {
super.viewDidLoad()
self.myCollView.delegate = self
self.myCollView.dataSource = self
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
getData()
myCollView.reloadData()
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return tasks.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "connectCell", for: indexPath) as! MyCollectionViewCell
let task = tasks[indexPath.row]
cell.labelTe?.text = task.name!
self.myCollView.backgroundColor = UIColor.clear
cell.layer.cornerRadius = 25
return cell
}
func getData() {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
do {
tasks = try context.fetch(Task.fetchRequest())
}
catch {
print("Ahhhhhhhh")
}
}
If I am guessing right, and upon clicking a cell on the first collection view, you want to segue to another collection view, where you will display display data according to the selected cell.
In order to achieve this, simply add collectionView(_ collectionView:, didSelectItemAt indexPath:) and prepare(for segue:, sender:) functions in your first view controller.
In the collectionView(_ collectionView:, didSelectItemAt indexPath:) function get the id of the task selected by determine which cell was selected, then pass this id to the second view controller in prepare(for segue:, sender:) function.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! MyCollectionViewCell
id = cell.labelTe?.text
}
or
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
id = tasks[indexPath.row].id
}
Then pass this value in your prepare(for segue:, sender:) function to the second viewController.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "YourIdentifier"{
let destinationViewController = segue.destination as! SecondViewController
destinationViewController.id = id
}
}
Make sure to add a var id in both your viewControllers. In the second viewController, use this id to fetch the data of the selected task that you want to display.
Hope this helps.
EDIT
Simply declare a new variable id of your particular type in both the viewControllers. In your first viewController :
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
var id = ""
and in the second viewController
class secondViewController: UIViewController {
var id = ""
Just make sure that both the variable are of same type.
I'm just trying to send text to a UILabel on another ViewController via a segue:
let controller = segue.destinationViewController as! DetailViewViewController
controller.detailViewLabel.text = "Hello!"
But when the segue is performed, detailViewLabel is nil every time.
However, if send the string to a new var of type string, then assign detailViewLabel.text to the new string in viewDidLoad, it works fine.
For example, this works fine:
var detailViewLabelText: String!
#IBOutlet weak var detailViewLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
detailViewLabel.text = detailViewLabelText
// Do any additional setup after loading the view.
}
Why can't I directly send the string to the text property of the UILabel via a segue?
Outlets in view controllers aren't connected until the first time the view is loaded. Views are loaded when the view property of the UIViewController is accessed for the first time.
So, your code should work if you force the view to load before trying to set the text on the label, e.g.
let controller = segue.destinationViewController as! DetailViewViewController
let _ = controller.view
controller.detailViewLabel.text = "Hello!"
You can also just initialize the string that you're trying to send outside of the class, then it's available to all classes. That's not really professional but it works :)
I do something similar to Daniel Hall, but I set the data to a variable and then, once the view loads, I set the label.text to the variable.
I have the following situation:
two ViewControllers each containing a box that is to be colored to a color picked from a color well in ViewController
The colorwell is set as continuous in order to see the changes reflected immediately
I am looking for a way to continuously pass the color well value on to the SecondViewController and on to a callback method that will color a box in the SecondViewController.
I found that the prepareForSegue method is commonly used to pass data between view controllers, this however only occurs once during the transition and not continuously.
Can someone point me out in the right direction? Googled for hours but I got really stuck with this.
Thanks.
import Cocoa
class ViewController: NSViewController {
#IBOutlet weak var box: NSBox!
#IBOutlet weak var well: NSColorWell!
#IBAction func well(sender: AnyObject) {
box.fillColor = well.color
}
override func prepareForSegue(segue: NSStoryboardSegue, sender: AnyObject?) {
let second = segue.destinationController as! SecondViewController
second.representedObject = well.color
}
}
import Cocoa
class SecondViewController: NSViewController {
#IBOutlet weak var box: NSBox!
override func viewWillAppear() {
// Note that box.fillColor requires box type to be custom
box.fillColor = self.representedObject as! NSColor
}
}
The prepareForSegue method is a chance to create links between two view controllers. It's pretty common for the source view controller to set itself up as the delegate of the destination view controller. It's also possible for the source view controller to save a reference to the destination view controller for future reference.
If you define a protocol with a method like
func colorValueHasChanged(newColor: NSColor)
Then you can use it in the IBAction for your color well to pass information about changes in the color well from one view controller to the other.
i want to do the following thing:
1- Create objects from a class (with 3 variables)
2- Store those objects in a array
3- Create a tableview
4- The cell from the tableview must provide input from the first variable.
I got stuck on the last step. Can anybody help me?
var totalBooks = [AnyObject]() *// create an empty array*
var newBook = Book(setTitle: "booktitle", setWriter: "writer", setFile: "file") *// creates object from a class with 3 variables*
totalBooks.append(newBook) *// puts the object in the array*
*cell.textLabel?.text = totalBooks **???*** *// must put the first variable (setTitle) as output information.*
Thanks in advance
1. You should create the array like this:
let totalBooks = [Book]()
2. setTitle is not a good name of a property - use title
3. Set your totalBooks array as a data source for your table view by conforming to the UITableViewDataSource Protocol.
You can read about it here and try it yourself - UITableViewDataSource
4. In tableView(_:cellForRowAtIndexPath:) set the cell's title label for the corresponding item in your totalBooks data source.
Here is a solution.
Create a property in your view controller var myBooks = [Book]()
In viewDidLoad method
override func viewDidLoad() {
super.viewDidLoad()
for _ in 1...3 {
var newBook = Book(setTitle: "booktitle", setWriter: "writer", setFile: "file")
myBooks.append(newBook)
}
}
In cellForRowAtIndexPath delegate method
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "cell")
let book = self.myBooks[indexPath.row]
cell.label.text = book.title //or book.setTitle, whatever you have
return cell
}
Ok, first time I have ever had to ask for help on here, usually I can search and find my answer but not this time. I have a table that displays a pictures and names. If one of them are selected it goes to another view and the data passes. However, I am trying to get that passed information to display in a table like: Name: (Passed Info), Age: (Passed Info), Gender: (Passed Info) etc. I know the data passes because I can display the info in a label, but I can not figure out how to get it to show in a table. Index issue? String Issue?
This is code that passes the info:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var detailsViewController: DetailsViewController = segue.destinationViewController as DetailsViewController
var horseIndex = appsTableView!.indexPathForSelectedRow()!.row
var selectedHorse = self.horses[horseIndex]
detailsViewController.horse = selectedHorse
This is the code on the controller getting the data where I want the table to display
class DetailsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var tableView: UITableView!
#IBOutlet weak var titleLabel: UILabel!
var horse: Herd?
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.horse.count;
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:UITableViewCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as UITableViewCell
cell.textLabel?.text = self.horse[indexPath.Row]
return cell
}
}
I get an error on the return self.horse.count stating not a member and an error on the self.horse[indexPath.Row] stating NSIndex does not have a member named row.
I feel like I am not unwrapping it properly or something, but I can not figure it out or find an answer in my searches. If you need more information please let me know and thanks in advance for any help.
CLARIFYING...
You correctly grab self.horses[horseIndex] in the segue method, so you've already done the work to get the 1 specific horse. No need to do it again. Is self.horses tied to the Herd type? Confusing why Herd shows up again in the DetailView - you don't seem to need it.
It sounds like what you actually want at this point is a tabular layout of the details of that single horse ... Correct? Like going from your entire "Contacts" list to the tabular view of a single contact?
That no longer involves the array of multiple horses, so your use of Herd? and herd.count aren't necessary. Use static labels or a static tableView to show the info from the 1 Horse.
WHAT'S YOUR DATA STRUCTURE & WHAT GOES IN DETAIL VIEW?
Presumably what you want to create (if you haven't already) is a Horse Type:
class Horse {
//you could use optional properties here, or require them for a fully initialized Horse.
let name:String?
let gender:String?
let breed:String?
var age:Int?
var restingHeartRate:Int?
init(name:String?, gender:String?, breed:String?, age:Int?, restingHeartRate:Int?) {
//set arguments passed in to respective properties of self...
}
func winTripleCrown() {
println("\(name!) Wins! Suck on that, Sea Biscuit!")
}
}
Ensure your horses array is declared to only take Horse instances, and fill it appropriately:
var horses = [Horse]()
let horse1 = Horse(name:"Bob" gender:"male" breed: "Yessir, I am a real horse" age:42 restingHeartRate: 30)
horses.append(horse1)
println(horses.count) //prints "1"
horses[0].winTripleCrown() //prints: "Bob Wins! Suck on that, Sea Biscuit!"
Use the Horse Type instead of Herd in the DetailViewController:
var horse: Horse? //nil until set in prepareForSegue
Then in the segue method:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
...
detailsViewController.horse = selectedHorse
Now, assuming you only put properly created Horse instances into the initial array, and there IS one at the selectedIndex, you're guaranteed that the horse variable in DetailViewController is a Horse instance, and that it's the one selected in the 1st "overall" tableView.
CREATE THE DETAIL VIEW
EASY:
The easy solution at this point is to create a detailView layout with labels, images, etc and hook them up to #properties in the DetailViewController. Then map the Horse properties to the #IBOutlets. Done. No need to mess w/tableViews anymore - just fake it or use a scrollView to make it look like a table.
TABLEVIEW ROUTE
But if you want to use a UITableView, then you'd need the delegate methods like you're using... The difference is you need to look at the # of properties of the single Horse instance - not anything about the overall list of Horses in a Herd or total array.
You COULD hard-code the # of rows if you're certain the # and order of Horse properties will always be consistent:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5 //1 row per property in a `Horse` instance as defined above
}
However, if you want to account for more dynamic results (like an array of wins/losses, or a photo gallery of variable length) you'd need to have enough cells to map all the Horse properties. How can you do that with a custom type that isn't an array or dictionary?
In Swift, you can determine how many properties a Type has by creating a "Mirror" type and using it for introspection, to reflect info back to you like the count property missing above.
let horseMirror = reflect(self.horse)
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.horseMirror.count //should also return "5" but no longer hard-coded
}
And then in cellForRowAtIndexPath: you can switch on the indexPath to assign the various Horse properties to the labels (or images, etc) in the tableView cells. Create custom tableView cell types w/unique identifiers if you want to show different information, or stick w/built-in options:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:UITableViewCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as UITableViewCell
//haven't played w/this to see if the optionals are correct. Consider it psuedocode...
switch indexPath.row {
case 1: cell.textLabel.text = self.horse.name?
case 2: cell.textLabel.text = self.horse.gender?
case 3: cell.textLabel.text = self.horse.breed?
case 4: cell.textLabel.text = String(self.horse.age?)
case 5: cell.textLabel.text = String(self.horse.restingHeartRate?)
default: cell.textLabel.text = ""
}
return cell
}