0

I have a ScrollView which contains a series of views which contain videos. The setup is as follows:

I add the following to my ScrollView:

@ViewBuilder private var items: some View {
    if let products = viewModel.allSortedExclusiveProducts {
        VStack(spacing: 0) {
            ForEach(products, id: \.id) { product in
                ExclusiveProductListingItemView(viewModel: ExclusiveProductListingItemViewModel(product: product), scrollOffset: $scrollOffset)
            }
        }
    }
}

Each ExclusiveProductListingItemView looks like this:

private func videoListing(videoURL: URL) -> some View {
    ZStack(alignment: .bottom) {
        makeVideo(url: videoURL)
            .aspectRatio(contentMode: .fit)
            .frame(width: UIScreen.main.bounds.width)
            .frame(maxHeight: .infinity)

        itemText
    }
}



private func makeVideo(url: URL) -> some View {
        VideoPlayerViewRepresentable(shouldPlayVideo: .constant(true), didError: .constant(false), didFinishPlayingVideo: .constant(false), moveTo: .constant(0.0), videoDidFinishLoading: .constant(true), isLoading: .constant(false), videoURL: url, localVideoFile: nil, backgroundColour: .black, videoType: .standard)
            .environmentObject(VideoPlayerEnvironmentObject())
   }

And the VideoPlayerViewRepresentable is as follows:

struct VideoPlayerViewRepresentable: UIViewControllerRepresentable {
    @EnvironmentObject private var videoPlayerEnvironmentObject: VideoPlayerEnvironmentObject

    @Binding var shouldPlayVideo: Bool
    @Binding var didError: Bool
    @Binding var didFinishPlayingVideo: Bool
    @Binding var moveTo: Double
    @Binding var videoDidFinishLoading: Bool
    @Binding var isLoading: Bool
    @State private var currentPlaybackTime: CMTime = .zero

    let videoURL: URL?
    let localVideoFile: String?
    let backgroundColour: UIColor
    let videoType: VideoType

    var cancellables = Set<AnyCancellable>()
    private let cacheManager = VideoCacheManager.shared
    var isFullScreenGallery: Bool = false

