Best practice for wrapping `AVPlayerView` in `SwiftUI`? - swift

I want to wrap AVPlayerView into SwiftUI. Here are my codes(playground):
import PlaygroundSupport
import SwiftUI
import AVKit
class RecorderPlayerModel: ObservableObject {
#Published var playerView: AVPlayerView
init() {
self.playerView = AVPlayerView()
self.playerView.player = AVPlayer()
}
func reload(url: URL) {
let asset = AVAsset(url: url)
let item = AVPlayerItem(asset: asset)
self.playerView.player?.replaceCurrentItem(with: item)
}
}
struct RecorderPlayerView: NSViewRepresentable {
typealias NSViewType = AVPlayerView
var playerView: AVPlayerView
func makeNSView(context: Context) -> AVPlayerView {
return playerView
}
func updateNSView(_ nsView: AVPlayerView, context: Context) {}
}
struct ContentView: View {
#StateObject var playerViewModel: RecorderPlayerModel = .init()
var body: some View {
VStack {
RecorderPlayerView(playerView: playerViewModel.playerView)
.clipShape(RoundedRectangle(cornerRadius: 8))
.onAppear {
let fileManager = FileManager.default
if let url = URL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4") {
Task {
do {
let (data, _) = try await URLSession.shared.data(from: url)
let fileUrl = fileManager.temporaryDirectory.appendingPathComponent("sample")
.appendingPathExtension(for: .mpeg4Movie)
try? fileManager.removeItem(at: fileUrl)
fileManager.createFile(atPath: fileUrl.path, contents: data)
playerViewModel.reload(url: fileUrl)
} catch {
print(error)
}
}
}
}
Button {
if playerViewModel.playerView.canBeginTrimming {
Task {
await playerViewModel.playerView.beginTrimming()
}
}
} label: {
Text("trim")
}
}.frame(width: 500, height: 500, alignment: .center)
}
}
PlaygroundPage.current.setLiveView(ContentView())
Since I want to trim the video, I cannot directly use VideoPlayer. But after wrapping AVPlayerView to NSViewRepresentable View, the trim view always lose interactivity.
Reproduce way: just double click at anywhere when trimming.
supplement
when losing interactivity, the console will log
-[AVTrimIndicatorAccessibilityElement accessibilityHitTest:]: unrecognized selector sent to instance 0x600001916ce0
updated
It is not triggered on all models of Mac.

I figure out the problem. It seems that applying .clipShape(...) on RecorderPlayerView will cause the problem.
Just remove the line .clipShape(RoundedRectangle(cornerRadius: 8)) will solve this problem.
RecorderPlayerView(playerView: playerViewModel.playerView)
// .clipShape(RoundedRectangle(cornerRadius: 8)) <-- remove this line
.onAppear {
...
}

Related

Load NSImage into Image(named:) in SwiftUI

