I can animate my shape with updating a CGFloat value type simply, now I was looking to more reflector in my code to make it working with Bool Type as well, here what I have tried:
struct ContentView: View {
#State private var show: Bool = true
var body: some View {
MyShape(show: show)
.stroke()
.animation(.default, value: show)
Button("show") { show.toggle() }
}
}
struct MyShape: Shape {
var show: Bool
var animatableData: Bool {
get { return show }
set(newValue) { show = newValue }
}
func path(in rect: CGRect) -> Path {
return Path { path in
path.addLines([CGPoint(x: 0, y: 100), CGPoint(x: show ? 200 : 0, y: 100)])
}
}
}
my working code version was using Bool value in initialization for the length of line and then my animatableData was the length of line, and that would work, as I mentioned looking to use just Bool and animate the Shape just with bool, currently my code does not animate, looking to use just Bool for making animation happen. Can we do this? or I must use a helper value type like CGFloat to translate true/false to CGFloat. But I am NOT looking for an answer using Bool and a helper value Type, I already now that way.
You can't use Bool directly as your animatableData because Bool doesn't implement VectorArithmetic. There isn't really a sensible implementation of VectorArithmetic for Bool. How would you implement scale(by:)?
AnimatableData requires VectorArithmetic so that SwiftUI can smoothly interpolate between two values of animatableData without knowing anything else about the animatableData. There's no way to smoothly interpolate between false and true.
Related
I'm trying to apply some validation to a TextField to add a red border around the field when the content is invalid (in this case, I'm validating that the content is a positive number that is less than the specified maxLength).
The validation works fine and the border is applied when the value is out of range. However, when the border is applied to the TextField, the TextField loses focus in the UI (and also loses focus when the border is removed).
Here is a snippet of my code (I've included some extensions I'm using, but I don't think those are relevant to the issue)
import Foundation
import SwiftUI
import Combine
struct MyView : View {
#Binding var value: Int
var label: String
var maxLength: Int
#State private var valid: Bool = true
var body: some View {
TextField(label, value: $value, format: .number)
.textFieldStyle(RoundedBorderTextFieldStyle())
.fixedSize()
.multilineTextAlignment(.trailing)
.onReceive(Just(value), perform: validate)
.if(!valid, transform: { $0.border(.red)})
}
func validate(val: Int) {
let newVal = val.clamped(to: 0...maxLength)
if newVal != val {
valid = false
} else {
valid = true
}
}
}
extension View {
#ViewBuilder
func `if`<Transform: View>(_ condition: Bool, transform: (Self) -> Transform) -> some View {
if condition { transform(self) }
else { self }
}
}
extension Comparable {
func clamped(to limits: ClosedRange<Self>) -> Self {
return min(max(self, limits.lowerBound), limits.upperBound)
}
}
Is there any way to preserve focus on the TextField when styles are conditionally applied to it?
Or am I approaching validation wrong altogether? Is there a better way to check fields and apply conditional styling?
Because of the way your if modifier is structured, SwiftUI is unable to see that the underlying View is the same in the two conditions. For more detail on this, I'd suggest you watch Demystifying SwiftUI from WWDC 2021.
One solution is the simplify your border modifier into the following:
.border(valid ? .clear : .red)
This way, SwiftUI can still tell that this is the same underlying View.
There are some kind of protocols in SwiftUI which they have a function, that struct should have same function to be able to conform to that protocol,
like ViewModifier protocol which need
func body(content: Content) -> some View { return content }
or Shape protocol which need
func path(in rect: CGRect) -> Path { return Path { path in } }
When we are using those struct that conform to these protocols, we actually does not provide any parameters to those required functions from protocol, they grab there need parameters magically from context.
Which this auto grab parameters is big question for me how this process happens?
There for I re created Shape protocol to actually try and test this auto grab feature! How can my function automatically grab needed parameters! Right now my code is not grabbing need data, instead of returning Shape, it returns View! which I did all the things the same like apple done! why Shape protocol of apple returns Shape but my protocol does not, also my functions does not auto grab the needed data.
import SwiftUI
struct ContentView: View {
var body: some View {
CustomShape() // print: CustomShape: (0.0, 0.0, 414.0, 405.0) without body of CustomShape: Shape!
CustomSHAPE() // it does not print! But also returns unwished View!
}
}
struct CustomShape: Shape {
//var body: some View { Color.blue } // if we have body, path would stop working!
func path(in rect: CGRect) -> Path {
print("CustomShape:", rect)
return Path { path in }
}
}
protocol SHAPE: Animatable, View { }
struct CustomSHAPE: SHAPE {
var body: some View { Color.red } // if we have body, path would stop working! also I cannot comment it like we can in CustomShape: Shape
func path(in rect: CGRect) -> Path {
print("CustomSHAPE:", rect)
return Path { path in }
}
}
I think the problem comes down thinking in terms of "auto grabbing parameters."
The Shape protocol is where path(in:) is declared for SwiftUI. View "knows" that any type conforming to Shape provides a path(in:) method that it can call with the bounds of the view into which it wants to render the shape.
Your CustomShape isn't auto-grabbing anything. It conforms to Shape so View calls its path(in:). Under the hood, hidden away from from us mere mortals who don't work at Apple, there is an NSView or UIView that View is trying to render into. After figuring out how big that underlying AppKit/UIKit view has to be, and where it is to be positioned, (ie, after applying its layout constraints), it simply passes its bounds to CustomShape's path(in:) to get a Path, which I'm sure is ultimately just a struct wrapper for a CGPath.
Your CustomSHAPE on the other hand does not conform to Shape. It conforms to a different protocol, SHAPE. View doesn't know anything about SHAPE, so it can't do anything with it. All it knows is that it conforms to View, so it has to restrict what it does with CustomSHAPE to only the things the View protocol guarantees.
Basically your custom shapes need to conform to SwiftUI's protocols for SwiftUI to know how to use them.
Now, if you write your own generic View that wraps SHAPE instances, maybe you could implement your own forwarding to SHAPE's path(in:) method, but I'm not sure off the top of my head how to implement it in a way that properly hooks into SwiftUI's rendering.
Generally speaking, you don't need magic for your protocol to be able to "auto grab" stuff. You can set default values for a protocol's declarations by using extensions. Here is a simple example:
An example without "auto grab":
protocol MyProtocol {
func doSomething() -> Bool
}
// ERRORRRR! MyStructure doesnt conform to MyProtocol because you havent defined
// the required function `func doSomething() -> Bool`
struct MyStructure: MyProtocol {
}
An example with "auto grab":
protocol MyProtocol {
func doSomething() -> Bool
}
extension MyProtocol {
func doSomething() -> Bool {
print("Im doing all i can!")
return false
}
}
// No Errors because `func doSomething() -> Bool` has defaulted to the func
// that we declared in `extension MyProtocol`. so it "auto grab" the default func.
struct MyStructure: MyProtocol {
}
Remember this was only a simple example. You can do much more complicated "auto grab"s using the power of extensions.
This is almost what is happening in Shape. Apple engineers have defined a default var body: some View for any Shape:
When you declare your own var body: some View in a type that conforms to Shape, you are overriding the default var body that Apple engineers have defined, with your insufficient var body that doesnt contain anything much.
I haven't digged into it but there is a chance that the _ShapeView<Self, ForegroundStyle> that you can see in the picture does do some actual wizardry by accessing stuff that are internal and we don't have access to yet.
EDIT:
All that being said, i digged into this more as i was curious, and here's a working example:
import SwiftUI
struct ContentView: View {
var body: some View {
CustomShape()
CustomSHAPE()
}
}
struct CustomShape: Shape {
func path(in rect: CGRect) -> Path {
print("CustomShape:", rect)
return Path { path in }
}
}
protocol SHAPE: Animatable, View {
func path(in rect: CGRect) -> Path
}
extension SHAPE {
var body: some View {
GeometryReader { geo in
self.path(in: geo.frame(in: .local))
}
}
}
struct CustomSHAPE: SHAPE {
func path(in rect: CGRect) -> Path {
print("CustomSHAPE:", rect)
return Path { path in }
}
}
in console you'll see something like this printed:
CustomShape: (0.0, 0.0, 390.0, 377.66666666666663)
CustomSHAPE: (0.0, 0.0, 390.0, 377.5)
I'm wanting to be able to toggle on and off a bool var (isActive) on a SKSpriteNode.
for example.
someNode.isActive.toggle()
I'm sure its simple but for some reason it is escaping me right now.
I'm having a brain fart, sorry.
It finally came to me.
protocol Active {
var isActive: Bool { get set }
}
extension SKSpriteNode: Active {
var isActive: Bool {
get {
return false
}
set {}
}
}
Sample of what I need:
.
As there is absent .onAnimationCompleted { // Some work... } its pretty problematic.
Generally I need the solution that will have a following characteristics:
Most short and elegant way of playing some ping-pong animation ONCE. Not infinite!
Make code reusable. As example - made it as ViewModifier.
To have a way to call animation externally
my code:
import SwiftUI
import Combine
struct ContentView: View {
#State var descr: String = ""
#State var onError = PassthroughSubject<Void, Never>()
var body: some View {
VStack {
BlurredTextField(title: "Description", text: $descr, onError: $onError)
Button("Commit") {
if self.descr.isEmpty {
self.onError.send()
}
}
}
}
}
struct BlurredTextField: View {
let title: String
#Binding var text: String
#Binding var onError: PassthroughSubject<Void, Never>
#State private var anim: Bool = false
#State private var timer: Timer?
#State private var cancellables: Set<AnyCancellable> = Set()
private let animationDiration: Double = 1
var body: some View {
TextField(title, text: $text)
.blur(radius: anim ? 10 : 0)
.animation(.easeInOut(duration: animationDiration))
.onAppear {
self.onError
.sink(receiveValue: self.toggleError)
.store(in: &self.cancellables)
}
}
func toggleError() {
timer?.invalidate()// no blinking hack
anim = true
timer = Timer.scheduledTimer(withTimeInterval: animationDiration, repeats: false) { _ in
self.anim = false
}
}
}
How about this? Nice call site, logic encapsulated away from your main view, optional blink duration. All you need to provide is the PassthroughSubject, and call .send() when you want the blink to happen.
import SwiftUI
import Combine
struct ContentView: View {
let blinkPublisher = PassthroughSubject<Void, Never>()
var body: some View {
VStack(spacing: 10) {
Button("Blink") {
self.blinkPublisher.send()
}
Text("Hi")
.addOpacityBlinker(subscribedTo: blinkPublisher)
Text("Hi")
.addOpacityBlinker(subscribedTo: blinkPublisher, duration: 0.5)
}
}
}
Here's the view extension you would call
extension View {
// the generic constraints here tell the compiler to accept any publisher
// that sends outputs no value and never errors
// this could be a PassthroughSubject like above, or we could even set up a TimerPublisher
// that publishes on an interval, if we wanted a looping animation
// (we'd have to map it's output to Void first)
func addOpacityBlinker<T: Publisher>(subscribedTo publisher: T, duration: Double = 1)
-> some View where T.Output == Void, T.Failure == Never {
// here I take whatever publisher we got and type erase it to AnyPublisher
// that just simplifies the type so I don't have to add extra generics below
self.modifier(OpacityBlinker(subscribedTo: publisher.eraseToAnyPublisher(),
duration: duration))
}
}
Here's the ViewModifier where the magic actually happens
// you could call the .modifier(OpacityBlinker(...)) on your view directly,
// but I like the View extension method, as it just feels cleaner to me
struct OpacityBlinker: ViewModifier {
// this is just here to switch on and off, animating the blur on and off
#State private var isBlurred = false
var publisher: AnyPublisher<Void, Never>
// The total time it takes to blur and unblur
var duration: Double
// this initializer is not necessary, but allows us to specify a default value for duration,
// and the call side looks nicer with the 'subscribedTo' label
init(subscribedTo publisher: AnyPublisher<Void, Never>, duration: Double = 1) {
self.publisher = publisher
self.duration = duration
}
func body(content: Content) -> some View {
content
.blur(radius: isBlurred ? 10 : 0)
// This basically subscribes to the publisher, and triggers the closure
// whenever the publisher fires
.onReceive(publisher) { _ in
// perform the first half of the animation by changing isBlurred to true
// this takes place over half the duration
withAnimation(.linear(duration: self.duration / 2)) {
self.isBlurred = true
// schedule isBlurred to return to false after half the duration
// this means that the end state will return to an unblurred view
DispatchQueue.main.asyncAfter(deadline: .now() + self.duration / 2) {
withAnimation(.linear(duration: self.duration / 2)) {
self.isBlurred = false
}
}
}
}
}
}
John's answer is absolutely great and helped me get to exactly what I was looking for. I extended the answer to allow for any view modification to "flash" once and return.
Example Result:
Example Code:
struct FlashTestView : View {
let flashPublisher1 = PassthroughSubject<Void, Never>()
let flashPublisher2 = PassthroughSubject<Void, Never>()
var body: some View {
VStack {
Text("Scale Out & In")
.padding(20)
.background(Color.white)
.flash(on: flashPublisher1) { (view, isFlashing) in
view
.scaleEffect(isFlashing ? 1.5 : 1)
}
.onTapGesture {
flashPublisher1.send()
}
Divider()
Text("Flash Text & Background")
.padding(20)
// Connivence view extension for background and text color
.flash(
on: flashPublisher2,
originalBackgroundColor: .white,
flashBackgroundColor: .blue,
originalForegroundColor: .primary,
flashForegroundColor: .white)
.onTapGesture {
flashPublisher2.send()
}
}
}
}
Here's the modified code from John's answer.
extension View {
/// Listens to a signal from a publisher and temporarily applies styles via the content callback.
/// - Parameters:
/// - publisher: The publisher that sends a signal to apply the temp styles.
/// - animation: The animation used to change properties.
/// - delayBack: How long, in seconds, after flashing starts should the styles start to revert. Typically this is the same duration as the animation.
/// - content: A closure with two arguments to allow customizing the view when flashing. Should return the modified view back out.
/// - view: The view being modified.
/// - isFlashing: A boolean to indicate if a flash should be applied. Example: `view.scaleEffect(isFlashing ? 1.5 : 1)`
/// - Returns: A view that applies its flash changes when it receives its signal.
func flash<T: Publisher, InnerContent: View>(
on publisher: T,
animation: Animation = .easeInOut(duration: 0.3),
delayBack: Double = 0.3,
#ViewBuilder content: #escaping (_ view: Self, _ isFlashing: Bool) -> InnerContent)
-> some View where T.Output == Void, T.Failure == Never {
// here I take whatever publisher we got and type erase it to AnyPublisher
// that just simplifies the type so I don't have to add extra generics below
self.modifier(
FlashStyleModifier(
publisher: publisher.eraseToAnyPublisher(),
animation: animation,
delayBack: delayBack,
content: { (view, isFlashing) in
return content(self, isFlashing)
}))
}
/// A helper function built on top of the method above.
/// Listens to a signal from a publisher and temporarily animates to a background color and text color.
/// - Parameters:
/// - publisher: The publisher that sends a signal to apply the temp styles.
/// - animation: The animation used to change properties.
/// - delayBack: How long, in seconds, after flashing starts should the styles start to revert. Typically this is the same duration as the animation.
/// - originalBackgroundColor: The normal state background color
/// - flashBackgroundColor: The background color when flashing.
/// - originalForegroundColor: The normal text color.
/// - flashForegroundColor: The text color when flashing.
/// - Returns: A view that flashes it's background and text color.
func flash<T: Publisher>(
on publisher: T,
animation: Animation = .easeInOut(duration: 0.3),
delayBack: Double = 0.3,
originalBackgroundColor: Color,
flashBackgroundColor: Color,
originalForegroundColor: Color,
flashForegroundColor: Color)
-> some View where T.Output == Void, T.Failure == Never {
// here I take whatever publisher we got and type erase it to AnyPublisher
// that just simplifies the type so I don't have to add extra generics below
self.flash(on: publisher, animation: animation) { view, isFlashing in
return view
// Need to apply arbitrary foreground color, but it's not animatable but need for colorMultiply to work.
.foregroundColor(.white)
// colorMultiply is animatable, so make foregroundColor flash happen here
.colorMultiply(isFlashing ? flashForegroundColor : originalForegroundColor)
// Apply background AFTER colorMultiply so that background color is not unexpectedly modified
.background(isFlashing ? flashBackgroundColor : originalBackgroundColor)
}
}
}
/// A view modifier that temporarily applies styles based on a signal from a publisher.
struct FlashStyleModifier<InnerContent: View>: ViewModifier {
#State
private var isFlashing = false
let publisher: AnyPublisher<Void, Never>
let animation: Animation
let delayBack: Double
let content: (_ view: Content, _ isFlashing: Bool) -> InnerContent
func body(content: Content) -> some View {
self.content(content, isFlashing)
.onReceive(publisher) { _ in
withAnimation(animation) {
self.isFlashing = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + delayBack) {
withAnimation(animation) {
self.isFlashing = false
}
}
}
}
}
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.