How to retrieve and show data on screen in SwiftUI? - ios

I am storing simple data in core data which i want to retrieve and show on screen, but I am not sure how and where should I write that code to show it on screen as I am always get out of bound error..
Also not all data is saving at time its only saving when i scroll til bottom
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.title, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
#StateObject private var viewModel = HomeViewModel()
var body: some View {
GeometryReader { geometry in
NavigationView {
ScrollView {
LazyVGrid(columns: Array(repeating: .init(.flexible()),
count: UIDevice.current.userInterfaceIdiom == .pad ? 4 : 2)) {
ForEach(viewModel.results, id: \.self) {
let viewModel = ResultVM(model: $0)
NavigationLink(destination: {
DetailView(data: viewModel.trackName)
}, label: {
SearchResultRow(resultVM: viewModel, coreDM: PersistenceController())
})
}
}
}
}
.onAppear(perform: {
viewModel.performSearch()
})
}
}
}
struct SearchResultRow: View {
let resultVM: ResultVM
let coreDM: PersistenceController
var body: some View {
HStack {
RoundedRectangle(cornerRadius: 16).fill(.yellow)
.frame(maxWidth: .infinity).aspectRatio(1, contentMode: .fit)
.overlay(Text(resultVM.trackName)) // want to show data from Core data here
}.padding()
.background(Color.red)
.onAppear(perform: {
coreDM.saveResult(title: resultVM.trackName)
})
}
}
Data showing from API which same data storing in CoreData
Method to save and retrieve (which is working fine)
func saveResult(title: String) {
let result = Item(context: container.viewContext)
result.title = title
do {
try container.viewContext.save()
}
catch {
print("error")
}
}
func getResult() -> [Item] {
let fetchRequest: NSFetchRequest<Item> = Item.fetchRequest()
do {
return try container.viewContext.fetch(fetchRequest)
}
catch {
return []
}
}
API call
import Foundation
import CoreData
class HomeViewModel: ObservableObject {
#Published var results = [ResultItem]()
func performSearch() {
guard let gUrl = URL(
string: "https://api.artic.edu/api/v1/artworks"
) else { return }
Task {
do {
let (data, _) = try await URLSession.shared.data(from: gUrl)
let response = try JSONDecoder()
.decode(ResponseData.self, from: data)
DispatchQueue.main.async { [weak self] in
self?.results = response.data ?? []
}
} catch {
print("*** ERROR ***")
}
}
}
}

