SwiftUI - Assign a value from a callback, display in view - ios

I'm stuck on something which should be trivial, using SwiftUI. I am pulling some data back from my API, and simply want to show each item on my view with ForEach.
I have the following function:
#State var workoutVideoList: GetWorkoutVideoListResponse!
func GetWorkoutVideoList() {
loadingWorkoutVideoList = true
PtPodsApiService.GetWorkoutList(firebaseToken: firebaseAuthToken) { (workoutListResult) -> () in
DispatchQueue.main.async() {
workoutVideoList = workoutListResult
loadingWorkoutVideoList = false
}
}
}
I then want to show another view and pass this object into it:
if workoutVideoList != nil {
BookAppointmentView(workoutVideoList: workoutVideoList)
}
else {
Text("Nope")
}
For whatever reason, my main thread sees workoutVideoList as nil, even though it's been assigned. I thought using the dispatcher to assign it would solve the problem, but no joy!
Any idea what I'm doing wrong here?
Thanks

Give #State a default value #State var workoutVideoList: GetWorkoutVideoListResponse = false and use onAppear to call GetWorkoutVideoList() but ideally you will eventually get this working in a ViewModel.

Related

Is there a way to have a "viewWillAppear" fuction that inizalises a class in swiftUI and prevents refrences until inialized?

I am looking for a way to initialize a class before my View loads in SwiftUI. This class takes two args which are the self of the initializer and an Int value. Then when initialized I want to call a get method inside of the Class and get an array of facebook videos (loaded in WebKit) to display. Where the problem I have comes in is if I fetch the get method too soon it crashes, and then it will not let me create a var/let with the type of self of my View calling it. To me, it seems like I need some sort of completion handler but I do not know to do that for this case.
Here is a portion of my fetchDataBook class that is being initialized to get information from a Facebook page I have permission to, and what needs to be initialized before the View loads the values of getWebView().
private let upperBoundRange: Int
private var requests = [URLRequest]()
private var WebViews = [WebView2]()
init(upperBoundRange: Int) {
self.upperBoundRange = upperBoundRange
getToken()
}
//
//
// Create some sort of completsion handler to tell when it is safe to fetch webviews
//
//
func getwebView(index: Int) -> WebView2 {
print("Get views", WebViews[index])
return WebViews[index]
}
private func setWebViews() {
for i in 0...upperBoundRange {
WebViews.append(WebView2(parent: parent, request: requests[i]))
}
}
private func setLinks(data: NSArray) {
// Code Not Shown to get links for setWebViews()
}
// Fetches FaceBook URL for Livestream
private func getJSON(token: String) {
// code not shown
}
private func getToken() {
// code now shown
}
Here is the basic struture of my SwiftUI View:
struct LiveMaster : View {
let fb = fetchDataBook(upperBoundRange 4)
ScrollView{
VStack {
self.fb.getwebView(index: 0)
// Each displays WebSite from fetch in fetchDataBook class but cannot display them until the var in fetchDataBook has the values
}
VStack {
}
VStack {
}
VStack {
}
}
Thank you and if you need more information I would be glad to edit this to clarify. This has been a problem for the past couple of days that I cannot find a solution to, nor an easy way to ask. Thank you.
The answer to my problem is as Paulw11 said Your model should #Publish an optional. This worked perfectly, as soon as I added a Bool value that would change once the values have been set since it was not properly updated with my Array of WebView2's. Then I just use #ObservedObject on my instances of that class and it worked perfectly with the if checks of that Bool value. Thank you #Paulw11 for the suggestion.

iOS - Sharing viewModel between views

I have a view whose ViewModel configures the view. The user can update the ViewModel and this object is later passed onto another view which will reflect the state of the preview view. Here is an example.
struct ViewSettings {
var btn1Selected: Bool
var btn2Selected: Bool
var btn3Selected: Bool
init() {
btn1Selected = true
btn2Selected = true
btn3Selected = true
}
}
class ViewOne: UIView {
var settings: ViewSettings
init(settings: ViewSettings) {
self.settings = settings
}
func configureView() {
btn1.isSelected = settings.btn1Selected
btn2.isSelected = settings.btn2Selected
btn3.isSelected = settings.btn3Selected
}
#objc func tapBtn1(_ sender: UIButton) {
btn1.isSelected = btn1.isSelected.toggle()
settings.btn1Selected.toggle()
}
#objc func tapBtn2(_ sender: UIButton) {
btn2.isSelected = btn2.isSelected.toggle()
settings.btn2Selected.toggle()
}
#objc func tapBtn3(_ sender: UIButton) {
btn3.isSelected = btn3.isSelected.toggle()
settings.btn3Selected.toggle()
}
}
This setting is later used inside another view. If btn1 is selected in ViewOne and when ViewTwo uses that setting, btn1 in ViewTwo is selected too.
Question:
I'm doing a direct mutation on the settings to achieve this. Is there a better design pattern that would let me arrive at the same solution?
your viewModel is struct that means its a value type, even when you think you are passing the same viewModel to other views, in reality you send a different copy of viewModel not the same instance.
So when you mutate value in view1 and pass the copy of it to view2, if view2 changes the value again, your viewModel in view1 will not be updated, its not passed by reference its passed by value.
so If your question was that I am mutating value directly will it cause side effects no because they are different copies, but if you want the changes to be reflected in all the views that holds this viewModel then it won't.
Finally answering your question
Question: I'm doing a direct mutation on the settings to achieve this.
Is there a better design pattern that would let me arrive at the same
solution?
At very first, sharing viewModel across view itself is arguable. Should this be done or not, as such the whole idea is opinion based. Some might say its fine some might say its not!
In general, I have seen people sharing viewModel across views but I personally refrain from doing so, but that doesn't mean that either of the approach is the only right way. Its best left to developers judgement.
Few clarifications:
struct ViewSettings {
var btn1Selected: Bool
var btn2Selected: Bool
var btn3Selected: Bool
init() {
btn1Selected = true
btn2Selected = true
btn3Selected = true
}
}
This looks more like DataModel and less of ViewModel isn't it? Its just a data container, no business logic, no data presentation/modification mechanisms nothing in it, its a plain data model. You are sharing DataModel across view and you wanna mutate and pass it on to next view I think its fine to go ahead with it.

