How to get the data from file on Icloud Swift - ios

I'm creating an app that gets a file from Icloud and converts it into a b64 format string, but I have a problem:
I really don't know how to get the data from this file. I thought that could be easy opening the Path from the imported file from ICloud but It returns nil when I try to acess to the route.
My code example is here, as you can see I have the temp route :( (file:///.../Aplication/xxx-xxx-xxx-xx/temp/com.domain.AppName/Carolina.cer):
extension KeyViewController: UIDocumentMenuDelegate {
func documentMenu(documentMenu: UIDocumentMenuViewController, didPickDocumentPicker documentPicker: UIDocumentPickerViewController) {
documentPicker.delegate = self
print ("documentMenu")
self.presentViewController(documentPicker, animated: true, completion: nil)
}
}
extension KeyViewController: UIDocumentPickerDelegate {
func documentPicker(controller: UIDocumentPickerViewController, didPickDocumentAtURL url: NSURL) {
print("url string")
print(String(url))
//prints file:///.../Aplication/xxx-xxx-xxx-xx/temp/com.domain.AppName/Carolina.cer
if controller.documentPickerMode == UIDocumentPickerMode.Import {
if (NSFileManager.defaultManager().fileExistsAtPath(String(url))){
print("exists")
}
else{
print("not exists")
}
dispatch_async(dispatch_get_main_queue()) {
if let fileName = url.lastPathComponent {
var fileNameArray = fileName.componentsSeparatedByString(".")
print ("fileNameArray")
print (fileNameArray)
if ((fileNameArray[1] != "cer") && (fileNameArray[1] != "key")) {
self.alertMessage = "Not a valid selection, choose .cer or .key Files"
self.showAlertMessage(self.alertMessage)
}
else{
//saving the name
if (fileNameArray[1]=="cer")
{
NSUserDefaults.standardUserDefaults().setObject(fileName, forKey: "temporalCerFile")
}
else{
//saving the name
NSUserDefaults.standardUserDefaults().setObject(fileName, forKey: "temporalKeyFile")
}
NSUserDefaults.standardUserDefaults().synchronize()
}
}
}
}
}
}
How can I get the content inside the file?. Please, hope you can help me

Your code isn't working because you are passing the incorrect value to fileExistsAtPath:.
The proper way to convert a file NSURL to a file string is to use the path() method.
if (NSFileManager.defaultManager().fileExistsAtPath(url.path())) {
It might be url.path instead of url.path(). I'm not sure about the older Swift syntax. In Swift 3 it would be:
if FileManager.default.fileExists(atPath: url.path) {
Once you know it exists, that are many ways to load the file. It all depends on what you need to do with it. But the first requirement is to move the file from the given url to a location inside your app's sandbox. At least if you need the file to survive the next use of your app. If you just need to look at the file this one time, you don't need this step.

Related

How to list files and folder in Google drive root folder

This code is used to show google drive contents in my iOS app. Now I could sign in and show contents by using below query "mimeType ='\(mimeType)' or mimeType = 'application/vnd.google-apps.folder'".
The problem is that it returns all the mp3 files even the ones inside sub-folders, that is not what I want. I want to show the same structure as google drive root. Then when a user enter any sub-folder, I would send another request to retrieve the mp3 files in that sub-folder.
So how could I reconstruct this query to achieve it?
// the code to filter/search google drive files.
import Foundation
import GoogleAPIClientForREST
class GoogleDriveAPI {
private let service: GTLRDriveService
init(service: GTLRDriveService) {
self.service = service
}
public func search(_ mimeType: String, onCompleted: #escaping ([GTLRDrive_File]?, Error?) -> ()) {
let query = GTLRDriveQuery_FilesList.query()
query.pageSize = 100
query.q = "mimeType ='\(mimeType)' or mimeType = 'application/vnd.google-apps.folder'"
self.service.executeQuery(query) { (ticket, results, error) in
onCompleted((results as? GTLRDrive_FileList)?.files, error)
}
}
if you do mimeType = 'application/vnd.google-apps.folder' then you are telling it that you only want folders or a specific mime type.
if you do 'root' in parents" it will return everything with a parent folder of root.
so if you do 'root' in parents" and mimeType = 'application/vnd.google-apps.folder' you will get all of the folders that have a parent folder of root.
Per DalmTo's answer, I have changed my code and have some test. Now I get what I want.
In short, I split the list file into two steps, first to search with query = 'root' in parents", which will list all contents in root folder. Then I do a filter files?.filter { $0.mimeType == "audio/mpeg" || $0.mimeType == "application/vnd.google-apps.folder"} before passing that data source to another ViewController.
To do this, I get every folders in root and every mp3 files in root, other type of files are ignored. Then if a user enter one folder in root, I would do another http request(file list) to get its content.
Google drive API function.
import Foundation
import GoogleAPIClientForREST
class GoogleDriveAPI {
private let service: GTLRDriveService
init(service: GTLRDriveService) {
self.service = service
}
public func search(onCompleted: #escaping ([GTLRDrive_File]?, Error?) -> ()) {
let query = GTLRDriveQuery_FilesList.query()
query.pageSize = 100
// query.q = "mimeType ='\(mimeType)' or mimeType = 'application/vnd.google-apps.folder'"
query.q = "'root' in parents"
self.service.executeQuery(query) { (ticket, results, error) in
onCompleted((results as? GTLRDrive_FileList)?.files, error)
}
}
call api from a ViewController, and I put a bit context here to make it clear.
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .gray
setViews()
// add an observer on notification "userDidSignInGoogle"
NotificationCenter.default.addObserver(self, selector: #selector(userDidSignInGoogle), name: .signInGoogleCompleted, object: nil)
setUpGoogleSignIn()
}
func setUpGoogleSignIn() {
GIDSignIn.sharedInstance().delegate = self
GIDSignIn.sharedInstance().uiDelegate = self
GIDSignIn.sharedInstance().scopes = [kGTLRAuthScopeDrive]
GIDSignIn.sharedInstance().signInSilently()
}
// MARK: - Notification
#objc private func userDidSignInGoogle(_ notification: Notification) {
// Update screen after user successfully signed in
updateScreen()
print("userDidSignInGoogle")
listAudioFilesAndFolders()
}
func listAudioFilesAndFolders() {
self.googleAPIs?.search(onCompleted: { files, error in
guard error == nil, files != nil else {
print("Err: \(String(describing: error))")
return
}
self.dismiss(animated: true) {
let vc = GoogleDriveFilesViewController()
// filter the files before passing it.
vc.audioFilesAndFolders = files?.filter { $0.mimeType == "audio/mpeg" || $0.mimeType == "application/vnd.google-apps.folder"}
UIApplication.getTopMostViewController()?.present(vc, animated: true)
}
})
}

IOS editor bug. archivedData renamed

Please help me! I am stuck in a loop and can't find my way out. I am trying to learn IOS programming for work so I thought I would start with their tutorial app the Meal list application. I am at the part where you are supposed to start saving persistent data and now the editor has me stuck in a never ending loop. I have a line of code...
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(meals, toFile: Meal.ArchiveURL.path)
That gives me a warning that says...
'archiveRootObject(_:toFile:)' was deprecated in iOS 12.0: Use
+archivedDataWithRootObject:requiringSecureCoding:error: instead
OK, so I change the line of code to...
let isSuccessfulSave = NSKeyedArchiver.archivedDataWithRootObject(meals)
Which then gives me the warning...
'archivedDataWithRootObject' has been renamed to
'archivedData(withRootObject:)'
OK, so I change the line of code to...
let isSuccessfulSave = NSKeyedArchiver.archivedData(withRootObject: meals)
Which tells me...
'archivedData(withRootObject:)' was deprecated in iOS 12.0: Use
+archivedDataWithRootObject:requiringSecureCoding:error: instead
OK... So... archivedData was deprecated and I have to use archivedDataWithRootObject, but using archivedDataWithRootObject has been renamed to archivedData, but archivedData is deprecated so use archivedDataWithRootObject which is renamed to archivedData which is deprecated... ad infinitum.
I have tried looking on the developer docs but they just tell me the same thing, one is deprecated, with no links or anything and searching google just gives me a bunch of pages showing me the syntax of using any of them. I am still really new to IOS programming and have no idea how to get out of this endless loop of deprecated to renamed to deprecated to...
Please help, I am lost and not sure how to continue. Thank you.
I am following the same example you are trying to do, and I figured out how to update the methods for storing and retrieving values in iOS 12, this should help you:
//MARK: Private Methods
private func saveMeals() {
let fullPath = getDocumentsDirectory().appendingPathComponent("meals")
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: meals, requiringSecureCoding: false)
try data.write(to: fullPath)
os_log("Meals successfully saved.", log: OSLog.default, type: .debug)
} catch {
os_log("Failed to save meals...", log: OSLog.default, type: .error)
}
}
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
private func loadMeals() -> [Meal]? {
let fullPath = getDocumentsDirectory().appendingPathComponent("meals")
if let nsData = NSData(contentsOf: fullPath) {
do {
let data = Data(referencing:nsData)
if let loadedMeals = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? Array<Meal> {
return loadedMeals
}
} catch {
print("Couldn't read file.")
return nil
}
}
return nil
}
Also you will find that you need to update ViewDidLoad as this:
override func viewDidLoad() {
super.viewDidLoad()
// Use the edit button item provided by the table view controller.
navigationItem.leftBarButtonItem = editButtonItem
let savedMeals = loadMeals()
if savedMeals?.count ?? 0 > 0 {
meals = savedMeals ?? [Meal]()
} else {
loadSampleMeals()
}
}
I hope this helps, for me the app is now working, storing and retrieving data.
FYI: This doesn't work with Xcode 11 beta and iOS 13 is should work with anything before those versions.
A general solution for iOS 12 would be:
class SettingsArchiver {
static func setData(_ value: Any, key: String) {
let ud = UserDefaults.standard
let archivedPool = try? NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: true)
ud.set(archivedPool, forKey: key)
}
static func getData<T>(key: String) -> T? {
let ud = UserDefaults.standard
if let val = ud.value(forKey: key) as? Data,
let obj = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(val) as? T {
return obj
}
return nil
}
}
You need
try {
let data = try NSKeyedArchiver.archivedData(withRootObject:meals,requiringSecureCoding:true)
try data.write(to:fullPath)
}
catch {
print(error)
}
Here in Docs it's IOS 11+
I would say, the answer directly addressing your question is to use the ArchiveURL defined in your Meal.swift data model (think MVC pattern) and reimplement the saveMeals() function in your MealTableViewController.swift controller using the recommended replacement to the deprecated archiveRootObject method this way:
private func saveMeals(){
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: meals, requiringSecureCoding: true)
try data.write(to: Meal.ArchiveURL)
}
catch {
print("Couldn't save to file")
}
}
Although this answer is a little late :-) I hope it helps whomever may come across this issue.

