How to change COLLADA(.dae) file to SceneKit(.scn) file programmatically Swift - swift

COLLADA(.dae) , SceneKit(.scn)
I am working with scenekit and problem is there is an admin who will upload .dae file to server and i'll get that link in app and by using code i have to show it on camera. In running app for .scn file url it is working fine but for .dae file url it is generating an error COLLADA files are not supported on this platform. so i want to convert .dae file to .scn file at run time. If anyone know any other source which can convert .dae file to .scn file kindly mention. By this source admin will upload .scn file to server after converting.

Finally solved thanks stackoverflow by using Model I/O swift concept
Step1#
// first need to download file to local directory
func downloadSceneTask(){
//1. Get The URL Of The SCN File
guard let url = URL(string: "Your_url") else { return }
//2. Create The Download Session
let downloadSession = URLSession(configuration: URLSession.shared.configuration, delegate: self, delegateQueue: nil)
//3. Create The Download Task & Run It
let downloadTask = downloadSession.downloadTask(with: url)
downloadTask.resume()
}
Step2#
Now i have url saved in my local device memory now i'll click anywhere in camera and load that local url in scene.
func addTapGestureToSceneView() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didReceiveTapGesture(_:)))
sceneView.addGestureRecognizer(tapGestureRecognizer)
}
#objc func didReceiveTapGesture(_ sender: UITapGestureRecognizer) {
let location = sender.location(in: sceneView)
guard let hitTestResult = sceneView.hitTest(location, types: [.featurePoint, .estimatedHorizontalPlane]).first
else { return }
let results = self.sceneView.hitTest(location, types: .featurePoint)
// 2
guard let result = results.first else {
return
}
// 3
let translation = result.worldTransform.translation
self.translation = translation
anchor = ARAnchor(transform: hitTestResult.worldTransform)
sceneView.session.add(anchor: anchor!)
}
Step3#
My ViewDidLoad function be like
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
addTapGestureToSceneView()
downloadSceneTask()
}
Step4#
Add Protocol ARSCNViewDelegate
// Getting file from local directory and load
extension ViewController: ARSCNViewDelegate {
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard !(anchor is ARPlaneAnchor) else { return }
if let droneNode = loadModel() {
DispatchQueue.main.async {
node.addChildNode(droneNode)
}
}
}
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]
return documentsDirectory
}
// Loads The SCNFile From The Documents Directory
func loadModel() -> SCNNode? {
//1. Get The Path Of The Downloaded File
let downloadedScenePath = getDocumentsDirectory().appendingPathComponent("table.obj")
let asset = MDLAsset(url: downloadedScenePath)
let object = asset.object(at: 0)
let node = SCNNode(mdlObject: object)
//7. Add It To The Scene
return node
}
}

Related

How do I load a Data object into a SCNScene?

