0

Building a pageview that acts as a vertical carousal for videos that autoplay when they are in viewport and pause when out of viewport. Autoplay when in view seems to be working so far(although a bit hacky and an improvement advice is welcomed). An important feature in the feed/pageview is that when the video ends, it is automatically scrolled to the next page/video. The current logic I am using is to simply increment the tab selection/current page when video is over/on button press. This does make it move to the next page but the auto play gets messed up because the app seem to think its still on the old video, in short the tab gets changed but the content of the child view remain same in the memory. Attaching the source code for the parent and Child views.

ByteFeedView.swift

import SwiftUI
import ActivityIndicators


struct ByteFeedView: View {
    var showIndicator=true
    
    @State private var bytes: [ByteFeedQuery.Data.Foryoubyte] = []
    @State var currentPage:Int = 0
    @State var selection = 1
    @State var limit: Int = 10
    @State var offset: Int = 10
    
    var body: some View {
        
        ZStack {

            Color.black.ignoresSafeArea() //Bacgkround of the whole screen is black and ignored safe area
         
            GeometryReader { proxy in
                if(bytes.isEmpty){
                    VStack(alignment: .leading){
        
                        Text("Loading bytes for you").foregroundColor(.white)
                        
                        
                    }   .frame(width: 400, height: 650)
                }
                else{
                    
             TabView(selection: $currentPage) {
                
                        ForEach(0..<bytes.count, id: \.self){ index in
                          
                            VStack{
                                Button("Next byte"){
                    
                          withAnimation {  self.currentPage = self.currentPage +  1 }} //incrementing currentPage to programmitcaly change current tab in view
                                Byte(topic_image: bytes[index].video.topic.imageUrl!,topic: bytes[index].video.topic.name, currentPage: $currentPage, byteId: Int(bytes[index].id) ?? 0, video_url:bytes[index].video.url ?? "", videoTitle: bytes[index].video.title ?? "", startTimestamp: bytes[index].startTimestamp, endTimestamp: bytes[index].endTimestamp).tag(Int(bytes[index].id)) .onChange(of: currentPage, perform: { value in
                                    print("\nValue is \(value)")
                                    print("byteid is \(bytes[index].id)")
                                    print("current page is \(currentPage)")
                       
                                       
                                      })
                            }.onAppear(){
                                if(bytes.count - currentPage == 4){
                                    Network.shared.apollo.fetch(query: ByteFeedQuery(limit:limit, offset: offset)) { result in
                                      
                                        switch result {
                                        case .success(let graphQLResult):
                                            if let bytes = graphQLResult.data?.foryoubytes {
                                              
                                                
                       
                                                
                                       
                                                DispatchQueue.main.async {
                                 
                                                    self.bytes += bytes
                          
                                                }
                                                offset = self.bytes.count + 10
                             
                                            }
                                        case .failure(let error):
                                            print(error)
                                        }
                                    }
                               
                                    
                                }
                            }
                        }
                  
                        .rotationEffect(.degrees(-90)) // Rotate content
                        .frame(
                            width: proxy.size.width,
                            height: proxy.size.height
                        )
                    }
                    .frame(
                        width: proxy.size.height, // Height & width swap
                        height: proxy.size.width
                    )
                   
                    .rotationEffect(.degrees(90), anchor: .topLeading) // Rotate TabView
                    .offset(x: proxy.size.width) // Offset back into screens bounds
                    .id(bytes.count)
                    .tabViewStyle(
                        PageTabViewStyle(indexDisplayMode: .always)
                    )
                }
            }
        }
        .onAppear(){
    
            Network.shared.apollo.fetch(query: ByteFeedQuery(limit:limit, offset: 0)) { result in
              
                switch result {
                case .success(let graphQLResult):
                    if let bytes = graphQLResult.data?.foryoubytes {
                        DispatchQueue.main.async {
                   
                            self.bytes = bytes
                            
                        }
                 
                    }
                case .failure(let error):
                    print(error)
                }
            }
        }


}
    

    
}

Byte.swift

import SwiftUI
import AVKit
import AVFAudio
import GoogleSignIn




struct Byte : View{
    @State var seekPos = 0.0
    //let analyticsModel = AnalyticsViewModel()
    let buildNumber: String = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String
    @State private var playing: Bool = false
    @State private var debouncer = 0
    @Binding var thisViewTag : Int
    @Binding var currentPage : Int
    @State var videoTitle : String
    @State var byteId : Int
    @State var video_url :String
    @State var topic: String
    @State var topic_image: String
    @State var startTimestamp : String
    @State var endTimestamp : String
    
    
    @State private var embeddedVideoRate: Float = 0.0
    @State private var embeddedVideoVolume: Float = 0.0
    @State var didAppear = false
    @State var appearCount = 0
    
    
    
