Получение оболочки SwiftUI AVPlayer для приостановки при исчезновении вида - PullRequest
1 голос
/ 11 марта 2020

TL; DR

Невозможно использовать привязку, чтобы сказать обернутому AVPlayer остановиться - почему бы и нет? "Один странный трюк" из Влад работает для меня, без состояния и привязки, но почему?

См. Также

Мой вопрос похож на этот , но этот плакат хотел обернуть AVPlayerViewController, и я хочу управлять воспроизведением программно .

Этот парень также задавался вопросом, когда был вызван updateUIView().

Что происходит (Журналы консоли показаны ниже.)

С кодом, показанным здесь,

  • Пользователь нажимает «Go на Mov ie»

    • MovieView, и отображается видео
    • Это потому, что updateUIView(_:context:) вызывается
  • Пользователь нажимает "Go back Home"

    • HomeView появляется
    • Воспроизведение останавливается
    • Снова вызывается updateUIView.
    • См. Журнал консоли 1
  • Но ... удалить строку ### и

    • Воспроизведение продолжается, даже когда возвращается исходный вид
    • updateUIView по прибытии, но не при отъезде
    • См. Журнал консоли 2
  • Если вы раскомментируете код %%% (и прокомментируете, что ему предшествует)

    • Вы получите код, который, как мне показалось, был логически и идиоматически правильным SwiftUI ...
    • ... но «это не работает». Т.е. видео играет по прибытии, но продолжается при отправлении.
    • См. Журнал консоли 3

Код

I do use @EnvironmentObject так что - это происходит некоторое разделение состояний.

1099 * Основной вид контента (ничего спорно здесь):
struct HomeView: View {
    @EnvironmentObject var router: ViewRouter

    var body: some View {
        ZStack() {  // +++ Weird trick ### fails if this is Group(). Wtf?
            if router.page == .home {
                Button(action: { self.router.page = .movie }) {
                    Text("Go to Movie")
                }
            } else if router.page == .movie {
                MovieView()
            }
        }
    }
}

, который использует один из них (до сих пор обычного декларативного SwiftUI):

1104

Теперь мы попадаем в AVKit -specifi c прочее. Я использую подход, описанный Крисом Ма sh.

Вышеупомянутым PlayerView, упаковщик:

struct PlayerView: UIViewRepresentable {
    @EnvironmentObject var router: ViewRouter
    // @Binding var isPlaying: Bool     // %%%

    private var myUrl : URL?   { Bundle.main.url(forResource: "myVid", withExtension: "mp4") }

    func makeUIView(context: Context) -> PlayerView {
        PlayerUIView(frame: .zero , url  : myUrl)
    }

    // ### This one weird trick makes OS call updateUIView when view is disappearing.
    class DummyClass { } ; let x = DummyClass()

    func updateUIView(_ v: PlayerView, context: UIViewRepresentableContext<PlayerView>) {
        print("> updateUIView()")
        print("  router.isPlayingAV = \(router.isPlayingAV)")
        // print("  isPlaying = \(isPlaying)") // %%%

        // This does work. But *only* with the Dummy code ### included.
        // See also +++ comment in HomeView
        if router.isPlayingAV  { v.player?.pause() }
        else                   { v.player?.play() }

        // This logic looks reversed, but is correct.
        // If it's the other way around, vid never plays. Try it!
        //   if isPlaying { v?.player?.play()   }   // %%%
        //   else         { v?.player?.pause()  }   // %%%

        print("< updateUIView()")
    }
}

И завернутый UIView:

class PlayerUIView: UIView {
    private let playerLayer = AVPlayerLayer()
    var player: AVPlayer?

    init(frame: CGRect, url: URL?) {
        super.init(frame: frame)
        guard let u = url else { return }

        self.player = AVPlayer(url: u)
        self.playerLayer.player = player
        self.layer.addSublayer(playerLayer)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        playerLayer.frame = bounds
    }

    required init?(coder: NSCoder) { fatalError("not implemented") }
}

И, конечно, маршрутизатор представления, основанный на примере Blckbirds

class ViewRouter : ObservableObject {
    let objectWillChange = PassthroughSubject<ViewRouter, Never>()

    enum Page { case home, movie }

    var page = Page.home { didSet { objectWillChange.send(self) } }

    // Claim: App will never play more than one vid at a time.
    var isPlayingAV = false  // No didSet necessary.
}

Журналы консоли

Консоль log 1 (воспроизведение останавливается по желанию)

> updateUIView()                // First call
  router.isPlayingAV = false    // Vid is not playing => play it.