Swift can't update view from parent

So I have a SwiftUI view that I instantiate in a parents ViewControllers viewDidLoad() method.
It looks something like this:
class ChildViewController: UIViewController {
private var historyView: HistoryView?
private var models: [HistoryModel]?
...
override func viewDidLoad() {
super.viewDidLoad()
historyView = HistoryView(models: models)
let historyViewHost = UIHostingController(rootView: historyView)
view.addSubview(historyViewHost.view)
historyViewHost.didMove(toParent: self)
historyViewHost.view.frame = view.frame
}
...
}
At some point i need to update the models in the historyView and I do so like this:
func updateHistory() {
let updatedModels = requests.map({ HistoryModel.fromRequest(request: $0) })
historyView!.historyCellModels = updatedModels
}
The problem is the models in the view do not actually get updated. And I don't mean that the view doesn't display the new models, but it actually doesn't even get the new list of models.
My HistoryView also updates everytime I hit a button, when I hit that button I have a breakpoint set in the construction of the view, and from the debugger I can see that the models do not get updated. I also have a breakpoint in my updateHistory method, where i can see that the models in the parent ARE updated, its just not getting passed down to the child.
I had the idea that maybe 2 instances of the view were being created and I was just updating the wrong one. So I viewed the memory of the historyView when I had a breakpoint inside it, and I wrote down where it was in memory. Then at the breakpoint I have in the parent view, i went to look at the memory of historyView and it pointed to 0x00! But whats even stranger is that historyView is not nil! I even did a force cast of it to a non-optional and the program had no issues.
So I figured the debugger must be lying to me and just not giving the right info. So I went to some old trusty print statements. When I added print statements like this:
func updateHistory() {
let updatedModels = requests.map({ HistoryCellModel.fromRequest(request: $0) })
print(updatedModels.count)
historyView!.historyCellModels = updatedModels
print(historyView?.historyCellModels.count)
}
And then I create a new model, and call the update function it will output:
11
Optional(10)
How is that possible!
My HistoryView looks something like this:
struct HistoryView: View {
#State var historyCellModels: [HistoryModel]
var body: some View {
List(historyCellModels) { ... }
}
}
I clearly set the two variables to be equal, the view is non-null, there is only one instance of it... I'm really not sure what the next step to hunting this bug is. Is there something obvious I could be missing?
#State is designed to be used only inside SwiftUI view itself (and recommended always to be declared as private).
So here is a way...
Use instead
struct HistoryView: View {
#ObservedObject var historyCellModels: HistoryViewModel
init(models: HistoryViewModel) {
historyCellModels = models
}
...
where
import Combine
class HistoryViewModel: ObservableObject {
#Published var historyCellModels: [HistoryModel]
}
and now it can be
class ChildViewController: UIViewController {
private var models: HistoryViewModel = HistoryViewModel()
...
and
func updateHistory() {
let updatedModels = requests.map({ HistoryModel.fromRequest(request: $0) })
models.historyCellModels = updatedModels
}
and all should work.

SwiftUI holding reference to deleted core data object causing crash

Im finding it impossible to use core data with SwiftUI, because as I pass a core data to a view observed object variable, the navigation link view will hold a reference to the object even after the view has disappeared, so as soon as I delete the object from context the app crashes, with no error messages.
I have confirmed this by wrapping the core data object variable into a view model as an optional, then set the object to nil right after the context delete action and the app works fine, but this is not a solution because I need the core data object to bind to the swift ui views and be the source of truth. How is this suppose to work? I seriously cannot make anything remotely complex with SwiftUI it seems.
I have tried assigning the passed in core data object to a optional #State, but this does not work. I cannot use #Binding because it's a fetched object. And I cannot use a variable, as swiftui controls require bindings. It only makes sense to use a #ObservedObject, but this cannot be an optional, which means when the object assigned to it gets deleted, the app crashes, because i cannot set it to nil.
Here is the core data object, which is an observable object by default:
class Entry: NSManagedObject, Identifiable {
#NSManaged public var date: Date
}
Here is a view that passes a core data entry object to another view.
struct JournalView: View {
#Environment(\.managedObjectContext) private var context
#FetchRequest(
entity: Entry.entity(),
sortDescriptors: [],
predicate: nil,
animation: .default
) var entries: FetchedResults<Entry>
var body: some View {
NavigationView {
List {
ForEach(entries.indices) { index in
NavigationLink(destination: EntryView(entry: self.entries[index])) {
Text("Entry")
}
}.onDelete { indexSet in
for index in indexSet {
self.context.delete(self.entries[index])
}
}
}
}
}
}
Now here is the view that accesses all the attributes from the core data entry object that was passed in. Once, I delete this entry, from any view by the way, it is still referenced here and causes the app to crash immediately. I believe this also has something to do with the Navigation Link initializing all destination view before they are even accessed. Which makes no sense why it would do that. Is this a bug, or is there a better way to achieve this?
I have even tried doing the delete onDisappear with no success. Even if I do the delete from the JournalView, it will still crash as the NavigationLink is still referencing the object. Interesting it will not crash if deleting a NavigationLink that has not yet been clicked on.
struct EntryView: View {
#Environment(\.managedObjectContext) private var context
#Environment(\.presentationMode) private var presentationMode
#ObservedObject var entry: Entry
var body: some View {
Form {
DatePicker(selection: $entry.date) {
Text("Date")
}
Button(action: {
self.context.delete(self.entry)
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Delete")
}
}
}
}
UPDATE
The crash is taking me to the first use of entry in the EntryView and reads Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).. thats the only message thrown.
The only work around I can think of is to add a property to the core data object "isDeleted" and set it to true instead of trying to delete from context. Then when the app is quit, or on launch, I can clean and delete all entries that isDeleted? Not ideal, and would prefer to figure out what it wrong here, as it appears I'm not doing anything different then the MasterDetailApp sample, which seems to work.
I basically had the same issue. It seems that SwiftUI loads every view immediately, so the view has been loaded with the Properties of the existing CoreData Object. If you delete it within the View where some data is accessed via #ObservedObject, it will crash.
My Workaround:
The Delete Action - postponed, but ended via Notification Center
Button(action: {
//Send Message that the Item should be deleted
NotificationCenter.default.post(name: .didSelectDeleteDItem, object: nil)
//Navigate to a view where the CoreDate Object isn't made available via a property wrapper
self.presentationMode.wrappedValue.dismiss()
})
{Text("Delete Item")}
You need to define a Notification.name, like:
extension Notification.Name {
static var didSelectDeleteItem: Notification.Name {
return Notification.Name("Delete Item")
}
}
On the appropriate View, lookout for the Delete Message
// Receive Message that the Disease should be deleted
.onReceive(NotificationCenter.default.publisher(for: .didSelectDeleteDisease)) {_ in
//1: Dismiss the View (IF It also contains Data from the Item!!)
self.presentationMode.wrappedValue.dismiss()
//2: Start deleting Disease - AFTER view has been dismissed
DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval(1)) {self.dataStorage.deleteDisease(id: self.diseaseDetail.id)}
}
Be safe on your Views where some CoreData elements are accessed - Check for isFault!
VStack{
//Important: Only display text if the disease item is available!!!!
if !diseaseDetail.isFault {
Text (self.diseaseDetail.text)
} else { EmptyView() }
}
A little bit hacky, but this works for me.
I encountered the same issue and did not really find a solution to the root problem. But now I "protect" the view that uses the referenced data like this:
var body: some View {
if (clip.isFault) {
return AnyView(EmptyView())
} else {
return AnyView(actualClipView)
}
}
var actualClipView: some View {
// …the actual view code accessing various fields in clip
}
That also feelds hacky, but works fine for now. It's less complex than using a notification to "defer" deletion, but still thanks to sTOOs answer for the hint with .isFault!
I have had the same issue for a while, the solution for me was pretty simple:
In the View where the #ObservedObject is stored I simply put this !managedObject.isFault.
I experienced this class only with ManagedObjects with a date property, I don't know if this is the only circumstance the crash verifies.
import SwiftUI
struct Cell: View {
#ObservedObject var managedObject: MyNSManagedObject
var body: some View {
if !managedObject.isFault {
Text("\(managedObject.formattedDate)")
} else {
ProgressView()
}
}
}
After some research online, it's clear to me that this crash can be caused by many things related to optionals. For me, I realized that declaring a non-optional Core Data attribute as an optional in the NSManagedObject subclass was causing the issue.
Specifically, I have a UUID attribute id in Core Data that cannot have a default value, but is not optional. In my subclass, I declared #NSManaged public var id: UUID. Changing this to #NSManaged public var id: UUID? fixed the problem immediately.
I had the same issue recently. Adding an entity property to the view fixed it.
ForEach(entities, id: \.self) { entity in
Button(action: {
}) {
MyCell(entity: entity)
}
}
To
ForEach(entities, id: \.self) { entity in
Button(action: {
}) {
MyCell(entity: entity, property: entity.property)
}
}
I suspect that the nullable Core Data entity is the cause of the issue, where as adding a non-nil property as a var (e.g, var property: String) fixed it
I have tried all previous solutions, none worked for me.
This one, worked.
I had my list like this:
List {
ForEach(filteredItems, id: \.self) { item in
ListItem(item:item)
}
.onDelete(perform: deleteItems)
private func deleteItems(offsets: IndexSet) {
//deleting items
This was crashing.
I modified the code to this one
List {
ForEach(filteredItems, id: \.self) { item in
ListItem(item:item)
}
.onDelete { offsets in
// delete objects
}
This works fine without crashing.
For heaven's sake, Apple!
A view modifier for this (based on conditional view modifiers):
import SwiftUI
import CoreData
extension View {
#ViewBuilder
func `if`<Transform: View>(
_ condition: Bool,
transform: (Self) -> Transform
) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
extension View {
func hidingFaults(_ object: NSManagedObject) -> some View {
self.if(object.isFault) { _ in EmptyView() }
}
}
Having said that, it's worth checking you're performing CoreData operations asynchronously on the main thread, doing it synchronously can be a source of grief (sometimes, but not always).
Apple says this (and it works perfectly) :
The behavior you've reported is the result of a system bug, and should
be fixed in a future release. As a workaround, you can prevent the
race condition by wrapping your deletion logic in
NSManagedObjectContext.perform:
private func deleteItems(offsets: IndexSet) {
withAnimation {
viewContext.perform {
offsets.map { molts[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
viewContext.rollback()
userMessage = "\(error): \(error.localizedDescription)"
displayMessage.toggle()
}
}
}
You can find the full thread here https://developer.apple.com/forums/thread/668299
For me, I got this because of a force-unwrapped binding.
I used a Binding($item.someProperty)! like this: TextField("Description", text: Binding($item.someProperty)!).
This was because item is a Core Data class and hence someProperty is a String? instead of a String. Binding(*)! was a solution proposed in https://stackoverflow.com/a/59004832.
I changed the implementation to use a null coalescing operator for bindings as proposed in https://stackoverflow.com/a/61002589, now it doesn't crash anymore.
Wrap your deletion logic in a withAnimation block to prevent a crash after deleting a Core Data object. No need for isFault, isDeleted, or deferring execution in other complicated ways.
withAnimation {
context.delete(object)
do try catch etc...
}

How to create an instance of an object in SwiftUI without duplication?

This is the next part of that question.
I've got the follow code.
The initial view of the app:
struct InitialView : View {
var body: some View {
Group {
PresentationButton(destination: ObjectsListView()) {
Text("Show ListView")
}
PresentationButton(destination: AnotherObjectsListView()) {
Text("Show AnotherListView")
}
}
}
}
The list view of the objects:
struct ObjectsListView : View {
#Environment(\.myObjectsStore.objects) var myObjectsStores: Places
var body: some View {
Group {
Section {
ForEach(myObjectsStore.objects) { object in
NavigationLink(destination: ObjectDetailView(object: object)) {
ObjectCell(object: object)
}
}
}
Section {
// this little boi
PresentationButton(destination: ObjectDetailView(objectToEdit: MyObject(store: myObjectsStore))) {
Text("Add New Object")
}
}
}
}
}
The detail view:
struct ObjectsDetailView : View {
#Binding var myObject: MyObject
var body: some View {
Text("\(myObject.title)")
}
}
So the problem is quite complex.
The ObjectsListView creates instance of the MyObject(store: myObjectsStore) on itself initialization while computing body.
The MyObject object is setting its store property on itself initialization, since it should know is it belongs to myObjectsStore or to anotherMyObjectsStore.
The myObjectsStore are #BindableObjects since their changes are managing by SwiftUI itself.
So this behavior ends up that I've unexpected MyObject() initializations since the Views are computing itself. Like:
First MyObject creates on the ObjectsListView initialization.
Second MyObject creates on its PresentationButton pressing (the expected one).
Third (any sometimes comes even fourth) MyObject creates on dismissing ObjectsDetailView.
So I can't figure what pattern should I use this case to create only one object?
The only thing that I'd come to is to make the follow code:
struct ObjectsListView : View {
#Environment(\.myObjectsStore.objects) var myObjectsStores: Places
#State var buttonPressed = false
var body: some View {
Group {
if buttonPressed {
ObjectDetailView(objectToEdit: MyObject(store: myObjectsStore))
} else {
Section {
ForEach(myObjectsStore.objects) { object in
NavigationLink(destination: ObjectDetailView(object: object)) {
ObjectCell(object: object)
}
}
}
Section {
Button(action: {
self.buttonPressed.toggle()
}) {
Text("Add New Object")
}
}
}
}
}
}
Which simply redraw ObjectsListView to detail view conditionally. But it's completely out of iOS guidelines. So how to create the Only One object for another view in SwiftUI?
UPD:
Here's the project that represents the bug with Object duplication.
I'm still have no idea why the objects are duplicating in this case. But at least I know the reason yet. And the reason is this line:
#Environment(\.myObjectsStore.objects) var myObjectsStores: Places
I've tried to share my model with this wrapper to make it available in every single view (including Modal one) without passing them as an arg to the new view initializer, which are unavailable by the other ways, like #EnvironmentObject wrapper. And for some reason #Environment(\.keyPath) wrapper makes duplications.
So I'd simply replace all variables from Environment(\.) to ObjectBinding and now everything works well.
I've found the solution to this.
Here's the project repo that represents the bug with Object duplication and the version that fix this. I'm still have no idea how objects have been duplicate in that case. But I figured out why. It happens because this line:
#Environment(\.myObjectsStore.objects) var myObjectsStores: MyObjectsStore
I've used #Environment(\.key) to connect my model to each view in the navigation stack including Modal one, which are unavailable by the other ways provided in SwiftUI, e.g.: #State, #ObjectBinding, #EnvironmentObject. And for some reason #Environment(\.key) wrapper produce these duplications.
So I'd simply replace all variables from #Environment(\.) to #ObjectBinding and now almost everything works well.
Note: The code is in the rep is still creates one additional object by each workflow walkthrough. So it creates two objects totally instead of one. This behavior could be fixed by way provided in this answer.

Resources