How to dismiss PKToolPicker when view disappears in SwiftUI? - swiftui

I'm trying to wrap PKCanvasView as a SwiftUI view called CanvasView. I'd like to be able to toggle the whole canvas on top of another view. When the CanvasView appears, I'd like the PKToolPicker to appear. When it disappears, I'd like the PKToolPicker to disappear.
I've found a few similar approaches on here but they only involve showing the picker or toggling the picker with a button; I'd like the picker visibility to be tied to the view visibility.
In the below example you can see that you can toggle the canvas, but once the tool picker is visible, it stays visible.
Here's my CanvasView:
import SwiftUI
import PencilKit
struct CanvasView: UIViewRepresentable {
class Coordinator: NSObject, PKCanvasViewDelegate {
var canvasView: Binding<PKCanvasView>
let onChange: () -> Void
init(canvasView: Binding<PKCanvasView>, onChange: #escaping () -> Void) {
self.canvasView = canvasView
self.onChange = onChange
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
if canvasView.drawing.bounds.isEmpty == false {
onChange()
}
}
}
#Binding var canvasView: PKCanvasView
#Binding var toolPickerIsActive: Bool
private let toolPicker = PKToolPicker()
let onChange: () -> Void
func makeUIView(context: Context) -> PKCanvasView {
canvasView.backgroundColor = .clear
canvasView.isOpaque = true
canvasView.delegate = context.coordinator
showToolPicker()
return canvasView
}
func updateUIView(_ uiView: PKCanvasView, context: Context) {
toolPicker.setVisible(toolPickerIsActive, forFirstResponder: uiView)
}
func showToolPicker() {
toolPicker.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
canvasView.becomeFirstResponder()
}
func makeCoordinator() -> Coordinator {
Coordinator(canvasView: $canvasView, onChange: onChange)
}
}
And an example ContentView:
struct ContentView: View {
#State private var canvasView = PKCanvasView()
#State private var toolPickerIsActive = false
#State private var canvasIsVisible = false
var body: some View {
ZStack {
if canvasIsVisible {
CanvasView(canvasView: $canvasView,
toolPickerIsActive: $toolPickerIsActive,
onChange: canvasDidChange)
.onAppear { toolPickerIsActive = true }
.onDisappear { toolPickerIsActive = false }
}
Button(action: {
canvasIsVisible.toggle()
}, label: {
Text("Toggle canvas view")
})
}
}
private func canvasDidChange() {
// Do something with updated canvas.
}
}
Any guidance would be much appreciated!

In your scenario the CanvasView is destroyed on disappear, so SwiftUI rendering engine just not update it on any state change (as it see that no needs for that).
The possible solution for this use-case is to hide picker on coordinator deinit (because it is destroyed with owner view).
Here is a demo. Tested with Xcode 12.4 / iOS 14.4
struct CanvasView: UIViewRepresentable {
class Coordinator: NSObject, PKCanvasViewDelegate {
var canvasView: Binding<PKCanvasView>
let onChange: () -> Void
private let toolPicker: PKToolPicker
deinit { // << here !!
toolPicker.setVisible(false, forFirstResponder: canvasView.wrappedValue)
toolPicker.removeObserver(canvasView.wrappedValue)
}
init(canvasView: Binding<PKCanvasView>, toolPicker: PKToolPicker, onChange: #escaping () -> Void) {
self.canvasView = canvasView
self.onChange = onChange
self.toolPicker = toolPicker
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
if canvasView.drawing.bounds.isEmpty == false {
onChange()
}
}
}
#Binding var canvasView: PKCanvasView
#Binding var toolPickerIsActive: Bool
private let toolPicker = PKToolPicker()
let onChange: () -> Void
func makeUIView(context: Context) -> PKCanvasView {
canvasView.backgroundColor = .clear
canvasView.isOpaque = true
canvasView.delegate = context.coordinator
showToolPicker()
return canvasView
}
func updateUIView(_ uiView: PKCanvasView, context: Context) {
toolPicker.setVisible(toolPickerIsActive, forFirstResponder: uiView)
}
func showToolPicker() {
toolPicker.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
canvasView.becomeFirstResponder()
}
func makeCoordinator() -> Coordinator {
Coordinator(canvasView: $canvasView, toolPicker: toolPicker, onChange: onChange)
}
}
struct ContentView: View {
#State private var canvasView = PKCanvasView()
#State private var toolPickerIsActive = false
#State private var canvasIsVisible = false
var body: some View {
ZStack {
if canvasIsVisible {
CanvasView(canvasView: $canvasView,
toolPickerIsActive: $toolPickerIsActive,
onChange: canvasDidChange)
.onAppear { toolPickerIsActive = true }
// .onDisappear { toolPickerIsActive = false }
}
Button(action: {
canvasIsVisible.toggle()
}, label: {
Text("Toggle canvas view")
})
}
}
private func canvasDidChange() {
// Do something with updated canvas.
}
}
Note: there might be redesign of ownership, so toolPicker will live only within coordinator, but it does not change idea, and is up to you.

