I have a api that give me the follong response:
[{
"id": 1,
"title": "Learn Grinding"
}, {
"id": 2,
"title": "See the messure".
}]
and in my view I like to view this in a list. Later I like to save it into Core Data. But for testing, i just like to see the values in a list. But there is nothing to see :(
The list is empty. With the same content I try to use the Apple-Example with the itunes-List and this work in this ViewController.
With the first Button I test, if I can add a value to the database, and this works fine. Only the List of the json-data would not be read and viewed.
import SwiftUI
struct Response: Codable {
var results: [Result]
}
struct Result: Codable {
var id: Int
var title: String
}
struct TutorialsView: View {
#StateObject var viewRouter: ViewRouter
#State private var results = [Result]()
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: Tutorials.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Tutorials.title, ascending: true)]
) var tutorials: FetchedResults<Tutorials>
var body: some View {
VStack {
Text("Tutorials")
.bold()
Button(action: {
let tutorial = Tutorials(context: managedObjectContext)
tutorial.title = "Describe how to messure"
PersistenceController.shared.save()
}, label: {
Text("Add Tutorial")
})
List {
ForEach(tutorials, id:\.self) { tutorial in
Text("\(tutorial.title ?? "Unknown")")
}
}
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.title)
.font(.headline)
}
}
.task {
await loadData()
}
}
}
func loadData() async {
guard let url = URL(string: "https://example.org") else {
print("Invalid URL")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
results = decodedResponse.results
}
} catch {
print("Invalid data")
}
}
}
struct TutorialsView_Previews: PreviewProvider {
static var previews: some View {
TutorialsView(viewRouter: ViewRouter())
}
}
When I change the url from the itunes to my own, I got this error in the console:
2022-02-17 14:36:19.868821+0100 GrindingCalculator[2460:27599] [boringssl]
boringssl_metrics_log_metric_block_invoke(151) Failed to log metrics
I also try to use a iPhone and install it on this device and there also would be nothing viewed.
Can you see, where my mistake is?
Related
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.
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
...
}
My aim is the change data in DetailView(). Normally in this case I'm using #State + #Binding and it's works fine with static data, but when I trying to update ViewModel with data from network request I'm loosing functionality of #State (new data doesn't passing to #State value it's stays empty). I checked network request and decoding process - everything ok with it. Sorry my code example a bit long but it's the shortest way that I found to recreate the problem...
Models:
struct LeagueResponse: Decodable {
var status: Bool?
var data: [League] = []
}
struct League: Codable, Identifiable {
let id: String
let name: String
var seasons: [Season]?
}
struct SeasonResponse: Codable {
var status: Bool?
var data: LeagueData?
}
struct LeagueData: Codable {
let name: String?
let desc: String
let abbreviation: String?
let seasons: [Season]
}
struct Season: Codable {
let year: Int
let displayName: String
}
ViewModel:
class LeagueViewModel: ObservableObject {
#Published var leagues: [League] = []
init() {
Task {
try await getLeagueData()
}
}
private func getLeagueData() async throws {
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api-football-standings.azharimm.site/leagues")!)
guard let leagues = try? JSONDecoder().decode(LeagueResponse.self, from: data) else {
throw URLError(.cannotParseResponse)
}
await MainActor.run {
self.leagues = leagues.data
}
}
func loadSeasons(forLeague id: String) async throws {
let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api-football-standings.azharimm.site/leagues/\(id)/seasons")!)
guard let seasons = try? JSONDecoder().decode(SeasonResponse.self, from: data) else {
throw URLError(.cannotParseResponse)
}
await MainActor.run {
if let responsedLeagueIndex = leagues.firstIndex(where: { $0.id == id }),
let unwrappedSeasons = seasons.data?.seasons {
leagues[responsedLeagueIndex].seasons = unwrappedSeasons
print(unwrappedSeasons) // successfully getting and parsing data
}
}
}
}
Views:
struct ContentView: View {
#StateObject var vm = LeagueViewModel()
var body: some View {
NavigationView {
VStack {
if vm.leagues.isEmpty {
ProgressView()
} else {
List {
ForEach(vm.leagues) { league in
NavigationLink(destination: DetailView(league: league)) {
Text(league.name)
}
}
}
}
}
.navigationBarTitle(Text("Leagues"), displayMode: .large)
}
.environmentObject(vm)
}
}
struct DetailView: View {
#EnvironmentObject var vm: LeagueViewModel
#State var league: League
var body: some View {
VStack {
if let unwrappedSeasons = league.seasons {
List {
ForEach(unwrappedSeasons, id: \.year) { season in
Text(season.displayName)
}
}
} else {
ProgressView()
}
}
.onAppear {
Task {
try await vm.loadSeasons(forLeague: league.id)
}
}
.navigationBarTitle(Text("League Detail"), displayMode: .inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
ChangeButton(selectedLeague: $league)
}
}
}
}
struct ChangeButton: View {
#EnvironmentObject var vm: LeagueViewModel
#Binding var selectedLeague: League // if remove #State the data will pass fine
var body: some View {
Menu {
ForEach(vm.leagues) { league in
Button {
self.selectedLeague = league
} label: {
Text(league.name)
}
}
} label: {
Image(systemName: "calendar")
}
}
}
Main goals:
Show selected league seasons data in DetailView()
Possibility to change seasons data in DetailView() when another league was chosen in ChangeButton()
You update view model but DetailView contains a copy of league (because it is value type).
The simplest seems to me is to return in callback seasons, so there is possibility to update local league as well.
func loadSeasons(forLeague id: String, completion: (([Season]) -> Void)?) async throws {
// ...
await MainActor.run {
if let responsedLeagueIndex = leagues.firstIndex(where: { $0.id == id }),
let unwrappedSeasons = seasons.data?.seasons {
leagues[responsedLeagueIndex].seasons = unwrappedSeasons
completion?(unwrappedSeasons) // << here !!
}
}
}
and make task dependent on league id so selection would work, like:
struct DetailView: View {
#EnvironmentObject var vm: LeagueViewModel
#State var league: League
var body: some View {
VStack {
// ...
}
.task(id: league.id) { // << here !!
Task {
try await vm.loadSeasons(forLeague: league.id) {
league.seasons = $0 // << update local copy !!
}
}
}
Tested with Xcode 13.4 / iOS 15.5
Test module is here
One question is if you already made LeagueViewModel an ObservableObject, why don't you display from it directly, and simply pass an id to your DetailView?
So your detail view will be:
struct DetailView: View {
#EnvironmentObject var vm: LeagueViewModel
#State var id: String
var body: some View {
VStack {
if let unwrappedSeasons = vm.leagues.first { $0.id == id }?.seasons {
List {
ForEach(unwrappedSeasons, id: \.year) { season in
Text(season.displayName)
}
}
} else {
ProgressView()
}
}
.task(id: id) {
Task {
try await vm.loadSeasons(forLeague: id)
}
}
.navigationBarTitle(Text("League Detail"), displayMode: .inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
ChangeButton(selectedId: $id)
}
}
}
}
The view will automatically update season data as it loads them.
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()
}
}
}
I am trying to fetch data from localhost, make a list of posts with List View and pass data to CustomDetailView. Here is my code for NetworkManager:
My ListView:
And StoryDetails View:
So what I have to pass to StoryDeatils_Preview?
Here is the StoryDetails code
import SwiftUI
struct StoryDetails: View {
var story: Story
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("story #123456")
.font(.callout)
.foregroundColor(Color.gray)
Spacer()
Text("5 days ago")
.font(.callout)
.foregroundColor(Color.gray)
Button(action:{
print("Hello there")
}){
Image(systemName:"info.circle").resizable()
.frame(width:22.0, height:22.0)
.accentColor(Color.gray)
}
}
Text(story.body)
.foregroundColor(.black)
.kerning(1)
.lineLimit(nil)
HStack {
Button(action: {
print("Hello World")
}){
HStack {
Image(systemName:"heart")
.accentColor(.black)
Text("233")
.foregroundColor(.black)
}
.padding(.trailing)
HStack {
Image(systemName:"bubble.left")
.accentColor(.black)
Text("45")
.foregroundColor(.black)
}
}
}
}
}
}
struct StoryDetails_Previews: PreviewProvider {
static var previews: some View {
StoryDetails(
story: Story(
id: 1,
author: 1,
body: "Testing",
edited_time: "September 2019",
pub_date: "October 2018",
comments: [Comment](),
story_likes: [StoryLike]()
)
)
}
}
Error:
Hi there first I need to see the StoryDetails() but if StoryDetails a Story it should be declared inside as var story: Story let me explain more in example code:
Here you can see my network manager class:
class NetworkManager: ObservableObject {
let url: String = "https://jsonplaceholder.typicode.com/todos/1"
var objectWillChange = PassthroughSubject<NetworkManager, Never>()
init() {
fetchData()
}
#Published var user: User? {
didSet {
objectWillChange.send(self)
print("set user")
}
}
func fetchData() {
guard let url = URL(string: url) else {return}
print("fetch data")
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard error == nil else {return}
print("no error")
guard let data = data else {return}
print("data is valid")
let user = try! JSONDecoder().decode(User.self, from: data)
DispatchQueue.main.async {
self.user = user
}
}.resume()
}
}
that's my content view where network manager is initialized inside:
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
VStack {
DetailsView(user: networkManager.user)
}
}
}
Details view struct holds user variable:
struct DetailsView: View {
var user: User?
var body: some View {
VStack {
Text("id: \(user?.id ?? 0)")
Text("UserID: \(user?.userId ?? 0 )")
Text("title: \(user?.title ?? "Empty")")
}
}
}
and that's the DetailsView as you can see inside of this struct I declared a user object of type User need to be pass it so if I want to show it in PreviewProvider it would be like the code below
struct DetailsView_Previews: PreviewProvider {
static var previews: some View {
DetailsView(user: User(id: 0, userId: 0, title: "hello", completed: false)
}
}
model:
struct User: Decodable {
var userId: Int = 0
var id: Int = 0
var title: String = ""
var completed: Bool = false
}
PS: For sure you can unwrap better than this way to provide
any nil exception it's just POC