swiftui list doens't appear but array isn't empty - list

I am working on a Swiftui file that loads data from Firebase.
It did work but when I added things it suddenly stopt working...
I tried to strip it back down but I can't get it working again.
Does anyone know what I do wrong?
import SwiftUI
import Firebase
struct Fav: View {
#StateObject var loader = Loader()
var body: some View {
ScrollView {
if loader.userfav.count != 0 {
List (loader.userfav, id: \.id) { fav in
Text(fav.name.capitalized)
}
}
else
{
Text("You haven't added favorits yet...")
}
}
.onAppear{
loader.loadfav(loadfavorits: "asd")
}
.navigationBarTitle("")
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
func deletefav (docid: String) {
print(docid)
}
}
struct Fav_Previews: PreviewProvider {
static var previews: some View {
Fav()
}
}
and the loader file
import Foundation
import Firebase
import FirebaseFirestore
class Loader : ObservableObject {
private var db = Firestore.firestore()
#Published var userfav = [fav]()
func loadfav (loadfavorits: String) {
userfav = [fav]()
db.collection("favo").whereField("user", isEqualTo: loadfavorits).getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting favorits: \(err.localizedDescription)")
}
else
{
for fav in querySnapshot!.documents {
let brand = fav.get("brand") as! String
let store = fav.get("store") as! String
let name = fav.get("name") as! String
let type = fav.get("type") as! String
let docid = fav.get("docid") as! String
self.userfav.append(fav(brand: brand, store: store, name: name, type: type, docid: docid))
}
}
}
}
}
It doesn't show the Text("You haven't added favorits yet...")
So that means dat loader.userfav.count is not empty

Having a List embedded in a ScrollView (which also scrolls) can lead to layout problems. Remove the outer ScrollView and the issue will be solved.

Related

Error: Thread 1: "executeFetchRequest:error: A fetch request must have an entity."

I'm trying to use CoreData in my Xcode-project (SwiftUI). I've created a Player entity and wanna use it in my View called "YouView". But when I'm trying to fetch the data, I get the error from Title. My app is called Dart Tools. My Application Language is German, so don't worry if you don't understand everything of the ui :).
Thanks for helping!
I already tried the .shared variant - same error
This is my .xcdatamodeld file (called DataModel.xcdatamodeld)
This is the Code of my DataController file:
import Foundation
import CoreData
class DataController: ObservableObject {
let container = NSPersistentContainer(name: "DataModel")
init() {
container.loadPersistentStores { desc, error in
if let error = error {
print("Daten wurden nicht geladen: \(error.localizedDescription)")
}
}
}
func save(context: NSManagedObjectContext) {
do {
try context.save()
print("Daten wurden gespiechert!")
} catch {
print("Daten konnten nicht gespeichert werden.")
}
}
func addPlayer(name: String, isUser: Bool, context: NSManagedObjectContext) {
let player = Player(context: context)
player.id = UUID()
player.name = name
player.isUser = isUser
save(context: context)
}
func editPlayerName(player: Player, name: String, context: NSManagedObjectContext) {
player.name = name
save(context: context)
}
}
Here is my DartToolsApp.swift file:
import SwiftUI
//The error is here:
#main
struct DartToolsApp: App {
#StateObject private var userDefaults = UserDefaults()
#StateObject private var dataController = DataController()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, DataController().container.viewContext)
.environmentObject(UserDefaults())
}
}
}
This is the file where I want to use the Data:
Btw, this view is a piece of a TabView.
import SwiftUI
import CoreData
struct YouView: View {
#EnvironmentObject var userDefaults: UserDefaults
// I guess the error is because of this line:
#FetchRequest(sortDescriptors: []) var players: FetchedResults<Player>
#Environment(\.managedObjectContext) var managedObjectContext
var body: some View {
NavigationStack {
Form {
}
.navigationTitle("\(findUser()) (Du)")
}
}
func findUser() -> String {
if let index = players.firstIndex(where: { $0.isUser }) {
let output = players[index].name!
return output
}
else {
return ""
}
}
}
This is the code to create the user:
DataController().addPlayer(name: nameText, isUser: true, context: managedObjectContext)
Wow, that was a lot of code. I hope you can help me!
You are creating new instances of DataController everywhere which is a problem in itself but what think causes your particular error is when you assign the \.managedObjectContext environment variable.
So instead of creating a new instance you should use your #StateObject instance instead.
So change this in the App code
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, dataController.container.viewContext)
}
}

