How to run a function inside a body of SWIFT UI? - ios

i'm trying to run a function inside a body. i'm not familiar how functions work in swiftUI.
here's the code below.
struct HomeView: View {
func getDirectory() -> String{
let fm = FileManager.default
let path = Bundle.main.resourcePath!
do {
let items = try fm.contentsOfDirectory(atPath: path)
for item in items {
print("Found \(item)")
}
} catch {
// failed to read directory – bad permissions, perhaps?
}
return path
}
let documentURL = Bundle.main.url(forResource: resourceURL, withExtension: "pdf")!
var body: some View {
let path = getDirectory()
print(path)
}
}
i'm getting an error saying.
''Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type''
how can i make this work?

I can't tell what you're trying to get from this document, but to answer your question directly, you need to provide a View within the body, not a String. You can add a Text that accepts a String parameter:
var body: some View {
Text(getDirectory())
}

var body: some View {
let noView: EmptyView = {
/*
Do what you want here
*/
return EmptyView()
}()
noView
}

Related

How to pass data from ViewModel to another ViewModel

I have two viewmodels, one contains the necessary data for the user, a classic api fetch, and the second viewmodel is also an api fetch, but I want to insert the data from the first into the second, whenever I try, it always outputs nil, although before that I ensure that it cannot output it because I know it exists because the fetch function was called first.
class UserPostViewModel: ObservableObject {
#Published var user: UserResponse?
#Published var phoneNumber:String = ""
#Published var firstName:String = ""
#Published var lastName:String = ""
#Published var selectedImage: UIImage?
#Published var normalizedMobileNumberField:String = ""
func createUser() {
guard let url = URL(string: "privateapi") else {
return
}
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .fragmentsAllowed)
let task = URLSession.shared.dataTask(with: request) {data, response, error in
guard let data = data, error == nil else{
return
}
do {
let response = try? JSONDecoder().decode(UserResponse.self, from: data)
if self.normalizedMobileNumberField != "The number you enter is invalid." {
DispatchQueue.main.async {
self.user = response
if let userData = self.user {
print(userData.data.id)
}
}
} else {
print("You can't register application")
}
}
catch {
print(error)
}
}
task.resume()
}
}
This is the second one:
class GetTokenViewModel: ObservableObject {
#Published var tokenData: DataToken?
var userVM = UserPostViewModel()
func fetchData() {
let urlString = "privateapi/\(userVM.user.token)"
guard let url = URL(string: urlString) else { return }
var request = URLRequest(url:url)
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
return
}
do {
let decodedData = try JSONDecoder().decode(ResponseToken.self, from: data)
DispatchQueue.main.async{
self.tokenData = decodedData.data
print("TOKEN: \(self.tokenData?.token)")
}
} catch {
print("Error decoding JSON in gettoken: \(error)")
}
}.resume()
}
}
How to pass data from First ViewModel to the second?
I know that these codes have some error but I deleted every private information, but I believe that there is minimal code for understanding my question.
In SwiftUI, the View struct is equivalent to a view model but it is an immutable value type instead of an object that helps prevent consistency bugs. The property wrappers help it behave like an object. You pass data down the View struct hierarchy either with let if you want read access or #Binding var if you want write access. SwiftUI will track that these lets/vars are read in body and then body will be called whenever changes are detected. Transform the data as you pass it along from complex model types to simple value types.
#State declares a source of truth and #StateObject is when you need a reference type in an #State because you are doing something asynchronous or need to work around the limitations of closures in structs. Because these states are sources of truth you can't pass anything in because they won't be able to detect changes the same way the View struct can with let or #Binding var.
The best way to call web API in SwiftUI now is .task and .task(id:). The task will start when the view appears, cancelled if it disappears, or restarted if the id changes. This can completely remove the need for an object.

Safe handling of Apple TabularData table data in Swift