    var body: some View{
        VStack(alignment: .leading){
   
            GeometryReader { gp in
                   VStack{
            Text("Build #\(String(buildNumber))").foregroundColor(Color.white).fontWeight(.bold)
                Text("\(videoTitle)").foregroundColor(Color.white)

                    
                       
            HStack{
                Text("Page is \(currentPage)").foregroundColor(Color.white)
                Text("Byte is \(byteId)").foregroundColor(Color.white)
            }
                ZStack{
            
        bytePlayer(video_url: video_url)
                    if(!playing){
                                               Button(action: {
                                                   if(playing){
                                                       playing=false
                                                   }
                                                   else{
                                                       playing=true
                                                   }
                                                   
                                               }) {
                                                   
                                                   Image(systemName: "play.circle.fill").resizable()
                                                       .frame(width: 45.0, height: 45.0)
                                               }
                                               .foregroundColor(.white)
                                               .padding(.all)
                                           }
                    
      
                    
          
                }
                       
                   }
                
            }
        }
    }
    
    init(topic_image:String, topic:String, currentPage:Binding<Int>, byteId: Int, video_url: String, videoTitle : String, startTimestamp: String, endTimestamp:String) {
      
        self._thisViewTag = currentPage
        self._currentPage = currentPage
        self.byteId = byteId
        self.video_url = video_url
        self.videoTitle = videoTitle
        self.topic_image = topic_image
        self.topic = topic
        self.startTimestamp =  startTimestamp
        self.endTimestamp = endTimestamp
    }
    
    
    
    private func metaData(title:String, topic_image:String, topic:String)-> some View{
        VStack(alignment: .leading){
            HStack{
                RemoteImage(url:topic_image)
                            .aspectRatio(contentMode: .fit)
                            .frame(width: 40).clipShape(Circle())
                Text(topic)
                    .fontWeight(.heavy).foregroundColor(Color.white).multilineTextAlignment(.trailing).frame(maxWidth: .infinity, alignment: .leading).font(.system(size: 15))
            }
            Text(videoTitle)
                .fontWeight(.medium).foregroundColor(Color.white).frame(maxWidth: .infinity, alignment: .leading).font(.system(size: 15))
            HStack(spacing: 8){
                Text("100 views")
                    .fontWeight(.semibold).foregroundColor(Color.white).font(.system(size: 15))
                Circle().foregroundColor(Color.white).frame(width:4,height:4)
                Text("View video")
                    .fontWeight(.semibold).foregroundColor(Color.red).font(.system(size: 15))
            }.padding(.top,1)
   
        }.padding(.bottom,12)
    }
    
    private func bytePlayer(video_url: String) -> some View {
        
VStack {
         
    Spacer()
    if #available(iOS 15.0, *) {
        BytePlayerView(
            videoUrl: video_url,
            rate: $embeddedVideoRate,
            volume: $embeddedVideoVolume,playing: $playing,startTimestamp: $startTimestamp, endTimestamp: $endTimestamp,currentPage: $currentPage, byteId: $byteId).frame(height:250)
            .onAppear(){
                if(currentPage == 0){
                   playing=true
                }
            }.onDisappear{
                print("\nbyteid out of view is \(byteId)")
                playing = false
            }
            .onChange(of: currentPage, perform: { value in
                //print("\nvalue is \(value)")
                //print("byteid is \(byteId)")
                //print("current page is \(currentPage)")
                //print("viewtag is \(thisViewTag)")
                    if value == thisViewTag {
                    playing = true
                      debouncer += 1
                      if debouncer == 1 {
                     
                      }
                    } else {
                      debouncer = 0
                    }
                  })
            .onTapGesture {
                if(playing){
                    playing=false
                }
                else{
                    playing=true
                }
                
            }
    }
         
    Spacer()
        }
    }
}

BytePlayer.swift


import Foundation
import SwiftUI
import AVFoundation
import AVKit


struct BytePlayerView: UIViewRepresentable {
    
    var analyticsModel = AnalyticsViewModel()
    
    
    let videoUrl : String
    @Binding var rate: Float
    @Binding var volume: Float
    @Binding var playing: Bool
    @Binding var startTimestamp: String
    @Binding var endTimestamp: String
    @Binding var currentPage: Int
    @Binding var byteId: Int
    
    
    
    func makeUIView(context: Context) -> BytePlayerUIView {
        analyticsModel.trackByteWatching(byteId: String(byteId), videoTitle:"Hello")
        
        let view = BytePlayerUIView(videoUrl: videoUrl,currentPage:$currentPage)
        let endTimestampInMiliseconds:Double = Double(endTimestamp)!
        let startTimestampInMiliseconds: Double = Double(startTimestamp)!
      
        view.jumpToStartTimestamp(startTimestamp: startTimestampInMiliseconds/1000)
        view.addTimer(startTimestamp: startTimestampInMiliseconds/1000, endTimestamp: endTimestampInMiliseconds/1000)
        view.overrideRinger()
    
        return view
        
    }
    
    
    func dismantleUIView(_ uiView: BytePlayerUIView) {
        uiView.pause()
        print("Should dismantle")
    }
    
    
    func updateUIView(_ uiView: BytePlayerUIView, context: Context) {
        if(playing){
            uiView.play()
        }
        else{
            uiView.pause()
        }
    }
    
    
    
    final class BytePlayerUIView: UIView{
        
