tvOS: Anyway to display a subtitle outside of the AVPlayer?

So the scenario is that there is a view where the user can enable/disable subtitles in an app I'm helping to develop.

On that view there is a sample text saying "This is what captions look like", and at the moment it's just a basic, unstyled UILabel. Ideally I would like it to be styled in a similar manner to how the user has customized their captions in the System Settings.

Is this possible in any way? I've envisioned two possible method:

  1. Create an AVPlayer instance and a .vtt file with the text, load it into the view and pause the player. I'm not sure this is possible with a sample video (and it would somehow have to be transparent as there is an image behind the sample sub text).

  2. Somehow get all the styling (font, size, background color, etc) the user has set for their subtitle and create an attributed string to match that

Method 2 seems like the most feasible way, but I don't know if we have access to those settings in code.

Upvotes: 2

Views: 1201

Answers (2)

Vurgun M
Vurgun M

Reputation: 107

Unextraordinary's answer is golden! Here is the recent version:

private func applyAccessibilitySubtitleStyle() {
    let domain = MACaptionAppearanceDomain.user

    let backgroundColor = UIColor(cgColor: MACaptionAppearanceCopyWindowColor(domain, nil).takeRetainedValue())
    let backgroundOpacity = MACaptionAppearanceGetWindowOpacity(domain, nil)

    var textAttributes = [NSAttributedString.Key: Any]()
    let fontDescriptor = MACaptionAppearanceCopyFontDescriptorForStyle(domain, nil, MACaptionAppearanceFontStyle.default).takeRetainedValue()
    let fontName = CTFontDescriptorCopyAttribute(fontDescriptor, kCTFontNameAttribute) as! String
    let fontColor = UIColor(cgColor: MACaptionAppearanceCopyForegroundColor(domain, nil).takeRetainedValue())
    let fontOpacity = MACaptionAppearanceGetForegroundOpacity(domain, nil)
    let textEdgeStyle = MACaptionAppearanceGetTextEdgeStyle(domain, nil)
    let textHighlightColor = UIColor(cgColor: MACaptionAppearanceCopyBackgroundColor(domain, nil).takeRetainedValue())
    let textHighlightOpacity = MACaptionAppearanceGetBackgroundOpacity(domain, nil)
    let textEdgeShadow = NSShadow()
    textEdgeShadow.shadowColor = UIColor.black
    let shortShadowOffset: CGFloat = 1.5
    let shadowOffset: CGFloat = 3.5

    switch textEdgeStyle {
    case .none:
        textEdgeShadow.shadowColor = UIColor.clear

    case .dropShadow:
        textEdgeShadow.shadowOffset = CGSize(width: -shortShadowOffset, height: shortShadowOffset)
        textEdgeShadow.shadowBlurRadius = 6

    case .raised:
        textEdgeShadow.shadowOffset = CGSize(width: 0, height: shadowOffset)
        textEdgeShadow.shadowBlurRadius = 5

    case .depressed:
        textEdgeShadow.shadowOffset = CGSize(width: 0, height: -shadowOffset)
        textEdgeShadow.shadowBlurRadius = 5

    case .uniform:
        textEdgeShadow.shadowColor = UIColor.clear
        textAttributes[NSAttributedString.Key.strokeColor] = UIColor.black
        textAttributes[NSAttributedString.Key.strokeWidth] = -2.0

    default:
        break
    }

    textAttributes[NSAttributedString.Key.font] = UIFont(name: fontName, size: (v.subtitlesLabel.font?.pointSize)!)
    textAttributes[NSAttributedString.Key.foregroundColor] = fontColor.withAlphaComponent(fontOpacity)
    textAttributes[NSAttributedString.Key.shadow] = textEdgeShadow
    textAttributes[NSAttributedString.Key.backgroundColor] = textHighlightColor.withAlphaComponent(textHighlightOpacity)
    
    v.subtitlesLabel.layer.backgroundColor = backgroundColor.withAlphaComponent(backgroundOpacity).cgColor
    v.subtitlesLabel.layer.cornerRadius = MACaptionAppearanceGetWindowRoundedCornerRadius(domain, nil)
    v.subtitlesLabel.attributedText = NSAttributedString(string: v.subtitlesLabel.text ?? "", attributes: textAttributes)
}

Upvotes: 0

