Changing Values with WillSet Property Observer - ios

I am attempting to create a UIView subclass. In the initialization, I set minimum and maximum y-axis origin values I want the subclass to have. This view will be moved up and down programmatically, by user gestures or through an animation. I was hoping I could use the willSet property user for this:
override var frame: CGRect {
willSet {
if newValue.origin.y > maximumOriginY {
self.frame.origin.y = maximumOriginY
}
if newValue.origin.y < minimumOriginY {
self.frame.origin.y = minimumOriginY
}
}
}
However, it doesn't seem to do anything. I have tried using DidSet property observer, but this resets the origin after it has been set, resulting in a stutter animation. Any ideas on how to get this to work with property observers or another way? Thanks

it is not possible to change the values within the willSet observer. overriding the getter and setter could work:
override var frame: CGRect {
get { return super.frame }
set {
var tempFrame = newValue
tempFrame.origin.y = min(max(tempFrame.origin.y, minimumOriginY), maximumOriginY)
super.frame = tempFrame
}
}

Related

Swift: Extension, All paths through this function will call itself [duplicate]

This question already has answers here:
Swift 4.2 Setter Getter, All paths through this function will call itself
(3 answers)
Why does Swift not allow stored properties in extensions?
(1 answer)
Closed 3 years ago.
I have a warning that I can not delete
All paths through this function will call itself
My code:
extension UIView {
var isLowAlpha: Bool {
get {
return self.isLowAlpha
}
set {
self.isUserInteractionEnabled = !newValue
self.alpha = newValue ? 0.3 : 1
}
}
}
I can not modify the code with this, because I have an error extension must not contain stored property ..:
extension UIView {
var isLowAlpha: Bool {
didSet {
self.isUserInteractionEnabled = !isLowAlpha
self.alpha = isLowAlpha ? 0.3 : 1
}
}
}
What is the solution?
One possible solution is to revert the process:
var isLowAlpha: Bool {
get {
return !isUserInteractionEnabled
}
set {
isUserInteractionEnabled = !newValue
alpha = newValue ? 0.3 : 1
}
}
or better, since you are not interested in the getter, make it a function:
func setIsLowAlpha(_ lowAlpha: Bool) {
self.isUserInteractionEnabled = !lowAlpha
self.lowAlpha = newValue ? 0.3 : 1
}
Anyway, looking at your code, you probably want to implement that a view is disabled. That's usually a task for UIControl subclasses, not UIView.
Also note the same can be implemented using a wrapper view:
class AlphaView: UIView {
var isLowAlpha: Bool = false {
didSet {
isUserInteractionEnabled = !isLowAlpha
alpha = isLowAlpha ? 0.3 : 1
}
}
}
And put your views inside.
In the first case you were recursively calling the variable's getter so it would never return. The way to fix this would be with a stored property _isLowAlpha, but unfortunately, as the second error mentions, Swift extensions cannot contain stored variables; they can only contain computed properties. If you really needed to add another property you would need to instead subclass UIView instead of making an extension.
However, in this case you can kind of cheat as long as you are not setting the UIView's alpha property elsewhere by directly using the alpha property:
extension UIView {
var isLowAlpha: Bool {
get {
return self.alpha == 0.3;
}
set {
self.alpha = newValue ? 0.3: 1
self.isUserInteractionEnabled = !newValue
}
}
}

UIView default properties and custom properties

I created UIView with default properties
var color = Label.Color.black { didSet{ setNeedsDisplay() }}
var count = Label.Count.one.rawValue { didSet{ setNeedsDisplay() }}
var shading = Label.Shading.empty { didSet{ setNeedsDisplay() }}
var shape = Label.Shape.triangle { didSet{ setNeedsDisplay() }}
var isSelected = false
And then I add some subviews using the same UIView in ViewController
func addCardsOnGridView() {
grid.frame = cardsView.bounds
grid.cellCount = game.cardsOnDeck.count
for cardView in cardsView.subviews{
cardView.removeFromSuperview()
}
for index in 0..<grid.cellCount {
if let cellFrame = grid[index] {
let card = game.cardsOnDeck[index]
let cardView = CardsView(frame: cellFrame.insetBy(dx: CardSize.inset, dy: CardSize.inset))
cardView.color = card.label.color
cardView.count = card.label.count.rawValue
cardView.shape = card.label.shape
cardView.shading = card.label.shading
//cardView.isSelected = card.isSelected
//print(cardsView.isSelected)
cardsView.addSubview(cardView)
} else {
print("grid[\(index)] doecnt exist")
}
}
}
and then I got this UIView with default comes as superview and subviews above superview:
How can I remove view with default UIView properties and redraw with custom properties?
I can't understand your question completely. What I understand is that you want to get specific view like triangle view.
One way to achieve this is by assigning tag to every subview like below when you are adding them in mainView
aView.tag = 6
and then when you want to get a subview, you can get using viewWithTag function like below
if let aCustomView = mainView.viewWithTag(6) as? CustomView {
aCustomView.myColor = .blue
}
An other way to get custom view is by looping thought subview and try to cast view into custom class like below
mainView.subviews.forEach({ (aView) in
if aView.tag == 6,
let customView = aView as? CustomView {
customView.myColor = .blue
}
})

Correct way to subclass CAShapeLayer

