How to reduce blur effect on UIVisualEffectView - ios

How can i reduce the blur effect on a UIVisualEffectView it gives me options of light, extraLight and dark which are not good enough for me i am trying to achieve something like this

We can do that absolutely natively and with correct expected look using animator
Usage:
let blurEffectView = BlurEffectView()
view.addSubview(blurEffectView)
BlurEffectView realisation:
class BlurEffectView: UIVisualEffectView {
var animator = UIViewPropertyAnimator(duration: 1, curve: .linear)
override func didMoveToSuperview() {
guard let superview = superview else { return }
backgroundColor = .clear
frame = superview.bounds //Or setup constraints instead
setupBlur()
}
private func setupBlur() {
animator.stopAnimation(true)
effect = nil
animator.addAnimations { [weak self] in
self?.effect = UIBlurEffect(style: .dark)
}
animator.fractionComplete = 0.1 //This is your blur intensity in range 0 - 1
}
deinit {
animator.stopAnimation(true)
}
}

There is no official way to change the blur level.
However, you can try something like this:
Create a custom blur view

Here's my riff on bodich's answer that fixes the problem of the blur resetting each time the app comes to the foreground. I'm still not convinced this hack is shippable since there may be other times that CoreAnimation decides to finish stale animations in which case the blur would suddenly become full intensity.
class BlurEffectView: UIVisualEffectView {
private var animator = UIViewPropertyAnimator(duration: 1, curve: .linear)
private var intensity: CGFloat = 0.25
init(intensity: CGFloat) {
self.intensity = intensity
super.init(effect: nil)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
deinit {
animator.stopAnimation(true)
}
override func didMoveToSuperview() {
guard let superview = superview else { return }
backgroundColor = .clear
frame = superview.bounds
autoresizingMask = [.flexibleWidth, .flexibleHeight]
clipsToBounds = true
NotificationCenter.default.addObserver(
self,
selector: #selector(appWillEnterFG(_:)),
name:UIApplication.willEnterForegroundNotification,
object: nil
)
setUpAnimation()
}
private func setUpAnimation() {
animator.stopAnimation(true)
effect = nil
animator.addAnimations { [weak self] in
self?.effect = UIBlurEffect(style: .light)
}
animator.fractionComplete = intensity
}
#objc func appWillEnterFG(_ note: Notification) {
setUpAnimation()
}
}

I know this is late, but if you're just using blur for a modal presentations, apple has a built-in UIModalPresentationStyle called blurOverFullScreen, and it works pretty well.
Just set the parent controller's modalPresentationStyle to .blurOverFullScreen and present the new view controller. If the new view is smaller than the screen, the background should be blurred out like seen in your picture.

Related

UISwitch ".onTintColor" and ".tintColor" not working [duplicate]