#EnvironmentVariable not being passed to child views

I have 2 views - which I want to navigate between, and have a viewModel object shared between them as an EnvironmentObject. I keep getting the "A View.environmentObject(_:) for TidesViewModel may be missing as an ancestor of this view." error - but none of the solutions I have found seem to work. Please find below my code. The following is the first view:
import SwiftUI
struct ContentView: View {
#ObservedObject var tidesViewModel: TidesViewModel = TidesViewModel()
var body: some View {
NavigationView
{
List
{
ForEach (tidesViewModel.stations.indices) {
stationid in
HStack
{
NavigationLink(destination: TideDataView(stationId: tidesViewModel.stations[stationid].properties.Id))
{
Text(tidesViewModel.stations[stationid].properties.Name)
}
}
}
}
}.environmentObject(tidesViewModel)
}
}
and below is the child view - which throws the error.
import SwiftUI
struct TideDataView: View {
#EnvironmentObject var tidesViewModel : TidesViewModel
var stationId: String
init(stationId: String) {
self.stationId = stationId
getTidesForStation(stationId: stationId)
}
var body: some View {
List
{
ForEach (tidesViewModel.tides.indices)
{
tideIndex in
Text(tidesViewModel.tides[tideIndex].EventType)
}
}
}
func getTidesForStation(stationId: String)
{
tidesViewModel.getTidalData(forStation: stationId)
}
}
For completeness - below is the Observable object being passed:
import Foundation
import SwiftUI
class TidesViewModel: ObservableObject
{
private var tideModel: TideModel = TideModel()
var currentStation: Feature?
init()
{
readStations()
}
var stations: [Feature]
{
tideModel.features
}
var tides: [TidalEvent]
{
tideModel.tides
}
func readStations()
{
let stationsData = readLocalFile(forName: "stations")
parseStations(jsonData: stationsData!)
}
private func readLocalFile(forName name: String) -> Data? {
do {
if let bundlePath = Bundle.main.path(forResource: name,
ofType: "json"),
let jsonData = try String(contentsOfFile: bundlePath).data(using: .utf8) {
return jsonData
}
} catch {
print(error)
}
return nil
}
private func parseStations(jsonData: Data) {
do {
let decodedData: FeatureCollection = try JSONDecoder().decode(FeatureCollection.self,
from: jsonData)
//print(decodedData)
tideModel.features = decodedData.features
} catch let jsonError as NSError{
print(jsonError.userInfo)
}
}
func getTidalData(forStation stationId: String)
{
let token = "f43c068141bb417fb88909be5f68781b"
guard let url = URL(string: "https://admiraltyapi.azure-api.net/uktidalapi/api/V1/Stations/" + stationId + "/TidalEvents") else {
fatalError("Invalid URL")
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue(token, forHTTPHeaderField: "Ocp-Apim-Subscription-Key")
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else{ return }
do{
let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64
let decodedData = try decoder.decode([TidalEvent].self, from: data)
DispatchQueue.main.async {
self.tideModel.tides = decodedData
}
}catch let error{
print(error)
}
}.resume()
}
}
You need to attach the modifier .environmentObject() directly to the view that will receive it. So, in your case, attach it to TideDataView, not to the NavigationView around it.
Your code would look like this:
NavigationLink {
TideDataView(stationId: tidesViewModel.stations[stationid].properties.Id)
.environmentObject(tidesViewModel)
} label: {
Text(tidesViewModel.stations[stationid].properties.Name)
}
// Delete the modifier .environmentObject() attached to the NavigationView

How to make a SwiftUI DocumentGroup app without starting on the file picker?

