Opening a folder with ReferenceFileDocument in SwiftUI - swift

I am trying to allow users to pick folders using the DocumentGroup context in SwiftUI, having defined a SourceFolderDocument class inheriting from ReferenceFileDocument.
However, when launching the app, only elements conforming to .sourceCode are pickable in the file picker. What would be the correct way to pick folders to open in SwiftUI?
Thank you in advance!
Here is the class in question:
final class SourceFolderDocument: ReferenceFileDocument{
static var readableContentTypes: [UTType] {[.folder, .sourceCode, .directory]}
init(configuration: ReadConfiguration) throws {
var documents : [SourceFile] = []
if(configuration.file.isDirectory){
guard let wrappers = configuration.file.fileWrappers
else{
throw CocoaError(.fileReadCorruptFile)
}
var documents : [SourceFile] = []
for document in wrappers {
if let fileContents = document.value.regularFileContents{
documents.append(SourceFile(name: document.key, contents: String(data: fileContents, encoding: .utf8)!, fileUTType: .sourceCode))
}
}
}else{
guard let data = configuration.file.regularFileContents
else{
throw CocoaError(.fileReadCorruptFile)
}
documents.append(SourceFile(name: configuration.file.filename ?? "blank", contents: String(data: data, encoding: .utf8) ?? "", fileUTType: .sourceCode))
}
self.sourceFolder = SourceFolder(name: configuration.file.filename!, documents: documents)
}
func snapshot(contentType: UTType) throws -> SourceFolder {
sourceFolder
}
func fileWrapper(snapshot: SourceFolder, configuration: WriteConfiguration) throws -> FileWrapper {
var fileWrappers: [String : FileWrapper] = [:]
for document in snapshot.documents {
fileWrappers[document.name] = FileWrapper(regularFileWithContents: document.contents.data(using: .utf8)!)
}
let fileWrapper = FileWrapper(directoryWithFileWrappers: fileWrappers)
return fileWrapper
}
init(){
sourceFolder = .defaultFolder
}
typealias Snapshot = SourceFolder
#Published var sourceFolder : SourceFolder
}

Since the menu is only using NSOpenPanel, I was about to suggest to create a small function:
fileprivate func openPanel() {
let panel = NSOpenPanel()
panel.allowsMultipleSelection = false
panel.canChooseDirectories = true
if panel.runModal() == .OK,
let url = panel.url {
Task {
try? await openDocument(at: url)
}
}
}
And then append to your view the new "Open Folder" menu:
.commandsReplaced {
CommandGroup(after: .newItem) {
Section {
Button("Open Folders") {
openPanel()
}
}
}
}
This would be creating (or replacing) the existing menus and it should also be possible to replace the "Open" menu.

Related

SwiftUI ShareLink set file name

I'm using the ShareLink to share an FileDocument which contains a String. The FileDocument is conform to the Transferable protocol.
This is the FileDocument Struct:
struct TransferableDocument: FileDocument, Transferable {
static var transferRepresentation: some TransferRepresentation
{
DataRepresentation(exportedContentType: .text) { log in
log.convertToData()
}
}
// tell the system to support only text
static var readableContentTypes: [UTType] = [.text]
// by default the document is empty
var text = ""
// this initializer creates a empty document
init(initialText: String = "") {
text = initialText
}
// this initializer loads data that has been saved previously
init(configuration: ReadConfiguration) throws {
if let data = configuration.file.regularFileContents {
text = String(decoding: data, as: UTF8.self)
}
}
// this will be called when the system wants to write the data to disk
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = Data(text.utf8)
return FileWrapper(regularFileWithContents: data)
}
func convertToData() -> Data
{
return text.data(using: .ascii) ?? Data()
}
}
And this is the ShareLink:
var doc: TransferableDocument
{
return TransferableDocument(initialText: "I'm a String")
}
ShareLink(item: doc ,preview: SharePreview("logfile"))
{
Text("Share")
}
When using AirDrop, the filename is set to the SharePreview title, in this case "logfile". When sharing it to Apps like Mail, the filename is simply set to "text".
Is there any way to set a default filename?
We had a similar issue and added an additional FileRepresentation to the transferRepresentation func with the appropriate file name configured when saving out the data. With that in place when shared to Mail the appropriate file name was used.
static var transferRepresentation: some TransferRepresentation
{
DataRepresentation(exportedContentType: .text) { log in
log.convertToData()
}
FileRepresentation(exportedContentType: .text) { log in
let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent("logfile").appendingPathExtension("txt")
try log.convertToData().write(to: fileURL)
return SentTransferredFile(fileURL)
}
}

How can I use UTType for comparing in a condition in Swift?

