I'm working on my view and I'm having an issue with getting a shadow around a button within the stack view. Most of the work I have done has been within the storyboard directly.
Here is the method I am using to apply the shadow to the view
func addShadow(to view: UIView) {
view.layer.shadowColor = shadowColor
view.layer.shadowOpacity = shadowOpacity
view.layer.shadowOffset = shadowOffset
if let bounds = view.subviews.first?.bounds {
view.layer.shadowPath = UIBezierPath(rect: bounds).cgPath
}
view.layer.shouldRasterize = true
}
and this is how I'm finding the button within the view from ViewController.swift
for subview in self.view.subviews {
if subview.isKind(of: UIButton.self) && subview.tag == 1 {
addShadow(to: subview)
}
}
I know the problem stems from the stack view and the UIView inside of the stack view that holds the button. (self.view > UIStackView > UIView > [UIButton, UILabel])
I know I could do this with recursion in the for-loop but I'm trying to be a little more precise to optimize performance and would prefer to add the shadows in one shot.
You have a few options:
add the shadow in the storyboard itself
add an outlet to the button, then add shadow in code
add the button to a collection, then enumerate over the collection adding shadows
recursively add the shadows (this isn't going to hit performance nearly as hard as you're thinking, adding the shadows hurts performance more than doing this recursively)
You are correct in that the button is a view on the stack view, so your for loop doesn't hit the button directly to add a shadow to it.
The easiest way to solve this is by far the recursive way, or something like this:
func addShadowsTo(subviews: [UIView]) {
for subview in subviews {
if subview.isKind(of: UIButton.self) && subview.tag == 1 {
addShadow(to: subview)
}
if let stackView = subview as? UIStackView {
addShadowToSubviews(subviews: stackView.subviews)
}
}
}
func viewDidload() {
super.viewDidLoad()
addShadowsTo(subviews: view.subviews)
}
If you want some instructions on how to do any of the other ways, just comment.
Related
Let's say you have a superview that has a smaller size than its subview. You set the clipsToBound property of the superview to false. If you tap on the protruding area of the subview that is outside of the bounds of the superview, why does the hit test return nil?
My understanding is that the hit test starts from the subview and work its way up to the superview. So why does the property of the superview that is to be tested later than the subview matter? Or does the hit test start from the root to tree views like the view controller -> the main view -> subviews?
I found a custom hit-test from here, which does allow you to tap on the subview's area outside of the bounds of the superview, but I'm not sure why reversing the order of subviews make a difference(it works, I'm just not sure why). My example even only has one subview.
class ViewController: UIViewController {
let superview = CustomSuperview(frame: CGRect(origin: .zero, size: .init(width: 100, height: 100)))
let subview = UIView(frame: CGRect(origin: .zero, size: .init(width: 200, height: 200)))
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.superview)
self.superview.backgroundColor = .red
self.superview.clipsToBounds = false
self.superview.addSubview(self.subview)
self.subview.backgroundColor = .blue
let tap = UITapGestureRecognizer(target: self, action: #selector(tapped))
self.subview.addGestureRecognizer(tap)
}
#objc func tapped(_ sender: UIGestureRecognizer) {
print("tapped")
}
}
class CustomSuperview: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if clipsToBounds || isHidden || alpha == 0 {
return nil
}
for subview in subviews.reversed() {
let subPoint = subview.convert(point, from: self)
if let result = subview.hitTest(subPoint, with: event) {
return result
}
}
return nil
}
}
My understanding is that the hit test starts from the subview and work its way up to the superview.
Then your understanding is completely wrong. Let’s fix that. First, let's clear up some other misunderstandings:
The reversed is a total red herring; it has nothing to do with it. The subviews are always tested in reverse order, because a subview in front needs to take precedence over a subview behind.
The clipsToBounds is a total red herring too. All it does is change whether you can see a subview outside its superview; it does not have any effect on whether you can touch a subview outside its superview.
Okay, so how does this work? Let's take view V which contains a subview A. Let's suppose that A is outside V. Assume you can see A, and you tap A.
Now hit-testing begins. But here's the thing: it starts at the level of the window, and works its way down the view hierarchy. So the window starts by interrogating its subviews; there is just one, the main view of the view controller.
So now we recurse, and the main view of the view controller interrogates its subviews. One of those is V. "Hey, V, was the tap inside you?" "No!" (You have to agree that that is the correct answer, because we already said that A is outside V.)
So the main view of the view controller gives up on V, and never finds out that the tap was on A, because we never recursed down that far. So it reports back up the chain: "The tap was not on any of my subviews, so I have to report that the tap was on me." The tap has fallen through to the view controller's main view.
But you can change that behaviour by overriding the implementation of hitTest:
override func hitTest(_ point: CGPoint, with e: UIEvent?) -> UIView? {
if let result = super.hitTest(point, with:e) {
return result
}
for sub in self.subviews.reversed() {
let pt = self.convert(point, to:sub)
if let result = sub.hitTest(pt, with:e) {
return result
}
}
return nil
}
I have a UIView that has another UIView as a subview, I want to animate these views when the user performs a specific action, I want to do this using the snapshotView method.
The problem is that I am unable to snapshot only the main view without the other one being a part of it, and I can't seem to access the subview either to transform it. So my question is, can I snapshot a UIView, without its subviews, or can I access a subview through a snapshot?
Here is simplified demo of possible approach (hide all subviews before snapshot and show right after done)
extension UIView {
func snapshotMe() -> UIView? {
_ = self.subviews.compactMap { $0.isHidden = true }
defer {
_ = self.subviews.compactMap { $0.isHidden = false }
}
return self.snapshotView(afterScreenUpdates: true)
}
}
of course if your view subviews might be in already mixed hidden/visible state then in provided above extension you have to filter visible only subview in advance.
Once you snapshot a view, it’s essentially just a flattened image. So as a workaround, you could remove the subview from your view, snapshot the view, put the subview back. Or you can snapshot your subview. Or you could do both.
I have a UICollectionViewCell subclass which has a UIStackView on it.
The StackView is populated with UIButtons and everything looks fine
however the buttons are untappable.
If I layout the same buttons on a UIScrollView (for the sake of experiment) instead of on a stackview, the buttons respond fine so it seems like something with the stackview is causing the issue
Any ideas?
Here is the code of how I add buttons to the StackView
func prepareStackView(buttonsArray: Array<UIButton>) {
var rect:CGRect = (stackView?.frame)!
rect.size.width = btn.frame.size.width * buttonsArray.count
stackView?.frame = rect //The frame of the stackview is set as such so that it looks exactly like I want
//Add all the buttons
for btn in buttonsArray {
//The buttons already have their selectors set
stackView?.addArrangedSubview(btn)
}
}
Use this code for UIButton to respond to click event:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if clipsToBounds || isHidden || alpha == 0 {
return nil
}
for subview in subviews.reversed() {
let subPoint = subview.convert(point, from: self)
if let result = subview.hitTest(subPoint, with: event) {
return result
}
}
return nil
}
Helpful link:
Capturing touches on a subview outside the frame of its superview using hitTest:withEvent:
I have also encountered similar problem and able to resolve. Please check my post where I have described the scenario of this type of cause and tried to explain how I was able to resolve.
UIButton is not clickable after first click
I have a static tableViews and a button to change the colour scheme (theme) of the app. Is there a fast way for me to change all the label colours throughout the table from white to black?
im thinking something like the following.
Pseudo code:
for subview in view.subviews {
if subview is UILabel {
subview.fontColor = .black
}
}
So when the orange switch is on the "Light" side I would like all labels to go black. I have used story board to construct it, so it would be nice if I could avoid having to connect all labels to the .swift file.
I am writing about code for your sudo code you need to iterate with recursion.
func getLabelsInView(view: UIView) -> [UILabel] {
var results = [UILabel]()
for subview in view.subviews as [UIView] {
if let label = subview as? UILabel {
results += [label]
} else {
results += getLabelsInView(view: subview)
}
}
return results
}
Call any where from you need to change color
let labels = getLabelsInView(self.view) // or any other view / table view
for label in labels {
label.textColor = .black
}
I am using project from github as a reference.
project URL:
https://github.com/lephuocdai/iOSsample/tree/master/PageViewDemo
In this project i want to show the UIPageControl at top left position .
I tried setting the rect property of pageControl using CGRectMake() to some value ;But it shows always at bottom center
here s a very neat and 100% effective way to get to change the position of the pageControl
extension UIPageViewController {
override open func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
for subV in self.view.subviews {
if type(of: subV).description() == "UIPageControl" {
let pos = CGPoint(x: newX, y: newY)
subV.frame = CGRect(origin: pos, size: subV.frame.size)
}
}
}
}
The project uses a UIPageViewController to handle the display and movement through the content.
You can supply data to that object so it displays a UIPageControl as you say.
BUT you have no control over the display of that item, other than maybe some colour styling.
If you want to position it, you'll need to implement you're own instance of UIPageControl and handle it's content, position and changes manually.
Override the viewDidLayoutSubviews() of the pageviewcontroller and use this
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// get pageControl and scroll view from view's subviews
let pageControl = view.subviews.filter{ $0 is UIPageControl }.first! as! UIPageControl
let scrollView = view.subviews.filter{ $0 is UIScrollView }.first! as! UIScrollView
// remove all constraint from view that are tied to pagecontrol
let const = view.constraints.filter { $0.firstItem as? NSObject == pageControl || $0.secondItem as? NSObject == pageControl }
view.removeConstraints(const)
// customize pagecontroll
pageControl.translatesAutoresizingMaskIntoConstraints = false
pageControl.addConstraint(pageControl.heightAnchor.constraintEqualToConstant(35))
pageControl.backgroundColor = view.backgroundColor
// create constraints for pagecontrol
let leading = pageControl.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor)
let trailing = pageControl.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor)
let bottom = pageControl.bottomAnchor.constraintEqualToAnchor(scrollView.topAnchor, constant:8) // add to scrollview not view
// pagecontrol constraint to view
view.addConstraints([leading, trailing, bottom])
view.bounds.origin.y -= pageControl.bounds.maxY
}
is your page control encompassed within some other View, if so then you may be setting the co-ordinates wrong, try to put log of your page control.frame so as to know where it lies
If using storyboards, place a UIPageControl object using the menu in the bottom right and set constraints.
If using frames, just programmatically add it:
var pageControl = UIPageControl()
pageControl.frame = CGRectMake(0,0,0,0) <- These are the coordinates.
self.view.addSubView(pageControl)
if you set the frame for UIPageControl , it doesnot work.
Other than this you can set the transform.
[_pageControl setTransform:CGAffineTransformMakeTranslation(100, 0.0)];
enjoy coding