Swift IBInspectable didSet versus get/set - swift

I am relatively new to IBDesignables and IBInspectable's and I noticed that a lot of tutorial use IBInspectable in this fashion.
#IBInspectable var buttonBorderWidth: CGFloat = 1.0 {
didSet {
updateView()
}
}
func updateView() {
// Usually there are more entries here for each IBInspectable
self.layer.borderWidth = buttonBorderWidth
}
But in some instances they use get and set like this for example
#IBInspectable
var shadowOpacity: Float {
get {
return layer.shadowOpacity
}
set {
layer.shadowOpacity = newValue
}
}
Can someone please explain: What is happening in each of these cases and how to choose which one to use?

I see two questions. The first one is “What is happening in each of these cases”, and is best answered by reading the “Properties” chapter of The Swift Programming Language. There are also already three other answers posted which address the first question, but none of them answer the second, and more interesting, question.
The second question is “how to choose which one to use”.
Your shadowOpacity example (which is a computed property) has the following advantages over your buttonBorderWidth example (which is a stored property with an observer):
All of the shadowOpacity-related code is in one place, so it's easier to understand how it works. The buttonBorderWidth code is spread between didSet and updateViews. In a real program these functions are more likely to be farther apart, and as you said, “Usually there are more entries here for each IBInspectable”. This makes it harder to find and understand all the code involved in implementing buttonBorderWidth.
Since the view's shadowOpacity property getter and setter just forward to the layer's property, the view's property doesn't take any additional space in the view's memory layout. The view's buttonBorderWidth, being a stored property, does take additional space in the view's memory layout.
There is an advantage to the separate updateViews here, but it is subtle. Notice that buttonBorderWidth has a default value of 1.0. This is different than the default value of layer.borderWidth, which is 0. Somehow we need to get layer.borderWidth to match buttonBorderWidth when the view is initialized, even if buttonBorderWidth is never modified. Since the code that sets layer.borderWidth is in updateViews, we can just make sure we call updateViews at some point before the view is displayed (e.g. in init or in layoutSubviews or in willMove(toWindow:)).
If we want to make buttonBorderWidth be a computed property instead, we either have to force-set the buttonBorderWidth to its existing value somewhere, or duplicate the code that sets layer.borderWidth somewhere. That is, we either have to do something like this:
init(frame: CGRect) {
...
super.init(frame: frame)
// This is cumbersome because:
// - init won't call buttonBorderWidth.didSet by default.
// - You can't assign a property to itself, e.g. `a = a` is banned.
// - Without the semicolon, the closure is treated as a trailing
// closure on the above call to super.init().
;{ buttonBorderWidth = { buttonBorderWidth }() }()
}
Or we have to do something like this:
init(frame: CGRect) {
...
super.init(frame: frame)
// This is the same code as in buttonBorderWidth.didSet:
layer.borderWidth = buttonBorderWidth
}
And if we have a bunch of these properties that cover layer properties but have different default values, we have to do this force-setting or duplicating for each of them.
My solution to this is generally to not have a different default value for my inspectable property than for the property it covers. If we just let the default value of buttonBorderWidth be 0 (same as the default for layer.borderWidth), then we don't have to get the two properties in sync because they're never out-of-sync. So I would just implement buttonBorderWidth like this:
#IBInspectable var buttonBorderWidth: CGFloat {
get { return layer.borderWidth }
set { layer.borderWidth = newValue }
}
So, when would you want to use a stored property with an observer? One condition especially applicable to IBInspectable is when the inspectable properties do not map trivially onto existing layer properties.
For example, in iOS 11 and macOS 10.13 and later, CALayer has a maskedCorners property that controls which corners are rounded by cornerRadius. Suppose we want to expose both cornerRadius and maskedCorners as inspectable properties. We might as well just expose cornerRadius using a computed property:
#IBInspectable var cornerRadius: CGFloat {
get { return layer.cornerRadius }
set { layer.cornerRadius = newValue }
}
But maskedCorners is essentially four different boolean properties combined into one. So we should expose it as four separate inspectable properties. If we use computed properties, it looks like this:
#IBInspectable var isTopLeftCornerRounded: Bool {
get { return layer.maskedCorners.contains(.layerMinXMinYCorner) }
set {
if newValue { layer.maskedCorners.insert(.layerMinXMinYCorner) }
else { layer.maskedCorners.remove(.layerMinXMinYCorner) }
}
}
#IBInspectable var isBottomLeftCornerRounded: Bool {
get { return layer.maskedCorners.contains(.layerMinXMaxYCorner) }
set {
if newValue { layer.maskedCorners.insert(.layerMinXMaxYCorner) }
else { layer.maskedCorners.remove(.layerMinXMaxYCorner) }
}
}
#IBInspectable var isTopRightCornerRounded: Bool {
get { return layer.maskedCorners.contains(.layerMaxXMinYCorner) }
set {
if newValue { layer.maskedCorners.insert(.layerMaxXMinYCorner) }
else { layer.maskedCorners.remove(.layerMaxXMinYCorner) }
}
}
#IBInspectable var isBottomRightCornerRounded: Bool {
get { return layer.maskedCorners.contains(.layerMaxXMaxYCorner) }
set {
if newValue { layer.maskedCorners.insert(.layerMaxXMaxYCorner) }
else { layer.maskedCorners.remove(.layerMaxXMaxYCorner) }
}
}
That's a bunch of repetitive code. It's easy to miss something if you write it using copy and paste. (I don't guarantee that I got it correct!) Now let's see what it looks like using stored properties with observers:
#IBInspectable var isTopLeftCornerRounded = true {
didSet { updateMaskedCorners() }
}
#IBInspectable var isBottomLeftCornerRounded = true {
didSet { updateMaskedCorners() }
}
#IBInspectable var isTopRightCornerRounded = true {
didSet { updateMaskedCorners() }
}
#IBInspectable var isBottomRightCornerRounded = true {
didSet { updateMaskedCorners() }
}
private func updateMaskedCorners() {
var mask: CACornerMask = []
if isTopLeftCornerRounded { mask.insert(.layerMinXMinYCorner) }
if isBottomLeftCornerRounded { mask.insert(.layerMinXMaxYCorner) }
if isTopRightCornerRounded { mask.insert(.layerMaxXMinYCorner) }
if isBottomRightCornerRounded { mask.insert(.layerMaxXMaxYCorner) }
layer.maskedCorners = mask
}
I think this version with stored properties has several advantages over the version with computed properties:
The parts of the code that are repeated are much shorter.
Each mask option is only mentioned once, so it's easier to make sure the options are all correct.
All the code that actually computes the mask is in one place.
The mask is constructed entirely from scratch each time, so you don't have to know the mask's prior value to understand what its new value will be.
Here's another example where I'd use a stored property: suppose you want to make a PolygonView and make the number of sides be inspectable. We need code to create the path given the number of sides, so here it is:
extension CGPath {
static func polygon(in rect: CGRect, withSideCount sideCount: Int) -> CGPath {
let path = CGMutablePath()
guard sideCount >= 3 else {
return path
}
// It's easiest to compute the vertices of a polygon inscribed in the unit circle.
// So I'll do that, and use this transform to inscribe the polygon in `rect` instead.
let transform = CGAffineTransform.identity
.translatedBy(x: rect.minX, y: rect.minY) // translate to the rect's origin
.scaledBy(x: rect.width, y: rect.height) // scale up to the rect's size
.scaledBy(x: 0.5, y: 0.5) // unit circle fills a 2x2 box but we want a 1x1 box
.translatedBy(x: 1, y: 1) // lower left of unit circle's box is at (-1, -1) but we want it at (0, 0)
path.move(to: CGPoint(x: 1, y: 0), transform: transform)
for i in 1 ..< sideCount {
let angle = CGFloat(i) / CGFloat(sideCount) * 2 * CGFloat.pi
print("\(i) \(angle)")
path.addLine(to: CGPoint(x: cos(angle), y: sin(angle)), transform: transform)
}
path.closeSubpath()
print("rect=\(rect) path=\(path.boundingBox)")
return path
}
}
We could write code that takes a CGPath and counts the number of segments it draws, but it is simpler to just store the number of sides directly. So in this case, it makes sense to use a stored property with an observer that triggers an update to the layer path:
class PolygonView: UIView {
override class var layerClass: AnyClass { return CAShapeLayer.self }
#IBInspectable var sideCount: Int = 3 {
didSet {
setNeedsLayout()
}
}
override func layoutSubviews() {
super.layoutSubviews()
(layer as! CAShapeLayer).path = CGPath.polygon(in: bounds, withSideCount: sideCount)
}
}
I update the path in layoutSubviews because I also need to update the path if the view's size changes, and a size change also triggers layoutSubviews.