I've learned that we can change the UISwitch button appearance in its "on" state,
but is it also possible to change the color of the UISwitch in the "off" state?
My solution with #swift2:
let onColor = _your_on_state_color
let offColor = _your_off_state_color
let mSwitch = UISwitch(frame: CGRect.zero)
mSwitch.on = true
/*For on state*/
mSwitch.onTintColor = onColor
/*For off state*/
mSwitch.tintColor = offColor
mSwitch.layer.cornerRadius = mSwitch.frame.height / 2.0
mSwitch.backgroundColor = offColor
mSwitch.clipsToBounds = true
Result:
Try using this
yourSwitch.backgroundColor = [UIColor whiteColor];
youSwitch.layer.cornerRadius = 16.0;
All thanks to #Barry Wyckoff.
You can use the tintColor property on the switch.
switch.tintColor = [UIColor redColor]; // the "off" color
switch.onTintColor = [UIColor greenColor]; // the "on" color
Note this requires iOS 5+
Swift IBDesignable
import UIKit
#IBDesignable
class UISwitchCustom: UISwitch {
#IBInspectable var OffTint: UIColor? {
didSet {
self.tintColor = OffTint
self.layer.cornerRadius = 16
self.backgroundColor = OffTint
}
}
}
set class in Identity inspector
change color from Attributes inspector
Output
Here's a pretty good trick: you can just reach right into the UISwitch's subview that draws its "off" background, and change its background color. This works a lot better in iOS 13 than it does in iOS 12:
if #available(iOS 13.0, *) {
self.sw.subviews.first?.subviews.first?.backgroundColor = .green
} else if #available(iOS 12.0, *) {
self.sw.subviews.first?.subviews.first?.subviews.first?.backgroundColor = .green
}
Working 100% IOS 13.0 and Swift 5.0 switch both state color set same #ios13 #swift #swift5
#IBOutlet weak var switchProfile: UISwitch!{
didSet{
switchProfile.onTintColor = .red
switchProfile.tintColor = .red
switchProfile.subviews[0].subviews[0].backgroundColor = .red
}
}
The Best way to manage background color & size of UISwitch
For now it's Swift 2.3 code
import Foundation
import UIKit
#IBDesignable
class UICustomSwitch : UISwitch {
#IBInspectable var OnColor : UIColor! = UIColor.blueColor()
#IBInspectable var OffColor : UIColor! = UIColor.grayColor()
#IBInspectable var Scale : CGFloat! = 1.0
override init(frame: CGRect) {
super.init(frame: frame)
self.setUpCustomUserInterface()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setUpCustomUserInterface()
}
func setUpCustomUserInterface() {
//clip the background color
self.layer.cornerRadius = 16
self.layer.masksToBounds = true
//Scale down to make it smaller in look
self.transform = CGAffineTransformMakeScale(self.Scale, self.Scale);
//add target to get user interation to update user-interface accordingly
self.addTarget(self, action: #selector(UICustomSwitch.updateUI), forControlEvents: UIControlEvents.ValueChanged)
//set onTintColor : is necessary to make it colored
self.onTintColor = self.OnColor
//setup to initial state
self.updateUI()
}
//to track programatic update
override func setOn(on: Bool, animated: Bool) {
super.setOn(on, animated: true)
updateUI()
}
//Update user-interface according to on/off state
func updateUI() {
if self.on == true {
self.backgroundColor = self.OnColor
}
else {
self.backgroundColor = self.OffColor
}
}
}
Swift 5:
import UIKit
extension UISwitch {
func set(offTint color: UIColor ) {
let minSide = min(bounds.size.height, bounds.size.width)
layer.cornerRadius = minSide / 2
backgroundColor = color
tintColor = color
}
}
Should you need other switches around your app, it might be also a good idea implementing #LongPham's code inside a custom class.
As others have pointed out, for the "off" state you'll need to change the background colour as well, since the default is transparent.
class MySwitch: UISwitch {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Setting "on" state colour
self.onTintColor = UIColor.green
// Setting "off" state colour
self.tintColor = UIColor.red
self.layer.cornerRadius = self.frame.height / 2
self.backgroundColor = UIColor.red
}
}
Swift 4 easiest and fastest way to get it in 3 steps:
// background color is the color of the background of the switch
switchControl.backgroundColor = UIColor.white.withAlphaComponent(0.9)
// tint color is the color of the border when the switch is off, use
// clear if you want it the same as the background, or different otherwise
switchControl.tintColor = UIColor.clear
// and make sure that the background color will stay in border of the switch
switchControl.layer.cornerRadius = switchControl.bounds.height / 2
If you manually change the size of the switch (e.g., by using autolayout), you will have to update the switch.layer.cornerRadius too, e.g., by overriding layoutSubviews and after calling super updating the corner radius:
override func layoutSubviews() {
super.layoutSubviews()
switchControl.layer.cornerRadius = switchControl.bounds.height / 2
}
In Swift 4+:
off state:
switch.tintColor = UIColor.blue
on state:
switch.onTintColor = UIColor.red
The UISwitch offTintColor is transparent, so whatever is behind the switch shows through. Therefore, instead of masking the background color, it suffices to draw a switch-shaped image behind the switch (this implementation assumes that the switch is positioned by autolayout):
func putColor(_ color: UIColor, behindSwitch sw: UISwitch) {
guard sw.superview != nil else {return}
let onswitch = UISwitch()
onswitch.isOn = true
let r = UIGraphicsImageRenderer(bounds:sw.bounds)
let im = r.image { ctx in
onswitch.layer.render(in: ctx.cgContext)
}.withRenderingMode(.alwaysTemplate)
let iv = UIImageView(image:im)
iv.tintColor = color
sw.superview!.insertSubview(iv, belowSubview: sw)
iv.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
iv.topAnchor.constraint(equalTo: sw.topAnchor),
iv.bottomAnchor.constraint(equalTo: sw.bottomAnchor),
iv.leadingAnchor.constraint(equalTo: sw.leadingAnchor),
iv.trailingAnchor.constraint(equalTo: sw.trailingAnchor),
])
}
[But see now my other answer.]
2020 As of Xcode 11.3.1 & Swift 5
Here's the simplest way I've found of doing setting the UISwitch off-state colour with one line of code. Writing this here since this page is what came up first when I was looking and the other answers didn't help.
This is if I wanted to set the off state to be red, and can be added to the viewDidLoad() function:
yourSwitchName.subviews[0].subviews[0].backgroundColor = UIColor.red
Note - what this is actually doing is setting the background colour of the switch. This may influence the colour of the switch in the on-state too (though for me this wasn't a problem since I wanted the on and off state to be the same colour).
A solution for this:
Simply tie in the colours with an 'if else' statement inside your IBAction. If the switch is off, colour the background red. If the switch is on, leave the background clear so your chosen 'on' colour will display properly.
This goes inside the switch IBAction.
if yourSwitch.isOn == false {
yourSwitch.subviews[0].subviews[0].backgroundColor = UIColor.red
} else {
yourSwitch.subviews[0].subviews[0].backgroundColor = UIColor.clear
}
I found some behaviour where, upon the app resuming from background, the switch background would return to clear. To remedy this problem I simply added in the following code to set the colour every time the app comes to the foreground:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationWillEnterForeground(_:)),
name: UIApplication.willEnterForegroundNotification,
object: nil)
}
#objc func applicationWillEnterForeground(_ notification: NSNotification) {
yourSwitch.subviews[0].subviews[0].backgroundColor = UIColor.red
yourSwitch.subviews[0].subviews[0].backgroundColor = UIColor.red
}
Seems simpler than the other answers. Hope that helps!
More safe way in Swift 3 without magical 16pt values:
class ColoredBackgroundSwitch: UISwitch {
var offTintColor: UIColor {
get {
return backgroundColor ?? UIColor.clear
}
set {
backgroundColor = newValue
}
}
override func layoutSubviews() {
super.layoutSubviews()
let minSide = min(frame.size.height, frame.size.width)
layer.cornerRadius = ceil(minSide / 2)
}
}
objective c category to use on any UISwitch in project using code or storyboard:
#import <UIKit/UIKit.h>
#interface UISwitch (SAHelper)
#property (nonatomic) IBInspectable UIColor *offTint;
#end
implementation
#import "UISwitch+SAHelper.h"
#implementation UISwitch (SAHelper)
#dynamic offTint;
- (void)setOffTint:(UIColor *)offTint {
self.tintColor = offTint; //comment this line to hide border in off state
self.layer.cornerRadius = 16;
self.backgroundColor = offTint;
}
#end
XCode 11, Swift 5
I don't prefer using subViews, cause you never know when apple gonna change the hierarchy.
so I use mask view instead.
it works with iOS 12, iOS 13
private lazy var settingSwitch: UISwitch = {
let swt: UISwitch = UISwitch()
// set border color when isOn is false
swt.tintColor = .cloudyBlueTwo
// set border color when isOn is true
swt.onTintColor = .greenishTeal
// set background color when isOn is false
swt.backgroundColor = .cloudyBlueTwo
// create a mask view to clip background over the size you expected.
let maskView = UIView(frame: swt.frame)
maskView.backgroundColor = .red
maskView.layer.cornerRadius = swt.frame.height / 2
maskView.clipsToBounds = true
swt.mask = maskView
// set the scale to your expectation, here is around height: 34, width: 21.
let scale: CGFloat = 2 / 3
swt.transform = CGAffineTransform(scaleX: scale, y: scale)
swt.addTarget(self, action: #selector(switchOnChange(_:)), for: .valueChanged)
return swt
}()
#objc
func switchOnChange(_ sender: UISwitch) {
if sender.isOn {
// set background color when isOn is true
sender.backgroundColor = .greenishTeal
} else {
// set background color when isOn is false
sender.backgroundColor = .cloudyBlueTwo
}
}
I tested on IOS 14, set background as off color and onTintColor as On and works:
uiSwitch.onTintColor = UIColor.blue
uiSwitch.backgroundColor = UIColor.red
XCode 11, Swift 4.2
Starting with Matt's solution I added it to a custom, IBDesignable control. There is a timing issue in that didMoveToSuperview() is called before the offTintColor is set that needed to be handled.
#IBDesignable public class UISwitchCustom: UISwitch {
var switchMask: UIImageView?
private var observers = [NSKeyValueObservation]()
#IBInspectable dynamic var offTintColor : UIColor! = UIColor.gray {
didSet {
switchMask?.tintColor = offTintColor
}
}
override init(frame: CGRect) {
super.init(frame: frame)
initializeObservers()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initializeObservers()
}
private func initializeObservers() {
observers.append(observe(\.isHidden, options: [.initial]) {(model, change) in
self.switchMask?.isHidden = self.isHidden
})
}
override public func didMoveToSuperview() {
addOffColorMask(offTintColor)
super.didMoveToSuperview()
}
private func addOffColorMask(_ color: UIColor) {
guard self.superview != nil else {return}
let onswitch = UISwitch()
onswitch.isOn = true
let r = UIGraphicsImageRenderer(bounds:self.bounds)
let im = r.image { ctx in
onswitch.layer.render(in: ctx.cgContext)
}.withRenderingMode(.alwaysTemplate)
let iv = UIImageView(image:im)
iv.tintColor = color
self.superview!.insertSubview(iv, belowSubview: self)
iv.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
iv.topAnchor.constraint(equalTo: self.topAnchor),
iv.bottomAnchor.constraint(equalTo: self.bottomAnchor),
iv.leadingAnchor.constraint(equalTo: self.leadingAnchor),
iv.trailingAnchor.constraint(equalTo: self.trailingAnchor),
])
switchMask = iv
switchMask?.isHidden = self.isHidden
}
}
all I finally used transform and layer.cornerRadius too.
But I have added translation to it to be center.
private func setSwitchSize() {
let iosSwitchSize = switchBlockAction.bounds.size
let requiredSwitchSize = ...
let transform = CGAffineTransform(a: requiredSwitchSize.width / iosSwitchSize.width, b: 0,
c: 0, d: requiredSwitchSize.height / iosSwitchSize.height,
tx: (requiredSwitchSize.width - iosSwitchSize.width) / 2.0,
ty: (requiredSwitchSize.height - iosSwitchSize.height) / 2.0)
switchBlockAction.layer.cornerRadius = iosSwitchSize.height / 2.0
switchBlockAction.transform = transform
}
And I did use backgroundColor and tintColor in designer.
Hope it helps.