Remove the view model we don't use view model objects in SwiftUI. The View struct stores the view data and does dependency tracking, and the property wrappers give it external change tracking features like an object. If you add an actual object on top you'll get consistency bugs that SwiftUI was designed to eliminate. So first remove this:
#StateObject private var viewModel = HomeViewModel()
You already have the fetch request property wrapper and as I said that gives the View change tracking, so when the items change (e.g. an item is added, removed or moved), the body will be called, so simply use it in your ForEach like this:
ForEach(items) { item in
NavigationLink(destination: {
DetailView(item: item)
}, label: {
SearchResultRow(item: item)
})
}
Then use #ObservedObject in your subview, this gives the View the ability to track changes to the item so body will be called when the item's properties change, like item.title. Do the same in the DetailView.
struct SearchResultRow: View {
#ObservedObject var item: Item
It seems to me you are trying to download and cache data in Core Data. That is unnecessary since you can simply set a NSURLCache on the NSURLSession and it will automatically cache it for you, it even works if offline. However if you really want to cache it yourself in Core Data then the architecture should be your UI fetches it from Core Data and then you have another object somewhere responsible for syncing down the remote data into Core Data. Normally this would be at the App struct level not in the View appearing. When the data is saved to core data the managed object context context will be changed which the #FetchRequest is listening for and body will be called.

Related

Issue passing data from API call in SwiftUI MVVM pattern

been going back and forth for 2 days trying to figure this out before posting and still hitting a wall.
Created an API specific class, a ViewModel, and a View and trying to shuttle data back and forth and while I see the API call is successful and I decode it without issue on logs, it never reflects on the UI or View.
As far as I see I appear to be trying to access the data before it's actually available. All help greatly appreciated!
API Class:
import Combine
import Foundation
class CrunchbaseApi:ObservableObject
{
#Published var companies:[Company] = [Company]()
#Published var singleCompany:Company?
func retrieve(company:String) async
{
let SingleEntityURL:URL = URL(string:"https://api.crunchbase.com/api/v4/entities/organizations/\(company)?card_ids=fields&user_key=**********REMOVED FOR SECURITY*****************")!
let task = URLSession.shared.dataTask(with:SingleEntityURL){ data, response, error in
let decoder = JSONDecoder()
if let data = data{
do {
self.singleCompany = try decoder.decode(Company.self, from: data)
} catch {
print(error.localizedDescription)
}
}
}
task.resume()
}
func retrieveCompanyList()
{
//declare
}
}
ViewModel:
import Combine
import Foundation
class CompanyViewModel: ObservableObject
{
var crunchbase:CrunchbaseApi = CrunchbaseApi()
#Published var singleCompany:Company?
func retrieveCompany(company:String) async
{
await self.crunchbase.retrieve(company: company)
self.singleCompany = crunchbase.singleCompany
}
}
View:
import SwiftUI
struct CompanyView: View
{
#State var companyViewModel:CompanyViewModel = CompanyViewModel()
var body: some View
{
NavigationView
{
VStack
{
Text("Company ID: \(companyViewModel.singleCompany?.id ?? "NOTHING")")
// Text("Company Name: \(companyViewModel.companyName)")
// Text("Company Summary: \(companyViewModel.companyDescription)")
// Text("Logo URL: \(companyViewModel.companyLogoURL)")
}.navigationTitle("Company")
}
}
}
Your assumption about accessing the data to early is correct. But there are more things going on here.
just declaring a function async like your retrieve func doesn´t make it async.
using a nested Observable class with #Published will not update the view
Observable classes should have either an #StateObject or an #ObservableObject property wrapper. Depending on if the class is injected or created in the view
Possible solution:
Move the function into the viewmodel:
class CompanyViewModel: ObservableObject
{
#Published var singleCompany:Company?
func retrieve(company:String)
{
let SingleEntityURL:URL = URL(string:"https://api.crunchbase.com/api/v4/entities/organizations/\(company)?card_ids=fields&user_key=**********REMOVED FOR SECURITY*****************")!
let task = URLSession.shared.dataTask(with:SingleEntityURL){ data, response, error in
let decoder = JSONDecoder()
if let data = data{
do {
self.singleCompany = try decoder.decode(Company.self, from: data)
} catch {
print(error.localizedDescription)
}
}
}
task.resume()
}
}
Change the View to hold the viewmodel as #StateObject, also add an .onApear modifier to load the data:
struct CompanyView: View
{
#StateObject var companyViewModel:CompanyViewModel = CompanyViewModel()
var body: some View
{
NavigationView
{
VStack
{
Text("Company ID: \(companyViewModel.singleCompany?.id ?? "NOTHING")")
// Text("Company Name: \(companyViewModel.companyName)")
// Text("Company Summary: \(companyViewModel.companyDescription)")
// Text("Logo URL: \(companyViewModel.companyLogoURL)")
}.navigationTitle("Company")
.onAppear {
companyViewModel.retrieve(company: "whatever")
}
}
}
}

Where to make an API call to populate a view with data

I am trying to create a view that displays results from an API call, however I keep on running into multiple errors.
My question is basically where is the best place to make such an API call.
Right now I am "trying" to load the data in the "init" method of the view like below.
struct LandingView: View {
#StateObject var viewRouter: ViewRouter
#State var user1: User
#State var products: [Product] = []
init(_ viewRouter : ViewRouter, user: User) {
self.user1 = user
_viewRouter = StateObject(wrappedValue: viewRouter)
ProductAPI().getAllProducts { productArr in
self.products = productArr
}
}
var body: some View {
tabViewUnique(prodArrParam: products)
}
}
I keep on getting an "escaping closure mutating self" error, and while I could reconfigure the code to stop the error,I am sure that there is a better way of doing what I want.
Thanks
struct ContentView: View {
#State var results = [TaskEntry]()
var body: some View {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.title)
}
// this one onAppear you can use it
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/todos") else {
print("Your API end point is Invalid")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let response = try? JSONDecoder().decode([TaskEntry].self, from: data) {
DispatchQueue.main.async {
self.results = response
}
return
}
}
}.resume()
}
}
In .onAppear you can make api calls

