How to use self in lazy initialization in Swift - swift

Some websites recommend to initialize views using lazy initialization instead of using a storyboard.
It works when self is not used in lazy initialization.
But when self is used in it, a compile error occurs.
For example, in the following code, label1 can be compiled successfully, but label2 can't, because self is used in it.
How to use self in lazy initializations?
class A {
private let label1: UILabel = {
return UILabel()
}()
private let label2: UILabel = {
let view = UILabel()
self.addTextToLabel(view) // compile error !!!
return view
}()
private func addTextToLabel(label: UILabel) {
label.text = "test"
}
}

Your question is written under the misunderstanding that you are currently using lazy initialization. But you are not. Both label1 and label2 are not using lazy initialization. They are being initialized immediately when A is being initialized and this is the cause of the error since self isn't ready when these property initializers are called.
The solution is to actually make label2 a lazy property.
private lazy var label2: UILabel = {
let view = UILabel()
self.addTextToLabel(label: view)
return view
}()
But note that this label2 initialization will not happen until the first time you actually try to access the label2 property.
As per the docs:
A lazy stored property is a property whose initial value is not calculated until the first time it is used. You indicate a lazy stored property by writing the lazy modifier before its declaration.
Lazy properties are useful when the initial value for a property is dependent on outside factors whose values are not known until after an instance’s initialization is complete.
Reference:
https://docs.swift.org/swift-book/LanguageGuide/Properties.html

Related

SWIFT best practice to share variable across view controllers and inside class functions

