"CALayer position contains NaN: [nan nan]" 自定义滑块导致的错误信息

问题描述

我有一个视频播放器开始播放视频(来自 firebase URL),在某些情况下(70% 的情况)我在物理设备上运行时收到此错误消息(异常)(但在模拟器中启动时没有问题) : “CALayer 位置包含 NaN:[nan nan]”

我发现当我评论“VideoPlayerControlsView()”时没有出现错误,所以我很确定问题是我的 CustomerSlider 对象位于这个 VideoPlayerControlsView 视图的内部。

我认为这可能是由加载远程视频引起的,因为视频没有加载,应用程序不知道 AVPlayer 对象的大小/边界,因此无法创建一些父视图(可能是 CustomerSlider)。 .

构建一个最小的可重现示例将是一场噩梦,我只是希望有些人能在我的代码/逻辑中发现错误..如果没有 - 当然会构建它。别无选择。

struct DetailedplayerView : View {
    // The progress through the video,as a percentage (from 0 to 1)
    @State private var videoPos: Double = 0
    // The duration of the video in seconds
    @State private var videoDuration: Double = 0
    // Whether we're currently interacting with the seek bar or doing a seek
    @State private var seeking = false

    private var player: AVPlayer = AVPlayer()
        
    init(item: ExerciseItem,hVideoURL: URL?) {

        if hVideoURL != nil {
            player = AVPlayer(url: hVideoURL!)
            player.isMuted = true
            player.play()
        } else {
            print("[debug] hVideoURL is nil")
        }
    }

    var body: some View {
        ZStack {
            //vstack {
            VideoPlayerView(videoPos: $videoPos,videoDuration: $videoDuration,seeking: $seeking,//timeline: $timeline,//videoTimeline: videoTimeline,player: player)
                .frame(width: UIScreen.screenHeight,height: UIScreen.screenWidth)
                
            vstack {
                
                Spacer()
                VideoPlayerControlsView(videoPos: $videoPos,**<<-----------------------**
                                        videoDuration: $videoDuration,player: player)
                    .frame(width: UIScreen.screenHeight - 2*Constants.scrollPadding,height: 20)
                    .padding(.bottom,20)
            }
                
        }
        .ondisappear {
            // When this View isn't being shown anymore stop the player
            self.player.replaceCurrentItem(with: nil)
        }
    }
}

struct VideoPlayerControlsView : View {
    @Binding private(set) var videoPos: Double
    @Binding private(set) var videoDuration: Double
    @Binding private(set) var seeking: Bool
//    @Binding private(set) var timeline: [Advice]

    @State var shouldStopPlayer: Bool = false
    
    @State var player: AVPlayer
    //let player: AVPlayer
    
    @State private var playerPaused = false
    
