When "draw" is called by UIKit? - ios

I have a done button with a IBAction as following:
#IBAction func done() {
print("Done pressed");
let hudView = HudView.hud(inView: (navigationController?.view)!, animated: true)
print("Set text");
hudView.text = "Toggled"
}
The HudView is a subclass of UIView has a convinience constructor
class func hud(inView view: UIView, animated: Bool) -> HudView
{
print("Constructor is called")
let hudView = HudView(frame: view.bounds)
hudView.isOpaque = false
view.addSubview(hudView)
view.isUserInteractionEnabled = false
return hudView
}
And also I overwrite the draw method in it.
override func draw(_ rect: CGRect) {
print("Draw is called")
...
}
The problem is that I don't understand why it prints out in such sequence
Done pressed
Constructor is called
Set text
Draw is called
Why is the draw method not called until I set the text? Why not immediately after it is constructed?

According to Apple's Official Documentation.
This method is called when a view is first displayed or when an event
occurs that invalidates a visible part of the view. You should never
call this method directly yourself. To invalidate part of your view,
and thus cause that portion to be redrawn, call the setNeedsDisplay()
or setNeedsDisplay(_:) method instead.
When you initialise the view, it's not added to the view hierarchy and display for that view is not needed. So draw(_:) is not called. When you added the view as subview to the view hierarchy, it has to display and the method is called.
Somethings other than setNeedsDisplay() and setNeedsDisplay(_:) that trigger this method are when you make layouts to the view or change the size of the view.

Related

CollectionView Touch is now working when placed on top of tabBar Swift