I want to load a 3d usdz blob into a view, but since I only have the data object, I'm trying to initialize the scene with that with no luck.
To that, I initialize the SCNSceneSource() and then open it using .scene().
Now what I don't understand:
If I use a URL and load the scene directly - it works.
If I use a Data object on the same URL it doesn't.
Apple docs says, the data should be of type NSData but that seems wrong.
import SceneKit
let url = URL(string: "file:///Users/thilo/Desktop/Input/UU2.usdz")!
// working
let src_ok = SCNSceneSource(url: url)
let scn_ok = src_ok?.scene(options: nil, statusHandler: {
a,b,c,d in print("OK: \(a) \(b) \(String(describing: c)) \(d) ")
})
print("Ok: \(scn_ok)")
// Not working?
let data = try! Data(contentsOf: url)
let src_bad = SCNSceneSource(data: data)
let scn_bad = src_bad?.scene(options: nil, status handler: {
a,b,c,d in print("BAD: \(a) \(b) \(String(describing: c)) \(d) ")
})
print("Failed: \(scn_bad)")
running on Playground says:
Ok: Optional(<SCNScene: 0x6000038e1200>)
BAD: 0.0 SCNSceneSourceStatus(rawValue: 4) nil 0x000000016fa948bf
BAD: 0.0 SCNSceneSourceStatus(rawValue: 4) nil 0x000000016fa942af
BAD: 0.0 SCNSceneSourceStatus(rawValue: -1) Optional(Error Domain=NSCocoaErrorDomain Code=260 "Could not load the scene" UserInfo={NSLocalizedDescription=Could not load the scene, NSLocalizedRecoverySuggestion=An error occurred while parsing the COLLADA file. Please check that it has not been corrupted.}) 0x000000016fa942af
Failed: nil
What am I missing?
SCNSceneSource doesn't support .usdz in Data context
Official documentation says that SCNSceneSource object supports only .scn, .dae and .abc file formats. But it turns out that SceneKit doesn't support URL-loading of .usdz only in the context of working with Data. Thus, when working with Data, use files in the .scn format.
import SceneKit
import Cocoa
class GameViewController : NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let url = URL(string: "file:///Users/swift/Desktop/ship.scn") {
let data = try! Data(contentsOf: url)
let source = SCNSceneSource(data: data)
let sceneView = self.view as! SCNView
sceneView.scene = source?.scene()
}
}
}
To load .usdz using URL, try SCNSceneSource.init?(url: URL)
class GameViewController : NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let url = URL(string: "file:///Users/swift/Desktop/ship.usdz") {
let source = SCNSceneSource(url: url)
let sceneView = self.view as! SCNView
sceneView.scene = source?.scene()
}
}
}
Or use SCNScene object to load .usdz model
class GameViewController : NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(fileURLWithPath: "/Users/swift/Desktop/ship.usdz")
do {
let scene = try SCNScene(url: url)
let sceneView = self.view as! SCNView
sceneView.scene = scene
sceneView.autoenablesDefaultLighting = true
} catch {
print(error.localizedDescription)
}
}
}
Gathering from the comment "does not support usdz" my solution is:
to create a temporary file ( .usdz) seems to be required by the API...
and then manually remove the temporary file after loading.
First extend FileManager with the below code:
public extension FileManager {
func temporaryFileURL(fileName: String = UUID().uuidString,ext: String) -> URL? {
return URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
.appendingPathComponent(fileName + ext)
}
}
For a limited hard-coded use case:
let fm = FileManager.default
let tempusdz = fm.temporaryFileURL(ext:".usdz")!
fm.createFile(atPath: tempusdz.path(), contents: sceneData)
let src = SCNSceneSource(url: tempusdz)
if let scene = src?.scene(options: nil) {
....
}
try? fm.removeItem(at: tempusdz)
of course this is a hack, because it will only work if the data is in usdz format.
Since usdz is a ZIP archive, maybe testing for a zip and then just doing the below is a better option:
let sceneData:Data? = data
var sceneSrc: SCNSceneSource? = nil
var tempURL:URL? = nil
if let dataStart = sceneData?.subdata(in: 0..<4),
let dataMagic = String(data: dataStart, encoding: String.Encoding.utf8) as String?,
dataMagic == "PK\u{3}\u{4}" {
let fm = FileManager.default
tempURL = fm.temporaryFileURL(ext: ".usdz")
if let tempURL {
fm.createFile(atPath: tempURL.path(), contents: sceneData)
sceneSrc = SCNSceneSource(url: tempURL)
}
} else {
sceneSrc = SCNSceneSource(data: sceneData!)
}
let scene = sceneSrc?.scene()
if let tempURL {
try? FileManager.default.removeItem(at: tempURL)
}
Does anyone knows a better solution?
Is there an easy way to check the type of the Data ?
potential solution could be to verify the format of the data object and ensure that it is a valid COLLADA file.
import Foundation
let url = URL(string: "file:///Users/thilo/Desktop/Input/UU2.usdz")!
let data = try! Data(contentsOf: url)
print("Data size: \(data.count)")
print("Data format: \(data.description)")
you usually get these types of errors when the data wasn't properly formatted

PDF view not presenting

