SwiftUI: Execute functions in Preview Provider - swiftui

I think I'm going about this SwiftUI thing all wrong. It's clear that we're just defining the layout as a structs and there can be limited conventional programming embroiled in the layout. I'm having difficulties thinking like this. What is the best way of doing this?
Take the example below. Project is an NSManagedObject. All I want to do is pass in example record so the SwiftUI will render. Nothing I try works.
struct ProjectView: View
{
#State var project: Project //NSManagedObject
var body: some View
{
TextField("", text: Binding<String>($project.projectName)!)
}
}
struct ProjectView_Previews: PreviewProvider
{
static var previews: some View
{
var p:Project
p = getFirstProject() //returns a Project
return ProjectView(project: p)
}
}
If I try returning the struct it says it cannot preview in the file.
If I don't return the struct I get a Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type error.
UPDATE:
var app = UIApplication.shared.delegate as! AppDelegate
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
lazy var persistentContainer: NSPersistentCloudKitContainer = {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
lazy var managedObjectContext: NSManagedObjectContext =
{
return persistentContainer.viewContext
}()
}
And the rest of the code:
func allRecords<T: NSManagedObject>(_ type : T.Type, sort: NSSortDescriptor? = nil) -> [T]
{
let context = app.managedObjectContext
let request = T.fetchRequest()
if let sortDescriptor = sort
{
request.sortDescriptors = [sortDescriptor]
}
do
{
let results = try context.fetch(request)
return results as! [T]
}
catch
{
print("Error with request: \(error)")
return []
}
}
func getCount() -> String
{
let r = allRecords(Project.self)
return String(r.count)
}
struct ProjectView: View
{
// #ObservedObject var project: Project
var body: some View
{
Text(getCount())
// TextField("", text: Binding<String>($project.projectName)!)
}
}
struct ProjectView_Previews: PreviewProvider
{
static var previews: some View
{
ProjectView()
}
}
r.count is returning 0, but in the main application thread it is returning 8. Has app.managedObjectContext not been defined properly? I think this has just got too complicated too quickly.

Assuming getFirstProject works correctly the following should work
struct ProjectView_Previews: PreviewProvider
{
static var previews: some View
{
ProjectView(project: getFirstProject())
}
}
However there are concerns about the following...
struct ProjectView: View
{
#State var project: Project //NSManagedObject
because #State is designed to be internal view state-only thing, but Project in your case is a model, so the recommended scenario for this is to use ObservableObject view model either by conforming Project or as standalone clue class holding Project instance(s).

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)
}
}

Updating SwiftUI from HealthKit Query

I want to output the variable 'healthStore.valueTest' via ContentView in SwiftUI.
The class healtStore is structured as follows:
class HealthStore {
var healthStore: HKHealthStore?
var query: HKStatisticsQuery?
var valueTest: HKQuantity?
init() {
if HKHealthStore.isHealthDataAvailable() {
healthStore = HKHealthStore()
}
}
func calculateBloodPressureSystolic() {
guard let bloodPressureSystolic = HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic) else {
// This should never fail when using a defined constant.
fatalError("*** Unable to get the bloodPressure count ***")
}
// let startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date())
// let anchorDate = Date.mondayAt12AM()
// let daily = DateComponents(day: 1)
// let predicate = HKQuery.predicateForSamples(withStart: startDate, end: Date(), options: .strictStartDate)
query = HKStatisticsQuery(quantityType: bloodPressureSystolic,
quantitySamplePredicate: nil,
options: .discreteAverage) {
query, statistics, error in
DispatchQueue.main.async{
self.valueTest = statistics?.averageQuantity()
}
}
healthStore!.execute(query!)
}
}
ContentView is built as follows:
import SwiftUI
import HealthKit
struct ContentView: View {
private var healthStore: HealthStore?
init() {
healthStore = HealthStore()
}
var body: some View {
Text("Hello, world!")
.padding().onAppear(){
if let healthStore = healthStore {
healthStore.requestAuthorization { success in
if success {
healthStore.calculateBloodPressureSystolic()
print(healthStore.query)
print(healthStore.valueTest)
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The value for the variable self.valueTest is assigned in the process DispatchQueue.main.async. Nevertheless, I get only a nil back when querying via ContentView.
You could set up your HealthStore class and use it as an EnvironmentObject. Assuming your app uses the SwiftUI lifecycle you can inject HealthStore into the environment in the #main entry point of your app.
import SwiftUI
#main
struct NameOfYourHeathApp: App {
let healthStore = HealthStore()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(healthStore)
}
}
}
Change your HealthStore class to this. (I removed your commented out code in my sample below)
import HealthKit
class HealthStore: ObservableObject {
var healthStore: HKHealthStore?
var query: HKStatisticsQuery?
var valueTest: HKQuantity?
init() {
if HKHealthStore.isHealthDataAvailable() {
healthStore = HKHealthStore()
}
}
// I moved the HealthStore conditional check out of your View logic
// and placed it here instead.
func setUpHealthStore() {
let typesToRead: Set = [
HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic)!
]
// I left the `toShare` as nil as I did not dig into adding bloodpressure reading to HealthKit.
healthStore?.requestAuthorization(toShare: nil, read: typesToRead, completion: { success, error in
if success {
self.calculateBloodPressureSystolic()
}
})
}
func calculateBloodPressureSystolic() {
guard let bloodPressureSystolic = HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic) else {
// This should never fail when using a defined constant.
fatalError("*** Unable to get the bloodPressure count ***")
}
query = HKStatisticsQuery(quantityType: bloodPressureSystolic,
quantitySamplePredicate: nil,
options: .discreteAverage) {
query, statistics, error in
DispatchQueue.main.async{
self.valueTest = statistics?.averageQuantity()
}
}
healthStore!.execute(query!)
}
}
Then use it in your ContentView like this.
import SwiftUI
struct ContentView: View {
#EnvironmentObject var healthStore: HealthStore
var body: some View {
Text("Hello, world!")
.onAppear {
healthStore.setUpHealthStore()
}
}
}
I didn't go through the trouble of setting up the proper permissions in the .plist file, but you'll also need to set up the Health Share Usage Description as well as Health Update Usage Description. I assume you have already done this but I just wanted to mention it.

How to initialize a Property from one struct in an other struct

I was looking for some Code (SwiftUI) to get a JSON File from a website into my Xcode Project, and I found a good Sample but when I try to change the Code and wanna use a 'var' from one struct in an other struct.
struct Course: Decodable, Identifiable, Hashable {
let id: Int
let name: String
var link: String
let imageUrl: String
let number_of_lessons: Int
}
class NetworkManager: ObservableObject {
#Published var courses = [Course]()
func getAllCourses() {
guard let url = URL(string: "https://api.letsbuildthatapp.com/jsondecodable/courses") else { return }
URLSession.shared.dataTask(with: url) { data, response, error in
do {
let courses = try JSONDecoder().decode([Course].self, from: data!)
DispatchQueue.main.async {
self.courses = courses
print(courses)
}
} catch {
print("Failed To decode: ", error)
}
}.resume()
}
init() {
getAllCourses()
}
}
struct SwiftUIView: View {
var cs:Course
var body: some View {
Text(cs.name)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView() //here it says Missing argument for parameter...
}
Xcode says: Missing argument for parameter 'cs' in call, Insert 'cs:<#Course#>'.
As you are trying to initialize a struct, you need to pass all properties with setters, here that is cs, as body is a get-only property.
You will need to pass a Course object via SwiftUIView(cs: course). This course can be just a static course, as it's only used in your SwiftUI preview.
Swift autogenerates initializers for structs, so your SwiftUIView has one like this:
public init(cs: Course) {
self.cs = cs
}
So in order to create an instance of your SwiftUIView in the previews you have to declare it like this:
struct ContentView_Previews: PreviewProvider {
static let cs = Course(id: 7,
name: "The best course",
link: "thebestcourse.com",
imageUrl: "thebestcourse.com/image.jpg",
number_of_lessons: 5)
static var previews: some View {
SwiftUIView(cs: cs)
}
}

SwiftUI: How to persist #Published variable using UserDefaults?

I want a #Published variable to be persisted, so that it's the same every time when I relaunch my app.
I want to use both the #UserDefault and #Published property wrappers on one variable. For example I need a '#PublishedUserDefault var isLogedIn'.
I have the following propertyWrapper
import Foundation
#propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
This is my Settings class
import SwiftUI
import Combine
class Settings: ObservableObject {
#Published var isLogedIn : Bool = false
func doLogin(params:[String:String]) {
Webservice().login(params: params) { response in
if let myresponse = response {
self.login = myresponse.login
}
}
}
}
My View class
struct HomeView : View {
#EnvironmentObject var settings: Settings
var body: some View {
VStack {
if settings.isLogedIn {
Text("Loged in")
} else{
Text("Not Loged in")
}
}
}
}
Is there a way to make a single property wrapper that covers both the persisting and the publishing?
import SwiftUI
import Combine
fileprivate var cancellables = [String : AnyCancellable] ()
public extension Published {
init(wrappedValue defaultValue: Value, key: String) {
let value = UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
self.init(initialValue: value)
cancellables[key] = projectedValue.sink { val in
UserDefaults.standard.set(val, forKey: key)
}
}
}
class Settings: ObservableObject {
#Published(key: "isLogedIn") var isLogedIn = false
...
}
Sample: https://youtu.be/TXdAg_YvBNE
Version for all Codable types check out here
To persist your data you could use the #AppStorage property wrapper.
However, without using #Published your ObservableObject will no longer put out the news about the changed data. To fix this, simply call objectWillChange.send() from the property's willSet observer.
import SwiftUI
class Settings: ObservableObject {
#AppStorage("Example") var example: Bool = false {
willSet {
// Call objectWillChange manually since #AppStorage is not published
objectWillChange.send()
}
}
}
It should be possible to compose a new property wrapper:
Composition was left out of the first revision of this proposal,
because one can manually compose property wrapper types. For example,
the composition #A #B could be implemented as an AB wrapper:
#propertyWrapper
struct AB<Value> {
private var storage: A<B<Value>>
var wrappedValue: Value {
get { storage.wrappedValue.wrappedValue }
set { storage.wrappedValue.wrappedValue = newValue }
}
}
The main benefit of this approach is its predictability: the author of
AB decides how to best achieve the composition of A and B, names it
appropriately, and provides the right API and documentation of its
semantics. On the other hand, having to manually write out each of the
compositions is a lot of boilerplate, particularly for a feature whose
main selling point is the elimination of boilerplate. It is also
unfortunate to have to invent names for each composition---when I try
the compose A and B via #A #B, how do I know to go look for the
manually-composed property wrapper type AB? Or maybe that should be
BA?
Ref: Property WrappersProposal: SE-0258
You currently can't wrap #UserDefault around #Published since that is not currently allowed.
The way to implement #PublishedUserDefault is to pass an objectWillChange into the wrapper and call it before setting the variable.
struct HomeView : View {
#StateObject var auth = Auth()
#AppStorage("username") var username: String = "Anonymous"
var body: some View {
VStack {
if username != "Anonymous" {
Text("Logged in")
} else{
Text("Not Logged in")
}
}
.onAppear(){
auth.login()
}
}
}
import SwiftUI
import Combine
class Auth: ObservableObject {
func login(params:[String:String]) {
Webservice().login(params: params) { response in
if let myresponse = response {
UserDefaults.standard.set(myresponse.login, forKey: "username")`
}
}
}
}

Ambiguous reference to member 'subscript' in VStack Swift UI 5

I try to run a function in a VStack statement but it don't work. When I run it in a button (with the action label) it work perfectly. How can I insert my func in a VStack?
I declare a QuizData class:
class QuizData: ObservableObject {
var allQuizQuestion: [QuizView] = [QuizView]()
let objectWillChange = PassthroughSubject<QuizData,Never>()
var currentQuestion: Int = 0 {
didSet {
withAnimation() {
objectWillChange.send(self)
}
}
}
}
and I use it there :
struct Quiz: View {
var continent: Continent
#EnvironmentObject var quizData: QuizData
var body: some View {
VStack
{
generateQuiz(continent: continent, quizData: self.quizData)
quizData.allQuizQuestion[quizData.currentQuestion]
}
.navigationBarTitle (Text(continent.name), displayMode: .inline)
}
}
The func generateQuiz is:
func generateQuiz(continent: Continent, quizData: QuizData) -> Void {
var capital: [Capital]
var alreadyUse: [Int]
for country in CountryData {
if country.continentId == continent.id
{
alreadyUse = [Int]()
capital = [Capital]()
capital.append(CapitalData[country.id])
for _ in 1...3 {
var index = Int.random(in: 1 ... CapitalData.count - 1)
while alreadyUse.contains(index) {
index = Int.random(in: 1 ... CapitalData.count - 1)
}
capital.append(CapitalData[index])
}
capital.shuffle()
quizData.allQuizQuestion.append(QuizView(country: country, question: QuestionData[country.id], capital: capital))
}
}
quizData.allQuizQuestion.shuffle()
}
I need to generate quiz question before the view appear. How should I do this?
First, you can't call a function that doesn't return some View in a VStack closure because that closure is not a normal closure, but a #ViewBuilder closure:
#functionBuilder
struct ViewBuilder {
// Build a value from an empty closure, resulting in an
// empty view in this case:
func buildBlock() -> EmptyView {
return EmptyView()
}
// Build a single view from a closure that contains a single
// view expression:
func buildBlock<V: View>(_ view: V) -> some View {
return view
}
// Build a combining TupleView from a closure that contains
// two view expressions:
func buildBlock<A: View, B: View>(_ viewA: A, viewB: B) -> some View {
return TupleView((viewA, viewB))
}
// And so on, and so forth.
...
}
It's a Swift 5.1 feature that lets you do things like these:
VStack {
Image(uiImage: image)
Text(title)
Text(subtitle)
}
With which you can easily create a view from several other views. For further information take a look at https://www.swiftbysundell.com/posts/the-swift-51-features-that-power-swiftuis-api
Now, if I get your issue (correct me if I'm wrong) you need to call a function before your view appears to generate some data. Honestly I'd prefer to pass that data to the view from the outside (creating the data before the view creation). But if you really need it you can do something like:
struct ContentView: View {
private var values: [Int]! = nil
init() {
values = foo()
}
var body: some View {
List(values, id: \.self) { val in
Text("\(val)")
}
}
func foo() -> [Int] {
[0, 1, 2]
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
Using the struct init and calling the function at the view creation.
EDIT: To answer your comment here below and since you are using an #EnvironmentObject you can do:
class ContentViewModel: ObservableObject {
#Published var values: [Int]!
init() {
values = generateValues()
}
private func generateValues() -> [Int] {
[0, 1, 2]
}
}
struct ContentView: View {
#EnvironmentObject var contentViewModel: ContentViewModel
var body: some View {
List(contentViewModel.values, id: \.self) { val in
Text("\(val)")
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(ContentViewModel()) //don't forget this
}
}
#endif
And in your SceneDelegate:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(
rootView: ContentView()
.environmentObject(ContentViewModel()) //don't forget this
)
self.window = window
window.makeKeyAndVisible()
}
}
This way you are creating a view model for your view and that view model will be accessible throughout your view hierarchy. Every time your view model will change your view will change too.