Create a generic willSet closure for styling IBOutlets in Swift - ios

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 ;)

Related

How can an outlet be nil even though it has been set

I have a viewController with two embedded viewControllers through containers. I then created outlets from the parent viewController's containers to the parent class. I want to either hide or show the containers depending on certain conditionals.
But if I simply write:
#IBOutlet var twoArmsContainer: UIView! {
didSet {
print("SETTING TWO ARM")
}
}
override func viewDidLoad() {
super.viewDidLoad()
twoArmsContainer.isHidden = true //container is nil
}
Then it crashes with twoArmsContainer being nil after the print in didSet has been triggered. How is it possible that the outlet is set, but then becomes nil? I have tried hiding it inside didSet and that works fine:
#IBOutlet var twoArmsContainer: UIView! {
didSet {
print("SETTING TWO ARM")
twoArmsContainer.isHidden = true //WORKS
}
}
What else can I say? The class I'm working in inherits from another class so there is a super.viewDidLoad. Not sure if that is relevant. I tried putting the outlets in the super class but with the same results. I also tried removing and readding the outlets again. Have never experienced this problem before. Let me know if I should show more code; perhaps the entire class. Not really sure what's relevant as I'm clueless of where to start.
Ok, so I found out what was wrong; an issue impossible to detect without access to the code, so in retrospect I should have posted both the parent class and container class. Sorry about that.
Anyway the issue was that the container viewController inherited from the parent viewController. This enabled me to share code in the two container viewControllers.
So the structure was basically this:
class WizardChooseArmViewController: WizardViewController {
...
This is the parent viewController which inherits from a base viewController. Then the container viewControllers looked like this:
final class WizardTwoArmsViewController: WizardChooseArmViewController {
...
Apparently it's a bad idea for containers to inherit from its parent, so I refactored and changed it to:
final class WizardTwoArmsViewController: WizardViewController {
...
Not quite sure why its not possible for containers to inherit from its parent. Would be great if someone could brief me.

Human readable tag/id for a View in iOS

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
}
}

Dynamic Label Titles

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.

Swift - Data Model

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.

Swift : Clear UIView

I'm using PNChart in a UIView and overtime my method runs it adds an addition line to the graph rather than recreating the entire graph. How do I clear a UIView, before add[ing]Subview?
#IBOutlet weak var lineChart: UIView!
...
// in function
theLineChart.chartData = [actualData]
theLineChart.strokeChart()
// want to clear self.lineChart here
self.lineChart.addSubview(theLineChart)
If by “clear” you mean remove all previously added subviews from it, you could try something like:
while let subview = lineChart.subviews.last {
subview.removeFromSuperview()
}
The caveat is that lineChart should remain a plain UIView (or your self-made subclass that you know the implementation of), as otherwise it may have internal subviews that you shouldn't remove.
Then again, if it is nothing but a plain UIView, you could simply replace the whole view with a new one (this might even make it simple to cross-fade between old and new views if such is desired).

Resources