SwiftUI, why is object nil in clousure - swiftui

Bellow is a simplification of my code, where the FDocument is a
class FDocument: NSObject, ObservableObject, Codable
When i hit the button, the document is assigned proper with a new instance and isDocumentPresented is changed, however in the
.fullScreenCover(isPresented: $isDocumentPresented)
the document is suddenly nil
struct MyView: View {
#State var document: FDocument?
#State var isDocumentPresented = false
var body: some View {
List {
Button("New") {
document = FDocument()
isDocumentPresented = true
}
}
.listStyle(.insetGrouped)
.fullScreenCover(isPresented: $isDocumentPresented) {
DocumentEditor(document: document!) // <- NIL HERE
}
}
}

Use variant with item instead, like
Button("New") {
document = FDocument()
// isDocumentPresented = true // << not needed !!
}
}
.listStyle(.insetGrouped)
.fullScreenCover(item: $document) { item in // << valid !!
DocumentEditor(document: item) // << here !!
}

Related

Checking if a View is an EmptyView in SwiftUI

Is there anyway of checking if a view added using ViewBuilder is an EmptyView. Here is a code sample:
struct ViewAsVariable<TestView:View>: View {
#ViewBuilder var aView:TestView
var body: some View {
if aView === EmptyView(){
Text( "view is empty")
}
else{
aView
}
}
init(destinationView: TestView ) {
self.aView = destinationView
}
}
You should test what type the associated generic type is rather than the instance of that type
if TestView.self == EmptyView.self { ...
You don't need a type check because EmptyView isn't going to give you anything useful to work with; use Never instead.
struct ViewAsVariable<TestView: View>: View {
let aView: TestView?
init(destinationView: TestView = Never?.none) {
aView = destinationView
}
var body: some View {
if let aView {
aView
} else {
Text("view is empty")
}
}
}
Type checks at runtime are rather unswifty.
And unlike UIKit SwiftUI is declarative. You should tell the view that it's empty rather than asking for it
struct ViewAsVariable<TestView:View>: View {
private let isEmpty : Bool
#ViewBuilder var aView:TestView
var body: some View {
if isEmpty {
Text( "view is empty")
}
else {
aView
}
}
init(destinationView: TestView, isEmpty: Bool = false ) {
self.isEmpty = isEmpty
self.aView = destinationView
}
}

Using ForEach inside a Picker

I'm having issues pulling data from an Array into a picker using SwiftUI. I can correctly make a list of the data I'm interested in, but can't seem to make the same logic work to pull the data into a picker. I've coded it a few different ways but the current way I have gives this error:
Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'Text' conform to 'TableRowContent'
The code is below:
import SwiftUI
struct BumpSelector: View {
#ObservedObject var model = ViewModel()
#State var selectedStyle = 0
init(){
model.getData2()}
var body: some View {
VStack{
List (model.list) { item in
Text(item.style)}
Picker("Style", selection: $selectedStyle, content: {
ForEach(0..<model.list.count, content: { index in
Text(index.style)
})
})
}
}
The model is here:
import Foundation
struct Bumps: Identifiable{
var id: String
var style: String
}
and the ViewModel is here:
import Foundation
import Firebase
import FirebaseFirestore
class ViewModel: ObservableObject {
#Published var list = [Bumps]()
#Published var styleArray = [String]()
func getData2() {
let db = Firestore.firestore()
db.collection("bumpStop").getDocuments { bumpSnapshot, error in
//Check for errors first:
if error == nil {
//Below ensures bumpSnapshot isn't nil
if let bumpSnapshot = bumpSnapshot {
DispatchQueue.main.async {
self.list = bumpSnapshot.documents.map{ bump in
return Bumps(id: bump.documentID,
style: bump["style"] as? String ?? "")
}
}
}
}
else {
//Take care of the error
}
}
}
}
index in your ForEach is just an Int, there is no style associated with an Int. You could try this approach to make the Picker work with its ForEach:
struct BumpSelector: View {
#ObservedObject var model = ViewModel()
#State var selectedStyle = 0
init(){
model.getData2()
}
var body: some View {
VStack{
List (model.list) { item in
Text(item.style)}
Picker("Style", selection: $selectedStyle) {
ForEach(model.list.indices, id: \.self) { index in
Text(model.list[index].style).tag(index)
}
}
}
}
}
EDIT-1:
Text(model.list[selectedStyle].style) will give you the required style of the selectedStyle.
However, as always when using index, you need to ensure it is valid at the time of use.
That is, use if selectedStyle < model.list.count { Text(model.list[selectedStyle].style) }.
You could also use this alternative approach that does not use index:
struct Bumps: Identifiable, Hashable { // <-- here
var id: String
var style: String
}
struct BumpSelector: View {
#ObservedObject var model = ViewModel()
#State var selectedBumps = Bumps(id: "", style: "") // <-- here
init(){
model.getData2()
}
var body: some View {
VStack{
List (model.list) { item in
Text(item.style)
}
Picker("Style", selection: $selectedBumps) {
ForEach(model.list) { bumps in
Text(bumps.style).tag(bumps) // <-- here
}
}
}
.onAppear {
if let first = model.list.first {
selectedBumps = first
}
}
}
}
Then use selectedBumps, just like any Bumps, such as selectedBumps.style

Problem with state management and Core Data

I'm fairly new to Swift and Core Data. I’m having a problem resolving a state issue in a new project of mine.
I have a parent view (CategoryView)that includes a context menu item to allow editing of certain category properties (EditCategoryView). When the EditCategoryView sheet is presented and an edit to a category property is made, the CategoriesView updates correctly when the sheet is dismissed. Works fine.
There is a navigation link off of CategoriesView (ItemsView) that also includes a context menu to allow editing of certain item properties (EditItemView). Unlike the prior example, when the EditItemView sheet is presented and an edit is made to an item property, the ItemsView does not update when the sheet is dismissed. The old item property still displays. If I navigate back to CategoriesView and then return to ItemsView, the updated item property displays correctly.
I’m stumped and clearly don’t understand how state is managed in a CoreData environment. My code for the 2 views seems to be similar, yet they are behaving distinctly different. I wonder if the problem relates to the difference in the structures used in the 2 ForEach lines. That is, in CategoriesView I'm looping on the results of a Fetch and in EventsView I'm looping on the results of a computed value.
Any suggestions? thanks in advance for any guidance.
I created a simple example project that demonstrates the problem. To reproduce:
tap on Load Sample Data
choose a Category
tap and hold an Item to bring up context menu
choose Edit and change the name of the item
you’ll note when sheet dismisses the updated name is not reflected
return to Category list and then select the item again to see the updated name
https://github.com/jayelevy/CoreDataState
edit to include the code for the minimal example referenced in the repo
xcdatamodeld
2 Entities
Category
Attribute: name: String
Relationships: items, destination: Item (many-to-one)
Item
Attribute: name: String
Relationships: category, destination: Category (to one)
#main
struct CoreDataStateApp: App {
#StateObject var dataController: DataController
init() {
let dataController = DataController()
_dataController = StateObject(wrappedValue: dataController)
}
var body: some Scene {
WindowGroup {
CategoriesView()
.environment(\.managedObjectContext, dataController.container.viewContext)
.environmentObject(dataController)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification), perform: save)
}
}
func save(_ note: Notification) {
dataController.save()
}
}
struct CategoriesView: View {
#EnvironmentObject var dataController: DataController
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(sortDescriptors: [SortDescriptor(\.name)])
var categories: FetchedResults<Category>
var body: some View {
NavigationView {
VStack {
List {
ForEach(categories) { category in
NavigationLink {
ItemsView(category: category)
} label : {
Text(category.categoryName)
}
}
}
}
.navigationTitle("My Categories")
.toolbar {
ToolbarItem(placement: .automatic) {
Button {
dataController.deleteAll()
try? dataController.createSampleData()
} label: {
Text("Load Sample Data")
}
}
}
}
}
}
problem occurs with the following view. When an item is edited in EditItemView, the updated property (name) does not display when returning to ItemsView from the sheet.
If you return to CategoryView and then return to ItemsView, the correct property name is displayed.
struct ItemsView: View {
#ObservedObject var category: Category
#State private var isEditingItem = false
var body: some View {
VStack {
List {
ForEach(category.categoryItems) { item in
NavigationLink {
//
} label: {
Text(item.itemName)
}
.contextMenu {
Button {
isEditingItem.toggle()
} label: {
Label("Edit Item", systemImage: "pencil")
}
}
.sheet(isPresented: $isEditingItem) {
EditItemView(item: item)
}
}
}
}
.navigationTitle(category.categoryName)
}
}
struct EditItemView: View {
var item: Item
#EnvironmentObject var dataController: DataController
#Environment(\.managedObjectContext) var managedObjectContext
#Environment(\.dismiss) private var dismiss
#State private var itemName: String
init(item: Item) {
// _item = ObservedObject(initialValue: item)
self.item = item
_itemName = State(initialValue: item.itemName)
}
var body: some View {
NavigationView {
VStack {
Form {
Section {
TextField("Item Name", text: $itemName)
}
}
}
.navigationTitle("Edit Item")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
// add any needed cancel logic
Button("Cancel") {
dismiss()
}
}
ToolbarItem {
Button {
saveItem()
dismiss()
} label: {
Text("Update")
}
.disabled(itemName.isEmpty)
}
}
}
}
func saveItem() {
item.name = itemName
dataController.save()
}
}
extension Category {
var categoryName: String {
name ?? "New Category"
}
var categoryItems: [Item] {
items?.allObjects as? [Item] ?? []
}
extension Item {
var itemName: String {
name ?? "New Item"
}
}
extension Binding {
func onChange(_ handler: #escaping () -> Void) -> Binding<Value> {
Binding(
get: { self.wrappedValue },
set: { newValue in
self.wrappedValue = newValue
handler()
}
)
}
}
class DataController: ObservableObject {
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "Model")
if inMemory {
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Fatal error loading store: \(error.localizedDescription)")
}
}
}
static var preview: DataController = {
let dataController = DataController(inMemory: true)
let viewContext = dataController.container.viewContext
do {
try dataController.createSampleData()
} catch {
fatalError("Fatal error creating preview: \(error.localizedDescription)")
}
return dataController
}()
func createSampleData() throws {
let viewContext = container.viewContext
for i in 1...4 {
let category = Category(context: viewContext)
category.name = "Category \(i)"
category.items = []
for j in 1...5 {
let item = Item(context: viewContext)
item.name = "Item \(j)"
item.category = category
}
}
try viewContext.save()
}
func save() {
if container.viewContext.hasChanges {
try? container.viewContext.save()
}
}
func delete(_ object: NSManagedObject) {
container.viewContext.delete(object)
}
func deleteAll() {
let fetchRequest1: NSFetchRequest<NSFetchRequestResult> = Item.fetchRequest()
let batchDeleteRequest1 = NSBatchDeleteRequest(fetchRequest: fetchRequest1)
_ = try? container.viewContext.execute(batchDeleteRequest1)
let fetchRequest2: NSFetchRequest<NSFetchRequestResult> = Category.fetchRequest()
let batchDeleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2)
_ = try? container.viewContext.execute(batchDeleteRequest2)
}
func count<T>(for fetchRequest: NSFetchRequest<T>) -> Int {
(try? container.viewContext.count(for: fetchRequest)) ?? 0
}
}
ItemsView needs its own #FetchRequest for CategoryItem with a predicate where category = %#.
Also, instead of passing your DataController object around just put your helper methods in an extension of NSManagedObjectContext. Then you can change DataController back to the struct it should be.
I imagine there are other opportunities to improve my code (obviously, still learning), per other posts. However, the resolution was quite simple.
Modified saveItem in EditItemView to include objectWillChange.send()
func saveItem() {
item.name = itemName
item.category = itemCategory
item.category?.objectWillChange.send()
dataController.save()
}