I have a MacOS app that I am rewriting in SwiftUI. I am completely new to SwiftUI.
I have an Image() and when I drag an image from the Desktop on to that image I want to load the dropped image into it. I am able to detect drop and I am able to switch out the images to show the cursor is hovering over the drop zone. But, what I don't understand is how can I take the file from the desktop and load it into the Image()?
I have the file's URL, I create an NSImage from the contentsOf but how do I get that NSImage into the Image(named:)?
"input" & "inputDropZone" are assets in the asset catalog
Is my approach wrong?
struct ContentView: View {
#State var sourceImage = "input"
var body: some View {
VStack {
Image(sourceImage)
.frame(width: 200.0, height: 200.0)
.onDrop(of: [UTType.fileURL.description], delegate: self)
}
}
}
extension ContentView : DropDelegate {
func performDrop(info: DropInfo) -> Bool {
guard let itemProvider = info.itemProviders(for: [UTType.fileURL.description]).first else { return false }
itemProvider.loadItem(forTypeIdentifier: UTType.fileURL.description) { item, error in
guard let data = item as? Data, let url = URL(dataRepresentation: data, relativeTo: nil) else { return }
if let image = NSImage(contentsOf: url) {
DispatchQueue.main.async {
sourceImage = ? //How do I load an NSImage into an Image(named:)?
}
}
}
return true
}
func dropEntered(info: DropInfo) {
DispatchQueue.main.async {
sourceImage = "inputDropZone"
}
}
func dropExited(info: DropInfo) {
DispatchQueue.main.async {
sourceImage = "input"
}
}
}
I got this to work by taking Asperi's & Vadian's suggestions. I combined Asperi's answer at:
https://stackoverflow.com/a/60832686/12299030
into my own code so that I could use the delegate methods. This is the final working code:
import SwiftUI
import UniformTypeIdentifiers
struct ContentView: View {
#State var sourceImage = NSImage(named: "input")
var body: some View {
VStack {
Image(sourceImage)
.frame(width: 200.0, height: 200.0)
.onDrop(of: [UTType.fileURL.description], delegate: self)
}
}
}
extension ContentView : DropDelegate {
func performDrop(info: DropInfo) -> Bool {
guard let providers = info.itemProviders(for: [UTType.fileURL.description]).first else { return false }
providers.loadDataRepresentation(forTypeIdentifier: "public.file-url", completionHandler: { (data, error) in
if let data = data, let path = NSString(data: data, encoding: 4), let url = URL(string: path as String) {
let image = NSImage(contentsOf: url)
image?.size = CGSize(width: 200.0, height: 200.0)
DispatchQueue.main.async {
self.sourceImage = image
}
}
})
return true
}
func dropEntered(info: DropInfo) {
DispatchQueue.main.async {
sourceImage = NSImage(named: "inputDropZone")
}
}
func dropExited(info: DropInfo) {
DispatchQueue.main.async {
sourceImage = NSImage(named: "input")
}
}
}

SwiftUI Image in App not loading when using Combine

