Home > front end >  How to achieve "application guide tutorial screen" with the focus on a particular view swi
How to achieve "application guide tutorial screen" with the focus on a particular view swi

Time:12-14

I want to achieve the following focused UI. It is basically the guideline tutorial view for my users.

enter image description here

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
}

Current output:
enter image description here

CodePudding user response:

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:

enter image description here

enter image description here

  • Related