First of all, what you are asking about is nothing to do with #IBInspectable or #IBDesignable. Those are just directives for XCode to use with the Interface Builder when you create your own View/ViewControllers. Any property with #IBInspectable also appears in the attributes inspector in the Interface Builder. And #IBDesignable is for displaying the custom view in Interface builder. Now to get to the didSet and get/set
didSet
This is what you call a Property Observer. You can define property observers for a stored property to monitor the changes in a property. There are 2 flavors to monitor the change willSet and didSetthat can be defined. So you define the observers to perform some block of code where there is a change to that property. If you define willSet that code will be called before the property is set. Likewise didSet is the block run after the property has been set. So depending on what you need to do you can implement either of the observers.
get/set
Besides stored properties you can define something called Computed properties. As the name implies computed properties do not create and store any values themselves. These values are computed when needed. So these properties need get and set code to compute the property when required. If there is only a get that means it’s a read only property.
Hope this helps. Read the Swift book and go through the first few lectures of CS193p on iTunesU

didSet means "do the following when the variable is set". In your case, if you change buttonBorderWidth, the function updateView() will be called.
get and set are what you actually get when you ask for the variable itself. If I set shadowOpacity, it will pass it on to the set code. If I get shadowOpacity, it will actually get me layer.shadowOpacity.