I have successfully imported a simple two-column CSV file using DataFrame.
Now I want to turn the two cells in each row into strings.
The left or right side will occasionally be missing a value; see print output.
table
When I try to process a row with a nil cell, the program crashes with "Fatal error: Unexpectedly found nil while unwrapping an Optional value"
So my question is, how do I turn the cells into strings ("" if Nil) safely?
Complete ContentView code below, thanks in advance for any help.
import SwiftUI
import TabularData
struct ContentView: View {
#State var openFile = false
var body: some View {
VStack {
Button(action: {openFile.toggle()}, label: {
Text("Open")
})
}.fileImporter(
isPresented: $openFile,
allowedContentTypes: [.commaSeparatedText],
allowsMultipleSelection: false) { (result) in
do {
let fileURL = try result.get().first
if fileURL!.startAccessingSecurityScopedResource() {
print(fileURL!)
importTable(url: fileURL!)
}
fileURL!.stopAccessingSecurityScopedResource()
} catch {
print("Unable to read file contents")
print(error.localizedDescription)
}
}
}
func importTable(url: URL) {
var importerTable: DataFrame = [:]
let options = CSVReadingOptions(hasHeaderRow: false, delimiter: ",")
do {
importerTable = try DataFrame(
contentsOfCSVFile: url,
options: options)
} catch {
print("ERROR reading CSV file")
print(error.localizedDescription)
}
print("\(importerTable)")
importerTable.rows.forEach { row in
let leftString = row[0]! as! String
let rightString = row[1]! as! String
print("Left: \(leftString) Right: \(rightString)")
}
}
}
A fileImporter context is quite insecure, any carelessly written exclamation mark can crash the app.
First you have to check fileURL and abort the import if it's nil
guard let fileURL = try result.get().first else { return }
if fileURL.startAccessingSecurityScopedResource() {
print(fileURL)
importTable(url: fileURL)
}
fileURL.stopAccessingSecurityScopedResource()
In importTable you must not continue after a do - catch if an error is thrown and you have to check if the CSV file contains three items per row.
func importTable(url: URL) {
var importerTable: DataFrame = [:]
let options = CSVReadingOptions(hasHeaderRow: false, delimiter: ",")
do {
importerTable = try DataFrame(
contentsOfCSVFile: url,
options: options)
print("\(importerTable)")
importerTable.rows.forEach { row in
if row.count > 2 {
let leftString = row[1] as! String
let rightString = row[2] as! String
print("Left: \(leftString) Right: \(rightString)")
}
}
} catch {
print("ERROR reading CSV file")
print(error.localizedDescription)
}
}

Is there a workaround for Converting from <operationQuery.Data> to Data type?

