ThiagoAM
ThiagoAM

Reputation: 1542

How to add a border color on a popover and its arrow in Swift?

I have the following popover in my app:

enter image description here

But I want it to look like this edited image (note that even the arrow has a border):

enter image description here

Is this possible nowadays? I already tried the following inside the UIViewController:

self.view.layer.borderColor = UIColor.white.cgColor
self.view.layer.borderWidth = 2

But that doesn't work for the arrow. Is there any turn around on this?

Upvotes: 10

Views: 2751

Answers (4)

Golompse
Golompse

Reputation: 101

I found Alexander-Langer's answer worked great except it resulted in two borders, one of which was across the middle of the popup. I found viewWillLayoutSubviews was being called multiple times during setup with two different view sizes and setting the a clear color for the smaller view resulted in a much neater outcome.

    // View Will Layout Subviews
//---------------------------------------------------------------------------------------------------------------------------------------
override func viewWillLayoutSubviews() {
    // Add a colored border around popup
    guard let shapeLayer = view.superview?.superview?.mask?.layer as? CAShapeLayer else { return }
    
    // Found this is called alternately for two different borders and one is smaller than the final
    // Views are about 53 pixels bigger than desired guide size,
    var colr = UIColor.green.cgColor
    if view.bounds.height < (262.0+40.0) {
        colr = UIColor.clear.cgColor
    }
    print("🔲 Rolling view bounds \(view.bounds.height) < ? pref size \(262.0+40.0))")
    
    let borderLayer = CAShapeLayer()
    borderLayer.path = shapeLayer.path
    borderLayer.lineWidth = 4
    borderLayer.fillColor = UIColor.clear.cgColor
    borderLayer.frame = shapeLayer.bounds
    borderLayer.strokeColor = colr
    view.superview?.superview?.layer.addSublayer(borderLayer)
}

In my case "262" was my preferred height and the popup view will be 53 larger to accommodate everything. I just made sure that the smaller view was less than 40 more than my preferred size and the smaller view is given a clear boundary.

Also, setting the view background to the same border color will fill in the little triangle beside the button.

Upvotes: 0

Alexander Langer
Alexander Langer

Reputation: 330

It took me a while, but finally I was able to solve this problem. It begins with a common UiPopoverPresentationController. Nothing special about it yet.

This Controller masks itself somewhere between viewWillAppear and viewDidAppear. So I took the path of its own mask when the popover was shaped and used this to make a sublayer with a stroke.

    override func viewWillLayoutSubviews() {
        guard let shapeLayer = view.superview?.superview?.mask?.layer as? CAShapeLayer else { return }
        let borderLayer = CAShapeLayer()
        borderLayer.path = shapeLayer.path
        borderLayer.lineWidth = 4
        borderLayer.strokeColor = borderColor.cgColor
        borderLayer.fillColor = UIColor.clear.cgColor
        borderLayer.frame = shapeLayer.bounds
        view.superview?.superview?.layer.addSublayer(borderLayer)
    }

The result looks like this.

Result looks like this

Upvotes: 5

Prasad Shinde
Prasad Shinde

Reputation: 85

You can use ElegantPopover to accomplish this.

var design = PSDesign()
design.borders = [PSBorder(filling: .pureColor(yourBorderColor), width: yourBorderWidth)]

// Corner radius of popover
design.cornerRadius = 15

// Customise the arrow as you like
let arrow = PSArrow(height: 30, base: 60, baseCornerRadius: 5, direction: .up)

let popoverController = ElegantPopoverController(contentView: yourContentView,
                                            design: design,
                                            arrow: arrow
                                            barButtonItem: barBtnItem)

present(popoverController, animated: true)

Upvotes: 0

Li Sim
Li Sim

Reputation: 216

Have you tried setting the stroke to the UIBezierPath instead? You can do so with the following lines of code before you close your UIBezierPath:

Color.purple.setStroke()
bezierPath.stroke()

Here's the result I manage to achieve on Playground:

enter image description here

However, I realized that I couldn't change the width of it so instead, I created another layer just for the border instead.

let borderLayer = CAShapeLayer()
borderLayer.frame = self.bounds
borderLayer.path = bezierPath.cgPath
borderLayer.lineWidth = 6.0
borderLayer.strokeColor = UIColor.black.cgColor
borderLayer.fillColor = UIColor.clear.cgColor
self.layer.insertSublayer(borderLayer, at: 0)

Playground result:

enter image description here

I think this should help but I can't really suggest much without the code on how the shape is achieved. Are you using UIBezierPath or CGContext?

Upvotes: 1

Related Questions