Using a button to add data that a user inputted - swiftui

Okay so I've been working on this for several days now and have not had any luck with an answer that makes any sense. I have a form in SwiftUI, using #ObservedObject to pull variables from a struct. In that form, I have a variety of text fields and pickers that the user can interact with. HOWEVER, I cannot figure out how to get my "Add" button to actually add that data to any of the other views in the app. I followed the sandwiches tutorial from WWDC20, with significant changes, so there is a swift file with "testData" and essentially I'm trying to get it so that the button uses the user input to append the testData and show that instead of nothing.
struct Horse: Identifiable {
var id = UUID()
var name: String
var gender: String
var breed: String
var type: String
var scale: String
var brand: String
var finish: String
var specialty: String
var imageName: String { return name }
var thumbnailName: String { return name + "Thumb" }
}
let testData = [
Horse(name: "Van Gogh", gender: "Stallion", breed: "Unknown", type: "Customized", scale: "Stablemate", brand: "Peter Stone", finish: "Gloss", specialty: "OOAK")
]
So this is what I'm using to establish testData and the parameters for what should be included in it.
func addANewHorse() {
withAnimation {
testStore.horses.append(Horse(name: "\(horseDetails.horseName)", gender: "\(horseDetails.selectedGender.rawValue)", breed: "\(horseDetails.horseBreed)", type: "\(horseDetails.type.rawValue)", scale: "\(horseDetails.scale.rawValue)", brand: "\(horseDetails.brand.rawValue)", finish: "\(horseDetails.finish.rawValue)", specialty: "\(horseDetails.specialty.rawValue)"))
}
}
Button("Add", action: {
addANewHorse();
self.presentationMode.wrappedValue.dismiss()
})
And that is what I'm using to try and append the testData to update with the users input. I know this is kind of choppy but does anyone have any advice whatsoever?
---EDIT---
My main app file looks like this...
#main
struct Pferd_HerdApp: App {
#StateObject private var store = HorseStore()
#StateObject private var horseDetails = HorseDetails()
var body: some Scene {
WindowGroup {
ContentView(store: store, horseDetails: HorseDetails())
}
}
}
my horse store class looks like this...
class HorseStore: ObservableObject {
#Published var horses: [Horse]
init(horses: [Horse] = []) {
self.horses = horses
}
}
let testStore = HorseStore(horses: testData)
Also, "HorseDetails" is the observableobject I'm trying to pull data from to append the testData, so here is the code for that
class HorseDetails: ObservableObject {
#Published var horseName = ""
#Published var selectedGender = Gender.allCases[0]
#Published var horseBreed = ""
#Published var purchaseDate = Date()
#Published var winCount = ""
#Published var notes = ""
#Published var brand = Brands.allCases[0]
#Published var type = Type.allCases[0]
#Published var scale = Scale.allCases[0]
#Published var finish = Finish.allCases[0]
#Published var specialRun = false
#Published var specialty = Specialty.allCases[0]
}
var horseDetails = HorseDetails()
and I changed the let for testData to a variable

Since your Question leaves a lot of code out, I will be making a few assumptions. I'm assuming that your form (where you have the button to add data) and your view for displaying the data are in two different views. You have not included your view model in the code, although there was an instance of your view model (testStore) used in the code above. You need to make sure that somewhere at the root of your view hierarchy, you made an instance of your view model (I'm assuming its called TestStoreViewModel) and passed that as an environment object to your subviews. For example, you should something like this
#main
struct YourApp: App {
let testStoreViewModel = TestStoreViewModel()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(testStoreViewModel)
}
}
}
in all of your views where you need to use the data from your TestStoreViewModel, you should declare it like so
#EnvironmentObject var testStore:TestStoreViewModel
Using environment objects means that your observable object is automatically synced across all of your views that use the environment object. Everything else in the code above should work fine with the use of EnvironmentObjects and a single source of truth. For more on environment objects, you can check out this article which in my opinion is great at explaining Environment Objects in swiftui. It is also important to note that in that article, it mentioned the use of a SceneDelegte and the ContentView being wrapped around a UIHostingController. That was replaced by the first block of code I showed you above.

Related

How would I save custom data after the app closes?

I am trying to store data after the app closes and one of the things I need to store is a variable that uses this struct
struct ToDoTasks: Identifiable, Hashable, Encodable, Decodable {
var id = UUID()
var task: String
var date: Date
}
The user can enter data where it is then stored here using the previous struct
#State var items:[ToDoTasks] = [ToDoTasks(task: "Test", date: Date())]
What would be the best way to save the items variable after the app closes?
I tried using #AppStorage but couldn’t get it to work.
In Apple's ScrumDinger sample they do the save in ScrumsView.swift like this:
#Environment(\.scenePhase) private var scenePhase
...
.onChange(of: scenePhase) { phase in
if phase == .inactive { saveAction() }
}

Creating a List of Toggle From a Set Using ForEach in swiftui

I' trying to create a set of Toggles, that need to be stored in one core data field of type "Transformable". I started with this example:
https://developer.apple.com/forums/thread/118595
in combination with other ideas from stack.
I'm trying to get this way:
Create a Set of structs like this
struct AllScopes: Identifiable, Hashable {
var id: UUID
var name: String
var notify: Bool
}
[...]
// all the stuff with View and body with
#State var scopes = Set<AllScopes>()
[...]
// and here I run through my FetchRequest to fill the Set
.onAppear {
for scope in allScopes {
scopes.insert(
AllScopes(
id: scope.id!,
name: scope.name!,
notify: false
)
)
}
}
In the end I've got a nice Set with all my scopes.
I call a new View with YearlyReportPage6(scopes: $scopes)
And now my problem - the next view:
struct YearlyReportPage6: View {
#Binding var scopes: Set<AllScopes>
init(scopes: Binding<Set<AllScopes>>) {
_scopes = scopes
}
var body: some View {
VStack {
ForEach(scopes.indices) { index in
Toggle(isOn: self.$scopes[index].notify) {
Text(self.scopes[index].name)
}
}
}
}
}
But all in ForEach creates errors. Either Binding in isOn: is wrong, or ForEach can't work with the set, or the Text is not a String, or, or, or...
In the end there should be a list of Toggles (checkboxes) and the selection should be stored in database.
Changing the Set into a simple Array like #State var scopes = [AllScopes]() will work for the Toggles, but how can I store this into a Transformable?
ForEach(Array(scopes).indices) { index in
Toggle(isOn: self.$scopes[index].notify) {
Text(self.scopes[index].name)
}
}
To summarize:
either how can I create the list of Toggles with the Set of AllScopes
or how can I store the Array / Dictionary into the Transformable field?
I hope, you can understand my clumsy English. :-)

FetchRequest for 12000 entries extremely slow on macOS app but not iOS app - despite code being the same (SwiftUI on macOS Big Sur / iOS 14)

Edit (original text below could be ignored)
I did some further testing using only a manual fetch (which gets executed only once). It seems it's just the fetch request (for only 12000 entities) itself which is horribly slow. Nevertheless, I wonder why I should get this issue only for macOS and not iOS given it's the same code?
==========================================
Original post
I am developing a simple SwiftUI app for macOS Big Sur/iOS 14 which synchronizes "Card"-entities (nothing fancy; around 20 properties and 7 relationships) via CloudKit. Everything works fine, however, after importing (only) 12000 entries (from a decoded JSON file), the macOS app (but not the iOS app) became unusable, i.e. freezes/has a very high CPU usage. Strangely, this issue also occurs if no data is shown on the UI, e.g. with the following simple view:
MainView & CardList
struct MainView: View {
#ObservedObject var model: MainViewModel
#Environment(\.colorScheme) var colorScheme
var body: some View {
CardList(predicate: Card.predicate(searchText: self.model.searchText,
pinned: self.model.filterPinned,
priority: self.model.getPrioritiesAsArray(),
subjects: self.model.selectedSubjects,
bundles: self.model.selectedBundles),
sortDescriptor: EntrySort(sortType: self.model.sortType, sortOrder:self.model.sortOrder).sortDescriptor,
model: self.model)
}
}
struct CardList: View {
#ObservedObject var model: MainViewModel
#Environment(\.colorScheme) var colorScheme
#Environment(\.managedObjectContext)
var context: NSManagedObjectContext
#FetchRequest(
entity: Card.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \Card.id, ascending: false)
]
)
private var result: FetchedResults<Card>
init(predicate: NSPredicate?,
sortDescriptor: NSSortDescriptor,
model: MainViewModel) {
let fetchRequest = NSFetchRequest<Card>(entityName: "Card")
fetchRequest.sortDescriptors = [sortDescriptor]
if let predicate = predicate {
fetchRequest.predicate = predicate
}
_result = FetchRequest(fetchRequest: fetchRequest)
self.model = model
}
var body: some View {
Text("TEST")
}
}
The "MainViewModel" look as follows (abbreviated for better legibility):
MainViewModel
class MainViewModel: ObservableObject {
#Published var searchText: String = ""
#Published var selectedSubjects: [Subject]?
#Published var selectedBundles: [Bundle]?
#Published var selectedTags: [Tag]?
#Published var filterPinned: Bool? = nil
#Published var filterPriorityVeryHigh: Bool = false
#Published var filterPriorityHigh: Bool = false
#Published var filterPriorityNormal: Bool = false
#Published var filterPriorityLow: Bool = false
func getPrioritiesAsArray() -> [CardPriority] {
var res: [CardPriority] = []
if self.filterPriorityLow { res.append(CardPriority.Low)}
if self.filterPriorityNormal { res.append(CardPriority.Normal)}
if self.filterPriorityHigh { res.append(CardPriority.High)}
if self.filterPriorityVeryHigh { res.append(CardPriority.VeryHigh)}
return res
}
#Published var sortType = SortType.dateCreated
#Published var sortOrder = SortOrder.ascending
}
AppDelegate
import SwiftUI
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
var coreDataStack = CoreDataStack(modelName: "[Name of Model]")
var mainViewModel = MainViewModel()
func applicationDidFinishLaunching(_ aNotification: Notification) {
coreDataStack.viewContext.automaticallyMergesChangesFromParent = true
let mainView = MainView(model: mainViewModel)
.environment(\.managedObjectContext, coreDataStack.viewContext)
// ...
}
Question
The very same data was successfully synchronized to the iOS version of the app and can be displayed there without any performance issues (using the same controls, namely a LazyVStack). However, the macOS app became totally unusable because of the constant high CPU usage. Also, I cannot see anything that would constantly change a variable and thereby trigger a constant refresh of the displayed entities (i.e. trigger a new fetch request).
Does anybody have an idea why this issue occurs?
Thanks in advance for any advise!
Sebastian
※ One thing I noticed is that Xcode floods the console with the warning "NSKeyedUnarchiveFromData' should not be used to for un-archiving and will be removed in a future release" because I am still using some [String]-Transformable-Properties on the Card entity. I will address this issue later, but since this warning seems to be displayed constantly, I wonder if #FetchRequest constantly fetches/evaluates all entities? Further, I assume the vast amount of warnings displayed should only have an influence on Xcode's CPU usage?
I solved this issue; it seems that it was indeed the warning "'NSKeyedUnarchiveFromData' should not be used to for un-archiving and will be removed in a future release" which caused this spike in CPU usage. After setting "NSSecureUnarchiveFromDataTransformer" to all [String]-Transformable-Properties of all database entities, the issue was gone.