I am getting a folder url from user and then looking to find any mp3 files in that folder, the question itself in title and me just looking to use UTType in process.
As you can see I took all steps in codes just need last step in isMP3 function to finish the puzzle. So how can I use a path or URL and fining out the UTType of it and using it for comparing.
Also in my approach Xcode gave an error and says:
Cannot find 'UTType' in scope
Not sure why I have this error, normally it should Not be case, because it is type defined by Apple.
struct ContentView: View {
#State private var fileImporterIsPresented: Bool = false
var body: some View {
Button("Select your Folder") { fileImporterIsPresented = true }
.fileImporter(isPresented: $fileImporterIsPresented, allowedContentTypes: [.folder], allowsMultipleSelection: false, onCompletion: { result in
switch result {
case .success(let urls):
if let unwrappedURL: URL = urls.first {
if let contents = try? FileManager.default.contentsOfDirectory(atPath: unwrappedURL.path) {
contents.forEach { item in
if isMP3(path: unwrappedURL.path + "/" + item) {
print(item)
}
}
}
}
case .failure(let error):
print("Error selecting file \(error.localizedDescription)")
}
})
}
}
func isMP3(path: String) -> Bool {
// trying use UTType here
if URL(fileURLWithPath: path).??? == UTType.mp3 {
return true
}
else {
return false
}
}
To use UTType you have to import explicit package
import UniformTypeIdentifiers
it can be something like
func isMP3(path: String) -> Bool {
if let type = UTType(filenameExtension: URL(fileURLWithPath: path).pathExtension), type == UTType.mp3 {
return true
}
else {
return false
}
}
Here's another solution that uses URLResourceValues.contentType with a fallback to UTType(filenameExtension:):
import UniformTypeIdentifiers
func isMP3(path: String) -> Bool {
if let contentType = URL(fileURLWithPath: path).contentType,
contentType.conforms(to: .mp3)
{
return true
}
else {
return false
}
}
extension URL {
var contentType: UTType? {
if let resourceValues = try? resourceValues(forKeys: [.contentTypeKey]),
let contentType = resourceValues.contentType {
return contentType
} else if let contentType = UTType(filenameExtension: pathExtension) {
return contentType
} else {
return nil
}
}
}

Assigning MyScruct().var = results results in no assignment

Ok.. probably bad title. But here, the problem.
struct DeckView: View {
#State public var results = [ScryfallCard]()
var body: some View {
List(results, id: \.id ) { item in
Mkae a list containing the results.
}.onAppear {
ScryfallData().parseBulkData()
print("Type of results::", type(of: results))
print("results.capacity:", results.capacity)
}
}
}
struct ScryfallData {
func parseBulkData() {
let fm = FileManager.default
let path = Bundle.main.resourcePath
let items = try! fm.contentsOfDirectory(atPath: path!)
var oracleFileName = ""
for fileName in items {
if fileName .hasPrefix("oracle-cards"){
oracleFileName = fileName
}
}
print("if let savedJson = Bundle.main.url")
if let savedJson = Bundle.main.url(forResource: oracleFileName, withExtension: "") {
if let dataOfJson = try? Data(contentsOf: savedJson) {
print("if let dataOfJSON: \(dataOfJson)")
do {
let scryfallDecodeData = try JSONDecoder().decode([ScryfallCard].self, from: dataOfJson)
print("scryfallDecodeData.capacity:", scryfallDecodeData.capacity)
/* error here*/ DeckView().results = scryfallDecodeData
print("DeckView().results: ", DeckView().results)
print("Decoded data:", type(of: scryfallDecodeData))
} catch {
debugPrint("decode failed")
}
}
}
}
}
I keep getting a blank List this in the debugger...
if let dataOfJSON: 73545913 bytes
scryfallDecodeData.capacity: 24391
DeckView().results: []
Decoded data: Array<ScryfallCard>
Type of results:: Array<ScryfallCard>
results.capacity: 0
This means that oiver on the line marked Error Here, I'm asigning the decoded data to the DeckView().results var, but the end result is the data is not getting asigned. Any idea what I'm doing wrong?
You should not be creating a View from view model (ScryfallData), but instead return the decoded data from the parseBulkData function and assign that to results inside the onAppear of your View.
Your models should never know about your UI. Your UI (View in case of SwiftUI) should own the models, not the other way around. This achieves good separation of concerns and also makes your business logic platform and UI agnostic.
struct DeckView: View {
#State public var results = [ScryfallCard]()
var body: some View {
List(results, id: \.id ) { item in
Text(item.text)
}.onAppear {
self.results = ScryfallData().parseBulkData()
}
}
}
struct ScryfallData {
func parseBulkData() -> [ScryfallCard] {
let fm = FileManager.default
let path = Bundle.main.resourcePath
let items = try! fm.contentsOfDirectory(atPath: path!)
var oracleFileName = ""
for fileName in items {
if fileName .hasPrefix("oracle-cards"){
oracleFileName = fileName
}
}
if let savedJson = Bundle.main.url(forResource: oracleFileName, withExtension: "") {
do {
let jsonData = try Data(contentsOf: savedJson)
let scryfallDecodeData = try JSONDecoder().decode([ScryfallCard].self, from: jsonData)
return scryfallDecodeData
} catch {
debugPrint("decode failed")
return []
}
}
return []
}
}

