How to using the fetched data in swiftUI - swift

I am currently working on an IOS app, I am trying to design a carousel with the fetched json data. This is how I get the targeted json data.
class loadDate: ObservableObject {
#Published var todos = [Result]()
init() {
let url = URL(string: "https://api.themoviedb.org/3/movie/now_playing?api_key=<api_key>&language=en-US&page=1")!
URLSession.shared.dataTask(with: url) { data, response, error in
// step 4
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
DispatchQueue.main.async {
self.todos = decodedResponse.results
}
// everything is good, so we can exit
return
}
}
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
}
It looks like I can only use the data in a list view, for example,
struct ContentView: View {
#ObservedObject var fetch = loadDate()
var body: some View {
HStack {
List(fetch.todos) { item in
HStack {
VStack(alignment: .leading) {
Text(item.title)
.font(.headline)
Text(item.poster_path)
//
}
}
}
}
}
}
Within this list view, I can access all elements like using fetch.todos[0].title. However, if outside the list view, fetch.todos[0].title would fail, can someone give some advice on how to use the data outside the list view .

You should provide a working example. (https://stackoverflow.com/help/minimal-reproducible-example)
But if todos is an empty array, fetch.todos[0].title will always fail. So whenever you need to access your todos directly you can use something like this
Group {
if fetch.todos.count > 0 {
Text("Title of first todo \(fetch.todos[0].title)")
} else {
Text("Todos are empty")
}
}

Try this:
class loadDate: ObservableObject {
#Published var todos : Result? = nil // Step 1
// ...
}
struct ContentView: View {
#ObservedObject var fetch = loadDate()
var body: some View {
HStack {
if let todos = fetch.todos {
VStack{
Text(todos.title)
.font(.headline)
Text(todos.poster_path)
}
}
}
}
}
https://stackoverflow.com/a/66695683/14287797

Related

Getting the out of range index error when trying to make a Page Tab View from API array

This is my Model:
class Api {
func getRockets(completion: #escaping ([Rocket]) -> ()) {
guard let url = URL(string: "https://api.spacexdata.com/v4/rockets") else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
let rockets = try JSONDecoder().decode([Rocket].self, from: data!)
DispatchQueue.main.async {
completion(rockets)
}
} catch {
print(error.localizedDescription)
}
}
.resume()
}
}
I try to make PageTabView using elements from the API array, but my app crashes with an out of range index error.
This is the View that doesn't work properly:
struct TestTabViewView: View {
#State var rockets: [Rocket] = [] //<-- The array of items from the API I use to make tabs
var body: some View {
TabView {
ForEach(rockets) { rocket in
Text(rocket.name)
}
}
.onAppear {
Api().getRockets { rockets in
self.rockets = rockets
}
}
.tabViewStyle(.page)
}
}
struct TestTabViewView_Previews: PreviewProvider {
static var previews: some View {
TestTabViewView()
}
}
If you put TabView inside an if-else statement, and in turn put this statement inside NavigationView or Group, and then call the .onAppear method for an external container (for example, Group), everything will work properly and there will be no an out of range index error.
struct TestTabViewView: View {
#State var rockets: [Rocket] = []
var body: some View {
Group {
if rockets.isEmpty {
ProgressView()
} else {
TabView {
ForEach(rockets) { rocket in
Text(rocket.name)
}
}
.tabViewStyle(.page)
}
}
.onAppear {
Api().getRockets { rockets in
self.rockets = rockets
}
}
}
}

How to make an additional API call once inside a detail view?

