I want to achieve the following focused UI. It is basically the guideline tutorial view for my users.
The background is my main view controller with a bunch of real functional views and I put a view with 50% transparency on black on top of it to achieve the focused UI but it's far away from what I am trying to achieve. Any suggestion would be highly appreciated.
Code:
func createOverlay(frame: CGRect,
xOffset: CGFloat,
yOffset: CGFloat,
radius: CGFloat) -> UIView {
// Step 1
let overlayView = UIView(frame: frame)
overlayView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
// Step 2
let path = CGMutablePath()
path.addArc(center: CGPoint(x: xOffset, y: yOffset),
radius: radius,
startAngle: 0.0,
endAngle: 2.0 * .pi,
clockwise: false)
path.addRect(CGRect(origin: .zero, size: overlayView.frame.size))
// Step 3
let maskLayer = CAShapeLayer()
maskLayer.backgroundColor = UIColor.black.cgColor
maskLayer.path = path
// For Swift 4.0
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
// For Swift 4.2
maskLayer.fillRule = .evenOdd
// Step 4
overlayView.layer.mask = maskLayer
overlayView.clipsToBounds = true
return overlayView
}
We can do this by using an "inverted" shadow-path on the overlay view's layer. That will give us a "feathered-edge" oval.
Here is an example view class:
class FocusView : UIView {
// this will be the frame of the "see-through" oval
public var ovalRect: CGRect = .zero {
didSet { setNeedsLayout() }
}
override func layoutSubviews() {
super.layoutSubviews()
// create an oval inside ovalRect
let clearPath = UIBezierPath(ovalIn: ovalRect)
// create a rectangle path larger than entire view
// so we don't see "feathering" on outer edges
let opaquePath = UIBezierPath(rect: bounds.insetBy(dx: -80.0, dy: -80.0)).reversing()
// append the paths so we get a "see-through" oval
clearPath.append(opaquePath)
self.layer.shadowPath = clearPath.cgPath
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOffset = CGSize.zero
// adjust the opacity as desired
self.layer.shadowOpacity = 0.5
// adjust shadow radius as desired (controls the "feathered" edge)
self.layer.shadowRadius = 8
}
}
and an example controller. We'll add 6 colored rectangles to use as a "background" and a UILabel
to "focus" on:
class ViewController: UIViewController {
let focusView = FocusView()
let label = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .darkGray
// let's add six different color rectangles as the "background"
let colors: [UIColor] = [
.systemRed, .systemGreen, .systemBlue,
.systemBrown, .systemYellow, .systemCyan,
]
let vStack = UIStackView()
vStack.axis = .vertical
vStack.distribution = .fillEqually
var i: Int = 0
for _ in 0..<3 {
let hStack = UIStackView()
hStack.distribution = .fillEqually
for _ in 0..<2 {
let v = UIView()
v.backgroundColor = colors[i % colors.count]
hStack.addArrangedSubview(v)
i += 1
}
vStack.addArrangedSubview(hStack)
}
vStack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(vStack)
label.font = .systemFont(ofSize: 30.0, weight: .bold)
label.textColor = .white
label.textAlignment = .center
label.text = "Example"
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
focusView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(focusView)
// start with it hidden and transparent
focusView.isHidden = true
focusView.alpha = 0.0
NSLayoutConstraint.activate([
vStack.topAnchor.constraint(equalTo: view.topAnchor),
vStack.leadingAnchor.constraint(equalTo: view.leadingAnchor),
vStack.trailingAnchor.constraint(equalTo: view.trailingAnchor),
vStack.bottomAnchor.constraint(equalTo: view.bottomAnchor),
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 80.0),
focusView.topAnchor.constraint(equalTo: view.topAnchor),
focusView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
focusView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
focusView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// we may want to make sure the focusView is on top of all other views
view.bringSubviewToFront(focusView)
// set the focusView's "see-through" oval rect
// it can be set with a hard-coded rect, or
// for this example, we'll use the label frame
// expanded by 80-points horizontally, 60-points vertically
focusView.ovalRect = label.frame.insetBy(dx: -40.0, dy: -30.0)
if focusView.isHidden {
focusView.isHidden = false
UIView.animate(withDuration: 0.3, animations: {
self.focusView.alpha = 1.0
})
} else {
UIView.animate(withDuration: 0.3, animations: {
self.focusView.alpha = 0.0
}, completion: { _ in
self.focusView.isHidden = true
})
}
}
}
Each tap anywhere will either fade-in or fade-out the focus view: