Is there a way to pass a ModelEntity to ARQuickLook preview controller? - ios

I'm working on a project where I have to download a USDZ file from a URL, preconfigured with white materials, then customize it in runtime and finally view it in AR with ARQuickLook.
At the moment, I thought the best way was to download the asset using the ModelEntity download method, change its properties and then show it with the ARQuickLook preview.
Currently, I am completely stuck in the last step where I am looking for the way to pass the modified model entity to the ARQuickLook preview controller, but it only accepts a URL and no other data types.
A simple code example below:
var modelURL: URL?
override func viewDidLoad() {
super.viewDidLoad()
self.downloadUSDZ()
}
#IBAction func arQuickLookButtonPressed(_ sender: Any) {
guard modelURL != nil else { return }
let previewController = QLPreviewController()
previewController.dataSource = self
present(previewController, animated: true, completion: nil)
}
func downloadUSDZ() {
modelURL = URL(string: "https://developer.apple.com/augmented-reality/quick-look/models/drummertoy/toy_drummer.usdz")!
guard let entity = try? ModelEntity.loadModel(contentsOf: modelURL!) else {
print("Entity download failed")
return
}
for child in entity.children {
var newMaterial = SimpleMaterial()
newMaterial.color.tint = UIColor.cyan
child.model?.materials = [newMaterial]
}
}
func numberOfPreviewItems(in controller: QLPreviewController) -> Int { return 1 }
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
let previewItem = ARQuickLookPreviewItem(fileAt: modelURL!) //<---- HERE I NEED TO DISPLAY THE MODIFIED MODEL ENTITY
previewItem.canonicalWebPageURL = URL(string: "https://developer.apple.com/augmented-reality/quick-look/models/drummertoy/")
previewItem.allowsContentScaling = false
return previewItem
}
Can anyone give me some advice on how to proceed?
Other ways to reach the goal are also accepted.

I'm not sure if this is doable with ARQuickLook. But we can use either SceneKit or RealityKit ARView and modify the ModelEntity at runtime. You could do something like this, Using ARView in RealityKit:
#IBOutlet var arView: ARView!
override func viewDidLoad() {
super.viewDidLoad()
let modelURL = URL(string: "https://developer.apple.com/augmented-reality/quick-look/models/drummertoy/toy_drummer.usdz")!
guard let entity = try? ModelEntity.loadModel(contentsOf: modelURL!) else {
print("Entity download failed")
return
}
for child in entity.children {
var newMaterial = SimpleMaterial()
newMaterial.color.tint = UIColor.cyan
child.model?.materials = [newMaterial]
}
let anchor = AnchorEntity(plane: .horizontal)
anchor.addChild(entity)
arView.scene.addAnchor(anchor)
}
Please keep in mind that you will have to manually add the transform/scale actions that you get automatically with ARQuickLook.

Related

Delegate is not invoked from DispatchQueue.global() thread

I have a scenario where I need to load data from a JSON object, I've created an helper class that does that and it looks like this
protocol JSONDumpHelperDelegate {
func helper(_: JSONDumpHelper, didFinishFetching: [Link])
func helper(_: JSONDumpHelper, completionProcess: Double)
}
struct JSONDumpHelper {
static let pointsOfInterest = OSLog(subsystem: "com.mattrighetti.Ulry", category: .pointsOfInterest)
var delegate: JSONDumpHelperDelegate?
func loadFromFile(
with filemanager: FileManager = .default,
from url: URL,
decoder: JSONDecoder = JSONDecoder(),
context: NSManagedObjectContext = CoreDataStack.shared.managedContext,
dataFetcher: DataFetcher = DataFetcher()
) {
os_signpost(.begin, log: JSONDumpHelper.pointsOfInterest, name: "loadFromFile")
let data = try! Data(contentsOf: url)
let dump = try! decoder.decode(Dump.self, from: data)
var tagsHash: [UUID:Tag] = [:]
if let tagsCodable = dump.tags {
for tagCodable in tagsCodable {
let tag = Tag(context: context)
tag.id = tagCodable.id
tag.name = tagCodable.name
tag.colorHex = tagCodable.colorHex
tagsHash[tag.id] = tag
}
}
var groupHash: [UUID:Group] = [:]
if let groupsCodable = dump.groups {
for groupCodable in groupsCodable {
let group = Group(context: context)
group.id = groupCodable.id
group.name = groupCodable.name
group.colorHex = groupCodable.colorHex
group.iconName = groupCodable.iconName
groupHash[group.id] = group
}
}
var links: [Link] = []
if let linksCodable = dump.links {
let total = linksCodable.count
var completed = 0.0
delegate?.helper(self, completionProcess: 0.0)
for linkCodable in linksCodable {
let link = Link(context: context)
link.id = linkCodable.id
link.url = linkCodable.url
link.createdAt = linkCodable.createdAt
link.updatedAt = linkCodable.updatedAt
link.colorHex = linkCodable.colorHex
link.note = linkCodable.note
link.starred = linkCodable.starred
link.unread = linkCodable.unread
if let uuidGroup = linkCodable.group?.id {
link.group = groupHash[uuidGroup]
}
if let tags = linkCodable.tags {
link.tags = Set<Tag>()
for tagUUID in tags.map({ $0.id }) {
link.tags?.insert(tagsHash[tagUUID]!)
}
}
links.append(link)
completed += 1
delegate?.helper(self, completionProcess: completed / Double(total))
}
}
os_signpost(.end, log: JSONDumpHelper.pointsOfInterest, name: "loadFromFile")
}
}
This could potentially be a very long running task, just imagine an array with 1k records that also need to fetch data from the internet (not shown in implementation, error still exist with posted code) and you can easily end up with 10s in execution time.
What I'm trying to achieve is to show the user an alert that will show him the progress of the import process, by updating the values with the delegate protocols below.
extension BackupViewController: JSONDumpHelperDelegate {
func helper(_: JSONDumpHelper, didFinishFetching: [Link]) {
DispatchQueue.main.async {
self.completionViewController.remove()
}
}
func helper(_: JSONDumpHelper, completionProcess: Double) {
DispatchQueue.main.async {
self.completionViewController.descriptionLabel.text = "Completed \(Int(completionProcess * 100))%"
}
}
}
The import method is fired from a UITableView, immediately after the user choses a file from a DocumentPickerViewController
extension BackupViewController: UIDocumentPickerDelegate {
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
dismiss(animated: true, completion: nil)
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
self.initImport(for: urls)
}
}
private func initImport(for urls: [URL]) {
if let url = urls.first {
completionViewController.add(self, frame: self.view.bounds)
completionViewController.descriptionLabel.text = "Fetching"
DispatchQueue.global(qos: .userInteractive).async {
self.dumpHelper.loadFromFile(from: url)
}
}
}
The problem I am facing is that when the user initiates the import process, the UI is not updated until the process itself is finished.
If I place breakpoints both at the protocol implementations and at the delegate calls in the helper class I can see that the delegate is not called immediately but they all get fired when the process ended (but the alert controller does not update its values).
Just to place some more info I'm going to replicate an import of N elements from JSON:
User clicks import process
initImport is executed (nothing is shown on UI even if I add the custom vc to view)
JSONDumpHelper.loadFromFile is executed entirely, calling delegate N times, nothing called in the delegate implementation
loadFromFile finishes execution
delegate implementation of helper(_:, completionProcess) is executed N times, UI always shows "Completed 0%"
delegate implementation of helper(_:, didFinishFetching) is executed, controller is removed from view
Can anybody point out what is wrong with this implementation? It seems like the loadFromFile function is not working in a separate Queue and UI is stuck and can't update as expected.

How to make an "Add to favourites" and show in favourite list in Swift 4?

I am building a table view app. There are images on the cells. My question is, how to add the images to the 'favorite"? In the 'favorite' page, I want to see the images on the cell, which is in other view controller. How to make the 'Add to Favorite' button and how the datas are added to the favorite page? Anyone please help me? I will be so grateful if anyone help me with that. I am using Swift 4 and I am new in Swift.
You can do something like this:
#IBAction func isFavoriteButtonTapped(_ sender: UIButton) {
let buttonPosition = sender.convert(CGPoint.zero, to: self.tableView)
guard let indexPath = self.tableView.indexPathForRow(at: buttonPosition) else { return }
guard let shoe_ID = sneakerModel[indexPath.row].ID else { return }
let checkFavorite = CoreDataManager.sharedInstance.loadIsFavorite(shoe_ID: shoe_ID)
if checkFavorite == true {
sender.setImage(UIImage(systemName: "star"), for: .normal)
CoreDataManager.sharedInstance.updateIsFavorite(shoe_ID: shoe_ID, isFavorite: false)
} else if checkFavorite == false {
sender.setImage(UIImage(systemName: "star.fill"), for: .normal)
CoreDataManager.sharedInstance.updateIsFavorite(shoe_ID: shoe_ID, isFavorite: true)
}
}
}
CoreData stuff:
func loadIsFavorite(shoe_ID: String) -> Bool{
let request: NSFetchRequest<Project> = NSFetchRequest< Project >(entityName: "Project")
var favorite = Bool()
do {
let sneaksObject = try context.fetch(request)
for i in 0..<sneaksObject.count {
if shoe_ID == sneaksObject[i].shoe_ID {
favorite = sneaksObject[i].isFavorite
}
}
} catch {
print(error.localizedDescription)
}
return favorite
}
func updateIsFavorite(shoe_ID: String, isFavorite: Bool) {
let request: NSFetchRequest< Project > = NSFetchRequest< Project >(entityName: "Project")
do {
let sneaksObject = try context.fetch(request)
for i in 0..<sneaksObject.count {
if shoe_ID == sneaksObject[i].shoe_ID {
sneaksObject[i].setValue(isFavorite, forKey: "isFavorite")
saveContext()
}
}
saveContext()
} catch {
print(error.localizedDescription)
}
}
Follow steps to achieve this.
Step 1 : Create a 'CoreData' model in your project.
Step 2 : Save the image details in CoreData
Step 3 : Fetch from CoreData and show it in another view controller
Learn more about CoreData

getting names displaying from firebase

I am trying to display the contents of a firebase database. I know that I am reading them correctly as I am able to print them as they are read in. The problem is when I call the method to display them on screen, they are "out of range".
I know this means the the methods are being called simultaneously therefore the array is empty. I have tried the "Sleep()" method and doesn't work.
//arrays of names and descriptions
var Names:[String] = []
var Desctiptions: [String] = []
inital method
override func viewDidLoad() {
super.viewDidLoad()
getRestauraunt()
//create slides
scrollView.delegate = self
slides = createSlides()
setupSlideScrollView(slides: slides)
}
func getRestauraunt(){
let db = Firestore.firestore()
db.collection("Test").getDocuments { (snapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in snapshot!.documents {
let name = document.get("Name") as! String
let description = document.get("Description") as! String
//print("Names: ",name," Description: ",description)
self.Names.append(name)
self.Desctiptions.append(description)
}
}
}
}
create slides method
func createSlides() -> [Slide] {
//firebase link
let slide1:Slide = Bundle.main.loadNibNamed("Slide", owner: self, options: nil)?.first as! Slide
slide1.labelTitle.text = Names[0]
}
I would like if someone could show me how to get the 'createSlides()' method to wait until the 'getRestauraunts()' method has finished. Thank you
Just call it from the end of the getrestaurant()'s getDocuments closure
func getRestauraunt(){
//as before...
} else {
for document in snapshot!.documents {
let name = document.get("Name") as! String
let description = document.get("Description") as! String
self.Names.append(name)
self.Desctiptions.append(description)
}
self.createSlides()
}
}
}
As an aside, it might also be worth creating a simple Document struct with name and description properties, and just having the one array: [Document]

UIDocument not working

I'm trying to open document file by following way but nothing happens.
I tried other ways too but it didn't work for me. What is the correct way open Document file (doc,docx,pdf,ppt, etc.). I read somewhere by UIWebView() also this can be done so what is the better way?
let fileURL = URL(fileURLWithPath: documentMessage.fileUrl)
let doc = DocumentPreview(fileURL)
doc.startPreview()
DocumentPreview.swift
class DocumentPreview : NSObject, UIDocumentInteractionControllerDelegate {
var url : URL?
var document : UIDocumentInteractionController?
var previewVC : UINavigationController?
init(_ url: URL) {
super.init()
self.url = url
document = UIDocumentInteractionController(url:url)
document?.delegate = self
}
func startPreview(){
document?.presentPreview(animated:true)
}
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
return previewVC!
}
func documentInteractionControllerDidEndPreview(_ controller: UIDocumentInteractionController) {
print("end Preview")
previewVC!.popViewController(animated: true)
}
}
Take a look at documentMessage. If the .fileUrl property is a URL, then this code is wrong:
let fileURL = URL(fileURLWithPath: documentMessage.fileUrl)
The simplest fix would be this:
let fileURL = documentMessage.fileUrl

Completion Handler is not working in viewDidLoad?