I have added a collectionView on top of a UITabBar but its touch is not working.The screeshot for the tabBar and collectionView
The code is attached below, I want the collectionView to be touchable. Here quickAccessView is the UIView that contains the collectionView. For constraints I'm using snapKit
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
self.tabBar.bringSubviewToFront(quickAccessView)
}
private func setupQuickAccessView(){
print("this is tabBar's height", self.tabBar.frame.size.height)
self.tabBar.frame.size.height = 150
print("this is new tabBar's height", self.tabBar.frame.size.height)
self.tabBar.addSubview(quickAccessView)
quickAccessView.clipsToBounds = true
}
private func addQuickAccessViewConstraints(){
quickAccessView.snp.makeConstraints { make in
make.right.left.equalTo(self.tabBar.safeAreaLayoutGuide)
make.height.equalTo(76)
make.bottom.equalTo(self.tabBar.snp.bottom).offset(-80)
}
}
this is after modification that Aman told
The UITabBarController
final class MainTabBarController: UITabBarController {
private lazy var quickAccessView: QuickAccessView = .fromNib()
var quickAccessSupportedTabBar: QuickAccessSupportedTabBar {
self.tabBar as! QuickAccessSupportedTabBar // Even code is crashing here
}
// Even code is crashing here
override func viewDidLoad() {
super.viewDidLoad()
self.tabBar.backgroundColor = .white
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.view.frame = self.quickAccessView.bounds
setupUI()
}
}
extension MainTabBarController{
private func setupUI(){
setupQuickAcessView()
addQuickAcessViewConstraints()
}
}
// MARK: - Setting Up Quick Access view
extension MainTabBarController {
private func setupQuickAcessView(){
self.quickAccessSupportedTabBar.addSubview(quickAccessView)
}
private func addQuickAcessViewConstraints(){
quickAccessView.snp.makeConstraints { make in
make.left.right.equalTo(self.quickAccessSupportedTabBar.safeAreaLayoutGuide)
make.height.equalTo(66)
make.bottom.equalTo(self.quickAccessSupportedTabBar.snp.top)
}
}
}
the UItabBar and here it is throwing error and I too am confuse that how to access it and convert it to points
class QuickAccessSupportedTabBar: UITabBar {
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// if `quickAccessView` is visible, then convert `point` to its coordinate-system
// and check if it is within its bounds; if it is, then ask `quickAccessView`
// to perform the hit-test; you may skip the `isHidden` check, in-case this view
// is always present in your app; I'm assuming based on your screenshot that
// the user can dismiss / hide the `quickAccessView` using the cross icon
if !quickAccessView.isHidden {
// Convert the point to the target view's coordinate system.
// The target view isn't necessarily the immediate subview
let targetPoint = quickAccessView.convert(point, from: self)
if quickAccessView.bounds.contains(targetPoint) {
// The target view may have its view hierarchy, so call its
// hitTest method to return the right hit-test view
return quickAccessView.hitTest(targetPoint, with: event)
}
}
// else execute tabbar's default implementation
return super.hitTest(point, with: event)
}
}
I think what may be happening here is that since you've added quickAccessView as tab bar's subview, it is not accepting touches. This would be so because the tabbar's hist test will fail in this scenario.
To get around this, instead of using a UITabBar, subclass UITabBar, let us call it ToastyTabBar for reference. See the code below:
class ToastyTabBar: UITabBar {
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// if `quickAccessView` is visible, then convert `point` to its coordinate-system
// and check if it is within its bounds; if it is, then ask `quickAccessView`
// to perform the hit-test; you may skip the `isHidden` check, in-case this view
// is always present in your app; I'm assuming based on your screenshot that
// the user can dismiss / hide the `quickAccessView` using the cross icon
if !quickAccessView.isHidden {
// Convert the point to the target view's coordinate system.
// The target view isn't necessarily the immediate subview
let targetPoint = quickAccessView.convert(point, from: self)
if quickAccessView.bounds.contains(targetPoint) {
// The target view may have its view hierarchy, so call its
// hitTest method to return the right hit-test view
return quickAccessView.hitTest(targetPoint, with: event)
}
}
// else execute tabbar's default implementation
return super.hitTest(point, with: event)
}
}
Set this as the class of your tabBar (both in the storyboard and the swift file), and then see if that solves it for you.
You'll have to figure out a way to make quickAccessView accessible to the tabbar for the hit test check. I haven't advised on that above because I'm not familiar with your class hierarchy, and how and where you set stuff up, but this should be trivial.
If this solves it for you, please consider marking this as the answer, and if it does not then please share a little more info here about where you're having the problem.
Edit (for someone using a UITabBarController):
In response to your comment about "how to access UITabBar class from UITabBarController" here's how I would go about it.
I'm assuming you have a storyboard with the UITabBarController.
The first step (ignore this step if you already have a UITabBarController custom subclass) is that you need to subclass UITabBarController. Let us call this class ToastyTabBarController for reference. Set this class on the UITabBarController in your storyboard using the identity inspector pane in xcode.
The second step is to set the class of the UITabBar in your storyboard as ToastyTabBar (feel free to name it something more 'professional' 😊).
This is to be done in the same storyboard, in your UITabBarController scene itself. It will show the tabBar under your UITabBarController, and you can set the custom class on it using the identity inspector pane just like earlier.
The next step is to expose a computed property on your custom UITabBarController class, as shown below.
var toastyTabBar: ToastyTabBar {
self.tabBar as! ToastyTabBar
}
And that's it. Now you have a property on your UITabBarController subclass which is of ToastyTabBar type and you can use this new property, toastyTabBar, however you require.
Hope this helps.

Why isn't the draw method of the custom CALayer called earlier?