SQLite.swift in SwiftUI app will not compile

I'm trying to read a database and copy the rows into an array in swiftUI but I don't know the correct syntax to append the array. Here is my databaseHelper.swift
import Foundation
import SQLite
class DatabaseHelper {
var database: Connection!
var path: String
let buttonsTable = "Button"
var db: String
var english: String
var categoryID: Int
var indonesian: String
init() {
do {
let path = Bundle.main.path(forResource: "sga", ofType: "db")!
let database = try Connection(path, readonly: true)
self.database = database
print("Database initialized at path \(path)")
} catch {
print("error")
}
}
func queryDatabase() -> [ButtonData] {
var buttonVars = [ButtonData]()
do {
let buttons = try self.database.prepare(self.buttonsTable)
for user in buttons {
// print("English: \(user[self.english]), ID: \(user[self.ID]), Indonesian: \(user[self.indonesian])")
buttonVars.append(ButtonData(english: row[english], categoryID: row[categoryID], indonesian: row[indonesian]))
}
} catch {
print(error)
}
return buttonVars
}
}
struct ButtonData: Hashable {
let english: String
let categoryID: Int
let indonesian: String
}
I'm not sure what is really happening in append statement. I want to append columns in the row but it doesn't accept anything.
The error message I am getting is...
"Cannot subscript a value of type 'Statement.Element' (aka 'Array>') with an argument of type 'String'
Thanks in advance :)
buttonTable is a sqlite.swift table and needs to be more than a string.
At some point you need to define your table like this:
static let buttonsTable = Table("Button")
static let english = Expression<String>("english")
static let indonesian = Expression<String>("indonesian")
static let categoryID = Expression<Int>("categoryID")
Assuming your db already exists, you should then be able to access it.
This section looks correct:
init() {
do {
let path = Bundle.main.path(forResource: "sga", ofType: "db")!
let database = try Connection(path, readonly: true)
self.database = database
print("Database initialized at path \(path)")
} catch {
print("error")
}
}
But then you would access it like this:
func queryDatabase() -> [ButtonData] {
var buttonVars = [ButtonData]()
do {
let buttons = try self.database.prepare(self.buttonsTable)
for row in buttons {
buttonVars.append(ButtonData(english: row[english], categoryID: row[categoryID], indonesian: row[indonesian]))
}
} catch {
print(error)
}
return buttonVars
}
I'll often put my return stuff in a map, like this:
func queryDatabase() -> [ButtonData] {
do {
return try self.database.prepare(self.buttonsTable).compactMap { row in
ButtonData(english: row[english], categoryID: row[categoryID], indonesian: row[indonesian]))
}
} catch {
print(error)
}
// Return an empty array if we caught an error
return []
}

RealmDB does not get populated

New to Realm... so hopefully a simple fix!
I have a data object of:
class GasFile : Object {
#objc dynamic var gasFilename : String = ""
// #objc dynamic var gasCategory : String? = ""
}
In my ViewController I have the following function:
func PopulateRealmWithFilenames() {
let fm = FileManager.default
var path = Bundle.main.resourcePath!
path += "/NBTFiles"
//let items = try! fm.contentsOfDirectory(atPath: path)
let items : [String] = try! fm.subpathsOfDirectory(atPath: path)
for item in items {
do {
print("item for realm is: \(item)")
try self.realm.write {
let newGasFile = GasFile()
newGasFile.gasFilename.append(item)
print("newGasFile written ok")
}
} catch {
print("Error writing new item to Realm \(error)")
}
}
}
This sets up the RealmDB ok, but never gets populated.
The aim of the function is to save all the filenames (not paths) of the files stored in a folder called NBTFiles.
I get the 'newGasfile written ok printed out' so it is getting into the loop.
datatype error ?
(I know I could just put it into an Array, but I want to use realm)
how do you want to add new object to realm without using adding function ?
you should use realm.add(object: newGasFile)
try this
func PopulateRealmWithFilenames() {
let fm = FileManager.default
var path = Bundle.main.resourcePath!
path += "/NBTFiles"
//let items = try! fm.contentsOfDirectory(atPath: path)
let items : [String] = try! fm.subpathsOfDirectory(atPath: path)
for item in items {
do {
print("item for realm is: \(item)")
try self.realm.write {
let newGasFile = GasFile()
newGasFile.gasFilename.append(item)
realm.add(object: newGasFile)
print("newGasFile written ok")
}
} catch {
print("Error writing new item to Realm \(error)")
}
}
}