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.
Related
Let's say I have created a UI with some views using an interface builder (not in code, so not programmatically).
How can I assign human readable ids / tags to these views, so that I could reference them in code?
I know that I can assign an integer tag to a view using attribute inspector and then make a dictionary (or enum) to store the mapping of tags to the views. However, this is an error-prone method which also scales really badly (imaging assigning integer tags to hundred of views in a complex app...).
Is there a better solution for this problem? Is there a way to directly assign a human readable tag / id to a view, like "resumeButton"?
UPDATE:
Here is an example scenario of what I want to achieve:
UI with five different buttons; the buttons have image and no title
all five buttons are connected to the same IBAction in code
in IBAction I have a switch statement, so that depending on which button is clicked, different versions of code are executed
UPDATE 2:
SOLUTION
I ended up implementing a simple custom view:
#IBDesignable
class CustomButton: UIButton {
#IBInspectable var stringTag: String = defaultID
}
This way I can see an additional property stringTag in Interface Builder and can simply add a value to it directly in Interface Builder.
You could create an extension property on UIView to store an identifier string. If you made that extension property IBInspectable, you could set and view it from the storyboard directly.
More detail on setting up such a property in this answer:
https://stackoverflow.com/a/37166043/1830999
The built-in tag property for UIView is just an integer, so it isn't very descriptive for humans who read it.
From what you're describing sounds to me that you are looking for an: Outlets
You are right about tags. Every connection with Interface Builder should be handled using IBAction and IBOutlet.
Since you say that every button has a different action, the simplest solution is to create a separate IBAction for each of them:
#IBAction private func onResumeButtonTapped() {
...
}
#IBAction private func onPauseButtonTapped() {
...
}
If you, for some reason, want to keep them connected to one function, you can use outlets:
#IBOutlet private var resumeButton: UIButton!
#IBOutlet private var pauseButton: UIButton!
#IBAction private func onButtonPressed(_ sender: UIButton) {
switch sender {
case resumeButton:
...
case pauseButton:
...
default:
break
}
}
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.
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 ;)
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.
I am attempting to learn Apple's Swift. I was recently trying to build a GUI app, but I have a question:
How do I interact with GUI elements of my app? For instance, I used interface builder to make a UILabel, and I connected it to my viewcontroller by control-clicking, so that I get the #IBOUTLET thing. Now, how do I, while in my view controller, edit the text of this UILabel? To state it another way, what code can I use to programatically control the text of something on my storyboard? All methods I have found online only appear to work with a button generated in code, not a button generated on a storyboard.
I've seen code like
self.simpleLabel.text = "message"
If this is right, how do I link it with the label in question? In other words, how do I adapt this code to be connected with the IBOutlet (If that's what I do)
If you've successfully linked the control with an IBOutlet on your UIViewController class, then the property is added to it and you would simply replace simpleLabel with whatever you named your IBOutlet connection to be like so:
class MyViewController: UIViewController {
#IBOutlet weak var myLabel: UILabel!
func someFunction() {
self.myLabel.text = "text"
}
}
The outlet you created must've been named by you. The outlet belongs to your view controller. self.simpleLabel means 'fetch the value of my property named 'simpleLabel'.
Since different outlets have different names, using self.simpleLabel here won't work until your outlet is named 'simpleLabel'. Try replacing 'simpleLabel' with the name you gave to the outlet when you created it.
The correct way now would be:
self.yourLabelName.text = "message"
If you have something like this for an IBOutlet:
#IBOutlet var someLabel: UILabel!
then you could set the text property just like in your example:
someLabel.text = "Whatever text"
If you're having problems with this, perhaps you're not assigning the text property in the right place. If it's in a function that doesn't get called, that line won't execute, and the text property won't change. Try overriding the viewDidLoad function, and put the line in there, like this:
override func viewDidLoad() {
super.viewDidLoad()
someLabel.text = "Whatever text"
}
Then, as soon as the view loads, you'll set the text property. If you're not sure if a line of code is executing or not, you can always put a breakpoint there, or add some output. Something like
println("Checkpoint")
inside a block of code you're unsure about could really help you see when and if it runs.
Hope this helps.
You may trying to change a UI component not in the main thread, in that case, do this:
DispatchQueue.main.async {
someLabel.text = "Whatever text"
}