SwiftUI: Real device shows strange behavior with asynchronous flow, while Simulator runs perfectly

*** EDIT 23.20.20 ***
Due to the strange behavior discovered after my original post, I need to completely rephrase my question. I meanwhile re-wrote large parts of my code as well.
The issue:
I run an asynchronous HTTP GET search query, which returns me an Array searchResults, which I store in an ObservedObject FoodDatabaseResults.
struct FoodItemEditor: View {
//...
#ObservedObject var foodDatabaseResults = FoodDatabaseResults()
#State private var activeSheet: FoodItemEditorSheets.State?
//...
var body: some View {
NavigationView {
VStack {
Form {
Section {
HStack {
// Name
TextField(titleKey: "Name", text: $draftFoodItem.name)
// Search and Scan buttons
Button(action: {
if draftFoodItem.name.isEmpty {
self.errorMessage = NSLocalizedString("Search term must not be empty", comment: "")
self.showingAlert = true
} else {
performSearch()
}
}) {
Image(systemName: "magnifyingglass").imageScale(.large)
}.buttonStyle(BorderlessButtonStyle())
//...
}
//...
}
//...
}
}
//...
}
.sheet(item: $activeSheet) {
sheetContent($0)
}
}
private func performSearch() {
UserSettings.shared.foodDatabase.search(for: draftFoodItem.name) { result in
switch result {
case .success(let networkSearchResults):
guard let searchResults = networkSearchResults else {
return
}
DispatchQueue.main.async {
self.foodDatabaseResults.searchResults = searchResults
self.activeSheet = .search
}
case .failure(let error):
debugPrint(error)
}
}
}
#ViewBuilder
private func sheetContent(_ state: FoodItemEditorSheets.State) -> some View {
switch state {
case .search:
FoodSearch(foodDatabaseResults: foodDatabaseResults, draftFoodItem: self.draftFoodItem) // <-- I set a breakpoint here
//...
}
}
}
class FoodDatabaseResults: ObservableObject {
#Published var selectedEntry: FoodDatabaseEntry?
#Published var searchResults: [FoodDatabaseEntry]?
}
I get valid search results in my performSearch function. The DispatchQueue.main.async closure makes sure to perform the update of my #Published var searchResults in the main thread.
I then open a sheet, displaying these search results:
struct FoodSearch: View {
#ObservedObject var foodDatabaseResults: FoodDatabaseResults
#Environment(\.presentationMode) var presentation
//...
var body: some View {
NavigationView {
List {
if foodDatabaseResults.searchResults == nil {
Text("No search results (yet)")
} else {
ForEach(foodDatabaseResults.searchResults!) { searchResult in
FoodSearchResultPreview(product: searchResult, isSelected: self.selectedResult == searchResult)
}
}
}
.navigationBarTitle("Food Database Search")
.navigationBarItems(leading: Button(action: {
// Remove search results and close sheet
foodDatabaseResults.searchResults = nil
presentation.wrappedValue.dismiss()
}) {
Text("Cancel")
}, trailing: Button(action: {
if selectedResult == nil {
//...
} else {
//... Do something with the result
// Remove search results and close sheet
foodDatabaseResults.searchResults = nil
presentation.wrappedValue.dismiss()
}
}) {
Text("Select")
})
}
}
}
When I run this on the Simulator, everything works as it should, see https://wolke.rueth.info/index.php/s/KbqETcDtSe4278d
When I run it on a real device with the same iOS version (14.0.1), the FoodSearch view first correctly displays the search result, but is then immediately called a second time with empty (nil) search results. You need to look very closely at the screen cast here and you'll see it displaying the search results for a very short moment before they disappear: https://wolke.rueth.info/index.php/s/9n2DZ88qSB9RWo4
When setting a breakpoint in the marked line in my sheetContent function, the FoodSearch sheet is indeed called twice on the real device, while it's only called once in the Simulator.
I have no idea what is going on here. Hope someone can help. Thanks!
*** ORIGINAL POST ***
I run an HTTP request, which updates a #Published variable searchResults in a DispatchQueue.main.async closure:
class OpenFoodFacts: ObservableObject {
#Published var searchResults = [OpenFoodFactsProduct]()
// ...
func search(for term: String) {
let urlString = "https://\(countrycode)-\(languagecode).openfoodfacts.org/cgi/search.pl?action=process&search_terms=\(term)&sort_by=unique_scans_n&json=true"
let request = prepareRequest(urlString)
let session = URLSession.shared
session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) in
guard error == nil else {
debugPrint(error!.localizedDescription)
return
}
if let data = data {
do {
let openFoodFactsSearchResult = try JSONDecoder().decode(OpenFoodFactsSearchResult.self, from: data)
guard let products = openFoodFactsSearchResult.products else {
throw FoodDatabaseError.noSearchResults
}
DispatchQueue.main.async {
self.searchResults = products
self.objectWillChange.send()
}
} catch {
debugPrint(error.localizedDescription)
}
}
}).resume()
}
struct OpenFoodFactsSearchResult: Decodable {
var products: [OpenFoodFactsProduct]?
enum CodingKeys: String, CodingKey {
case products
}
}
struct OpenFoodFactsProduct: Decodable, Hashable, Identifiable {
var id = UUID()
// ...
enum CodingKeys: String, CodingKey, CaseIterable {
// ...
}
// ...
}
I call the search function from my view:
struct FoodSearch: View {
#ObservedObject var foodDatabase: OpenFoodFacts
// ...
var body: some View {
NavigationView {
List {
ForEach(foodDatabase.searchResults) { searchResult in
FoodSearchResultPreview(product: searchResult, isSelected: self.selectedResult == searchResult)
}
}
// ...
}
.onAppear(perform: search)
}
private func search() {
foodDatabase.search(for: draftFoodItem.name)
}
}
My ForEach list will never update, although I have a valid searchResult set in my OpenFoodFacts observable object and also sent an objectWillChange signal. Any idea what I'm missing?
Funny enough: On the simulator it works as expected:
https://wolke.rueth.info/index.php/s/oy4Xf6C5cgrEZdK
On a real device not:
https://wolke.rueth.info/index.php/s/TQz8HnFyjLKtN74