I'm downloading a PDF from a url and then displaying it using PDFKit in Swift5. However, when I tap my viewCell it is not presenting the PDF and potentially not downloading the PDF. How can I verify this?
I have two files, PlanogramViewController and ShowPDFViewController.
var pdfURL: URL!
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let plano = self.data[indexPath.row] as? Planogram {
guard let url = URL(string: Plano.pdfUrl) else { return }
let urlSession = URLSession(configuration: .default, delegate: self, delegateQueue: OperationQueue())
let downloadTask = urlSession.downloadTask(with: url)
downloadTask.resume()
let pdfViewController = ShowPDFViewController()
pdfViewController.pdfURL = self.pdfURL
present(pdfViewController, animated: false, completion: nil)
tableView.deselectRow(at: indexPath, animated: true)
}
}
extension PlanogramViewController: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print("downloadLocation:", location)
// create destination URL with the original pdf name
guard let url = downloadTask.originalRequest?.url else { return }
let documentsPath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
let destinationURL = documentsPath.appendingPathComponent(url.lastPathComponent)
// delete original copy
try? FileManager.default.removeItem(at: destinationURL)
// copy from temp to Document
do {
try FileManager.default.copyItem(at: location, to: destinationURL)
self.pdfURL = destinationURL
} catch let error {
print("Copy Error: \(error.localizedDescription)")
}
}
}
So I'm not getting any download verification from my extension. It should be printing my initial download location and then moving it to the cache. If it encounters an error it should also print an error, but I'm getting nothing in the console. As if nothing is downloading.
Then here is the pdfViewController it's supposed to present once it has the PDF downloaded:
import UIKit
import PDFKit
class ShowPDFViewController: UIViewController {
var pdfView = PDFView()
var pdfURL: URL!
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(pdfView)
if let document = PDFDocument(url: pdfURL) {
pdfView.document = document
}
DispatchQueue.main.asyncAfter(deadline: .now()+3) {
self.dismiss(animated: true, completion: nil)
}
}
override func viewDidLayoutSubviews() {
pdfView.frame = view.frame
}
}
The current behavior is it just highlights the cell I tapped. Nothing in the console and nothing presented on the display.
You need to conform to the UITableViewDelegate and UITableViewDataSourceDelegate protocols in the view controller that has the tableView.
and add
tableView.delegate = self
tableView.dataSource = self
in the viewDidLoad()
Then, implement the required methods, and add optional methods to further customize your tableView.

Offline MapBox Sideloading Merge