UIViewPropertyAnimator: Hide both TabBar and StatusBar simultaneously (iOS 13)

Trying to hide both TabBar and StatusBar simultaneously and inside the same animation block, I came across an incomprehensible layout behavior. Starting to hide TabBar in the usual way with tabbar item viewcontroller:
import UIKit
class TestViewController: UIViewController {
var mainViewController: UITabBarController {
get {
return UIApplication.shared.windows.first {$0.rootViewController != nil}?.rootViewController as! UITabBarController
}
}
var offset: CGFloat!
override func viewDidLoad() {
super.viewDidLoad()
offset = mainViewController.tabBar.frame.height
}
#IBAction func HideMe(_ sender: Any) {
let tabBar = self.mainViewController.tabBar
let animator = UIViewPropertyAnimator(duration: 1, curve: .linear) {
tabBar.frame = tabBar.frame.offsetBy(dx: 0, dy: self.offset)
}
animator.startAnimation()
}
}
So far so good:
Now let's add animation for StatusBar:
import UIKit
class TestViewController: UIViewController {
var mainViewController: UITabBarController {
get {
return UIApplication.shared.windows.first {$0.rootViewController != nil}?.rootViewController as! UITabBarController
}
}
var isTabBarHidden = false {
didSet(newValue) {
setNeedsStatusBarAppearanceUpdate()
}
}
override var prefersStatusBarHidden: Bool {
get {
return isTabBarHidden
}
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
get {
return .slide
}
}
var offset: CGFloat!
override func viewDidLoad() {
super.viewDidLoad()
offset = mainViewController.tabBar.frame.height
}
#IBAction func HideMe(_ sender: Any) {
let tabBar = self.mainViewController.tabBar
let animator = UIViewPropertyAnimator(duration: 1, curve: .linear) {
tabBar.frame = tabBar.frame.offsetBy(dx: 0, dy: self.offset)
self.isTabBarHidden = true
}
animator.startAnimation()
}
}
Now StatusBar is sliding, bur TabBar froze (I don't know why):
Any attempts to update layout using layoutIfNeeded(), setNeedsLayout() etc. were unsuccessful. Now let's swap animations for TabBar and StatusBar:
#IBAction func HideMe(_ sender: Any) {
let tabBar = self.mainViewController.tabBar
let animator = UIViewPropertyAnimator(duration: 1, curve: .linear) {
self.isTabBarHidden = true
tabBar.frame = tabBar.frame.offsetBy(dx: 0, dy: self.offset)
}
animator.startAnimation()
}
Both are sliding now, but TabBar started to jump at the begining of animation:
I found that when adding directives for StatusBar to an animation block, a ViewDidLayoutSubviews() starts to be called additionally. Actually, you can fix the initial position of TabBar inside ViewDidLayoutSubviews():
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if isTabBarHidden {
let tabBar = self.mainViewController.tabBar
tabBar.frame = tabBar.frame.offsetBy(dx: 0, dy: self.offset)
}
}
The disadvantage of this method is that TabBar can twitch during the movement, depending on the speed of movement and other factors.
Another way (without using ViewDidLayoutSubviews()) is contrary to logic but works in practice. Namely, you can put one animation in a completion block of another one:
#IBAction func HideMe(_ sender: Any) {
let tabBar = self.mainViewController.tabBar
let animator1 = UIViewPropertyAnimator(duration: 1, curve: .linear) {
self.isTabBarHidden = !self.isTabBarHidden
}
animator1.addCompletion({_ in
let animator2 = UIViewPropertyAnimator(duration: 1, curve: .linear) {
tabBar.frame = tabBar.frame.offsetBy(dx: 0, dy: self.offset)
}
animator2.startAnimation()
})
animator1.startAnimation()
}
Following the logic, we have two consecutive animations. And TabBar animation should begin after StatusBar animation ends. However, in practice:
The disadvantage of this method is that if you want to reverse the animation (for example, the user tapped the screen while TabBar is moving), the variable animator1.isRunning will be false, although physically the StatusBar will still move around the screen (I also don't know why).
Looking forward to reading your comments, suggestions, explanations.
The logic is that setNeedsStatusBarAppearanceUpdate() is animated asyncroniusly. I.e. StatusBar animation ends immediately after start and then runs in a thread that cannot be paused or reversed. It’s a pity that iOS SDK doesn't provide StatusBar animation control.
How to prevent the effect of setNeedsStatusBarAppearanceUpdate() animation on the layout, I still do not know.

Start CAAnimation when view is not in view hierarchy

I have a UI component (a loading spinner) that has an endless animation:
class MySpinner: UIView {
...
override var bounds: CGRect {
didSet {
updateLayers()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.addSublayer(self.animatedLayer)
self.updateLayers()
self.startAnimating()
}
private func updateLayers() {
//Set up path, stroke color and such
self.animatedLayer.frame = self.bounds
}
private func startAnimating() {
CATransaction.begin()
CATransaction.setDisableActions(true) //disable automatic animations by iOS
let endlessRotationAnimation = CAKeyframeAnimation(keyPath: "transform.rotation")
endlessRotationAnimation.values = [ 0.0, CGFloat(2.0 * .pi) ]
endlessRotationAnimation.keyTimes = [ 0.0, 1.0 ]
endlessRotationAnimation.repeatDuration = .infinity
endlessRotationAnimation.duration = 1.0
self.animatedLayer.add(endlessRotationAnimation, forKey: "rotationAnimation")
CATransaction.commit()
}
}
It turns out this is not working. I assume that if I call startAnimating() before the view is added to the window, iOS will immediately remove the animation. If I first add the view to the view hierarchy and then call startAnimating(), things start working.
Is there a way to prevent iOS removing my animation? Or is there some documentation about this behaviour, I couldn't find any?
I was able to work around this issue by overriding didMoveToWindow.
public override func didMoveToWindow() {
super.didMoveToWindow()
startAnimating()
}

UIView shadow on Ipad doesn't span the width of the view correctly

I have created a simple extension on UIView to add a "drop" shadow to UIViews, it works great on iPhone, but does not work correctly on iPad. I can't figure out what I am missing, any ideas on how to improve/fix this so that the shadow shows correctly on both iPad and iPhone would be awesome
extension UIView {
func addShadow() {
self.layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOpacity = 0.5
self.layer.shadowOffset = .zero
self.layer.shadowRadius = 10
}
}
This is what it comes out looking like on iPad:
I am calling it from a UIView like so:
class AggregateDataViewImplementation: UIView, AggregateDataView {
override func awakeFromNib() {
layoutView()
}
private func layoutView() {
self.addShadow()
}
...
}
Maybe you problem is here:
self.layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath
the shadow's rect is according to here, if the self.bounds is not accroding to the actual rect,may have a error show. Suggest that write like this ,
self.bgView.layer.shadowPath = UIBezierPath(rect: self.bgView.bounds).cgPath
self.bgView.layer.shadowColor = UIColor.black.cgColor
self.bgView.layer.shadowOpacity = 0.5
self.bgView.layer.shadowOffset = .zero
self.bgView.layer.shadowRadius = 10
hope to help you.
Reading the comments got me thinking, and I was able to get this working perfectly by moving things around a bit! Thank you to all to took the time to comment!
I kept the extension from UIView, but moved the call from the awakeFromNib() call into the viewDidLoad() call of the views parent VC. In order to deal with device rotation I added a call to viewWillTransition(...) the full snippet is below.
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
guard let primaryView = primaryView as? AggregateDataViewImplementation else { return }
primaryView.didTapClose = { [weak self] in
self?.didTapClose?()
}
primaryView.tableView.delegate = self
primaryView.tableView.dataSource = self
primaryView.tableView.tableFooterView = UIView()
let nib = UINib(nibName: "AggregateDataTableViewCell", bundle: nil)
primaryView.tableView.register(nib, forCellReuseIdentifier: "AggregateDataTableViewCell")
primaryView.addShadow()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard let primaryView = primaryView as? AggregateDataViewImplementation else { return }
primaryView.tableView.reloadData()
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { (_) in
guard let primaryView = self.primaryView as? AggregateDataViewImplementation else { return }
primaryView.addShadow()
}, completion: nil)
}
thanks again!