If a user uses the Document App template in Xcode to create a SwiftUI app, macOS starts them off with a new document. This is good. I can work with that to present onboarding UI within a new document.
However, with that same app running on iOS, the user is instead greeted by the stock document view controller to create or pick a document.
This is not helpful in that I don't have a way to present onboarding or any other custom information.
I did notice that if you add a WindowGroup to the Scene, the app will display that window group. But then I don't know how to get the user to the picker UI.
Has anyone figured out how to do things like present onboarding on top of this DocumentGroup-based app?
Here is a full document app
import SwiftUI
import UniformTypeIdentifiers
#main
struct DocumentTestApp: App {
var body: some Scene {
DocumentGroup(newDocument: DocumentTestDocument()) { file in
ContentView(document: file.$document)
}
}
}
struct ContentView: View {
#Binding var document: DocumentTestDocument
var body: some View {
TextEditor(text: $document.text)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(document: .constant(DocumentTestDocument()))
}
}
extension UTType {
static var exampleText: UTType {
UTType(importedAs: "com.example.plain-text")
}
}
struct DocumentTestDocument: FileDocument {
var text: String
init(text: String = "Hello, world!") {
self.text = text
}
static var readableContentTypes: [UTType] { [.exampleText] }
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let string = String(data: data, encoding: .utf8)
else {
throw CocoaError(.fileReadCorruptFile)
}
text = string
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = text.data(using: .utf8)!
return .init(regularFileWithContents: data)
}
}
App shows first window scene by default, so place first on-boarding window scene and afterwards a DocumentGroup. Somewhere at the end of on boarding process (success path) call document controller to create new document (DocumentGroup is based on same NSDocumentController engine inside).
Update: below is for macOS
*just recognised that original question is for iOS
So a possible approach is
#main
struct DocumentTestApp: App {
var body: some Scene {
WindowGroup("On-Boarding") {
// ContentView()
// In some action at the end of this scene flow
// just close current window and open new document
Button("Demo") {
NSApp.sendAction(#selector(NSWindow.performClose(_:)), to: nil, from: nil)
NSDocumentController.shared.newDocument(nil)
}
}
DocumentGroup(newDocument: DocumentTestDocument()) { file in
ContentView(document: file.$document)
}
}
}
Alright friends, here is a nice and hacky way to get things going, reaching into the key windows, and setting up onboarding/paywall/whatever you want!
import SwiftUI
#main
struct ExampleApp: App {
#StateObject var captain = Captain()
var body: some Scene {
DocumentGroup(newDocument: ExampleOfDocumentGroupAndOnboardingPaywallDocument()) { file in
ContentView(document: file.$document)
}
}
}
class Captain: ObservableObject {
var onboardingSheet: UIViewController?
#objc func loadData() {
onboardingSheet = try? OnboardingOrPaywall(dismissHandler: dismissSheet).presentFromDocumentGroup()
}
func dismissSheet() {
onboardingSheet?.dismiss(animated: true)
}
init() {
NotificationCenter.default.addObserver(self,
selector: #selector(loadData),
name: UIApplication.didBecomeActiveNotification,
object: nil)
}
}
public protocol DocumentGroupSheet: View {}
struct OnboardingOrPaywall: DocumentGroupSheet {
var dismissHandler: () -> Void
var body: some View {
Button("Done") {
dismissHandler()
}
Text("Let me introduce you to this delicious app!")
}
}
public enum DocumentGroupSheetError: Error {
case noParentWindow
}
public extension DocumentGroupSheet {
func presentFromDocumentGroup() throws -> UIViewController {
let window = UIApplication.shared.activeKeyWindows.first
let parent = window?.rootViewController
guard let parent = parent else { throw DocumentGroupSheetError.noParentWindow }
let sheet = UIHostingController(rootView: body)
sheet.modalPresentationStyle = .fullScreen
parent.present(sheet, animated: false, completion: nil)
return sheet
}
}
public extension UIApplication {
var activeWindowScenes: [UIWindowScene] {
connectedScenes
.filter { $0.activationState == .foregroundActive }
.compactMap { $0 as? UIWindowScene }
}
var activeWindows: [UIWindow] {
activeWindowScenes
.flatMap { $0.windows }
}
var activeKeyWindows: [UIWindow] {
activeWindows
.filter { $0.isKeyWindow }
}
}

SwiftUI List is working but not the picker

