Is there any way where we can get the current page number in PDFView and use it in SwiftUI - swift

I am making an app pdf reader using PDFKit but I am unable to get the current page number. I can get the total pages by pdfView.document?.pageCount. The alternative way we can use for this is to change the page by button and count it but I want the PDFView default feature Changing the page by Swipe by sitting the pdfView.usePageViewController(true) but it does not give any method to get the current page number
Code
struct ContentView: View {
let url = Bundle.main.url(forResource: "file", withExtension: "pdf")
var body: some View {
VStack{
PDFKitRepresentedView(data: try! Data(contentsOf: url!))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import PDFKit
import SwiftUI
struct PDFKitRepresentedView: UIViewRepresentable {
typealias UIViewType = PDFView
let data: Data
func makeUIView(context _: UIViewRepresentableContext<PDFKitRepresentedView>) -> UIViewType {
// Create a `PDFView` and set its `PDFDocument`.
let pdfView = PDFView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
pdfView.document = PDFDocument(data: data)
pdfView.backgroundColor = UIColor.red
pdfView.displayMode = .singlePage
pdfView.displayDirection = .horizontal
pdfView.usePageViewController(true)
pdfView.maxScaleFactor = pdfView.scaleFactorForSizeToFit
pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit
pdfView.autoScales = true
return pdfView
}
func updateUIView(_ pdfView: UIViewType, context _: UIViewRepresentableContext<PDFKitRepresentedView>) {
pdfView.document = PDFDocument(data: data)
}
}
Update
According to suggestion given by
workingdog support Ukraine below the coordinator class printing the result but when I use Binding to pass the currentPage to SwiftUI its not working the page number is not updating in UI and on swiping its repeating the first two pages only
New Updated code
struct ContentView: View {
#State var currentPage = -1
#State var totalPages :Int?
let url = Bundle.main.url(forResource: "file", withExtension: "pdf")
var body: some View {
VStack{
HStack{
Text("\(currentPage)/")
Text("\(totalPages ?? 0)")
}
if let url = url {
PDFKitRepresentedView(data:try! Data(contentsOf: url),totalPages: $totalPages,currentPage: $currentPage)
}
}
}
}
struct PDFKitRepresentedView: UIViewRepresentable {
typealias UIViewType = PDFView
let data: Data
#Binding var totalPages:Int?
#Binding var currentPage :Int
let pdfView = PDFView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
func makeUIView(context: Context) -> UIViewType {
pdfView.document = PDFDocument(data: data)
pdfView.backgroundColor = UIColor.red
pdfView.displayMode = .singlePage
pdfView.displayDirection = .horizontal
pdfView.usePageViewController(true)
pdfView.maxScaleFactor = pdfView.scaleFactorForSizeToFit
pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit
pdfView.autoScales = true
pdfView.delegate = context.coordinator
return pdfView
}
func updateUIView(_ pdfView: UIViewType, context _: Context) {
pdfView.document = PDFDocument(data: data)
DispatchQueue.main.async {
totalPages = pdfView.document?.pageCount
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self, cp: $currentPage)
}
class Coordinator: NSObject, PDFViewDelegate {
var parent: PDFKitRepresentedView
#Binding var currentPage :Int
init(_ parent: PDFKitRepresentedView,cp:Binding<Int>) {
self.parent = parent
_currentPage = cp
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(pageChangeHandler(_:)), name: .PDFViewPageChanged, object: nil)
}
#objc func pageChangeHandler(_ notification: Notification) {
if let thePage = parent.pdfView.currentPage,
let ndx = parent.pdfView.document?.index(for: thePage),
currentPage != ndx {
DispatchQueue.main.async {
self.currentPage = ndx
}
print("--------> currentPageIndex: \(ndx) ")
}
}
}
}

According to the docs at: https://developer.apple.com/documentation/pdfkit/pdfview there is a currentPage that returns the current page. You could then use something like this to get the index of it:
if let thePage = pdfView.currentPage, let ndx = pdfView.document?.index(for: thePage) {
print("--> currentPageIndex: \(ndx) ")
// ....
}
EDIT-1:
Try the following approach, using a Coordinator class for the PDFViewDelegate and getting notified when a .PDFViewPageChanged with a NotificationCenter.default.addObserver(...).
You will have to adjust the code for your own purpose.
struct PDFKitRepresentedView: UIViewRepresentable {
typealias UIViewType = PDFView
let data: Data
let pdfView = PDFView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
func makeUIView(context: Context) -> UIViewType {
pdfView.document = PDFDocument(data: data)
pdfView.backgroundColor = UIColor.red
pdfView.displayMode = .singlePage
pdfView.displayDirection = .horizontal
pdfView.usePageViewController(true)
pdfView.maxScaleFactor = pdfView.scaleFactorForSizeToFit
pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit
pdfView.autoScales = true
pdfView.delegate = context.coordinator
return pdfView
}
func updateUIView(_ pdfView: UIViewType, context _: Context) {
pdfView.document = PDFDocument(data: data)
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, PDFViewDelegate {
var parent: PDFKitRepresentedView
var prevPage = -1
init(_ parent: PDFKitRepresentedView) {
self.parent = parent
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(pageChangeHandler(_:)), name: .PDFViewPageChanged, object: nil)
}
#objc func pageChangeHandler(_ notification: Notification) {
if let thePage = parent.pdfView.currentPage,
let ndx = parent.pdfView.document?.index(for: thePage),
prevPage != ndx {
print("--------> currentPageIndex: \(ndx) ")
prevPage = ndx
}
}
}
}
EDIT-2:
To access the currentPage in ContentView, that is, outside the PDFViewer,
you can use the following approach.
It uses a .onReceive(...) of a page change notification, and some minor changes of
the original code.
struct ContentView: View {
#State var currentPage = 0
let pdfViewer: PDFViewer // <--- here
init() {
if let url = Bundle.main.url(forResource: "file", withExtension: "pdf"),
let docData = try? Data(contentsOf: url) {
self.pdfViewer = PDFViewer(data: docData)
} else {
self.pdfViewer = PDFViewer(data: Data())
}
}
var body: some View {
VStack {
Text("page \(currentPage)")
pdfViewer
.onReceive(NotificationCenter.default.publisher(for: .PDFViewPageChanged)) { _ in
if let thePage = pdfViewer.pdfView.currentPage,
let ndx = pdfViewer.pdfView.document?.index(for: thePage), currentPage != ndx {
currentPage = ndx
}
}
}
}
}
struct PDFViewer: UIViewRepresentable {
typealias UIViewType = PDFView
let data: Data
let pdfView = PDFView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
func makeUIView(context: Context) -> UIViewType {
pdfView.document = PDFDocument(data: data)
pdfView.backgroundColor = UIColor.red
pdfView.displayMode = .singlePage
pdfView.displayDirection = .horizontal
pdfView.usePageViewController(true)
pdfView.maxScaleFactor = pdfView.scaleFactorForSizeToFit
pdfView.minScaleFactor = pdfView.scaleFactorForSizeToFit
pdfView.autoScales = true
return pdfView
}
func updateUIView(_ pdfView: UIViewType, context _: Context) {
pdfView.document = PDFDocument(data: data)
}
}