Inspired by this example, I have a created custom CALayer subclasses for wedges and arcs. The allow me to draw arcs and wedges and animate changes in them so that they sweep radially.
One of the frustrations with them, is that apparently when you go this route of having a subclass drawInContext() you are limited by the clip of the layer's frame. With stock layers, you have the masksToBounds which is by default false! But it seems once you the subclass route with drawing, that because implicitly and unchangeably true.
So I thought I would try a different approach, by subclassing CAShapeLayer instead. And instead of adding a custom drawInContext(), I would simply have my variables for start and sweep update the path of the receiver. This works nicely BUT it will no longer animate as it used to:
import UIKit
class WedgeLayer:CAShapeLayer {
var start:Angle = 0.0.rotations { didSet { self.updatePath() }}
var sweep:Angle = 1.0.rotations { didSet { self.updatePath() }}
dynamic var startRadians:CGFloat { get {return self.start.radians.raw } set {self.start = newValue.radians}}
dynamic var sweepRadians:CGFloat { get {return self.sweep.radians.raw } set {self.sweep = newValue.radians}}
// more dynamic unit variants omitted
// we have these alternate interfaces because you must use a type
// which the animation framework can understand for interpolation purposes
override init(layer: AnyObject) {
super.init(layer: layer)
if let layer = layer as? WedgeLayer {
self.color = layer.color
self.start = layer.start
self.sweep = layer.sweep
}
}
override init() {
super.init()
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updatePath() {
let center = self.bounds.midMid
let radius = center.x.min(center.y)
print("updating path \(self.start) radius \(radius)")
if self.sweep.abs < 1.rotations {
let _path = UIBezierPath()
_path.moveToPoint(center)
_path.addArcWithCenter(center, radius: radius, startAngle: self.start.radians.raw, endAngle: (self.start + self.sweep).radians.raw, clockwise: self.sweep >= 0.radians ? true : false)
_path.closePath()
self.path = _path.CGPath
}
else {
self.path = UIBezierPath(ovalInRect: CGRect(around: center, width: radius * 2, height: radius * 2)).CGPath
}
}
override class func needsDisplayForKey(key: String) -> Bool {
return key == "startRadians" || key == "sweepRadians" || key == "startDegrees" || key == "sweepDegrees" || key == "startRotations" || key == "sweepRotations" || super.needsDisplayForKey(key)
}
}
Is it not possible to make it regenerate the path and update as it animates the value? With the print() statement there, I can see it interpolating through the values as expected during an animation. I have tried adding setNeedsDisplay() in various locations, but to no avail.
There are a couple tricks to getting this to work properly:
Don't use the didSet. Rather, you should override display() and update self.path there.
Use (presentation() as! WedgeLayer? ?? self).sweep to access the property. (presentation, known as presentationLayer in Obj-C and Swift 2, allows you to access the currently-visible property values during animation.)
If you want implicit animation, implement actionForKey as described in this answer.

Design pattern for setAnimated and IBInspectable property in Swift

I can’t figure out how to incorporate a design pattern in to my Swift class which has a property with the following requirements:
Property with default value and is IBInspectable
accompanying setProperty:animated: method
All of the ways I’ve tried require a separate private ‘instance' variable e.g. _property, like below:
#IBInspectable var progress: CGFloat = 0.5 {
didSet {
_progress = progress
setNeedsDisplay()
}
}
private var _progress: CGFloat = 0.5
func setProgress(progress: CGFloat, animated: Bool)
{
if _progress == progress
{
return
}
if animated
{
// Animate changes
// [Animation code]
}
else
{
// No need to animate changes
setNeedsDisplay()
}
titleLabel?.text = "\(NSInteger(progress * 100))%"
// Update value
_progress = progress
}
This doesn’t account for getting the property value though. As a property cannot have a get and didSet method.
So what is the correct way of having a property which is IBInspectable and can be set in the setAnimated method without automatically updating, bypassing the animation?

scrollViewDidEndDecelerating being called for a simple touch

I'm implementing UIScrollViewDelegate and doing a lot of stuff with it. However, I just found an annoying issue.
I expect scrollViewDidEndDecelerating: to be called always after scrollViewWillBeginDecelerating:. However, if I simply touch my ScrollView (actually I'm touching a button inside the scrollView), scrollViewDidEndDecelerating: gets called and scrollViewWillBeginDecelerating: was not called.
So how can I avoid scrollViewDidEndDecelerating: being called when I simply press a button inside my UIScrollView?
Thank you!
Create a member BOOL called buttonPressed or similar and initialise this to false in your init: method.
Set the BOOL to true whenever your button/s are hit, and then perform the following check:
-(void)scrollViewDidEndDecelerating: (UIScrollView*)scrollView
{
if (!buttonPressed)
{
// resume normal processing
}
else
{
// you will need to decide on the best place to reset this variable.
buttonPressed = NO;
}
}
I had the same problem I fixed it by making a variable that hold the current page num and compare it with the local current page variable if they r equal then don't proceed.
var currentPage : CGFloat = 0.0
var oldPage : CGFloat = 0.0
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView){
// Test the offset and calculate the current page after scrolling ends
let pageWidth:CGFloat = scrollView.frame.width
let currentPage:CGFloat = floor((scrollView.contentOffset.x-pageWidth/2)/pageWidth)+1
// Change the indicator
print("that's the self.currentPage \(self.currentPage) and that's the current : \(currentPage)")
guard self.currentPage != currentPage else { return }
oldPage = self.currentPage
self.currentPage = currentPage;
print("that's the old \(oldPage) and that's the current : \(currentPage)")
//Do something
}

Resources