I have this view. The list works fine by showing the data. The picker is not working. It does not display any data. Both use the same objects and functions. I do not know the reason for the picker not showing data. I want to use the picker. I placed the List just to try to determine the problem but I still don't know.
import SwiftUI
struct GameListPicker: View {
#ObservedObject var gameListViewModel = GameListViewModel()
#State private var selectedGameList = ""
var body: some View {
VStack{
List(gameListViewModel.gameList){gameList in
HStack {
Text(gameList.gameName)
}
}
.onAppear() {self.gameListViewModel.fetchData()}
Picker(selection: $selectedGameList, label: Text("")){
ForEach(gameListViewModel.gameList) { gameList in
Text(gameList.gameName)
}
}
.onAppear() {self.gameListViewModel.fetchData()}}
}
}
This is the GameListViewModel
import Foundation
import Firebase
class GameListViewModel: ObservableObject{
#Published var gameList = [GameListModel]()
let db = Firestore.firestore()
func fetchData() {
db.collection("GameData").addSnapshotListener {(querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.gameList = documents.map { queryDocumentSnapshot -> GameListModel in
let data = queryDocumentSnapshot.data()
let gameName = data["GameName"] as? String ?? ""
return GameListModel(id: gameName, gameName: gameName)
}
}
}
}
This is the GameListModel
import Foundation
struct GameListModel: Codable, Hashable,Identifiable {
var id: String
//var id: String = UUID().uuidString
var gameName: String
}
Thanks in advance for any help
Picker is not designed to be dynamic, so it is not refreshed when data updated, try to force-rebuild picker when data changed, like below
Picker(selection: $selectedGameList, label: Text("")){
ForEach(gameListViewModel.gameList) { gameList in
Text(gameList.gameName)
}
}.id(gameListViewModel.gameList) // << here !!

Why doesn't this SwiftUI View update when changing a #State variable?

I've got a SwiftUI View that takes a Hacker News API submission ID, and then fetches the details for that item in fetchStory().
When fetchStory() completed its HTTP call, it updates the #State private var url on the View, however the View never re-renders to show the new value -- it always shows the initial empty value.
Why?
import Foundation
import SwiftUI
struct StoryItem: Decodable {
let title: String
let url: String?
}
struct StoryView: View {
public var _storyId: Int
#State private var url: String = "";
init(storyId: Int) {
self._storyId = storyId
self.fetchStory()
}
var body: some View {
VStack {
Text("(Load a webview here for the URL of Story #\(self._storyId))")
Text("URL is: \(self.url)") // this never changes!
}
}
private func fetchStory() {
let url = URL(string: "https://hacker-news.firebaseio.com/v0/item/\(self._storyId).json")!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else {
print(String(error.debugDescription))
return
}
do {
let item: StoryItem = try JSONDecoder().decode(StoryItem.self, from: data)
if let storyUrl = item.url {
self.url = storyUrl
} else {
print("No url")
}
} catch {
print(error)
}
}
task.resume()
}
}
struct StoryView_Previews: PreviewProvider {
static var previews: some View {
StoryView(storyId: 22862053)
}
}
something like this:
import SwiftUI
struct StoryItem: Decodable {
let title: String
let url: String?
}
class ObservedStoryId: ObservableObject {
#Published var storyId: String = ""
init(storyId: String) {
self.storyId = storyId
}
}
struct StoryView: View {
#ObservedObject var storyId: ObservedStoryId
#State var url: String = ""
var body: some View {
VStack {
Text("(Load a webview here for the URL of Story #\(self.storyId.storyId))")
Text("URL is: \(self.url)")
Text(self.storyId.storyId)
}.onReceive(storyId.$storyId) { _ in self.fetchStory() }
}
private func fetchStory() {
let url = URL(string: "https://hacker-news.firebaseio.com/v0/item/\(self.storyId.storyId).json")!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else {
print(String(error.debugDescription))
return
}
do {
let item: StoryItem = try JSONDecoder().decode(StoryItem.self, from: data)
if let storyUrl = item.url {
self.url = storyUrl
} else {
print("No url")
}
} catch {
print(error)
}
}
task.resume()
}
}
and call it like this:
struct ContentView: View {
#ObservedObject var storyId = ObservedStoryId(storyId: "22862053")
var body: some View {
StoryView(storyId: storyId)
}
}
to fix your problem:
init(storyId: Int) {
self._storyId = storyId
}
var body: some View {
VStack {
Text("(Load a webview here for the URL of Story #\(self._storyId))")
Text("URL is: \(self.url)") // this now works
}.onAppear(perform: fetchStory)
}
why does this works and not your code,
my guess is this: "self.url" can only be updated/changed within the special SwiftUI View functions, such as onAppear(), elsewhere it does not change a thing.