I have an issue with a OSX app developed with SwiftUI.
Here is my model.
class Chapter: Identifiable, Hashable, ObservableObject {
var content: [ChapterContent] = []
var id: Int
// Other Equatable/Hashable related code...
}
// A chapter content contains either a message or an event,
// both can't be nil at the same time
struct ChapterContent: Identifiable, Hashable {
var event: Event?
var message: Message?
var id: Int {
return self.message?.id ?? self.event?.id ?? .min
}
init(_ event: Event) {
self.event = event
self.message = nil
}
init(_ message: Message) {
self.event = nil
self.message = message
}
var representingView: AnyView {
// A view that represents the event or the message
}
// Other Equatable/Hashable related code...
}
The app starts with a ContentView() containing an array of chapters, represented in a list.
The view also contains a button to add a new chapter (with a hardcoded id so far just to test this).
struct ContentView: View {
var chapters: [Chapter]
var body: some View {
NavigationView {
VStack {
List {
ForEach(chapters, id: \.self) { chapter in
Text("Chapter C\(chapter.id)")
}
}
Spacer()
NavigationLink(
destination: ChapterView(chapter: Chapter(id: 1)),
label: {
Text("ADD CHAPTER")
})
}
}
}
}
In this ChapterView, I display all the content of the chapter and also two buttons to add either a new event or a new message into this chapter. To simplify it I'll just show the ADD EVENT button.
struct ChapterView: View {
#State var chapter: Chapter
var body: some View {
NavigationView {
VStack(spacing: 0) {
VStack(spacing: 0) {
ForEach(chapter.content, id: \.self) { content in
content.representingView
}
.padding(0)
}
HStack(spacing: 0) {
NavigationLink(
destination: EventFormView(with: $chapter),
label: {
Text("ADD EVENT")
})
}
}
}
}
}
And in the EventFormView:
struct EventFormView: View {
#Binding var chapter: Chapter
init(with chapter: Binding<Chapter>) {
self._chapter = chapter
}
}
Somewhere in EventFormView I add the newly created event with:
chapter.content.append(newEventChapterContent)
After checking while debugging, I ensured that the value is actually added into the content array of the chapter. However, this doesn't trigger any UI update in the list (which is contained in ChapterView).
I guess it is related to the way I use the State/Binding protocols. I should even maybe use ObservedObject but I don't know how to do so and correctly transmit data from view to view.
Thank you for your help
At first make content published so it can be observed by views
class Chapter: Identifiable, Hashable, ObservableObject {
#Published var content: [ChapterContent] = []
now as it is observable, wrap it in observed in views
struct ChapterView: View {
#ObservedObject var chapter: Chapter
struct EventFormView: View {
#ObservedObject var chapter: Chapter
// previous init not needed
and create it as usual, because observable object is a reference, ie
EventFormView(chapter: chapter)
Related
When I update a binding property from an array in a pushed view 2+ layers down, the navigation pops back instantly after a change to the property.
Xcode 13.3 beta, iOS 15.
I created a simple demo and code is below.
Shopping Lists
List Edit
List section Edit
Updating the list title (one view deep) is fine, navigation stack stays same, and changes are published if I return. But when adjusting a section title (two deep) the navigation pops back as soon as I make a single change to the property.
I have a feeling I'm missing basic fundamentals here, and I have a feeling it must be related to the lists id? but I'm struggling to figure it out or work around it.
GIF
Code:
Models:
struct ShoppingList {
let id: String = UUID().uuidString
var title: String
var sections: [ShoppingListSection]
}
struct ShoppingListSection {
let id: String = UUID().uuidString
var title: String
}
View Model:
final class ShoppingListsViewModel: ObservableObject {
#Published var shoppingLists: [ShoppingList] = [
.init(
title: "Shopping List 01",
sections: [
.init(title: "Fresh food")
]
)
]
}
Content View:
struct ContentView: View {
var body: some View {
NavigationView {
ShoppingListsView()
}
}
}
ShoppingListsView
struct ShoppingListsView: View {
#StateObject private var viewModel = ShoppingListsViewModel()
var body: some View {
List($viewModel.shoppingLists, id: \.id) { $shoppingList in
NavigationLink(destination: ShoppingListEditView(shoppingList: $shoppingList)) {
Text(shoppingList.title)
}
}
.navigationBarTitle("Shopping Lists")
}
}
ShoppingListEditView
struct ShoppingListEditView: View {
#Binding var shoppingList: ShoppingList
var body: some View {
Form {
Section(header: Text("Title")) {
TextField("Title", text: $shoppingList.title)
}
Section(header: Text("Sections")) {
List($shoppingList.sections, id: \.id) { $section in
NavigationLink(destination: ShoppingListSectionEditView(section: $section)) {
Text(section.title)
}
}
}
}
.navigationBarTitle("Edit list")
}
}
ShoppingListSectionEditView
struct ShoppingListSectionEditView: View {
#Binding var section: ShoppingListSection
var body: some View {
Form {
Section(header: Text("Title")) {
TextField("title", text: $section.title)
}
}
.navigationBarTitle("Edit section")
}
}
try this, works for me:
struct ContentView: View {
var body: some View {
NavigationView {
ShoppingListsView()
}.navigationViewStyle(.stack) // <--- here
}
}
Try to make you object confirm to Identifiable and return value which unique and stable, for your case is ShoppingList.
Detail view seems will pop when object id changed.
The reason your stack is popping back to the root ShoppingListsView is that the change in the list is published and the root ShoppingListsView is registered to listen for updates to the #StateObject.
Therefore, any change to the list is listened to by ShoppingListsView, causing that view to be re-rendered and for all new views on the stack to be popped in order to render the root ShoppingListsView, which is listening for updates on the #StateObject.
The solution to this is to change the #StateObject to #EnvironmentObject
Please refactor your code to change ShoppingListsViewModel to use an #EnvironmentObject wrapper instead of a #StateObject wrapper
You may pass the environment object in to all your child views and also add a boolean #Published flag to track any updates to the data.
Then your ShoppingListView would look as below
struct ShoppingListsView: View {
#EnvironmentObject var viewModel = ShoppingListsViewModel()
var body: some View {
List($viewModel.shoppingLists, id: \.id) { $shoppingList in
NavigationLink(destination: ShoppingListEditView(shoppingList: $shoppingList)) {
Text(shoppingList.title)
}
}
.navigationBarTitle("Shopping Lists")
}
}
Don't forget to pass the viewModel in to all your child views.
That should fix your problem.
I'm having trouble with what I think may be a bug, but most likely me doing something wrong.
I have a slightly complex navigation state variable in my model that I'm using for tracking/setting state between tab and sidebar presentations when multitasking on iPad. That all works fine except in tab mode, once I use a navigation link once I can't seem to use one again, whether the binding is on my tab view or navigation links in a list.
Would really appreciate any thoughts on this,
Cheers!
Example
NavigationItem.swift
enum SubNavigationItem: Hashable {
case overview, user, hobby
}
enum NavigationItem: Hashable {
case home(SubNavigationItem)
case settings
}
Model.swift
final class Model: ObservableObject {
#Published var selectedTab: NavigationItem = .home(.overview)
}
SwiftUIApp.swift
#main
struct SwiftUIApp: App {
#StateObject var model = Model()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(model)
}
}
}
ContentView.swift
struct ContentView: View {
var body: some View {
AppTabNavigation()
}
}
AppTabNavigation.swift
struct AppTabNavigation: View {
#EnvironmentObject private var model: Model
var body: some View {
TabView(selection: $model.selectedTab) {
NavigationView {
HomeView()
}
.tabItem {
Label("Home", systemImage: "house")
}
.tag(NavigationItem.home(.overview))
NavigationView {
Text("Settings View")
}
.tabItem {
Label("Settings", systemImage: "gear")
}
.tag(NavigationItem.settings)
}
}
}
HomeView.swift
I created a binding here because selection required an optional <NavigationItem?> not
struct HomeView: View {
#EnvironmentObject private var model: Model
var body: some View {
let binding = Binding<NavigationItem?>(
get: {
model.selectedTab
},
set: {
guard let item = $0 else { return }
model.selectedTab = item
}
)
List {
NavigationLink(
destination: Text("Users"),
tag: .home(.user),
selection: binding
) {
Text("Users")
}
NavigationLink(
destination: Text("Hobbies"),
tag: .home(.hobby),
selection: binding
) {
Text("Hobbies")
}
}
.navigationTitle("Home")
}
}
Second Attempt
I tried making the selectedTab property optional as #Lorem Ipsum suggested. Which means I can remove the binding there. But then the TabView doesn't work with the property. So I create a binding for that and have the same issue but with the tab bar!
Make the selected tab optional
#Published var selectedTab: NavigationItem? = .home(.overview)
And get rid of that makeshift binding variable. Just use the variable
$model.selectedTab
If the variable can never be nil then something is always selected IAW with that makeshift variable it will just keep the last value.
Simple use case: A list of States with a Recents section that shows those States you have navigated to recently. When a link is tapped, the animation begins but is then aborted presumably as a result of the onAppear handler in the detail view changing the recents property of the model -- which is observed by the framework. Log messages indicate the framework is unhappy, but unclear how to make it happy short of a ~2 second delay before adding to recents...
import SwiftUI
class USState: Identifiable{
typealias ID = String
var id: ID
var name: String
init(_ name: String, id: String){
self.id = id
self.name = name
}
}
/** The model is a small set of us states for example purposes.
It also publishes a recents property which has a lifo stack of states that have been viewed.
*/
class StateModel: ObservableObject{
var states: [USState]
var stateMap: [USState.ID: USState]
#Published var recents = [USState]()
init(){
states = [
USState("California", id: "CA"),
USState("Georgia", id: "GA"),
USState("New York", id: "NY"),
USState("New Jersey", id: "NJ"),
USState("Montana", id: "MT")
]
stateMap = [USState.ID: USState]()
for state in states{
stateMap[state.id] = state
}
}
func addRecent(_ state: USState){
recents.removeAll(where: {$0.id == state.id})
recents.insert(state, at: 0)
}
func allExceptRecent() -> [USState]{
states.filter{ state in
recents.contains{
state.id == $0.id
} == false
}
}
}
/** A simple view to serve as the destination of a state link
*/
struct StateView: View{
#EnvironmentObject var stateModel: StateModel
var usState: USState
var body: some View{
Text(usState.name)
.onAppear{
DispatchQueue.main.async {
withAnimation {
stateModel.addRecent(usState)
}
}
}
}
}
/** A list of states broken into two sections, those that have been recently viewed, and the remainder.
Desired behavior is that when a state is tapped, it should navigate to its respective detail view and update the list of recents.
The issue is that the recents updating appears to confuse SwiftUI and the navigation is aborted.
*/
struct SidebarBounce: View {
#EnvironmentObject var model: StateModel
#SceneStorage("selectionStore") private var selectionStore: USState.ID?
struct Header: View{
var text: String
var body: some View{
Text(text)
.font(.headline)
.padding()
}
}
struct Row: View{
var text: String
var body: some View{
VStack{
HStack{
Text(text)
.padding([.leading, .trailing])
.padding([.top, .bottom], 8)
Spacer()
}
Divider()
}
}
}
var body: some View{
ScrollView{
LazyVStack(alignment: .leading, spacing: 0){
Section(
header: Header(text: "Recent")
){
ForEach(model.recents){place in
NavigationLink(
destination: StateView(usState: place),
tag: place.id,
selection: $selectionStore
){
Row(text: place.name)
}
.id("Recent \(place.id)")
}
}
Section(
header: Header(text: "All")
){
ForEach(model.allExceptRecent()){place in
NavigationLink(
destination: StateView(usState: place),
tag: place.id,
selection: $selectionStore
){
Row(text: place.name)
}
.id("All \(place.id)")
}
}
}
}
}
}
struct BounceWrap: View{
let model = StateModel()
var body: some View{
NavigationView{
SidebarBounce()
.navigationTitle("Aborted Navigation")
Text("Nothing Selected")
}
.environmentObject(model)
}
}
#main
struct DemoApp: App {
var body: some Scene {
WindowGroup {
BounceWrap()
}
}
}
Note: This must be run as an app (iPhone or simulator) rather than in preview.
I have tried to set the clicked state as a recent state before executing the animation, and the issue seems to be resolved as it can be seen from the GIF below:
To set the state as recent before executing the NavigationView, we need to implement a programmatic NavigationView as seen below:
struct SidebarBounce: View {
#EnvironmentObject var model: StateModel
#SceneStorage("selectionStore") private var selectionStore: USState.ID?
#State private var clickedState: USState? // <-- see here
#State private var expandState = false // <-- and here
.
.
.
Section(
header: Header(text: "All")
){
ForEach(model.allExceptRecent()){place in
Button(action: {
withAnimation {
model.addRecent(place) // <-- Making state recent
}
clickedState = place // <-- programmatically executing NavigationLink below
expandState = true // <-- programmatically executing NavigationLink below
}, label: {
Row(text: place.name)
})
}
}
}
// Programmatic NavigationLink that is triggered by a Bool value vvv
NavigationLink(
destination: clickedState == nil ? nil : StateView(usState: clickedState!),
isActive: $expandState,
label: EmptyView.init)
}
}
}
I think the reason this is happening is because the StateModel object is identical in both views, and updating it from a view causes all views to update even if it was not the active view.
If any other issues occur, try having a unique ViewModel for each view, and each ViewModel listens to changes happening in StateModel (Singleton), and the view listens for changes from the ViewModel and reflects them into the UI.
I have a simple watchOS 6.2.8 SwiftUI application in which I present a list of messages to the user.
These messages are modelled as classes and have a title, body and category name. I also have a Category object which is a view on these messages that only shows a specific category name.
I specifically mention watchOS 6.2.8 because it seems SwiftUI behaves a bit different there than on other platforms.
class Message: Identifiable {
let identifier: String
let date: Date
let title: String
let body: String
let category: String
var id: String {
identifier
}
init(identifier: String, date: Date, title: String, body: String, category: String) {
self.identifier = identifier
self.date = date
self.title = title
self.body = body
self.category = category
}
}
class Category: ObservableObject, Identifiable {
let name: String
#Published var messages: [Message] = []
var id: String {
name
}
init(name: String, messages: [Message] = []) {
self.name = name
self.messages = messages
}
}
Category itself is an #ObservableObject and publishes messages, so that when a category gets updated (like in the background), the view that is displaying the category message list will also update. (This works great)
To store these messages I have a simple MessageStore, which is an #ObservableObject that looks like this:
class MessageStore: ObservableObject {
#Published var messages: [Message] = []
#Published var categories: [Category] = []
static let sharedInstance = MessageStore()
func insert(message: Message) throws { ... mutage messages and categories ... }
func delete(message: Message) throws { ... mutage messages and categories ... }
}
(For simplicity I use a singleton, because there are problems with environment objects not being passed properly on watchOS)
The story keeps messages and categories in sync. When a new Message is added that has a category name set, it will also create or update a Category object in the categories list.
In my main view I present two things:
An All Messages NavigationLink that goes to a view to display all messages
For each Category I display a NavigationLink that goes to a view to display messages just in that specific category.
This all works, amazingly. But there is one really odd thing happening that I do not understand. (First SwiftUI project)
When I go into the All Messages list and delete all messages containing to a specific category, something unexpected happen when I navigate back to the main view.
First I observe that the category is properly removed from the list.
But then, the main view automatically quickly navigates to the All Messages list and then back.
The last part is driving me .. crazy .. I don't understand why this is happening. From a data perspective everyting looks good - the messages have been deleted and the category too. The final UI state, after the automagical navigation, also looks good - the message count for All Messages is correct and the category with now zero messages is not shown in the list anymore.
Here is the code for the main ContentView and also for the AllMessagesView. If helpful I can post the complete code here of course.
struct AllMessagesView: View {
#ObservedObject var messageStore = MessageStore.sharedInstance
#ViewBuilder
var body: some View {
if messageStore.messages.count == 0 {
Text("No messages").multilineTextAlignment(.center)
.navigationBarTitle("All Messages")
} else {
List {
ForEach(messageStore.messages) { message in
MessageCellView(message: message)
}.onDelete(perform: deleteMessages)
}
.navigationBarTitle("All Messages")
}
}
func deleteMessages(at offsets: IndexSet) {
for index in offsets {
do {
try messageStore.delete(message: messageStore.messages[index])
} catch {
NSLog("Failed to delete message: \(error.localizedDescription)")
}
}
}
}
//
struct CategoryMessagesView: View {
#ObservedObject var messageStore = MessageStore.sharedInstance
#ObservedObject var category: Category
var body: some View {
Group {
if category.messages.count == 0 {
Text("No messages in category “\(category.name)”").multilineTextAlignment(.center)
} else {
List {
ForEach(category.messages) { message in
MessageCellView(message: message)
}.onDelete(perform: deleteMessages)
}
}
}.navigationBarTitle(category.name)
}
func deleteMessages(at offsets: IndexSet) {
for index in offsets {
do {
try messageStore.delete(message: category.messages[index])
} catch {
NSLog("Cannot delete message: \(error.localizedDescription)")
}
}
}
}
struct ContentView: View {
#ObservedObject var messageStore = MessageStore.sharedInstance
var body: some View {
List {
Section {
NavigationLink(destination: AllMessagesView()) {
HStack {
Image(systemName: "tray.2")
Text("All Messages")
Spacer()
Text("\(messageStore.messages.count)")
.font(messageCountFont())
.bold()
.layoutPriority(1)
.foregroundColor(.green)
}
}
}
Section {
Group {
if messageStore.categories.count > 0 {
Section {
ForEach(messageStore.categories) { category in
NavigationLink(destination: CategoryMessagesView(category: category)) {
HStack {
Image(systemName: "tray") // .foregroundColor(.green)
Text("\(category.name)").lineLimit(1).truncationMode(.tail)
Spacer()
Text("\(category.messages.count)")
.font(self.messageCountFont())
.bold()
.layoutPriority(1)
.foregroundColor(.green)
}
}
}
}
} else {
EmptyView()
}
}
}
}
}
// TODO This is pretty inefficient
func messageCountFont() -> Font {
let font = UIFont.preferredFont(forTextStyle: .caption1)
return Font(font.withSize(font.pointSize * 0.75))
}
}
Apologies, I know this is a lot of code, but I feel I need to give enough context and visibility to show what is going on here.
Full project at https://github.com/st3fan/LearningSwiftUI/tree/master/MasterDetail - I don't think more code is relevant, but if it is, let me know and I'll move it into the question here.
The problem is in updated ForEach which result in recreating List and thus breaks link. This looks like SwiftUI defect, so worth submitting feedback to Apple.
The tested workaround is to move All Messages navigation link out of list (looks a bit different but might be appropriate). Tested with Xcode 12 / watchOS 7.0
struct ContentView: View {
#ObservedObject var messageStore = MessageStore.sharedInstance
var body: some View {
VStack {
NavigationLink(destination: AllMessagesView()) {
HStack {
Image(systemName: "tray.2")
Text("All Messages")
Spacer()
Text("\(messageStore.messages.count)")
.font(messageCountFont())
.bold()
.layoutPriority(1)
.foregroundColor(.green)
}
}
List {
ForEach(messageStore.categories) { category in
NavigationLink(destination: CategoryMessagesView(category: category)) {
HStack {
Image(systemName: "tray") // .foregroundColor(.green)
Text("\(category.name)").lineLimit(1).truncationMode(.tail)
Spacer()
Text("\(category.messages.count)")
.font(self.messageCountFont())
.bold()
.layoutPriority(1)
.foregroundColor(.green)
}
}
}
}
}
}
// ... other code
I've been writing my first SwiftUI application, which manages a book collection. It has a List of around 3,000 items, which loads and scrolls pretty efficiently. If use a toggle control to filter the list to show only the books I don't have the UI freezes for twenty to thirty seconds before updating, presumably because the UI thread is busy deciding whether to show each of the 3,000 cells or not.
Is there a good way to do handle updates to big lists like this in SwiftUI?
var body: some View {
NavigationView {
List {
Toggle(isOn: $userData.showWantsOnly) {
Text("Show wants")
}
ForEach(userData.bookList) { book in
if !self.userData.showWantsOnly || !book.own {
NavigationLink(destination: BookDetail(book: book)) {
BookRow(book: book)
}
}
}
}
}.navigationBarTitle(Text("Books"))
}
Have you tried passing a filtered array to the ForEach. Something like this:
ForEach(userData.bookList.filter { return !$0.own }) { book in
NavigationLink(destination: BookDetail(book: book)) { BookRow(book: book) }
}
Update
As it turns out, it is indeed an ugly, ugly bug:
Instead of filtering the array, I just remove the ForEach all together when the switch is flipped, and replace it by a simple Text("Nothing") view. The result is the same, it takes 30 secs to do so!
struct SwiftUIView: View {
#EnvironmentObject var userData: UserData
#State private var show = false
var body: some View {
NavigationView {
List {
Toggle(isOn: $userData.showWantsOnly) {
Text("Show wants")
}
if self.userData.showWantsOnly {
Text("Nothing")
} else {
ForEach(userData.bookList) { book in
NavigationLink(destination: BookDetail(book: book)) {
BookRow(book: book)
}
}
}
}
}.navigationBarTitle(Text("Books"))
}
}
Workaround
I did find a workaround that works fast, but it requires some code refactoring. The "magic" happens by encapsulation. The workaround forces SwiftUI to discard the List completely, instead of removing one row at a time. It does so by using two separate lists in two separate encapsualted views: Filtered and NotFiltered. Below is a full demo with 3000 rows.
import SwiftUI
class UserData: ObservableObject {
#Published var showWantsOnly = false
#Published var bookList: [Book] = []
init() {
for _ in 0..<3001 {
bookList.append(Book())
}
}
}
struct SwiftUIView: View {
#EnvironmentObject var userData: UserData
#State private var show = false
var body: some View {
NavigationView {
VStack {
Toggle(isOn: $userData.showWantsOnly) {
Text("Show wants")
}
if userData.showWantsOnly {
Filtered()
} else {
NotFiltered()
}
}
}.navigationBarTitle(Text("Books"))
}
}
struct Filtered: View {
#EnvironmentObject var userData: UserData
var body: some View {
List(userData.bookList.filter { $0.own }) { book in
NavigationLink(destination: BookDetail(book: book)) {
BookRow(book: book)
}
}
}
}
struct NotFiltered: View {
#EnvironmentObject var userData: UserData
var body: some View {
List(userData.bookList) { book in
NavigationLink(destination: BookDetail(book: book)) {
BookRow(book: book)
}
}
}
}
struct Book: Identifiable {
let id = UUID()
let own = Bool.random()
}
struct BookRow: View {
let book: Book
var body: some View {
Text("\(String(book.own)) \(book.id)")
}
}
struct BookDetail: View {
let book: Book
var body: some View {
Text("Detail for \(book.id)")
}
}
Check this article https://www.hackingwithswift.com/articles/210/how-to-fix-slow-list-updates-in-swiftui
In short the solution proposed in this article is to add .id(UUID()) to the list:
List(items, id: \.self) {
Text("Item \($0)")
}
.id(UUID())
"Now, there is a downside to using id() like this: you won't get your update animated. Remember, we're effectively telling SwiftUI the old list has gone away and there's a new list now, which means it won't try to move rows around in an animated way."
I think we have to wait until SwiftUI List performance improves in subsequent beta releases. I’ve experienced the same lag when lists are filtered from a very large array (500+) down to very small ones. I created a simple test app to time the layout for a simple array with integer IDs and strings with Buttons to simply change which array is being rendered - same lag.
Instead of a complicated workaround, just empty the List array and then set the new filters array. It may be necessary to introduce a delay so that emptying the listArray won't be omitted by the followed write.
List(listArray){item in
...
}
self.listArray = []
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
self.listArray = newList
}
Looking for how to adapt Seitenwerk's response to my solution, I found a Binding extension that helped me a lot. Here is the code:
struct ContactsView: View {
#State var stext : String = ""
#State var users : [MockUser] = []
#State var filtered : [MockUser] = []
var body: some View {
Form{
SearchBar(text: $stext.didSet(execute: { (response) in
if response != "" {
self.filtered = []
self.filtered = self.users.filter{$0.name.lowercased().hasPrefix(response.lowercased()) || response == ""}
}
else {
self.filtered = self.users
}
}), placeholder: "Buscar Contactos")
List{
ForEach(filtered, id: \.id){ user in
NavigationLink(destination: LazyView( DetailView(user: user) )) {
ContactCell(user: user)
}
}
}
}
.onAppear {
self.users = LoadUserData()
self.filtered = self.users
}
}
}
This is the Binding extension:
extension Binding {
/// Execute block when value is changed.
///
/// Example:
///
/// Slider(value: $amount.didSet { print($0) }, in: 0...10)
func didSet(execute: #escaping (Value) ->Void) -> Binding {
return Binding(
get: {
return self.wrappedValue
},
set: {
execute($0)
self.wrappedValue = $0
}
)
}
}
The LazyView is optional, but I took the trouble to show it, as it helps a lot in the performance of the list, and prevents swiftUI from creating the NavigationLink target content of the whole list.
struct LazyView<Content: View>: View {
let build: () -> Content
init(_ build: #autoclosure #escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
This code will work correctly provided that you initialize your class in the 'SceneDelegate' file as follows:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var userData = UserData()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView:
contentView
.environmentObject(userData)
)
self.window = window
window.makeKeyAndVisible()
}
}