    var body: some View {
        HStack {
            // Play/pause button
            Button(action: togglePlayPause) {
                Image(systemName: playerPaused ? "arrowtriangle.right.fill" : "pause.fill")
                    .foregroundColor(Color.mainSubtitleColor)
                    .contentShape(Rectangle())

                    .padding(.trailing,10)
            }
            
            // Current video time
            if videoPos.isFinite && videoPos.isCanonical && videoDuration.isFinite && videoDuration.isCanonical {
            Text(Utility.formatSecondsToHMS(videoPos * videoDuration))
                .foregroundColor(Color.mainSubtitleColor)
            }
            
            // Slider for seeking / showing video progress
            CustomSlider(value: $videoPos,shouldStopPlayer: self.$shouldStopPlayer,range: (0,1),knobWidth: 4) { modifiers in
              ZStack {
                Group {

                    Color(#colorLiteral(red: 1,green: 1,blue: 1,alpha: 0.5799999833106995))//Color((red: 0.4,green: 0.3,blue: 1)
                        .opacity(0.4)
                        .frame(height: 4)
                        .modifier(modifiers.barRight)

                    Color.mainSubtitleColor//Color(red: 0.4,blue: 1)
                        .frame(height: 4)
                        .modifier(modifiers.barLeft)

                }
                .cornerRadius(5)

                vstack {
                    Image(systemName: "arrowtriangle.down.fill") // SF Symbol
                    .foregroundColor(Color.mainSubtitleColor)
                    .offset(y: -3)
                }
                .frame(width: 20,height: 20)
                .contentShape(Rectangle())
                .modifier(modifiers.knob)
              }
            }
            .onChange(of: shouldStopPlayer) { _ in
                if shouldStopPlayer == false {
                    print("[debug] shouldStopPlayer == false")
                    sliderEditingChanged(editingStarted: false)
                } else {
                    if seeking == false {
                        print("[debug] shouldStopPlayer == true")
                        sliderEditingChanged(editingStarted: true)
                    }
                }
            }
            .frame(height: 20)

            // Video duration
            if videoDuration.isCanonical && videoDuration.isFinite {
            Text(Utility.formatSecondsToHMS(videoDuration))
                .foregroundColor(Color.mainSubtitleColor)
            }
        }
        .padding(.leading,40)
        .padding(.trailing,40)
    }
    
    private func togglePlayPause() {
        pausePlayer(!playerPaused)
    }
    
    private func pausePlayer(_ pause: Bool) {
        playerPaused = pause
        
        if playerPaused {
            player.pause()
        }
        else {
           player.play()
        }
    }
    
    private func sliderEditingChanged(editingStarted: Bool) {
        if editingStarted {
            // Set a flag stating that we're seeking so the slider doesn't
            // get updated by the periodic time observer on the player
            seeking = true
            pausePlayer(true)
        }
        
        // Do the seek if we're finished
        if !editingStarted {
            let targetTime = CMTime(seconds: videoPos * videoDuration,preferredTimescale: 600)
            player.seek(to: targetTime) { _ in
                // Now the seek is finished,resume normal operation
                self.seeking = false
                self.pausePlayer(false)
            }
        }
    }
}

extension Double {
    func convert(fromrange: (Double,Double),toRange: (Double,Double)) -> Double {
        // Example: if self = 1,fromrange = (0,2),toRange = (10,12) -> solution = 11
        var value = self
        value -= fromrange.0
        value /= Double(fromrange.1 - fromrange.0)
        value *= toRange.1 - toRange.0
        value += toRange.0
        return value
    }
}

struct CustomSliderComponents {
    let barLeft: CustomSliderModifier
    let barRight: CustomSliderModifier
    let knob: CustomSliderModifier
}

struct CustomSliderModifier: ViewModifier {
    enum Name {
        case barLeft
        case barRight
        case knob
    }
    let name: Name
    let size: CGSize
    let offset: CGFloat

    func body(content: Content) -> some View {
        content
            .frame(width: (size.width >= 0) ? size.width : 0)
            .position(x: size.width*0.5,y: size.height*0.5)
            .offset(x: offset)
    }
}

struct CustomSlider<Component: View>: View {

    @Binding var value: Double
    var range: (Double,Double)
    var knobWidth: CGFloat?
    let viewbuilder: (CustomSliderComponents) -> Component
    @Binding var shouldStopPlayer: Bool
    
