Overriding UIStackView background color variable - swift

I was shocked to learn that ability to set a background color to a UIStackView come only starting from iOS14. In the older versions such attempts are just ignored.
I have to support older versions as well, so I wrote this code to fix the issue:
public extension UIStackView {
private var helperSubview: UIView {
subviews.first(where: { $0.id == "helperSubview" }) ?? {
let hsv = UIView()
hsv.id = "helperSubview"
insertSubview(hsv, at: 0)
hsv.fillSuperview()
return hsv
}()
}
override var backgroundColor: UIColor? {
didSet {
if #available(iOS 14.0, *) {
return
} else {
helperSubview.backgroundColor = backgroundColor
}
}
}
}
The code works just fine.
But there is one strange moment: in iOS14 didSet doesn't fire at all (like we can even don't check #availability). That suits me, this behaviour doesn't cause any problems. But I don't understand why does it behave like that?

You can't use extension to override a class property. You need to subclass UIStackView if you would like to override any property or method.
From the docs:
Extensions can add new functionality to a type, but they can’t override existing functionality

Related

Remove BackgroundView from UITargetedPreview in Swift

I'm trying to remove the background view for my UITargetPreview. I made the background color clear, however, you can still see the frame of the background.
This is what it currently looks like:
I currently have a view that has the text container and the image inside of it and that's what I use as the view for the UITargetedPreview.
Is there a way to only show the image and the text and not the background frame?
There is a tricky method to hide the shadow and to do that you should find a view with _UIPlatterSoftShadowView class name in the view hierarchy and then hide it.
func viewByClassName(view: UIView, className: String) -> UIView? {
let name = NSStringFromClass(type(of: view))
if name == className {
return view
}
else {
for subview in view.subviews {
if let view = viewByClassName(view: subview, className: className) {
return view
}
}
}
return nil
}
override func tableView(_ tableView: UITableView, willDisplayContextMenu configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
DispatchQueue.main.async {
if let window = UIApplication.shared.delegate?.window! {
if let view = self.viewByClassName(view: window, className: "_UIPlatterSoftShadowView") {
view.isHidden = true
}
}
}
}
NOTE: It's not documented internal class and can be changed anytime further but it works now on both ios 13/14.
Have you tried subclassing the UIView as a UIControl?
I had a similar issue but in my case the view for UITargetedPreview was glitchy. However, changing the UIView to a UIControl fixed everything.
try removing shadow of that background view.
You need to study UIBezierPath() to outline the specific area you want to enclose before you present the target view.
After that, you shall assign the specific path to shadow path / visible path
let params = UIPreviewParameters()
params.backgroundColor = .clear
if #available(iOS 14.0, *) {
params.shadowPath = bubblePath
} else {
params.visiblePath = bubblePath
}

How to Implement NSSlider Accessible for VoiceOver on MacOS?

I have a slider:NSSlider and valueLabel:NSTextField, and I'm wondering what's the proper way to make it accessible for VoiceOver users.
First I connected a send action for slider to sliderChanged function to update valueLabel.
valueLabel.stringValue = String(slider.integerValue)
VoiceOver reads the label correctly, but it reads the slider in percentage. To fix this, I changed sliderChanged function to setAccessibilityValueDescription.
slider.setAccessibilityValueDescription(String(slider.integerValue))
Now VoiceOver correctly reads the value for the slider. However, it sees both valueLabel and slider, so it's redundant.
I tried valueLabel.setAccessibilityElement(false), but VoiceOver doesn't seem to ignore.
Could someone advise what would be the proper way to implement this? Thanks!
The best way to do this is to create a custom "ContainerView" class (which inherits from UIView) that contains the label and the slider, make the ContainerView an accessibilityElement, and set its accessibilityTraits to "adjustable." By creating a ContainerView that holds both the valueLabel and the slider, you remove the redundancy that is present in your current implementation, while not affecting the layout or usability of the slider/valueLabel for a non-VoiceOver user. This answer is based on this video, so if something is unclear or you want more in-depth info, please watch the video!
Setting a view's UIAccessibilityTraits to be "Adjustable" allows you to use its functions accessibilityIncrement and accessibilityDecrement, so that you can update whatever you need to (slider, textfield, etc). This trait allows any view to act like a typical adjustable (without having to add UIGestureRecognizers or additional VoiceOver announcements).
I posted my code below for convenience, but it is heavily based on the video that I linked to above. (I personally am an iOS developer, so my Swift code is iOS-based)
Note -- I had to override the "accessibilityValue" variable -- this was to make VoiceOver announce changes in the slider whenever the user swiped up or down.
My ContainerView class contains the following code:
class ContainerView: UIView {
static let LABEL_TAG = 1
static let SLIDER_TAG = 2
var valueLabel: UILabel {
return self.viewWithTag(ContainerView.LABEL_TAG) as! UILabel
}
var slider: UISlider {
return self.viewWithTag(ContainerView.SLIDER_TAG) as! UISlider
}
override var accessibilityValue: String? {
get { return valueLabel.text }
set {}
}
override var isAccessibilityElement: Bool {
get { return true }
set { }
}
override var accessibilityTraits: UIAccessibilityTraits {
get { return UIAccessibilityTraitAdjustable }
set { }
}
override init(frame: CGRect) {
super.init(frame: frame)
valueUpdated()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
valueUpdated()
}
func valueUpdated() {
valueLabel.text = String(slider.value)
slider.sendActions(for: .valueChanged)
}
override func accessibilityIncrement() {
super.accessibilityIncrement()
slider.setValue(slider.value + 1, animated: true)
valueUpdated()
}
override func accessibilityDecrement() {
super.accessibilityDecrement()
slider.setValue(slider.value - 1, animated: true)
valueUpdated()
}
}
Hope this helps!