I'm fetching books from an endpoint as such:
class APIManager: ObservableObject {
#Published var books = [Book]()
func fetchBooks() {
if let url = URL(string: urlEndpoint) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil {
if let safeData = data {
do {
let response = try JSONDecoder().decode([Book].self, from: safeData)
DispatchQueue.main.async {
self.books = response
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
My BookView looks like this:
struct BookView: View {
#ObservedObject var apiManager = APIManager()
var body: some View {
NavigationView {
List {
ForEach(apiManager.books) { book in
NavigationLink(
destination: BookDetailView(id: book.id, chapter: book.chapters),
label: {
HStack {
Text(book.name)
Spacer()
Text(book.testament)
}
})
}.navigationBarTitle("Book Title Here")
}.onAppear {
self.apiManager.fetchBooks()
}
}
}
}
When navigating to BookDetailView - I need to make another API call to fetch additional details about the book (such as chapters), given the book id that is passed here:
...
destination: BookDetailView(id: book.id, chapter: book.chapters)
...
Do I simply repeat the process and make another function in my APIManager class and add another #Published var chapters = [Chapter]()
And inside BookDetailView go
// Loop through each chapter here
// I want to display chapter details in this view
Text("You are viewing book id \(id). Chapter: \(chapter)").onAppear {
self.apiManager.fetchChapterDetails()
}
Doing so returns UIScrollView does not support multiple observers implementing
Whats the procedure here?
I would suggest making a separate ViewModel for the Detail Page. In that ViewModel you can pass in the id of the Book and make a separate API function. Also think about extracting the API Calls into a Service class, which being called from the ViewModel.
The Detail View and ViewModel could look like that...
class BookDetailViewModel: ObservableObject {
let bookId: Int
init(withBookId bookId: Int) {
self.bookId = bookId
}
func fetchBookInfos() {
// ...
}
}
struct BookDetailView: View {
#ObservedObject var viewModel: BookDetailViewModel
var body: some View {
NavigationView {
List {
Text("Book id \(viewModel.bookId)")
}.onAppear {
self.viewModel.fetchBookInfos()
}
}
}
}
When creating the Detail View, pass in the ViewModel.

Cannot find 'entity' in scope SwiftUI

I'm trying to display the detail of an item from a list on to a sheet. I'm getting this error Cannot find 'entity' in scope even though it's declared in the detail item struct.
This error doesn't appear if the sheet is inside of the list BUT this causes only the first item detail to be shown even if you select any of the other items below the first.
This is a macOS app.
#StateObject var vm = CoreDataViewModel()
List {
ForEach(vm.savedEntites) { entity in
Text(entity.name ?? "No Name")
.font(.system(size: 25))
HStack {
Button(action: {vm.deleteMemory(entity: entity)}) {
Label("Delete",systemImage: "trash")
}.foregroundColor(Color(.red))
Button(action: {showingDetailScreen.toggle()}) {
Label("Details", systemImage: "pencil")
}.foregroundColor(Color(.red))
}
}// list ends here
.sheet(isPresented: $showingDetailScreen) {
DetailItemView(entity: entity,isVisible: self.$showingDetailScreen)
}
}
Detail Item View Struct
struct DetailItemView: View {
var entity: MemoryEntity
#Environment(\.presentationMode) var presentationMode
#Binding var isVisible: Bool
var body: some View {
Text(entity.name ?? "No Name")
HStack {
Button("Exit") {
self.isVisible = false
}
}
.frame(width: 300, height: 150)
}
}
ViewModel
class CoreDataViewModel: ObservableObject {
let container: NSPersistentContainer
#Published var savedEntites: [MemoryEntity] = []
#Published var selectedEntity: String = ""
init() {
container = NSPersistentContainer(name: "MemoryContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
print("Error loading CoreData. \(error)")
}
}
FetchMemories()
}
func FetchMemories() {
let request = NSFetchRequest<MemoryEntity>(entityName: "MemoryEntity")
do {
savedEntites = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching \(error)")
}
}
}
The scope of entity is inside ForEach brackets,
ForEach(vm.savedEntites) { entity in
} //End scope entity var
If you want to show a present outside ForEach, as a suggestion you could declare in your viewModel a selectedEntity as #Published
List {
ForEach(vm.savedEntites) { entity in
Button(action: {
vm.selectedEntity = entity
showingDetailScreen.toggle()
}) {
}
.sheet(isPresented: $showingDetailScreen) {
DetailItemView(entity: vm.selectedEntity,isVisible: self.$showingDetailScreen)
}

Detail View of a CoreData list item in SwiftUI

I'm trying to pass the details of a CoreData list item on to a separate detail view...
My Item Model
class CoreDataViewModel: ObservableObject {
let container: NSPersistentContainer
#Published var savedEntites: [MemoryEntity] = []
init() {
container = NSPersistentContainer(name: "MemoryContainer")
container.loadPersistentStores { (description, error) in
if let error = error {
print("Error loading CoreData. \(error)")
}
}
FetchMemories()
}
func FetchMemories() {
let request = NSFetchRequest<MemoryEntity>(entityName: "MemoryEntity")
do {
savedEntites = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching \(error)")
}
}
}
My content View
#StateObject var vm = CoreDataViewModel()
List {
ForEach(vm.savedEntites) { entity in
Text(entity.name ?? "No Name")
.font(.system(size: 25))
HStack {
Button(action: {showingDetailScreen.toggle()}) {
Label("View Details", systemImage: "pencil")
}.foregroundColor(Color(.red))
.sheet(isPresented: $showingDetailScreen) {
DetailItemView(isVisible: self.$showingDetailScreen, entity: entity)
}
}
}
Detail View, this is where i want details of the item to be shown. For now i just want the "name" to be displayed on this view.
struct DetailItemView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var isVisible: Bool
var entity: MemoryEntity
var body: some View {
Text("\(entity.name)")
HStack {
Button("Exit") {
self.isVisible = false
}
}
.frame(width: 300, height: 150)
}
}
...But i'm getting this error in DetailItemView right next to Text("\(entity.name)"):
No exact matches in call to instance method 'appendInterpolation'
First of all creating a string from a string with String Interpolation is redundant.
Secondly your code already includes the answer (right after ForEach). As name is an optional you have to unwrap it.
Text(entity.name ?? "No Name")
in DetailItemView do what you did in ContentView use:
Text(entity.name ?? "No Name")

SwiftUI ObservedObject does not updated when new items are added from a different view

I have a view model which handles the loading of new data once the app launches and when a new item is added. I have an issue when it comes to showing new items when are added from a new view, for example, a sheet or even a NavigationLink.
View Model
class GameViewModel: ObservableObject {
//MARK: - Properties
#Published var gameCellViewModels = [GameCellViewModel]()
var game = [GameModel]()
init() {
loadData()
}
func loadData() {
if let retrievedGames = try? Disk.retrieve("games.json", from: .documents, as: [GameModel].self) {
game = retrievedGames
}
self.gameCellViewModels = game.map { game in
GameCellViewModel(game: game)
}
print("Load--->",gameCellViewModels.count)
}
func addNew(game: GameModel){
self.game.append(game)
saveData()
loadData()
}
private func saveData() {
do {
try Disk.save(self.game, to: .documents, as: "games.json")
}
catch let error as NSError {
fatalError("""
Domain: \(error.domain)
Code: \(error.code)
Description: \(error.localizedDescription)
Failure Reason: \(error.localizedFailureReason ?? "")
Suggestions: \(error.localizedRecoverySuggestion ?? "")
""")
}
}
}
View to load the ViewModel data, leading add button is able to add and show data but the trailing which opens a new View does not update the view. I have to kill the app to get the new data.
NavigationView{
List {
ForEach(gameList.gameCellViewModels) { gameList in
CellView(gameCellViewModel: gameList)
}
}.navigationBarTitle("Games Played")
.navigationBarItems(leading: Text("Add").onTapGesture {
let arr:[Int] = [1,2,3]
self.gameList.addNew(game: GameModel(game: arr))
}, trailing: NavigationLink(destination: ContentView()){
Text("Play")
})
}
Play View sample
#State var test = ""
var body: some View {
VStack(){
TextField("Enter value", text: $test)
.keyboardType(.numberPad)
Button(action: {
var arr:[Int] = []
arr.append(Int(self.test)!)
self.gameList.addNew(game: GameModel(game: arr))
}) {
Text("Send")
}
}
}
To what I can see the issue seems to be here:
List {
// Add id: \.self in order to distinguish between items
ForEach(gameList.gameCellViewModels, id: \.self) { gameList in
CellView(gameCellViewModel: gameList)
}
}
ForEach needs something to orientate itself on in order to know what elements are already displayed and which are not.
If this did not solve the trick. Please update the code you provided to Create a minimal, Reproducible Example