Related

How to use Photo library in SwiftUI

Xcode ver is 14.2.
Target iOS for 16.0.
I want to upload a image from photo library using PHPickerConfiguration().
It can't appear in the UserForm view.
struct UserForm: View {
#State var profileImage: Image?
#Binding var selectedImage: UIImage?
var body: some View {
if profileImage == nil{
Button(action:{
isShowAction.toggle()
}) {
Image(systemName: "person.crop.rectangle.badge.plus.fill")
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.foregroundColor(.gray)
.padding(.vertical)
}
}else if let image = profileImage{
image
.resizable()
.scaledToFill()
.frame(width: 100,height: 100)
.clipShape(Circle())
.clipped()
.foregroundColor(.gray)
}
.sheet(isPresented: $imagePickerPresented, onDismiss: loadImage, content:{
AlbumPicker(image: $selectedImage)
}
)
}
}
extension UserForm{
func loadImage(){
guard let selectedImage = selectedImage else{ return }
profileImage = Image(uiImage: selectedImage)
}
}
import SwiftUI
import PhotosUI
struct AlbumPicker: UIViewControllerRepresentable {
#Binding var image: UIImage?
#Environment(\.presentationMode) var mode
func makeUIViewController(context: UIViewControllerRepresentableContext<AlbumPicker>) -> PHPickerViewController {
var configuration = PHPickerConfiguration()
configuration.filter = .images
configuration.selectionLimit = 1
configuration.preferredAssetRepresentationMode = .current
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = context.coordinator
return picker
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: UIViewControllerRepresentableContext<AlbumPicker>) {}
class Coordinator: NSObject, UINavigationControllerDelegate, PHPickerViewControllerDelegate {
var parent: AlbumPicker
init(_ parent: AlbumPicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
guard let itemProvider = results.first?.itemProvider else {
return
}
let typeChecked = itemProvider.registeredTypeIdentifiers.map { itemProvider.hasItemConformingToTypeIdentifier($0) }
guard !typeChecked.contains(false) else {
return
}
itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier) { (url, error) in
guard let url = url else {
return
}
guard let imageData = try? Data(contentsOf: url) else {
return
}
self.parent.image = UIImage(data: imageData)
self.parent.mode.wrappedValue.dismiss()
}
}
}
}
When I push the button, immediately I got findWriterForTypeAndAlternateType:119: unsupported file format 'public.heic' using actual device.
And it can't hold profileImage in the view after I chose a image in photo library.
How does it be fixed?
Thank you