First post to Stacked Overflow
I am having some difficulty with merge the MapBox database off-line content via sideloading. I have tried the examples in GitHub to no avail.
Can someone shed some light on the code snippet below I am using
The file path is correct and writable
The file size is 66MB so there is data in there
When I call the addContents function of the MGLOfflineStorage class the pack result is zero and the content not merged.
Any ideas?
CM
import UIKit
import Mapbox
class ViewController: UIViewController, MGLMapViewDelegate {
var mapView: MGLMapView!
var progressView: UIProgressView!
override func viewDidLoad() {
super.viewDidLoad()
let mapView = MGLMapView(frame: view.bounds, styleURL: MGLStyle.streetsStyleURL)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.delegate = self
view.addSubview(mapView)
mapView.setCenter(CLLocationCoordinate2D(latitude: 22.27933, longitude: 114.16281),
zoomLevel: 13, animated: false)
testAddFileContent()
NotificationCenter.default.addObserver(self, selector: #selector(offlinePackProgressDidChange), name: NSNotification.Name.MGLOfflinePackProgressChanged, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(offlinePackDidReceiveError), name: NSNotification.Name.MGLOfflinePackError, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(offlinePackDidReceiveMaximumAllowedMapboxTiles), name: NSNotification.Name.MGLOfflinePackMaximumMapboxTilesReached, object: nil)
print(MGLOfflineStorage.shared.packs?.count)
}
func testAddFileContent() {
let documentPaths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentDir = documentPaths[0]
let fileManager = FileManager.default
let directoryExists: Bool = fileManager.fileExists(atPath: documentDir)
if !directoryExists {
try? fileManager.createDirectory(atPath: documentDir, withIntermediateDirectories: true, attributes: nil)
}
let bundle = Bundle.main
// Valid database
do {
let resourceURL = bundle.url(forResource: "cache", withExtension: ".db")
let filePath = bundle.path(forResource: "cache", ofType: ".db")
// try? fileManager.moveItem(at: resourceURL! to: filePath!)
let attributes = [FileAttributeKey.posixPermissions: NSNumber(value: 0o777)]
try? fileManager.setAttributes(attributes, ofItemAtPath: filePath!)
var fileSize : UInt64
do {
//return [FileAttributeKey : Any]
let attr = try FileManager.default.attributesOfItem(atPath: filePath ?? "<#default value#>")
fileSize = attr[FileAttributeKey.size] as! UInt64
//if you convert to NSDictionary, you can get file size old way as well.
let dict = attr as NSDictionary
fileSize = dict.fileSize()
print(fileSize)
} catch {
print("Error: \(error)")
}
MGLOfflineStorage.keyPathsForValuesAffectingValue(forKey: "packs")
MGLOfflineStorage.shared.addContents(ofFile: filePath!, withCompletionHandler: nil)
print(MGLOfflineStorage.shared.packs?.count)
// loadOffline()
}
}
Merge of offline sideloaded cache.db for MapBox
I had this same issue and finally found the issue. In your main.storyboard click on the map view you are using, then click on the attribute inspector in the top right. Change the default style URL to the style URL you used when you downloaded your map region. Also change the latitude, longitude, and zoom values to match your offline region.

Mac - Swift 3 - queuing audio files and playing

I would like to write an app in swift 3 in order to play queued audio files without any gap, crack or noise when passing from one to another.
My first try was using AvAudioPlayer and AvAudioDelegate (AVAudioPlayer using array to queue audio files - Swift), but I don't know how to preload the next song to avoid gap. Even if I know how to do it, I am not certain it is the best way to achieve my goal.
AVQueuePlayer seems to be a better candidate for the job, it is made for that purpose, but I don't find any example to help me out.
Maybe it is only a problem of preloading or buffering? I am a bit lost in this ocean of possibilities.
Any suggestion is welcomed.
It is far to be perfect, specially if you want to do it twice or more ("file exist" error), but it can serve as a base.
What it does is taking two files (mines are aif samples of ap. 4 sec.), encode them in one file and play the resulting files. If you have hundreds of them, assembled aleatory or not, it can make great fun.
All credits for the mergeAudioFiles function goes to #Peyman and #Pigeon_39. Concatenate two audio files in Swift and play them
Swift 3
import Cocoa
import AVFoundation
var action = AVAudioPlayer()
let path = Bundle.main.path(forResource: "audiofile1.aif", ofType:nil)!
let url = URL(fileURLWithPath: path)
let path2 = Bundle.main.path(forResource: "audiofile2.aif", ofType:nil)!
let url2 = URL(fileURLWithPath: path2)
let array1 = NSMutableArray(array: [url, url2])
class ViewController: NSViewController, AVAudioPlayerDelegate
{
#IBOutlet weak var LanceStop: NSButton!
override func viewDidLoad()
{
super.viewDidLoad()
}
override var representedObject: Any?
{
didSet
{
// Update the view, if already loaded.
}
}
#IBAction func Lancer(_ sender: NSButton)
{
mergeAudioFiles(audioFileUrls: array1)
let url3 = NSURL(string: "/Users/ADDUSERNAMEHERE/Documents/FinalAudio.m4a")
do
{
action = try AVAudioPlayer(contentsOf: url3 as! URL)
action.delegate = self
action.numberOfLoops = 0
action.prepareToPlay()
action.volume = 1
action.play()
}
catch{print("error")}
}
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool)
{
if flag == true
{
}
}
var mergeAudioURL = NSURL()
func mergeAudioFiles(audioFileUrls: NSArray) {
//audioFileUrls.adding(url)
//audioFileUrls.adding(url2)
let composition = AVMutableComposition()
for i in 0 ..< audioFileUrls.count {
let compositionAudioTrack :AVMutableCompositionTrack = composition.addMutableTrack(withMediaType: AVMediaTypeAudio, preferredTrackID: CMPersistentTrackID())
let asset = AVURLAsset(url: (audioFileUrls[i] as! NSURL) as URL)
let track = asset.tracks(withMediaType: AVMediaTypeAudio)[0]
let timeRange = CMTimeRange(start: CMTimeMake(0, 600), duration: track.timeRange.duration)
try! compositionAudioTrack.insertTimeRange(timeRange, of: track, at: composition.duration)
}
let documentDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! as NSURL
self.mergeAudioURL = documentDirectoryURL.appendingPathComponent("FinalAudio.m4a")! as URL as NSURL
let assetExport = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetAppleM4A)
assetExport?.outputFileType = AVFileTypeAppleM4A
assetExport?.outputURL = mergeAudioURL as URL
assetExport?.exportAsynchronously(completionHandler:
{
switch assetExport!.status
{
case AVAssetExportSessionStatus.failed:
print("failed \(assetExport?.error)")
case AVAssetExportSessionStatus.cancelled:
print("cancelled \(assetExport?.error)")
case AVAssetExportSessionStatus.unknown:
print("unknown\(assetExport?.error)")
case AVAssetExportSessionStatus.waiting:
print("waiting\(assetExport?.error)")
case AVAssetExportSessionStatus.exporting:
print("exporting\(assetExport?.error)")
default:
print("Audio Concatenation Complete")
}
})
}
}