Language:Swift
Hello, I'd like some help in resolving an error being thrown when I try to retrieve data from an Apollo GraphQL request that I'm making. The API in use is the AniList API utilizing GraphQL.
Here's what I've tried:
In my model I'm making the Apollo GraphQL query inside of a search() function. I want to then use the Codable protocol to fill an array of anime objects. Currently it's setup to return just for 1 anime object. I was planning on using this anime list as a data set for TableView later. I wanted to take small steps so my current goal is to at least get the Codable protocol to work and return the response data to an anime Struct object.
The documentation for Apollo shows how to get individual fields but when I try to get the corresponding fields from my response , I don't even have the option.
func search(){
Network.shared.apollo.fetch(query: AnisearchQuery()){ result in
guard let data = try? result.get().data else { return }
var topData:APIResponse?
do{
topData = JSONDecoder().decode(APIResponse.self, from: data.self)
}catch{
}
}
}
Here are the data structures that I've set up as a representation of the JSON data I expect to receive with respect to the hierarchy it is laid out in the response.
struct APIResponse:Codable{
let data:data
}
struct data:Codable{
let Page:page
let media:media
}
struct media:Codable{
let animeResults:anime
}
struct anime:Codable{
var romaji:String
var english: String
var native:String
var episodes:Int
var duration:Int
var medium:String
}
Here is the error in question.
"Cannot convert value of type 'AnisearchQuery.Data' to expected argument type 'Data'". This is generated by this line of code
topData = JSONDecoder().decode(APIResponse.self, from: data.self)
For further context , AnisearchQuery.Data is generated in response to the query I created for the codgen.
Here's what the data would look like in JSON format
This is the setup of the query:
query anisearch($page:Int, $perPage:Int, $search:String){
Page (page:$page, perPage:$perPage){
pageInfo {
total
currentPage
lastPage
hasNextPage
perPage
}
media(search:$search){
title{
romaji
english
native
}
episodes
duration
coverImage{
medium
}
}
}
}
Here's the Data object in the API.swift file:
public struct Data: GraphQLSelectionSet {
public static let possibleTypes: [String] = ["Query"]
public static var selections: [GraphQLSelection] {
return [
GraphQLField("Page", arguments: ["page": GraphQLVariable("page"), "perPage": GraphQLVariable("perPage")], type: .object(Page.selections)),
]
}
I'd be open to any alternative methods as to getting this task done or perhaps fixes to the error being thrown.
Many thanks in advance.
Inefficient Workaround
var animeCollection:SearchAnimeQuery.Data?
var media:[SearchAnimeQuery.Data.Page.Medium]?
var filteredData:[SearchAnimeQuery.Data.Page.Medium] = []
func loadData(search:String = "") {
if !search.isEmpty{
Network.shared.apollo.fetch(query: SearchAnimeQuery(search: search)){
[weak self] result in
//Make Sure ViewController Has not been deallocated
guard let self = self else{
return
}
/*defer {
}*/
switch result {
case .success(let graphQLResult):
if let animeData = graphQLResult.data {
DispatchQueue.main.async {
self.animeCollection = animeData
self.media = self.animeCollection?.page?.media as! [SearchAnimeQuery.Data.Page.Medium]
self.filteredData = self.media!
self.tableView.reloadData()
}
}
if let errors = graphQLResult.errors {
let message = errors
.map { $0.localizedDescription }
.joined(separator: "\n")
print(message)
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
}

SwiftUI: How to iterate over Documents in Cloud Firestore Collection and get Data?

I have this small project where a user can post an Image together with a quote, I would then like to display the Image and the quote togehter in their profile, as well as somewhere else so other users can see the post.
If I have this Cloud Firestore setup
where all of the Image Docs have the same 3 fields, but with different values.
How can I then iterate over all of the Image Docs and get the the Url and the quote? So I later can display the url together with the correct Quote?
And if this is for some reason not possible, is it then possible to get the number of Documents in a Collection?
BTW, I am not very experienced so I would appreciate a "kid friendly" answer if possible
Firestore
.firestore()
.collection("Images")
.getDocuments { (snapshot, error) in
guard let snapshot = snapshot, error == nil else {
//handle error
return
}
print("Number of documents: \(snapshot.documents.count ?? -1)")
snapshot.documents.forEach({ (documentSnapshot) in
let documentData = documentSnapshot.data()
let quote = documentData["Quote"] as? String
let url = documentData["Url"] as? String
print("Quote: \(quote ?? "(unknown)")")
print("Url: \(url ?? "(unknown)")")
})
}
You can get all of the documents in a collection by calling getDocuments.
Inside that, snapshot will be an optional -- it'll return data if the query succeeds. You can see I upwrap snapshot and check for error in the guard statement.
Once you have the snapshot, you can iterate over the documents with documents.forEach. On each document, calling data() will get you a Dictionary of type [String:Any].
Then, you can ask for keys from the dictionary and try casting them to String.
You can wee that right now, I'm printing all the data to the console.
Keep in mind that getDocuments is an asynchronous function. That means that it runs and then returns at an unspecified time in the future. This means you can just return values out of this function and expect them to be available right after the calls. Instead, you'll have to rely on things like setting properties and maybe using callback functions or Combine to tell other parts of your program that this data has been received.
If this were in SwiftUI, you might do this by having a view model and then displaying the data that is fetched:
struct ImageModel {
var id = UUID()
var quote : String
var url: String
}
class ViewModel {
#Published var images : [ImageModel] = []
func fetchData() {
Firestore
.firestore()
.collection("Images")
.getDocuments { (snapshot, error) in
guard let snapshot = snapshot, error == nil else {
//handle error
return
}
print("Number of documents: \(snapshot.documents.count ?? -1)")
self.images = snapshot.documents.compactMap { documentSnapshot -> ImageModel? in
let documentData = documentSnapshot.data()
if let quote = documentData["Quote"] as? String, let url = documentData["Url"] as? String {
return ImageModel(quote: quote, url: url)
} else {
return nil
}
}
}
}
}
struct ContentView {
#ObservedObject var viewModel = ViewModel()
var body : some View {
VStack {
ForEach(viewModel.images, id: \.id) { item in
Text("URL: \(item.url)")
Text("Quote: \(item.quote)")
}
}.onAppear { viewModel.fetchData() }
}
}
Note: there are now fancier ways to get objects decoded out of Firestore using FirebaseFirestoreSwift and Combine, but that's a little outside the scope of this answer, which shows the basics

How to access & get nested values from IOS Swift 'Any' type?

I am trying to read from Firestore into a Dictionary[Any] type using Struct. I can get the values loaded into variable "data" dictionary with Any type.
However I cannot loop thru it to access normal nested Dictionary variable.
I cannot get Key, values printed.
Following is my code:
class PullQuestions {
//shared instance variable
**public var data = [Any]()**
private var qdb = Firestore.firestore()
public struct questionid
{
let qid : String
var questions : [basequestion]
var answers: [baseans]
}
public struct basequestion {
let category : String
let question : String
}
public struct baseans {
let answer : String
}
class var sharedManager: PullQuestions {
struct Static {
static let instance = PullQuestions()
}
return Static.instance
}
static func getData(completion: #escaping (_ result: [Any]) -> Void) {
let rootCollection = PullQuestions.sharedManager.qdb.collection("questions")
//var data = [Any]()
rootCollection.order(by: "upvote", descending: false).getDocuments(completion: {
(querySnapshot, error) in
if error != nil {
print("Error when getting data \(String(describing: error?.localizedDescription))")
} else {
guard let topSnapshot = querySnapshot?.documents else { return }
// var questiondoc = [basequestion]()
for questioncollection in topSnapshot {
rootCollection.document(questioncollection.documentID).collection("answers").getDocuments(completion: {
(snapshot, err) in
guard let snapshot = snapshot?.documents else { return }
var answers = [baseans]()
for document in snapshot { //There should be only one Document for each answer collection
//Read thru all fields
for i in 0..<document.data().count
{
let newAns = baseans(answer: answer)
print("Answer Docs=>", (answer))
answers.append(newAns)
}
}
let qid = questioncollection.documentID
let category = questioncollection.data()["category"] as! String
let question = questioncollection.data()["question"] as! String
let newQuestions = basequestion(category: category ,question: question)
let newQuestionDict = questionid(qid: qid, questions: [newQuestions], answers: answers)
PullQuestions.sharedManager.data.append(newQuestionDict)
//Return data on completion
completion(PullQuestions.sharedManager.data)
})
}
}
})
}
}
I can print like this
print("Count =>", (PullQuestions.sharedManager.data.count))
// print(PullQuestions.sharedManager.data.first ?? "Nil")
print(PullQuestions.sharedManager.data[0])
for element in PullQuestions.sharedManager.data
{
print("Elements in data:=>", (element))
}
I could access only the key.. how do i go and get the nested values ?
First of all, consider using Swift code conventions (e.g. your structs are named with small letters, but you should start with capital), this will make your code more readable.
Returning to your question. You use an array instead of dictionary (this piece of code: public var data = [Any]()). And here you are trying to print values:
for element in PullQuestions.sharedManager.data
{
print("Elements in data:=>", (element))
}
In this context element is an Any object, thus you cannot access any underlying properties. In order to do this you have two options:
1. You should specify the type of array's objects in it's declaration like this:
public var data = [questionid]()
or you can user this:
public var data: [questionid] = []
These two are equals, use the one you prefer.
2. If for any reasons you don't want to specify the type in declaration, you can cast it in your loop. Like this:
for element in PullQuestions.sharedManager.data
{
if let element = element as? quetionid {
print("Elements in data:=>", (element))
// you can also print element.qid, element.questions, element.answers
} else {
print("Element is not questionid")
}
}
You could of course use the force cast:
let element = element as! questionid
and avoid if let syntax (or guard let if you prefer), but I wouldn't recommend this, because it (potentially) can crash your app if element will be nil or any other type.

Resources