    init(value: Binding<Double>,shouldStopPlayer: Binding<Bool>,range: (Double,knobWidth: CGFloat? = nil,_ viewbuilder: @escaping (CustomSliderComponents) -> Component
    ) {
        _value = value
        _shouldStopPlayer = shouldStopPlayer
        self.range = range
        self.viewbuilder = viewbuilder
        self.knobWidth = knobWidth
    }

    var body: some View {
      return GeometryReader { geometry in
        self.view(geometry: geometry) // function below
      }
    }

    private func view(geometry: GeometryProxy) -> some View {
      let frame = geometry.frame(in: .global)
      let drag = DragGesture(minimumdistance: 0)
        .onChanged { drag in
                    shouldStopPlayer = true
                    self.onDragChange(drag,frame)
        }
        .onEnded { drag in
            shouldStopPlayer = false
            //self.updatedValue = value
            print("[debug] slider drag gesture ended,value = \(value)")
        }
      let offsetX = self.getoffsetX(frame: frame)

      let knobSize = CGSize(width: knobWidth ?? frame.height,height: frame.height)
      let barLeftSize = CGSize(width: CGFloat(offsetX + knobSize.width * 0.5),height:  frame.height)
      let barRightSize = CGSize(width: frame.width - barLeftSize.width,height: frame.height)

      let modifiers = CustomSliderComponents(
          barLeft: CustomSliderModifier(name: .barLeft,size: barLeftSize,offset: 0),barRight: CustomSliderModifier(name: .barRight,size: barRightSize,offset: barLeftSize.width),knob: CustomSliderModifier(name: .knob,size: knobSize,offset: offsetX))

      return ZStack { viewbuilder(modifiers).gesture(drag) }
    }
    
    private func onDragChange(_ drag: DragGesture.Value,_ frame: CGRect) {
        let width = (knob: Double(knobWidth ?? frame.size.height),view: Double(frame.size.width))
        let xrange = (min: Double(0),max: Double(width.view - width.knob))
        var value = Double(drag.startLocation.x + drag.translation.width) // knob center x
        value -= 0.5*width.knob // offset from center to leading edge of knob
        value = value > xrange.max ? xrange.max : value // limit to leading edge
        value = value < xrange.min ? xrange.min : value // limit to trailing edge
        value = value.convert(fromrange: (xrange.min,xrange.max),toRange: range)
        //print("[debug] slider drag gesture detected,value = \(value)")
        self.value = value
    }
    
    private func getoffsetX(frame: CGRect) -> CGFloat {
        let width = (knob: knobWidth ?? frame.size.height,view: frame.size.width)
        let xrange: (Double,Double) = (0,Double(width.view - width.knob))

        let result = self.value.convert(fromrange: range,toRange: xrange)
        return CGFloat(result)
    }
  
}

一些额外的代码展示了如何触发DetailedplayerView:

struct DetailedVideo: View {
    var item: ExerciseItem
    var url: URL
    @Binding var isPaused: Bool
    
    var body: some View {
        ZStack {
            
            DetailedplayerView(item: self.item,hVideoURL: url)
                //.frame(width: 500,height: 500) //@@UPDATED: Apr 10 
            
            HStack {
                
                vstack {
                    
                    ZStack {
                        //Rectangle 126
                        RoundedRectangle(cornerRadius: 1)
                            .fill(Color(#colorLiteral(red: 0.3063802123069763,green: 0.3063802123069763,blue: 0.3063802123069763,alpha: 1)))
                            .frame(width: 2,height: 20.3)
                            .rotationEffect(.degrees(-135))
                        
                        //Rectangle 125
                        RoundedRectangle(cornerRadius: 1)
                            .fill(Color(#colorLiteral(red: 0.3063802123069763,height: 20.3)
                            .rotationEffect(.degrees(-45))
                        
                    }
                    .frame(width: 35,height: 35)//14.4
                    .contentShape(Rectangle())
                    .onTapGesture {
                        print("[debugUI] isPaused = false")
                        self.isPaused = false
                    }
                    .offset(x:20,y:20)
                    
                    Spacer()
                }
                
                Spacer()
                
            }
        }
        .ignoresSafeArea(.all)
    }
}

@viewbuilder
    var detailedVideoView: some View {
        if self.hVideoURL != nil {
            DetailedVideo(item: self.exerciseVM.exerciseItems[self.exerciseVM.currentIndex],url: self.hVideoURL!,isPaused: self.$exerciseVM.isPaused) // when is paused - we are playing detailed video?
                .frame(width: UIScreen.screenHeight,height: UIScreen.screenWidth) //UPDATED: Apr 9,2021
                .onAppear {
                    AppDelegate.orientationLock = UIInterfaceOrientationMask.landscapeLeft
                    UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue,forKey: "orientation")
                    UINavigationController.attemptRotationToDeviceOrientation()
                }
                .ondisappear {
                    dispatchQueue.main.async {
                        AppDelegate.orientationLock = UIInterfaceOrientationMask.portrait
                        UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue,forKey: "orientation")
                        UINavigationController.attemptRotationToDeviceOrientation()
                    }
                }
        } else {
            EmptyView()
        }
    }

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)