I have been trying to asynchronously load an image in my app using combine. Currently all the other pieces of data are loading fine, but my image seem to be stuck in a progress view. Why? I am not too familiar with how combine works as I have been trying to follow a tutorial and adapting it to fit my needs, which is why I ran into this problem.
This is my code:
Main View:
import SwiftUI
struct ApodView: View {
#StateObject var vm = ApodViewModel()
var body: some View {
ZStack {
// Background Layer
Color.theme.background
.ignoresSafeArea()
// Content Layer
VStack() {
Text(vm.apodData?.title ?? "Placeholder")
.font(.title)
.fontWeight(.bold)
.multilineTextAlignment(.center)
.foregroundColor(Color.theme.accent)
ApodImageView(apodData: vm.apodData ?? ApodModel(date: "", explanation: "", url: "", thumbnailUrl: "", title: ""))
ZStack() {
Color.theme.backgroundTextColor
ScrollView(showsIndicators: false) {
Text(vm.apodData?.explanation ?? "Loading...")
.font(.body)
.fontWeight(.semibold)
.foregroundColor(Color.theme.accent)
.multilineTextAlignment(.center)
.padding()
}
}
.cornerRadius(10)
}
.padding()
}
}
}
ImageView:
import SwiftUI
struct ApodImageView: View {
#StateObject var vm: ApodImageViewModel
init(apodData: ApodModel) {
_vm = StateObject(wrappedValue: ApodImageViewModel(apodData: apodData))
}
var body: some View {
ZStack {
if let image = vm.image {
Image(uiImage: image)
.resizable()
.scaledToFit()
} else if vm.isLoading {
ProgressView()
} else {
Image(systemName: "questionmark")
.foregroundColor(Color.theme.secondaryText)
}
}
.frame(maxWidth: .infinity, maxHeight: 250)
.cornerRadius(10)
}
}
Image ViewModel:
import Foundation
import SwiftUI
import Combine
class ApodImageViewModel: ObservableObject {
#Published var image: UIImage?
#Published var isLoading: Bool = false
private let apodData: ApodModel
private let dataService: ApodImageService
private var cancellables = Set<AnyCancellable>()
init(apodData: ApodModel) {
self.apodData = apodData
self.dataService = ApodImageService(apodData: apodData)
self.addSubscribers()
self.isLoading = true
}
private func addSubscribers() {
dataService.$image
.sink { [weak self] _ in
self?.isLoading = false
} receiveValue: { [weak self] returnedImage in
self?.image = returnedImage
}
.store(in: &cancellables)
}
}
Networking For Image:
import Foundation
import SwiftUI
import Combine
class ApodImageService: ObservableObject {
#Published var image: UIImage?
private var imageSubscription: AnyCancellable?
private let apodData: ApodModel
init(apodData: ApodModel) {
self.apodData = apodData
getApodImage()
}
private func getApodImage() {
guard let url = URL(string: apodData.thumbnailUrl ?? apodData.url) else { return }
imageSubscription = NetworkingManager.download(url: url)
.tryMap({ data -> UIImage? in
return UIImage(data: data)
})
.sink(receiveCompletion: NetworkingManager.handleCompletion, receiveValue: { [weak self] returnedImage in
self?.image = returnedImage
self?.imageSubscription?.cancel()
})
}
}
General Networking Code:
import Foundation
import Combine
class NetworkingManager {
enum NetworkingError: LocalizedError {
case badURLResponse(url: URL)
case unknown
var errorDescription: String? {
switch self {
case .badURLResponse(url: let url): return "Bad response from URL: \(url)"
case .unknown: return "Unknown Error Returned"
}
}
}
static func download(url: URL) -> AnyPublisher<Data, Error> {
return URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .background))
.tryMap({ try handleURLResponse(output: $0, url: url) })
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
Where you have:
private func addSubscribers() {
dataService.$image
.sink { [weak self] _ in
self?.isLoading = false
} receiveValue: { [weak self] returnedImage in
self?.image = returnedImage
}
.store(in: &cancellables)
}
You are subscribing to the published value of the image property. That image property stream will never complete. It is an infinite sequence tracking the value of that property over time "forever".
I don't think your receiveCompletion will ever be called so self?.isLoading = false will never happen.

How to know when PDF document is downloaded in PDFKit?