SwiftUI Lists + Firebase Firestore, fetching data and then unfetching? (Bug)

I'm not sure what the problem is with my project. Basically, I have a pretty typical data structure: I'm using Firestore for a real-time doc based database, and I have a bunch of different collections and docs and fields. Just very simple, nothing crazy.
I have some model classes, and then some ViewModels to fetch data, filter, add documents to Firestore, and so on.
The app itself is almost 100% SwiftUI, and I'd like to keep it that way, just a challenge for my own development. I've hit a bit of a wall though.
In the app, I have a series of Views with NavigationView Lists that I pass small pieces of data to as you navigate. An example (this is for educational institutions) might be List of Schools > List of Classes > List of Students in Class > List of Grades for Student. Basically the perfect use for a navigation view and a bunch of lists.
The bug:
When I move from one list to the next in the stack, I fetch the firestore data, which loads for a second (enough that the list populates), and then "unloads" back to nothing. Here is some code where this happens (I've cleaned it up to make it as simple as possible):
struct ClassListView: View {
let schoolCode2: String
let schoolName2: String
#ObservedObject private var viewModelThree = ClassViewModel()
var body: some View {
VStack{
List{
if viewModelThree.classes.count > 0{
ForEach(self.viewModelThree.classes) { ownedClass in
NavigationLink(destination: StudentListView()){
Text(ownedClass.className)
}
}
} else {
Text("No Classes Found for \(schoolName2)")
}
}
}
.navigationBarTitle(schoolName2)
.onAppear(){
print("appeared")
self.viewModelThree.fetchData(self.schoolCode2)
}
}
}
So that's the ClassListView that I keep having issues with. For debugging, I added the else Text("No Classes Found") line and it does in fact show. So basically, view loads (this is all in a Nav view from a parent), it fetches the data, which is shown for a second (list populates) and then unloads that data for some reason, leaving me with just the "No classes found".
For more context, here is the code for the ClassViewModel (maybe that's where I'm going wrong?):
struct Classes: Identifiable, Codable {
#DocumentID var id: String? = UUID().uuidString
var schoolCode: String
var className: String
}
enum ClassesCodingKeys: String, CodingKey {
case id
case schoolCode
case className
}
class ClassViewModel: ObservableObject {
#Published var classes = [Classes]()
private var db = Firestore.firestore()
func fetchData(_ schoolCode: String) {
db.collection("testClasses")
.order(by: "className")
.whereField("schoolCode", isEqualTo: schoolCode)
.addSnapshotListener{ (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("no docs found")
return
}
self.classes = documents.compactMap{ queryDocumentSnapshot -> Classes? in
return try? queryDocumentSnapshot.data(as: Classes.self)
}
}
}
func addClass(currentClass: Classes){
do {
let _ = try db.collection("testClasses").addDocument(from: currentClass)
}
catch {
print(error)
}
}
}
Most relevant bit is the fetchData() function above.
Maybe the problem is in the view BEFORE this (the parent view?). Here it is:
struct SchoolUserListView: View {
#State private var userId: String?
#EnvironmentObject var session: SessionStore
#ObservedObject private var viewModel = UserTeacherViewModel()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State private var counter = 0
#State private var showingAddSchool: Bool = false
#Environment(\.presentationMode) var presentationMode
func getUser() {
session.listen()
}
var body: some View {
VStack{
List{
if viewModel.teachers.count > 0{
ForEach(viewModel.teachers) { ownedClass in
NavigationLink(destination: ClassListView(schoolName2: ownedClass.schoolName, schoolCode2: ownedClass.schoolCode)){
Text(ownedClass.schoolName)
}
}
} else {
Button(action:{
self.presentationMode.wrappedValue.dismiss()
}){
Text("You have no schools, why not add one?")
}
}
}
.navigationBarTitle("Your Schools")
}
.onAppear(){
self.getUser()
self.userId = self.session.session?.uid
}
.onReceive(timer){ time in
if self.counter == 1 {
self.timer.upstream.connect().cancel()
} else {
print("fetching")
self.viewModel.fetchData(self.userId!)
}
self.counter += 1
}
}
}
And the FURTHER Parent to that View (and in fact the starting view of the app):
struct StartingView: View {
#EnvironmentObject var session: SessionStore
func getUser() {
session.listen()
}
var body: some View {
NavigationView{
VStack{
Text("Welcome!")
Text("Select an option below")
Group{
NavigationLink(destination:
SchoolUserListView()
){
Text("Your Schools")
}
NavigationLink(destination:
SchoolCodeAddView()
){
Text("Add a School")
}
Spacer()
Button(action:{
self.session.signOut()
}){
Text("Sign Out")
}
}
}
}
.onAppear(){
self.getUser()
}
}
}
I know that is a lot, but just so everyone has the order of parents:
StartingView > SchoolUserListView > ClassListView(BUG occurs here)
By the way, SchoolUserListView uses a fetchData method just like ClassListView(where the bug is) and outputs it to a foreach list, same thing, but NO problems? I'm just not sure what the issue is, and any help is greatly appreciated!
Here is a video of the issue:
https://imgur.com/a/kYtow6G
Fixed it!
The issue was the EnvironmentObject for presentationmode being passed in the navigation view. That seems to cause a LOT of undesired behavior.
Be careful passing presentationMode as it seems to cause data to reload (because it is being updated).

Using URLSession to load JSON data for SwiftUI Views

What are proven approaches for structuring the networking layer of a SwiftUI app? Specifically, how do you structure using URLSession to load JSON data to be displayed in SwiftUI Views and handling all the different states that can occur properly?
Here is what I came up with in my last projects:
Represent the loading process as a ObservableObject model class
Use URLSession.dataTaskPublisher for loading
Using Codable and JSONDecoder to decode the response to Swift types using the Combine support for decoding
Keep track of the state in the model as a #Published property so that the view can show loading/error states.
Keep track of the loaded results as a #Published property in a separate property for easy usage in SwiftUI (you could also use View#onReceive to subscribe to the publisher directly in SwiftUI but keeping the publisher encapsulated in the model class seemed more clean overall)
Use the SwiftUI .onAppear modifier to trigger the loading if not loaded yet.
Using the .overlay modifier is convenient to show a Progress/Error view depending on the state
Extract reusable components for repeatedly occuring tasks (here is an example: EndpointModel)
Standalone example code for that approach (also available in my SwiftUIPlayground):
// SwiftUIPlayground
// https://github.com/ralfebert/SwiftUIPlayground/
import Combine
import SwiftUI
struct TypiTodo: Codable, Identifiable {
var id: Int
var title: String
}
class TodosModel: ObservableObject {
#Published var todos = [TypiTodo]()
#Published var state = State.ready
enum State {
case ready
case loading(Cancellable)
case loaded
case error(Error)
}
let url = URL(string: "https://jsonplaceholder.typicode.com/todos/")!
let urlSession = URLSession.shared
var dataTask: AnyPublisher<[TypiTodo], Error> {
self.urlSession
.dataTaskPublisher(for: self.url)
.map { $0.data }
.decode(type: [TypiTodo].self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
func load() {
assert(Thread.isMainThread)
self.state = .loading(self.dataTask.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
break
case let .failure(error):
self.state = .error(error)
}
},
receiveValue: { value in
self.state = .loaded
self.todos = value
}
))
}
func loadIfNeeded() {
assert(Thread.isMainThread)
guard case .ready = self.state else { return }
self.load()
}
}
struct TodosURLSessionExampleView: View {
#ObservedObject var model = TodosModel()
var body: some View {
List(model.todos) { todo in
Text(todo.title)
}
.overlay(StatusOverlay(model: model))
.onAppear { self.model.loadIfNeeded() }
}
}
struct StatusOverlay: View {
#ObservedObject var model: TodosModel
var body: some View {
switch model.state {
case .ready:
return AnyView(EmptyView())
case .loading:
return AnyView(ActivityIndicatorView(isAnimating: .constant(true), style: .large))
case .loaded:
return AnyView(EmptyView())
case let .error(error):
return AnyView(
VStack(spacing: 10) {
Text(error.localizedDescription)
.frame(maxWidth: 300)
Button("Retry") {
self.model.load()
}
}
.padding()
.background(Color.yellow)
)
}
}
}
struct TodosURLSessionExampleView_Previews: PreviewProvider {
static var previews: some View {
Group {
TodosURLSessionExampleView(model: TodosModel())
TodosURLSessionExampleView(model: self.exampleLoadedModel)
TodosURLSessionExampleView(model: self.exampleLoadingModel)
TodosURLSessionExampleView(model: self.exampleErrorModel)
}
}
static var exampleLoadedModel: TodosModel {
let todosModel = TodosModel()
todosModel.todos = [TypiTodo(id: 1, title: "Drink water"), TypiTodo(id: 2, title: "Enjoy the sun")]
todosModel.state = .loaded
return todosModel
}
static var exampleLoadingModel: TodosModel {
let todosModel = TodosModel()
todosModel.state = .loading(ExampleCancellable())
return todosModel
}
static var exampleErrorModel: TodosModel {
let todosModel = TodosModel()
todosModel.state = .error(ExampleError.exampleError)
return todosModel
}
enum ExampleError: Error {
case exampleError
}
struct ExampleCancellable: Cancellable {
func cancel() {}
}
}
Splitting off the state / data / networking into a separate #ObservableObject class outside the View Struct is definitely the way to go. There are too many SwiftUI "Hello World" examples out there stuffing it all into the View struct.
As a best practice you could look to standardize your #ObservableObject naming inline with MVVM and call that "Model" class a ViewModel, as in:
#StateObject var viewModel = TodosViewModel()
The majority of code in there is handling overlay state, onAppear events and display issues for the View.
Create a new TodosModel class and reference that in the ViewModel:
#ObservedObject var model = TodosModel()
Then move all the networking / api / JSON code into that class with one method called by ViewModel:
public func getList() -> AnyPublisher<[TypiTodo], Error>
The View-ViewModel-Model are now split up, related to Paul D's comment, the ViewModel could combine 1 or more Models to return whatever the view needs. And, more importantly, the TodoModel entity knows nothing about the View and can focus on http / JSON / CRUD.
Below is great example using Combine / HTTP / JSON decode. You can see how it uses tryMap, mapError to further separate the networking from the decode errors. https://gist.github.com/stinger/e8b706ab846a098783d68e5c3a4f0ea5
See a very short and clear explanation of the difference between #StateObject and #ObservedObject in this article:
https://levelup.gitconnected.com/state-vs-stateobject-vs-observedobject-vs-environmentobject-in-swiftui-81e2913d63f9

