Export Array using fileExporter - ios

I am loading in a json file and creating an array. When a button is clicked, additional data is inserted into the array. What I want to do is export the modified array to a file. So essentially the array that has new data inserted into it.
What I'm not sure about is whether it is possible when exporting data from an array? or maybe I am going about this the wrong way?
EDIT: I don't necessarily want to export a json file, that was just the file type I first tried. I would be happy to export text files or csv's
ContentView
import SwiftUI
import UniformTypeIdentifiers
struct ContentView: View {
#State private var name = ""
#FocusState private var nameIsFocused: Bool
#State var labels: [LabelData] = []
#State var index = 0
#State var saveFile = false
var body: some View {
HStack {
Button(action: {
index += 1
if index <= labels.count {
labels[index - 1]._label = "Yellow" }
}) {
Text("Y")
}
Button(action: {
saveFile.toggle()
//print(labels[index - 1])
}) {
Text("Export")
.frame(width: 100, height: 100)
.foregroundColor(Color(red: 0.362, green: 0.564, blue: 1))
.background(Color(red: 0.849, green: 0.849, blue: 0.849))
.clipShape(RoundedRectangle(cornerRadius: 25.0, style: .continuous))
}
.offset(x: 0, y: 0)
.fileExporter(isPresented: $saveFile, document: Doc(url: Bundle.main.path(forResource: "labeldata", ofType: "json")!), contentType: .json) { (res) in
do {
let fileUrl = try res.get()
print(fileUrl)
}
catch {
print("cannot save doc")
print(error.localizedDescription)
}
}
}
VStack{
VStack {
if index < labels.count{
if let test = labels[index] {
Text(test._name)
}}}
.offset(x: 0, y: -250)
.frame(
minWidth: 0,
maxWidth: 325
)
VStack {
if index < labels.count{
if let test = labels[index] {
Text(test._name)
}}}
.offset(x: 0, y: -150)
.frame(
minWidth: 0,
maxWidth: 325
)
VStack {
if index < labels.count{
if let test = labels[index] {
Text(test._label)
}}}
.offset(x: 0, y: -50)
.frame(
minWidth: 0,
maxWidth: 325
)
}
.onAppear {
labels = load("labeldata.json")
}
}
}
struct Doc : FileDocument {
var url : String
static var readableContentTypes: [UTType]{[.json]}
init(url : String) {
self.url = url
}
init(configuration: ReadConfiguration) throws {
url = ""
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let file = try! FileWrapper(url: URL(fileURLWithPath: url), options: .immediate)
return file
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
LabelData
import Foundation
struct LabelData: Codable {
var _id: Int
var _name: String
var _type: String
var _description: String
var _label: String
}
labeldata.json
[
{
"_id" : 1,
"_name" : "Label1",
"_type" : "type1",
"_description" : "description1",
"_label" : ""
},
{
"_id" : 2,
"_name" : "Label2",
"_type" : "type2",
"_description" : "description2",
"_label" : ""
}
]
DataLoader
import Foundation
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}

The fileExporter writes in-memory data to location selected by a user, so we need to create document with our content and generate FileWrapper from content to exported data (CSV in this example).
So main parts, at first, exporter:
.fileExporter(isPresented: $saveFile,
document: Doc(content: labels), // << document from content !!
contentType: .plainText) {
and at second, document:
struct Doc: FileDocument {
static var readableContentTypes: [UTType] { [.plainText] }
private var content: [LabelData]
init(content: [LabelData]) {
self.content = content
}
// ...
// simple wrapper, w/o WriteConfiguration multi types or
// existing file selected handling (it is up to you)
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let text = content.reduce("") {
$0 + "\($1._id),\($1._name),\($1._type),\($1._description),\($1._label)\n"
}
return FileWrapper(regularFileWithContents:
text.data(using: .utf8) ?? Data()) // << here !!
}
Tested with Xcode 13.4 / iOS 15.5
Test module is here

It sounds like you want to create a new JSON file from the modified data within the array. It is a bit unusual to want to create a new JSON file. Maybe you want to persist the data? In that case you wouldn't save it as JSON you would persist it with a proper DB (DataBase) like CoreData, FireBase, Realm, or ect...
But if you really want to do this. Then you need to create a new JSON file from the data in the array. You have a load<T: Decodable> function but you are going to want a save<T: Codable> function. Most people would, once again, use this opportunity to save the data to a DB.
This guys does a pretty good job explaining this: Save json to CoreData as String and use the String to create array of objects
So here is a good example of saving JSON data to a file:
let jsonString = "{\"location\": \"the moon\"}"
if let documentDirectory = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first {
let pathWithFilename = documentDirectory.appendingPathComponent("myJsonString.json")
do {
try jsonString.write(to: pathWithFilename,
atomically: true,
encoding: .utf8)
} catch {
// Handle error
}
}

Related

Swift JSON Parsing issue - Possible problem with struct?

Upon login and validation, the user is sent to the main application page. I have the following code set.
import SwiftUI
typealias MyDefendant = [Defendant]
struct ContentView: View {
var email: String
#State var myDefendant: MyDefendant = []
func getUserData(completion:#escaping (MyDefendant)->()) {
var urlRequest = URLRequest(url: URL(string: "https://milanobailbonds.com/getDefendant.php")!)
urlRequest.httpMethod = "post"
let authData = [
"defEmail" : email
] as [String : Any]
do {
let authBody = try JSONSerialization.data(withJSONObject: authData, options: .prettyPrinted)
urlRequest.httpBody = authBody
urlRequest.addValue("application/json", forHTTPHeaderField: "content-type")
} catch let error {
debugPrint(error.localizedDescription)
}
URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if let error = error {
print(error.localizedDescription)
return
}
guard let data = data else {
return
}
do {
let responseString = String(data: data, encoding: .utf8)!
print(responseString)
var returnValue: MyDefendant?
let decoder = JSONDecoder()
returnValue = try decoder.decode([Defendant].self, from: data)
completion(returnValue!)
}
catch { fatalError("Couldn't Parse")
}
}.resume()
return
}
var body: some View {
NavigationView {
VStack {
Text(email)
Text("I Need Bail")
.font(.largeTitle)
.fontWeight(.semibold)
Button {
print("Test")
} label: {
Label("I Need Bail", systemImage: "iphone.homebutton.radiowaves.left.and.right")
.labelStyle(IconOnlyLabelStyle())
.font(.system(size: 142.0))
}
} .foregroundColor(.green)
.shadow(color: .black, radius: 2, x: 2, y: 2)
.navigationBarTitle("Home")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading:
Button {
print("Test")
} label: {
Label("I Need Bail", systemImage: "line.3.horizontal")
.labelStyle(IconOnlyLabelStyle())
})
}.onAppear() {
getUserData() { myDefendant in
self.myDefendant = myDefendant
}
}
}
}
In my data models I have created a struct for Defendant as such:
struct Defendant: Codable, Hashable, Identifiable {
var id: Int
var defImage: String
var defName: String
var defAddress: String
var defCity: String
var defState: String
var defZip: String
var defPhone: String
var defEmail: String
var defUserName: String
var defPW: String
var defDOB: String
var defPriorFTA: Int
var defFTAExplained: String
var defAssignedAgency: Int
} // Defendant Model
The PHP is working fine and returning valid JSON with all of the required items for the struct.
"\"[\\n {\\n \\\"Id\\\": 5,\\n \\\"defImage\\\": \\\"\\\",\\n \\\"defName\\\": \\\"Some Dude\\\",\\n \\\"defAddress\\\": \\\"123 Main St\\\",\\n \\\"defCity\\\": \\\"Some City\\\",\\n \\\"defState\\\": \\\"FL\\\",\\n \\\"defZip\\\": \\\"12345\\\",\\n \\\"defPhone\\\": \\\"888-888-8888\\\",\\n \\\"defEmail\\\": \\\"someone#someone.com\\\",\\n \\\"defUserName\\\": \\\"\\\",\\n \\\"defPW\\\": \\\"91492cffa4032765f6b025ec6b2c873e49fe5e58\\\",\\n \\\"defDOB\\\": \\\"01\\\\\\/01\\\\\\/1955\\\",\\n \\\"defPriorFTA\\\": 0,\\n \\\"defFTAExplained\\\": \\\"\\\",\\n \\\"defAssignedAgency\\\": 0\\n }\\n]\""
Unfortunately, I keep getting an error "Unable to Parse".
I'm new to Swift, and coding in general.
Any thoughts or ideas are greatly appreciated.
Thank you
Im not sure but you can try to change id into Id in your struct. Please remember name of your struct property must exactly the same with key in Json response.