SwiftUI IOS 14 New AppDeligate with .environmentObject

For the new delegate file in iOS 14 I need to include both the .environmentObject settings and the UserSettings: ObservableObject (which is a Realm Class).
But I first need to create the User data if there is none (first time user) otherwise it give me a null error and crashes.
Where would I put the code to initiate the user before calling it in the body loads?
#main
struct myapp_App: App {
let userSettings = UserSettings() // calling the data which will not exist if initial user
var body: some Scene {
WindowGroup {
ContentView().environmentObject(userSettings)
}
}
}
Thank you.
I put the code to create the user if first time inside the int() of the ObservableObject.
I hope can only hope this code is proper? But it works.
Good luck.
class UserSettings: ObservableObject {
#Published var name: String? = nil
#Published var email: String? = nil
init(){
if User.userExist() == false {
User.initiateUser()
}
let u = User.getUser()
name = u!.name
email = u!.email
}
}

SwiftUI - Navigate to view after retrieving data

So I'm retrieving data from FireStore. I'm retrieving the data successfully. When I tap my search button the first time the data is being downloaded and the new view is pushed. As a result, I get a blank view. But when I go back, hit search again, sure enough I can see my data being presented.
How can I make sure I first have the data I'm searching for THEN navigate to the new view? I've used #State variables etc. But nothing seems to be working. I am using the MVVM approach.
My ViewModel:
class SearchPostsViewModel: ObservableObject {
var post: [PostModel] = []
#State var searchCompleted: Bool = false
func searchPosts(completed: #escaping() -> Void, onError: #escaping(_ errorMessage: String) -> Void) {
isLoading = true
API.Post.searchHousesForSale(propertyStatus: propertyStatus, propertyType: propertyType, location: location, noOfBathrooms: noOfBathroomsValue, noOfBedrooms: noOfBedroomsValue, price: Int(price!)) { (post) in
self.post = post
print(self.post.count)
self.isLoading = false
self.searchCompleted.toggle()
}
}
}
The code that does work, but with the bug:
NavigationLink(destination: FilterSearchResults(searchViewModel: self.searchPostsViewModel)
.onAppear(perform: {
DispatchQueue.main.async {
self.createUserRequest()
}
})
)
{
Text("Search").modifier(UploadButtonModifier())
}
Try with the following modified view model
class SearchPostsViewModel: ObservableObject {
#Published var post: [PostModel] = [] // << make both published
#Published var searchCompleted: Bool = false
func searchPosts(completed: #escaping() -> Void, onError: #escaping(_ errorMessage: String) -> Void) {
isLoading = true
API.Post.searchHousesForSale(propertyStatus: propertyStatus, propertyType: propertyType, location: location, noOfBathrooms: noOfBathroomsValue, noOfBedrooms: noOfBedroomsValue, price: Int(price!)) { (post) in
DispatchQueue.main.async {
self.post = post // << update on main queue
print(self.post.count)
self.isLoading = false
self.searchCompleted.toggle()
}
}
}
}
You should look at the Apple documentation for #State and ObservableObject
https://developer.apple.com/documentation/combine/observableobject
https://developer.apple.com/documentation/swiftui/state
Your issue is with using an #State in a non-UI class/View.
It might help if you start with the Apple SwiftUI tutorials. So you understand the differences in with the wrappers and learn how it all connects.
https://developer.apple.com/tutorials/swiftui
Also, when you post questions make sure your code can be copied and pasted onto Xcode as-is so people can test it. You will get better feedback if other developers can see what is actually happening. As you progress it won't be as easy to see issues.