    private let playerItemPublisher = NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime)

    static func dismantleUIViewController(_ uiViewController: AVPlayerViewController, coordinator: Coordinator) {
        uiViewController.player?.removeObserver(coordinator, forKeyPath: #keyPath(AVPlayer.currentItem.status), context: nil)

        if !coordinator.isFullScreenGallery {
            uiViewController.player?.replaceCurrentItem(with: nil)
        }

        coordinator.cancellable?.cancel()
        coordinator.playerDurationObservation?.invalidate()
    }

    private func handleVideoPlayback(context: Context, player: AVPlayer?, playerViewController: AVPlayerViewController, seekTo: CMTime?) {

        if shouldPlayVideo {
            player?.currentItem?.seek(to: seekTo ?? CMTime.zero, completionHandler: nil)
            playerViewController.player?.play()
        }

        context.coordinator.cancellable = playerItemPublisher
            .sink { notification in
                if let playerItem: AVPlayerItem = notification.object as? AVPlayerItem {
                    DispatchQueue.main.async {
                        if shouldPlayVideo {
                            didFinishPlayingVideo = true
                            playerItem.seek(to: CMTime.zero, completionHandler: nil)
                            playerViewController.player?.play()
                        }
                    }
                }
            }
    }

    func makeUIViewController(context: Context) -> AVPlayerViewController {
        let player = videoPlayerEnvironmentObject.player(for: videoURL)
        let playerViewController = AVPlayerViewController()
        playerViewController.view.backgroundColor = backgroundColour
        playerViewController.videoGravity = .resizeAspectFill
        context.coordinator.isFullScreenGallery = isFullScreenGallery
        context.coordinator.isFullScreenGallery = false
        playerViewController.showsPlaybackControls = isFullScreenGallery && videoType != .interactive
        playerViewController.player = player

        // Check if cache is available
        if let videoURL {
            cacheManager.getFileWith(stringUrl: videoURL.absoluteString, autoPlay: videoType == .standard, player: player) { result in
                switch result {
                case .success(let cachedURL):
                    // Update the player with the cached URL
                    let cachedPlayer = AVPlayer(url: cachedURL)
                    cachedPlayer.addObserver(context.coordinator, forKeyPath: #keyPath(AVPlayer.currentItem.status), options: [.new, .old], context: nil)

                    let currentPlayerTime = player?.currentTime()
                    playerViewController.player = cachedPlayer

                    // Start playing the video from the cache if necessary
                    if shouldPlayVideo {
                        // Update the current playback time
                        DispatchQueue.main.async {
                            handleVideoPlayback(context: context, player: player, playerViewController: playerViewController, seekTo: currentPlayerTime)
                        }
                    }
                case .failure:
                    didError = true
                }
            }
        } else if let localVideoFile, let localVideoURL = Bundle.main.url(forResource: localVideoFile, withExtension: "mp4") {
            let localPlayer = AVPlayer(url: localVideoURL)
            localPlayer.addObserver(context.coordinator, forKeyPath: #keyPath(AVPlayer.currentItem.status), options: [.new, .old], context: nil)
            playerViewController.player = localPlayer
            localPlayer.play()
        }

        return playerViewController
    }

    func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
        guard videoType == .interactive else {
            if shouldPlayVideo {
                uiViewController.player?.play()
            } else {
                uiViewController.player?.pause()
            }
            return
        }
        guard let duration = uiViewController.player?.currentItem?.duration,
              duration != .indefinite else {
            return
        }

        let timestampToSeekTo = CMTimeMultiplyByFloat64(duration, multiplier: Float64(moveTo))
        uiViewController.player?.seek(to: timestampToSeekTo, toleranceBefore: .zero, toleranceAfter: .zero)

        DispatchQueue.main.async {
            if uiViewController.player?.timeControlStatus == .playing || uiViewController.player?.timeControlStatus == .paused {
                videoDidFinishLoading = true
            }
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(didError: $didError, isLoading: $isLoading)
    }

    final class Coordinator: NSObject, AVPlayerViewControllerDelegate {
        @Binding private var didError: Bool
        @Binding var isLoading: Bool

        var cancellable: AnyCancellable?
        var isFullScreenGallery: Bool = false
        var playerDurationObservation: NSKeyValueObservation?
        var playerErrorObservation: NSKeyValueObservation?
        var currentPlaybackTime: CMTime = .zero

        init(didError: Binding<Bool>, isLoading: Binding<Bool>) {
            self._didError = didError
            self._isLoading = isLoading
        }

        func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
            playerViewController.showsPlaybackControls = true
        }

        func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
            coordinator.animate(alongsideTransition: nil) { _ in
                playerViewController.player?.play()
            }
        }

        override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
            if keyPath == #keyPath(AVPlayer.currentItem.status) {
                let status: AVPlayerItem.Status

                if let currentStatus = (change?[NSKeyValueChangeKey.newKey] as? NSNumber)?.intValue {
                    status = AVPlayerItem.Status(rawValue: currentStatus) ?? .unknown
                    if status == .readyToPlay {
                        isLoading = false
                    }
                } else {
                    status = .unknown
                }

                if status == .failed {
                    didError = true
                    isLoading = false
                }
            }
        }
    }
}

Now, I need the videoListing views to take up the full width of the screen, and for the height to adjust dynamically. I thought that by adding:

.aspectRatio(contentMode: .fit)
.frame(width: UIScreen.main.bounds.width)
.frame(maxHeight: .infinity)

this would take care of this. However, whilst the videos are full width, the height is not right at all. I am missing a large portion of the top and bottom of each video. The have the right aspect ratio, but the top and bottom are cut off.

The only way I seem to be able to resolve this is by setting an explicit height, but I can't do this as I do not know what height the videos will be in advance.

DevB1
  • 1,235
  • 3
  • 17
  • 43

0 Answers0