Custom event modifier in SwiftUI

I created a custom button, that shows a popover. Here is my code:
PopupPicker
struct PopupPicker: View {
#State var selectedRow: UUID?
#State private var showPopover = false
let elements: [PopupElement]
var body: some View {
Button((selectedRow != nil) ? (elements.first { $0.id == selectedRow! }!.text) : elements[0].text) {
self.showPopover = true
}
.popover(isPresented: self.$showPopover) {
PopupSelectionView(elements: self.elements, selectedRow: self.$selectedRow)
}
}
}
PopupSelectionView
struct PopupSelectionView: View {
var elements: [PopupElement]
#Binding var selectedRow: UUID?
var body: some View {
List {
ForEach(self.elements) { element in
PopupText(element: element, selectedRow: self.$selectedRow)
}
}
}
}
PopupText
struct PopupText: View {
var element: PopupElement
#Binding var selectedRow: UUID?
var body: some View {
Button(element.text) {
self.presentation.wrappedValue.dismiss()
self.selectedRow = self.element.id
}
}
}
That works fine, but can I create a custom event modifier, so that I can write:
PopupPicker(...)
.onSelection { popupElement in
...
}
I can't give you a full solution as I don't have all of your code and thus your methods to get the selected item anyhow, however I do know where to start.
As it turns out, declaring a function with the following syntax:
func `onSelection`'(arg:type) {
...
}
Creates the functionality of a .onSelection like so:
struct PopupPicker: View {
#Binding var selectedRow: PopupElement?
var body: some View {
...
}
func `onSelection`(task: (_ selectedRow: PopupElement) -> Void) -> some View {
print("on")
if self.selectedRow != nil {
task(selectedRow.self as! PopupElement)
return AnyView(self)
}
return AnyView(self)
}
}
You could theoretically use this in a view like so:
struct ContentView: View {
#State var popupEl:PopupElement?
var body: some View {
PopupPicker(selectedRow: $popupEl)
.onSelection { element in
print(element.name)
}
}
}
However I couldn't test it properly, please comment on your findings
Hope this could give you some insight in the workings of this, sorry if I couldn't give a full solution