Related

Can't drag down/dismiss UIColorPickerViewController

I am displaying a UIColorPickerViewController as a sheet using the sheet() method, everything works fine but I can't drag down/dismiss the view anymore.
import Foundation
import SwiftUI
struct ColorPickerView: UIViewControllerRepresentable {
private var selectedColor: UIColor!
init(selectedColor: UIColor) {
self.selectedColor = selectedColor
}
func makeUIViewController(context: Context) -> UIColorPickerViewController {
let colorPicker = UIColorPickerViewController()
colorPicker.selectedColor = self.selectedColor
return colorPicker
}
func updateUIViewController(_ uiViewController: UIColorPickerViewController, context: Context) {
// Silent
}
}
.sheet(isPresented: self.$viewManager.showSheet, onDismiss: {
ColorPickerView()
}
Any idea how to make the drag/down dismiss gesture works?
Thanks!
Ran into the same problem when trying to build a color picker similar to above. What worked was "wrapping" the color picker in a view with a Dismiss button. And also discovered that the bar at the top of the view would allow the picker to now be dragged down and away. Below is my wrapper. (One could add more features such as a title to the bar.)
struct ColorWrapper: View {
var inputColor: UIColor
#Binding var isShowingColorPicker: Bool
#Binding var selectedColor: UIColor?
var body: some View {
VStack {
HStack {
Spacer()
Button("Dismiss", action: {
isShowingColorPicker = false
}).padding()
}
ColorPickerView(inputColor: inputColor, selectedColor: $selectedColor)
}
}
}
And for completeness, here is my version of the color picker:
import SwiftUI
struct ColorPickerView: UIViewControllerRepresentable {
typealias UIViewControllerType = UIColorPickerViewController
var inputColor: UIColor
#Binding var selectedColor: UIColor?
#Environment(\.presentationMode) var isPresented
func makeUIViewController(context: Context) -> UIColorPickerViewController {
let picker = UIColorPickerViewController()
picker.delegate = context.coordinator
picker.supportsAlpha = false
picker.selectedColor = inputColor
return picker
}
func updateUIViewController(_ uiViewController: UIColorPickerViewController, context: Context) {
uiViewController.supportsAlpha = false
}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
class Coordinator: NSObject, UINavigationControllerDelegate, UIColorPickerViewControllerDelegate {
var parent: ColorPickerView
init(parent: ColorPickerView) {
self.parent = parent
}
func colorPickerViewControllerDidFinish(_ viewController: UIColorPickerViewController) {
parent.isPresented.wrappedValue.dismiss()
}
func colorPickerViewController(_ viewController: UIColorPickerViewController, didSelect color: UIColor, continuously: Bool) {
parent.selectedColor = color
// parent.isPresented.wrappedValue.dismiss()
}
}
}

UISearchController how to activate from SwiftUI

I found an integration of the uiSearchController in SwiftUI, but I don't know how to let it become active?
I found this:
I want that the searchBar becomes active when changing an Bool in the SwiftUI View with a #State for example.
If I add a Binding to the view modifier and set the isActive property of the searchController in
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
viewController.navigationItem.hidesSearchBarWhenScrolling = false
}
then is doesn't become active.
Im not really familiar with UIKit, perhaps anybody knows how to correctly activate the searchbar that one can start typing for a search.
class SearchBar: NSObject, ObservableObject {
#Published var text: String = ""
let searchController: UISearchController = UISearchController(searchResultsController: nil)
override init() {
super.init()
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchResultsUpdater = self
}
}
extension SearchBar: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
// Publish search bar text changes.
if let searchBarText = searchController.searchBar.text {
self.text = searchBarText
}
}
}
final class ViewControllerResolver: UIViewControllerRepresentable {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
}
func makeUIViewController(context: Context) -> ParentResolverViewController {
ParentResolverViewController(onResolve: onResolve)
}
func updateUIViewController(_ uiViewController: ParentResolverViewController, context: Context) {
}
}
class ParentResolverViewController: UIViewController {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("Use init(onResolve:) to instantiate ParentResolverViewController.")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if let parent = parent {
onResolve(parent)
}
}
}
struct SearchBarModifier: ViewModifier {
let searchBar: SearchBar
func body(content: Content) -> some View {
content
.overlay(
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
viewController.navigationItem.hidesSearchBarWhenScrolling = false
}
.frame(width: 0, height: 0)
)
}
}
extension View {
func add(_ searchBar: SearchBar) -> some View {
return self.modifier(SearchBarModifier(searchBar: searchBar))
}
}
To activate a UISearchBar (which is what you're using), just do:
searchController.searchBar.becomeFirstResponder()
(from this answer)
Now all we need to do is reference searchController.searchBar from the SwiftUI view. First, add a function to your SearchBar class.
class SearchBar: NSObject, ObservableObject {
#Published var text: String = ""
let searchController: UISearchController = UISearchController(searchResultsController: nil)
override init() {
super.init()
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchResultsUpdater = self
}
/// add this function
func activate() {
searchController.searchBar.becomeFirstResponder()
}
}
Then, just call it. I think this is better than setting a #State, but if you require that, let me know and I'll edit my answer.
struct ContentView: View {
#StateObject var searchBar = SearchBar()
var body: some View {
NavigationView {
Button(action: {
searchBar.activate() /// activate the search bar
}) {
Text("Activate search bar")
}
.modifier(SearchBarModifier(searchBar: searchBar))
.navigationTitle("Navigation View")
}
}
}
Result:

Implement PencilKit undo functionality using SwiftUI

Edit: Thanks to some of the feedback, I have been able to get this partially working (updated code to reflect current changes).
Even though the app appears to be working as intended, I am still getting the 'Modifying state...' warning. How can I update the view's drawing in updateUIView and push new drawings onto the stack with the canvasViewDrawingDidChange without causing this issue? I have tried wrapping it in a dispatch call, but that just creates an infinite loop.
I'm trying to implement undo functionality in a UIViewRepresentable (PKCanvasView). I have a parent SwiftUI view called WriterView which holds two buttons and the canvas.
Here's the parent view:
struct WriterView: View {
#State var drawings: [PKDrawing] = [PKDrawing()]
var body: some View {
VStack(spacing: 10) {
Button("Clear") {
self.drawings = []
}
Button("Undo") {
if !self.drawings.isEmpty {
self.drawings.removeLast()
}
}
MyCanvas(drawings: $drawings)
}
}
}
Here is how I've implemented my UIViewRepresentable:
struct MyCanvas: UIViewRepresentable {
#Binding var drawings: [PKDrawing]
func makeUIView(context: Context) -> PKCanvasView {
let canvas = PKCanvasView()
canvas.delegate = context.coordinator
return canvas
}
func updateUIView(_ uiView: PKCanvasView, context: Context) {
uiView.drawing = self.drawings.last ?? PKDrawing()
}
func makeCoordinator() -> Coordinator {
Coordinator(self._drawings)
}
class Coordinator: NSObject, PKCanvasViewDelegate {
#Binding drawings: [PKDrawing]
init(_ drawings: Binding<[PKDrawing]>) {
self._drawings = drawings
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
drawings.append(canvasView.drawing)
}
}
}
I am getting the following error:
[SwiftUI] Modifying state during view update, this will cause undefined behavior.
Presumably it is being caused by my coordinator's did change function, but I'm not sure how to fix this. What is the best way to approach this?
Thanks!
I finally (accidentally) figured out how to do this using UndoManager. I'm still not sure exactly why this works, because I never have to call self.undoManager?.registerUndo(). Please comment if you understand why I never have to register an event.
Here's my working parent view:
struct Writer: View {
#Environment(\.undoManager) private var undoManager
#State private var canvasView = PKCanvasView()
var body: some View {
VStack(spacing: 10) {
Button("Clear") {
canvasView.drawing = PKDrawing()
}
Button("Undo") {
undoManager?.undo()
}
Button("Redo") {
undoManager?.redo()
}
MyCanvas(canvasView: $canvasView)
}
}
}
Here's my working child view:
struct MyCanvas: UIViewRepresentable {
#Binding var canvasView: PKCanvasView
func makeUIView(context: Context) -> PKCanvasView {
canvasView.drawingPolicy = .anyInput
canvasView.tool = PKInkingTool(.pen, color: .black, width: 15)
return canvasView
}
func updateUIView(_ canvasView: PKCanvasView, context: Context) { }
}
This certainly feels more like the intended approach for SwiftUI and is certainly more elegant than the attempts I made earlier.
just for completeness and if you want to show the PKToolPicker, here is my UIViewRepresentable.
import Foundation
import SwiftUI
import PencilKit
struct PKCanvasSwiftUIView : UIViewRepresentable {
let canvasView = PKCanvasView()
#if !targetEnvironment(macCatalyst)
let coordinator = Coordinator()
class Coordinator: NSObject, PKToolPickerObserver {
// initial values
var color = UIColor.black
var thickness = CGFloat(30)
func toolPickerSelectedToolDidChange(_ toolPicker: PKToolPicker) {
if toolPicker.selectedTool is PKInkingTool {
let tool = toolPicker.selectedTool as! PKInkingTool
self.color = tool.color
self.thickness = tool.width
}
}
func toolPickerVisibilityDidChange(_ toolPicker: PKToolPicker) {
if toolPicker.selectedTool is PKInkingTool {
let tool = toolPicker.selectedTool as! PKInkingTool
self.color = tool.color
self.thickness = tool.width
}
}
}
func makeCoordinator() -> PKCanvasSwiftUIView.Coordinator {
return Coordinator()
}
#endif
func makeUIView(context: Context) -> PKCanvasView {
canvasView.isOpaque = false
canvasView.backgroundColor = UIColor.clear
canvasView.becomeFirstResponder()
#if !targetEnvironment(macCatalyst)
if let window = UIApplication.shared.windows.filter({$0.isKeyWindow}).first,
let toolPicker = PKToolPicker.shared(for: window) {
toolPicker.addObserver(canvasView)
toolPicker.addObserver(coordinator)
toolPicker.setVisible(true, forFirstResponder: canvasView)
}
#endif
return canvasView
}
func updateUIView(_ uiView: PKCanvasView, context: Context) {
}
}
I think the error probably comes from the private func clearCanvas()
and private func undoDrawing(). Try this to see if it works:
private func clearCanvas() {
DispatchQueue.main.async {
self.drawings = [PKDrawing()]
}
}
Similarly for undoDrawing().
If it is from canvasViewDrawingDidChange, do same trick.
I have something working with this:
struct MyCanvas: UIViewRepresentable {
#Binding var drawings: [PKDrawing]
func makeUIView(context: Context) -> PKCanvasView {
let canvas = PKCanvasView()
canvas.delegate = context.coordinator
return canvas
}
func updateUIView(_ canvas: PKCanvasView, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self._drawings)
}
class Coordinator: NSObject, PKCanvasViewDelegate {
#Binding var drawings: [PKDrawing]
init(_ drawings: Binding<[PKDrawing]>) {
self._drawings = drawings
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
self.drawings.append(canvasView.drawing)
}
}
}
I think I have something working without the warning using a different approach.
struct ContentView: View {
let pkCntrl = PKCanvasController()
var body: some View {
VStack(spacing: 10) {
Button("Clear") {
self.pkCntrl.clear()
}
Spacer()
Button("Undo") {
self.pkCntrl.undoDrawing()
}
Spacer()
MyCanvas(cntrl: pkCntrl)
}
}
}
struct MyCanvas: UIViewRepresentable {
var cntrl: PKCanvasController
func makeUIView(context: Context) -> PKCanvasView {
cntrl.canvas = PKCanvasView()
cntrl.canvas.delegate = context.coordinator
cntrl.canvas.becomeFirstResponder()
return cntrl.canvas
}
func updateUIView(_ uiView: PKCanvasView, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, PKCanvasViewDelegate {
var parent: MyCanvas
init(_ uiView: MyCanvas) {
self.parent = uiView
}
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
if !self.parent.cntrl.didRemove {
self.parent.cntrl.drawings.append(canvasView.drawing)
}
}
}
}
class PKCanvasController {
var canvas = PKCanvasView()
var drawings = [PKDrawing]()
var didRemove = false
func clear() {
canvas.drawing = PKDrawing()
drawings = [PKDrawing]()
}
func undoDrawing() {
if !drawings.isEmpty {
didRemove = true
drawings.removeLast()
canvas.drawing = drawings.last ?? PKDrawing()
didRemove = false
}
}
}

