Reading/Writing data that is accessible from iOS app extension - ios

I'm working on an iOS Content Blocker, but I would like to have the user choose what lists they want to block (ads, tracks, adult sites etc.). I found that the app extension and the containing app's bundles are separate and have no access to each other's files, so a shared container is needed. I created an app group, but it seems like what I write there does not actually get written. What I am attempting to do is read a .json file from the bundle, and then write it to a sharedJson.json file that the content blocker extension can access.
func writeJsonToSharedJson(arrayOfStrings:[String]) -> Bool {
let composedString = arrayOfStrings.joined(separator: "\n")
let sharedJsonPath = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.alexspear.BlockerTestGroup")?.appendingPathComponent("sharedJson.json")
//let sharedJsonPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("sharedJson.json")
do {
try composedString.write(to: sharedJsonPath!, atomically: true, encoding: .utf8)
}
catch {
print("Could not write to sharedJson.json\n")
return false
}
return verifyJsonWrite()
}
The result is that through the verifyJsonWrite() function, there is nothing there. Am I incorrect in assuming you can create a file in the app group container? I have also tried using FileManager's createFile function with the same result.

Related

FileManager can't locate PDF directory in Xcode project

I am trying to list all PDFs inside a folder that is stored in my Xcode project using SwiftUI, but after trying many different methods I found on here I cannot get it working.
Currently I am using file manager to list the items but when I try and print or list the items found it returns nil.
I have added the PDFs by dragging them into the Xcode project. I have also made sure that they are in my Copy Bundle Resource so I can use FileManager. See below:
Here is my Xcode structure, I am trying to list all items inside PDF. As you can see below the PDFs are stored outside the main folder structure in "/Swift/Products/Detail/Tab5/PDF", so when trying to list the files using Bundle.main.bundlePath, it looks at products.app.
Here is the code where I am trying to use FileManager to find all PDFs inside the folder:
struct ProductTab5View: View {
#State var files = getFiles()
var body: some View {
VStack{
ForEach(0..<files.count, id: \.self) { item in
Text(files[item])
}
}
}
}
func getFiles() -> Array<String>{
// Get document directory url
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
var files: [String] = []
do {
// Get the directory contents urls (including subfolders urls)
let directoryContents = try FileManager.default.contentsOfDirectory(at: documentsUrl, includingPropertiesForKeys: nil)
print(directoryContents)
// filter directory contents:
let pdfFiles = directoryContents.filter{ $0.pathExtension == "pdf" }
let pdfFileNames = pdfFiles.map{ $0.deletingPathExtension().lastPathComponent }
files.append(contentsOf: pdfFileNames)
} catch {
print(error)
}
return files
}
In such a way resources are copied flat into root application bundle, not user 's Documents as it was tried in provided code snapshot (and no PDF sub-directory is created for you).
So the solution would be just to iterate internal pdf files, like
func getFiles() -> Array<String> {
return Bundle.main.urls(forResourcesWithExtension: "pdf", subdirectory: nil)?
.compactMap { $0.lastPathComponent } ?? []
}
Tested with Xcode 12.1 / iOS 14.1
There are a few potential problems with your current code.
The first is this line:
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
Unless you have gone through a process separately from what you've shown of adding all of these files to the app's document directory, then there's no reason to believe that these files will be there. In other words: your Xcode project is not the same as your app's document directory. Think of the app's document directory as a place where you may store user-created or perhaps downloaded content as managed by the app -- not Xcode itself.
Next thing to check is whether all of these files are truly added to your target. Check your target's "Build Phases" -> "Copy Bundle Resources" section to see if they appear there.
If they do, you can use FileManager, but you have to access the correct directory, which is inside the main bundle -- not the app's user document directory.
The following answer goes into details about this (including making sure you create folder references): Getting a list of files in the Resources folder - iOS
The gist will be doing something like this:
if let files = try? FileManager.default.contentsOfDirectory(atPath: Bundle.main.bundlePath) { //may need to dig into subdirectories
for file in files {
//manipulate the file
}
}
and then using FileManager to list the documents in that path.
Note that you can also get all the files (including subdirectories) recursively by doing something like this:
let url = URL(fileURLWithPath: Bundle.main.bundlePath)
if let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) {
for case let fileURL as URL in enumerator {
print(fileURL)
}
}
By using the above code during your debugging process, this should give you an insight into what your directory structure is really like inside your bundle. Once you've figured that out, adjust the previous code sample to be something like:
if let files = try? FileManager.default.contentsOfDirectory(atPath: Bundle.main.bundlePath + mySubPath) {
for file in files {
//manipulate the file
}
}
which will give you just the files in the one subdirectory you want once you fill in mySubPath. Note that if you want recursive search, you can use the code sample above.
You may want to exclude non-file items eventually -- see this answer for more details about recursive directory lists in Swift: listing all files in a folder recursively with swift

open documents directory of my iOS app in Swift