< updateUIView()
> onAppear()
< onAppear()
> updateUIView()                // Second call
  router.isPlayingAV = true     // Vid is playing => pause it.
< updateUIView()
> onDisappear()                 // After the fact, we clear
< onDisappear()                 // the isPlayingAV flag.

Console log 2 (странный трюк отключен; воспроизведение продолжается)

> updateUIView()                // First call
  router.isPlayingAV = false
< updateUIView()
> onAppear()
< onAppear()
                                // No second call.
> onDisappear()
< onDisappear()

Console log 3 (попытка использовать состояние и привязку; воспроизведение продолжается )

> updateUIView()
  isPlaying = false
< updateUIView()
> onAppear()
< onAppear()
> updateUIView()
  isPlaying = true
< updateUIView()
> updateUIView()
  isPlaying = true
< updateUIView()
> onDisappear()
< onDisappear()

1 Ответ

1 голос
/ 11 марта 2020

Ну ... на

}.onDisappear {
    print("> onDisappear()")
    self.router.isPlayingAV = false
    print("< onDisappear()")
}

это называется после удаления (это похоже на didRemoveFromSuperview, а не will...), так что я не вижу ничего плохого / неправильного / неожиданного в том, что подпредставления (или даже оно само) не обновляются (в данном случае updateUIView) ... Я бы скорее удивился, если бы это было так (зачем обновлять представление, которого нет в иерархии представлений ?!).

Так что

class DummyClass { } ; let x = DummyClass()

- это, скорее, какая-то дикая ошибка или ... ошибка. Забудьте об этом и никогда не используйте такие вещи при выпуске продуктов.

Хорошо, теперь можно спросить, как это сделать? Основная проблема, которую я здесь вижу, связана с дизайном, а именно с тесной связью модели и вида в PlayerUIView и, как следствие, невозможностью управлять рабочим процессом. AVPlayer здесь не является частью представления - это модель и в зависимости от ее состояний AVPlayerLayer dr aws content. Таким образом, решение состоит в том, чтобы разделить эти сущности и управлять ими отдельно: представления по видам, модели по моделям.

Вот демонстрация модифицированного и упрощенного подхода, который ведет себя как ожидалось (без странных вещей и без o Ограничения группы / ZStack), и его можно легко расширить или улучшить (на уровне модели / модели)

Протестировано с Xcode 11.2 / iOS 13.2

Полный код модуля (можно копировать - вставлено в ContentView.swift в проекте из шаблона)

import SwiftUI
import Combine
import AVKit

struct MovieView: View {
    @EnvironmentObject var router: ViewRouter

    // just for demo, but can be interchangable/modifiable
    let playerModel = PlayerViewModel(url: Bundle.main.url(forResource: "myVid", withExtension: "mp4")!)

    var body: some View {
        VStack() {
            PlayerView(viewModel: playerModel)
            Button(action: { self.router.page = .home }) {
                Text("Go back Home")
            }
        }.onAppear {
            self.playerModel.player?.play() // << changes state of player, ie model
        }.onDisappear {
            self.playerModel.player?.pause() // << changes state of player, ie model
        }
    }
}

class PlayerViewModel: ObservableObject {
    @Published var player: AVPlayer? // can be changable depending on modified URL, etc.
    init(url: URL) {
        self.player = AVPlayer(url: url)
    }
}

struct PlayerView: UIViewRepresentable { // just thing wrapper, as intended
    var viewModel: PlayerViewModel

    func makeUIView(context: Context) -> PlayerUIView {
        PlayerUIView(frame: .zero , player: viewModel.player) // if needed viewModel can be passed completely
    }

    func updateUIView(_ v: PlayerUIView, context: UIViewRepresentableContext<PlayerView>) {
    }
}

class ViewRouter : ObservableObject {
    enum Page { case home, movie }

    @Published var page = Page.home // used native publisher
}

class PlayerUIView: UIView {
    private let playerLayer = AVPlayerLayer()
    var player: AVPlayer?

    init(frame: CGRect, player: AVPlayer?) { // player is a model so inject it here
        super.init(frame: frame)

        self.player = player
        self.playerLayer.player = player
        self.layer.addSublayer(playerLayer)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        playerLayer.frame = bounds
    }

    required init?(coder: NSCoder) { fatalError("not implemented") }
}

struct ContentView: View {
    @EnvironmentObject var router: ViewRouter

    var body: some View {
        Group {
            if router.page == .home {
                Button(action: { self.router.page = .movie }) {
                    Text("Go to Movie")
                }
            } else if router.page == .movie {
                MovieView()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...