So I figured it out! It basically makes use a combination of the Media Accessibility API, which allows you to get the values the user has chosen for their captions/subtitle settings, Attributed Strings, and a subclass UILabel (although this could maybe be substituted with a UITextView as that will allow you to set it's UIEdgeInsets natively)

So, first, the subclass is to allow the UILabel to be inset. This is because captions can have a background color AND a text highlight color and without the inset, the text highlight is all you see. So the function the subclass is simple:

class InsetUILabel: UILabel {
    override func drawTextInRect(rect: CGRect) {
        let inset: CGFloat = 15
        let insets: UIEdgeInsets = UIEdgeInsets(top: inset, left: inset/2, bottom: inset, right: inset/2)
        super.drawTextInRect(UIEdgeInsetsInsetRect(rect, insets))
    }
}

And for generating the actual label. This uses a label called textSample, but you can obviously make it a little more general.

import MediaAccessibility

func styleLabel(sampleText: String) {
    let domain = MACaptionAppearanceDomain.User

    // Background styling
    let backgroundColor = UIColor(CGColor: MACaptionAppearanceCopyWindowColor(domain, nil).takeRetainedValue())
    let backgroundOpacity = MACaptionAppearanceGetWindowOpacity(domain, nil)
    textSample.layer.backgroundColor = backgroundColor.colorWithAlphaComponent(backgroundOpacity).CGColor
    textSample.layer.cornerRadius = MACaptionAppearanceGetWindowRoundedCornerRadius(domain, nil)

    // Text styling
    var textAttributes = [String:AnyObject]()
    let fontDescriptor = MACaptionAppearanceCopyFontDescriptorForStyle(domain, nil, MACaptionAppearanceFontStyle.Default).takeRetainedValue()
    let fontName = CTFontDescriptorCopyAttribute(fontDescriptor, "NSFontNameAttribute") as! String
    let fontColor = UIColor(CGColor: MACaptionAppearanceCopyForegroundColor(domain, nil).takeRetainedValue())
    let fontOpacity = MACaptionAppearanceGetForegroundOpacity(domain, nil)
    let textEdgeStyle = MACaptionAppearanceGetTextEdgeStyle(domain, nil)
    let textHighlightColor = UIColor(CGColor: MACaptionAppearanceCopyBackgroundColor(domain, nil).takeRetainedValue())
    let textHighlightOpacity = MACaptionAppearanceGetBackgroundOpacity(domain, nil)
    let textEdgeShadow = NSShadow()
    textEdgeShadow.shadowColor = UIColor.blackColor()
    let shortShadowOffset: CGFloat = 1.5
    let shadowOffset: CGFloat = 3.5

    switch(textEdgeStyle) {
    case .None:
        textEdgeShadow.shadowColor = UIColor.clearColor()

    case .DropShadow:
        textEdgeShadow.shadowOffset = CGSize(width: -shortShadowOffset, height: shortShadowOffset)
        textEdgeShadow.shadowBlurRadius = 6

    case .Raised:
        textEdgeShadow.shadowOffset = CGSize(width: 0, height: shadowOffset)
        textEdgeShadow.shadowBlurRadius = 5

    case .Depressed:
        textEdgeShadow.shadowOffset = CGSize(width: 0, height: -shadowOffset)
        textEdgeShadow.shadowBlurRadius = 5

    case .Uniform:
        textEdgeShadow.shadowColor = UIColor.clearColor()
        textAttributes[NSStrokeColorAttributeName] = UIColor.blackColor()
        textAttributes[NSStrokeWidthAttributeName] = -2.0

    default:
        break
    }

    textAttributes[NSFontAttributeName] = UIFont(name: fontName, size: (textSample.font?.pointSize)!)
    textAttributes[NSForegroundColorAttributeName] = fontColor.colorWithAlphaComponent(fontOpacity)
    textAttributes[NSShadowAttributeName] = textEdgeShadow
    textAttributes[NSBackgroundColorAttributeName] = textHighlightColor.colorWithAlphaComponent(textHighlightOpacity)

    textSample.attributedText = NSAttributedString(string: sampleText, attributes: textAttributes)
}

Now the text highlight section makes use of shadows, with values I think look pretty good, but you might want to tweak them a tiny bit. Hope this helps!

Upvotes: 5

Related Questions