In my iOS Swift App, I have created files in my App's documents directory through this code:
let localFileName = String("\(fileName).rtf")
let text = String("text text text")
if let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
let fileURL = dir.appendingPathComponent(localFileName)
do {
try text.write(to: fileURL, atomically: false, encoding: .utf8)
}
catch {
}
}
Now I want to Add a link in my app to this directory where file is created so that user can see files. Currently I am doing this by this code:
let importMenu = UIDocumentPickerViewController(documentTypes: [String(kUTTypeRTF)], in: .open)
importMenu.delegate = self
importMenu.modalPresentationStyle = .formSheet
present(importMenu, animated: true, completion: nil)
but this code is for picking documents, not for opening directory, So How I can open my App's Documents directory, not for picking documents, just only for showing documents?
It is not possible to explore/open a directory in iOS app. Apple doesn't provide any api for the same. You need to create it by your own.
What you can do
You need to fetch all the files from the specific directory and list them all in either tableview or collection view.
And when user click on the any file, you can show that in web view or based on the file type you can do any specific operations.
So ultimately you need to explore more about FileManager.
This class contains what you want.

FileProvider: "CopyItem()" is called twice -> error (FTP download)

The first view of my app (Swift 5, Xcode 10, iOS 12) has a "username" TextField and a "login" Button. Clicking on the button checks if there's a file for the entered username on my FTP server and downloads it to the Documents folder on the device. For this I'm using FileProvider.
My code:
private func download() {
print("start download") //Only called once!
let foldername = "myfolder"
let filename = "mytestfile.txf"
let server = "192.0.0.1"
let username = "testuser"
let password = "testpw"
let credential = URLCredential(user: username, password: password, persistence: .permanent)
let ftpProvider = FTPFileProvider(baseURL: server, mode: FTPFileProvider.Mode.passive, credential: credential, cache: URLCache())
ftpProvider?.delegate = self as FileProviderDelegate
let fileManager = FileManager.default
let source = "/\(foldername)/\(filename)"
let dest = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent(filename)
let destPath = dest.path
if fileManager.fileExists(atPath: destPath) {
print("file already exists!")
do {
try fileManager.removeItem(atPath: destPath)
} catch {
print("error removing!") //TODO: Error
}
print("still exists: \(fileManager.fileExists(atPath: destPath))")
} else {
print("file doesn't already exist!")
}
let progress = ftpProvider?.copyItem(path: source, toLocalURL: dest, completionHandler: nil)
progressBar.observedProgress = progress
}
I'm checking if the file already exists on the device because FileProvider doesn't seem to provide a copyItem function for downloading that also lets you overwrite the local file.
The problem is that copyItem tries to do everything twice: Downloading the file the first time succeeds (and it actually exists in Documents, I checked) because I manually delete the file if it already exists. The second try fails because the file already exists and this copyItem function doesn't know how to overwrite and of course doesn't call my code to delete the original again.
What can I do to fix this?
Edit/Update:
I created a simple "sample.txt" at the root of my ftp server (text inside :"Hello world from sample.txt!"), then tried to just read the file to later save it myself. For this I'm using this code from the "Sample-iOS.swift" file here.
ftpProvider?.contents(path: source, completionHandler: {
contents, error in
if let contents = contents {
print(String(data: contents, encoding: .utf8))
}
})
But it also does this twice! The output for the "sample.txt" file is:
Optional("Hello world from sample.txt!")
Fetching on sample.txt succeed.
Optional("Hello world from sample.txt!Hello world from sample.txt!")
Fetching on sample.txt succeed.
Why is it calling this twice too? I'm only calling my function once and "start download" is also only printed once.
Edit/Update 2:
I did some more investigating and found out what's called twice in the contents function:
It's the whole self.ftpDownload section!
And inside FTPHelper.ftpLogin the whole self.ftpRetrieve section is
called twice.
And inside FTPHelper.ftpRetrieve the whole self.attributesOfItem
section is called twice.
And probably so on...
ftpProvider?.copyItem uses the same ftpDownload func, so at least I know why both contents() and copyItem() are affected.
The same question remains though: Why is it calling these functions twice and how do I fix this?
This isn't an answer that shows an actual fix for FileProvider!
Unfortunately the library is pretty buggy currently, with functions being called twice (which you can kind of prevent by using a "firstTimeCalled" bool check) and if the server's slow(-ish), you also might not get e.g. the full list of files in a directory because FileProvider stops receiving answers before the server's actually done.
I haven't found any other FTP libraries for Swift that work (and are still supported), so now I'm using BlueSocket (which is able to open sockets, send commands to the server and receive commands from it) and built my own small library that can send/receive,... files (using the FTP codes) around it.

SCNParticleSystem load from document Directory