iOS fileImporter "No such file or directory"

I am trying to get users to import files from elsewhere in their iPad into my app using file importer. I am able to show the file browser dialog to choose the files. I am trying to copy the files into the app's cache before proceeding, and receive the error "no such file". I have extracted the code into a simple app to reproduce the errorand included it below. This code works as expected when I made a MacOS version. But the same code in an iOS app produces the error that there is no such file in the iPad (device attached).
Please advise as to where the error maybe.
import SwiftUI
import UniformTypeIdentifiers
struct ContentView: View {
#State var files : [String] = []
var body: some View {
VStack(alignment: .leading, spacing:0) {
FileImportButton(filePaths: $files)
.frame(width: 100, height: 100)
.overlay(Rectangle().stroke(lineWidth: 2))
Text(fileNames())
}
.frame(minWidth: 200, minHeight: 200)
}
func fileNames() -> String {
var out = ""
for name in files {
out = "\(out)\n\(name)"
}
return out
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct FileImportButton: View {
#Binding var filePaths : [String]
var types : [UTType] = [.image,.text,.rtf,.audio,.video,.url]
#State var showFileImport = false
var body: some View {
Button("Add\nAssets") {
showFileImport = true
}
.fileImporter(isPresented: $showFileImport, allowedContentTypes: types,
allowsMultipleSelection: true) { result in
do {
let urls = try result.get()
for url in urls {
if url.startAccessingSecurityScopedResource() {
let pathName = url.lastPathComponent
let folderURLs = FileManager.default.urls(
for: .cachesDirectory,
in: .userDomainMask
)
let toUrl = folderURLs[0].appendingPathComponent(pathName)
let data = try Data(contentsOf: url)
try data.write(to: toUrl, options: .atomic)
filePaths.append(pathName)
}
}
} catch {
print(error)
}
}
}
}

Firestore data is not displayed in listview. Identifiable not set up correctly?

I'm trying to fetch some data from a firebase document and have it displayed in a ListView. Getting the data and decoding it into my own struct works fine (tested by letting the console print it), but it won't show up in the actual ListView. I tried to create 2 example datasets to see if there is anything wrong with the implementation of my struct and/or ListView, but those display just fine.
My best guess is that the ListView isnt happy with the id I get from the firebase document.
Here is how I fetch the data(pls ignore the ugly solution in checkAvailablePkgs() for now, that is going to change):
class firebaseController : ObservableObject {
let databaseFIRB = Firestore.firestore()
#Published var forUserAvailablePkgs : [DLPackage] = []
func checkAvailablePkgs() {
if Auth.auth().currentUser != nil {
var allAccessiblePkgs : [String] = []
let userUID = Auth.auth().currentUser!.uid
databaseFIRB.collection("user-access").document(userUID).getDocument { (document, error) in
if let document = document, document.exists {
let dataDescription = document.data().map(String.init(describing:)) ?? "nil"
let pkgArr = dataDescription.components(separatedBy: ", ")
for partString in pkgArr {
var tmpStringSlicing : String
tmpStringSlicing = partString.replacingOccurrences(of: "\": <null>", with: "")
tmpStringSlicing = tmpStringSlicing.replacingOccurrences(of: "[\\[\\]\"]", with: "", options: .regularExpression)
allAccessiblePkgs.append(tmpStringSlicing)
}
self.checkIndividualPkgInformation(pkgIDs: allAccessiblePkgs)
} else {
print("Document does not exist")
}
}
}
}
func checkIndividualPkgInformation(pkgIDs: [String]) {
for id in pkgIDs {
databaseFIRB.collection("download-pkgs").document(id).getDocument { (document, error) in
if let document = document, document.exists {
let result = Result {
try document.data(as: DLPackage.self)
}
switch result {
case .success(let pkg):
if let pkg = pkg {
// A `Package` value was successfully initialized from the DocumentSnapshot.
print("Package: \(pkg)")
self.forUserAvailablePkgs.append(pkg)
} else {
// A nil value was successfully initialized from the DocumentSnapshot,
// or the DocumentSnapshot was nil.
print("Document does not exist")
}
case .failure(let error):
// A `City` value could not be initialized from the DocumentSnapshot.
print("Error decoding package: \(error)")
}
} else {
print("Package-Document does not exist")
}
}
}
}
}
This is the struct I want to save it as:
struct DLPackage: Identifiable, Codable {
#DocumentID var id = UUID().uuidString
var name : String = ""
var size : String = ""
var contentpaths : [String] = [""]
}
And here is the ListView that refuses to Display the aquired data:
struct FIRBauthView: View {
let firebaseCtrl: firebaseController = firebaseController()
var body: some View {
VStack() {
List(firebaseCtrl.forUserAvailablePkgs) {item in
HStack() {
Text(String(item.name)).font(.custom("Avenir", size: 26))
Spacer()
Text(String(item.size)).font(.custom("Avenir", size: 26))
}
}
Button {
firebaseCtrl.checkAvailablePkgs()
} label: {
Text("Check")
}
}
}
}
I can give my firebaseControler().forUserAvailablePkgs an initial value of:
forUserAvailablePkgs.append(contentsOf: [DLPackage(name: "Example", size: "1GB", contentpaths: ["hello","something"]),DLPackage(name: "Nr2", size: "1GB", contentpaths: ["hello","something else"])])
and they will show up just fine.
This is my print from the firebase data loaded into the forUserAvailablePkgs Array:
[Light_Player.DLPackage(_id: FirebaseFirestoreSwift.DocumentID<Swift.String>(value: Optional("CDB68E0C-DCE2-4FAA-AD55-872322A55E56")), name: "Example", size: "1GB", contentpaths: ["hello", "something"]),
Light_Player.DLPackage(_id: FirebaseFirestoreSwift.DocumentID<Swift.String>(value: Optional("DCAAF136-24FE-470A-B897-2D178AEDB3A0")), name: "Nr2", size: "1GB", contentpaths: ["hello", "something else"]),
Light_Player.DLPackage(_id: FirebaseFirestoreSwift.DocumentID<Swift.String>(value: Optional("package2")), name: "Bewährte Klassiker", size: "3.4GB", contentpaths: ["placeholder for another path", "placeholder for a different path"]),
Light_Player.DLPackage(_id: FirebaseFirestoreSwift.DocumentID<Swift.String>(value: Optional("package1")), name: "Basic", size: "7.3GB", contentpaths: ["placeholder for path", "placeholder for path 2"])]
I tried changing the id for the struct to:
#DocumentID var id: String?
since I found it in a tutorial online, however the code doesn't copile that way.
I also tried overwriting the ids from the firebase data with generated UUIDs after fetching the data from the database, but I still couldn't get the ListView to display it.
Any help is appreciated.
Well after a good night of sleep I was cured of my temporary blindness and found that I was missing the #ObservedObject property in my view.
struct FIRBauthView: View {
#ObservedObject var firebaseCtrl: firebaseController = firebaseController()
instead of
struct FIRBauthView: View {
let firebaseCtrl: firebaseController = firebaseController()
Now the data is showing up as intended.

Swift JSON Decoding: Displaying Results

I am trying to make a view that shows the stats of a bulb, I want to show if the device is on or off and what its brightness is. I already have an API that can return this information in JSON and also have a web GUI. But I want to make an app on my iPhone so I am very new to Swift so used this video to parse the JSON response from the API and print it to the console. I now don't know how to actually put the information I get into visible pieces of text. I will show you the JSON return I get and the code I have already done:
Parsed JSON
BulbInfo(error_code: 0, result: UITest.Result(device_on: true, brightness: 100))
API return JSON
{'error_code': 0,
'result': {
'device_id': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
'fw_ver': '1.1.9 Build 20210122 Rel. 56165',
'hw_ver': '1.0.0',
'type': 'SMART.TAPOBULB',
'model': 'L510 Series',
'mac': 'xx-xx-xx-xx-xx-xx',
'hw_id': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
'fw_id': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
'oem_id': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
'specs': 'EU',
'lang': 'en_US',
'device_on': True,
'on_time': 3065,
'overheated': False,
'nickname': 'TWFpbiBMaWdodA==',
'avatar': 'hang_lamp_1',
'brightness': 100,
'default_states': {
'brightness': {
'type': 'last_states',
'value': 100
}
},
'time_diff': 0,
'region': 'Europe/London',
'longitude': -xxxxx,
'latitude': xxxxxx,
'has_set_location_info': True,
'ip': '192.168.x.xxx',
'ssid': 'xxxxxxxxxxxx',
'signal_level': 1,
'rssi': -xx
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
func getDeviceInfo(){
let urlString = "http://192.168.x.xxx:xxx/get_bulb_info"
let url = URL(string:urlString)
let session = URLSession.shared
let dataTask = session.dataTask(with: url!){(data,response,error)in
// Check for error
if error == nil && data != nil {
// Parse JSON
let decoder = JSONDecoder()
do{
let bulbInfo = try decoder.decode(BulbInfo.self, from: data!)
print(bulbInfo)
}
catch{
print(error)
}
}
}
dataTask.resume()
}
var body: some View {
Text("Main Light:").padding()
Button(action:getDeviceInfo){
Text("Get Device Info!")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Bulb.swift
//
// Bulb.swift
// UITest
//
// Created by James Westhead on 18/12/2021.
//
import Foundation
struct BulbInfo: Codable{
var error_code: Int
var result: Result
}
struct Result: Codable{
var device_on:Bool
var brightness: Int
}
Add to ContentView
#State var bulbInfo: BulbInfo? = nil
Then remove the let from the do catch block
You can access the information by using something like
bulbInfo.result.device_on.description
or
bulbInfo.result.brightness.description
inside the Text

Trouble displaying data from Contentful in SwiftUI

I'm trying to display data from Contentful into my SwiftUI app but I'm hitting an issue.
The goal is to display a list of movies and have them tappable. When I select a movie I want to get the data for that movie, so the title & movie trailer for example.
But in my selectable row I'm getting Use of undeclared type 'movie' and in my movies View I'm getting Use of undeclared type 'fetcher'
Here's what I have tried below:
import SwiftUI
import Combine
import Contentful
struct Movie: Codable, Identifiable, FieldKeysQueryable, EntryDecodable {
static let contentTypeId: String = "movies"
// FlatResource Memberes.
let id: String
var updatedAt: Date?
var createdAt: Date?
var localeCode: String?
var title: String
var movieId: String
var movieTrailer: String
enum FieldKeys: String, CodingKey {
case title, movieId, movieTrailer
}
enum CodingKeys: String, CodingKey {
case id = "id"
case title = "title"
case movieId = "movieId"
case movieTrailer = "movieTrailer"
}
}
public class MovieFetcher: ObservableObject {
#Published var movies = [Movie]()
init() {
getArray(id: "movies") { (items) in
items.forEach { (item) in
self.movies.append(Movie(id: item.id, title: item.title, movieId: item.movieId, movieTrailer: item.movieTrailer))
}
}
}
func getArray(id: String, completion: #escaping([Movie]) -> ()) {
let client = Client(spaceId: spaceId, accessToken: accessToken, contentTypeClasses: [Movie.self])
let query = QueryOn<Movie>.where(contentTypeId: "movies")
client.fetchArray(of: Movie.self, matching: query) { (result: Result<ArrayResponse<Movie>>) in
switch result {
case .success(let array):
DispatchQueue.main.async {
completion(array.items)
}
case .error(let error):
print(error)
}
}
}
}
struct moviesView : View {
#ObservedObject var fetcher = MovieFetcher()
#State var selectMovie: Movie? = nil
#Binding var show: Bool
var body: some View {
HStack(alignment: .bottom) {
if show {
ScrollView(.horizontal) {
Spacer()
HStack(alignment: .bottom, spacing: 30) {
ForEach(fetcher.movies, id: \.self) { item in
selectableRow(movie: item, selectMovie: self.$selectMovie)
}
}
.frame(minWidth: 0, maxWidth: .infinity)
}
.padding(.leading, 46)
.padding(.bottom, 26)
}
}
}
}
struct selectableRow : View {
var movie: Movie
#Binding var selectedMovie: Movie?
#State var initialImage = UIImage()
var urlString = "\(urlBase)\(movie.movieId).png?"
var body: some View {
ZStack(alignment: .center) {
if movie == selectedMovie {
Image("")
.resizable()
.frame(width: 187, height: 254)
.overlay(
RoundedRectangle(cornerRadius: 13)
Image(uiImage: initialImage)
.resizable()
.cornerRadius(13.0)
.frame(width: 182, height: 249)
.onAppear {
guard let url = URL(string: self.urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
guard let image = UIImage(data: data) else { return }
RunLoop.main.perform {
self.initialImage = image
}
}.resume()
}
} else {
Image(uiImage: initialImage)
.resizable()
.cornerRadius(13.0)
.frame(width: 135, height: 179)
.onAppear {
guard let url = URL(string: self.urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
guard let image = UIImage(data: data) else { return }
RunLoop.main.perform {
self.initialImage = image
}
}.resume()
}
}
}
.onTapGesture {
self.selectedMovie = self.movie
}
}
}
I suppose it was intended
struct moviesView : View {
#ObservedObject var fetcher = MovieFetcher()
#State var selectMovie: Movie? = nil // type is Movie !!
...
and here
struct selectableRow : View {
var movie: Movie
#Binding var selectedMovie: Movie? // type is Movie !!
The good practice is to use Capitalized names for types and lowerCased types for variables/properties, following this neither you nor compiler be confused.

Resources