iosswiftuirefreshcontrol

iOS UIRefreshControl subview opacity similar to the attributedTitle opacity


I have a UIRefreshControl subclass that in addition to the title adds a custom control to its subviews. It works great except of one thing, the pull-based-on-distance-opacity is applied only to the text, and not the subview.

class CustomRefreshControl: UIRefreshControl {
    private let customView = MyCustomView()
    
    override init() {
        super.init()
        addSubview(customView)
    }

How can I dynamically change the customView opacity based on the UIRefreshControl pull distance? I want it to be the same as the attributedTitle opacity.


Solution

  • I don't think there is a dedicated way to match the opacity of the attributed title. I don't even think that UIRefreshControl is designed to be modified in this way, adding random subviews to it.

    I would suggest creating your own view that resembles a UIRefreshControl, that changes its opacity and frame depending on the scroll view's contentOffset. See here for a starting point.

    Assuming a vertical-scroll only scroll view, you could do something like:

    // CustomRefreshControl is just a UIView subclass
    var refreshControl: CustomRefreshControl!
    
    // this is in a UIViewController, but it should be trivial to wrap 
    // a custom UIScrollView instead, if you want that.
    override func viewDidLoad() {
        scrollView.delegate = self
        refreshControl = .init(frame: .zero)
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView.contentOffset.y < 0 {
            if refreshControl.superview == nil {
                scrollView.addSubview(refreshControl)
                scrollView.sendSubviewToBack(refreshControl)
            }
            // update the opacity
            // could be a method in CustomRefreshControl instead, to
            // encapsulate the logic
            refreshControl.layer.opacity = Float(-scrollView.contentOffset.y / 100)
            refreshControl.frame = CGRect(x: 0, y: scrollView.contentOffset.y, width: scrollView.frame.width, height: 100)
        } else {
            refreshControl.removeFromSuperview()
        }
    }
    

    I wouldn't recommend this, but a really hacky solution would be to inspect the view hierarchy of UIRefreshControl and find that it has a subview of type _UIRefreshControlModernContentView. The underscore prefix suggests that this is not a stable API, so could break in future iOS versions.

    As a subview of _UIRefreshControlModernContentView, there is a UILabel displaying the attributedTitle. You can constantly try to set that label's layer.opacity to your custom view's layer.opacity.

    Since the label for attributedTitle appears to move when you scroll, layoutSubviews will be called, and you can do this in there:

    override func layoutSubviews() {
        if let contentViewClass = NSClassFromString("_UIRefreshControlModernContentView"),
           let contentView = subviews.first(where: { $0.isKind(of: contentViewClass) }),
           let label = contentView.subviews.first(where: { $0 is UILabel }) {
            customView.layer.opacity = label.layer.opacity
        }
    }