I'm building a document application and I want to offer a lot of export options, most of them just different forms of text file, plane text, markdown, json etc.
I have a small sample app, see code, that works, allowing me to export some text as plane text to a .txt file, or as markdown as a .md file. But that's just 2 of the 5-10 formats I want to support and this already has a lot of repetitive code in it.
This is my .info tab:
So, my question is, how can I reduce this to idealy just one .fileExporter( call and still have the different file extensions on the exported files.
import SwiftUI
import UniformTypeIdentifiers
struct ContentView: View {
#State private var showingTXTExporter = false
#State private var showingMDExporter = false
#State var txtDocument = txtFile()
#State var mdDocument = mdFile()
var body: some View {
VStack {
Text("Save as TXT file!")
.padding().onTapGesture {
txtDocument.setText(text: "Title \n Body of the message. \n")
showingTXTExporter.toggle()
}.fileExporter(isPresented: $showingTXTExporter, document: txtDocument, contentType: .txtFile, defaultFilename: "email as text") { result in
switch result {
case .success(let url):
print("Saved to \(url)")
case .failure(let error):
print(error.localizedDescription)
}
}
Text("Save as MD file!")
.padding().onTapGesture {
mdDocument.setText(text: "# Title \n Body of the message. \n")
showingMDExporter.toggle()
}.fileExporter(isPresented: $showingMDExporter, document: mdDocument, contentType: .mdFile, defaultFilename: "email as markdown") { result in
switch result {
case .success(let url):
print("Saved to \(url)")
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
}
extension UTType {
static let txtFile = UTType(exportedAs: "com.myappurl.txt")
static let mdFile = UTType(exportedAs: "com.myappurl.md")
}
// Setting up my txt document type
struct txtFile: FileDocument {
// tell the system we support only plain text
static var readableContentTypes = [UTType.txtFile]
// by default our document is empty
var text = ""
// a simple initializer that creates new, empty documents
init(initialText: String = "") {
text = initialText
}
mutating func setText(text: String) {
self.text = text
}
// 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 our data to disk
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = Data(text.utf8)
return FileWrapper(regularFileWithContents: data)
}
}
// Setting up my markdown document
// this feels very wasteful, there must be a better way!
struct mdFile: FileDocument {
// tell the system we support only plain text
static var readableContentTypes = [UTType.mdFile]
// by default our document is empty
var text = ""
// a simple initializer that creates new, empty documents
init(initialText: String = "") {
text = initialText
}
mutating func setText(text: String) {
self.text = text
}
// 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 our data to disk
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = Data(text.utf8)
return FileWrapper(regularFileWithContents: data)
}
}
Related
How to use downloaded URL from getData class in AsyncImage?
struct RecentItemsView: View {
var item: dataType // var's from getData class
var body: some View {
HStack(spacing: 15) {
AsyncImage(url: URL(string: item.pic), content: { image in // item.pic here
image.resizable()
}, placeholder: {
ProgressView()
})
I have full URL from downloadURL but when Im using item.pic parameter in AsyncImage I get error: (See Image)
I understand that the error contains the path to the image, which is not suitable for AsyncImage, that's why I downloaded the full URL, the question is how to use the received URL in AsyncImage?
class getData : ObservableObject {
#Published var datas = [dataType]()
init() {
let db = Firestore.firestore()
db.collection("items").getDocuments { (snap, err) in
if err != nil {
print((err?.localizedDescription)!)
return
}
for i in snap!.documents {
let id = i.documentID
let title = i.get("title") as! String
let description = i.get("description") as! String
let pic = i.get("pic") as! String
self.datas.append(dataType(id: id, title: title, description: description, pic: pic))
let storage = Storage.storage()
let storageRef = storage.reference().child("\(pic)")
storageRef.downloadURL { url, error in
if let error = error {
print("Failed to download url:", error)
return
} else {
print(url!) // Full Url- https://firebasestorage.googleapis.com:...
}
}
}
}
}
}
struct dataType : Identifiable {
var id = UUID().uuidString
var title : String
var description : String
var pic : String
}
Error:
Storage:
Firestore:
This is going to look quite a bit different from your current approach but give it a try, it will simplify your code overall.
Main differences are the use of async await and FirebaseFirestoreSwift.
I choose using async await/Concurrency because it provides a more linear approach to the code and I think resolves your issue about sharing the variable with all the objects.
This is what your ObservableObject will look like
//Keeps UI Updates on the main thread
#MainActor
//Classes and structs should always be uppercased
class GetData : ObservableObject {
#Published var datas = [DataType]()
private var task: Task<Void, Never>? = nil
init() {
task = Task{
do{
try await getData()
}catch{
//Ideally you should present this error
//to the users so they know that something has gone wrong
print(error)
}
}
}
deinit{
task?.cancel()
}
func getData() async throws {
let documentPath = "items"
let svc = FirebaseService()
//async await allows a more linear approach. You can get the images individually
var items : [DataType] = try await svc.retrieve(path: documentPath)
for (idx, item) in items.enumerated() {
//Check if your url is a full url
if !item.pic.localizedCaseInsensitiveContains("https"){
//If it isnt a full url get it from storage and replace the url
items[idx].pic = try await svc.getImageURL(imagePath: item.pic).absoluteString
//Optional update the object so you dont have to retrieve the
//The url each time.
try svc.update(path: documentPath, object: items[idx])
}
}
datas = items
}
}
and your struct should change to use #DocumentID.
//This is a much simpler solution to decoding
struct DataType : Identifiable, FirestoreProtocol {
#DocumentID var id : String?
//If you get decoding errors make these variables optional by adding a ?
var title : String
var description : String
var pic : String
}
Your Views can now be modified to use the updated variables.
#available(iOS 15.0, *)
public struct DataTypeListView: View{
#StateObject var vm: GetData = .init()
public init(){}
public var body: some View{
List(vm.datas){ data in
DataTypeView(data: data)
}
}
}
#available(iOS 15.0, *)
struct DataTypeView: View{
let data: DataType
var body: some View{
HStack{
Text(data.title)
AsyncImage(url: URL(string: data.pic), content: { phase in
switch phase{
case .success(let image):
image
.resizable()
.scaledToFit()
.frame(width: 50, height: 50)
case .failure(let error):
Image(systemName: "rectangle.fill")
.onAppear(){
print(error)
}
case .empty:
Image(systemName: "rectangle.fill")
#unknown default:
Image(systemName: "rectangle.fill")
}
})
}
}
}
The class GetData is pretty bare bones an uses the code below to actually make the calls, I like using generics to simplify code and so it can be reused by various places.
You don't have to completely understand what is going on with this now but you should, I've put a ton of comments so it should be easy.
import FirebaseStorage
import FirebaseFirestore
import FirebaseFirestoreSwift
import SwiftUI
import FirebaseAuth
struct FirebaseService{
private let storage: Storage = .storage()
private let db: Firestore = .firestore()
///Retrieves the storage URL for an image path
func getImageURL(imagePath: String?) async throws -> URL{
guard let imagePath = imagePath else {
throw AppError.unknown("Invalid Image Path")
}
typealias PostContinuation = CheckedContinuation<URL, Error>
//Converts an completion handler approach to async await/concurrency
return try await withCheckedThrowingContinuation { (continuation: PostContinuation) in
storage.reference().child(imagePath).downloadURL { url, error in
if let error = error {
continuation.resume(throwing: error)
} else if let url = url {
continuation.resume(returning: url)
} else {
continuation.resume(throwing: AppError.unknown("Error getting image url"))
}
}
}
}
///Retireves the documetns from the Firestore and returns an array of objects
func retrieve<FC>(path: String) async throws -> [FC] where FC : FirestoreProtocol{
let snapshot = try await db.collection(path).getDocuments()
return snapshot.documents.compactMap { doc in
do{
return try doc.data(as: FC.self)
}catch{
//If you get any decoding errors adjust your struct, you will
//likely need optionals
print(error)
return nil
}
}
}
///Updates the provided document into the provided path
public func update<FC : FirestoreProtocol>(path: String, object: FC) throws{
guard let id = object.id else{
throw AppError.needValidId
}
try db.collection(path).document(id).setData(from: object)
}
}
enum AppError: LocalizedError{
case unknown(String)
case needValidId
}
protocol FirestoreProtocol: Identifiable, Codable{
///Use #DocumentID from FirestoreSwift
var id: String? {get set}
}
All of this code works, if you put all this code in a .swift file it will compile and it should work with your database.
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)
}
}
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.
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 []
}
I am new to SwiftUI and Swift . I got a Search Bar and a Listview whenever a user types something in the searchbar I do an http request and new data comes in . The issue is that the list is not updating with the new data and I think I know why . I need to pass my SearchBar response into the ObservedObject variable . I was reading this swiftui ObservedObject function call in all view however I still didn't find my answer . This is my code
struct RegistrationView: View {
#State private var searchTerm: String = ""
#State var txt = "" // Txt has the SearchBar text
#ObservedObject var getData = datas(location: "") // I need to pass it here
var body: some View {
VStack {
Text("Registration")
searchView(txt: $txt)
// datas(location: txt)
NavigationView {
List(getData.jsonData.filter{ txt == "" ? true : $0.name.localizedCaseInsensitiveContains(txt)}) { i in
ListRow(name: i.name,statelong: i.statelong)
}
}
.padding(.top, 5.0)
}
}
}
class datas: ObservableObject
{
#Published var jsonData = [datatype]()
init(location: String) {
let session = URLSession(configuration: .default)
if location == "" {
return
}
let parameter = "location=\(location)"
if location == "" {
return
}
let url = URL(string:"url")!
let request = RequestObject(AddToken: true, Url: url, Parameter: parameter)
session.dataTask(with:request, completionHandler: {(data, response, error) in
do
{
if data != nil
{
let fetch = try JSONDecoder().decode([datatype].self, from: data!)
DispatchQueue.main.async {
self.jsonData = fetch
print(fetch)
}
}
}
catch
{
print(error.localizedDescription)
}
}).resume()
}
}
In the above code I want to pass in the txt variable into the getData variable or do something like this #ObservedObject var getData = datas(location: txt) . When the SearchBar is updated then txt gets whatever is inserted into the SearchBar .
If I do something like this
#ObservedObject var getData = datas(location: "Me")
Then the list will update and correctly have everything that starts with Me my only issue is getting the SearchBar value inside datas so I don't have to hardcode things . As stated before I need to pass in txt to datas . Any help would be great
You don't need to init the class with that variable. You can just make a function for that and fetch it when ever you need. It could be just once.
class datas: ObservableObject {
#Published var jsonData = [datatype]()
func get(location: String) {
let session = URLSession(configuration: .default)
guard !location.isEmpty else { return }
let parameter = "location=\(location)"
let url = URL(string:"url")!
let request = RequestObject(AddToken: true, Url: url, Parameter: parameter)
session.dataTask(with:request, completionHandler: {(data, response, error) in
do {
guard data != nil else { return }
let fetch = try JSONDecoder().decode([datatype].self, from: data!)
DispatchQueue.main.async {
self.jsonData = fetch
print(fetch)
}
} catch {
print(error.localizedDescription)
}
}).resume()
}
}