        @Binding var currentPage:Int
        public var player: AVPlayer?
        var timeObserverToken: Any?
        @Published private var playing = true
        var currenTime = 0.0
        var timertest: Timer?
        var currenTimeString = "00:00"
        
        func jumpToStartTimestamp(startTimestamp: Double){
            let startTimestamp_CMTime=CMTime(seconds:startTimestamp, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
            self.player!.seek(to: startTimestamp_CMTime, toleranceBefore: .zero, toleranceAfter: .zero)

        }
        
        var isPlaying: Bool {
            if (self.player!.rate != 0 && self.player!.error == nil) {
                    return true
                } else {
                    return false
                }
            }
        
        func overrideRinger(){
            do {
                try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback)
                //print("AVAudioSession Category Playback OK")
                do {
                    try AVAudioSession.sharedInstance().setActive(true)
                    //print("AVAudioSession is Active")
                } catch _ as NSError {
                    //print(error.localizedDescription)
                }
            } catch _ as NSError {
                //print(error.localizedDescription)
            }
        }
        
        func addTimer(startTimestamp: Double,endTimestamp: Double){
            timertest=Timer.scheduledTimer(withTimeInterval: 1/60, repeats: true) { timer in
                
                if self.player!.currentItem?.status == .readyToPlay {
                    
                    let timeElapsed = CMTimeGetSeconds(self.player!.currentTime())
                    let endTimestamp_CMTime=CMTime(seconds:endTimestamp, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
                    
                    let secs = Int(timeElapsed)
                    let endTimeStampSecs = CMTimeGetSeconds(endTimestamp_CMTime)
                
                    let startTimestamp_CMTime=CMTime(seconds:startTimestamp, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
                    
                    
                    
                    self.currenTime = timeElapsed
                    self.currenTimeString = NSString(format: "%02d:%02d", secs/60, secs%60) as String
                    
                    if(endTimeStampSecs-timeElapsed<=2){
                        self.setVolume( 0.3)
                    }
                    if(timeElapsed >= endTimeStampSecs){
                        
                        self.timertest?.invalidate()
                        self.player?.seek(to: startTimestamp_CMTime, toleranceBefore: .zero, toleranceAfter: .zero)
                        self.pause()
                        self.moveToNextPage()
                 
                  
                        
                        
                    }
                    
                    
              
                }
            }
            
        }
        
        
        func moveToNextPage(){
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                    withAnimation {  self.currentPage = self.currentPage +  1 }
                }
        }
        
        
        
        fileprivate let seekDuration: Float64 = 5
        @IBAction func doForwardJump(startTimestamp:String) {
            let moveForword : Float64 = 5
            
            if player == nil { return }
            if let duration  = player!.currentItem?.duration {
                let playerCurrentTime = CMTimeGetSeconds(player!.currentTime())
                let newTime = playerCurrentTime + moveForword
                if newTime < CMTimeGetSeconds(duration)
                {
                    let selectedTime: CMTime = CMTimeMake(value: Int64(newTime * 1000 as Float64), timescale: 1000)
                    player!.seek(to: selectedTime)
                }
                player?.pause()
                player?.play()
            }
            
        }
        
        func playPause() {
            if playing {
                self.playing=false
                player?.pause()
            } else {
                self.playing=true
                player?.play()
            }
            
        }
        
        
        func play(){
            self.playing=true;
            player?.playImmediately(atRate: 1)
        }
        
        
        func pause(){
            self.playing=false;
            player?.pause()
        }
        func cleanup() {
            self.playing=false;
            player?.pause()
            //player?.removeAllItems()
            player = nil
        }
        
        func setVolume(_ value: Float) {
            player?.volume = value
        }
        
        func setRate(_ value: Float) {
            player?.rate = value
        }
        
        private var token: NSKeyValueObservation?
        
        override class var layerClass: AnyClass {
            return AVPlayerLayer.self
        }
        
        var playerLayer: AVPlayerLayer {
            return layer as! AVPlayerLayer
        }
        
        init(videoUrl : String, currentPage:Binding<Int>){
            
            self._currentPage=currentPage
            player = AVPlayer(url: URL(string: videoUrl)!)
            super.init(frame: .zero)
            playerLayer.player = player
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
    }
}

If this info isn't enough, can share whatever else is needed.

  • 1
    Welcome to Stack Overflow! I think this is a case of too much information, but not a [reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). Your question involves the state of your play needing to be restarted. We can't run this code, so we can test what is happening, and the amount of code is a bit overwhelming. Unless someone sees something from reading the code, you won't get an answer. Give us something more simple that has the same problem. You may end up resolving it yourself by making the minimal, reproducible example. – Yrb Oct 09 '21 at 19:14
  • It's not unusual that the system would keep the view that is being scrolled out in memory because you might want to scroll back to it. That view might be removed from memory if the system decides it is low on resources, but generally speaking iOS tries to hold on to things like off-screen views to avoid having to do a bunch of work to load them back in. – Scott Thompson Oct 10 '21 at 01:18
  • I think OP is going to have to maintain separate state for each view, and track it in the model. – Yrb Oct 10 '21 at 17:22

0 Answers0