STerrier
STerrier

Reputation: 4015

Swift - how to resize a UIView dynamically based on the size of a video player frame

I have a video player and a UIView overlaying it which I use for Gesture Recognition in the video area. I am doing this so you can tap different area of the video to play, rewind and do other functions.

At the moment, this works fine in landscape but if I rotate the device to portrait. The UIView doesn't resize but the video player does. I tried to use constraints programmatically but I can't figure out how to access the NSLayout anchor of the video player.

I have a function which retrieve the size of the video frame which I currently use to set the UIView to the size of the video player.

The code below can be just copied into a project to play a video and show the UIView I want to adjust. You just need to add UIView to the UIViewController storyboard. The code does not contain the GestureRecogniser part.

import UIKit
import AVFoundation

class ViewController: UIViewController {

let controlContainerView: UIView = {
    let view = UIView()
    view.backgroundColor = UIColor(white: 0, alpha: 1)
    return view
}()

let activityIndicatorView: UIActivityIndicatorView = {
    let aiv = UIActivityIndicatorView(style: .whiteLarge)
    aiv.translatesAutoresizingMaskIntoConstraints = false
    aiv.startAnimating()
    return aiv
}()

@IBOutlet weak var videoContainer: UIView!
var player: AVPlayer?
var playerLayer: AVPlayerLayer?
var tapRegionBounds: UIView!
var tapRegionAreaCreated: Bool = false

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    let urlString = "https://www.radiantmediaplayer.com/media/bbb-360p.mp4"
    videoPlayer(filename: urlString)
    player?.addObserver(self, forKeyPath: "currentItem.loadedTimeRanges", options: .new, context: nil)

}

override func viewDidAppear(_ animated: Bool) {
    self.playerLayer?.frame = CGRect(x: 0, y: 0, width: self.videoContainer.frame.width, height: self.videoContainer.frame.height)
    updateTapRegions()
}

func videoPlayer(filename: String){
    let item = AVPlayerItem(url: URL(string: filename)!)
    player = AVPlayer(playerItem: item)
    playerLayer = AVPlayerLayer(player: player)
    playerLayer?.videoGravity = .resizeAspect
    playerLayer?.frame = CGRect(x: 0, y: 0, width: self.videoContainer.frame.width, height: self.videoContainer.frame.height)
    videoContainer.layer.addSublayer(playerLayer!)
    player?.play()
}


override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if keyPath == "currentItem.loadedTimeRanges" {
        activityIndicatorView.stopAnimating()
        controlContainerView.backgroundColor = .clear
    }
}

func videoFrame() -> CGRect? {
    guard let layer = playerLayer
        else { return nil }
    let frame = layer.videoRect
    if frame.height > 0.0 {
        let frameInWindow = layer.convert(frame, to: nil)
        return view.convert(frameInWindow, from: nil)
    } else {
        return nil
    }
}

//THE CODE BELOW KIND OF WORKS TO SHOW HOW I WANT IT TO WORK BUT WITHOUT HAVING TO DELETE THE VIEW EACH TIME
func updateTapRegions() {
    guard let video = videoFrame() else { return }
    let container = videoContainer.bounds

    if tapRegionAreaCreated == true {
        tapRegionBounds.removeFromSuperview()
        tapRegionAreaCreated = false
    }

    tapRegionBounds = UIView()
    tapRegionBounds.frame = CGRect(x: video.minX, y: video.minY, width: container.width, height: video.height)
    tapRegionBounds?.alpha = 0.5
    tapRegionBounds.backgroundColor = UIColor.red

    if tapRegionAreaCreated == false {
        view.addSubview(tapRegionBounds)
        tapRegionAreaCreated = true
    }
}

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    coordinator.animate(alongsideTransition: { (context) in
    }) { (context) in
        self.playerLayer?.frame = CGRect(x: 0, y: 0, width: self.videoContainer.frame.width, height: self.videoContainer.frame.height)
        self.updateTapRegions()
    }
}

}

I tried to update the frame in viewWillLayoutSubviews() but nothing changed either. I know my updateTapRegions function isn't the right way but if anyone could point me in the right direction that would be great thanks.

Upvotes: 1

Views: 2973

Answers (2)

matt
matt

Reputation: 535999

Unfortunately you didn't say in your question how you are resizing the video container view on which all this depends. But here's a possibility. I've used auto layout to get these results in portrait and landscape:

enter image description here

enter image description here

Now suppose we have a sublayer (an AVPlayerLayer) and a subview (the tap gesture view). Well, the subview can be resized to fit automatically using autolayout! So only the player layer is left; it doesn't automatically resize using autolayout, but you can resize it manually in viewDidLayoutSubviews.

Upvotes: 2

STerrier
STerrier

Reputation: 4015

Finally, I worked it out by moving the code from transition function(even though the video player resized properly) to viewDidLayoutSubviews which then worked fine. Also I no longer to create and remove the view each time the device changes orientation.

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    self.playerLayer?.frame = CGRect(x: 0, y: 0, width: self.videoContainer.frame.width, height: self.videoContainer.frame.height)

    guard let video = videoFrame() else { return }

    if tapRegionBounds != nil {
         self.tapRegionBounds.frame = CGRect(x: video.minX, y: video.minY, width: video.width, height: video.height)
    }
}

Upvotes: 1

Related Questions