I'm experiencing strange behavior with an #State property that isn't being properly updated in its originating view after being changed in another view. I'm using Xcode 12.3 and iOS 14.
What happens is that an #State "session" value-based item and #State "flow" value-based item are sent as bound parameters to another view. When a button is tapped there, it changes their values, and a fullScreenCover call in the originating view is supposed to get the correct view to display next in the flow from a switch statement. But the "session" item is nil in that switch statement unless I include an onChange modifier that looks for changes in either of the two #State properties. The onChange call doesn't have to have any code in it to have this effect.
I'm still relatively new to SwiftUI (although fairly experienced with iOS and Mac development). But this is confusing the heck out of me. I don't understand why it isn't working as expected, nor why adding an empty onChange handler makes it work.
If you'd like to experience this for yourself, here's code to assemble a simple demo project:
// the model types
struct ObservationSession: Codable {
public let id: UUID
public var name: String
public init(name: String) {
self.name = name
self.id = UUID()
}
}
struct SessionListModals {
enum Flow: Identifiable {
case configuration
case observation
case newSession
var id: Flow { self }
}
}
// ContentView
struct ContentView: View {
#State private var mutableSession: ObservationSession?
#State private var flow: SessionListModals.Flow?
var body: some View {
VStack {
Button("New Session", action: {
mutableSession = ObservationSession(name: "")
flow = .newSession
})
.padding()
}
.fullScreenCover(item: $flow) {
viewForFlow($0)
}
// Uncomment either of these 2 onChange blocks to see successful execution of this flow
// Why does that make a difference?
// .onChange(of: mutableSession?.name, perform: { value in
// //
// })
// .onChange(of: flow, perform: { value in
// //
// })
}
#ViewBuilder private func viewForFlow(_ flow: SessionListModals.Flow) -> some View {
switch flow {
case .newSession:
// MARK: - Show New Session View
NavigationView {
NewSessionView(session: $mutableSession, flow: $flow)
.navigationTitle("Create a session")
.navigationBarItems(leading: Button("Cancel", action: {
self.flow = nil
}))
}
case .observation:
// MARK: - Show RecordingView
NavigationView {
let name = mutableSession?.name ?? "Unnamed session"
RecordingView(sessionName: name)
.navigationBarItems(leading: Button("Close", action: {
self.flow = nil
}))
}
default:
NavigationView {
EmptyView()
.navigationBarItems(leading: Button("Close", action: {
self.flow = nil
}))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
// NewSessionView
struct NewSessionView: View {
#Binding var session: ObservationSession?
#Binding var flow: SessionListModals.Flow?
var body: some View {
VStack {
Text("Tap button to create a new session")
Button("New Session", action: {
createNewSession()
})
.padding()
}
}
private func createNewSession() {
let newSession = ObservationSession(name: "Successfully Created A New Session")
session = newSession
flow = .observation
}
}
struct NewSessionView_Previews: PreviewProvider {
static let newSession = ObservationSession(name: "Preview")
static let flow: SessionListModals.Flow = .newSession
static var previews: some View {
NewSessionView(session: .constant(newSession), flow: .constant(flow))
}
}
// RecordingView
struct RecordingView: View {
var sessionName: String
var body: some View {
Text(sessionName)
}
}
struct RecordingView_Previews: PreviewProvider {
static var previews: some View {
RecordingView(sessionName: "Preview")
}
}
class ObservationSession: //Codable, //implement Codable manually
ObservableObject {
public let id: UUID
//This allows you to observe the individual variable
#Published public var name: String
public init(name: String) {
self.name = name
self.id = UUID()
}
}
struct SessionListModals {
enum Flow: Identifiable {
case configuration
case observation
case newSession
var id: Flow { self }
}
}
// ContentView
class ContentViewModel: ObservableObject {
#Published var mutableSession: ObservationSession?
}
struct ContentView: View {
//State stores the entire object and observes it as a whole it does not individually observe its variables that is why .onChange works
#StateObject var vm: ContentView3Model = ContentView3Model()
#State private var flow: SessionListModals.Flow?
var body: some View {
VStack {
Button("New Session", action: {
//Since you want to change it programatically you have to put them in another object
vm.mutableSession = ObservationSession(name: "")
flow = .newSession
})
.padding()
}
.fullScreenCover(item: $flow) {
viewForFlow($0)
}
}
#ViewBuilder private func viewForFlow(_ flow: SessionListModals.Flow) -> some View {
switch flow {
case .newSession:
// MARK: - Show New Session View
NavigationView {
NewSessionView(session: $vm.mutableSession, flow: $flow)
.navigationTitle("Create a session")
.navigationBarItems(leading: Button("Cancel", action: {
self.flow = nil
}))
}
case .observation:
// MARK: - Show RecordingView
NavigationView {
let name = vm.mutableSession?.name ?? "Unnamed session"
RecordingView(sessionName: name)
.navigationBarItems(leading: Button("Close", action: {
self.flow = nil
}))
}
default:
NavigationView {
EmptyView()
.navigationBarItems(leading: Button("Close", action: {
self.flow = nil
}))
}
}
}
}
Related
I am planning to implement following features in the SwiftUI list - delete, insert, move and select.
With the existing list I am able to delete a row. But can't select a row does not work with List(selection: self.$selectedObject). When I hit edit it always enters into delete mode. And I comment the delete code nothing happens when I tap on edit button. This the first problem.
Also, selectedObject can it be moved to Model instead of keeping it with the ContentView?
Like UITableView, I am not able to get the insert green button. Is it like SwiftUI does not support the green insert button?
Overall trying to understand how the insert, delete, move and select functionality can work with the List SwiftUI.
Another problem I have noticed is that animation is very fast and not smooth when it enters into edit mode (with delete actions).
struct ContentView: View {
#StateObject private var model = Model()
#State var selectedObject: Locations?
var body: some View {
NavigationView {
List(selection: self.$selectedObject) {
ForEach(model.identifiableLocations) { location in
Text(location.name)
}
.onDelete(perform: delete(of:))
}.listStyle(.plain)
.navigationTitle("Places")
.toolbar {
EditButton()
Button {
model.addLocation(name: "Test")
} label: {
Image(systemName: "plus")
}
}
}
}
func delete(of indexSet: IndexSet){
indexSet.forEach { index in
model.delete(itemAt: index)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().previewDevice(PreviewDevice(rawValue: "iPhone 14"))
}
}
extension ContentView {
#MainActor class Model: ObservableObject {
#Published private(set) var identifiableLocations = [Locations(name: "USA"),
Locations(name: "Switzerland")]
}
}
extension ContentView.Model {
func addLocation(name: String) {
identifiableLocations.append(Locations(name: name))
}
func delete(itemAt index: Int) {
identifiableLocations.remove(at: index)
}
}
struct Locations {
var name: String
}
extension Locations: Identifiable,Hashable {
var id: String {
return UUID().uuidString
}
}
to make selection work, the list cells need a .tag(). This value is going into the selection var.
yes, selectedObject can be moced to the view model as an additional #Published var
SwiftUI List does not have an insert method, but your Add Button already does that.
The animation is broke because your id in Location is not stable, but generated on each call by the computed var. id should be stable!
Here a running code with comments:
#MainActor
class ViewModel: ObservableObject {
#Published private(set) var identifiableLocations = [
Locations(name: "USA"),
Locations(name: "Switzerland")
]
// published selection var
#Published var selectedObject: Locations?
func addLocation(name: String) {
identifiableLocations.append(Locations(name: name))
}
func delete(itemAt index: Int) {
identifiableLocations.remove(at: index)
}
// new move func
func move(fromOffsets: IndexSet, toOffset: Int) -> Void {
identifiableLocations.move(fromOffsets: fromOffsets, toOffset: toOffset)
}
}
struct Locations: Identifiable, Hashable {
let id = UUID() // id has to stay stable
// var id: String {
// return UUID().uuidString
// }
var name: String
}
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
// #State var selectedObject: Locations? // is now in viewmodel
var body: some View {
NavigationView {
List(selection: $viewModel.selectedObject) {
ForEach(viewModel.identifiableLocations) { location in
Text(location.name)
.tag(location) // this makes selction work
}
.onDelete(perform: delete(of:))
.onMove(perform: viewModel.move)
}
.listStyle(.plain)
.navigationTitle("Places")
.toolbar {
EditButton()
Button {
viewModel.addLocation(name: "Test")
} label: {
Image(systemName: "plus")
}
}
}
}
func delete(of indexSet: IndexSet){
indexSet.forEach { index in
self.viewModel.delete(itemAt: index)
}
}
}
I modified an app to use the new NavigationSplitView and NavigationStack, but I can't figure out how to have the NavigationPath retain the state when it's not the active view.
Below is some sample code. I run it on an iPad in landscape mode, or on a Mac (Designed for iPad). It starts with View1 selected and displays View1 in the details. I then tap on SubView and it pushes to SubView. If I then tap on View2, View2 is displayed in the details. If I tap on View1 again, View1 is back at it's root, and is no longer pushed to the SubView. How can I fix this so that when I go back to View1 it is still pushed to SubView?
import SwiftUI
struct ViewType: Identifiable, Hashable {
let id: String
}
private var viewTypes = [
ViewType(id: "View1"),
ViewType(id: "View2"),
]
struct ContentView: View {
#StateObject private var navigationModel = NavigationModel()
#State var selection: Set<String> = [viewTypes[0].id]
var body: some View {
NavigationSplitView {
List(viewTypes, selection: $selection) { viewType in
Text("\(viewType.id)")
}
} detail: {
switch selection.first ?? "Unknown" {
case "View1":
View1()
case "View2":
View2()
default:
Text("Unknown")
}
}
.navigationTitle(selection.first ?? "Unknown")
.environmentObject(navigationModel)
}
}
struct View1: View {
#EnvironmentObject var navigationModel: NavigationModel
var body: some View {
NavigationStack(path: $navigationModel.path) {
Text("View1")
NavigationLink("SubView", value: "SubView")
.navigationDestination(for: String.self) { name in
Text(name)
.onAppear {
print((navigationModel.path.count))
}
}
}
}
}
struct View2: View {
var body: some View {
NavigationStack() {
Text("View2")
}
}
}
final class NavigationModel: ObservableObject {
#Published var path = NavigationPath() {
didSet {
print("path.count: \(path.count)")
}
}
}
I have an array of custom buttons capsuleButtons in ViewModel.swift file which are displayed using ForEach in ContentView. Each button is connected to an external property named isActive to display its current state, buttons created using the ForEach are not updating where as if created without ForEach (with title ContentView btn), is updating as expected. What am I missing here?
ContentView
struct ContentView: View {
#StateObject var vm = ViewModel()
var body: some View {
VStack {
ForEach(vm.capsuleButtons, id: \.self) { button in
HStack {
button
}
}
CapsuleButton(title: "ContentView btn", action: { vm.isActive.toggle() } , active: vm.isActive)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct CapsuleButton: View, Hashable {
static func == (lhs: CapsuleButton, rhs: CapsuleButton) -> Bool {
lhs.title == rhs.title && lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(title)
hasher.combine(id)
}
var id = UUID()
var title: String
let action: () -> Void
var active = false
var body: some View {
Button(title, action: action)
.background( active ? Color.pink : Color.blue)
.foregroundColor(active ? Color.white : Color.black)
}
}
ViewModel.swift
import Foundation
class ViewModel: ObservableObject {
#Published var isActive = false
var capsuleButtons = [CapsuleButton]()
init() {
self.isActive = false
self.capsuleButtons = [CapsuleButton(title: "Button 1", action: {self.isActive.toggle()}, active: isActive),
CapsuleButton(title: "Button 2", action: {self.isActive.toggle()}, active: isActive)]
}
}
You're doing a few things that are or are close to breaking the SwiftUI diffing system.
First, you're telling the system an untruth by trying to make your View conform to Hashable and Equatable and then saying that the title and id are the only properties that matter, where you definitely also want active to be part of it.
Secondly, in general, you shouldn't be storing references to Views -- especially with IDs that get created on init -- that's a SwiftUI anti pattern. Instead, render a View hierarchy based on your model.
Finally, in your actions, you are always setting self.isActive -- I assume that you actually wanted to set the Button's active state, but you were always setting it on the ViewModel's state.
Something like this (not the only solution) would work instead:
struct ContentView: View {
#StateObject var vm = ViewModel()
var body: some View {
VStack {
ForEach(vm.capsuleButtons) { button in
HStack {
CapsuleButton(model: button) {
vm.action(forID: button.id)
}
}
}
}
}
}
struct CapsuleButtonModel: Identifiable, Hashable {
var id = UUID()
var title: String
var active: Bool
}
struct CapsuleButton: View {
var model: CapsuleButtonModel
let action: () -> Void
var body: some View {
Button(model.title, action: action)
.background(model.active ? Color.pink : Color.blue)
.foregroundColor(model.active ? Color.white : Color.black)
}
}
class ViewModel: ObservableObject {
#Published var isActive = false
#Published var capsuleButtons = [CapsuleButtonModel]()
init() {
self.isActive = false
self.capsuleButtons = [CapsuleButtonModel(title: "Button 1", active: isActive),
CapsuleButtonModel(title: "Button 2", active: isActive)]
}
func action(forID: UUID) {
self.capsuleButtons = self.capsuleButtons.map { btn in
if btn.id == forID {
var copy = btn
copy.active.toggle()
return copy
} else {
return btn
}
}
}
}
You could also get rid of the action(for) function and replace it with a Binding (in particular, read about SwiftUI element binding syntax: https://www.swiftbysundell.com/articles/bindable-swiftui-list-elements/). Then it turns into something even more simple:
struct ContentView: View {
#StateObject var vm = ViewModel()
var body: some View {
VStack {
ForEach($vm.capsuleButtons) { $button in
HStack {
CapsuleButton(model: $button)
}
}
}
}
}
struct CapsuleButtonModel: Identifiable, Hashable {
var id = UUID()
var title: String
var active: Bool
}
struct CapsuleButton: View {
#Binding var model: CapsuleButtonModel
var body: some View {
Button(model.title, action: {
model.active.toggle()
})
.background(model.active ? Color.pink : Color.blue)
.foregroundColor(model.active ? Color.white : Color.black)
}
}
class ViewModel: ObservableObject {
#Published var isActive = false
#Published var capsuleButtons = [CapsuleButtonModel]()
init() {
self.isActive = false
self.capsuleButtons = [CapsuleButtonModel(title: "Button 1", active: isActive),
CapsuleButtonModel(title: "Button 2", active: isActive)]
}
}
I am facing an issue while popping to a specific view. Let me explain the hierarchy.
ContentView -> 2 tabs, TabAView and TabBView
Inside TabBView. There is 1 view used ConnectView: Where is a Button to connect. After tapping on the button of Connect, the user move to another View which is called as UserAppView. From Here User can check his profile and update also. After the Update API call, need to pop to UserAppView from UserFirstFormView.
Here is the code to understand better my problem.
ContentView.swift
struct ContentView: View {
enum AppPage: Int {
case TabA=0, TabB=1
}
#StateObject var settings = Settings()
#ObservedObject var userViewModel: UserViewModel
var body: some View {
NavigationView {
TabView(selection: $settings.tabItem) {
TabAView(userViewModel: userViewModel)
.tabItem {
Text("TabA")
}
.tag(AppPage.TabA)
TabBView(userViewModel: userViewModel)
.tabItem {
Text("Apps")
}
.tag(AppPage.TabB)
}
.accentColor(.white)
.edgesIgnoringSafeArea(.top)
.onAppear(perform: {
settings.tabItem = .TabA
})
.navigationBarTitleDisplayMode(.inline)
}
.environmentObject(settings)
}
}
This is TabAView:
struct TabAView: View {
#ObservedObject var userViewModel: UserViewModel
#EnvironmentObject var settings: Settings
init(userViewModel: UserViewModel) {
self.userViewModel = userViewModel
}
var body: some View {
Vstack {
/// code
}
.onAppear(perform: {
/// code
})
.environmentObject(settings)
}
}
This is another TabBView:
struct TabBView: View {
#ObservedObject var userViewModel: UserViewModel
init(userViewModel: UserViewModel) {
self.userViewModel = userViewModel
}
var body: some View {
VStack (spacing: 10) {
NavigationLink(destination: ConnectView(viewModel: ConnectViewModel(id: id!), userViewModel: userViewModel)) {
UserCardWidget()
}
}
}
}
There is 1 connectView used on the TabBView through which the user will connect. ConnectViewModel is used here to call connect API.
class ConnectViewModel: ObservableObject {
var id: String?
init(id: String) {
self.id = id
}
func connect(completion: #escaping () -> Void) {
APIService.shared.connectApp(id: self.id!) { connected in
DispatchQueue.main.async {
self.isConnected = connected ?? false
completion()
}
}
}
}
This is connect view
struct ConnectView: View {
#ObservedObject var connectViewModel: ConnectViewModel
#ObservedObject var userViewModel: UserViewModel
#State var buttonTitle = "CONNECT WITH THIS"
#State var isShowingDetailView = false
var body: some View {
VStack {
Spacer()
if let id = connectViewModel.id {
NavigationLink(destination: UserAppView(id: id, userViewModel: userViewModel), isActive: $isShowingDetailView) {
Button(buttonTitle, action: {
connectViewModel.connect {
buttonTitle = "CONNECTED"
isShowingDetailView = true
}
})
}
}
}
}
}
This is the UserAppViewModel where API is hit to fetch some user-related details:
class UserAppViewModel: ObservableObject {
var id = ""
func getdetails() {
APIService.shared.getDetails() { userDetails in
DispatchQueue.main.async {
/// code
}
}
}
}
This is UserAppView class
struct UserAppView: View {
#ObservedObject var userViewModel: UserViewModel
#State private var signUpInButtonClicked: Bool = false
#StateObject private var userAppViewModel = UserAppViewModel()
init(id: String, userViewModel: UserViewModel) {
self.id = id
self.userViewModel = userViewModel
}
var body: some View {
VStack {
Text(userAppViewModel.status)
VStack {
Spacer()
NavigationLink(
destination: ProfileView(userAppViewModel: userAppViewModel, isActive: $signUpInButtonClicked)) { EmptyView() }
if /// Condition {
Button(action: {
signUpInButtonClicked = true
}, label: {
ZStack {
/// code
}
.frame(maxWidth: 77, maxHeight: 25)
})
}
}.onAppear(perform: {
**userAppViewModel.getDetails**(id: id)
})
}
}
From Here, the User Can Navigate to ProfileView.
struct ProfileUpdateView: View {
#State private var navigationSelectionFirstFormView = false
#State private var navigationSelectionLastFormView = false
public var body: some View {
VStack {
NavigationLink(destination: UserFirstFormView(userAppViewModel: userAppViewModel), isActive: $navigationSelectionFirstFormView) {
EmptyView()
}
NavigationLink(destination: UserLastFormView(userAppViewModel: userAppViewModel), isActive: $navigationSelectionLastFormView) {
EmptyView()
}
}
.navigationBarItems(trailing: Button(action: {
if Condition {
navigationSelectionFirstFormView = true
} else {
navigationSelectionLastFormView = true
}
}, label: {
HStack {
Text("Action")
.foregroundColor(Color.blue)
}
})
)
}
}
Further, their user will move to the next screen to update the profile.
struct UserFirstFormView: View {
var body: some View {
VStack {
/// code
///
Button("buttonTitle", action: {
API Call completion: { status in
if status {
self.rootPresentationMode.wrappedValue.dismiss()
}
})
})
.frame(maxHeight: 45)
}
}
}
I am trying to pop from this view once the API response is received but nothing is working.
I have removed most of the code from a confidential point of view but this code will explain the reason and error. Please look into the code and help me.
You could use the navigation link with, tag: and selection: overload and let the viewmodel control what link is open, here a example
enum profileViews {
case view1
case view2}
in your viewModel add an published var that will hold the active view
#Published var activeView: profileViews?
then in your navigation link you can do it like this
NavigationLink(
destination: secondView(profileViewModel: ProfileViewModel ),
tag: profileViews.view1,
selection: self.$profileViewModel.activeView
){}
Then you could pop any view just updating the variable inside the view model
self.profileViewModel.activeView = nil
I have the following model object that I use to populate a List with a Toggle for each row, which is bound to measurement.isSelected
final class Model: ObservableObject {
struct Measurement: Identifiable {
var id = UUID()
let name: String
var isSelected: Binding<Bool>
var selected: Bool = false
init(name: String) {
self.name = name
let selected = CurrentValueSubject<Bool, Never>(false)
self.isSelected = Binding<Bool>(get: { selected.value }, set: { selected.value = $0 })
}
}
#Published var measurements: [Measurement]
#Published var hasSelection: Bool = false // How to set this?
init(measurements: [Measurement]) {
self.measurements = measurements
}
}
I'd like the hasSelection property to be true whenever any measurement.isSelected is true. I'm guessing somehow Model needs to observe changes in measurements and then update its hasSelection property… but I've no idea where to start!
The idea is that hasSelection will be bound to a Button to enable or disable it.
Model is used as follows…
struct MeasurementsView: View {
#ObservedObject var model: Model
var body: some View {
NavigationView {
List(model.measurements) { measurement in
MeasurementView(measurement: measurement)
}
.navigationBarTitle("Select Measurements")
.navigationBarItems(trailing: NavigationLink(destination: NextView(), isActive: $model.hasSelection, label: {
Text("Next")
}))
}
}
}
struct MeasurementView: View {
let measurement: Model.Measurement
var body: some View {
HStack {
Text(measurement.name)
.font(.subheadline)
Spacer()
Toggle(measurement.name, isOn: measurement.isSelected)
.labelsHidden()
}
}
}
For info, here's a screenshot of what I'm trying to achieve. A list of selectable items, with a navigation link that is enabled when one or more is selected, and disabled when no items are selected.
#user3441734 hasSelection should ideally be a get only property, that
is true if any of measurement.isSelected is true
struct Data {
var bool: Bool
}
class Model: ObservableObject {
#Published var arr: [Data] = []
var anyTrue: Bool {
arr.map{$0.bool}.contains(true)
}
}
example (as before) copy - paste - run
import SwiftUI
struct Data: Identifiable {
let id = UUID()
var name: String
var on_off: Bool
}
class Model: ObservableObject {
#Published var data = [Data(name: "alfa", on_off: false), Data(name: "beta", on_off: false), Data(name: "gama", on_off: false)]
var bool: Bool {
data.map {$0.on_off} .contains(true)
}
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
List(0 ..< model.data.count) { idx in
HStack {
Text(verbatim: self.model.data[idx].name)
Toggle(isOn: self.$model.data[idx].on_off) {
EmptyView()
}
}
}
Text("\(model.bool.description)").font(.largeTitle).padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
When the model.data is updated
#Published var data ....
its publisher calls objectWillChange on ObservableObject.
Next SwiftUI recognize that ObservedObject needs the View to be "updated". The View is recreated, and that will force the model.bool.description will have fresh value.
LAST UPDATE
change this part of code
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
NavigationView {
List(0 ..< model.data.count) { idx in
HStack {
Text(verbatim: self.model.data[idx].name)
Toggle(isOn: self.$model.data[idx].on_off) {
EmptyView()
}
}
}.navigationBarTitle("List")
.navigationBarItems(trailing:
NavigationLink(destination: Text("next"), label: {
Text("Next")
}).disabled(!model.bool)
)
}
}
}
and it is EXACTLY, WHAT YOU HAVE in your updated question
Try it on real device, otherwise the NavigationLink is usable only once (this is well known simulator bug in current Xcode 11.3.1 (11C504)).
The problem with your code at the moment is that even if you observe the changes to measurements, they will not get updated when the selection updates, because you declared the var isSelected: Binding<Bool> as a Binding. This means that SwiftUI is storing it outside of your struct, and the struct itself doesn't update (stays immutable).
What you could try instead is declaring #Published var selectedMeasurementId: UUID? = nil on your model So your code would be something like this:
import SwiftUI
import Combine
struct NextView: View {
var body: some View {
Text("Next View")
}
}
struct MeasurementsView: View {
#ObservedObject var model: Model
var body: some View {
let hasSelection = Binding<Bool> (
get: {
self.model.selectedMeasurementId != nil
},
set: { value in
self.model.selectedMeasurementId = nil
}
)
return NavigationView {
List(model.measurements) { measurement in
MeasurementView(measurement: measurement, selectedMeasurementId: self.$model.selectedMeasurementId)
}
.navigationBarTitle("Select Measurements")
.navigationBarItems(trailing: NavigationLink(destination: NextView(), isActive: hasSelection, label: {
Text("Next")
}))
}
}
}
struct MeasurementView: View {
let measurement: Model.Measurement
#Binding var selectedMeasurementId: UUID?
var body: some View {
let isSelected = Binding<Bool>(
get: {
self.selectedMeasurementId == self.measurement.id
},
set: { value in
if value {
self.selectedMeasurementId = self.measurement.id
} else {
self.selectedMeasurementId = nil
}
}
)
return HStack {
Text(measurement.name)
.font(.subheadline)
Spacer()
Toggle(measurement.name, isOn: isSelected)
.labelsHidden()
}
}
}
final class Model: ObservableObject {
#Published var selectedMeasurementId: UUID? = nil
struct Measurement: Identifiable {
var id = UUID()
let name: String
init(name: String) {
self.name = name
}
}
#Published var measurements: [Measurement]
init(measurements: [Measurement]) {
self.measurements = measurements
}
}
I'm not sure exactly how you want the navigation button in the navbar to behave. For now I just set the selection to nil when it's tapped. You can modify it depending on what you want to do.
If you want to support multi-selection, you can use a Set of selected ids instead.
Also, seems like the iOS simulator has some problems with navigation, but I tested on a physical device and it worked.