I'm wanting to make a super simple app where I've got a table view and when you click on a cell you're taken to a detail page.
Normally when I've populated a tableview with data it's been from an outside source like an API and I've created my class instances as the information comes in.
If I were to create a "static" application where I've got 15 recipes that I'm wanting to display with information that I'm setting, is my code below really the way to do it?
var lineBreak = "\n"
class Recipie {
var ingredients = ""
var howTo = ""
init(ingredients: String, howTo:String) {
self.ingredients = ingredients
self.howTo = howTo
}
}
class ViewController: UIViewController {
#IBOutlet weak var ingredientsField: UITextView!
#IBOutlet weak var howToField: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
let cereal = Recipie(ingredients: "Milk\(lineBreak)Cereal\(lineBreak)Fruit", howTo: "Put it in the bowl, eat it")
ingredientsField.text = cereal.ingredients
howToField.text = cereal.howTo
}
}
From there I'd create an array of recipes and base my tableview off of the array and go through all those normal motions.
This seems hideous. I imagine on a larger if a small business asked me to create an application for them showcasing their meals/reviews that the same technique one would use to create this 10-recipe app would be used for that. I just have a hard time believing that I'm on the right track here with potentially creating 100 recipe items in my viewdidload like this. I don't see the need to use CoreData or anything with this, and I don't see how it'd change the functionality at all. This technically "works", it just seems very wrong.
Any suggestions/guidance is appreciated.
Related
I have an app I am working on where it shows a balance for users that changes. I would like to display these values as they change and the only way I know how to do that would be with a button. But the values often change on a different view controller so I was wondering if I can set labels equal to a variable that update along with those variables.
Since the values change outside of the view controllers with the labels, the normal way I change labels, using a button, does not apply.
Thanks in advance!
As a general solution, you could achieve this by declaring a property observer in your view controller, example:
class ViewController: UIViewController {
var updatedData = "" {
didSet {
lblData.text = "Data: \(updatedData)"
}
}
#IBOutlet weak var lblData: UILabel!
}
At this point, each time updatedData value is edited, the lblData label text will be updated.
Note that there is also willSet option which is called just before the value is stored, for more information, you could check Swift Properties Documentation - Property Observers.
I'm new to Swift and I find myself in the situation where I have a view controller that should manage 36 textfields and 15 labels (update with some math performed from the textfield).
Now my issue is that seems too much to connect all those outlets in the view controller.
I wanted to create a data model which will store into an Array the data and then perform the math and update the UI.
Like I said, I'm new to Swift and it seems I'm not able to create a model using outlets.
This is what I've done:
All the textfields and labels are contained in a subview inside the main view. So I've associated the subview with the class of the model (EnergyModel) which it already gives me an error right away.
Here's the code of my model:
class EnergyCalcModel {
#IBOutlet weak var lightstextfield1: UITextField!
#IBOutlet weak var lightstextfield2: UITextField!
private var _lights1: String
private var _lights2: String
var lights2: Double {
if lightstextfield2.text != nil {
_lights2 = (lightstextfield2.text!).doubleValue
}
return _lights2
}
var _lights1: String {
if lightstextfield1.text != nil {
_lights1 = (lightstextfield1.text!).doubleValue
}
return _lights1
}
init(lights1: Double, lights2: Double) {
self._lights1 = lights1
self._lights2 = lights2
}
}
and in my main VC:
var energyModel: EnergyCalcModel!
func calculate() {
label.text = energyModel.lights1 * energyModel.lights2
}
Could you please advise?
I'm not sure what you're concerned about "overloading". If it's memory you're concerned about, that memory is already used when you put all these UI elements on the screen. Keeping references to them in your view controller won't make things any worse. It might make the code ugly and repetitive, for example if you end up with instance variables named textField1 through textField36 instead of something descriptive.
If it makes sense for your app and for the purpose of this view, go ahead and include them all. There's nothing to "overload" in that regard.
Some things that might improve the code-- depending very heavily on how you use these text fields and labels-- might include:
Using a dynamic structure like a table view. If each table view cell has one label and one text field, you can have as many as you need easily. You can also have the number change depending on your data, if that makes sense for your needs.
Instead of using 36 different outlets, use an outlet collection (#IBOutletCollection) that contains all of them. Finding the right outlet in the array may require a little work, though, because you can't rely on the array order. You might do that using the tag property on the view, or you might sort it somehow.
Is it possible to create some sort of generic willSet closure to style IBOutlets in a view controller?
Currently I use this code to set the tint color of UIImageView once its set. ".primary" is a a static variable on UIColor which I created via a UIColor extension:
#IBOutlet weak var someImageView:UIImageView! {
willSet {
newValue.tintColor = .primary
}
}
Now I have a whole bunch of outlets which I want to style in the same way and for me that seems like a lot of duplicated code, especially if I start to apply more styling than just setting the tint so I want something more generic. I came up with:
let tintClosure: (_ newValue: UIView?) -> () = { $0?.tintColor = .primary }
#IBOutlet weak var someImageView:UIImageView! { willSet{ tintClosure(newValue) } }
Now I have to write the code for the actual tinting only once and can just call the closure in willSet. However, I wonder if there is an even more efficient way so I don't even have to call the closure with (newValue) by myself but just giving it the tintClosure.
Like if a function expects a completion handler and you have a function which fits its declaration and you just pass the name of the function instead of a closure in which you call the function.
Something really fancy like:
#IBOutlet weak var someImageView:UIImageView! { superDuperClosure(willSet) }
or
#IBOutlet weak var someImageView:UIImageView! { willSet{ crazyImplementationOfTintClosre } }
or
#IBOutlet weak var someImageView:UIImageView! { willSet.howIsThisEvenPossible }
or
#IBOutlet weak var someImageView:UIImageView! { nowWeAreTalking }
Well, maybe I'm just thinking over the top and I already found the most simplistic way of doing it. Maybe I don't have a deep enough understanding of how willSet works. So dear Swift gurus, teach me how to become a swifty developer.
Update
Based on the second suggestion from Paulo Mattos I had another idea. I created a outlet collection containing all the image views. Then I just iterate over all of them tinting each one with this nice little piece of code:
#IBOutlet var imageViewsToTint: [UIImageView]! { willSet { newValue.forEach { $0.tintColor = .primary } } }
The wonderful thing with this approach is, that you basically can create outlet collection for various purposes like one for tinting, one for text size etc. and then you can just connect the views you want to style to the appropriate collections.
Your solution already feels pretty minimalistc to me :) I can't come up with a shorter hack around that, sorry. This, somewhat exotic, Swift Evolution proposal could provide some help, but it was postponed for now at least.
Let's quickly explore some alternatives beyond willSet (probably gonna be downvoted for this, anyway...):
UIView subclass. I have a UITextField that I use across my app a lot! As such, I implemented a simple subclass and did all styling in there (instead of on each view controller which it appears). Of course, this might not be viable across all your views.
viewDidLoad. You could group all your views in an array and then do the styling inside the viewDidLoad method. Might be more error prone than your current solution — you might miss a view here and there — but gets the job done without too much hacking ;)
Something very strange has been happening when I change my UILabel's text in my view controller's viewDidLoad method. Although I am 100% certain that the label is connected (I have reconnected it multiple times as well as changed the name), it still gives me an EXC_BAD_EXCEPTION error when trying to change it. My code is below.
**NOTE: I should also mention that this error does not occur when the VC first initializes, but when I press a button that segues to another VC.
class BroadwayOrderReview: UIViewController, UITableViewDelegate, UITableViewDataSource, ClassNameDelegate {
#IBOutlet weak var BroadwayOrderReviewTableView: UITableView!
#IBOutlet weak var finalOrderPriceTotalLbl: UILabel!
var OrderDictionary: [String:String] = [:]
func addButtonAction(addedList:[String:Float],numOrders:[String:Int]) {
print(addedList)
print(numOrders)
}
override func awakeFromNib() {
}
override func viewDidLoad() {
super.viewDidLoad()
print("NUM ORDERS \(numOrders)")
self.finalOrderPriceTotalLbl.text = "0.00"
let totalPriceArray = Array(numOrders.keys).sort(<)
for (key) in totalPriceArray {
print("TOTAL PRICE ARRAY \(totalPriceArray)")
self.finalOrderPriceTotalLbl.text = String(Float(self.finalOrderPriceTotalLbl.text!)! + (Float(numOrders[key]!) * addedList[key]!))
print("TOTAL ORDER LBL \(finalOrderPriceTotalLbl.text)")
}
BroadwayOrderReviewTableView.delegate = self
BroadwayOrderReviewTableView.dataSource = self
for (name,orders) in numOrders {
print(name)
OrderDictionary["\(numOrders[name]!) \(name)"] = String(addedList[name]! * Float(numOrders[name]!))
}
print(OrderDictionary)
}
Thank you for any and all help, I really appreciate it.
You should not use a capital for naming your tableView.
BroadwayOrderReviewTableView should be broadwayOrderReviewTableView. You only want to use class, struct with first letter caps.
Did you try to delete your label from your code and storyboard?
It looks like the connection isn't there.
With what you shared it seems it can't access a value because it hasn't been initialized.
Something i didn't know when I started learning dev. you can actually type in the console log.
type po finalOrderPriceTotalLbl (for print out finalOrderPriceTotalLbl)
You can only use that when your app is on standby with breakpoint, and maybe crashes (I forgot for one sec) to setup a breakpoint just click on the number's line. to remove just drag it out.
If the print give you nil. you know there is a issue with your label connection for sure.
I created a storyboard and added the navbar buttons I want to be displayed
But when I simulate my app, this is what I get
I have also tried to programatically add the button using navigationItem.setRightBarButtonItem(), but that doesn't show up either.
However, when I try the same thing using a normal UIViewController, it works
So my question is if this is the behavior of PFQueryTableViewController, and if so, what do I need to do to correct it?
If more details are needed, I'd be happy to provide them - just curious if anyone has run into this problem before.
EDIT
I now have the button showing up on a basic UIViewController using suggestion of #Haidous, but I can't get my PFQueryTableViewController to show up inside it. Here's what I have:
#IBOutlet weak var containerV: UIView!
var favoriteVC:FavoritesViewController = FavoritesViewController(className: "Cat")
override func viewDidLoad() {
super.viewDidLoad()
//?what goes here?
containerV = favoriteVC.view
// Do any additional setup after loading the view.
}
I'm not sure how to make it show up
******EDIT WITH SOLUTION******
I was able to simply create a container in my storyboard and add my view controller like this (in case anyone else runs into the same problem and stumbles on this question)
class WrapperFavoritesViewControllerContainerViewController: UIViewController {
#IBOutlet weak var containerV: UIView!
var favoriteVC:FavoritesViewController = FavoritesViewController(className: "Cat")
override func viewDidLoad() {
super.viewDidLoad()
addChildViewController(favoriteVC)
self.containerV.addSubview(favoriteVC.tableView)
favoriteVC.didMoveToParentViewController(self)
}
I do not know if it is because of the PFQueryTableViewController or not.. but a workaround I found was creating a normal View Controller and setting up your Nav Bar there then placing a container view and connecting it to the PFQueryTableViewController thus PFQueryTableViewController becomes the Child View Controller. Hope I helped :)