Subclassing Views

I want to subclass UIView to support gradients (border / fill). I have subclass with #IBInspectable vars, so I'm able to setup this behavior in IB.
I also need to subclass also UIButton with the same methods. Is there any way I can do it without copying all the methods and instance variables to that subclass of UIButton?
Multiple class inheritance is not allowed in Swift (only multiple protocol inheritance), therefore what you are trying to achieve is not straight-forwardly possible.
One of the possible workarounds, however, is to use extension for UIView. Provided that both UIView (itself) and UIButton are variants of UIView, the following code would apply to them all.
Example with corner radius:
extension UIView {
#IBInspectable var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
layer.masksToBounds = newValue > 0
}
}
}
Now, even non-subclassed UIButton will acquire this property, which will be reflected in Interface Builder.
You can try employing the power of extensions to implement unified IBInspectables. One obstacle you will inevitably stumble across, however, is that you won't be able to have any storage in the extension. But for several cases this can serve as a solution.
P.S. Few other examples of use (added to UIView extension):
#IBInspectable var borderColor: UIColor? {
get { return layer.borderColor.map(UIColor.init) }
set { layer.borderColor = newValue?.cgColor }
}
#IBInspectable var borderWidth: CGFloat {
get { return layer.borderWidth }
set { layer.borderWidth = newValue }
}

Why my background gradient view is not loading in my customer UINavigationController class

I'm trying to set a gradient to the background of my subclassed NavigationController. When I add a colour to the same code it works well but I can't seem to let my gradient show up. I created a subclass of a UIView that returns a CAGradientLayer as its background view.
Here is my subclassed UIView : (Note the colours are weird so I am sure its loading the right Gradient.
#IBDesignable
class GenericBackgrounView: UIView {
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
///The roundness for the corner
#IBInspectable var cornerRadius: CGFloat = 0.0 {
didSet{
setupGradient()
}
}
func setupGradient() {
//let gradientColors = [bgDarkColor.cgColor, bgDarkColor.blended(withFraction: 0.5, of: bgLightColor).cgColor, bgLightColor.cgColor]
let gradientColors = [UIColor.brown.cgColor, UIColor.red.blended(withFraction: 0.5, of: UIColor.cyan).cgColor, UIColor.yellow.cgColor]
gradientLayer.colors = gradientColors
gradientLayer.locations = ESDefault.backgroundGradientColorLocations
setNeedsDisplay()
}
var gradientLayer: CAGradientLayer {
return self.layer as! CAGradientLayer
}
override func awakeFromNib() {
super.awakeFromNib()
setupGradient()
}
override func prepareForInterfaceBuilder() {
setupGradient()
}
}
And Here is my UINavigationController :
class GenericNavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
let backView = GenericBackgrounView(frame: self.view.frame)
backView.bounds = self.view.bounds
self.view.backgroundColor = UIColor.clear
self.view.addSubview(backView)
self.view.sendSubview(toBack: backView)
// Do any additional setup after loading the view.
}
}
Also note that my GenericBackgroundView works fine when I use it for any views I add in the interface builder.
I have been at this to long. I think I will suggest to Apple to setup some kind of Theming API in both code and Interface Builder... and the ability to add gradients straight into Interface Builder...
Thanks for you help.
Instead of setting it up in awakeFromNib() , try calling it in viewDidLayoutSubviews(). Reason is that in viewDidLayoutSubviews() will have the correct frame of the view , while in awakeFromNib() you wouldn't know the right frame of the view.From Apple Documentation.
Alright, I've tinkered a bit and found some working code. I would still love to understand the reason why this works and not the way I had it before. I hate feeling it works by magic...
here is the working code : (Remember that my gradient is in form of CAGradientLayer and I have made some static variable that has defaults.
import UIKit
class GenericNavigationController: UINavigationController {
let backViewGradient = Default.testGradientCALayer
override func viewDidLoad() {
super.viewDidLoad()
setupBackground()
}
func setupBackground() {
backViewGradient.frame = self.view.frame
self.view.layer.insertSublayer(backViewGradient, at: 0)
}
override func prepareForInterfaceBuilder() {
setupBackground()
}
}
What I'm wondering is how come since all the UIControls that are subclassed from UIView don't all work the same. They should all have a view that is the background and we should all be able to either add a layer or a subview to them and be able to get my previous code to work or my latest code too which does not work with TableViewCells.
I will leave this question open because I would love to know the truth behind this. I don't think I can fully grasp Swift or Xcode if it behaves somewhat magically and inconsistent.

Is there an equivalent of CSS classes on controls in Swift and iOS?

I have a lot of UIButtons on a view that have to be styled through code. I need to give them all rounded borders, which cannot be done in XCode's interface builder.
So I'm wondering, is there a way in Swift to style a whole bunch of elements all at once, like using a CSS class to style stuff on the web?
Each button has an IBOutlet in my controller and it would be nice to style them all at the same time.
"which cannot be done in XCode's interface builder".
Sure it can, but you need to consider the various factors which limit you to think in terms of CSS.
Design an extension to a UIButton. Then make it IBDesignable. If you want to actually see it in IB, make it IBInspectable. Maybe your code will look something like this:
#IBDesignable
public class Button: UIButton {
#IBInspectable public var borderColor:UIColor? {
didSet {
layer.borderColor = borderColor?.cgColor
}
}
#IBInspectable public var borderWidth:CGFloat = 0 {
didSet {
layer.borderWidth = borderWidth
}
}
#IBInspectable public var cornerRadius:CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
layer.masksToBounds = newValue > 0
}
}
}
In particular, pay attention to the cornerRadius inspectable property.
Finally here's a link that however old, I still find worthy of explaining things better than I can.