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)
}
}
}
}
Related
I am new to Swift and have been using the new PhotosPicker in SwiftUI 4.0 . I am able to display selected images but not video . When I select a video I can get a few pieces of information like this below
PhotosPickerItem(_itemIdentifier: Optional("40F724uF-24M7-4523-9B2B-AD43FB2C7D71/L0/001"), _shouldExposeItemIdentifier: false, _supportedContentTypes: [<_UTCoreType 0x106c4cd60> com.apple.quicktime-movie (not dynamic, declared)], _itemProvider: <PUPhotosFileProviderItemProvider: 0x600003ea05a0> {types = (
"com.apple.quicktime-movie"
)})
I am wondering if there is somehow that I can use that item identifier to load the video selected . I am been looking at different examples but none of them show videos for PhotosPicker . I have a very small project that I am testing this on, any suggestions would be great
import SwiftUI
import Combine
import PhotosUI
import AVKit
struct PlayVideoView: View {
#State private var selectedItem: [PhotosPickerItem] = []
#State private var data: Data?
#State var player = AVPlayer(url: URL(string: "https://swiftanytime-content.s3.ap-south-1.amazonaws.com/SwiftUI-Beginner/Video-Player/iMacAdvertisement.mp4")!)
var body: some View {
PhotosPicker(selection: $selectedItem,
maxSelectionCount: 1,
matching: .any(of: [.images,.videos])) {
Image(systemName: "photo")
.resizable()
.foregroundColor(.blue)
.frame(width: 24, height: 24)
.padding(.top, 5.0)
}.onChange(of: selectedItem) { newMedia in
Task {
guard let item = selectedItem.first else {
return
}
item.loadTransferable(type: Data.self) { result in
switch result {
case .success(let data):
if let data = data {
print(item) // get video url here and display in videoplayer
self.data = data
} else {
print("data is nil")
}
case .failure(let failure):
fatalError("\(failure)")
}
}
}
}
VideoPlayer(player: player) // selected video url should go here
.frame(width: 400, height: 300, alignment: .center)
}
}
struct PlayVideoView_Previews: PreviewProvider {
static var previews: some View {
PlayVideoView()
}
}
You could try this approach, using the code from https://github.com/zunda-pixel/SamplePhotosPicker.
The Movie code replicated here, uses the TransferRepresentation to represent
a url from the temp file.
import Foundation
import SwiftUI
import PhotosUI
import AVKit
import CoreTransferable
// from: https://github.com/zunda-pixel/SamplePhotosPicker
struct Movie: Transferable {
let url: URL
static var transferRepresentation: some TransferRepresentation {
FileRepresentation(contentType: .movie) { movie in
SentTransferredFile(movie.url)
} importing: { receivedData in
let fileName = receivedData.file.lastPathComponent
let copy: URL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName)
if FileManager.default.fileExists(atPath: copy.path) {
try FileManager.default.removeItem(at: copy)
}
try FileManager.default.copyItem(at: receivedData.file, to: copy)
return .init(url: copy)
}
}
}
struct ContentView: View {
var body: some View {
PlayVideoView()
}
}
struct PlayVideoView: View {
#State private var selectedItem: [PhotosPickerItem] = []
#State var player = AVPlayer(url: URL(string: "https://swiftanytime-content.s3.ap-south-1.amazonaws.com/SwiftUI-Beginner/Video-Player/iMacAdvertisement.mp4")!)
var body: some View {
PhotosPicker(selection: $selectedItem,
maxSelectionCount: 1,
matching: .any(of: [.images,.videos])) {
Image(systemName: "photo")
.resizable()
.foregroundColor(.blue)
.frame(width: 24, height: 24)
.padding(.top, 5.0)
}.onChange(of: selectedItem) { newMedia in
Task {
guard let item = selectedItem.first else { return }
item.loadTransferable(type: Movie.self) { result in // <-- here
switch result {
case .success(let movie):
if let movie = movie {
player = AVPlayer(url: movie.url) // <-- here
} else {
print("movie is nil")
}
case .failure(let failure):
fatalError("\(failure)")
}
}
}
}
VideoPlayer(player: player)
.frame(width: 400, height: 300, alignment: .center)
}
}
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
}
}
I am attempting to build a basic SwiftUI weather app. The app allows the user to search weather by city name, using the OpenWeatherMap API. I configured the inputted city name from the text field to be injected into name: "" in WeatherModel, inside the fetchWeather() function in the viewModel. I then configured the OpenWeatherMap URL string to take in searchedCity.name as a parameter (see viewModel below). This setup seems to work fine, as I am able to search for weather by city name. However, I want to seek feedback as to whether or not the practice of passing searchCity.name directly into the URL (in the viewModel) is correct. In regards to:
let searchedCity = WeatherModel(...
... I am not sure what to do with the CurrentWeather and WeatherInfo inside that instance of WeatherModel. Since I'm only using "searchedCity" to pass the name of the city into the URL, how should "CurrentWeather.init(temp: 123.00)" and "weather: [WeatherInfo.init(description: "")]" be set? Is it correct to implement values for temp and description, such as 123 and ""?
Here is my full code below:
ContentView
import SwiftUI
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
// #State private var textField = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter City Name", text: $viewModel.enterCityName).textFieldStyle(.roundedBorder)
Button(action: {
viewModel.fetchWeather()
viewModel.enterCityName = ""
}, label: {
Text("Search")
.padding(10)
.background(Color.green)
.foregroundColor(Color.white)
.cornerRadius(10)
})
Text(viewModel.title)
.font(.system(size: 32))
Text(viewModel.temp)
.font(.system(size: 44))
Text(viewModel.descriptionText)
.font(.system(size: 24))
Spacer()
}
.navigationTitle("Weather MVVM")
}.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Model
import Foundation
// Data, Model should mirror the JSON layout
//Codable is the property needed to convert JSON into a struct
struct WeatherModel: Codable {
let name: String
let main: CurrentWeather
let weather: [WeatherInfo]
}
struct CurrentWeather: Codable {
let temp: Float
}
struct WeatherInfo: Codable {
let description: String
}
ViewModel
import Foundation
class WeatherViewModel: ObservableObject {
//everytime these properties are updated, any view holding onto an instance of this viewModel will go ahead and updated the respective UI
#Published var title: String = "-"
#Published var temp: String = "-"
#Published var descriptionText: String = "-"
#Published var enterCityName: String = ""
init() {
fetchWeather()
}
func fetchWeather() {
let searchedCity = WeatherModel(name: enterCityName, main: CurrentWeather.init(temp: 123.00), weather: [WeatherInfo.init(description: "")])
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(searchedCity.name)&units=imperial&appid=<myAPIKey>") else {
return
}
let task = URLSession.shared.dataTask(with: url) { data, _, error in
// get data
guard let data = data, error == nil else {
return
}
//convert data to model
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.title = model.name
self.temp = "\(model.main.temp)"
self.descriptionText = model.weather.first?.description ?? "No Description"
}
}
catch {
print(error)
}
}
task.resume()
}
}
There are many ways to do what you ask, the following code is just one approach. Since you only need the city name to get the result, just use only that in the url string. Also using your WeatherModel in the WeatherViewModel avoids duplicating the data into various intermediate variables.
PS: do not post your secret appid key in your url.
import Foundation
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = "" // <-- use this to get the city name
var body: some View {
NavigationView {
VStack {
TextField("Enter City Name", text: $cityName).textFieldStyle(.roundedBorder)
Button(action: {
viewModel.fetchWeather(for: cityName) // <-- let the model fetch the results
cityName = ""
}, label: {
Text("Search")
.padding(10)
.background(Color.green)
.foregroundColor(Color.white)
.cornerRadius(10)
})
// --- display the results ---
Text(viewModel.cityWeather.name).font(.system(size: 32))
Text("\(viewModel.cityWeather.main.temp)").font(.system(size: 44))
Text(viewModel.cityWeather.firstWeatherInfo()).font(.system(size: 24))
Spacer()
}
.navigationTitle("Weather MVVM")
}.navigationViewStyle(.stack)
}
}
class WeatherViewModel: ObservableObject {
// use your WeatherModel that you get from the fetch results
#Published var cityWeather: WeatherModel = WeatherModel()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=YOURKEY") else { return }
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else { return }
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityWeather = model
}
}
catch {
print(error) // <-- need to deal with errors here
}
}
task.resume()
}
}
struct WeatherModel: Codable {
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Float = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}
I want to seek feedback as to whether or not the practice of passing searchCity.name directly into the URL (in the viewModel) is correct.e
You should alway avoid to pass fake values to an object/class like Int(123).
Instead you should use nullable structures or classes.
I don't see the need of create a whole WeatherModel instance just to read one property from it, one property that you already have in a viewmodel's enterCityName property. Just use the viewmodel's enterCityName property instead.
I am trying import a JSON file onto my app and I trying to read the name so that I can use that string to display the JSON onto my app. I have the file importer working but I need help on saving that name of the file as a String.
import SwiftUI
struct ContentView: View {
#State var imported = false
var body: some View {
VStack{
Button(action: {imported.toggle()}, label: {
Text("Import")
})
}
.fileImporter(isPresented: $imported, allowedContentTypes: [.json]) { (res) in
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
try something like this:
struct ContentView: View {
#State var imported = false
#State var fileName = ""
var body: some View {
VStack (spacing: 30) {
Button(action: {imported.toggle()}, label: {
Text("Import")
})
Text("file selected is \(fileName)") // <--- the file name you selected
}
.fileImporter(isPresented: $imported, allowedContentTypes: [.json]) { res in
do {
let fileUrl = try res.get()
self.fileName = fileUrl.lastPathComponent // <--- the file name you want
let fileData = try Data(contentsOf: fileUrl)
// try! JSONDecoder().decode([YourObject].self, from: fileData)
} catch{
print ("error reading: \(error.localizedDescription)")
}
}
}
}
I'm trying to get the data from a Rest Api to download and render in a list view in SwiftUI.
I think I manage to get the JSON to download and assign to all the relevant Structs correctly but nothing displays in the list view on the simulator when I go to build it.
I'm not even sure I need to have the 'Enum CodingKeys in there.
Can anyone point out where I may be going wrong?
import Foundation
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var fetcher = LaunchDataFetcher()
var body: some View {
VStack {
List(fetcher.launches) { launch in
VStack (alignment: .leading) {
Text(launch.mission_name)
Text(launch.details)
.font(.system(size: 11))
.foregroundColor(Color.gray)
}
}
}
}
}
public class LaunchDataFetcher: ObservableObject {
#Published var launches = [launch]()
init(){
load()
}
func load() {
let url = URL(string: "https://api.spacexdata.com/v3/launches")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([launch].self, from: d)
DispatchQueue.main.async {
self.launches = decodedLists
}
}else {
print("No Data")
}
} catch {
print ("Error")
}
}.resume()
}
}
struct launch: Codable {
public var flight_number: Int
public var mission_name: String
public var details: String
enum CodingKeys: String, CodingKey {
case flight_number = "flight_number"
case mission_name = "mission_name"
case details = "details"
}
}
// Now conform to Identifiable
extension launch: Identifiable {
var id: Int { return flight_number }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
First of all, I try to find which error of your code and recognize the launch.details of response data somewhere have null. So that I just mark this property Optional and it work.
For more details, you can refer below code :
struct launch: Codable {
public var flight_number: Int
public var mission_name: String
public var details: String?
enum CodingKeys: String, CodingKey {
case flight_number = "flight_number"
case mission_name = "mission_name"
case details = "details"
}
}
At catch line, get the error message to know exactly what's happen
func load() {
let url = URL(string: "https://api.spacexdata.com/v3/launches")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([launch].self, from: d)
DispatchQueue.main.async {
print(decodedLists)
self.launches = decodedLists
}
}else {
print("No Data")
}
} catch let parsingError {
print ("Error", parsingError)
}
}.resume()
}
Hope this help!
Any messages in console? I think you need to add NSAppTransportSecurity>NSAllowsArbitraryLoads>YES in your .plist
Transport security has blocked a cleartext HTTP
With the help from Trai Nguyen, I managed to get the simulator to display the data.
All I was missing was to ensure the variable named 'details' had an optional property (by adding on the "?") - like so:
public var details: String?
and then making sure when I render the text in my view, I had to give it a value to insert if the return is null - like so:
Text(launch.details ?? "No Data Found")
Here is the complete code that works for me:
import Foundation
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var fetcher = LaunchDataFetcher()
var body: some View {
VStack {
List(fetcher.launches) { launch in
VStack (alignment: .leading) {
Text(launch.mission_name)
Text(launch.details ?? "No Data Found")
.font(.system(size: 11))
.foregroundColor(Color.gray)
}
}
}
}
}
public class LaunchDataFetcher: ObservableObject {
#Published var launches = [launch]()
init(){
load()
}
func load() {
let url = URL(string: "https://api.spacexdata.com/v3/launches")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([launch].self, from: d)
DispatchQueue.main.async {
print(decodedLists)
self.launches = decodedLists
}
}else {
print("No Data")
}
} catch let parsingError {
print ("Error", parsingError)
}
}.resume()
}
}
struct launch: Codable {
public var flight_number: Int
public var mission_name: String
public var details: String?
}
/// Now conform to Identifiable
extension launch: Identifiable {
var id: Int { return flight_number }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}