I guess I have traced down the issue of the error Instance member 'txtProblem' cannot be used on type 'TextProblemViewController', however, I wonder what is the best practice on how to deal with the underlying issue.
I have a global data structure TextProblems which I use to fetch JSON data. This data is used by multiple view controllers. That is why I declared it as a global variable
var problems:TextProblems!
class TextProblemViewController: UIViewController {
Now I wanted to break my viewDidLoad() apart and moved some code to a separate function, thus
override func viewDidLoad() {
super.viewDidLoad()
do {
problems = try readJsonProblem()
} catch {
print(error.localizedDescription)
}
txtProblem.text = fillParamters(textRaw: problems.textProblem[i].problemStmnt.problemText, parameters: problems.textProblem[i].problemStmnt.problemPrmtr.map {$0.param})
became
let ProblemStmntView: UIView = {
let view = UIView()
view.frame = CGRect(x: 0, y: 0, width: 100, height: 200)
view.backgroundColor = UIColor.white
txtProblem.text = fillParamters(textRaw: problems.textProblem[i].problemStmnt.problemText, parameters: problems.textProblem[i].problemStmnt.problemPrmtr.map {$0.param})
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
do {
problems = try readJsonProblem()
} catch {
print(error.localizedDescription)
}
view.addSubview(ProblemStmntView)
This results in the above error message at the line txtProblem.text = fillParameters, where txtProblem us an IBOutlet of a label
I have seen 3 resolutions and imagine a 3rd one - and I guess there are more:
declare the variable as static (but this does not work outside a class when declaring the global variable
make the variable lazy, however, I think I cannot declare an IBOutlet as lazy
pass the data from viewDidLoad to my closure (the example showed passing it to a function as a solution), however, not sure how to pass the data to the initialization closure
declare the global variable as static within the structure
So my question is, what do you assume to be best practice when sharing data not initialized before run time across view controllers and class functions? Would you declare a global variable or somehow pass along the data between view controllers, functions (and initialization closures???)?

Swift - `self` in variable initialization closure

How does button.addTarget(self, action: #selector(taptap), for: .touchUpInside)
work without a lazy keyword?
Dropped lazy by mistake, and have a closure to initialize a button like below:
class MyView: UIView {
let button: UIButton = {
let button = UIButton()
button.addTarget(self, action: #selector(taptap), for: .touchUpInside)
print(self) // (Function)
// button.frame = bounds <- Cannot assign here
return button
}()
lazy var button2: UIButton = {
let button = UIButton()
print(self) // <sample.MyView ...>
return button
}()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
addSubview(button2)
addSubview(button)
button.frame = bounds
print(self) // <sample.MyView ...>
}
#objc func taptap() {
print("taptap")
}
}
And the printed result is:
(Function)
<sample.MyView: 0x7f961dd09d80; frame = (67 269; 240 128); autoresize = RM+BM; layer = <CALayer: 0x6080000268a0>>
<sample.MyView: 0x7f961dd09d80; frame = (67 269; 240 128); autoresize = RM+BM; layer = <CALayer: 0x6080000268a0>>
taptap
What's the difference self in button closure and self in others? And why my button is works?
It works in NSObject subclasses because NSObject (or rather NSObjectProtocol) declares method self. That method is also available on metatypes (which are also NSObject instances) and therefore you can call it in static context.
The fact that it actually works on UIButton is probably a quirk of the compiler and the fact that UIButton accepts Any? as target.
Don't use it, it's not how it's intended to work.
See the bug SR-4559
In short, when you declare an instance property using closure initialization, that property will be created before self would be available (before the instance had been initalized properly), hence you cannot access self. A lazy instance property can only ever be accessed after the instance had been initalized, so you can access self from a lazy propery.
Longer version:
If you use closure initializiation by let button: UIButton = { return UIButton() }(), the button variable will be handled the exact same way in runtime as if you simply declared it like let button:UIButton = UIButton(). Pay attention to the () at the end of the closure. That essentially executes the closure right away when the instance property is being initialized, this is why it can actually be declared as immutable. Since instance properties are being initialized before the class initializer would be called, self is not available inside the closure for of a property.
When you declare a variable using the lazy keyword, it will only be evaluated when it is first accessed. Due to this, self is available inside the closure declaring a lazy property, since a property cannot be accessed before the class instance would be created, so in all cases, self is already initialized and available by the time you'd ever access the lazy property.

Swift: slow build times when changing struct code

tl;dr
Applying a code change to a commonly used struct causes very slow build times. Can this be avoided?
I have a rather large Swift project (Xcode 9.2) where I use a struct to hold all styling information (colors, spacings, etc.) used by in the app, something like:
struct Style {
var iconColor: UIColor = .darkGray
var lightTextColor: UIColor = .gray
// ... and many more properties ...
static var defaultStyle: Style {
return Style()
}
static var fancyStyle: Style {
var style = Style()
// ... override style props for more fancyness ...
return style
}
}
Every view controller (there are about 30 VCs in the project, all created in code -- no storyboards used) has a Style instance and uses it when rendering its UI:
class MyViewController: UIViewController {
var style = Style.defaultStyle // can be overridden by creator of the VC
override func viewDidLoad() {
super.viewDidLoad()
myLabel.textColor = style.lightTextColor
}
}
This works pretty nicely and allows to change settings in one place only, without polluting the namespace with global constants.
However, I noticed that when changing a property's default value inside the struct the compile time rises dramatically, Xcode basically performs a full rebuild. E.g. changing the above definition of Style.lightTextColor to var lightTextColor: UIColor = .green leads to a build time comparable to a full rebuild of the project. If, however, I change the value directly where it is used; e.g. in the view controller: myLabel.textColor = .green, the project builds very quickly.
Is there a solution for this, preferably by configuring the compiler to do ... less work in this case?

What am I doing when I initialize a property in the form let var : ClassName = {...}()

I have this in my view controller
let inputsContainerView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.whiteColor()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 5
view.layer.masksToBounds = true
return view
}()
I am not exactly sure how the initialization within the curly braces works. Is this shorthand? If so, what would be the long form?
Note, I am just trying to understand this syntax more than anything, it works just fine.
You are defining a function and calling that function to get its result. Then you assign that result (view, the value returned from the function) as the initial value of your variable inputsContainerView.
I call this idiom define-and-call, and it's very useful in exactly this situation, namely, you want to initialize a variable but your initializer needs to consist of several lines of code.
You are defining a closure that takes no parameters and returns a UIView and then you are calling that closure with () to get back a UIView which is assigned to inputsContainerView.
Is this shorthand? If so, what would be the long form?
Yes, the full form of a closure specifies the inputs and outputs. The long form would add () -> UIView in. Swift is able to infer the inputs and outputs from the values passed in () and the fact that the result is assigned to a UIView constant which is why you were able to leave them off.
let inputsContainerView: UIView = { () -> UIView in
let view = UIView()
view.backgroundColor = UIColor.whiteColor()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.cornerRadius = 5
view.layer.masksToBounds = true
return view
}()
You can read more about closures here.

Changing the value of a variable in another a class using a UIButton Swift 2

I have a View Controller (called BackgroundViewController) which has a few buttons, each of them set the color of the background of a different view, my main view. (just called ViewController, yes I started this project about a month ago, before I knew that I should name it something better). For that I set a class, SoundboardBrain, which I intend to use to hold a lot of the app's logic. Here's the class so far:
var backgroundName = String()
init(){
backgroundName = "Image"}
func changeBackgroundName(background: String){
backgroundName = background}
Now, BackgroundViewController is kind of like a settings pane, where the user could select one of the options and a bullet point appears by the one that he checked. Here's one of the of the buttons:
#IBAction func whiteButton(sender: AnyObject){
whiteBullet.hidden = false
imageBullet.hidden = true
}
//Here I call the changeBackground function I defined in SoundboardBrain
SoundboardBrain.changeBackgroundName("White")
//I then print the result of that and I still get "Image" NO MATTER WHAT!
So all I want to know is how to change a variable initialized in a class with a UIButton or another object of a ViewController.
You should keep the instance of SoundBrain somewhere in variable, or use a singleton. You could be initializing a new SoundBrain instance later.
Singleton is better for main app logic. Example:
class SoundboardBrain {
static let shared = SoundboardBrain()
var backgroundName = "Image"
func changeBackgroundName(background: String) {
backgroundName = background
}
}
SoundboardBrain.shared.backgroundName
// now the property is "Image"
// in UIButton
SoundboardBrain.shared.changeBackgroundName("something")
SoundboardBrain.shared.backgroundName
// now it's "something"
Example was made in Playground, but it doesn't matter.