How to use NSOpenPanel to select a file for QLPreviewView?

Thanks to this answer i can see QuickLook preview of a pdf embedded in my swiftui view.
But it's only to preview the pdf that's stored in the app bundle resource. How do i use NSOpenPanel to choose a file and display it in the QLPreviewView?
import SwiftUI
import AppKit
import Quartz
func loadPreviewItem(with name: String) -> NSURL {
let file = name.components(separatedBy: ".")
let path = Bundle.main.path(forResource: file.first!, ofType: file.last!)
let url = NSURL(fileURLWithPath: path ?? "")
print(url)
return url
}
struct MyPreview: NSViewRepresentable {
var fileName: String
func makeNSView(context: NSViewRepresentableContext<MyPreview>) -> QLPreviewView {
let preview = QLPreviewView(frame: .zero, style: .normal)
preview?.autostarts = true
preview?.previewItem = loadPreviewItem(with: fileName) as QLPreviewItem
return preview ?? QLPreviewView()
}
func updateNSView(_ nsView: QLPreviewView, context: NSViewRepresentableContext<MyPreview>) {
}
typealias NSViewType = QLPreviewView
}
struct ContentView: View {
var body: some View {
// example.pdf is expected in app bundle resources
VStack {
MyPreview(fileName: "testing.pdf")
Divider()
}
Button("Select PDF") {
let openPanel = NSOpenPanel()
openPanel.allowedFileTypes = ["pdf"]
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = false
openPanel.canChooseFiles = true
openPanel.runModal()
}
}
}
*UPDATE
This is what i've tried but nothing happens after i choose a pdf file with the openpanel. The View is blank. I think there's something that i haven't done correctly in the updateNSView.
struct ContentView: View {
#State var filename = ""
var body: some View {
VStack {
MyPreview(fileName: filename)
Divider()
}
Button("Select PDF") {
let openPanel = NSOpenPanel()
openPanel.allowedFileTypes = ["pdf"]
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = false
openPanel.canChooseFiles = true
openPanel.runModal()
print(openPanel.url!.lastPathComponent)
filename = openPanel.url!.lastPathComponent
//
}
}
}
struct MyPreview: NSViewRepresentable {
var fileName: String
func makeNSView(context: NSViewRepresentableContext<MyPreview>) -> QLPreviewView {
let preview = QLPreviewView(frame: .zero, style: .normal)
preview?.autostarts = true
preview?.previewItem = loadPreviewItem(with: fileName) as QLPreviewItem
return preview ?? QLPreviewView()
}
func updateNSView(_ nsView: QLPreviewView, context: NSViewRepresentableContext<MyPreview>) {
let preview = QLPreviewView(frame: .zero, style: .normal)
preview?.refreshPreviewItem()
}
typealias NSViewType = QLPreviewView
}
*UPDATE 2
My latest attempt using now a model to update the file url after choosing one with the open panel but nothing still happens.
It updates pdfurl successfully with the file url but the QLPreviewView doesn't update with the changes in updateNSView. I'm using the refreshItemPreview() which should work but i'm not sure what i'm doing wrong here
class PDFViewModel: ObservableObject {
#Published var pdfurl = ""
}
struct MyPreview: NSViewRepresentable {
#ObservedObject var pdfVM = PDFViewModel()
func makeNSView(context: NSViewRepresentableContext<MyPreview>) -> QLPreviewView {
let preview = QLPreviewView(frame: .zero, style: .normal)
preview?.previewItem = NSURL(string: pdfVM.pdfurl)
return preview ?? QLPreviewView()
}
func updateNSView(_ nsView: QLPreviewView, context: NSViewRepresentableContext<MyPreview>) {
let preview = QLPreviewView(frame: .zero, style: .normal)
preview?.refreshPreviewItem()
}
typealias NSViewType = QLPreviewView
}
struct ContentView: View {
#ObservedObject var pdfVM = PDFViewModel()
var body: some View {
VStack {
MyPreview()
Divider()
}
Button("Select PDF") {
let openPanel = NSOpenPanel()
openPanel.allowedFileTypes = ["pdf"]
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = false
openPanel.canChooseFiles = true
openPanel.runModal()
pdfVM.pdfurl = "\(openPanel.url!)"
print("the url is now \(pdfVM.pdfurl)")
}
}
}
The answer is quick direct. NSOpenPanel is subclassed from NSSavePanel. You can find want you need there.
let response = openPanel.runModal()
if response == .OK, let fileURL = openPanel.url {
// read your file here
}
Problem
It looks like you are not getting the file correctly - currently, you are only getting the last part of the URL. You should get the full URL if you are not loading from Bundle.main.
Solution
For example, this function will get you a full URL from an item.
func openPanelURL() -> URL?{
let openPanel = NSOpenPanel()
openPanel.allowedFileTypes = ["pdf"]
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = false
openPanel.canChooseFiles = true
openPanel.runModal()
return response == .OK ? openPanel.url : nil
}
Source: https://serialcoder.dev/text-tutorials/macos-tutorials/save-and-open-panels-in-swiftui-based-macos-apps/
Then, you can just pass in your PreviewItemURL into the NSViewRepresentable, like so.
struct MyPreview: NSViewRepresentable {
typealias NSViewType = QLPreviewView
var fileName: String
func makeNSView(context: NSViewRepresentableContext<MyPreview>) -> QLPreviewView {
let preview = QLPreviewView(frame: .zero, style: .normal)
preview?.autostarts = true
preview?.previewItemURL = NSURL(string: filename)!
// WARNING! I am force unwrapping above using the !, this is unsafe for a production app
//As it can cause the app to crash. Instead, optionally unwrap using the ? and ?? operators
return preview ?? QLPreviewView()
}
func updateNSView(_ nsView: QLPreviewView, context: NSViewRepresentableContext<MyPreview>) {
let preview = QLPreviewView(frame: .zero, style: .normal)
preview?.refreshPreviewItem()
}
}
Docs
Docs for the NSURL init: https://developer.apple.com/documentation/foundation/nsurl/1414650-fileurl
And for previewItemURL: https://developer.apple.com/documentation/quicklook/qlpreviewitem/1419913-previewitemurl
Warning
I am force unwrapping in the code above - this will crash if you have a nil or incorrect value. In a production app, I would suggest using optional unwrapping. See an article from Hacking With Swift here: https://www.hackingwithswift.com/quick-start/understanding-swift/when-should-you-force-unwrap-optionals-in-swift.
Hacky solution but it works....
I used a looped timer that monitors and sets the preview item url which is also now stored with AppStorage
class PDFViewModel: ObservableObject {
#AppStorage("pdfurl") var pdfurl: String = ""
}
struct MyPreview: NSViewRepresentable {
#ObservedObject var pdfVM = PDFViewModel()
let preview = QLPreviewView(frame: .zero, style: .normal)
func makeNSView(context: NSViewRepresentableContext<MyPreview>) -> QLPreviewView {
preview?.autostarts = true
preview?.shouldCloseWithWindow = false
preview?.previewItem = NSURL(string: pdfVM.pdfurl)
updatedocument()
return preview ?? QLPreviewView()
}
func updatedocument() {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
let delay = DispatchTime.now() + 0.3
DispatchQueue.main.asyncAfter(deadline: delay, execute: {
preview?.previewItem = NSURL(string: pdfVM.pdfurl)
})
}
}
func updateNSView(_ nsView: QLPreviewView, context: NSViewRepresentableContext<MyPreview>) {
}
typealias NSViewType = QLPreviewView
}
struct ContentView: View {
#ObservedObject var pdfVM = PDFViewModel()
var body: some View {
VStack {
MyPreview()
Divider()
}
Button("Select Document") {
let openPanel = NSOpenPanel()
openPanel.allowedFileTypes = ["pdf", "jpg","doc","pptx", "png", "xls", "xlsx", "docx", "jpeg", "txt"]
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = false
openPanel.canChooseFiles = true
openPanel.begin { (result) in
if result == NSApplication.ModalResponse.OK {
if let url = openPanel.url {
pdfVM.pdfurl = "\(url)"
}
} else if result == NSApplication.ModalResponse.cancel {
}
}
}
}
}