I'm using this library in my app for banners. I am trying to get the Link by parsing the JSON.
The Images are are not showing in the slideshow view. If I press the slideshow view, after that everything works fine. I thought that there was some issue with my completion handler.
But I can't solve it yet :)
#IBOutlet weak var slideshow: ImageSlideshow!
var transitionDelegate: ZoomAnimatedTransitioningDelegate?
var Banner : [AlamofireSource] = []
override func viewDidLoad() {
super.viewDidLoad()
Banners { (imagesource) in
if imagesource != nil {
self.bannershow()
}
}
}
func Banners(completionHandler: ([AlamofireSource]?) -> ()) -> (){
Alamofire.request(.GET, "http://46.420.116.11/mobileapp/gps/api.php?rquest=get_banners")
.responseJSON{ response in
if let data = response.result.value{
let json = JSON(data)
let count = json["image_path"].count
for index in 0...count-1 {
let image :String = json["image_path"][index].stringValue
let source : AlamofireSource = AlamofireSource(urlString: image)!
self.Banner.append(source)
}
completionHandler(self.Banner)
}
}
}
func bannershow(){
self.slideshow.backgroundColor = UIColor.whiteColor()
self.slideshow.slideshowInterval = 2.0
self.slideshow.contentScaleMode = UIViewContentMode.ScaleToFill
self.slideshow.setImageInputs(self.Banner)
let recognizer = UITapGestureRecognizer(target: self, action: "click")
self.slideshow.addGestureRecognizer(recognizer)
}
func click() {
let ctr = FullScreenSlideshowViewController()
ctr.pageSelected = {(page: Int) in
self.slideshow.setScrollViewPage(page, animated: false)
}
ctr.initialPage = slideshow.scrollViewPage
ctr.inputs = slideshow.images
self.transitionDelegate = ZoomAnimatedTransitioningDelegate(slideshowView: slideshow);
ctr.transitioningDelegate = self.transitionDelegate!
self.presentViewController(ctr, animated: true, completion: nil)
}
You probably have a threading problem. There is no guarantee that the Banners completion handler is called on the main thread. You need to step out to the main thread explicitly before doing anything that touches your properties or (especially) the interface.
I think your problem might be that you're expecting the images to be available immediately but they need to be downloaded before, so they won't be available immediately after your viewDidLoad method finished. That's why you should probably configure your slideshow in the viewDidLoad and not in your bannershow() method. Something like this might be an improvement:
#IBOutlet weak var slideshow: ImageSlideshow!
var bannerImages : [AlamofireSource] = []
override func viewDidLoad() {
super.viewDidLoad()
slideshow.backgroundColor = UIColor.whiteColor()
slideshow.slideshowInterval = 2.0
slideshow.contentScaleMode = UIViewContentMode.ScaleToFill
let recognizer = UITapGestureRecognizer(target: self, action: "click")
slideshow.addGestureRecognizer(recognizer)
getBanners { imagesource in
self.showBanner()
}
}
func getBanners(completionHandler: ([AlamofireSource]?) -> ()) -> (){
Alamofire.request(.GET, "http://46.420.116.11/mobileapp/gps/api.php?rquest=get_banners")
.responseJSON{ response in
if let data = response.result.value{
let json = JSON(data)
let count = json["image_path"].count
for index in 0...count-1 {
let image :String = json["image_path"][index].stringValue
let source : AlamofireSource = AlamofireSource(urlString: image)!
self.bannerImages.append(source)
}
}
completionHandler(self.bannerImages)
}
}
func showBanner() {
slideshow.setImageInputs(bannerImages)
}
Move the code to viewWillAppear.
override func viewWillAppear(animated: Bool) {
Banners { (imagesource) in
if imagesource != nil {
self.bannershow()
}
}
}
func Banners(completionHandler: ([AlamofireSource]?) -> ()) -> (){
Alamofire.request(.GET, "http://46.420.116.11/mobileapp/gps/api.php?rquest=get_banners")
.responseJSON{ response in
if let data = response.result.value{
let json = JSON(data)
let count = json["image_path"].count
for index in 0...count-1 {
let image :String = json["image_path"][index].stringValue
let source : AlamofireSource = AlamofireSource(urlString: image)!
self.Banner.append(source)
}
completionHandler(self.Banner)
}
}
}
You are executing for loop in Banners fun
for index in 0...count-1 {
let image :String = json["image_path"][index].stringValue
let source : AlamofireSource = AlamofireSource(urlString: image)!
self.Banner.append(source)
}
Replace this code in some other method and place an Optional
var image :String? = json["image_path"][index].stringValue
or place an thumb image, That will make you confirm that image is downloaded successfully or not .
Let me know if it works
Thanks, Happy Coding
Maybe you don't see images because you update them in a secondary thread, you have to perform image update in main thread;
In swift ( performSelectorOnMainThread() is not available), you may use something like this :
dispatch_async(dispatch_get_main_queue(), {
myslideshow.updateimage();
})
Not really sure, but I am guessing that since your images need to get downloaded, they are nil when you call self.slideshow.setImageInputs(self.Banner), which internally sets the image from the list to an imageview which is added inside the scrollView. So one way I can think of is use SDWebImage to set the image to the imageView so that it updates the respective imageViews once the image is ready(downloaded). I think you will need to use it in the InputSource.swift class in the setToImageView(_:) method. You will have to check this though, this is the only possible problem i could think of that might be causing your issue.

Resources