I have been working on an News App, in that i want to check Whether user is opening the app first time in a day, if yes then fetch the ArticlesData json from API or if no then fetch it from coreData which is previously got saved.
My 2 Entites and i have set the codeGen of both these 2 Entites to Manual/None in CodeGen Config and both these entites have one to on relationShip
NSManagedSubclass for both these entites – manualy created which is confirmable to Decodable
//Article.swift
import Foundation
import CoreData
class Article: NSManagedObject, Decodable {
@NSManaged var title: String?
@NSManaged var articleDescription: String?
@NSManaged var content: String?
@NSManaged var url: String?
@NSManaged var image: String?
@NSManaged var publishedAt: String?
@NSManaged var source: Source?
@NSManaged var category: String?
enum CodingKeys: String, CodingKey {
case title, articleDescription = "description", content, url, image, publishedAt, source, category
}
required convenience init(from decoder: Decoder) throws {
// Extract managed object context from the decoder's userInfo dictionary
guard let context = decoder.userInfo[CodingUserInfoKey.managedObjectContext] as? NSManagedObjectContext else {
throw DecoderConfigurationError.missingManagedObjectContext
}
// Initialize the Article with the managed object context
self.init(context: context)
// Decode other properties
let container = try decoder.container(keyedBy: CodingKeys.self)
self.title = try container.decode(String.self, forKey: .title)
self.articleDescription = try container.decode(String.self, forKey: .articleDescription)
self.content = try container.decode(String.self, forKey: .content)
self.url = try container.decode(String.self, forKey: .url)
self.image = try container.decode(String.self, forKey: .image)
self.publishedAt = try container.decode(String.self, forKey: .publishedAt)
self.category = try container.decode(String.self, forKey: .category)
// Decode Source entity
let sourceData = try container.decode(SourceData.self, forKey: .source)
// Create and associate source
let source = Source(context: context)
source.name = sourceData.name
source.url = sourceData.url
self.source = source
}
}
//Source.swift
import Foundation
import CoreData
class Source: NSManagedObject, Decodable {
@NSManaged var name: String? // Adjust the type based on your data model
@NSManaged var url: String?
enum CodingKeys: CodingKey {
case name, url
}
required convenience init(from decoder: Decoder) throws {
// Extract managed object context from the decoder's userInfo dictionary
guard let context = decoder.userInfo[CodingUserInfoKey.managedObjectContext] as? NSManagedObjectContext else {
throw DecoderConfigurationError.missingManagedObjectContext
}
// Initialize the Source with the managed object context
self.init(context: context)
// Decode other properties
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.url = try container.decode(String.self, forKey: .url)
}
}
//CoreDataStack.swift
import Foundation
import CoreData
class CoreDataStack {
static let shared = CoreDataStack()
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "ArticlesCD")
container.loadPersistentStores { _, error in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
return container
}()
var viewContext: NSManagedObjectContext {
return persistentContainer.viewContext
}
func saveContext() {
if viewContext.hasChanges {
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
//Main Logic FetchData.swift
import Foundation
import CoreData
class FetchNews {
private let lastLaunchDateKey = "LastLaunchDate"
func fetchData(overRideValue : Bool = false, category: String, context: NSManagedObjectContext, completion: @escaping ([ArticleData]) -> ()) {
let isFirstLaunchOfDay = checkIsFirstLaunchOfDay(overRideValue: overRideValue)
if isFirstLaunchOfDay {
// Proceed with fetching data
let mainURL = "https://gnews.io/api/v4/top-headlines?category=\(category)"
let apikey = "apiKEY"
let remainingPart = "&lang=en&country=ind&max=10&apikey=\(apikey)"
let urlString = mainURL + remainingPart
let url = URL(string: urlString)
let urlSession = URLSession.shared.dataTask(with: url!) { (data, response, error) in
if let _ = error {
print("Problem in fetching the data")
}
if let data = data {
do {
let receivedArticles = try JSONDecoder().decode(ArticlesData.self, from: data)
DispatchQueue.main.async {
self.saveToCoreData(articles: receivedArticles.articles, category: category, context: context)
self.updateLastLaunchDate()
print("Fetched The data from api")
completion(receivedArticles.articles)
}
} catch {
print("Error in the struct or code")
}
}
}
urlSession.resume()
} else if(!isFirstLaunchOfDay) {
// Fetch data from Core Data, it's not the first launch of the day
let existingArticles = fetchArticlesFromCoreData(category: category, context: context)
print("Fetched The data from coreData")
completion(existingArticles)
}
}
private func saveToCoreData(articles: [ArticleData], category: String, context : NSManagedObjectContext) {
deleteArticles(for: category, context: context)
for articleData in articles {
let article = Article(context: context)
article.title = articleData.title
article.articleDescription = articleData.description
article.content = articleData.content
article.url = articleData.url
article.image = articleData.image
article.publishedAt = articleData.publishedAt
article.category = category
// Assuming you have a Source entity
let source = Source(context: context)
source.name = articleData.source.name
source.url = articleData.source.url
article.source = source
}
do {
try context.save()
print("Data saved to Core Data")
} catch {
print("Error saving data to Core Data: \(error)")
}
}
private func fetchArticlesFromCoreData(category: String, context: NSManagedObjectContext) -> [ArticleData] {
let request = NSFetchRequest<Article>(entityName: "Article")
request.predicate = NSPredicate(format: "category == %@", category)
do {
let fetchedArticles = try context.fetch(request)
return fetchedArticles.map { article in
// Map Core Data Article objects to ArticleData
return ArticleData(
title: article.title ?? "",
description: article.articleDescription ?? "",
content: article.content ?? "",
url: article.url ?? "",
image: article.image ?? "",
publishedAt: article.publishedAt ?? "",
source: SourceData(name: article.source?.name ?? "", url: article.source?.url ?? "")
)
}
} catch {
print("Error fetching articles from Core Data: \(error)")
return []
}
}
private func updateLastLaunchDate() {
let userDefaults = UserDefaults.standard
userDefaults.set(Date(), forKey: lastLaunchDateKey)
}
private func checkIsFirstLaunchOfDay(overRideValue : Bool = false) -> Bool {
if(overRideValue == true){
return true
}
let userDefaults = UserDefaults.standard
let lastLaunchDate = userDefaults.object(forKey: lastLaunchDateKey) as? Date
let calendar = Calendar.current
let currentDate = Date()
if let lastLaunchDate = lastLaunchDate, calendar.isDate(currentDate, inSameDayAs: lastLaunchDate) {
// Not the first launch of the day
return false
}
return true
}
private func deleteArticles(for category: String, context: NSManagedObjectContext) {
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Article")
request.predicate = NSPredicate(format: "category == %@", category)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: request)
do {
try context.execute(deleteRequest)
print("Deleted existing articles for category: \(category)")
} catch {
print("Error deleting existing articles: \(error)")
}
}
}
//I am accessing that Main logic from below view allArticlesListView.swift
import SwiftUI
struct allArticlesListView: View {
@Environment(\.managedObjectContext) private var viewContext
@Environment(\.colorScheme) var colorScheme
var fetchNews = FetchNews()
@State var title = "Sports"
@State var isGotData = false
@State var articles : [ArticleData] = []
var body: some View {
if isGotData {
ScrollView(.vertical){
VStack(spacing : 0){
VStack(alignment: .leading,spacing: 15) {
HStack{
Text("TOP NEWS")
.font(.largeTitle.bold())
.frame(height: 45)
.padding(.horizontal,15)
}
GeometryReader{
let rect = $0.frame(in: .scrollView(axis : .vertical))
let minY = rect.minY
let topValue : CGFloat = 75.0
let offset = min(minY - topValue, 0)
let progress = max(min( -offset / topValue, 1), 0)
let scale = 1 + progress
ZStack{
Rectangle()
.fill(Color.blue)
.overlay(alignment: .leading) {
Circle()
.fill(Color.blue)
.overlay {
Circle()
.fill(.white.opacity(0.2))
}
.scaleEffect(2,anchor: .topLeading)
.offset(x: -50,y: -40)
}
.clipShape(RoundedRectangle(cornerRadius: 25,style: .continuous))
.scaleEffect(scale,anchor: .bottom)
VStack(alignment: .center, spacing: 4){
Text("\(title)".capitalized)
.font(.title.bold())
}
.foregroundStyle(.white)
.frame(maxWidth: .infinity, alignment : .leading)
.padding(15)
.offset(y : progress * -25 )
}
.offset(y : -offset)
//Moving till top value
.offset(y : progress * -topValue)
}
.padding(.horizontal,15)
.containerRelativeFrame(.horizontal)
.frame(height: 125)
LazyVStack(spacing: 15) {
ForEach(articles, id: \.self) { article in
NavigationLink(destination: detailedArticleView(singleArticle: article)) {
VStack(alignment: .leading) {
ArticleView(article: article, category: title)
}
}
}
}
.padding(15)
.mask{
Rectangle()
.visualEffect { content, geometryProxy in
content
.offset(y : backgroundLimitOffset(geometryProxy))
}
}
.background{
GeometryReader {
let rect = $0.frame(in: .scrollView)
let minY = min(rect.minY - 125, 0)
let progress = max(min(-minY / 25, 1), 0)
RoundedRectangle(cornerRadius: 30 * progress,style: .continuous)
.fill(colorScheme == .dark ? .black : .white)
.overlay(alignment: .top){
}
/// Limiting Background Scroll below the header
.visualEffect { content, geometryProxy in
content
.offset(y : backgroundLimitOffset(geometryProxy))
}
}
}
}
.padding(.vertical,15)
}
}
.scrollTargetBehavior(CustomScrollBehaviourSample())
.scrollIndicators(.hidden)
.background {
LinearGradient(colors: [.green, .white,.cyan,.indigo], startPoint: .topLeading, endPoint: .bottomTrailing)
.ignoresSafeArea()
}
}else{
ProgressView()
.onAppear {
fetchNews.fetchData(overRideValue: false, category: title, context: viewContext) { fetchedArticles in
articles = fetchedArticles
isGotData = true
print("Fetched articles")
}
}
}
}
/// Background Limit Offset
func backgroundLimitOffset(_ proxy : GeometryProxy) -> CGFloat {
let minY = proxy.frame(in: .scrollView).minY
return minY < 75 ? -minY + 75 : 0
}
}
//I am calling the above view from this below view CategoryView.swift
import SwiftUI
struct CategoryView: View {
private var twoColumnGrid = [GridItem(.flexible()), GridItem(.flexible())]
let categories : [String] = [Category.world,Category.nation,Category.business,Category.technology,Category.entertainment,Category.sports,Category.science,Category.health]
var body: some View {
ScrollView(){
LazyVGrid(columns: twoColumnGrid, spacing: 20) {
// Display the item
ForEach(categories, id: \.self) { category in
NavigationLink {
allArticlesListView(title : category)
.navigationTitle("")
.navigationBarTitleDisplayMode(.inline)
} label: {
Rectangle()
.overlay(
content: {
Image("\(category)")
.resizable()
.scaledToFill() // Use scaledToFill to fill the entire rectangle
}
)
.overlay(alignment: .bottomLeading) {
ZStack {
Text(category)
.font(.callout)
.padding(6)
.foregroundColor(.white)
}
.background(Color.black)
.opacity(0.8)
.cornerRadius(10.0)
.padding(6)
}
.frame(maxWidth: 150, minHeight: 150)
.clipped() // Ensure the content is clipped to the rectangle
.clipShape(UnevenRoundedRectangle(topLeadingRadius: 20, bottomLeadingRadius: 20, bottomTrailingRadius: 20, topTrailingRadius: 0, style: .continuous))
.padding()
}
.navigationBarTitleDisplayMode(.large)
}
}
}
.padding()
.scrollIndicators(.hidden)
}
}
I am calling the above view inside Content.swift View and Content View From @main VIEW
import SwiftUI
struct ContentView: View {
@State private var selectionIndex = 0
var body: some View {
if (AuthService.shared.currentUser != nil) {
TabView(selection: $selectionIndex){
allArticlesListView()
.tabItem {
Label("Categories", systemImage: "rectangle.portrait.on.rectangle.portrait.angled.fill")
}
.tag("0")
.navigationTitle("Hello")
MainView()
.tabItem {
Label("CatchUp👆🏽", systemImage: "rectangle.portrait.on.rectangle.portrait.angled.fill")
}
.tag("2")
VStack{
Text("Go Back")
Button {
do {
try AuthService.shared.signOut()
} catch {
print(error.localizedDescription)
}
} label: {
Text("Sign Out")
}
}
.tabItem {
Label("More", systemImage: "rectangle.portrait.on.rectangle.portrait.angled.fill")
}
.tag("1")
}
}else if(AuthService.shared.firstLogin){
onBoardingView()
}else{
SignInView()
}
}
}
import SwiftUI
import FirebaseCore
@main
struct TrendTroveApp: App {
let coreDataStack = CoreDataStack.shared
init(){
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
NavigationStack{
ContentView()
.environment(\.managedObjectContext, coreDataStack.viewContext)
}
}
}
}
If I run the above code i am getting error : I debugged the code and found which line is producing error – When i call the fetchNews.fetchData Method inside the allArticlesListView.swift, it will go to that method in that FetchNews Class it will go into else block of that method which calls fetchArticlesFromCoreData Method in that method the line 1 inside the do block code is giving me the error “let fetchedArticles = try context.fetch(request)”. Eventhough I am doing this in do catch block the error is not getting catched.
Intresting thing is : When i pass the allArticlesListView() [which already has defualt values] in the content view instead of CategoryView() – The code is working super fine.
I know it is long code but the thing is the code is working fine in one way so it means there is no problem from entites, thier manual declarations, they confirming to decodable and their relationship.
Problem Might be in CoreDataStack, how i am passing persistance container and logic in FetchNews or any backGround thread running unkowingly or some other problem. But to show the full picture i have added everything. It would be great if someOne analyse this and provide some solutions or suggestions, As a junior developer i am open to discuss and learn.