SwiftUI, Core Data, and PhotoKit: Views not updating when state changes (state management hell)

I'm learning Swift/SwiftUI by building a photo organizer app. It displays a user's photo library in a grid like the built-in photos app, and there's a detail view where you can do things like favorite a photo or add it to the trash.
My app loads all the data and displays it fine, but the UI doesn't update when things change. I've debugged enough to confirm that my edits are applied to the underlying PHAssets and Core Data assets. It feels like the problem is that my views aren't re-rendering.
I used Dave DeLong's approach to create an abstraction layer that separates Core Data from SwiftUI. I have a singleton environment object called DataStore that handles all interaction with Core Data and the PHPhotoLibrary. When the app runs, the DataStore is created. It makes an AssetFetcher that grabs all assets from the photo library (and implements PHPhotoLibraryChangeObserver). DataStore iterates over the assets to create an index in Core Data. My views' viewmodels query core data for the index items and display them using the #Query property wrapper from Dave's post.
App.swift
#main
struct LbPhotos2App: App {
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.dataStore, DataStore.shared)
}
}
}
PhotoGridView.swift (this is what contentview presents)
struct PhotoGridView: View {
#Environment(\.dataStore) private var dataStore : DataStore
#Query(.all) var indexAssets: QueryResults<IndexAsset>
#StateObject var vm = PhotoGridViewModel()
func updateVm() {
vm.createIndex(indexAssets)
}
var body: some View {
GeometryReader { geo in
VStack {
HStack {
Text("\(indexAssets.count) assets")
Spacer()
TrashView()
}.padding(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16))
ScrollView {
ForEach(vm.sortedKeys, id: \.self) { key in
let indexAssets = vm.index[key]
let date = indexAssets?.first?.creationDate
GridSectionView(titleDate:date, indexAssets:indexAssets!, geoSize: geo.size)
}
}.onTapGesture {
updateVm()
}
}.onAppear {
updateVm()
}
.navigationDestination(for: IndexAsset.self) { indexAsset in
AssetDetailView(indexAsset: indexAsset)
}
}
}
}
PhotoGridViewModel.swift
class PhotoGridViewModel: ObservableObject {
#Published var index: [String:[IndexAsset]] = [:]
var indexAssets: QueryResults<IndexAsset>?
func createIndex() {
guard let assets = self.indexAssets else {return}
self.createIndex(assets)
}
func createIndex(_ queryResults: QueryResults<IndexAsset>) {
indexAssets = queryResults
if queryResults.count > 0 {
var lastDate = Date.distantFuture
for i in 0..<queryResults.count {
let item = queryResults[i]
let isSameDay = isSameDay(firstDate: lastDate, secondDate: item.creationDate!)
if isSameDay {
self.index[item.creationDateKey!]?.append(item)
} else {
self.index[item.creationDateKey!] = [item]
}
lastDate = item.creationDate!
}
}
self.objectWillChange.send()
}
var sortedKeys: [String] {
return index.keys.sorted().reversed()
}
private func isSameDay(firstDate:Date, secondDate:Date) -> Bool {
return Calendar.current.isDate(
firstDate,
equalTo: secondDate,
toGranularity: .day
)
}
}
Here's where I actually display the asset in GridSectionView.swift
LazyVGrid(columns: gridLayout, spacing: 2) {
let size = geoSize.width/4
ForEach(indexAssets, id:\.self) { indexAsset in
NavigationLink(
value: indexAsset,
label: {
AssetCellView(indexAsset: indexAsset, geoSize:geoSize)
}
).frame(width: size, height: size)
.buttonStyle(.borderless)
}
}
AssetCellView.swift
struct AssetCellView: View {
#StateObject var vm : AssetCellViewModel
var indexAsset : IndexAsset
var geoSize : CGSize
init(indexAsset: IndexAsset, geoSize: CGSize) {
self.indexAsset = indexAsset
self.geoSize = geoSize
_vm = StateObject(wrappedValue: AssetCellViewModel(indexAsset: indexAsset, geoSize: geoSize))
}
var body: some View {
ZStack(alignment: .bottomTrailing) {
if (vm.indexAsset != nil && vm.image != nil) {
vm.image?
.resizable()
.aspectRatio(contentMode: .fit)
.border(.blue, width: vm.indexAsset!.isSelected ? 4 : 0)
}
if (vm.indexAsset != nil && vm.indexAsset!.isFavorite) {
Image(systemName:"heart.fill")
.resizable()
.frame(width: 20, height: 20)
.foregroundStyle(.ultraThickMaterial)
.shadow(color: .black, radius: 12)
.offset(x:-8, y:-8)
}
}
}
}
AssetCellViewModel.swift
class AssetCellViewModel: ObservableObject{
#Environment(\.dataStore) private var dataStore
#Published var image : Image?
var indexAsset : IndexAsset?
var geoSize : CGSize
init(indexAsset: IndexAsset? = nil, geoSize:CGSize) {
self.indexAsset = indexAsset
self.geoSize = geoSize
self.requestImage(targetSize: CGSize(width: geoSize.width/4, height: geoSize.width/4))
}
func setIndexAsset(_ indexAsset:IndexAsset, targetSize: CGSize) {
self.indexAsset = indexAsset
self.requestImage(targetSize: targetSize)
}
func requestImage(targetSize: CGSize? = nil) {
if (self.indexAsset != nil) {
dataStore.fetchImageForLocalIdentifier(
id: indexAsset!.localIdentifier!,
targetSize: targetSize,
completionHandler: { image in
withAnimation(Animation.easeInOut (duration:0.15)) {
self.image = image
}
}
)
}
}
}
some of DataStore.swift
public class DataStore : ObservableObject {
static let shared = DataStore()
let persistenceController = PersistenceController.shared
#ObservedObject var assetFetcher = AssetFetcher()
let dateFormatter = DateFormatter()
var imageManager = PHCachingImageManager()
let id = UUID().uuidString
init() {
print("🔶 init dataStore: \(self.id)")
dateFormatter.dateFormat = "yyyy-MM-dd"
assetFetcher.iterateResults{ asset in
do {
try self.registerAsset(
localIdentifier: asset.localIdentifier,
creationDate: asset.creationDate!,
isFavorite: asset.isFavorite
)
} catch {
print("Error registering asset \(asset)")
}
}
}
func registerAsset(localIdentifier:String, creationDate:Date, isFavorite:Bool) throws {
let alreadyExists = indexAssetEntityWithLocalIdentifier(localIdentifier)
if alreadyExists != nil {
// print("🔶 Asset already registered: \(localIdentifier)")
// print(alreadyExists![0])
return
}
let iae = IndexAssetEntity(context: self.viewContext)
iae.localIdentifier = localIdentifier
iae.creationDate = creationDate
iae.creationDateKey = dateFormatter.string(from: creationDate)
iae.isFavorite = isFavorite
iae.isSelected = false
iae.isTrashed = false
self.viewContext.insert(iae)
try self.viewContext.save()
print("🔶 Registered asset: \(localIdentifier)")
}
And AssetFetcher.swift
class AssetFetcher:NSObject, PHPhotoLibraryChangeObserver, ObservableObject {
#Published var fetchResults : PHFetchResult<PHAsset>? = nil
let id = UUID().uuidString
override init() {
super.init()
print("🔶 init assetfetcher: \(id)")
self.startFetchingAllPhotos()
}
deinit {
PHPhotoLibrary.shared().unregisterChangeObserver(self)
}
func startFetchingAllPhotos() {
getPermissionIfNecessary(completionHandler: {result in
print(result)
})
let fetchOptions = PHFetchOptions()
var datecomponents = DateComponents()
datecomponents.month = -3
//TODO: request assets dynamically
let threeMonthsAgo = Calendar.current.date(byAdding: datecomponents, to:Date())
fetchOptions.predicate = NSPredicate(format: "creationDate > %# AND creationDate < %#", threeMonthsAgo! as NSDate, Date() as NSDate)
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
fetchOptions.wantsIncrementalChangeDetails = true
// fetchOptions.fetchLimit = 1000
let results = PHAsset.fetchAssets(with: .image, options: fetchOptions)
PHPhotoLibrary.shared().register(self)
print("🔶 \(PHPhotoLibrary.shared())")
self.fetchResults = results
}
func iterateResults(_ callback:(_ asset: PHAsset) -> Void) {
print("iterateResults")
guard let unwrapped = self.fetchResults else {
return
}
for i in 0..<unwrapped.count {
callback(unwrapped.object(at: i))
}
}
func photoLibraryDidChange(_ changeInstance: PHChange) {
print("🔶 photoLibraryDidChange")
DispatchQueue.main.async {
if let changeResults = changeInstance.changeDetails(for: self.fetchResults!) {
self.fetchResults = changeResults.fetchResultAfterChanges
// self.dataStore.photoLibraryDidChange(changeInstance)
// self.updateImages()
self.objectWillChange.send()
}
}
}
}

SwiftUI UIViewRepresentable AVPlayer crashing due to "periodTimeObserver"

I have a SwiftUI application which has a carousel of videos. I'm using an AVPlayer with UIViewRepresentable and I'm creating the carousel with a ForEach loop of my custom UIViewRepresentable view. I want to have a "periodicTimeObserver" on the active AVPlayer, but it crashes and says
"An instance of AVPlayer cannot remove a time observer that was added
by a different instance of AVPlayer SwiftUI"
My question is how can I remove the periodicTimeObserver of an AVPlayer inside of a UIViewRepresentable inside of a UIView, without causing the app to crash?
Here is my code:
ForEach(videosArray.indices, id: \.self) { i in
let videoURL = videosArray[i]
ZStack {
VStack {
VideoView.init(viewModel: viewModel, videoURL: URL(string: videoURL)!, videoIndex: i)
}
}
}
struct VideoView: UIViewRepresentable {
#ObservedObject var viewModel = viewModel.init()
var videoURL:URL
var previewLength:Double?
var videoIndex: Int
func makeUIView(context: Context) -> UIView {
return PlayerView.init(frame: .zero, url: videoURL, previewLength: previewLength ?? 6)
}
func updateUIView(_ uiView: UIView, context: Context) {
if videoIndex == viewModel.currentIndexSelected {
if let playerView = uiView as? PlayerView {
if !viewModel.isPlaying {
playerView.pause()
} else {
playerView.play(customStartTime: viewModel.newStartTime, customEndTime: viewModel.newEndTime)
}
}
} else {
if let playerView = uiView as? PlayerView {
playerView.pause()
}
}
}
}
public class ViewModel: ObservableObject {
#Published public var currentIndexSelected: Int = 0
#Published public var isPlaying: Bool = true
#Published public var newStartTime = 0.0
#Published public var newEndTime = 30.0
}
class PlayerView: UIView {
private let playerLayer = AVPlayerLayer()
private var previewTimer:Timer?
var previewLength:Double
var player: AVPlayer?
var timeObserver: Any? = nil
init(frame: CGRect, url: URL, previewLength:Double) {
self.previewLength = previewLength
super.init(frame: frame)
player = AVPlayer(url: url)
player!.volume = 0
player!.play()
playerLayer.player = player
playerLayer.videoGravity = .resizeAspectFill
playerLayer.backgroundColor = UIColor.black.cgColor
layer.addSublayer(playerLayer)
}
required init?(coder: NSCoder) {
self.previewLength = 15
super.init(coder: coder)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
func pause() {
if let timeObserver = timeObserver {
self.player?.removeTimeObserver(timeObserver)
self.timeObserver = nil
}
player?.pause()
}
#objc func replayFinishedItem(noti: NSNotification) {
print("REPLAY FINISHED NOTIIIII: \(noti)")
if let timeDict = noti.object as? [String: Any], let startTime = timeDict["startTime"] as? Double, let endTime = timeDict["endTime"] as? Double/*, let player = timeDict["player"] as? AVPlayer, let observer = timeDict["timeObserver"]*/ {
self.removeTheTimeObserver()
self.play(customStartTime: startTime, customEndTime: endTime)
}
}
#objc func removeTheTimeObserver() {
print("ATTEMPT TO REMOVE IT!")
if let timeObserver = timeObserver {
self.player?.removeTimeObserver(timeObserver)
self.timeObserver = nil
}
}
func play(at playPosition: Double = 0.0, customStartTime: Double = 0.0, customEndTime: Double = 15.0) {
var startTime = customStartTime
var endTime = customEndTime
if customStartTime > customEndTime {
startTime = customEndTime
endTime = customStartTime
}
if playPosition != 0.0 {
player?.seek(to: CMTime(seconds: playPosition, preferredTimescale: CMTimeScale(1)))
} else {
player?.seek(to: CMTime(seconds: startTime, preferredTimescale: CMTimeScale(1)))
}
player?.play()
var timeDict: [String: Any] = ["startTime": startTime, "endTime": endTime]
NotificationCenter.default.addObserver(self, selector: #selector(self.replayFinishedItem(noti:)), name: .customAVPlayerShouldReplayNotification, object: nil)
self.timeObserver = self.player?.addPeriodicTimeObserver(forInterval: CMTime.init(value: 1, timescale: 100), queue: DispatchQueue.main, using: { [weak self] time in
guard let strongSelf = self else {
return
}
let currentTime = CMTimeGetSeconds(strongSelf.player!.currentTime())
let currentTimeStr = String(currentTime)
if let currentTimeDouble = Double(currentTimeStr) {
let userDefaults = UserDefaults.standard
userDefaults.set(currentTimeDouble, forKey: "currentTimeDouble")
NotificationCenter.default.post(name: .currentTimeDouble, object: currentTimeDouble)
if currentTimeDouble >= endTime {
if let timeObserver = strongSelf.timeObserver {
strongSelf.player?.removeTimeObserver(timeObserver)
strongSelf.timeObserver = nil
}
strongSelf.player?.pause()
NotificationCenter.default.post(name: .customAVPlayerShouldReplayNotification, object: timeDict)
} else if let currentItem = strongSelf.player?.currentItem {
let seconds = currentItem.duration.seconds
if currentTimeDouble >= seconds {
if let timeObserver = strongSelf.timeObserver {
strongSelf.player?.removeTimeObserver(timeObserver)
strongSelf.timeObserver = nil
}
NotificationCenter.default.post(name: .customAVPlayerShouldReplayNotification, object: timeDict)
}
}
}
})
}
}

ARSession CurrentFrame is missing the AR interpretation Model Entities

I have the following ARView:
import SwiftUI
import UIKit
import RealityKit
import ARKit
struct ARViewContainer: UIViewRepresentable {
#EnvironmentObject var selectedFood: SelectedFood
#EnvironmentObject var arSession: ARSessionObservable
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
let config = ARWorldTrackingConfiguration()
config.planeDetection = [.vertical, .horizontal]
config.environmentTexturing = .automatic
if ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh) {
config.sceneReconstruction = .mesh
}
arView.session.delegate = context.coordinator
arView.session.run(config)
arSession.session = arView.session
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {
if (!selectedFood.food.image.isEmpty) {
let data = try! Data(contentsOf: URL(string: self.selectedFood.food.image)!)
let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try! data.write(to: fileURL)
do {
let texture = try TextureResource.load(contentsOf: fileURL)
var material = SimpleMaterial()
material.baseColor = MaterialColorParameter.texture(texture)
material.tintColor = UIColor.white.withAlphaComponent(0.99)
let entity = ModelEntity(mesh: .generatePlane(width: 0.1, height: 0.1), materials: [material])
let anchor = AnchorEntity(.plane(.any, classification: .any, minimumBounds: .zero))
anchor.addChild(entity)
uiView.scene.addAnchor(anchor)
} catch {
print(error.localizedDescription)
}
}
}
class Coordinator: NSObject, ARSessionDelegate, ARSCNViewDelegate {
var arVC: ARViewContainer
init(_ arViewContainer: ARViewContainer) {
self.arVC = arViewContainer
}
func session(_ session: ARSession, didUpdate frame: ARFrame) {
}
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
}
}
}
And in HomeView i have the following two variables:
#StateObject var arSession: ARSessionObservable = ARSessionObservable()
#State private var capturedImage: UIImage = UIImage()
The following button with action:
Button {
if let capturedFrame = arSession.session.currentFrame {
let ciimg = CIImage(cvPixelBuffer: capturedFrame.capturedImage)
if let cgImage = convertCIImageToCGImage(inputImage: ciimg) {
capturedImage = UIImage(cgImage: cgImage).rotate(radians: .pi / 2)
self.isShowingMail = true
}
}
} label: {
Image("ShareScreen")
.resizable()
.aspectRatio(contentMode:.fit)
.frame(width: 66, height: 66, alignment: .center)
}
Which takes the currentFrame from the session and opens a Mail sharing model with attachment:
.sheet(isPresented: $isShowingMail) {
MailComposeViewController(toRecipients: [], mailBody: nil, imageAttachment: capturedImage) {
self.isShowingMail = false
}
The mail sharing:
func makeUIViewController(context: UIViewControllerRepresentableContext<MailComposeViewController>) -> MFMailComposeViewController {
let mail = MFMailComposeViewController()
mail.mailComposeDelegate = context.coordinator
mail.setToRecipients(self.toRecipients)
if let body = mailBody {
mail.setMessageBody(body, isHTML: true)
}
if let image = imageAttachment {
if let imageData = image.pngData() {
mail.addAttachmentData(imageData, mimeType: "image/png", fileName: "image.png")
}
}
return mail
}
The problem is that on the preview there are present the Model Entities, photo below:
And when i press share, on the mail preview the model is missing from the frame:
I managed to make it work by moving arView: ARView! outside the ARViewContainer
var arView: ARView!
struct ARViewContainer: UIViewRepresentable {
func makeUIView(context: Context) -> ARView {
arView = ARView(frame: .zero)
let config = ARWorldTrackingConfiguration()
config.planeDetection = [.vertical, .horizontal]
config.environmentTexturing = .automatic
if ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh) {
config.sceneReconstruction = .mesh
}
arView.session.delegate = context.coordinator
arView.session.run(config)
return arView
}
}
And then calling the snapshot function to arView in the other View:
Button {
arView.snapshot(saveToHDR: false) { image in
let image = UIImage(data: (image?.pngData())!)
capturedImage = image!
self.isShowingMail = true
}