I'm building a really simple app, which displays PDF documents. However, how to know when PDF document is rendered?
Here's how my app looks like:
Now, I have a ProgressView which shows while the document is being downloaded. How do I hide the ProgressView when the document is downloaded and begins to render? I'm doing all of this in SwiftUI, I connected the PDFKit using UIKit in a SwiftUI app. Now, how do I. do it? I found this, but this applies only to UIKit: How to know when a PDF is rendered using PDFKit
My code:
PDFRepresentedView:
import SwiftUI
import PDFKit
struct PDFRepresentedView: UIViewRepresentable {
typealias UIViewType = PDFView
let url: URL
let singlePage: Bool
init(_ url: URL, singlePage: Bool = false) {
self.url = url
self.singlePage = singlePage
}
func makeUIView(context _: UIViewRepresentableContext<PDFRepresentedView>) -> UIViewType {
let pdfView = PDFView()
pdfView.document = PDFDocument(url: url)
pdfView.autoScales = true
if singlePage {
pdfView.displayMode = .singlePage
}
return pdfView
}
func updateUIView(_ pdfView: UIViewType, context: UIViewRepresentableContext<PDFRepresentedView>) {
pdfView.document = PDFDocument(url: url)
}
}
PDFReaderView:
import SwiftUI
struct PDFReaderView: View {
var url: URL
var body: some View {
ProgressView("pdf.downloading")
}
}
struct PDFReaderView_Previews: PreviewProvider {
static var previews: some View {
PDFReaderView(url: URL(string: "https://isap.sejm.gov.pl/isap.nsf/download.xsp/WDU19600300168/U/D19600168Lj.pdf")!)
}
}
HomeView:
import SwiftUI
struct HomeView: View {
#AppStorage("needsAppOnboarding") private var needsAppOnboarding: Bool = true
var body: some View {
NavigationView {
List(deeds) { deed in
NavigationLink(destination: PDFReaderView(url: deed.url)) {
Text(deed.name)
}
}
.navigationTitle("title")
}
.sheet(isPresented: $needsAppOnboarding) {
OnboardingView()
}
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
Seems like a better option than letting PDFDocument do the loading from the URL would be to load the data yourself. Then, you can respond appropriately to errors, etc.
import PDFKit
import SwiftUI
import Combine
class DataLoader : ObservableObject {
#Published var data : Data?
var cancellable : AnyCancellable?
func loadUrl(url: URL) {
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.receive(on: RunLoop.main)
.sink(receiveCompletion: { (completion) in
switch completion {
case .failure(let failureType):
print(failureType)
//handle potential errors here
case .finished:
break
}
}, receiveValue: { (data) in
self.data = data
})
}
}
struct ContentView : View {
#StateObject private var dataLoader = DataLoader()
var body: some View {
VStack {
if let data = dataLoader.data {
PDFRepresentedView(data: data)
} else {
Text("Loading")
}
}.onAppear {
dataLoader.loadUrl(url: URL(string: "https://isap.sejm.gov.pl/isap.nsf/download.xsp/WDU19600300168/U/D19600168Lj.pdf")!)
}
}
}
struct PDFRepresentedView: UIViewRepresentable {
typealias UIViewType = PDFView
let data: Data
let singlePage: Bool = false
func makeUIView(context _: UIViewRepresentableContext<PDFRepresentedView>) -> UIViewType {
let pdfView = PDFView()
pdfView.document = PDFDocument(data: data)
pdfView.autoScales = true
if singlePage {
pdfView.displayMode = .singlePage
}
return pdfView
}
func updateUIView(_ pdfView: UIViewType, context: UIViewRepresentableContext<PDFRepresentedView>) {
pdfView.document = PDFDocument(data: data)
}
}
In this example, DataLoader is responsible for getting the Data from the URL. Note the comment I've left in about where you might respond to errors.
Back in the main view, "Loading" is displayed unless there's Data available, in which case PDFRepresentedView is now shown, which now takes a Data object instead of a URL.

SwiftUI Network Image show different views on loading and error

I would like to make sure that when the image is being downloaded the ProgressView is visible and if the url being passed is invalid (empty), a placeholder is put.
How can I do this?
Code:
import Foundation
import SwiftUI
// Download image from URL
struct NetworkImage: View {
public let url: URL?
var body: some View {
Group {
if let url = url, let imageData = try? Data(contentsOf: url),
let uiImage = UIImage(data: imageData) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
}
else {
ProgressView()
}
}
}
}
struct NetworkImage_Previews: PreviewProvider {
static var previews: some View {
let url = [
"",
"http://google.com",
"https://yt3.ggpht.com/a/AATXAJxAbUTyYnKsycQjZzCdL_gWVbJYVy4mVaVGQ8kRMQ=s176-c-k-c0x00ffffff-no-rj"
]
NetworkImage(url: URL(string: url[0])!)
}
}
You can create a ViewModel to handle the downloading logic:
extension NetworkImage {
class ViewModel: ObservableObject {
#Published var imageData: Data?
#Published var isLoading = false
private var cancellables = Set<AnyCancellable>()
func loadImage(from url: URL?) {
isLoading = true
guard let url = url else {
isLoading = false
return
}
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.sink { [weak self] in
self?.imageData = $0
self?.isLoading = false
}
.store(in: &cancellables)
}
}
}
And modify your NetworkImage to display a placeholder image as well:
struct NetworkImage: View {
#StateObject private var viewModel = ViewModel()
let url: URL?
var body: some View {
Group {
if let data = viewModel.imageData, let uiImage = UIImage(data: data) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
} else if viewModel.isLoading {
ProgressView()
} else {
Image(systemName: "photo")
}
}
.onAppear {
viewModel.loadImage(from: url)
}
}
}
Then you can use it like:
NetworkImage(url: URL(string: "https://stackoverflow.design/assets/img/logos/so/logo-stackoverflow.png"))
(Note that the url parameter is not force unwrapped).