GestureRecognizer not responding to tap

After initialisation of by subclass of UIImageView I have the following line of code:
self.userInteractionEnabled = true
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "handleTap:"))
I created the necessary associated function :
func handleTap(gestureRecognizer: UITapGestureRecognizer) {
print("In handler")
}
On tapping on the view in question, "In handler was never printed to the console". I then removed the handler function to see if the compiler would complain about the missing function. It didn't.
I'm positively stumped. I'd truly appreciate any light people can shed on this.
Update: My class is actually a UIImageView as opposed to UIView
I was using UITapGestureRecognizer that I placed on a UILabel using Storyboard.
To get this to work I also had to place a checkmark in the block labeled: "User Interaction Enabled" in the UILabel Attributes Inspector in the Storyboard.
I discovered the answer after carefully combing through my code.
One of the parent views was created without supplying a frame:
While it's a noobish enough error to warrant deletion of this questions, odds are someone else will also have the same issue in the future...
Try this
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.view.userInteractionEnabled = true
var tapGesture = UITapGestureRecognizer(target: self, action: Selector("handleTap:"))
self.view.addGestureRecognizer(tapGesture)
}
func handleTap(sender : UIView) {
println("Tap Gesture recognized")
}
In addition to the other answers, this can be caused by adding the gesture recognizer to multiple views. Gesture recognizers are for single views only.
Reference: https://stackoverflow.com/a/5567684/6543020
I ran into this problem with programmatic views.
My UIView with the gesture recognizer had .isUserInteractionEnabled = true, but it did not respond to taps until I set .isUserInteractionEnabled = true for its parent views as well.
Most likely you add UIGestureRecognizer in wrong place. Here is working sample with UIView from storyboard. If you create your UIView dynamically then you should put this initialization in the correct constructor.
class TestView: UIView
{
override func awakeFromNib()
{
self.userInteractionEnabled = true
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "handleTap:"))
}
func handleTap(gestureRecognizer: UITapGestureRecognizer)
{
println("Here")
}
}
I found the solution to this problem after lot of trial and error. So there are two solution two this
1. Either add the GestureRecognizer in viewDidLoad() and turn
userInteractionEnabled = true
2. If using computed property use lazy var instead of let to the property.
lazy var profileImageView: UIImageView = {
let iv = UIImageView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
iv.image = #imageLiteral(resourceName: "gameofthrones_splash")
iv.contentMode = .scaleAspectFill
iv.translatesAutoresizingMaskIntoConstraints = false
iv.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleSelectProfileImageView)))
iv.isUserInteractionEnabled = true
return iv
}()
For anyone that is still having problems even with all the previous answers, make sure you are using UITapGestureRecognizer instead of UIGestureRecognizer, I kept missing this detail while trying to find what was wrong.
This Code works for me with XCode 7.0.1
import UIKit
class ImageView: UIImageView {
init(frame: CGRect, sender: Bool, myImage: UIImage) {
super.init(frame: frame)
self.image = myImage
initBorderStyle(sender)
// enable user interaction on image.
self.userInteractionEnabled = true
let gesture = UITapGestureRecognizer(target: self, action: "previewImage:")
addGestureRecognizer(gesture)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func previewImage(myGesture: UITapGestureRecognizer? = nil) {
print("i'm clicked")
}
private func initBorderStyle(sender: Bool) {
self.layer.masksToBounds = true
self.layer.cornerRadius = 8
self.layer.borderWidth = 0.5
self.layer.borderColor = getBorderColor(sender)
self.backgroundColor = getColor(sender)
}
func getBorderColor(sender: Bool) -> CGColor {
var result: CGColor
if sender {
result = UIColor(red: 0.374, green: 0.78125, blue: 0.0234375, alpha: 0.5).CGColor
} else {
result = UIColor(red: 0.3125, green: 0.6015625, blue: 0.828125, alpha: 0.5).CGColor
}
return result
}
}
Xcode 11.4 Swift 5.2
UserInteraction is enabled by default on Custom UIView
import UIKit
class YourView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapped))
self.addGestureRecognizer(tapGesture)
}
#objc func tapped() {
// do something
}
}
If you want to be notified elsewhere that the custom UIView has been tapped, then it is convenient to use a Notification.
It is best to create this as an extension to prevent it being Stringly typed.
extension Notification.Name {
static let DidTapMyView = Notification.Name("DidTapMyView")
}
In your custom UIView when the tapped function is called
#objc func tapped() {
NotificationCenter.default.post(name: .DidTapMyView, object: self)
}
In your UIViewController where you want to listen for the Notification:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(myViewWasTapped), name: .DidTapMyView, object: nil)
}
#objc func myViewWasTapped() {
// Notified here - do something
}
Here's a list of my findings in 2021, XCode 13:
Make sure both your view and all of its superviews have set width & height constraints(this one is crucial)
Set isUserInteractionEnabled = true for your view
There's no need to set explicit frame or isUserInteractionEnabled for other super views
In addition the above (userInteractionEnabled, clipsToBounds, etc.), if you are working with a view in a child view controller be sure that you have added it as a child with myParentViewController.addChild(myChildViewController) — we've run into a couple of situations now where visible views were not firing recognizing gestures because their view controllers hadn't been added, and presumably the VCs themselves were not being retained.
It turned out that my gesture didn't work because it was in a normal class, and not a subclass of UIView or anything.
I solved the issue by creating a subclass of UIView that I had an instance of in this normal class and placed the gesture logic in that.
I solved my issue by setting the height to the UIView.
optionView.heightAnchor.constraint(equalToConstant: 18).isActive = true
I had the same issue in my programmatically created ViewController. The bug was fixed when I added a backgroundColor to the view.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
let tap = UITapGestureRecognizer(target: self.view, action: #selector(UIView.endEditing))
view.addGestureRecognizer(tap)
}
}

Resources