Below is my code to draw a SwiftUI view that use a publisher to get its items that need drawing in a list. The items all have boolean values drawn with a Toggle.
My view is dumb so I can use any type of boolean value, perhaps UserDefaults backed, core data backed, or simply a boolean property somewhere... anyway, this doesn't redraw when updating a bool outside of the view when one of the booleans is updated.
The onReceive is called and I can see the output change in my console, but binding isn't a part of my struct of ToggleItem and so SwiftUI doesn't redraw.
My code...
I have a struct that looks like this, note the binding type here...
struct ToggleItem: Identifiable, Equatable {
let id: String
let name: String
let isOn: Binding<Bool>
public static func == (lhs: ToggleItem, rhs: ToggleItem) -> Bool {
lhs.id == rhs.id
}
}
And in my SwiftUI I have this...
struct MyView: View {
#State private var items: [ToggleItem] = []
let itemsPublisher: AnyPublisher<[ToggleItem], Never>
// ...
var body: some View {
List {
// ...
}
.onReceive(itemsPublisher) { newItems in
print("New items: \(newItems)")
items.removeAll() // hacky redraw
items = newItems
}
}
I can see what's going on here, as Binding<Bool> isn't a value, so SwiftUI sees the array of newItems equal to the items it's already drawn, as a result, this doesn't redraw.
Is there something I'm missing, perhaps some ingenious bit of SwiftUI/Combine that redraws this for me?
how about doing something like this instead to keep one source of truth:
struct ToggleItem: Identifiable, Equatable {
let id: String
let name: String
var isOn: Bool
public static func == (lhs: ToggleItem, rhs: ToggleItem) -> Bool {
lhs.id == rhs.id
}
}
class ItemsPublisher: ObservableObject {
#Published var items: [ToggleItem] = [ToggleItem(id: "1", name: "name1", isOn: false)] // for testing
}
struct ContentView: View {
#ObservedObject var itemsPublisher = ItemsPublisher() // to be passed in from parent
var body: some View {
VStack {
Button("Add item") {
let randomString = UUID().uuidString
let randomBool = Bool.random()
itemsPublisher.items.append(ToggleItem(id: randomString, name: randomString, isOn: randomBool))
}
List ($itemsPublisher.items) { $item in
Toggle(isOn: $item.isOn) {
Text(item.name)
}
}
Spacer()
}.padding(.top, 50)
.onReceive(itemsPublisher.items.publisher) { newItem in
print("----> new item: \(newItem)")
}
}
}
It seems as though removing the bindings is the right way forward!
Looking at the docs this makes sense now https://developer.apple.com/documentation/swiftui/binding
Use a binding to create a two-way connection between a property that stores data, and a view that displays and changes the data.
Although not explicit, this does suggest that the binding "property wrapper" is only to be used as a property in a SwiftUI view rather than a data model.
My changes
I added a closure to my view
let itemDidToggle: (ToggleItem, Bool) -> Void
and this is called in the Toggle binding's set() function which updates the value outside of the view, keeping the view dumb. This triggers the publisher to get called and update my stack. This coupled with updating the == equatable function to include the isOn property makes everything work...
My code
import UIKit
import PlaygroundSupport
import Combine
import SwiftUI
public struct MyItem {
public let identifier: String
public let description: String
}
public class ItemsManager {
public private(set) var items: [MyItem]
public let itemsPublisher: CurrentValueSubject<[MyItem], Never>
private let userDefaults: UserDefaults
public init(items: [MyItem], userDefaults: UserDefaults = .standard) {
self.userDefaults = userDefaults
self.items = items
self.itemsPublisher = .init(items)
}
public func isItemEnabled(identifier: String) -> Bool {
guard let item = item(for: identifier) else {
return false
}
if let isOnValue = userDefaults.object(forKey: item.identifier) as? NSNumber {
return isOnValue.boolValue
} else {
return false
}
}
public func setEnabled(_ isEnabled: Bool, forIdentifier identifier: String) {
userDefaults.set(isEnabled, forKey: identifier)
itemsPublisher.send(items)
}
func item(for identifier: String) -> MyItem? {
return items.first { $0.identifier == identifier }
}
}
struct MyView: View {
#State private var items: [ToggleItem] = []
let itemsPublisher: AnyPublisher<[ToggleItem], Never>
let itemDidToggle: (ToggleItem, Bool) -> Void
public init(
itemsPublisher: AnyPublisher<[ToggleItem], Never>,
itemDidToggle: #escaping (ToggleItem, Bool) -> Void
) {
self.itemsPublisher = itemsPublisher
self.itemDidToggle = itemDidToggle
}
var body: some View {
List {
Section(header: Text("Items")) {
ForEach(items) { item in
Toggle(
item.name,
isOn: .init(
get: { item.isOn },
set: { itemDidToggle(item, $0) }
)
)
}
}
}
.animation(.default, value: items)
.onReceive(itemsPublisher) { newItems in
print("New items: \(newItems)")
items = newItems
}
}
struct ToggleItem: Swift.Identifiable, Equatable {
let id: String
let name: String
let isOn: Bool
public static func == (lhs: ToggleItem, rhs: ToggleItem) -> Bool {
lhs.id == rhs.id && lhs.isOn == rhs.isOn
}
}
}
let itemsManager = ItemsManager(items: (1...10).map { .init(identifier: UUID().uuidString, description: "item \($0)") })
let publisher = itemsManager.itemsPublisher
.map { myItems in
myItems.map { myItem in
MyView.ToggleItem(id: myItem.identifier, name: myItem.description, isOn: itemsManager.isItemEnabled(identifier: myItem.identifier))
}
}
.eraseToAnyPublisher()
let view = MyView(itemsPublisher: publisher) { item, newValue in
itemsManager.setEnabled(newValue, forIdentifier: item.id)
}
Related
I am attempting to pass a Binding through my NavigationStack enum into my View. I'm not sure if I can pass Binding into an enum, if I cannot then how should I go about this. Thanks in advance!
#available(iOS 16.0, *)
enum Route: Hashable, Equatable {
//ERROR HERE: Not sure how to get Binding in enum or if possible
case gotoBView(input: Binding<String>)
#ViewBuilder
func view(_ path: Binding<NavigationPath>) -> some View{
switch self {
case .gotoBView(let input): BView1(bvar: input)
}
}
var isEmpty: Bool {
return false
}
}
//START VIEW
#available(iOS 16.0, *)
struct ContentView25: View {
#State var input = "Hello"
#State var path: NavigationPath = .init()
var body: some View {
NavigationStack(path: $path){
NavigationLink(value: Route.gotoBView(input: $input), label: {Text("Go To A")})
.navigationDestination(for: Route.self){ route in
route.view($path)
}
}
}
}
//View to navigate to with binding
#available(iOS 16.0, *)
struct BView1: View {
#Binding var bvar: String
var body: some View {
Text(bvar)
}
}
Here is a workaround, in previous iOS versions this has dismissed the NavigationLink, In iOS 16.2 it does not behave this way, I would do extensive testing before using this in a production app.
import SwiftUI
#available(iOS 16.0, *)
enum Route: Hashable, Equatable {
case gotoBView(input: Binding<String>)
#ViewBuilder
func view(_ path: Binding<NavigationPath>) -> some View{
switch self {
case .gotoBView(let input): BView1(bvar: input)
}
}
//Create a custom implementation of Hashable that ignores Binding
func hash(into hasher: inout Hasher) {
switch self {
case .gotoBView(let input):
hasher.combine(input.wrappedValue)
}
}
//Create a custom implementation of Equatable that ignores Binding
static func == (lhs: Route, rhs: Route) -> Bool {
return lhs.hashValue == rhs.hashValue
}
}
SwiftUI is all about identity and NavigationPath uses Hashable and Equatable to function. This bypasses SwiftUI's implementation.
With the stack, you don't need an enum, you can have multiple navigationDestination for each value type. To use it with a binding you can do it the old way, with a func that looks it up by ID, like how we did it before ForEach supported bindings, e.g.
struct NumberItem: Identifiable {
let id = UUID()
var number: Int
var text = ""
}
struct ContentView: View {
#State var numberItems = [NumberItem(number: 1), NumberItem(number: 2), NumberItem(number: 3), NumberItem(number: 4), NumberItem(number: 5)]
var body: some View {
NavigationStack {
List {
ForEach(numberItems) { numberItem in
NavigationLink(value: numberItem.id) {
Text("\(numberItem.number)")
}
}
}
.navigationDestination(for: NumberItem.ID.self) { numberItemID in
ChildDetailView(numberItems: $numberItems, numberItemID: numberItemID)
}
//.navigationDestination(for: AnotherItem.ID.self) { anotherItemID in
// ...
//}
}
}
}
// this wrapper View was originally needed to make bindings in
// navigationDestinations work at all, now its needed to fix a bug
// with the cursor jumping to the end of a text field which is
// using the binding.
struct ChildDetailView: View {
#Binding var numberItems: [NumberItem]
let numberItemID: UUID
var body: some View {
ChildDetailView2(numberItem: binding(for: numberItemID))
}
private func binding(for numberItemID: UUID) -> Binding<NumberItem> {
guard let index = numberItems.firstIndex(where: { $0.id == numberItemID }) else {
fatalError("Can't find item in array")
}
return $numberItems[index]
}
}
struct ChildDetailView2: View {
#Binding var numberItem: NumberItem
var body: some View {
VStack {
Text("\(numberItem.number)")
TextField("Test", text: $numberItem.text) // cursor jumps to the end if not wrapped in an extra View like this one is.
Button {
numberItem.number += 10
} label: {
Text("Add 10")
}
}
.navigationTitle("Detail")
}
}
I was wondering how to provide an empty state view in a list when the data source of the list is empty. Below is an example, where I have to wrap it in an if/else statement. Is there a better alternative for this, or is there a way to create a modifier on a List that'll make this possible i.e. List.emptyView(Text("No data available...")).
import SwiftUI
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
VStack {
if objects.isEmpty {
Text("Oops, loos like there's no data...")
} else {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
}
}
}
}
struct EmptyListExample_Previews: PreviewProvider {
static var previews: some View {
EmptyListExample(objects: [])
}
}
I quite like to use an overlay attached to the List for this because it's quite a simple, flexible modifier:
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
VStack {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
.overlay(Group {
if objects.isEmpty {
Text("Oops, loos like there's no data...")
}
})
}
}
}
It has the advantage of being nicely centred & if you use larger placeholders with an image, etc. they will fill the same area as the list.
One of the solutions is to use a #ViewBuilder:
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
listView
}
#ViewBuilder
var listView: some View {
if objects.isEmpty {
emptyListView
} else {
objectsListView
}
}
var emptyListView: some View {
Text("Oops, loos like there's no data...")
}
var objectsListView: some View {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
}
}
You can create a custom modifier that substitutes a placeholder view when your list is empty. Use it like this:
List(items) { item in
Text(item.name)
}
.emptyPlaceholder(items) {
Image(systemName: "nosign")
}
This is the modifier:
struct EmptyPlaceholderModifier<Items: Collection>: ViewModifier {
let items: Items
let placeholder: AnyView
#ViewBuilder func body(content: Content) -> some View {
if !items.isEmpty {
content
} else {
placeholder
}
}
}
extension View {
func emptyPlaceholder<Items: Collection, PlaceholderView: View>(_ items: Items, _ placeholder: #escaping () -> PlaceholderView) -> some View {
modifier(EmptyPlaceholderModifier(items: items, placeholder: AnyView(placeholder())))
}
}
I tried #pawello2222's approach, but the view didn't get rerendered if the passed objects' content change from empty(0) to not empty(>0), or vice versa, but it worked if the objects' content was always not empty.
Below is my approach to work all the time:
struct SampleList: View {
var objects: [IdentifiableObject]
var body: some View {
ZStack {
Empty() // Show when empty
List {
ForEach(objects) { object in
// Do something about object
}
}
.opacity(objects.isEmpty ? 0.0 : 1.0)
}
}
}
You can make ViewModifier like this for showing the empty view. Also, use View extension for easy use.
Here is the demo code,
//MARK: View Modifier
struct EmptyDataView: ViewModifier {
let condition: Bool
let message: String
func body(content: Content) -> some View {
valideView(content: content)
}
#ViewBuilder
private func valideView(content: Content) -> some View {
if condition {
VStack{
Spacer()
Text(message)
.font(.title)
.foregroundColor(Color.gray)
.multilineTextAlignment(.center)
Spacer()
}
} else {
content
}
}
}
//MARK: View Extension
extension View {
func onEmpty(for condition: Bool, with message: String) -> some View {
self.modifier(EmptyDataView(condition: condition, message: message))
}
}
Example (How to use)
struct EmptyListExample: View {
#State var objects: [Int] = []
var body: some View {
NavigationView {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
.onEmpty(for: objects.isEmpty, with: "Oops, loos like there's no data...") //<--- Here
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button("Add") {
objects = [1,2,3,4,5,6,7,8,9,10]
}
Button("Empty") {
objects = []
}
}
}
}
}
}
In 2021 Apple did not provide a List placeholder out of the box.
In my opinion, one of the best way to make a placeholder, it's creating a custom ViewModifier.
struct EmptyDataModifier<Placeholder: View>: ViewModifier {
let items: [Any]
let placeholder: Placeholder
#ViewBuilder
func body(content: Content) -> some View {
if !items.isEmpty {
content
} else {
placeholder
}
}
}
struct ContentView: View {
#State var countries: [String] = [] // Data source
var body: some View {
List(countries) { country in
Text(country)
.font(.title)
}
.modifier(EmptyDataModifier(
items: countries,
placeholder: Text("No Countries").font(.title)) // Placeholder. Can set Any SwiftUI View
)
}
}
Also via extension can little bit improve the solution:
extension List {
func emptyListPlaceholder(_ items: [Any], _ placeholder: AnyView) -> some View {
modifier(EmptyDataModifier(items: items, placeholder: placeholder))
}
}
struct ContentView: View {
#State var countries: [String] = [] // Data source
var body: some View {
List(countries) { country in
Text(country)
.font(.title)
}
.emptyListPlaceholder(
countries,
AnyView(ListPlaceholderView()) // Placeholder
)
}
}
If you are interested in other ways you can read the article
I have created a drag-and-drop viewmodifier that works as expected, but now I would like to make it accept any object. I can add <T: Identifiable> to all the functions, structs, and view-modifiers, but when I try to do add it to my singleton class, I get "Static stored properties not supported in generic types".
I need the singleton class, so I can put the .dropObjectOutside viewmodifier anywhere in my view-hierarchy, so I've tried downcasting the ID to a String, but I can't seem to make that work.
Is there a way to downcast or make this code accept any object?
import SwiftUI
// I want this to be any object
struct StopContent: Identifiable {
var id: String = UUID().uuidString
}
// Singleton class to hold drag state
class DragToReorderController: ObservableObject {
// Make it a singleton, so it can be accessed from any view
static let shared = DragToReorderController()
private init() { }
#Published var draggedID: String? // How do I make this a T.ID or downcast T.ID to string everywhere else?
#Published var dragActive:Bool = false
}
// Add ViewModifier to view
extension View {
func dragToReorder(_ item: StopContent, array: Binding<[StopContent]>) -> some View {
self.modifier(DragToReorderObject(sourceItem: item, contentArray: array))
}
func dropOutside() -> some View {
self.onDrop(of: [UTType.text], delegate: DropObjectOutsideDelegate())
}
}
import UniformTypeIdentifiers
// MARK: View Modifier
struct DragToReorderObject: ViewModifier {
let sourceItem: StopContent
#Binding var contentArray: [StopContent]
#ObservedObject private var dragReorder = DragToReorderController.shared
func body(content: Content) -> some View {
content
.onDrag {
dragReorder.draggedID = sourceItem.id
dragReorder.dragActive = false
return NSItemProvider(object: String(sourceItem.id) as NSString)
}
.onDrop(of: [UTType.text], delegate: DropObjectDelegate(sourceItem: sourceItem, listData: $contentArray, draggedItem: $dragReorder.draggedID, dragActive: $dragReorder.dragActive))
.onChange(of: dragReorder.dragActive, perform: { value in
if value == false {
// Drag completed
}
})
.opacity(dragReorder.draggedID == sourceItem.id && dragReorder.dragActive ? 0 : 1)
}
}
// MARK: Drop and reorder
struct DropObjectDelegate: DropDelegate {
let sourceItem: StopContent
#Binding var listData: [StopContent]
#Binding var draggedItem: String?
#Binding var dragActive: Bool
func dropEntered(info: DropInfo) {
if draggedItem == nil { draggedItem = sourceItem.id }
dragActive = true
// Make sure the dragged item has moved and that it still exists
if sourceItem.id != draggedItem {
if let draggedItemValid = draggedItem {
if let from = listData.firstIndex(where: { $0.id == draggedItemValid } ) {
// If that is true, move it to the new location
let to = listData.firstIndex(where: { $0.id == sourceItem.id } )!
if listData[to].id != draggedItem! {
listData.move(fromOffsets: IndexSet(integer: from),
toOffset: to > from ? to + 1 : to)
}
}
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
dragActive = false
draggedItem = nil
return true
}
}
// MARK: Drop and cancel
struct DropObjectOutsideDelegate: DropDelegate {
// Using a singleton so we can drop anywhere
#ObservedObject private var dragReorder = DragToReorderController.shared
func dropEntered(info: DropInfo) {
dragReorder.dragActive = true
}
func performDrop(info: DropInfo) -> Bool {
dragReorder.dragActive = false
dragReorder.draggedID = nil
return true
}
}
For this, you have to add Identifiable generic constraint everywhere. Also, use Int for draggedID instead of String.
Here is the demo code.
// Singleton class to hold drag state
class DragToReorderController: ObservableObject {
// Make it a singleton, so it can be accessed from any view
static let shared = DragToReorderController()
private init() { }
#Published var draggedID: Int?
#Published var dragActive: Bool = false
}
// Add ViewModifier to view
extension View {
func dragToReorder<T: Identifiable>(_ item: T, array: Binding<[T]>) -> some View {
self.modifier(DragToReorderObject(sourceItem: item, contentArray: array))
}
func dropOutside() -> some View {
self.onDrop(of: [UTType.text], delegate: DropObjectOutsideDelegate())
}
}
import UniformTypeIdentifiers
// MARK: View Modifier
struct DragToReorderObject<T: Identifiable>: ViewModifier {
let sourceItem: T
#Binding var contentArray: [T]
#ObservedObject private var dragReorder = DragToReorderController.shared
func body(content: Content) -> some View {
content
.onDrag {
dragReorder.draggedID = sourceItem.id.hashValue
dragReorder.dragActive = false
return NSItemProvider(object: String(sourceItem.id.hashValue) as NSString)
}
.onDrop(of: [UTType.text], delegate: DropObjectDelegate(sourceItem: sourceItem, listData: $contentArray, draggedItem: $dragReorder.draggedID, dragActive: $dragReorder.dragActive))
.onChange(of: dragReorder.dragActive, perform: { value in
if value == false {
// Drag completed
}
})
.opacity((dragReorder.draggedID == sourceItem.id.hashValue) && dragReorder.dragActive ? 0 : 1)
}
}
// MARK: Drop and reorder
struct DropObjectDelegate<T: Identifiable>: DropDelegate {
let sourceItem: T
#Binding var listData: [T]
#Binding var draggedItem: Int?
#Binding var dragActive: Bool
func dropEntered(info: DropInfo) {
if draggedItem == nil { draggedItem = sourceItem.id.hashValue }
dragActive = true
// Make sure the dragged item has moved and that it still exists
if sourceItem.id.hashValue != draggedItem {
if let draggedItemValid = draggedItem {
if let from = listData.firstIndex(where: { $0.id.hashValue == draggedItemValid } ) {
// If that is true, move it to the new location
let to = listData.firstIndex(where: { $0.id == sourceItem.id } )!
if listData[to].id.hashValue != draggedItem! {
listData.move(fromOffsets: IndexSet(integer: from),
toOffset: to > from ? to + 1 : to)
}
}
}
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
dragActive = false
draggedItem = nil
return true
}
}
// MARK: Drop and cancel
struct DropObjectOutsideDelegate: DropDelegate {
// Using a singleton so we can drop anywhere
#ObservedObject private var dragReorder = DragToReorderController.shared
func dropEntered(info: DropInfo) {
dragReorder.dragActive = true
}
func performDrop(info: DropInfo) -> Bool {
dragReorder.dragActive = false
dragReorder.draggedID = nil
return true
}
}
I am trying to use an ActionSheet to manipulate items of a List. How can I call a function (in this example deleteItem) that is part of the data model, using an ActionSheet and manipulte the selected item, similar to what .onDelete does?
My view presents items from a model using the following code:
struct ItemManager: View {
#ObservedObject var model: ItemModel
var body: some View {
List {
ForEach(model.items) { item in
ItemCell(item: item)
}
.onDelete { self.model.deleteItem(at: $0) }
}
}
}
struct ItemCell: View {
var item: Item
#State private var isActionSheetVisible = false
private var actionSheet: ActionSheet {
let button1 = ActionSheet.Button.default(Text("Delete")){
self.isActionSheetVisible = false
}
let button2 = ActionSheet.Button.cancel(){
self.isActionSheetVisible = false
}
let buttons = [button1, button2]
return ActionSheet(title: Text("Actions"), buttons: buttons)
}
var body: some View {
VStack(alignment: .leading) {
Button(action: {
self.isActionSheetVisible = true
}) {
Text(item.title).font(.headline)
}.actionSheet(isPresented: self.$isActionSheetVisible) {
self.actionSheet
}
}
}
}
My model has some simple properties and a function that deletes items from the collection:
struct Item: Identifiable, Equatable {
let title: String
var id: String {
title
}
}
class ItemModel: ObservableObject {
#Published var items: [Item] = [Item(title: "temp.1"), Item(title: "temp.2")]
public func deleteItem(at indices: IndexSet) {
indices.forEach { items.remove(at: $0) }
}
}
extension Item {
static let previewItem = Item(title: "temp.3")
}
Update: Added Equatable in the Item declaration to comform.
You could try passing the ItemModel to the ForEach() like so:
ForEach(model.items) { item in
ItemCell(item: item, model: self.model)
}
Then in your ItemCell you can:
struct ItemCell: View {
var item: Item
var model: ItemModel // Add the model variable
#State private var isActionSheetVisible = false
private var actionSheet: ActionSheet {
let button1 = ActionSheet.Button.default(Text("Delete")) {
// Get the index
if let index = self.model.items.firstIndex(of: self.item) {
// Delete the item based on the index
self.model.items.remove(at: index)
// Dismiss the ActionSheet
self.isActionSheetVisible = false
} else {
print("Could not find item!")
print(self.item)
}
}
}
}
Im trying to create an environment object that is editable and putting it in a list.
The Variables are only refreshing when I switch the tab for example (so whenever I leave the NavigationView) and then come back.
The same worked with a ModalView before. Is it a bug maybe? Or am I doing something wrong?
import SwiftUI
import Combine
struct TestView: View {
#State var showSheet: Bool = false
#EnvironmentObject var feed: TestObject
func addObjects() {
var strings = ["one","two","three","four","five","six"]
for s in strings {
var testItem = TestItem(text: s)
self.feed.items.append(testItem)
}
}
var body: some View {
TabView {
NavigationView {
List(feed.items.indices, id:\.self) { i in
NavigationLink(destination: detailView(feed: self._feed, i: i)) {
HStack {
Text(self.feed.items[i].text)
Text("(\(self.feed.items[i].read.description))")
}
}
}
}
.tabItem({ Text("Test") })
.tag(0)
Text("Blank")
.tabItem({ Text("Test") })
.tag(0)
}.onAppear {
self.addObjects()
}
}
}
struct detailView: View {
#EnvironmentObject var feed: TestObject
var i: Int
var body: some View {
VStack {
Text(feed.items[i].text)
Text(feed.items[i].read.description)
Button(action: { self.feed.items[self.i].isRead.toggle() }) {
Text("Toggle read")
}
}
}
}
final class TestItem: ObservableObject {
init(text: String) {
self.text = text
self.isRead = false
}
static func == (lhs: TestItem, rhs: TestItem) -> Bool {
lhs.text < rhs.text
}
var text: String
var isRead: Bool
let willChange = PassthroughSubject<TestItem, Never>()
var read: Bool {
set {
self.isRead = newValue
}
get {
self.isRead
}
}
}
class TestObject: ObservableObject {
var willChange = PassthroughSubject<TestObject, Never>()
#Published var items: [TestItem] = [] {
didSet {
willChange.send(self)
}
}
}
try passing .environmentObject on your destination:
NavigationLink(destination: detailView(feed: self._feed, i: i).environmentObject(x))
You have to use willSet instead of didSet.
TestItem should be a value type: struct or enum. SwiftUI's observation system properly works only with value types.