Impossible to stream video after downloading from Parse

It seems that I can't stream my videos once I downloaded them from Parse backend. However I know that the videos are working well as you can play it from Chrome but not from Safari ( I don't know why ...).
My code to play the video on the AVPlayer is also right since I test it for multiple NSUrl from local database.
Several posts about it confirm that there is a problem going on but no one has an answer.
iOS - Can't stream video from Parse Backend
Cant stream video (PFFile) from parse server
Thanks in advance if someone can help me.
The secret is to get the PFFIle data, save it locally with the proper extention (mov) and treaming the local file.
look at this entry [Cant stream video (PFFile) from parse server][1], the second anwser.
The PFFIle was created with the following code:
let videoPath = info[UIImagePickerControllerMediaURL] as! NSURL
let imageData = NSData (contentsOfURL:videoPath)! as NSData
do {
let file = try PFFile(name: "_mov", data: imageData, contentType: "video/quicktime")
videoSaveSelected(file )
} catch let error as NSError {
print("Error generating PFFile: \(error)")
}
This is the code for the player
import UIKit
import AVKit
import MobileCoreServices
import AssetsLibrary
import AVFoundation
import Parse
import ParseUI
class VideoPlayerViewController: UIViewController {
var videoUrl: NSURL!
var file: PFFile!
override func viewDidLoad() {
super.viewDidLoad()
// navigation
let backButtonCustom = UIButton( frame: CGRectMake(0, 0, 22, 22))
let backIcon = UIImage(named: "0836-arrow-left")?.imageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate)
backButtonCustom.setImage(backIcon, forState: UIControlState.Normal)
backButtonCustom.addTarget(self, action: #selector(ReqDataEntryLgViewController.doneButtonTapped), forControlEvents: UIControlEvents.TouchUpInside)
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backButtonCustom)
let doneButton = UIBarButtonItem(title: "Done", style: .Done, target: self, action: #selector(ReqDataEntryLgViewController.doneButtonTapped))
self.navigationItem.setRightBarButtonItems([doneButton], animated: true)
// video player
let playerController = AVPlayerViewController()
self.addChildViewController(playerController)
self.view.addSubview(playerController.view)
playerController.view.frame = self.view.frame
// get data from PFFile in database
file!.getDataInBackgroundWithBlock({
(movieData: NSData?, error: NSError?) -> Void in
if (error == nil) {
let documentsPath : AnyObject = NSSearchPathForDirectoriesInDomains(.DocumentDirectory,.UserDomainMask,true)[0]
let destinationPath:NSString = documentsPath.stringByAppendingString("/file.mov")
let filemanager = NSFileManager.defaultManager()
do {
try filemanager.removeItemAtPath(destinationPath as String)
} catch {
print("Ooops! Something went wrong: \(error)")
}
// write to local file
movieData!.writeToFile ( destinationPath as String, atomically:true)
// do {
// let attr : NSDictionary? = try NSFileManager.defaultManager().attributesOfItemAtPath(destinationPath as String)
//
// if let _attr = attr {
// let fileSize = _attr.fileSize()
// print ("Movie Size \(fileSize)" )
// }
// } catch {
// print("Ooops! Something went wrong getting size: \(error)")
// }
let playerItem = AVPlayerItem(asset: AVAsset(URL: NSURL(fileURLWithPath: destinationPath as String)))
let player = AVPlayer(playerItem: playerItem)
playerController.player = player
player.play()
} else {
print ("error on getting movie data \(error?.localizedDescription)")
}
})
}
func doneButtonTapped(){
navigationController?.popViewControllerAnimated(true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}