Updating image view in SwiftUI after downloading image

On the click of a button I am trying to download a new random image and update the view. When the app loads it displays the downloaded image. When the button is clicked the image seems to download but the view is never updated and displays the place holder image. Am I missing something here, any ideas? Here is a simplified version.
import SwiftUI
struct ContentView : View {
#State var url = "https://robohash.org/random.png"
var body: some View {
VStack {
Button(action: {
self.url = "https://robohash.org/\(Int.random(in:0 ..< 10)).png"
}) {
Text("Get Random Robot Image")
}
URLImage(url: url)
}
}
}
class ImageLoader: BindableObject {
var downloadedImage: UIImage?
let didChange = PassthroughSubject<ImageLoader?, Never>()
func load(url: String) {
guard let imageUrl = URL(string: url) else {
fatalError("Image URL is not correct")
}
URLSession.shared.dataTask(with: imageUrl) { data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
self.didChange.send(nil)
}
return
}
self.downloadedImage = UIImage(data: data)
DispatchQueue.main.async {
print("downloaded image")
self.didChange.send(self)
}
}.resume()
}
}
import SwiftUI
struct URLImage : View {
#ObjectBinding private var imageLoader = ImageLoader()
var placeholder: Image
init(url: String, placeholder: Image = Image(systemName: "photo")) {
self.placeholder = placeholder
self.imageLoader.load(url: url)
}
var body: some View {
if let uiImage = self.imageLoader.downloadedImage {
print("return downloaded image")
return Image(uiImage: uiImage)
} else {
return placeholder
}
}
}
The problem seems to be related to some kind of lost synchronization between the ContentView and the ImageURL (that happens after the button click event).
A possible workaround is making the ImageURL a #State property of the ContentView.
After that, inside the scope of the button click event, we can call the image.imageLoader.load(url: ) method. As the download of the image ends, the publisher (didChange) will notify the ImageURL and then the change is correctly propagated to the ContentView.
import SwiftUI
import Combine
enum ImageURLError: Error {
case dataIsNotAnImage
}
class ImageLoader: BindableObject {
/*
init(url: URL) {
self.url = url
}
private let url: URL */
let id: String = UUID().uuidString
var didChange = PassthroughSubject<Void, Never>()
var image: UIImage? {
didSet {
DispatchQueue.main.async {
self.didChange.send()
}
}
}
func load(url: URL) {
print(#function)
self.image = nil
URLSession.shared.dataTask(with: url) { (data, res, error) in
guard error == nil else {
return
}
guard
let data = data,
let image = UIImage(data: data)
else {
return
}
self.image = image
}.resume()
}
}
URLImage view:
struct URLImage : View {
init() {
self.placeholder = Image(systemName: "photo")
self.imageLoader = ImageLoader()
}
#ObjectBinding var imageLoader: ImageLoader
var placeholder: Image
var body: some View {
imageLoader.image == nil ?
placeholder : Image(uiImage: imageLoader.image!)
}
}
ContentView:
struct ContentView : View {
#State var url: String = "https://robohash.org/random.png"
#State var image: URLImage = URLImage()
var body: some View {
VStack {
Button(action: {
self.url = "https://robohash.org/\(Int.random(in: 0 ..< 10)).png"
self.image.imageLoader.load(url: URL(string: self.url)!)
}) {
Text("Get Random Robot Image")
}
image
}
}
}
Anyway I will try to investigate the problem and if I will know something new I will modify my answer.