I have a custom UIView using UIKit Dynamics to perform an animation when the user taps on a button. The view in question is a simple one, that I lay out manually in layoutSubviews(). However, layoutSubviews() gets called for each frame of animation while UIKit Dynamics are in action, and any layout changes I make in that time (responding, for instance, to a taller status bar) result in distortion of my dynamic views.
How can I respond to a change in view size while a UIKit Dynamics animation is in progress?
Update
I created a demo project (which very closely matches my use case, though it's stripped down), and posted it on GitHub. The storyboard uses AutoLayout, but the view opts out of AutoLayout for laying out its own subviews with translatesAutoresizingMaskIntoConstraints = false. To reproduce the behavior, run in the simulator (I chose iPhone 5) and then hit ⌘Y as the star swings to witness the distortion. This is the view code:
import UIKit
class CustomView: UIView {
var swingingView: UIView!
var animator: UIDynamicAnimator!
var attachment: UIAttachmentBehavior!
var lastViewFrame = CGRectZero
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.translatesAutoresizingMaskIntoConstraints = false
swingingView = UIImageView(image: UIImage(named: "Star"))
self.addSubview(swingingView)
}
override func layoutSubviews() {
// Don't run for every frame of the animation. Only when responding to a layout change
guard self.frame != lastViewFrame else {
return
}
lastViewFrame = self.frame
swingingView.frame = CGRect(x: 0, y: self.frame.size.height / 2, width: 100, height: 100)
// Only run this setup code once
if animator == nil {
animator = UIDynamicAnimator(referenceView: self)
let gravity = UIGravityBehavior(items: [swingingView])
gravity.magnitude = 1.5
animator.addBehavior(gravity)
attachment = UIAttachmentBehavior(item: swingingView,
offsetFromCenter: UIOffset(horizontal: 0, vertical: swingingView.frame.size.height / -2),
attachedToAnchor: CGPoint(x: self.bounds.size.width / 2, y: 0))
attachment.length = CGFloat(250.0)
animator.addBehavior(attachment)
}
animator.updateItemUsingCurrentState(swingingView)
}
}
You should use func updateItemUsingCurrentState(_ item: UIDynamicItem) per the UIDynamicAnimator class reference
A dynamic animator automatically reads the initial state (position and
rotation) of each dynamic item you add to it, and then takes
responsibility for updating the item’s state. If you actively change
the state of a dynamic item after you’ve added it to a dynamic
animator, call this method to ask the animator to read and incorporate
the new state.
The issue is that you are re-setting the frame of a view that has been rotated (i.e. that has a transform applied to it). The frame is the size of the view within the parent's context. and thus you are unintentionally changing the bounds of this image view.
This issue is compounded by the fact that you're using the default content mode of .ScaleToFill and thus when the bounds of the image view change, the star is getting (further) distorted. (Note, the image wasn't square to start with, so I'd personally use .ScaleAspectFit, but that's up to you.)
Anyway, you should be able to remedy this problem by (a) setting the frame when you first add the UIImageView to the view hierarchy; (b) do not change the frame in layoutSubviews, but rather just adjust the center of the image view.
Related
I have a UIViewController that embeds a UITableView. This table view is 3/4 the size of the entire screen in height. the remaining 1/4th of the UIViewController has a rounded UIButton that triggers a new UIView on top of the parent view. (UITableView).
Upon instantiating and calling a UIView with a background that is set to:
self.backgroundColor = UIColor(black: 1, alpha: 0.5) it would normally fill the entire view with a black see-through background that will then have an additional UIView with the following constraints:
Leading: 10
Trailing: 10
Top: 50
Bottom: 50
this, in turn, gives me a 'Card' effect on top of the tableView. This 'Card' view then has a UITextview property that is supposed to show the keyboard when the user taps the view with the textview embedded.
The Problem:
Upon selecting the UITextview, or even touching this 'Card' view, the background table is being selected and interacted with. Neither the 'Card' textfield raises the keyboard nor does it make itself solely interactive as the parent table controller seems to be getting the touch events.
Is there any solution to this problem that is encountered in iOS 11? I never experienced this issue in iOS 10. I am using iPhone X for the further note on my issue.
Here is an example of the actual issue occurring.
NOTE: If I were to select the dark area of the card itself, the table view would receive touch events but not the text area within the card view. The reason this card view has the keyboard showing is that I called it manually in code with the textview.becomeFirstResponder method.
ADDITIONAL NOTES: I have also enabled the isUserInteractionEnabled = false on the overall presenting child view. Still does not work and parent view receives touches only.
Code for the Card
class Card: UIView {
// instantiating the textview
var textview = UITextview()
var cardView = UIView()
private override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor(black: 1, alpha: 0.5)
self.cardView.backgroundColor = UIColor.white
self.cardView.layer.cornerRadius = 8
// Just demo the textview
self.textview.frame = CGRect(x: 10, y: 100, width: 300. height: 100)
self.addSubview(cardView) // adding the cardView as a subview to the background colored view
cardView.addSubview(textview)
}
required init?(coder aDecoder: NSCoder) {
fatalError("Error")
}
private func ConstrainCardWith() {
// Constraints
}
}
extension UIView {
func Width()-> CGFloat {
return UIScreen.main.bounds.size.width
}
func Height()-> CGFloat{
return UIScreen.main.bounds.size.height
}
}
The question is your container view(cardview) has no frame, so your child view(textview) can not be touch cause it is out of bounds, so just add some frame in your card view will solve this question
I have come across a conundrum of sorts in regards unexpected (at least for me) sizes of UIViews.
Consider this UIViewController class. interfaceBuilderView was declared in a Storyboard file and constrained to take up the whole area of the UIView. So, I would expect to have interfaceBuilderView be the same size as programicallyCreatedView when calling *.frame.width. But they aren't.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var interfaceBuilderView: MyCustomView!
override func viewDidLoad() {
super.viewDidLoad()
let programmicallyCreatedView = MyCustomView(frame: self.view.frame)
//commented out this to get first picture
self.view.addSubview(programmicallyCreatedView)
self.interfaceBuilderView.setAppearance()
self.programmicallyCreatedView.setAppearance()
print(self.view.frame.width)//prints 375
print(self.interfaceBuilderView.frame.width)//prints 600
print(self.programmicallyCreatedView.frame.width)//prints 375
}
}
Now, consider this implementation of the MyCustomView class.
import UIKit
class MyCustomView: UIView {
func setAppearance() {
let testViewWidth: CGFloat = 200.0
let centerXCoor = (self.frame.width - testViewWidth) / 2.0
let testView = UIView(frame: CGRectMake(centerXCoor, 0, testViewWidth, 100))
testView.backgroundColor = UIColor.redColor()
self.addSubview(testView)
}
}
As you can see, I simply draw a red rectagle of width 200.0, and it is supposed to be centered. Here are the results.
Using the Interface Builder created view.
And using the programmatically created view.
As you can see, the programmatically created view achieves the desired results. No doubt because the size printed is the same as the superview (375).
Therefore, my question is simply why is this happening? Furthermore, how can I use a view declared in interface builder and programmatically add other views to it with dimensions and placement that I expect?
A few thoughts:
This code is accessing frame values in viewDidLoad, but the frame values are not yet reliable at that point. The view hasn't been laid out yet. If you're going to mess around with custom frame values, do this in viewDidAppear or viewDidLayoutSubviews.
Nowadays, we really don't generally use frame values anymore. Instead, we define constraints to define the layout programmatically. Unlike custom frame values, you can define constraints when you add the subviews in viewDidLoad.
You have the scene's main view, the MyCustomView and then yet another UIView which is red. That strikes me as unnecessarily confusing.
I would advise that you just add your programmatically created subview in viewDidLoad and specify its constraints. Using the new iOS 9 constraints syntax, you can just specify that it should be centered, adjacent to the top of the view, half the width, and one quarter the height:
override func viewDidLoad() {
super.viewDidLoad()
let redView = UIView()
redView.backgroundColor = UIColor.redColor()
redView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(redView)
NSLayoutConstraint.activateConstraints([
redView.centerXAnchor.constraintEqualToAnchor(view.centerXAnchor),
redView.topAnchor.constraintEqualToAnchor(view.topAnchor),
redView.widthAnchor.constraintEqualToAnchor(view.widthAnchor, multiplier: 0.5),
redView.heightAnchor.constraintEqualToAnchor(view.heightAnchor, multiplier: 0.25)
])
}
Clearly, adjust these constraints as suits you, but hopefully this illustrates the idea. Don't use frame anymore, but rather use constraints.
Background
In order to make a text view that scrolls horizontally for vertical Mongolian script, I made a custom UIView subclass. The class takes a UITextView, puts it in a UIView, rotates and flips that view, and then puts that view in a parent UIView.
The purpose for the rotation and flipping is so that the text will be vertical and so that line wrapping will work right. The purpose of sticking everything in a parent UIView is so that Auto layout will work in a storyboard. (See more details here.)
Code
I got a working solution. The full code on github is here, but I created a new project and stripped out all the unnecessary code that I could in order to isolate the problem. The following code still performs the basic function described above but also still has the slow loading problem described below.
import UIKit
#IBDesignable class UIMongolTextView: UIView {
private var view = UITextView()
private var oldWidth: CGFloat = 0
private var oldHeight: CGFloat = 0
#IBInspectable var text: String {
get {
return view.text
}
set {
view.text = newValue
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect){
super.init(frame: frame)
}
override func sizeThatFits(size: CGSize) -> CGSize {
// swap the length and width coming in and going out
let fitSize = view.sizeThatFits(CGSize(width: size.height, height: size.width))
return CGSize(width: fitSize.height, height: fitSize.width)
}
override func layoutSubviews() {
super.layoutSubviews()
// layoutSubviews gets called multiple times, only need it once
if self.frame.height == oldHeight && self.frame.width == oldWidth {
return
} else {
oldWidth = self.frame.width
oldHeight = self.frame.height
}
// Remove the old rotation view
if self.subviews.count > 0 {
self.subviews[0].removeFromSuperview()
}
// setup rotationView container
let rotationView = UIView()
rotationView.frame = CGRect(origin: CGPointZero, size: CGSize(width: self.bounds.height, height: self.bounds.width))
rotationView.userInteractionEnabled = true
self.addSubview(rotationView)
// transform rotationView (so that it covers the same frame as self)
rotationView.transform = translateRotateFlip()
// add view
view.frame = rotationView.bounds
rotationView.addSubview(view)
}
func translateRotateFlip() -> CGAffineTransform {
var transform = CGAffineTransformIdentity
// translate to new center
transform = CGAffineTransformTranslate(transform, (self.bounds.width / 2)-(self.bounds.height / 2), (self.bounds.height / 2)-(self.bounds.width / 2))
// rotate counterclockwise around center
transform = CGAffineTransformRotate(transform, CGFloat(-M_PI_2))
// flip vertically
transform = CGAffineTransformScale(transform, -1, 1)
return transform
}
}
Problem
I noticed that the custom view loads very slowly. I'm new to Xcode Instruments so I watched the helpful videos Debugging Memory Issues with Xcode and Profiler and Time Profiler.
After that I tried finding the issue in my own project. It seems like no matter whether I use the Time Profiler or Leaks or Allocations tools, they all show that my class init method is doing too much work. (But I kind of knew that already from the slow load time before.) Here is a screen shot from the Allocations tool:
I didn't expand all of the call tree because it wouldn't have fit. Why are so many object being created? When I made a three layer custom view I knew that it wasn't ideal, but the number of layers that appears to be happening from the call tree is ridiculous. What am I doing wrong?
You shouldn't add or delete any subview inside layoutSubviews, as doing so triggers a call to layoutSubviews again.
Create your subview when you create your view, and then only adjust its position in layoutSubviews rather than deleting and re-adding it.
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
I have a zooming UIScrollView, and a non-zooming overlay view on which I animate markers. These markers need to track the location of some of the content of the UIScrollView (similar to the way a dropped pin needs to track a spot on the map as you pan and zoom).
I do so by triggering an update of the overlay view in response to the UIScrollView's layoutSubviews. This works, and the overlay tracks perfectly when zooming and panning.
But when the pinch gesture ends the UIScrollView automatically performs a final animation, and the overlay view is out of sync for the duration of this animation.
I made a simplified project to isolate this problem. The UIScrollView contains an orange square, and the overlay view displays a 2-pixel red outline around the frame of this orange square. As you can see below, the red outline always moves to where it should be, except for a short period of time after touch ends, when it visibly jumps ahead to the final position of the orange square.
The full Xcode project for this test is available here: https://github.com/Clafou/ScrollviewZoomTrackTest but all the code is in the two files shown below:
TrackedScrollView.swift:
class TrackedScrollView: UIScrollView {
#IBOutlet var overlaysView: UIView?
let square: UIView
required init(coder aDecoder: NSCoder) {
square = UIView(frame: CGRect(x: 50, y: 300, width: 300, height: 300))
square.backgroundColor = UIColor.orangeColor()
super.init(coder: aDecoder)
self.addSubview(square)
self.maximumZoomScale = 1
self.minimumZoomScale = 0.5
self.contentSize = CGSize(width: 500, height: 900)
self.delegate = self
}
override func layoutSubviews() {
super.layoutSubviews()
overlaysView?.setNeedsLayout()
}
}
extension TrackedScrollView: UIScrollViewDelegate {
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return square
}
}
OverlaysView.swift:
class OverlaysView: UIView {
#IBOutlet var trackedScrollView: TrackedScrollView?
let outline: CALayer
required init(coder aDecoder: NSCoder) {
outline = CALayer()
outline.borderColor = UIColor.redColor().CGColor
outline.borderWidth = 2
super.init(coder: aDecoder)
self.layer.addSublayer(outline)
}
override func layoutSubviews() {
super.layoutSubviews()
if let trackedScrollView = self.trackedScrollView {
CATransaction.begin()
CATransaction.setDisableActions(true)
let frame = trackedScrollView.convertRect(trackedScrollView.square.frame, toView: self)
outline.frame = CGRectIntegral(CGRectInset(frame, -3, -3))
CATransaction.commit()
}
}
}
Among the things I tried was using a CADisplayLink and presentationLayer and this allowed me to animate the overlay, but the coordinates that I obtained from presentationLayer lagged slightly behind the actual UIScrollView, so this still didn't look right. I think the right approach would be to tie my overlay update to the system-created UIScrollView animation, but I haven't had success hacking this so far.
How can I update this code to always track the UIScrollView's zooming content?
UIScrollView sends scrollViewDidZoom: to its delegate in its animation block, if it's “bouncing” back to its minimum or maximum zoom when the pinch ends. Update the frames of your overlays in scrollViewDidZoom: if zoomBouncing is true. If you're using Auto Layout call layoutIfNeeded.
scrollViewDidZoom: is only called once during the zoom bounce animation but adjusting your frames or calling layoutIfNeeded will ensure these changes are animated along with the zoom bounce, thus keeping them perfectly in sync.
Demo:
Fixed sample project: https://github.com/mayoff/ScrollviewZoomTrackTest