#IBInspectable var buttonBorderWidth: CGFloat = 1.0
In that example, buttonBorderWidth is an actual property of the view. The attributes inspector can write to it and read it directly. The didSet observer is just so that something happens in response to our changing that property.
That's totally different from the other example:
#IBInspectable
var shadowOpacity: Float {
get {
return layer.shadowOpacity
}
set {
layer.shadowOpacity = newValue
}
}
In that example, the goal is to make the layer's shadowOpacity inspectable. But you can't do that, because it's not a property of the view. Therefore we put a façade in front of the layer property, in the form of a computed "property" of the view; the attributes inspector can't see layer.shadowOpacity, but it can see the view's shadowOpacity which, unbeknownst to it, is just a way of accessing the layer's shadowOpacity.

Related

Swift property observer issue

I've an issue with my property observer and would like to know a little bit more about the Swift behaviour.
I've the following architecture which use callbacks:
A higher lever class
class MyFirstClass : NSScrollView {
var colorDidUpdate: (() -> ())?
var color = NSColor.black {
didSet {
colorDidUpdate?()
}
}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
/* Some init */
var mySecondClass = MySecondClass(frame: frame)
colorDidUpdate = mySecondClass.enclosingViewDidUpdateColor
mySecondClass.color = color
documentView = mySecondClass
/* Some other init */
}
}
Then a second class that act like a link between the first and third class
class MySecondClass {
var viewColorDidUpdate: (() -> ())?
var color: NSColor?
var myThirdClass = MyThirdClass(frame: frame)
init(frame frameRect: NSRect) {
/* Some init */
viewColorDidUpdate = myThirdClass.updateColor
myThirdClass.color = color
/* Some other init */
}
func enclosingViewDidUpdateColor() {
viewStyleDidUpdate?()
}
}
And finally a third class where the draw is done.
class MyThirdClass {
var color: NSColor?
func draw(_ dirtyRect: NSRect) {
guard let color = color else { return }
// The color is black instead of green.
}
/* proerties and functions... */
func updateColor() {
functionThatWillTriggerTheDrawFunction() // Will trigger draw function
}
}
If I set a new color to MyClass property like
var myClass = MyClass()
myClass.color = .green
The printed color is not "green" but it still black...
I thought that when we were in the didSet scope the variable was already set, am I wrong?
Should I use an other pattern?
I thought that when we were in the didSet scope the variable was already set, am I wrong?
No that's true, but something else is happening I guess (although the relevant code doesn't seem to be included)
A closure as the name suggests closes over variables it uses at the time of its definition. So you are most likely defining/using the closure at the time where your color still is black. You can not use a closure to give you the current value of a variable that is captured but you could pass the value into the closure as a parameter. I hope this makes sense.
If you provide a more complete code sample I can get a better idea about what the problem in your case might be.
Update (after you provided more code):
You are only ever setting the colors of the second and third classes on their init methods. You are never updating their color properties when you update the first classes color property.
I am sure the simplification of your presented code is partly to blame, but there are a few things you might want to consider to make things easier to follow:
Try not saving the color in each and every component separately but rather pass it along as parameters of your functions/methods. This makes it easier to see, what is happening. For example, you could call your change handler colorDidUpdate(to color: NSColor) and pass in the new value. This way, at least your second class doesn't need to store the color but rather pass it along into updateColor(_ color: NSColor) which could set the third class' color property and trigger a redraw.
In general I think it is beneficial to pass in any change to a change handler and not read it from a global state. Try not to store everything but pass along the information you need without storing it in between (if possible). This makes it easier to see where and how the data and information flows in your app and might indicate problems with the architecture.
It is a bit hard suggesting to architect the entire thing differently since the code is just fragments here, but it looks like you could improve and simplify a bit.