Stop Offline MapBox Map re-downloading

I am using MapBox to download an offline map. So that my user has access to a specific area when they go travelling.
Using the MapBox Offline documentation, it appears that the MapBox Map always tries to download (re-download) whenever there is a connection.
How do I set up my MapBox so that it performs a check in storage to see if the map has been downloaded?
func startOfflinePackDownload() {
let region = MGLTilePyramidOfflineRegion(styleURL: mapView.styleURL, bounds: mapView.visibleCoordinateBounds, fromZoomLevel: mapView.zoomLevel, toZoomLevel: 13)
let userInfo = ["name": "My Offline Pack"]
let context = NSKeyedArchiver.archivedData(withRootObject: userInfo)
MGLOfflineStorage.shared().addPack(for: region, withContext: context) { (pack, error) in
guard error == nil else {
// The pack couldn’t be created for some reason.
print("Error: \(error?.localizedDescription ?? "unknown error")")
return
}
// Start downloading.
pack!.resume()
}
}
I found the below code to check to see if the download already exists... So this would go at the start of my 'startOfflinePackDownload()' function above.
However, the newer version of MapBox doesn't recognise the code. Is someone able to help me on this please?
MGLOfflineStorage.sharedOfflineStorage().getPacksWithCompletionHandler { (packs, error) in guard error == nil else {
return
}
for pack in packs {
let userInfo = NSKeyedUnarchiver.unarchiveObjectWithData(pack.context) as! [String: String]
if userInfo["name"] == "My Offline Pack" {
// allready downloaded
return
}
}
See the mapbox Doc , MGLOfflinePackStateUnknown = 0 means the the tiles is already downloaded, so we can check the Offline pack state likewise :
Code is in Objective c , You can convert to swift
-(void)mapViewDidFinishLoadingMap:(MGLMapView *)mapView {
NSArray *arrTiles = MGLOfflineStorage.sharedOfflineStorage.packs;
if (arrTiles.count==0) {
[self startOfflinePackDownload];
}
for (MGLOfflinePack *downloadPack in arrTiles) {
NSLog(#"title: %#",downloadPack.region.description );
switch (downloadPack.state) {
case MGLOfflinePackStateUnknown:
[downloadPack requestProgress];
break;
case MGLOfflinePackStateComplete:
break;
case MGLOfflinePackStateInactive:
[downloadPack resume];
break;
case MGLOfflinePackStateActive:
[self startOfflinePackDownload];
break;
case MGLOfflinePackStateInvalid:
// NSAssert(NO, #"Invalid offline pack at index path %#", indexPath);
break;
}
}
}
You should use MGLOfflineStorage.shared().packs, note that use this method only after map is fully loaded. Implement MGLMapViewDelegate method:
func mapViewDidFinishLoadingMap(_ mapView: MGLMapView) {
print(MGLOfflineStorage.shared().packs)
}
This code snippet will print all your packs, that currently stored on device. Don't do that in viewDidLoad or viewWillAppear methods, MGLOfflineStorage.shared().packs will return nil.
After you receive your packs you can iterate through them and choose that pack what your need to resume downloading or deleting it from offline storage
UPDATE
Save somewhere in code your content pack name of your downloading region and Bool variable to determine if your pack is already downloaded
let packageName = "YourPackageName"
var isPackageNameAlreadyDownloaded = false
Func below checks if packageName is already downloaded:
func downloadPackage() {
if let packs = MGLOfflineStorage.shared().packs {
if packs.count > 0 {
// Filter all packs that only have name
let filteredPacks = packs.filter({
guard let context = NSKeyedUnarchiver.unarchiveObject(with: $0.context) as? [String:String] else {
print("Error retrieving offline pack context")
return false
}
let packTitle = context["name"]!
return packTitle.contains("(Data)") ? false : true
})
// Check if filtered packs contains your downloaded region
for pack in filteredPacks {
var packInfo = [String:String]()
guard let context = NSKeyedUnarchiver.unarchiveObject(with: pack.context) as? [String:String] else {
print("Error retrieving offline pack context")
return
}
// Recieving packageName
let packTitle = context["name"]!
if packTitle == packageName {
// Simply prints how download progress
print("Expected: \(pack.progress.countOfResourcesExpected); Completed: \(pack.progress.countOfBytesCompleted)")
print("Tile bytes completed: \(pack.progress.countOfTileBytesCompleted); Tiles Completed: \(pack.progress.countOfTilesCompleted)")
// If package isn't fully downloaded resume progress. If it downloaded - it'll check and won't redownload it
pack.resume()
isPackageNameAlreadyDownloaded = true
break
} else {
// This is another region
}
}
}
}
// If region is downloaded - return
if isPackageNameAlreadyDownloaded {
return
}
// if not - create region, map style url (which you recieve from MapBox Styler
let region = MGLTilePyramidOfflineRegion(styleURL: URL(string: YourMapStyleUrl)!, bounds: YourBoundaries, fromZoomLevel: 12, toZoomLevel: 16.5)
// Save packageName in Library and archive in package context.
let userInfo = ["name": packageName]
let context = NSKeyedArchiver.archivedData(withRootObject: userInfo)
// Create and register an offline pack with the shared offline storage object.
MGLOfflineStorage.shared().addPack(for: region, withContext: context) { (pack, error) in
guard error == nil else {
// The pack couldn’t be created for some reason.
print("Error: \(error?.localizedDescription ?? "unknown error")")
return
}
// Start downloading.
pack!.resume()
print(MGLOfflineStorage.shared().packs)
// Shows the download progress in logs
print(pack!.progress)
}
}

MessageAppExtension: how to load sticker images from assets to MSStickerBrowserView?

Alright, I know this is new for everybody but I would think it'd be a simple concept - I am following this here to make a custom sticker message app extension:
https://code.tutsplus.com/tutorials/create-an-imessage-app-in-ios-10--cms-26870
Ive copied everything exactly and am trying to create a basic MSStickerBrowserView displaying (then later filtering using logic, but haven't attempted that yet) sticker pngs I have in my assets folder here:
The tutorial did not load from assets it seems but rather just from their project, regardless their code is old as here:
var stickers = [MSSticker]()
func loadStickers() {
for i in 1...2 {
if let url = Bundle.main.urlForResource("Sticker \(i)", withExtension: "png") { //ERROR!!
do {
let sticker = try MSSticker(contentsOfFileURL: url, localizedDescription: "")
stickers.append(sticker)
} catch {
print(error)
}
}
}
}
I get the error
Bundle has no member URLforResource
I can't find anything on this. How can I just display my stickers programmatically in the app?
Error:
These are the images Im trying to load regardless of their name:
The reason that tutorial doesn't use asset catalogs is that you cannot get a valid fileURL for images placed in an .xcassets folder when calling the urlForResource method on the bundle.
You need to add your assets individually like you would other files you are bringing in to the app. Calling pathForResource or urlForResource on the bundle at that point will no longer return nil.
EDIT: Here is a function that will take a folder name, loop through it's contents and return [MSSticker]? based on what it finds
func createStickers(from folderName: String) -> [MSSticker]? {
guard
let path = Bundle.main.resourcePath
else { return nil }
var stickers = [MSSticker]()
let folderPath = "\(path)/\(folderName)"
let folderURL = URL(fileURLWithPath: folderPath)
//get a list of urls in the chosen directory
do {
let imageURLs = try FileManager.default.contentsOfDirectory(at: folderURL,
includingPropertiesForKeys: nil,
options: .skipsHiddenFiles)
//loop through the found urls
for url in imageURLs {
//create the sticker and add it, or handle error
do {
let sticker = try MSSticker(contentsOfFileURL: url, localizedDescription: "yourDescription")
stickers.append(sticker)
} catch let error {
print(error.localizedDescription)
}
}
} catch let error {
print(error.localizedDescription)
}
//return nil if stickers array is empty
return stickers.isEmpty ? nil : stickers
}
This should let you just call this and get what you are after:
let stickers = createStickers(from: "YourFolderName")
Please note not to include the forward slash ('/') at the beginning of the folder name.
Just replace "resourceUrl" with:
let url = Bundle.main.url(forResource: "Sticker \(i)", withExtension: "png")
The code got replaced in Swift 3.
You can put the images in a folder like so (XCODE Viewport):
It make things more organised but doesnt need as much code as if you would put them in a .xcasset.
It can be put done by creating a new group instead of creating an .xcasset by (Right Clicking Message Extension and clicking New Group):
The following code for the StickerBrowserView can be called like so:
import UIKit
import Messages
class StickerBrowserViewController: MSStickerBrowserViewController {
var stickers = [MSSticker]()
func changeBrowserViewBackgroundColor(color: UIColor){
stickerBrowserView.backgroundColor = color
}
func loadStickers(){
createSticker(asset: "1", localizedDescription:"grinning face")
createSticker(asset: "2", localizedDescription:"grimacing face")
createSticker(asset: "3", localizedDescription:"grinning face with smiling eyes")
createSticker(asset: "4", localizedDescription:"face with tears of joy")
createSticker(asset: "5", localizedDescription:"smiling face with open mouth")
createSticker(asset: "6", localizedDescription:"smiling face with open mouth and smiling eyes")
}
func createSticker(asset: String, localizedDescription: String){
guard let stickerPath = Bundle.main.path(forResource:asset, ofType:"png") else {
print("couldn't create the sticker path for", asset)
return
}
// we use URL so, it's possible to use image from network
let stickerURL = URL(fileURLWithPath:stickerPath)
let sticker: MSSticker
do {
try sticker = MSSticker(contentsOfFileURL: stickerURL, localizedDescription: localizedDescription)
// localizedDescription for accessibility
stickers.append(sticker)
}catch {
print(error)
return
}
}
override func numberOfStickers(in stickerBrowserView: MSStickerBrowserView) -> Int{
return stickers.count
}
override func stickerBrowserView(_ stickerBrowserView: MSStickerBrowserView, stickerAt index: Int) -> MSSticker{
return stickers[index] as MSSticker
}
}
(Ps. Not my blog, but found it on google and it has been very useful)

How to retrieve the list of all installed keyboards in Swift?

I am trying to write a simple function that will check to see if a specific keyboard is installed.
Here is what I have in the function so far:
func isCustomKeyboardEnabled() {
let bundleID:NSString = "com.company.MyApp.Keyboard"
let installedKeyboards = NSUserDefaults.standardUserDefaults().objectForKey("AppleKeyboards")
println(installedKeyboards)
}
This is what it returns in the console:
Optional((
"en_GB#hw=British;sw=QWERTY",
"emoji#sw=Emoji",
"com.nuance.swype.app.Global-Keyboard",
))
I am having an hard time checking to see if my bundleID is in this returned object. I've tried a for in and anif(contains(x,x)) but it fails to build. Any help would be much appreciated.
Swift 2.0 Solution:
func installedKeyboards(){
if let installedKeyboard = NSUserDefaults.standardUserDefaults().objectForKey("AppleKeyboards") as? [String]{
if installedKeyboard.contains("Your Unique Identifier"){
print("Custom Keyboard Found")
}else{
print("Custom Keyboard Not Installed")
}
}
}
You've got an Optional response there, meaning that the value could be nil. Try doing this instead:
if let installedKeyboards = NSUserDefaults.standardUserDefaults().objectForKey("AppleKeyboards") {
if (contains(installedKeyboards, "Your keyboard") {
// Do stuff.
}
}
Here's the Swift 4 version from Statik answer:
func installedKeyboards() {
if let installedKeyboard = UserDefaults.standard.object(forKey: "AppleKeyboards") as? [String] {
if installedKeyboard.contains("Your Unique Identifier") {
print("Custom Keyboard Found")
}
else {
print("Custom Keyboard Not Installed")
}
}
}

Resources