How to change #State property wrapper from nested view

I'm wondering how I should change #State property wrapper showErrorAlert in view below
struct SettingsView: View {
#State private var shouldPresent = false
#State var showErrorAlert = false
var body: some View {
VStack {
Form {
Text("Settings")
.font(.title)
Button("Import source data") {
self.shouldPresent.toggle()
}
.sheet(isPresented: $shouldPresent) {
DocumentPicker()
}
Button("Show error alert") {
self.showErrorAlert.toggle()
}
.alert(isPresented: $showErrorAlert, content: {
Alert(title: Text("Error"))
})
}
}
}
}
from DocumentPicker struct code in case that reading of selected file fails.
struct DocumentPicker: UIViewControllerRepresentable {
func makeCoordinator() -> DocumentPicker.Coordinator {
return DocumentPicker.Coordinator(parent: self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<DocumentPicker>) -> UIDocumentPickerViewController {
let picker = UIDocumentPickerViewController(documentTypes: [String(kUTTypeJSON)], in: .import)
picker.allowsMultipleSelection = false
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: UIViewControllerRepresentableContext<DocumentPicker>) {
}
class Coordinator: NSObject, UIDocumentPickerDelegate {
var myParent: DocumentPicker
init(parent: DocumentPicker) {
myParent = parent
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
let fileURL = urls.first!
do {
let origFile = try String(contentsOf: fileURL)
//File processing will be here
} catch let error {
print(error)
}
}
}
}
I mean how to set property wrapper value to true to show the alert. Should I rather use #ObservedObject or #EnvironmentObject instead?
Thanks.
To change the wrapper value in your DocumentPicker struct you can define a #Binding variable and pass your value to it, this toggle your variable on your parent view, but before showing the alert you need to dismiss the DocumentPicker

SwiftUI: How to Properly Code AVPlayer After Loading A Video From the Device with an ImagePickerController?

I used SwiftUI to create a Video Player, which loads a video using an imagePickerController, and then it is suppose to play the video once retrieved from the device. I found that the Video Player was not refreshing after retrieving the video. I am not sure how to give the appropriate #State|#Binding which is necessary to refresh it.
I learned how to code a Video Player using available online resources. And I have found a way to load a video from my device and load it to my video player. However, when I press the play button, after I have loaded the video, only the sound was played. I have tried to make the video player #State|#Binding but cannot find the solution as it does not appear to be intuitively done so.
Can anyone suggest how to update my code for the Video Player using SwiftUI?
P.S. 1) You must use an actual device to load video; and 2) The slider does not work yet. I will work on that next.
Disclosure:
I have adapted this code from online resources.
The original source code for the majority of this work can be found at these links:
How to open the ImagePicker in SwiftUI?
https://www.raywenderlich.com/5135-how-to-play-record-and-merge-videos-in-ios-and-swift
https://medium.com/#chris.mash/avplayer-swiftui-part-2-player-controls-c28b721e7e27
import SwiftUI
import AVKit
import PhotosUI
import MobileCoreServices
struct ContentView: View {
#State var showImagePicker: Bool = false
#State var url: URL?
var body: some View {
ZStack {
VStack {
Button(action: {
withAnimation {
self.showImagePicker.toggle()
}
}) {
Text("Show image picker")
}
// The video player will needs to be a #State??? as it is not updated with UIView changes but works when no view changes occur.
PlayerContainerView(player: AVPlayer(url: url ?? URL(string: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8")!))
}
if (showImagePicker) {
ImagePicker(isShown: $showImagePicker, url: $url)
}
}
}
}
struct PlayerView: UIViewRepresentable {
let player: AVPlayer
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PlayerView>) {
}
func makeUIView(context: Context) -> UIView {
return PlayerUIView(player: player)
}
}
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
init(player: AVPlayer) {
super.init(frame: .zero)
playerLayer.player = player
layer.addSublayer(playerLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
}
struct PlayerContainerView : View {
#State var seekPos = 0.0
private let player: AVPlayer
init(player: AVPlayer) {
self.player = player
}
var body: some View {
VStack {
PlayerView(player: player)
PlayerControlsView(player: player)
}
}
}
struct PlayerControlsView : View {
#State var playerPaused = true
#State var seekPos = 0.0
let player: AVPlayer
var body: some View {
HStack {
Button(action: {
self.playerPaused.toggle()
if self.playerPaused {
self.player.pause()
}
else {
self.player.play()
}
}) {
Image(systemName: playerPaused ? "play" : "pause")
.padding(.leading, 20)
.padding(.trailing, 20)
}
Slider(value: $seekPos, from: 0, through: 1, onEditingChanged: { _ in
guard let item = self.player.currentItem else {
return
}
let targetTime = self.seekPos * item.duration.seconds
self.player.seek(to: CMTime(seconds: targetTime, preferredTimescale: 600))
})
.padding(.trailing, 20)
}
}
}
struct ImagePicker: UIViewControllerRepresentable {
#Binding var isShown: Bool
#Binding var url: URL?
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
#Binding var isShown: Bool
#Binding var url: URL?
init(isShown: Binding<Bool>, url: Binding<URL?>) {
$isShown = isShown
$url = url
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let info = convertFromUIImagePickerControllerInfoKeyDictionary(info)
guard let mediaType = info[convertFromUIImagePickerControllerInfoKey(UIImagePickerController.InfoKey.mediaType)] as? String,
mediaType == (kUTTypeMovie as String),
let uiURL = info[convertFromUIImagePickerControllerInfoKey(UIImagePickerController.InfoKey.mediaURL)] as? URL
else { return }
url = uiURL
isShown = false
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
isShown = false
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(isShown: $isShown, url: $url)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.mediaTypes = [kUTTypeMovie as String]
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController,
context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
fileprivate func convertFromUIImagePickerControllerInfoKeyDictionary(_ input: [UIImagePickerController.InfoKey: Any]) -> [String: Any] {
return Dictionary(uniqueKeysWithValues: input.map {key, value in (key.rawValue, value)})
}
fileprivate func convertFromUIImagePickerControllerInfoKey(_ input: UIImagePickerController.InfoKey) -> String {
return input.rawValue
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView(showImagePicker: true)
}
}
#endif
The video player does not play the video as expected, it only played the sound which indicates to me that it is playing the video; however, I cannot see it being played. It remains a black box.
UPDATE: The following is the fully edited code that works as expected (except for the slider), which was answered in comments below:
import SwiftUI
import AVKit
import PhotosUI
import MobileCoreServices
struct ContentView: View {
#State var showImagePicker: Bool = false
#State var url: URL?
var body: some View {
ZStack {
VStack {
Button(action: {
withAnimation {
self.showImagePicker.toggle()
}
}) {
Text("Show image picker")
}
PlayerContainerView(player: AVPlayer(url: url ?? URL(string: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8")!))
}
if (showImagePicker) {
ImagePicker(isShown: $showImagePicker, url: $url)
}
}
}
}
struct PlayerView: UIViewRepresentable {
let player: AVPlayer
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PlayerView>) {
(uiView as? PlayerUIView)?.updatePlayer(player: player)
}
func makeUIView(context: Context) -> UIView {
return PlayerUIView(player: player)
}
}
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
init(player: AVPlayer) {
super.init(frame: .zero)
playerLayer.player = player
layer.addSublayer(playerLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
func updatePlayer(player: AVPlayer) {
self.playerLayer.player = player
}
}
struct PlayerContainerView : View {
#State var seekPos = 0.0
private let player: AVPlayer
init(player: AVPlayer) {
self.player = player
}
var body: some View {
VStack {
PlayerView(player: player)
PlayerControlsView(player: player)
}
}
}
struct PlayerControlsView : View {
#State var playerPaused = true
#State var seekPos = 0.0
let player: AVPlayer
var body: some View {
HStack {
Button(action: {
self.playerPaused.toggle()
if self.playerPaused {
self.player.pause()
}
else {
self.player.play()
}
}) {
Image(systemName: playerPaused ? "play" : "pause")
.padding(.leading, 20)
.padding(.trailing, 20)
}
Slider(value: $seekPos, from: 0, through: 1, onEditingChanged: { _ in
guard let item = self.player.currentItem else {
return
}
let targetTime = self.seekPos * item.duration.seconds
self.player.seek(to: CMTime(seconds: targetTime, preferredTimescale: 600))
})
.padding(.trailing, 20)
}
}
}
struct ImagePicker: UIViewControllerRepresentable {
#Binding var isShown: Bool
#Binding var url: URL?
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
#Binding var isShown: Bool
#Binding var url: URL?
init(isShown: Binding<Bool>, url: Binding<URL?>) {
_isShown = isShown
_url = url
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let info = convertFromUIImagePickerControllerInfoKeyDictionary(info)
guard let mediaType = info[convertFromUIImagePickerControllerInfoKey(UIImagePickerController.InfoKey.mediaType)] as? String,
mediaType == (kUTTypeMovie as String),
let uiURL = info[convertFromUIImagePickerControllerInfoKey(UIImagePickerController.InfoKey.mediaURL)] as? URL
else { return }
url = uiURL
isShown = false
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
isShown = false
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(isShown: $isShown, url: $url)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.mediaTypes = [kUTTypeMovie as String]
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController,
context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
fileprivate func convertFromUIImagePickerControllerInfoKeyDictionary(_ input: [UIImagePickerController.InfoKey: Any]) -> [String: Any] {
return Dictionary(uniqueKeysWithValues: input.map {key, value in (key.rawValue, value)})
}
fileprivate func convertFromUIImagePickerControllerInfoKey(_ input: UIImagePickerController.InfoKey) -> String {
return input.rawValue
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView(showImagePicker: true)
}
}
#endif
You left your updateUIView empty. You should implement it:
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PlayerView>) {
(uiView as? PlayerUIView)?.updatePlayer(player: player)
}
And also add the following method to your PlayerUIView:
func updatePlayer(player: AVPlayer) {
self.playerLayer.player = player
}