JSON with SwiftUI using Array - swift

I am new to SwiftUI and only used UIKit before. I tried to use JSON to show a title but all tutorial videos work with lists. I dont want to use any list with JSON which shows all data. Only want to fetch for example the second or a specific array for title.
How can I remove the list in SwiftUI?
My View:
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
NavigationView {
List(networkManager.posts) { post in
HStack {
Text(String(post.points))
Text(post.title)
}}
.navigationBarTitle("H4X0R NEWS")
}
.onAppear {
self.networkManager.fetchData()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
NetworkManager:
class NetworkManager: ObservableObject {
#Published var posts = [Post]()
func fetchData() {
if let url = URL(string: "https://hn.algolia.com/api/v1/search?tags=front_page") {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil {
let decoder = JSONDecoder()
if let safeData = data {
do {
let results = try decoder.decode(Results.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.hits
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
And my struct files for Json:
struct Results: Decodable {
let hits: [Post]
}
struct Post: Decodable, Identifiable {
var id: String {
return objectID
}
let objectID: String
let points: Int
let title: String
}

I dont want to use any list with JSON which shows all data. Only want
to fetch for example the second or a specific array for title.
You can use a computed property to access the specific element (and its title) from the posts array:
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
// return the title of the second item in the `posts` array
var title: String {
guard networkManager.posts.count >= 2 else {
// decide what to do when data is not yet loaded or count is <= 1
return "Loading..."
}
return networkManager.posts[1].title
}
var body: some View {
NavigationView {
Text(title)
.navigationBarTitle("H4X0R NEWS")
}
.onAppear {
self.networkManager.fetchData()
}
}
}

Related

Unable to show image form Core data on offline Mode In swift UI

I am using AsyncImage to download the image and display it into view. I have created the respective properties into core data including image type is string to save the data locally . I am trying to load the into offline mode , it was able to show the rest of the properties without Image and it showing blank ..
Here is the code for Core Data manager .
class CoreDataManager {
let persistentContainer: NSPersistentContainer
static let shared: CoreDataManager = CoreDataManager()
private init() {
persistentContainer = NSPersistentContainer(name: "ReditModel")
persistentContainer.loadPersistentStores { description, error in
if let error = error {
// fatalError("Unable to initialize Core Data \(error)")
print("Unable to save the data :\(error.localizedDescription)")
}
}
}
}
Here is the code for main ..
#main
struct CoreDataDemoApp: App {
#StateObject private var viewModel = RedditViewModel()
let persistentContainer = CoreDataManager.shared.persistentContainer
var body: some Scene {
WindowGroup {
ContentView().environment(\.managedObjectContext, persistentContainer.viewContext).environmentObject(viewModel)
}
}
}
Here is the view model..
#MainActor
class RedditViewModel: ObservableObject {
#Published private(set) var stories = [Story]()
private var redditService: RedditService
init(redditService: RedditService = RedditService()) {
self.redditService = redditService
}
// Swift 5.5
func fetchData(viewContext: NSManagedObjectContext) async {
let url = NetworkURLs.urlBase
do {
let response = try await redditService.getModel(from: url)
let stories = response.data.children.map { $0.data }
self.stories = stories
saveRecord(viewContext: viewContext)
} catch (let error) {
print(error)
}
}
public func saveRecord(viewContext: NSManagedObjectContext) {
do {
stories.forEach { story in
let redit = ReditEntity(context: viewContext)
redit.title = story.title
redit.numComments = Int64(story.numComments)
redit.score = Int64(story.score)
redit.urlImage = story.thumbnail
}
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
}
Here is the code for Row view .
struct RowView: View {
#EnvironmentObject var viewModel: RedditViewModel
let title: String
let comments: String
let score: String
let urlImage: String?
var body: some View {
VStack(alignment: .leading) {
HStack {
if let urlImage = urlImage, urlImage.contains("https"), let url = URL(string: urlImage) {
AsyncImage(url: url)
}
VStack(alignment: .leading) {
HeadTitleView(title: title)
Text("Comments: \(comments)")
Text("Score: \(score)")
Spacer()
}
}
}
}
}
Here is the content view ..
struct ContentView: View {
#EnvironmentObject private var viewModel: RedditViewModel
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(entity: ReditEntity.entity(), sortDescriptors: [])
private var dbStories: FetchedResults<ReditEntity>
var dbFatchReditRecord: NSFetchRequest<ReditEntity> = ReditEntity.fetchRequest()
var body: some View {
VStack {
Text("Reddit Service")
.font(.largeTitle)
List {
ForEach(dbStories) { story in
// custom cell
RowView(title: story.title ?? "", comments: "\(story.numComments)", score: "\(story.score)", urlImage: story.urlImage)
}
}
}
.onAppear {
if dbStories.isEmpty {
Task {
await viewModel.fetchData(viewContext: viewContext)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let persistedContainer = CoreDataManager.shared.persistentContainer
ContentView().environment(\.managedObjectContext, persistedContainer.viewContext) }
}
Here is the screenshot when I run the app on Offline model ..
Out of curiosity: How are you supposed to be able to load a https url while being offline?
-> urlImage.contains("https")
You should download the image and store it on your local file system. For example give your data model 2 attributes: offlineUrlString and onlineUrlString. Then as a fallback if the online url results in an empty image, then use the offline image. Once you are online again, update the offline image again.

Swift UI unable to fetch data form Core Data

I am trying to save and fetch data form core data by using swift UI . I debug the code , I can see the the data is saved into core data and it has 5 record but the problem is when I tried to reload and re-run it it showing only one record.
Here is the code View Model ..
import Foundation
import CoreData
#MainActor
class RedditViewModel: ObservableObject {
#Published private(set) var stories = [Story]()
private var redditService: RedditService
init(redditService: RedditService = RedditService()) {
self.redditService = redditService
}
// Swift 5.5
func fetchData(viewContext: NSManagedObjectContext) async {
let url = NetworkURLs.urlBase
do {
let response = try await redditService.getModel(from: url)
let stories = response.data.children.map { $0.data }
self.stories = stories
saveRecord(viewContext: viewContext)
} catch (let error) {
print(error)
}
}
public func saveRecord(viewContext: NSManagedObjectContext) {
do {
let redit = ReditEntity(context: viewContext)
stories.forEach { story in
redit.title = story.title
redit.numComments = Int64(story.numComments)
redit.score = Int64(story.score)
redit.urlImage = story.thumbnail
}
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
}
Here is the code for main app ..
import SwiftUI
#main
struct CoreDataDemoApp: App {
#StateObject private var viewModel = RedditViewModel()
let persistentContainer = CoreDataManager.shared.persistentContainer
var body: some Scene {
WindowGroup {
ContentView().environment(\.managedObjectContext, persistentContainer.viewContext).environmentObject(viewModel)
}
}
}
Here is code into content view ..
import SwiftUI
import CoreData
struct ContentView: View {
#EnvironmentObject private var viewModel: RedditViewModel
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(entity: ReditEntity.entity(), sortDescriptors: [])
private var dbStories: FetchedResults<ReditEntity>
var dbFatchReditRecord: NSFetchRequest<ReditEntity> = ReditEntity.fetchRequest()
var body: some View {
VStack {
Text("Reddit Service")
.font(.largeTitle)
List {
ForEach(dbStories) { story in
// custom cell
RowView(title: story.title ?? "", comments: "\(story.numComments)", score: "\(story.score)", urlImage: story.urlImage)
}
}
}
.onAppear {
if dbStories.isEmpty {
Task {
await viewModel.fetchData(viewContext: viewContext)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let persistedContainer = CoreDataManager.shared.persistentContainer
ContentView().environment(\.managedObjectContext, persistedContainer.viewContext) }
}
Here is the code for RowView.swift
import SwiftUI
struct RowView: View {
#EnvironmentObject var viewModel: RedditViewModel
let title: String
let comments: String
let score: String
let urlImage: String?
var body: some View {
VStack(alignment: .leading) {
HStack {
if let urlImage = urlImage, urlImage.contains("https"), let url = URL(string: urlImage) {
AsyncImage(url: url)
}
VStack(alignment: .leading) {
HeadTitleView(title: title)
Text("Comments: \(comments)")
Text("Score: \(score)")
Spacer()
}
}
}
}
}
Here is the code for Header.swift ..
import SwiftUI
struct HeadTitleView: View {
#EnvironmentObject var viewModel: RedditViewModel
let title: String
var body: some View {
Text(title)
}
}
Here is the screenshot ..
let redit = ReditEntity(context: viewContext)
stories.forEach { story in
redit.title = story.title
...
}
Here you are creating an entity object but then you reuse that same object inside the loop so what you end up doing is updating this single object with each story instead of creating a new object for each story.
Simply swap the lines to fix the problem
stories.forEach { story in
let redit = ReditEntity(context: viewContext)
redit.title = story.title
...
}

Why is my function returning an empty array?

I am trying to call the results of this function in my SwiftUI view:
class GetMessages: ObservableObject {
let BASE_URL = "apicallurl.com"
#Published var messages = [Timestamp]()
func fetchMessages() {
guard let url = URL(string: BASE_URL) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard error == nil else {print(error!.localizedDescription); return }
let theData = try! JSONDecoder().decode([String: Timestamp].self, from: data!)
DispatchQueue.main.async {
self.messages = Array(theData.values)
}
}
.resume()
}
}
I am testing the output with a print statement in the onAppear:
struct HomeTab: View {
#StateObject var getMsgs = GetMessages()
var body: some View {
NavigationView {
VStack(spacing: 0) {
greeting.edgesIgnoringSafeArea(.top)
messages
Spacer()
}
.onAppear {
print(getMsgs.fetchMessages())
print(getMsgs.messages)
}
}.navigationBarHidden(true)
}
both print statements print () or []
But when i print print(self.messages) in my GetMessages class the data prints fine.
Why is it empty in my Hometab view?
When you use getMsgs.fetchMessages() it may take some times to fetch the results. Once the results are available
the messages of getMsgs in HomeTab will be updated, and this will trigger a view refresh,
because it is a #StateObject and is "monitored" by the view.
However you should not try to print(getMsgs.messages) before the results are available.
So try the following sample code:
struct HomeTab: View {
#StateObject var getMsgs = GetMessages()
var body: some View {
NavigationView {
List {
ForEach(getMsgs.messages, id: \.self) { msg in
Text("\(msg)")
}
}
.onAppear {
getMsgs.fetchMessages()
// no printing of getMsgs.messages here
}
}.navigationBarHidden(true)
}
}

Preview Crashed when I get data from api

I'm trying to fetch data from API according to a tutorial on YouTube and I does exactly as the video but the preview somehow crashed. But when I commented out the Api().getPosts , the preview is able to resume again. If that line of code is wrong how can I write it instead?
User Interface code:
import SwiftUI
struct ContentView: View {
#State var posts: [Post] = []
var body: some View {
VStack {
List(posts) { post in
Text(post.title)
}
.onAppear{
Api().getPosts { (posts) in
self.posts = posts
}
}
}//:VSTACK
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Api Service code:
import SwiftUI
struct Post: Codable, Identifiable {
var id = UUID()
var title: String
var body: String
}
class Api {
func getPosts(completion: #escaping([Post]) -> ()) {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else { return
print("Something occured!")
}
//CALL
URLSession.shared.dataTask(with: url) { data, response, error in
let posts = try! JSONDecoder().decode([Post].self, from: data!)
DispatchQueue.main.async {
completion(posts)
}
print(posts)
}//:URLSESSION
.resume()
}
}
use this code for your Post model:
struct Post: Codable, Identifiable {
// let id = UUID() // <-- or this
var id: Int // <-- here
var title: String
var body: String
}

Can't access class variables besides from in SwiftIU view

So I'm trying to access my data by id using a url such as http://localhost:8000/albums/whateverid.
First I gain the ids
class Webservice {
func getAllPosts(completion: #escaping ([Post]) -> ()) {
guard let url = URL(string: "http://localhost:8000/albums")
else {
fatalError("URL is not correct!")
}
URLSession.shared.dataTask(with: url) { data, _, _ in
let posts = try!
JSONDecoder().decode([Post].self, from: data!); DispatchQueue.main.async {
completion(posts)
}
}.resume()
}
}
struct Post: Codable, Hashable, Identifiable {
let id: String
let title: String
let path: String
let description: String
}
Set the variables to the data from class Webservice
final class PostListViewModel: ObservableObject {
init() {
fetchPosts()
}
#Published var posts = [Post]()
private func fetchPosts() {
Webservice().getAllPosts {
self.posts = $0
print("posts \(self.posts)")
}
}
}
And this is how I'm trying to grab album by id by using the id I fetched from the code above
I create a class that when a id is inserted will give me the album data back by id
class SecondWebService: Identifiable {
var id:String = ""
init(id: String?) {
self.id = id!
}
func getAllPostsById(completion: #escaping ([PostById]) -> ()) {
guard let url = URL(string: "http://localhost:8000/albums/\(id)")
else {
fatalError("URL is not correct!")
}
URLSession.shared.dataTask(with: url) { data, _, _ in
let posts = try!
JSONDecoder().decode([PostById].self, from: data!); DispatchQueue.main.async {
completion(posts)
}
}.resume()
}
}
Variables
struct PostById: Codable, Hashable, Identifiable {
let id: String
let name: String?
let path: String
}
Here's where I try to insert the id from class PostListViewModel into my class SecondWebService to get the data back set my variables to that data
final class PostListViewByIdModel: ObservableObject {
#ObservedObject var model = PostListViewModel()
init() {
fetchPostsById()
}
#Published var postsById = [PostById]()
private func fetchPostsById() {
for post in model.posts {
SecondWebService(id: post.id).getAllPostsById {
self.postsById = $0
print("postById \(post)")
}
}
}
}
For some reason above when I try to print nothing will display because I believe posts in model.posts isn't getting read
When I use it here in List() it works but not in init:
struct ContentView: View {
#ObservedObject var model = PostListViewModel()
init() {
for post in model.posts {
print(post)
}
}
var body: some View {
NavigationView {
List(model.posts) { post in
VStack{
Text("Title: ").bold()
+ Text("\(post.title)")
NavigationLink(destination: Album(post: post)) {
ImageView(withURL: "http://localhost:8000/\(post.path.replacingOccurrences(of: " ", with: "%20"))")
}
Text("Description: ").bold()
+ Text("\(post.description)")
}
}
}
}
}
I'm very curious on why nothing is printing when I use model.posts in my for loop. Only when I use it in the SwiftUI functions does it work.
I'm very curious on why nothing is printing when I use model.posts in my for loop
It is because of asynchronous nature of the following call
private func fetchPosts() {
Webservice().getAllPosts {
so in init
#ObservedObject var model = PostListViewModel() // just created
init() {
for post in model.posts { // posts are empty because
// `Webservice().getAllPosts` has not finished yet
print(post)
}
Update: You need to call second service after first one got finished, here is possible approach (only idea - cannot test)
struct ContentView: View {
#ObservedObject var model = PostListViewModel()
#ObservedObject var model2 = PostListViewByIdModel()
// delete init() - it is not needed here
...
var body: some View {
NavigationView {
List(model.posts) { post in
...
}
.onReceive(model.$posts) { posts in // << first got finished
self.model2.fetchPostsById(for: posts) // << start second
}
.onReceive(model2.$postsById) { postById in
// do something here
}
}
}
and updated second service
final class PostListViewByIdModel: ObservableObject {
#Published var postsById = [PostById]()
func fetchPostsById(for posts: [Post]) { // not private now
for post in model.posts {
SecondWebService(id: post.id).getAllPostsById {
self.postsById = $0
print("postById \(post)")
}
}