How to set CAShapeLayer mask on the UILabel in Swift

I need to adjust the mask in the UILable for the animation, but this layer.mask method does not run
class FirstView: UIView {
var showAbout: UILabel {
let label = UILabel()
...
return label
}
var showAboutMask: CAShapeLayer {
let layer = CAShapeLayer()
...
return layer
}
override init(frame: CGRect) {
super.init(frame: frame)
showAbout.layer.mask = showAboutMask
addSubview(showAbout)
}
}
The label is shown but the mask layer does not appear
It looks like you are modifying once instance of showAbout and then adding another instance as a subview. This happens because showAbout is a "computed property". Computed properties don't actually store a value, instead they compute the value each time it's called (like a function).
You can verify this behavior by setting a breakpoint in the body of showAbout and note that it gets called twice.
Because of this, in initializeViews when you call
showAbout.layer.mask = showAboutMask
you compute one UILabel instance and modify the mask of its layer. Then, when you call
addSubview(showAbout)
you compute another UILabel instance (without the layer mask) and add it as a subview.
Computed properties have their use cases but here you probably want to store the UILabel instance. One way of doing this would be to lazily assign it the value of computing a closure, similar to how showAboutMask is assigned:
lazy var showAbout: UILabel = {
// same label creation as before
}()
At the time of initializing the label, the mask should be specified
var showAbout: UILabel {
....
label.layer.mask = showAboutMask
return label
}

Why is CALayer's opacity value not reflected as UIView's alpha?