Issue
I trigger random animations on a selection of UIViews using a Timer. All animations work as intended but one where the draw method of a CALayer is called after the exit of the timer selector.
Detailed Description
For the sake of clarity and simplification, let me schematise the actions performed.
All the animations I have created so far work as intended: they are a combination of CABasicAnimations on existing subviews/sublayers or new ones added to the selected view hierarchy. They are coded as an UIView extension, so that they can be called on any views, irrespectively of the view or the view controller the view is in. Well, all work except one.
I have indeed created a custom CALayer class which consists in drawing patterns on a CALayer. In an extension of this custom class, there is a method to animate those patterns (see hereafter the code). So all in all, when I reach the step/method animate selected view and run this particular animation, here is what should happen:
a method named animatePattern is called
this method adds the custom CALayer class to the selected view and then calls the animation extension of this layer
The issue: if with all the other animations, all the drawings are performed prior to the exit of the animate selected view step/method, in that particular case, the custom CALayer class draw method is called after the exit of the performAnimation method, which in turn results in the crash of the animation.
I should add that I have tried the custom CALayer class animation in a separate and simplified playground and it works well (I add the custom layer to a UIView in the UIViewController's viewDidLoad method and then I call the layer animation in the UIViewController's viewDidAppear method.)
The code
the method called by animate selected view step/method:
func animatePattern(for duration: TimeInterval = 1) {
let patternLayer = PatternLayer(effectType: .dark)
patternLayer.frame = self.bounds
self.layer.addSublayer(patternLayer)
patternLayer.play(for: duration)
}
(note that this method is in a UIView extension, therefore self here represents the UIView on which the animation has been called)
the simplified custom CALayer Class:
override var bounds: CGRect {
didSet {
self.setNeedsDisplay()
}
// Initializers
override init() {
super.init()
self.setNeedsDisplay()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init(effectType: EffectType){
super.init()
// sets various properties
self.setNeedsDisplay()
}
// Drawing
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
guard self.frame != CGRect.zero else { return }
self.masksToBounds = true
// do the drawings
}
the animation extension of the custom CALayer class:
func play(for duration: Double, removeAfterCompletion: RemoveAfterCompletion = .no) {
guard self.bounds != CGRect.zero else {
print("Cannot animate nil layer")
return }
CATransaction.begin()
CATransaction.setCompletionBlock {
if removeAfterCompletion.removeStatus {
if case RemoveAfterCompletion.yes = removeAfterCompletion { self.fadeOutDuration = 0.0 }
self.fadeToDisappear(duration: &self.fadeOutDuration)
}
}
// perform animations
CATransaction.commit()
}
Attempts so far
I have tried to force draw the layer by inserting setNeedsDisplay / setNeedsLayout at various places in the code but it does not work: debugging the custom CALayer class's draw method is constantly reached after the exit of the performAnimation method, whilst it should be called when the layer's frame is modified in the animatePattern method. I must miss something quite obvious but I am currently running in circles and I'd appreciate a fresh pair of eyes on it.
Thank you in advance for taking the time to consider this issue!
Best,
You override the draw(_ context) as the UIView can be the delegate of CALayer.
UIView: {
var patternLayer : PatternLayer
func animatePattern(for duration: TimeInterval = 1) {
patternLayer = PatternLayer(effectType: .dark)
patternLayer.frame = self.bounds
self.layer.addSublayer(patternLayer)
}
func draw(_ layer: CALayer, in ctx: CGContext){
layer.draw(ctx)
if NECESSARY { patternLayer.play(for: duration)}
}
}
As it is frequently the case, when you take the time to write down your issue, new ideas start to pop up. In fact, I remembered that self.setNeedsDisplay() informs the system that the layer needs to be (re)drawn but it does not (re)draw it at once: it will be done during the next refresh. It seems then that the refresh occurs after the end of the cycle for that specific animation. In order to overcome this is issue, I first added a call for the display method right after the patternLayer bounds are set, and it worked, but given #Matt comment, I changed the solution by calling displayIfNeeded method instead. It works as well.
func animatePattern(for duration: TimeInterval = 1) {
let patternLayer = PatternLayer(effectType: .dark)
patternLayer.frame = self.bounds
self.layer.addSublayer(patternLayer)
// asks the system to draw the layer now
patternLayer.displayIfNeeded()
patternLayer.play(for: duration)
}
Again, should anyone come up with another more elegant solution/explanation, please do not refrain to share!

How to check if the code is run inside UIView's animation block

I have a function in my UIView subclass. This function adjust the layout of the view.
For example
func addItemAtIndex(index: Int){
// layout the view
item[index].frame.size = view.frame.size
}
I can call myView.addItem(5) as normal.
Since this function adjust the view's layout, I can also call this function inside UIView's animation block. And it will layout the view with animation.
Like this,
UIView.animateWithDuration(0.3){
myView.addItem(5)
}
However, I want to do the different behaviour when it's animated. Is it possible to check if the function get called inside UIView's animation block ?
Like this,
func addItemAtIndex(index: Int){
if insideUIViewAnimationBlock{
// layout with animation
}else{
// layout
}
}
I'm not looking for addItemAtIndex(index: Int, animated: Bool) approach.
CALayer associated with the UIView is disabled implicit animation by default. It will be reenable inside an animation block. You can know whether inside or outside animation block to use that.
print("Outside animation block: \(self.view.actionForLayer(self.view.layer, forKey: "position"))")
UIView.animateWithDuration(5) { () -> Void in
print("Inside animation block: \(self.view.actionForLayer(self.view.layer, forKey: "position"))")
}
For more information:
https://www.objc.io/issues/12-animations/view-layer-synergy/

Is it possible to call a block completion handler from another function in iOS?

I have a custom UIView with a UITapGestureRecognizer attached to it. The gesture recognizer calls a method called hide() to remove the view from the superview as such:
func hide(sender:UITapGestureRecognizer){
if let customView = sender.view as? UICustomView{
customView.removeFromSuperview()
}
}
The UICustomView also has a show() method that adds it as a subview, as such:
func show(){
// Get the top view controller
let rootViewController: UIViewController = UIApplication.sharedApplication().windows[0].rootViewController!!
// Add self to it as a subview
rootViewController.view.addSubview(self)
}
Which means that I can create a UICustomView and display it as such:
let testView = UICustomView(frame:frame)
testView.show() // The view appears on the screen as it should and disappears when tapped
Now, I want to turn my show() method into a method with a completion block that is called when the hide() function is triggered. Something like:
testView.show(){ success in
println(success) // The view has been hidden
}
But to do so I would have to call the completion handler of the show() method from my hide() method.
Is this possible or am I overlooking something?
Since you are implementing the UICustomView, all you need to do is store the 'completion handler' as part of the UICustomView class. Then you call the handler when hide() is invoked.
class UICustomView : UIView {
var onHide: ((Bool) -> ())?
func show (onHide: (Bool) -> ()) {
self.onHide = onHide
let rootViewController: UIViewController = ...
rootViewController.view.addSubview(self)
}
func hide (sender:UITapGestureRecognizer){
if let customView = sender.view as? UICustomView{
customView.removeFromSuperview()
customView.onHide?(true)
}
}
Of course, every UIView has a lifecycle: viewDidAppear, viewDidDisappear, etc. As your UICustomView is a subclass of UIView you could override one of the lifecycle methods:
class UICustomView : UIView {
// ...
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear (animated)
onHide?(true)
}
}
You might consider this second approach if the view might disappear w/o a call to hide() but you still want onHide to run.

viewDidAppear & viewDidLoad Are Are Initializing Positions Too Late

I am looking to update a UIView thats in a storyboard (and instantiated from the storyboard) when it loads in the app.
I need to position a few icons in a dynamic way (that the interface builder doesn't let me do quite yet). However, if I put my code in viewDidAppear or viewDidLoad, it seems to be getting called too late.
Here is what happens the first few second or two when the view loads:
And then a bit later it goes to the right position (as the code was called).
My question is where do I need to initialize the positions of these objects so they dont snap over a second later. viewDidLoad and viewDidAppear are too late? Why!? :)
override func viewDidAppear(animated: Bool)
{
initView()
_gridLines = false
}
func initView()
{
_cameraFeed.initAfterLoad()
//center the buttons on half distance between middle button and screen edge
var middleDistance:CGFloat = _swapButton.frame.origin.x + _swapButton.frame.width/2
_linesButton.frame.origin.x = middleDistance/2 - _linesButton.frame.width/2
_flashButton.frame.origin.x = middleDistance + middleDistance/2 - _flashButton.frame.width/2
_selectPhotos.frame.origin.x = middleDistance/2 - _selectPhotos.frame.width/2
}
Swift and objc answers welcome!
Try putting the code setting the frame in viewWillAppear instead of viewDidAppear.
Also, you should call the super.
override func viewWillAppear(animated: Bool) {
initView()
_gridLines = false
super.viewWillAppear(animated)
}

Resources