I am trying load SCNParticleSystem from download bundle which i am not able to load.
Path for the resource.
file:///var/mobile/Containers/Data/Application/A91E9970-CDE1-43D8-B822-4B61EFC6149B/Documents/so/solarsystem.bundle/Contents/Resources/
let objScene = SCNParticleSystem(named: "stars", inDirectory: directory)
This object is nil.
This is a legitimate problem since SceneKit does not provide an out-of-the-box solution for initializing particle systems from files that are outside of the main bundle (the only init method SCNParticleSystem.init(named:inDirectory:) implies that SCNParticleSystem.scnp files are in the main bundle).
Luckily for us .scnp files are just encoded/archived SCNParticleSystem instances that we can easily decode/unarchive using NSKeyedUnarchiver:
extension SCNParticleSystem {
static func make(fromFileAt url: URL) -> SCNParticleSystem? {
guard let data = try? Data(contentsOf: url),
let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
let system = object as? SCNParticleSystem else { return nil }
return system
}
}
If you do not need to support iOS 9 and iOS 10 you can use NSKeyedUnarchiver.unarchivedObject(ofClass: SCNParticleSystem.self, from: data) instead of NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(_:) and type casting, which was introduced in iOS 11.0.
Another issue that you're most likely to encounter is missing particle images. That is because by default SceneKit will look for them in the main bundle. As of current versions of iOS (which is iOS 12) and Xcode (Xcode 10) particle images in .scnp files (particleImage property) are String values which are texture filenames in the main bundle (that might change, but probably won't, however there's not much else we could use).
So my suggestion is to take that filename and look for the texture file with the same name in the same directory where the .scnp file is:
extension SCNParticleSystem {
static func make(fromFileAt url: URL) -> SCNParticleSystem? {
guard let data = try? Data(contentsOf: url),
let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
let system = object as? SCNParticleSystem else { return nil }
if let particleImageName = system.particleImage as? String {
let particleImageURL = url
.deletingLastPathComponent()
.appendingPathComponent(particleImageName)
if FileManager.default.fileExists(atPath: particleImageURL.path) {
system.particleImage = particleImageURL
}
}
return system
}
}
You can just set the URL of the image file and SceneKit will handle it from there.
As a little side-note, the recommended directory for downloadable content is Application Support directory, not Documents.
Application Support: Use this directory to store all app data files except those associated with the user’s documents. For example, you might use this directory to store app-created data files, configuration files, templates, or other fixed or modifiable resources that are managed by the app. An app might use this directory to store a modifiable copy of resources contained initially in the app’s bundle. A game might use this directory to store new levels purchased by the user and downloaded from a server.
(from File System Basics)
Don't have enough reps to add the comment so adding it as the answer.
The answer by Lësha Turkowski works for sure but was had issues with loading the particle images using only NSURL.
All particles were appearing square which meant,
If the value is nil (the default), SceneKit renders each particle as a
small white square (colorized by the particleColor property).
SCNParticleSystem particleImage
In the documentation it says You may specify an image using an
NSImage (in macOS) or UIImage (in iOS) instance, or an NSString or
NSURL instance containing the path or URL to an image file.
Instead of using the NSURL, ended up using the UIImage and it loaded up fine.
extension SCNParticleSystem {
static func make(fromFileAt url: URL) -> SCNParticleSystem? {
guard let data = try? Data(contentsOf: url),
let object = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data),
let system = object as? SCNParticleSystem else { return nil }
if let particleImageName = system.particleImage as? String {
let particleImageURL = url
.deletingLastPathComponent()
.appendingPathComponent(particleImageName)
if FileManager.default.fileExists(atPath: particleImageURL.path) {
// load up the NSURL contents in UIImage
let particleUIImage = UIImage(contentsOfFile: particleImageURL.path)
system.particleImage = particleUIImage
}
}
return system
}
}
I found out, that sometimes when dragging a SCNParticleSystem file into your project (probably form a different project) a silent error can happen due to some bugs in Xcode. As a result you can't get a reference to an instance of your SCNParticleSystem.
Solution: Check your BuildSettings in your target. The SCNPaticleSystem AND the associated ImageFile should be listed there and then you should get it right. (see screenShot below)

How to I hide specific file in File App of Device

First of all I know this is a subjective question, not code based, How ever I need to find solution. Please provide any references
I am working on a task in which I saving the files in Device Document directory. Upto here all is working fine. However when I see these files from :
Files App -> App Folder and the files
I can see all those files.
Now I want to hide few of them, How can I achieve these....?
As per your requirement. You need to change some lines of code in saving file.
Firstly, The FileApp shows all the data that save in documentsDirectory of app whether you put code for hide or not. To hide few files you need to make separate path for them.
First, the file you want to show in documentDirectory:
let documentsDirectory = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)).last! as URL
The file you don’t want to show with user, you need to create path here:
let libraryDirectory = (FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)).last! as URL
Once you set the library path for saving all your important files. All will be hidden and cannot be accessed by user.
To know more about files, please refer to this link:
https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html
Xcode 9 • Swift 4 or Xcode 8 • Swift 3
extension URL {
var isHidden: Bool {
get {
return (try? resourceValues(forKeys: [.isHiddenKey]))?.isHidden == true
}
set {
var resourceValues = URLResourceValues()
resourceValues.isHidden = newValue
do {
try setResourceValues(resourceValues)
} catch {
print("isHidden error:", error)
}
}
}
}
for more reference see chain here Cocoa Swift, get/set hidden flag on files and directories
or
https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/ManagingFIlesandDirectories/ManagingFIlesandDirectories.html

Resources