I created the method below as part of custom CAAnimationGroup. The method first adds itself to weak reference to a CALayer assigned at initialization.
Then it iterates over it's own animations array and applies each animation 's toValue to the associated keyPath using KVC on the weak CALayer reference.
final public class FAAnimationGroup : CAAnimationGroup {
weak var weakLayer : CALayer?
override init() {
super.init()
animations = [CAAnimation]()
fillMode = kCAFillModeForwards
removedOnCompletion = true
}
override public func copyWithZone(zone: NSZone) -> AnyObject {
let animationGroup = super.copyWithZone(zone) as! FAAnimationGroup
animationGroup.weakLayer = weakLayer
return animationGroup
}
......
func applyFinalState() {
guard let animationLayer = weakLayer else {
return
}
animationLayer.addAnimation(self, forKey: self.animationKey)
if let groupAnimations = animations {
for animation in groupAnimations {
if let toValue = animation.toValue {
animationLayer.setValue(toValue, forKeyPath: animation.keyPath!)
}
}
}
}
}
So everything works accordingly for bounds, size, transform, and alpha for all my views just as expected with the current removedOnCompletion flag and fillMode values.
Once the animation is complete, I query the UIView, and it's backing layer. What is see is the frame reflects the correct result, the view's alpha reflects the animated opacity value. Great!
But here comes the fun part. When animation the opacity of a UISlider from 0.0 to 1.0. Once the animation is complete, I begin to adjust the UISlider value, and right as I move it, the alpha goes back to 0.0.
I tried to set the removedCompletion flag to false, and as expected, keeping the animation around kept the layer in it's final state, but that is not what I wanted. I need it to remove itself after finishing, since I did set the values directly on the the backing layer.
So after setting the removedCompletion back to true, I tried the following which has me completely stumped leading up to my question....
.....
if let groupAnimations = animations {
for animation in groupAnimations {
if let toValue = animation.toValue {
if animation.keyPath! == "opacity" {
animationLayer.owningView()!.setValue(toValue, forKeyPath: "alpha")
} else {
animationLayer.setValue(toValue, forKeyPath: animation.keyPath!)
}
}
}
}
In the code above, I would, instead of setting opacity on the layer, I set the alpha value on the owningView associated with animating layer (aka the layer's delegate). In this instance everything worked accordingly, I adjusted the slider and it did not reset to alpha 0.0
The fact that this is happening only with a UISlider is possibly irrelevant. I thought that by setting the UIView's properties, the backing layer will reflect the equivalent, and I assumed the vice versa to also be true.
Question
Why are the final alpha/opacity values in sync when I set the alpha of the view, but not reflected when I set the opacity on it's backing layer? What is the relationship between UIView and CALayer in this specific example?
From what I understood the two are very intricately interlinked, the UIView is kind of a wrapper full of access to the backing layer which redraws itself accordingly. What is this opacity/alpha relationship in the context of animations?

When to use didset or get set when using #IBInspectable

After looking at different tutorial. i don't know when to use didset or get set to update the variable.
Could anyone explain a little bit more detail for when to use didset or get set?
#IBInspectable var circleColor: UIColor = UIColor.redColor() {
didSet { //after properties are set in storyboard, update here
circleLayer.strokeColor = circleColor.CGColor
self.toggleButon()
}
}
/**
Radius of RadioButton circle.
*/
#IBInspectable var circleRadius: CGFloat = 5.0
#IBInspectable var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
layer.masksToBounds = newValue > 0
}
}
for circle radius, it doesn't have to use didset to update its value. i can't get it.
Here i am giving you one example and try to explain you how to use, hope it will helpful for you.
I am using this class for UIView here for did set and get set with Story Board my class name here "MyCustomView"
import Foundation
import UIKit
import QuartzCore
/// Computed properties, based on the backing CALayer property, that are visible in Interface Builder.
#IBDesignable public class MyCustomView: UIView {
/// When positive, the background of the layer will be drawn with rounded corners. Also effects the mask generated by the `masksToBounds' property. Defaults to zero. Animatable.
#IBInspectable var cornerRadius: Double {
get {
return Double(self.layer.cornerRadius)
}
set {
self.layer.cornerRadius = CGFloat(newValue)
}
}
/// The width of the layer's border, inset from the layer bounds. The border is composited above the layer's content and sublayers and includes the effects of the `cornerRadius' property. Defaults to zero. Animatable.
#IBInspectable var borderWidth: Double {
get {
return Double(self.layer.borderWidth)
}
set {
self.layer.borderWidth = CGFloat(newValue)
}
}
/// The color of the layer's border. Defaults to opaque black. Colors created from tiled patterns are supported. Animatable.
#IBInspectable var borderColor: UIColor? {
get {
return UIColor(CGColor: self.layer.borderColor!)
}
set {
self.layer.borderColor = newValue?.CGColor
}
}
/// The color of the shadow. Defaults to opaque black. Colors created from patterns are currently NOT supported. Animatable.
#IBInspectable var shadowColor: UIColor? {
get {
return UIColor(CGColor: self.layer.shadowColor!)
}
set {
self.layer.shadowColor = newValue?.CGColor
}
}
/// The opacity of the shadow. Defaults to 0. Specifying a value outside the [0,1] range will give undefined results. Animatable.
#IBInspectable var shadowOpacity: Float {
get {
return self.layer.shadowOpacity
}
set {
self.layer.shadowOpacity = newValue
}
}
/// The shadow offset. Defaults to (0, -3). Animatable.
#IBInspectable var shadowOffset: CGSize {
get {
return self.layer.shadowOffset
}
set {
self.layer.shadowOffset = newValue
}
}
/// The blur radius used to create the shadow. Defaults to 3. Animatable.
#IBInspectable var shadowRadius: Double {
get {
return Double(self.layer.shadowRadius)
}
set {
self.layer.shadowRadius = CGFloat(newValue)
}
}
}
And you can use this with story board import this class with your "UIView"
after you will see some
and you directly set here view cornet radius, shadow and Shadow
and result you can see inside your storyboard directly without running code
Output here from this code
This answer explains the difference in usage of set vs didSet quite clearly.
IN SUMMARY
willSet should be used for doing something before the value is set. (the value is not updated at this point).
set is to update the value
didSet if you want to do anything after the value is set (the value has been updated at this point)
ALSO
if you implement set, you will also need to implement get
but didSet can also be used without having to implement any other method
#IBInspectable will work with both property kinds:
Use didSet{} for stored properties:
didSet is a property observer.
Use set{} get{} for computed properties.
in the following example: firstName And lastName are stored properties.
fullName is a Computed property:
struct Person{
var firstName:String
var lastName:String{
didSet{
//property observer
print("It was set")
}
}
var fullName:String{
get{
return "\(firstName)-\(lastName)"
}
set{
let split = newValue.split(separator: "-")
firstName = String(split.first ?? "")
lastName = String(split.last ?? "")
}
}
}
var p = Person(firstName: "Moshe", lastName: "Doe")
print(p.firstName)
print(p.lastName)
print(p.fullName)
p.fullName = "Jhon-Doe"
print(p.firstName)
print(p.lastName)
print(p.fullName)
Also look into the language guide: (Properties)
https://docs.swift.org/swift-book/LanguageGuide/Properties.html
One final note about properties and #IBInspectable:
Validation of a value may be achieved using a combo of a computed property with a stored property (Backing property): Here is an example:
//Bound between 0 and 5
var boundRating:Int = 3{
didSet{
renderRating()
}
}
#IBInspectable
var rating:Int{
set{
if newValue <= 5 && newValue >= 0{
boundRating = newValue
renderRating()
}
}
get{
return boundRating
}
}