How to generate fixed number of views from json API in SwiftUI?

So I am learning how to use SwiftUI with a json api. I am currently generating views through a list and a ForEach Loop. I am wondering how I can make it so that it only generates the first, say 10 elements in the posts array, instead of generating the entire list from the api. Basically I want to use RandomElement() to display 10 random posts from the entire array. I am just beginning here and learning so any help woul dbe appreicated.
Below is the code for my main view that is displaying the list
import SwiftUI
struct postList: View {
//state variable of the posts
#State var posts: [Post] = []
var array = [Post]()
var body: some View {
List {
ForEach(posts) { post in
VStack(alignment: .leading) {
Text(post.title)
.font(.headline)
Text(post.body)
.font(.callout)
}
.padding()
}
}
.onAppear() {
Api().getPosts { (posts) in
self.posts = posts
}
}
}
}
struct postList_Previews: PreviewProvider {
static var previews: some View {
postList()
}
}
Down here is the Data file I use to retrieve the json data
import SwiftUI
struct Post: Codable, Identifiable {
let id = UUID()
var title: String
var body: String
}
class Api {
func getPosts(completion: #escaping ([Post]) -> ()) {
guard let url = URL(string: "http://jsonplaceholder.typicode.com/posts") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
let posts = try! JSONDecoder().decode([Post].self, from: data!)
DispatchQueue.main.async {
completion(posts)
}
}
.resume()
}
}

Resources