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.
Related
I'm building a collectionview. Below of it I placed some buttons as shown in the picture.
What I want is to make the UICollectionView background pass taps below, so the desired buttons can receive taps.
I don't need to add Tap gesture recognizers to the background view (the problem I'm describing is just an example here), I need the buttons' actuons to be triggered directly when they're tapped.
I thought I could do this by making the background clear or disabling user interaction for the background view. While disabling it for the entire collection view works, this other way does not.
How can I make the background view of my collectionView be "invisible" so that taps go straight to the below buttons instead of going to the collectionview background?
The following is an example of my layout.
Assuming your collectionView and your buttons share the same superview, this should do the trick.
What you want to do is bypass the backgroundView and forward hits to the subviews underneath the collectionView.
Notice that we are picking the last subview with the matching criteria. That is because the last subview in the array is the closest to the user's finger.
class SiblingAwareCollectionView: UICollectionView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hit = super.hitTest(point, with: event)
guard hit === backgroundView else {
return hit
}
let sibling = superview?
.subviews
.filter { $0 !== self }
.filter { $0.canHit }
.last { $0.point(inside: convert(point, to: $0), with: event) }
return sibling ?? hit
}
}
If you look at the documentation for hitTest(_:with:) it says:
This method ignores view objects that are hidden, that have disabled user interactions, or have an alpha level less than 0.01.
For convenience, here is an extension to ensure we are playing by the rules:
extension UIView {
var canHit: Bool {
!isHidden && isUserInteractionEnabled && alpha >= 0.01
}
}
I am trying to get all views inside my UIViewController.
I am using this code:
for subview : UIView in self.view.subviews{
//code here
}
the problem is that I don't get views that are inside a UIView.
so I get all view except of their children.
please, how can I get ALL Views that are either child or parent inside my UIViewController? even if a view has a child that has a child that has a child. I want to go through all views.
thanx in advance :)
(please in swift 4.0)
You need to recursively process all subviews.
func processSubviews(of view: UIView) {
// 1. code here do something with view
for subview in view.subviews {
// 2. code here do something with subview
processSubviews(of: subview)
// 3. code here do something with subview
}
// 4. code here do something with view
}
You need to put your code at either position 1, 2, 3, or 4 (or possibly two or more of those places) depending on the results you want.
Then you can call this as:
processSubviews(of: self.view)
For get all views (as Label, Button, View, ...) in your view hierarchy and access to all child view in your viewController you should using from a recursive func to iterate all views :
func getAllSubviews(view:UIView) {
for subview in view.subviews {
if let takenLabel = subview as? UILabel {
takenLabel.backgroundColor = UIColor.red
}
getAllSubviews(view:subview)
}
}
You can using from this func to access other view like button and change them .
in this case i'm changing background color of all label in viewController to red
There are many different way to get subviews
To Get all the subviews in the parent view
for subview in view.subviews{
// you can get all of your views one by one here.
}
To Filter if any tagged view is available or not (lets assume view is tagged by 100)
view.subviews.filter{$0.tag == 100}
Extensions to check if Any view contains sub views
extension UIView{
func isAvailabletoParentView(inParentView : UIView, subViewTag : Int) -> Bool{
var isAvailable = false
for subviews in inParentView.subviews{
if subviews.tag == subViewTag{
isAvailable = true
return true
}else{
isAvailable = false
}
}
return isAvailable
}
}
Call Extension like
subview.isAvailabletoParentView(inParentView: parentView, subViewTag: anyTagToSubView)
Hope it will helps , Happy coding ๐.
Issue: The viewWithGesture contains the viewUserSees, and is draggable within the blue containerView. However, the viewWithGesture is a subView of the containerView, so when the viewWithGesture is at an extreme (illustrated here - half in and half out of the containerView), only half of the viewWithGesture responds to touches, making it very hard to drag.
Note: I realize I should redo all the math that keeps it in the container and move it outside of the containerView, but I am very curious to learn how to do this the "worse" way.
I have researched this a bunch and tried to implement hittest() and pointInside(), but so far I have managed to just make the app crash spectacularly.
Is there a good, relatively clean way to let the user grab from outside the containerView? (swift3 if possible)
EDIT: The green box is transparent and half of it is in the containerView and half is not.
In order for a view to receive a touch, the view and all its ancestors must return true from pointInside:withEvent:.
Normally, pointInside:withEvent: returns false if the point is outside the view's bounds. Since a touch in the green area is outside the container view's bounds, the container view returns false, so the touch won't hit the gesture view.
To fix this, you need to create a subclass for the container view and override its pointInside:withEvent:. In your override, return true if the point is in the container view's bounds or in the gesture view's bounds. Perhaps you can be lazy (especially if your container view doesn't have many subviews) and just return true if the point is in any subview's bounds.
class ContainerView: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if super.point(inside: point, with: event) { return true }
for subview in subviews {
let subviewPoint = subview.convert(point, from: self)
if subview.point(inside: subviewPoint, with: event) { return true }
}
return false
}
}
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.
This question already has answers here:
Is there a UIView resize event?
(7 answers)
Closed 7 months ago.
I am trying some stuffs out with CATiledLayer inside UIScrollView.
Somehow, the size of UIView inside the UIScrollView gets changed to a large number. I need to find out exactly what is causing this resize.
Is there a way to detect when the size of UIView(either frame, bounds) or the contentSize of UIScrollView is resized?
I tried
override var frame: CGRect {
didSet {
println("frame changed");
}
}
inside UIView subclass,
but it is only called once when the app starts, although the size of UIView is resized afterwards.
There's an answer here:
https://stackoverflow.com/a/27590915/5160929
Just paste this outside of a method body:
override var bounds: CGRect {
didSet {
// Do stuff here
}
}
viewWillLayoutSubviews() and viewDidLayoutSubviews() will be called whenever the bounds change. In the view controller.
You can also use KVO:
You can set a KVO like this, where view is the view you want to observe frame changes for:
self.addObserver(view, forKeyPath: "center", options: NSKeyValueObservingOptions.New, context: nil)
And you can get the changes with this notification:
override func observeValueForKeyPath(keyPath: String!, ofObject object: AnyObject!, change: NSDictionary!, context: CMutableVoidPointer) {
}
The observeValueForKeyPath will be called whenever the frame of the view you are observing changes.
Also remember to remove the observer when your view is about to be deallocated:
view.removeObserver(self, forKeyPath:"center")
You can create a custom class, and use a closure to get the updated rect comfortably. Especially handy when dealing with classes (like CAGradientLayer which want you to give them a CGRect):
GView.swift:
import Foundation
import UIKit
class GView: UIView {
var onFrameUpdated: ((_ bounds: CGRect) -> Void)?
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: layer)
self.onFrameUpdated?(self.bounds)
}
}
Example Usage:
let headerView = GView()
let gradientLayer = CAGradientLayer()
headerView.layer.insertSublayer(gradientLayer, at: 0)
gradientLayer.colors = [
UIColor.mainColorDark.cgColor,
UIColor.mainColor.cgColor,
]
gradientLayer.locations = [
0.0,
1.0,
]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
headerView.onFrameUpdated = { _ in // here you have access to `bounds` and `frame` with proper values
gradientLayer.frame = headerView.bounds
}
If you are not adding your views through code, you can set the Custom Class property in storyboard to GView.
Please note that the name GView was chosen as a company measure and probably choosing something like FrameObserverView would be better.
This is a simple and not-too-hacky solution: You remember the last size of your view, compare it to the new size in an overridden layoutSubviews method, and then do something when you determine that the size has changed.
/// Create this as a private property in your UIView subclass
private var lastSize: CGSize = .zero
open override func layoutSubviews() {
// First call super to let the system update the layout
super.layoutSubviews()
// Check if:
// 1. The view is part of the view hierarchy
// 2. Our lastSize var doesn't still have its initial value
// 3. The new size is different from the last size
if self.window != nil, lastSize != .zero, frame.size != lastSize {
// React to the size change
}
lastSize = frame.size
}
Note that you don't have to include the self.window != nil check, but I assume that in most cases you are only interested in being informed of size changes for views that are part of the view hierarchy.
Note also that you can remove the lastSize != .zero check if you want to be informed about the very first size change when the view is initially displayed. Often we are not interested in that event, but only in subsequent size changes due to device rotation or a trait collection change.
Enjoy!
The answers are correct, although for my case the constraints I setup in storyboard caused the UIView size to change without calling back any detecting functions.
For UIViews, as easy as:
override func layoutSubviews() {
super.layoutSubviews()
setupYourNewLayoutHereMate()
}
You can use the FrameObserver Pod.
It is not using KVO or Method Swizzling so won't be breaking your code if the underlying implementation of UIKit ever changes.
whateverUIViewSubclass.addFrameObserver { frame, bounds in // get updates when the size of view changes
print("frame", frame, "bounds", bounds)
}
You can call it on a UIView instance or any of its subclasses, like UILabel, UIButton, UIStackView, etc.
STEP 1:viewWillLayoutSubviews
Called to notify the view controller that its view is about to
layout its subviews
When a view's bounds change, the view adjusts the position of its
subviews. Your view controller can override this method to make
changes before the view lays out its subviews. The default
implementation of this method does nothing.
STEP 2:viewDidLayoutSubviews
Called to notify the view controller that its view has just laid out
its subviews.
When the bounds change for a view controller's view, the view
adjusts the positions of its subviews and then the system calls this
method. However, this method being called does not indicate that the
individual layouts of the view's subviews have been adjusted. Each
subview is responsible for adjusting its own layout.
Your view controller can override this method to make changes after
the view lays out its subviews. The default implementation of this
method does nothing.
Above these methods are called whenever bounds of UIView is changed