I have been customising tab bar with rounded button in center and set corner radius curve as well, I have set in storyboard as below,
I have rendered image as original, but my issue is when I run in simulator, the upper half of rounded circle is missing as shown in image,
I have set class for UITabBar,
class ProminentTabBar: UITabBar {
var prominentButtonCallback: (()->())?
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let items = items, items.count>0 else {
return super.hitTest(point, with: event)
}
let middleItem = items[items.count/2]
let middleExtra = middleItem.imageInsets.top
let middleWidth = bounds.width/CGFloat(items.count)
let middleRect = CGRect(x: (bounds.width-middleWidth)/2, y: middleExtra, width: middleWidth, height: abs(middleExtra))
if middleRect.contains(point) {
prominentButtonCallback?()
return nil
}
return super.hitTest(point, with: event)
}
}
and tabbarcontroller added below lines as well,
override func viewDidLoad() {
super.viewDidLoad()
let prominentTabBar = self.tabBar as! ProminentTabBar
prominentTabBar.prominentButtonCallback = prominentTabTaped
}
func prominentTabTaped() {
selectedIndex = (tabBar.items?.count ?? 0)/2
}
This source was from stack overflow ticket:- How do we create a bigger center UITabBar Item
Does anyone have solution for this>?
make
tabbar.clipsToBounds = false
You can change it via code or in StoryBoard
Keep coding........ :)
Related
Sample project can be found at https://github.com/SRowley90/LargeTitleIssueTestiOS
I am trying to position a segmented control below the Large title in an iOS app. I have a UIToolbar which contains the segmented control inside.
When scrolling up the title and toolbar behave as expected.
When scrolling down the navigation bar is correct, but it doesn't push the UITabBar or the UITableView down, meaning the title goes above the segmented control as can be seen in the images below.
I'm pretty sure it's something to do with the constraints I have set, but I can't figure out what.
The TabBar is fixed to the top, left and right.
The TableView is fixed to the bottom, left and right.
The tableView is fixed vertically to the TabBar
I have the position UITabBarDelegate method set:
func position(for bar: UIBarPositioning) -> UIBarPosition {
return .topAttached
}
Take the delegation of the tableView somewhere:
tableView.delegate = self
Override the scrollViewDidScroll and update toolbar position appearance (since the real position should not change according to have that nice bounce effect.
extension ViewController: UIScrollViewDelegate {
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
var verticalOffset = scrollView.contentOffset.y + defaultNavigationBarHeight
if scrollView.refreshControl?.isRefreshing ?? false {
verticalOffset += 60 // After is refreshing changes its value the toolbar goes 60 points down
print(toolbar.frame.origin.y)
}
if verticalOffset >= 0 {
toolbar.transform = .identity
} else {
toolbar.transform = CGAffineTransform(translationX: 0, y: -verticalOffset)
}
}
}
You can use the following check before applying transformation to make it more reliable and natural to default iOS style:
if #available(iOS 11.0, *) {
guard let navigationController = navigationController else { return }
guard navigationController.navigationBar.prefersLargeTitles else { return }
guard navigationController.navigationItem.largeTitleDisplayMode != .never else { return }
}
Using UIScrollViewDelegate didn't work well with CollectionView and toolbar for me. So, I did:
final class CollectionViewController: UICollectionViewController {
private var observesBag: [NSKeyValueObservation] = []
private let toolbar = UIToolbar()
override func viewDidLoad() {
super.viewDidLoad()
let statusBarHeight = UIApplication.shared.statusBarFrame.height
let navigationBarHeight = navigationController?.navigationBar.frame.height ?? 0
let defaultNavigationBarHeight = statusBarHeight + navigationBarHeight
let observation = navigationController!
.navigationBar
.observe(\.center, options: NSKeyValueObservingOptions.new) { [weak self] navBar, _ in
guard let self = self else { return }
let newNavigatonBarHeight = navBar.frame.height + statusBarHeight
let yTranslantion = newNavigatonBarHeight - defaultNavigationBarHeight
if yTranslantion > 0 {
self.toolbar.transform = CGAffineTransform(
translationX: 0,
y: yTranslantion
)
} else {
self.toolbar.transform = .identity
}
}
observesBag.append(observation)
}
}
Observe the "center" of the navigationBar for changes and then translate the toolbar in the y-axis.
Even though it worked fine when I tried to use this solution with UIRefreshControl and Large Titles it didn't work well.
I set up the refresh control like:
private func setupRefreshControl() {
let refreshControl = UIRefreshControl()
self.webView.scrollView.refreshControl = refreshControl
}
the height of the UINavigationBar is changed after the complete refresh triggers.
I want a popover without rounded corners and with no arrow.
I have done the following code but it did not work:
//SerachPopViewController.swift
//MARK: InitCoder
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
//popover settings
//popoverPresentationController!.permittedArrowDirections = .Any
modalPresentationStyle = .Popover
popoverPresentationController!.delegate = self
//permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0)
self.preferredContentSize = CGSize(width:340,height:380)
}
//QueryTableViewController.swift
#IBAction func searchFilter(sender: AnyObject) {
let searchPopController = storyboard!.instantiateViewControllerWithIdentifier("SerachPopViewController") as! SerachPopViewController
searchPopController.serachPopDelegate = self
searchPopController.modalPresentationStyle = .Popover
searchPopController.preferredContentSize = CGSize(width:340,height:380)
let popoverPresentationController = searchPopController.popoverPresentationController
popoverPresentationController!.sourceView = self.view;
popoverPresentationController!.sourceRect = CGRectMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds),0,0)
popoverPresentationController!.permittedArrowDirections = UIPopoverArrowDirection();
self.presentViewController(searchPopController, animated: true, completion: nil)
}
I am able to display popover view with arrow and rounded arrow.
Please help me to achieve:
popup view with rectangle corner
popup view without direction arrows
Using the concept above, you can also set the corner radius in the completion parameter.
Swift 3
let popoverViewController: UIViewController = // Some view controller to be presented in a popover
// Set popover properties here...
// i.e. popoverViewController.modalPresentationStyle = .popover
present(popoverViewController, animated: true, completion: {
popoverViewController.view.superview?.layer.cornerRadius = 0
// Additional code here
})
In iOS 11 its not possible to use #SHN solution for removing rounded corners. The corner radius is set to default value after viewWillAppear.
Radius must be set in viewDidAppear method
override func viewDidAppear(_ animated: Bool) {
view.superview?.layer.cornerRadius = 0
super.viewDidAppear(animated)
}
To get popover without arrow when you are initiating popover, use:
popover!.permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0)
For popover without corner radius, in the popover content view controller use:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// Do any additional setup after loading the view, typically from a nib.
self.view.superview?.layer.cornerRadius = 0.0;
}
I was not 100% happy with shawnynicole's answer because I realized the change from the rounded to the rectangular corners are notable/visible.
So I came up with this: subclass the view controller (in my case it was an UINavigationController) and override viewDidLayoutSubviews and update corners there. This is better because corners change animation is not visible and it will be updated every time needed (on rotations, etc).
It works in iOS11 and probably should work on others versions too.
class PopoverNavigationController: UINavigationController {
#IBInspectable var cornerRadius: Int = -1
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let r = CGFloat(cornerRadius)
if r >= 0 && view.superview?.layer.cornerRadius != r {
view.superview?.layer.cornerRadius = r
}
}
}
I don't know why It does't work on my Simulator with iOS11.1 until trying setting backgroundColor.I added function viewWillTransition for rotating device.
override func viewDidAppear(_ animated: Bool) {
view.superview?.backgroundColor = UIColor.white
view.superview?.layer.cornerRadius = 0
super.viewDidAppear(animated)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
view.superview?.backgroundColor = UIColor.white
view.superview?.layer.cornerRadius = 0
super.viewWillTransition(to: size, with: coordinator)
}
For Swift 4, in case you present your popover embedded in a navigation controller:
override func viewDidAppear(_ animated: Bool) {
navigationController?.view.superview?.layer.cornerRadius = 0
super.viewDidAppear(animated)
}
That's the way I do it in order to achieve such a functionality which works also on device rotation:
In the view controller that you set as a popover add the following code:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
view.superview?.layer.cornerRadius = 0
}
extension MyPopoverViewController: UIPopoverPresentationControllerDelegate {
func popoverPresentationController(_ popoverPresentationController: UIPopoverPresentationController, willRepositionPopoverTo rect: UnsafeMutablePointer<CGRect>, in view: AutoreleasingUnsafeMutablePointer<UIView>) {
self.view.setNeedsLayout()
}
Finally, set your popover's delegate to the instance of this view controller before presenting it:
...
popover.delegate = myPopoverViewController
present(myPopoverViewController, animated: true)
Many of these answers fix the OP's question about corner radius, but you can get the same effect by changing your presented view controller's top view's background to clear. Then just round the corners of the next top view to the desired amount (or not, if you want it to be a straight top).
I prefer this way because it gives you more WYSIWYG control over the presented view. For example, you can present a floating square by just centering a view in the view controller and presenting it.
Edited
See the comment section with Nathan for the latest project. There is only problem remaining: getting the right button.
Edited
I want to have a UIView that the user can rotate. That UIView should contain some UIButtons that can be clicked. I am having a hard time because I am using a UIControl subclass to make the rotating view and in that subclass I have to disable user interactions on the subviews in the UIControl (to make it spin) which may cause the UIButtons not be tappable. How can I make a UIView that the user can spin and contains clickable UIButtons? This is a link to my project which gives you a head start: it contains the UIButtons and a spinnable UIView. I can however not tap the UIButtons.
Old question with more details
I am using this pod: https://github.com/joshdhenry/SpinWheelControl and I want to react to a buttons click. I can add the button, however I can not receive tap events in the button. I am using hitTests but they never get executed. The user should spin the wheel and be able to click a button in one of the pie's.
Get the project here: https://github.com/Jasperav/SpinningWheelWithTappableButtons
See the code below what I added in the pod file:
I added this variable in SpinWheelWedge.swift:
let button = SpinWheelWedgeButton()
I added this class:
class SpinWheelWedgeButton: TornadoButton {
public func configureWedgeButton(index: UInt, width: CGFloat, position: CGPoint, radiansPerWedge: Radians) {
self.frame = CGRect(x: 0, y: 0, width: width, height: 30)
self.layer.anchorPoint = CGPoint(x: 1.1, y: 0.5)
self.layer.position = position
self.transform = CGAffineTransform(rotationAngle: radiansPerWedge * CGFloat(index) + CGFloat.pi + (radiansPerWedge / 2))
self.backgroundColor = .green
self.addTarget(self, action: #selector(pressed(_:)), for: .touchUpInside)
}
#IBAction func pressed(_ sender: TornadoButton){
print("hi")
}
}
This is the class TornadoButton:
class TornadoButton: UIButton{
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let pres = self.layer.presentation()!
let suppt = self.convert(point, to: self.superview!)
let prespt = self.superview!.layer.convert(suppt, to: pres)
if (pres.hitTest(suppt)) != nil{
return self
}
return super.hitTest(prespt, with: event)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let pres = self.layer.presentation()!
let suppt = self.convert(point, to: self.superview!)
return (pres.hitTest(suppt)) != nil
}
}
I added this to SpinWheelControl.swift, in the loop "for wedgeNumber in"
wedge.button.configureWedgeButton(index: wedgeNumber, width: radius * 2, position: spinWheelCenter, radiansPerWedge: radiansPerWedge)
wedge.addSubview(wedge.button)
This is where I thought I could retrieve the button, in SpinWheelControl.swift:
override open func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let p = touch.location(in: touch.view)
let v = touch.view?.hitTest(p, with: nil)
print(v)
}
Only 'v' is always the spin wheel itself, never the button. I also do not see the buttons print, and the hittest is never executed. What is wrong with this code and why does the hitTest not executes? I rather have a normal UIBUtton, but I thought I needed hittests for this.
Here is a solution for your specific project:
Step 1
In the drawWheel function in SpinWheelControl.swift, enable user interaction on the spinWheelView. To do this, remove the following line:
self.spinWheelView.isUserInteractionEnabled = false
Step 2
Again in the drawWheel function, make the button a subview of the spinWheelView, not the wedge. Add the button as a subview after the wedge, so it will appear on top of the wedge shape layer.
Old:
wedge.button.configureWedgeButton(index: wedgeNumber, width: radius * 0.45, position: spinWheelCenter, radiansPerWedge: radiansPerWedge)
wedge.addSubview(wedge.button)
spinWheelView.addSubview(wedge)
New:
wedge.button.configureWedgeButton(index: wedgeNumber, width: radius * 0.45, position: spinWheelCenter, radiansPerWedge: radiansPerWedge)
spinWheelView.addSubview(wedge)
spinWheelView.addSubview(wedge.button)
Step 3
Create a new UIView subclass that passes touches through to its subviews.
class PassThroughView: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
for subview in subviews {
if !subview.isHidden && subview.alpha > 0 && subview.isUserInteractionEnabled && subview.point(inside: convert(point, to: subview), with: event) {
return true
}
}
return false
}
}
Step 4
At the very beginning of the drawWheel function, declare the spinWheelView to be of type PassThroughView. This will allow the buttons to receive touch events.
spinWheelView = PassThroughView(frame: self.bounds)
With those few changes, you should get the following behavior:
(The message is printed to the console when any button is pressed.)
Limitations
This solution allows the user to spin the wheel as usual, as well as tap any of the buttons. However, this might not be the perfect solution for your needs, as there are some limitations:
The wheel cannot be spun if the users touch down starts within the bounds of any of the buttons.
The buttons can be pressed while the wheel is in motion.
Depending on your needs, you might consider building your own spinner instead of relying on a third-party pod. The difficulty with this pod is that it is using the beginTracking(_ touch: UITouch, with event: UIEvent?) and related functions instead of gesture recognizers. If you used gesture recognizers, it would be easier to make use of all the UIButton functionality.
Alternatively, if you just wanted to recognize a touch down event within the bounds of a wedge, you could pursue your hitTest idea further.
Edit: Determining which button was pressed.
If we know the selectedIndex of the wheel and the starting selectedIndex, we can calculate which button was pressed.
Currently, the starting selectedIndex is 0, and the button tags increase going clockwise. Tapping the selected button (tag = 0), prints 7, which means that the buttons are "rotated" 7 positions in their starting state. If the wheel started in a different position, this value would differ.
Here is a quick function to determine the tag of the button that was tapped using two pieces of information: the wheel's selectedIndex and the subview.tag from the current point(inside point: CGPoint, with event: UIEvent?) implementation of the PassThroughView.
func determineButtonTag(selectedIndex: Int, subviewTag: Int) -> Int {
return subviewTag + (selectedIndex - 7)
}
Again, this is definitely a hack, but it works. If you are planning to continue to add functionality to this spinner control, I would highly recommend creating your own control instead so you can design it from the beginning to fit your needs.
I was able to tinker around with the project and I think I have the solution to your problem.
In your SpinWheelControl class, you are setting the userInteractionEnabled property of the spinWheelViews to false. Note that this is not what you exactly want, because you are still interested in tapping the button which is inside the spinWheelView. However, if you don't turn off user interaction, the wheel won't turn because the child views mess up the touches!
To solve this problem, we can turn off the user interaction for the child views and manually trigger only the events that we are interested in - which is basically touchUpInside for the innermost button.
The easiest way to do that is in the endTracking method of the SpinWheelControl. When the endTracking method gets called, we loop through all the buttons manually and call endTracking for them as well.
Now the problem about which button was pressed remains, because we just sent endTracking to all of them. The solution to that is overriding the endTracking method of the buttons and trigger the .touchUpInside method manually only if the touch hitTest for that particular button was true.
Code:
TornadoButton Class: (the custom hitTest and pointInside are no longer needed since we are no longer interested in doing the usual hit testing; we just directly call endTracking)
class TornadoButton: UIButton{
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
if let t = touch {
if self.hitTest(t.location(in: self), with: event) != nil {
print("Tornado button with tag \(self.tag) ended tracking")
self.sendActions(for: [.touchUpInside])
}
}
}
}
SpinWheelControl Class: endTracking method:
override open func endTracking(_ touch: UITouch?, with event: UIEvent?) {
for sv in self.spinWheelView.subviews {
if let wedge = sv as? SpinWheelWedge {
wedge.button.endTracking(touch, with: event)
}
}
...
}
Also, to test that the right button is being called, just set the tag of the button equal to the wedgeNumber when you are creating them. With this method, you will not need to use the custom offset like #nathan does, because the right button will respond to the endTracking and you can just get its tag by sender.tag.
The general solution would be to use a UIView and place all your UIButtons where they should be, and use a UIPanGestureRecognizer to rotate your view, calculate speed and direction vector and rotate your view. For rotating your view I suggest using transform because it's animatable and also your subviews will be also rotated. (extra: If you want to set direction of your UIButtons always downward, just rotate them in reverse, it will cause them to always look downward)
Hack
Some people also use UIScrollView instead of UIPanGestureRecognizer. Place described View inside the UIScrollView and use UIScrollView's delegate methods to calculate speed and direction then apply those values to your UIView as described. The reason for this hack is because UIScrollView decelerates speed automatically and provides better experience. (Using this technique you should set contentSize to something very big and relocate contentOffset of UIScrollView to .zero periodically.
But I highly suggest the first approach.
As for my opinion, you can use your own view with few sublayers and all other stuff you need.
In this case u will get full flexibility but you also should write a little bit more code.
If you like this option u can get something like on gif below (you can customize it as u wish - add text, images, animations etc):
Here I show you 2 continuous pan and one tap on purple section - when tap is detected6 bg color changed to green
To detect tap I used touchesBegan as shown below.
To play with code for this you can copy-paste code below in to playground and modify as per your needs
//: A UIKit based Playground for presenting user interface
import UIKit import PlaygroundSupport
class RoundView : UIView {
var sampleArcLayer:CAShapeLayer = CAShapeLayer()
func performRotation( power: Float) {
let maxDuration:Float = 2
let maxRotationCount:Float = 5
let currentDuration = maxDuration * power
let currrentRotationCount = (Double)(maxRotationCount * power)
let fromValue:Double = Double(atan2f(Float(transform.b), Float(transform.a)))
let toValue = Double.pi * currrentRotationCount + fromValue
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = fromValue
rotateAnimation.toValue = toValue
rotateAnimation.duration = CFTimeInterval(currentDuration)
rotateAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
rotateAnimation.isRemovedOnCompletion = true
layer.add(rotateAnimation, forKey: nil)
layer.transform = CATransform3DMakeRotation(CGFloat(toValue), 0, 0, 1)
}
override func layoutSubviews() {
super.layoutSubviews()
drawLayers()
}
private func drawLayers()
{
sampleArcLayer.removeFromSuperlayer()
sampleArcLayer.frame = bounds
sampleArcLayer.fillColor = UIColor.purple.cgColor
let proportion = CGFloat(20)
let centre = CGPoint (x: frame.size.width / 2, y: frame.size.height / 2)
let radius = frame.size.width / 2
let arc = CGFloat.pi * 2 * proportion / 100 // i.e. the proportion of a full circle
let startAngle:CGFloat = 45
let cPath = UIBezierPath()
cPath.move(to: centre)
cPath.addLine(to: CGPoint(x: centre.x + radius * cos(startAngle), y: centre.y + radius * sin(startAngle)))
cPath.addArc(withCenter: centre, radius: radius, startAngle: startAngle, endAngle: arc + startAngle, clockwise: true)
cPath.addLine(to: CGPoint(x: centre.x, y: centre.y))
sampleArcLayer.path = cPath.cgPath
// you can add CATExtLayer and any other stuff you need
layer.addSublayer(sampleArcLayer)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let point = touches.first?.location(in: self) {
if let layerArray = layer.sublayers {
for sublayer in layerArray {
if sublayer.contains(point) {
if sublayer == sampleArcLayer {
if sampleArcLayer.path?.contains(point) == true {
backgroundColor = UIColor.green
}
}
}
}
}
}
}
}
class MyViewController : UIViewController {
private var lastTouchPoint:CGPoint = CGPoint.zero
private var initialTouchPoint:CGPoint = CGPoint.zero
private let testView:RoundView = RoundView(frame:CGRect(x: 40, y: 40, width: 100, height: 100))
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
testView.layer.cornerRadius = testView.frame.height / 2
testView.layer.masksToBounds = true
testView.backgroundColor = UIColor.red
view.addSubview(testView)
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(MyViewController.didDetectPan(_:)))
testView.addGestureRecognizer(panGesture)
}
#objc func didDetectPan(_ gesture:UIPanGestureRecognizer) {
let touchPoint = gesture.location(in: testView)
switch gesture.state {
case .began:
initialTouchPoint = touchPoint
break
case .changed:
lastTouchPoint = touchPoint
break
case .ended, .cancelled:
let delta = initialTouchPoint.y - lastTouchPoint.y
let powerPercentage = max(abs(delta) / testView.frame.height, 1)
performActionOnView(scrollPower: Float(powerPercentage))
initialTouchPoint = CGPoint.zero
break
default:
break
}
}
private func performActionOnView(scrollPower:Float) {
testView.performRotation(power: scrollPower)
} } // Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
I want a popover without rounded corners and with no arrow.
I have done the following code but it did not work:
//SerachPopViewController.swift
//MARK: InitCoder
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
//popover settings
//popoverPresentationController!.permittedArrowDirections = .Any
modalPresentationStyle = .Popover
popoverPresentationController!.delegate = self
//permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0)
self.preferredContentSize = CGSize(width:340,height:380)
}
//QueryTableViewController.swift
#IBAction func searchFilter(sender: AnyObject) {
let searchPopController = storyboard!.instantiateViewControllerWithIdentifier("SerachPopViewController") as! SerachPopViewController
searchPopController.serachPopDelegate = self
searchPopController.modalPresentationStyle = .Popover
searchPopController.preferredContentSize = CGSize(width:340,height:380)
let popoverPresentationController = searchPopController.popoverPresentationController
popoverPresentationController!.sourceView = self.view;
popoverPresentationController!.sourceRect = CGRectMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds),0,0)
popoverPresentationController!.permittedArrowDirections = UIPopoverArrowDirection();
self.presentViewController(searchPopController, animated: true, completion: nil)
}
I am able to display popover view with arrow and rounded arrow.
Please help me to achieve:
popup view with rectangle corner
popup view without direction arrows
Using the concept above, you can also set the corner radius in the completion parameter.
Swift 3
let popoverViewController: UIViewController = // Some view controller to be presented in a popover
// Set popover properties here...
// i.e. popoverViewController.modalPresentationStyle = .popover
present(popoverViewController, animated: true, completion: {
popoverViewController.view.superview?.layer.cornerRadius = 0
// Additional code here
})
In iOS 11 its not possible to use #SHN solution for removing rounded corners. The corner radius is set to default value after viewWillAppear.
Radius must be set in viewDidAppear method
override func viewDidAppear(_ animated: Bool) {
view.superview?.layer.cornerRadius = 0
super.viewDidAppear(animated)
}
To get popover without arrow when you are initiating popover, use:
popover!.permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0)
For popover without corner radius, in the popover content view controller use:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// Do any additional setup after loading the view, typically from a nib.
self.view.superview?.layer.cornerRadius = 0.0;
}
I was not 100% happy with shawnynicole's answer because I realized the change from the rounded to the rectangular corners are notable/visible.
So I came up with this: subclass the view controller (in my case it was an UINavigationController) and override viewDidLayoutSubviews and update corners there. This is better because corners change animation is not visible and it will be updated every time needed (on rotations, etc).
It works in iOS11 and probably should work on others versions too.
class PopoverNavigationController: UINavigationController {
#IBInspectable var cornerRadius: Int = -1
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let r = CGFloat(cornerRadius)
if r >= 0 && view.superview?.layer.cornerRadius != r {
view.superview?.layer.cornerRadius = r
}
}
}
I don't know why It does't work on my Simulator with iOS11.1 until trying setting backgroundColor.I added function viewWillTransition for rotating device.
override func viewDidAppear(_ animated: Bool) {
view.superview?.backgroundColor = UIColor.white
view.superview?.layer.cornerRadius = 0
super.viewDidAppear(animated)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
view.superview?.backgroundColor = UIColor.white
view.superview?.layer.cornerRadius = 0
super.viewWillTransition(to: size, with: coordinator)
}
For Swift 4, in case you present your popover embedded in a navigation controller:
override func viewDidAppear(_ animated: Bool) {
navigationController?.view.superview?.layer.cornerRadius = 0
super.viewDidAppear(animated)
}
That's the way I do it in order to achieve such a functionality which works also on device rotation:
In the view controller that you set as a popover add the following code:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
view.superview?.layer.cornerRadius = 0
}
extension MyPopoverViewController: UIPopoverPresentationControllerDelegate {
func popoverPresentationController(_ popoverPresentationController: UIPopoverPresentationController, willRepositionPopoverTo rect: UnsafeMutablePointer<CGRect>, in view: AutoreleasingUnsafeMutablePointer<UIView>) {
self.view.setNeedsLayout()
}
Finally, set your popover's delegate to the instance of this view controller before presenting it:
...
popover.delegate = myPopoverViewController
present(myPopoverViewController, animated: true)
Many of these answers fix the OP's question about corner radius, but you can get the same effect by changing your presented view controller's top view's background to clear. Then just round the corners of the next top view to the desired amount (or not, if you want it to be a straight top).
I prefer this way because it gives you more WYSIWYG control over the presented view. For example, you can present a floating square by just centering a view in the view controller and presenting it.
I have a UIButton that is 36x36 and I want to add padding (in essence like CSS) so that the touchable area is the recommended 44x44.
I've tried adding edge inserts through the Interface Builder and also with the code below but nothing I've tried increases the touchable area.
resetButton.contentEdgeInsets.top = 50
resetButton.contentEdgeInsets.left = 50
Does an edge inset only work with buttons that include text?
Update:
I've tried setting contentEdgeInsets, imageEdgeInsets, and titleEdgeInsets to no avail.
I could expand touchable area by editing the actual image and increasing the border around the icon. (This wouldn't be the preferred way but I'll probably go this route if I can't find another solution.)
I overwrote in Swift this answer and it works:
import UIKit
import ObjectiveC
private var KEY_HIT_TEST_EDGE_INSETS: String = "HitTestEdgeInsets"
extension UIButton {
public func setHitTestEdgeInsets(inout hitTestEdgeInsets: UIEdgeInsets) {
let value = NSValue(&hitTestEdgeInsets, withObjCType:NSValue(UIEdgeInsets: UIEdgeInsetsZero).objCType)
objc_setAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
private func hitTestEdgeInsets() -> UIEdgeInsets {
let value = objc_getAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS)
if (value != nil) {
var edgeInsets: UIEdgeInsets = UIEdgeInsetsZero
value.getValue(&edgeInsets)
return edgeInsets
} else {
return UIEdgeInsetsZero
}
}
override public func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
if UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets(), UIEdgeInsetsZero) || !self.enabled || self.hidden {
return super.pointInside(point, withEvent: event)
}
let relativeFrame = self.bounds
let hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.hitTestEdgeInsets());
return CGRectContainsPoint(hitFrame, point)
}
}
Use:
var insets: UIEdgeInsets = UIEdgeInsetsMake(-20, -20, -20, -20)
button.setHitTestEdgeInsets(&insets)