Why am I not able to connect my CoreData in SwiftUI? - swift

Disclaimer: I am trying to learn the basics of IOS development, so this question might be very basic.
I'm currently building out my first IOS project, which consists of pulling a random Poem from an API and then giving the user the possibility to save that poem to a "Saved Poem" list. My app currently has a Poem Detail screen (ContentView) and a Home Page screen (where the saved poem list will be).
I've attempted to follow Paul Hudson's tutorial on CoreData (https://www.youtube.com/watch?v=7_Afen3PlDE&ab_channel=PaulHudson). Currently, my goal is to save a poem once the "Bookmark" button on the Detail Screen is tapped. Once a poem saved to CoreData, I would like to display it in a list on the home page.
Code for the Detail View (which includes the Bookmark button)
import SwiftUI
struct ContentView: View {
#ObservedObject var fetch = FetchPoem()
#Environment(\.managedObjectContext) var moc
var currentDate = Text(Date().addingTimeInterval(600), style: .date)
var body: some View {
VStack {
HStack{
Button(action: {}) {
Image(systemName: "arrow.backward")
.font(.system(size: 25, weight: .heavy))
.foregroundColor(.black)
}
Spacer(minLength: 0)
Button(action: {
let savedpoem = SavedPoem(context: self.moc)
savedpoem.id = UUID()
savedpoem.title = "\(poem.title)"
savedpoem.author = "\(poem.author)"
savedpoem.lines = "\(joined)"
try? self.moc.save()
}) {
Image(systemName: "bookmark")
.font(.system(size: 25, weight: .heavy))
.foregroundColor(.black)
}
}
.padding(.vertical, 10)
ScrollView {
VStack {
HStack{
VStack (alignment: .leading) {
Text("Today's Poem, \(currentDate)")
.font(.subheadline)
.foregroundColor(Color.gray)
.padding(.bottom, 20)
.padding(.top, 10)
if let poem = fetch.poems.first {
let joined = poem.lines.joined(separator: "\n")
Text("\(poem.title)")
.font(.largeTitle)
.fontWeight(.heavy)
.foregroundColor(.black)
.padding(.bottom, 20)
.lineSpacing(0)
Text("BY "+poem.author.uppercased())
.font(.subheadline)
.foregroundColor(Color.gray)
.padding(.bottom, 20)
HStack {
Text("\(joined)")
.font(.body)
.foregroundColor(.black)
.padding(.bottom)
.lineSpacing(5)
Spacer()
}
} else {
Spacer()
}
}
}
}
}
Button("Get Next Poem") { fetch.getPoem() }
}
.background(Color.white.ignoresSafeArea())
.padding(.horizontal)
}
Code for the Home Page View
import SwiftUI
import CoreData
struct HomeView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: SavedPoem.entity(), sortDescriptors: []) var savedpoems:FetchedResults<SavedPoem>
var body: some View {
VStack{
List{
ForEach(savedpoems, id: \.id) { savedpoem in
Text(savedpoem.name ?? "Unkown")
}
}
}
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView()
}
}
My CoreData Data Model
And finally, my Data Model to pull from the API:
import Foundation
struct Poem: Codable, Hashable {
let title, author: String
let lines: [String]
let linecount: String
}
class FetchPoem: ObservableObject {
// 1.
#Published var poems = [Poem]()
init() {
getPoem()
}
func getPoem() {
let url = URL(string: "https://poetrydb.org/random/1")!
// 2.
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let poemData = data {
// 3.
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
Unfortunately, this code doesn't build and is throwing me the following errors:
On the Home Page:
Cannot find type 'SavedPoem' in scope
Cannot infer key path type from context; consider explicitly specifying a root type
Generic parameter 'Content' could not be inferred
Missing argument for parameter 'content' in call
In Detail View:
Cannot Find "Poem" in Scope
Any ideas? Thanks in advance.

Related

Having trouble with showing another view

I'm currently trying to input another listview in my contentView file to test if it'll show, but for some reason it isn't showing the list. I'm having a bit of trouble understanding why this is happening as I am not receiving any error message.
This is the code for the list file
import SwiftUI
extension Image{
func anotherImgModifier() -> some View{
self
.resizable()
.scaledToFill()
.frame( width: 75, height: 75)
.cornerRadius(9)
}
}
struct PokeListView: View {
#State var imgURL: String = ""
#EnvironmentObject var pokeWebService: PokeWebService
//functions
// func loadImage() async -> [Image]{
// for
// }
var body: some View {
NavigationView {
List( pokeWebService.pokeList?.results ?? [], id: \.id){ pokemon in
NavigationLink(destination: PokeDetailsView(urlString: pokemon.url, counter: 4, name: pokemon.name)) {
AsyncImage(url:URL(string: "https://play.pokemonshowdown.com/sprites/bw/\(pokemon.name).png")){ image in
image.anotherImgModifier()
}
placeholder: {
Image(systemName: "photo.circle.fill").iconModifer()
}.padding(40)
Text(pokemon.name.uppercased()).font(.system(size: 15, weight: .heavy, design: .rounded))
.foregroundColor(.gray)
.task{
do{
try await pokeWebService.getPokemonFromPokemonList(from: pokemon.url)
} catch{
print("---> task error: \(error)")
}
}
}
}
}
.task {
do{
try await pokeWebService.getPokemonList()
} catch{
print("---> task error: \(error)")
}
}
}
}
struct PokeListView_Previews: PreviewProvider {
static var previews: some View {
PokeListView()
.previewLayout(.sizeThatFits)
.padding()
.environmentObject(PokeWebService())
}
}
This is the code for the ContentView where I was trying to input the list file.
import SwiftUI
struct ContentView: View {
#StateObject var newsWebService = NewsWebService()
#StateObject var pokeWebService = PokeWebService()
let gbImg = Image("pokeball").resizable()
#State private var gridLayout: [GridItem] = [ GridItem(.flexible()), GridItem(.flexible())]
#State private var gridColumn: Int = 2
#State var selection: Int? = nil
var body: some View {
NavigationView{
ScrollView(.vertical, showsIndicators: false, content: {
VStack(alignment: .center, spacing: 15, content: {
Spacer()
NewsCapsule()
//GRID
//BERRIES, POKEMON, GAMES
GroupBox(label: Label{
Text("PokéStuff")
} icon: {
Image("pokeball").resizable().scaledToFit().frame(width: 30, height: 30, alignment: .leading)
}
, content: {
PokeListView()
}).padding(.horizontal, 20).foregroundColor(.red)
})//:VSTACK
})//:SCROLLVIEW
.navigationBarTitle("Pokemon",displayMode: .large)
.toolbar(content: {
ToolbarItem(placement: .navigationBarTrailing, content: {
Image(systemName: "moon.circle")
.resizable()
.scaledToFit()
.font(.title2)
.foregroundColor(.red)
})
})
}//:NAVIGATIONBAR
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(NewsWebService())
.environmentObject(PokeWebService())
}
}
How would I get to fix this?
EDIT-1:
with further tests, this is what worked for me:
in ContentView, add .frame(height: 666) to the VStack {...}.
This is the reason why you do not see anything. You need a frame height.
Also in ContentView, add .environmentObject(pokeWebService) to the NavigationView,
and just use PokeListView(). This is to pass the pokeWebService
to that view. After that, all works for me. You may want to experiment
with different frame sizes and such likes. You should also remove the NavigationView from your PokeListView, there is no need for it.

How could I make certain menu items appear depending on the navigation "bar" I press

I am building an app that will store multiple Nintendo consoles and their details (kinda like Mactracker but for Nintendo stuff).
I wanna store certain consoles in categories on the main menu but I'm not sure how I could do it (I'm pretty new to swiftui so sorry if that's a stupid question).
I already have categories set on MainMenu based on the category I've put in my consoles array. But I can't get the console menu (where all the consoles are) to store the consoles only of the category I tap on.
My consoles array has multiple consoles but I've put just one to save space.
Main Menu:
import SwiftUI
struct MainMenu: View {
var con: [ConsoleDetails] = ConsoleList.consoles
var body: some View {
NavigationView{
List(ConsoleList.categories.sorted(by: {$0.key > $1.key}), id:\.key){con in
NavigationLink(destination: ConsoleMenu(), label:{
Image(systemName: "folder.fill")
.scaledToFit()
.frame(height: 30)
.cornerRadius(4)
.padding(.vertical, 4)
VStack{
Text(con.key)
.fontWeight(.semibold)
}
}).navigationTitle("app.name")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MainMenu()
}
}
ConsoleMenu:
import SwiftUI
struct ConsoleMenu: View {
var con: [ConsoleDetails] = ConsoleList.consoles
var body: some View {
NavigationView{
List(con, id:\.id){ cons in
NavigationLink(destination: ConsoleDetailView(con: cons), label:{
Image(cons.imgName)
.resizable()
.scaledToFit()
.frame(height: 50)
.cornerRadius(4)
.padding(.vertical, 4)
VStack{
Text(cons.consoleName)
.fontWeight(.semibold)
}
}).navigationTitle("\(cons.category)")
}
}
}
}
struct ConsoleSection_Previews: PreviewProvider {
static var previews: some View {
ConsoleMenu()
.preferredColorScheme(.dark)
}
}
Consoles:
import Foundation
struct ConsoleDetails: Identifiable{
let id = UUID()
var imgName: String = ""
var consoleName: String = ""
var description: String = ""
var initialPrice: Double = 0.0
var ReleaseDate: String = ""
var Discontinuation: Int = 0
var category: String = ""
}
struct ConsoleList{
static let categories = Dictionary(grouping: consoles, by: {$0.category } )
static let consoles = [
ConsoleDetails(imgName: "FAMICOM",
consoleName: "Famicom",
description: "It was released in 1983 in Japan and due to it success it gave birth to the NES",
initialPrice: 179,
ReleaseDate: "Release Date: July 15, 1983",
Discontinuation: 2003,
category: "Home Consoles"),
//there's more consoles but I just put one to save space on here
Few modifications:
In main view use only categories and pass only useful consoles to console menu :
struct MainMenu: View {
// Use categories ordered by reversed alphabetical order
var categories = ConsoleList.categories.sorted(by: {$0.key > $1.key})
// var con: [ConsoleDetails] = ConsoleList.consoles
var body: some View {
NavigationView{
// Loop on categories
List(categories, id:\.key){category in
NavigationLink(destination: ConsoleMenu(con: category.value), label:{
Image(systemName: "folder.fill")
.scaledToFit()
.frame(height: 30)
.cornerRadius(4)
.padding(.vertical, 4)
VStack{
Text(category.key)
.fontWeight(.semibold)
}
}).navigationTitle("app.name")
}
}
}
}
In Console menu use the consoles furnished by main menu
struct ConsoleMenu: View {
// Consoles are given by caller
var con: [ConsoleDetails] /* = ConsoleList.consoles */
var body: some View {
NavigationView{
List(con, id:\.id){ cons in
NavigationLink(destination: ConsoleDetailView(con: cons), label:{
Image(cons.imgName)
.resizable()
.scaledToFit()
.frame(height: 50)
.cornerRadius(4)
.padding(.vertical, 4)
VStack{
Text(cons.consoleName)
.fontWeight(.semibold)
}
}).navigationTitle("\(cons.category)")
}
}
}
}

How do I pass a var from one viewModel to a new viewModel in my .fullScreenCover() View?

I'm building an app with swiftUI & firebase/firestore. I open a fullscreenCover sheet of a product selected from a catalog and the user can add it to a list below it. The product is selected on the previous page, and that is passed onto the .fullScreeenCover, where I'm introducing a new ViewModel for the list.
Where I'm getting confused: How do I pass the product ID passed into this fullScreenCover view into my newly introduced list's viewModel so that I can run the "add to list" function?
ViewModel for my List:
import SwiftUI
import Firebase
class ListViewModel: ObservableObject {
let var product: Product
#Published var userList = [List]()
#Published var list: List
init(list: List) {
self.list = list
}
func fetchList() {
let docRef = Firestore.firestore().collection("List")
guard let uid = AuthViewModel.shared.userSession?.uid else { return }
docRef.whereField("uid", isEqualTo: uid).getDocuments { snapshot, _ in
guard let documents = snapshot?.documents else { return }
self.userList = documents.map({ List(dictionary: $0.data())} )}
}
func AddProductToList(product: Product, list: List) {
let listRef = Firestore.firestore().collection("List").document(list.id).collection("Products")
let productRef = Firestore.firestore().collection("Products").document(product.id)
productRef.getDocument { snapshot, _ in
listRef.document(self.product.id).setData([:]) { _ in
print("\(self.product.title) was saved to \(self.list.name)")
}
}
}
}
Code for .fullScreenCover() sheet View
import SwiftUI
struct ListCoverView: View {
#Binding var isPresented: Bool
let viewModel: LikeViewModel
#StateObject var listViewModel = ListViewModel(list)
var body: some View {
ZStack {
VStack (spacing: 10) {
VStack {
WebImage(url: URL(string: viewModel.product.image))
.resizable()
.frame(width: 220, height: 220)
.padding(.top,10)
Text("\(viewModel.product.title)")
.fontWeight(.bold)
.foregroundColor(.black)
.padding(.horizontal)
Text(viewModel.product.company)
.foregroundColor(.black)
.fontWeight(.bold)
}
.onAppear(perform: {
// Fetch products
listViewModel.fetchList()
})
ScrollView {
VStack {
Button(action: listViewModel.AddProductToList(Product)) {
ForEach(listViewModel.userList){list in
ListRow(list: listViewModel.list, viewModel: listViewModel)
}
}
}
}
}
}
Spacer()
Button(action: {isPresented.toggle()}, label : {
Text("Close")
.font(.system(size: 20))
.foregroundColor(.black)
})
.padding()
}
}
Parent View of the .fullScreenCover()
import SwiftUI
import SDWebImageSwiftUI
struct CardView: View {
let product: Product
#ObservedObject var viewModel: LikeViewModel
#State private var isShowingNewListSheet = false
init(product: Product) {
self.product = product
self.viewModel = LikeViewModel(product: product)
}
var body: some View {
VStack {
WebImage(url: URL(string: product.image))
.resizable()
.aspectRatio(contentMode: .fit)
Text(product.title)
.fontWeight(.bold)
.foregroundColor(.black)
.padding(.horizontal)
Text(product.company)
.foregroundColor(.black)
.fontWeight(.semibold)
.padding(.trailing,65)
HStack{
Button(action: {
viewModel.didLike ? viewModel.UnlikeProduct() : viewModel.LikeProduct()
}, label : {
Image(systemName: viewModel.didLike ? "heart.fill" : "heart")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(viewModel.didLike ? .red : .black)
})
.padding(.trailing,5)
Button(action: { isShowingNewListSheet.toggle()
}, label : {
Image(systemName: "square.and.arrow.down")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(.black)
})
.fullScreenCover(isPresented: $isShowingNewListSheet) {
ListCoverView(isPresented: $isShowingNewListSheet, viewModel: viewModel)
}
}
}
.padding(.bottom)
.background(Color(.white))
.cornerRadius(15)
}
}

SwiftUI 2 Observable objects in one view which depend on each other

Thanks for all the support I have received, I trying to build an macos app that tags pdfs for machine learning purposes. I have followed Stanford SwiftUI course, and I want to create main view for my app that contains the document and to type a regex string to find in the document. The deal is I need to create a document chooser, to add documents to be analized, but I don't know how to deal with 2 view models in the same view. In fact one of those view models depend on the other one. The solution I found (not a solution a messy workaround) is to initialize the document manager as a separate view and use it as a navigation view, with a navigation link, but the look is horrible. I'll paste the code and explain it better.
This is the stores view
struct PDFTaggerDocumentStoreView: View {
#EnvironmentObject var store:PDFTaggerDocumentStore
var body: some View {
NavigationView {
List {
Spacer()
Text("Document Store").fontWeight(.heavy)
Button(action: {self.store.addDocument()}, label: {Text("Add document")})
Divider()
ForEach(store.documents){ doc in
NavigationLink(destination: PDFTaggerMainView(pdfTaggerDocument: doc)) {
Text(self.store.name(for: doc))
}
}
.onDelete { indexSet in
indexSet.map{self.store.documents[$0]}.forEach { (document) in
self.store.removeDocuments(document)
}
}
}
}
}
}
The main view.
struct PDFTaggerDocumentView: View {
#ObservedObject var document:PDFTaggerDocument
#State private var expression = ""
#State private var regexField = ""
#State private var showExpressionEditor = false
var body: some View {
VStack {
ZStack {
RoundedRectangle(cornerRadius: 15, style: .continuous)
.stroke(Color.white, lineWidth: 0.5)
.frame(width: 600)
.padding()
VStack {
Text("Try expression ")
HStack {
TextField("Type regex", text: $document.regexString)
.frame(width: 200)
Image(nsImage: NSImage(named: "icons8-save-80")!)
.scaleEffect(0.3)
.onTapGesture {
self.showExpressionEditor = true
print(self.document.regexString)
print(self.regexField)
}
.popover(isPresented: $showExpressionEditor) {
ExpressionEditor().environmentObject(self.document)
.frame(width: 200, height: 300)
}
}
Picker(selection: $expression, label: EmptyView()) {
ForEach(document.expressionNames.sorted(by: >), id:\.key) { key, value in
Text(key)
}
}
Button(action: self.addToDocument, label: {Text("Add to document")})
.padding()
.frame(width: 200)
}
.frame(width:600)
.padding()
}
.padding()
Rectangle()
.foregroundColor(Color.white).overlay(OptionalPDFView(pdfDocument: document.backgroundPDF))
.frame(width:600, height:500)
.onDrop(of: ["public.file-url"], isTargeted: nil) { (providers, location) -> Bool in
let result = providers.first?.hasItemConformingToTypeIdentifier("public.file-url")
providers.first?.loadDataRepresentation(forTypeIdentifier: "public.file-url") { data, error in
if let safeData = data {
let newURL = URL(dataRepresentation: safeData, relativeTo: nil)
DispatchQueue.main.async {
self.document.backgroundURL = newURL
}
}
}
return result!
}
}
}
I'd like to be able to initialize both views models in the same view, and make the document view, be dependent on the document chooser model.
Is there a way I can do it?
Thanks a lot for your time.

Asynchronous Image Loading Bug

I am hitting a strange bug when it comes to Asynchronous Image Loading, where when I enter a view, the shows up like it is suppose to do, then for some reason the image drops and all I see is the "Loading..." placeholder. I used this tutorial when building my loader and the following script is my Article View. I have a Global Functions file, which includes reference to Combine and Foundation for my various functions through out the app. I am just not fully understanding why the image is showing for a brief moment, then calls the placeholder. Thanks!
import SwiftUI
struct ArticleView: View {
#Environment(\.imageCache) var cache: ImageCache
var articleID: Int
#ObservedObject private var data = Result2()
let defaults = UserDefaults.standard
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
init(articleID: Int){
self.articleID = articleID
self.data.results.append(Story.init(id: 0, title: "test", image: "", story: "", published: "", author: ""))
self.loadArticle(CDNLink: "http://\(self.defaults.object(forKey: "domain") as! String)/cdn?funct=fetchArticle&articleID=\(self.articleID)")
}
var backBtn: some View {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label:{
Image(systemName: "lessthan.circle")
.resizable()
.frame(width: 30, height: 30)
.foregroundColor(Color.primaryRed)
.padding(.top, 6)
.padding(.leading, 10)
})
}
var body: some View {
VStack(alignment: .leading){
ScrollView {
ForEach(data.results, id: \.id) { result in
Group{
AsyncImage(
url: URL(string: result.image)!,
placeholder: Text("Loading..."), configuration: { $0.resizable() })
.frame(height: 250)
.aspectRatio(contentMode: .fill)
VStack(alignment: .leading) {
Text("\(result.title)")
.font(.system(size: 18))
.fontWeight(.heavy)
.foregroundColor(.primary)
.multilineTextAlignment(.leading)
HStack {
Text("\(result.author)")
.font(.system(size: 12))
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
Text("|")
.font(.system(size: 12))
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
Text("\(result.published)")
.font(.system(size: 12))
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
}.frame(width: UIScreen.main.bounds.size.width - 40, alignment: .leading)
}.frame(width: UIScreen.main.bounds.size.width - 40)
}
Text("\(result.story)")
}
}
}
.frame(maxWidth: .infinity)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: backBtn)
}
func loadArticle(CDNLink: String) {
guard let url = URL(string: CDNLink) else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if error != nil {
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
return
} else {
do {
let decodedResponse = try JSONDecoder().decode(Response2.self, from: data!)
print(decodedResponse)
DispatchQueue.main.async {
self.data.results.append(contentsOf: decodedResponse.results)
self.data.results.remove(at: 0)
}
} catch let err {
print("Error parsing: \(err)")
}
}
}.resume()
}
}
struct Response2: Codable {
var results: [Story]
}
struct Story: Codable, Identifiable {
var id: Int
var title: String
var image: String
var story: String
var published: String
var author: String
}
struct ArticleView_Previews: PreviewProvider {
static var previews: some View {
ArticleView(articleID: 0)
}
}
Edit: I have made the changes suggested and I am still seeing the error, you can see the error live by checking out this link.
Hi most likely it is due to the fact that you are using #ObservedObject. This object is discarded and initialized every time your view state changes and is triggering a view re-render. Try using #StateObject instead of #ObservedObject. The #StateObject will be initialized only once in the view.
A #StateObject can be also used with an ObservedObject model. It is kind of a combination of #ObservedObject and #State.
Okay so I created a new solution to patch this issue up and I am very glad that this little work around is working. So, I started looking at what I could do server side to optimize content being pulled into the app. What I did was use PHP to encode the image with base64, then the app pulls it in and decodes the base64 data and the image populates with lightning speed!
Server Side code:
$article['image'] = base64_encode(file_get_contents(PATH . "/cache/content/topstory/" . $row['app_article']));
Client Side code:
let dataDecoded:NSData = NSData(base64Encoded: result.image, options: NSData.Base64DecodingOptions(rawValue: 0))!
let decodedimage:UIImage = UIImage(data: dataDecoded as Data)!
Image(uiImage: decodedimage)
.frame(height: 250)
.aspectRatio(contentMode: .fill)