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()
}
}
Related
I'm trying to load data into a CoreData entity "Articles" from a function I would like to call in an init() {} call when my app starts which means I'm not doing this from within a view.
I get the message "Accessing Environment's value outside of being installed on a View. This will always read the default value and will not update."
and would like to work around that. I'm using Xcode 14.2
I do have a standard PersistenceController setup and so on
Here is where I run into the issue "let section = SectionsDB(context: managedObjectContext)"
#main
struct ArticlesExampleApp: App {
let persistanceController = PersistanceController.shared
init() {
let x = Articles.loadSections()
}
var body: some Scene {
WindowGroup {
MasterView()
.environment(\.managedObjectContext, persistanceController.container.viewContext)
}
}
class Articles {
class func loadSections() -> Int {
#Environment(\.managedObjectContext) var managedObjectContext
// Initialize some variables
let myPath = Bundle.main.path(forResource: "Articles", ofType: "json")
// Initialize some counters
var sectionsCount = 0
do {
let myData = try Data(contentsOf: URL(fileURLWithPath: myPath!), options: .alwaysMapped)
// Decode the json
let decoded = try JSONDecoder().decode(ArticlesJSON.self, from: myData)
// **here is where I run into the error on this statement**
let section = SectionsDB(context: managedObjectContext)
while sectionsCount < decoded.sections.count {
print("\(decoded.sections[sectionsCount].section_name) : \(decoded.sections[sectionsCount].section_desc)")
section.name = decoded.sections[sectionsCount].section_name
section.desc = decoded.sections[sectionsCount].section_desc
sectionsCount+=1
}
PersistanceController.shared.save()
} catch {
print("Error: \(error)")
}
return sectionsCount
}
}
Since you are already using a singleton, you can just use that singleton in your loadSections function:
let section = SectionsDB(context: PersistanceController.shared.container.viewContext)
And, remove the #Environment(\.managedObjectContext) var managedObjectContext line
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 have prepared a simple test project at Github to demo my problem:
I have a SwiftUI List and I try to display the var items:[String] in it.
When I only have a hardcoded array like below - it works fine and displays in iPhone:
items = (1...200).map { number in "Item \(number)" }
But when I try to fetch JSON web page and append results to items then I get the error:
Escaping closure captures mutating 'self' parameter
I understand that the line items.append(str) modifies the parent ContentView object out of dataTask closure and that is not good for some reason... but how to fix my code then?
import SwiftUI
struct TopResponse: Codable {
let data: [Top]
}
struct Top: Codable {
let uid: Int
let elo: Int
let given: String
let photo: String?
let motto: String?
let avg_score: Double?
let avg_time: String?
}
struct ContentView: View {
var items:[String];
init() {
items = (1...200).map { number in "Item \(number)" }
let url = URL(string: "https://slova.de/ws/top")!
let task = URLSession.shared.dataTask(with: url) {
data, response, error in
let decoder = JSONDecoder()
guard let data = data else { return }
do {
let tops = try decoder.decode(TopResponse.self, from: data)
for (index, top) in tops.data.enumerated() {
let str = "\(index + 1): \(top.given)"
items.append(str) // this results in compile error!
}
} catch {
print("Error while parsing: \(error)")
}
}
task.resume()
}
var body: some View {
List(items, id: \.self) { item in
Text(item)
}
}
}
Should I move the items out of the View maybe?
My final target is to have the JSON data in Core Data and then update/notify the List from it.
I have such an app in Android (structured as MVVM) and now I am trying to port it to SwiftUI, being a Swift newbie.
UPDATE:
I have added a view model file as suggested by achu (thanks!) and it kind of works, but the List is only updated with new items when I drag at it. And there is a warning
[SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
I will move the items to ViewModel and eventually move the service call to an APIManager class
EDIT: The UI update should be in the main thread. Added service call on ViewModel init().
struct TestView: View {
#ObservedObject var viewModel = TestViewModel()
var body: some View {
List(viewModel.items, id: \.self) { item in
Text(item)
}
}
}
class TestViewModel: ObservableObject {
#Published var items: [String] = []
init() {
self.fetchData()
}
func fetchData() {
let url = URL(string: "https://slova.de/ws/top")!
let task = URLSession.shared.dataTask(with: url) {
data, response, error in
let decoder = JSONDecoder()
guard let data = data else { return }
do {
let tops = try decoder.decode(TopResponse.self, from: data)
for (index, top) in tops.data.enumerated() {
let str = "\(index + 1): \(top.given)"
self.updateItems(str)
}
} catch {
print("Error while parsing: \(error)")
}
}
task.resume()
}
func updateItems(_ str: String) {
DispatchQueue.main.async {
self.items.append(str)
}
}
}
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 []
}
}
I am trying to retrieve user data once the user gets to the dashboard of my app
I have essentially this to get data:
class UserController: ObservableObject {
#Published var firstName: String = ""
func fetchUser(token: String) {
/* Do url settings */
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else { return }
let rData = try! JSONDecoder().decode(User.self, from: data)
let userData = [
"id": rData.id,
"firstName": rData.firstName,
"lastName": rData.lastName,
"department": rData.department,
]
UserDefaults.standard.set(userData, forKey: "user")
DispatchQueue.main.async {
self.firstName = rData.firstName
}
}.resume()
}
}
And then my view looks like this
struct HomeViewCollection: View {
#Binding var isAuthenticated: Bool
#ObservedObject var userController: UserController = UserController()
var body: some View {
VStack {
Text("Hello \(userController.firstName)!")
}
}
}
I'm just not sure how can I activate fetchUser from the View.
I have tried this in the controller
init() {
guard let tokenData = KeyChain.load(key: "token") else { return }
var token = String(data: tokenData, encoding: .utf8)
if(token != nil) {
print("Token: \(token)")
fetchUser(token: token!)
}
}
That didn't work, and then I tried userController.fetchUser(token: KeyChainTokenHere) and that didn't work because it doesn't conform to the struct.
Try passing the token to HomeViewCollection and initiating the call in onAppear completion block.
struct HomeViewCollection: View {
var token: String
#Binding var isAuthenticated: Bool
#ObservedObject var userController = UserController()
var body: some View {
VStack {
Text("Hello \(userController.firstName)!")
}
.onAppear {
self.userController.fetchUser(token: self.token)
}
}
}
Also, make sure the firstName property is getting set.
#Published var firstName: String = "" {
didSet {
print("firstName is set as \(firstName)")
}
}