Is it possible use non read-only computed property in extension?

Is it possible for a computed property in extension to have both getter and setter? Apple's guide does not mention it and the only example I have seen only shows read-only computed property in extension.
Is it possible computed property in extension that has getter and setter?
Yes.
Probably one of the most common uses of computed properties in extensions in my experience is providing a wrapper to make easier access to particular properties.
For example, when we want to modify the border layer, border color, or corner radius of anything out of UIKit, we're stuck going through the layer property.
But we can extend UIView with a property with both a setter & getter to provide a much more convenient means of changing the properties of its layer:
extension UIView {
var borderColor: UIColor? {
get {
guard let color = self.layer.borderColor else {
return nil
}
return UIColor(CGColor: color)
}
set {
self.layer.borderColor = newValue?.CGColor
}
}
}
Moreover, if we really want to, we can leverage the Objective-C run time to emulate stored properties in extensions (which of course mean setting & getting). Take part of this Stack Overflow answer for example:
private var kAssociationKeyNextField: UInt8 = 0
extension UITextField {
#IBOutlet var nextField: UITextField? {
get {
return objc_getAssociatedObject(self, &kAssociationKeyNextField) as? UITextField
}
set(newField) {
objc_setAssociatedObject(self, &kAssociationKeyNextField, newField, .OBJC_ASSOCIATION_RETAIN)
}
}
}
This serves as just one example of a property in an extension with a setter & getter.
This works:
extension Bool
{
public var integerValue: Int
{
get
{
return true ? 1 : 0
}
set
{
self = (newValue > 0) ? true : false
}
}
}
So yes.