SwiftUI Previews w/Mock Realm - swiftui

I am running into a problem trying to use an in memory realm configuration with previews. When the preview executes it returns an error. The diagnostics gives the following error:
MessageError: Connection interrupted
I have the following class defined to mock the realm data.
class MockRealms {
static var previewRealm: Realm {
get {
var realm: Realm
let identifier = "previewRealm"
let config = Realm.Configuration(inMemoryIdentifier: identifier)
do {
realm = try Realm(configuration: config)
try realm.write {
for category in Categories.allCases {
_ = Category(name: category.rawValue)
}
}
try realm.write {
_ = Settings(type: .fixed, int: 3.375, dp: 25.0, term: 30)
}
return realm
} catch let error {
fatalError("Error: \(error.localizedDescription)")
}
}
}
}
In the view we are using the #ObservedResults to get the categories from the Realm.
struct PropertyListView: View {
#ObservedResults(Category.self) var categories
#Binding var showInfo: Bool
#Binding var showInfoButton: Bool
#Binding var showGuide: Bool
#Binding var showGuideButtton: Bool
var body: some View {
NavigationView {
ScrollView {
VStack {
ForEach(categories) { category in
PropertyCategoryView(category: category)
}
}
}
}
.navigationBarTitle("Property List")
.navigationBarBackButtonHidden(false)
}
.navigationViewStyle(.stack)
}
The preview section is as follows:
struct PropertyListView_Previews: PreviewProvider {
static var previews: some View {
Group {
PropertyListView(showInfo: .constant(false), showInfoButton:
.constant(true), showGuide: .constant(false), showGuideButton:
.constant(false))
.environment(\.realm, MockRealms.previewRealm)
PropertyListView(showInfo: .constant(false), showInfoButton:
.constant(true), showGuide: .constant(false), showGuideButton:
.constant(false))
.environment(\.realm, MockRealms.previewRealm)
.preferredColorScheme(.dark)
}
}
}
Any help would be most appreciated.

On the main content view I call the PropertyListView with an environment setting realmConfiguration and pass that into the PropertyListView. So I changed the PreviewProvider to pass simply the in-memory Realm configuration and then everything seems to work just fine.
struct PropertyListView_Previews: PreviewProvider {
static var previews: some View {
Group {
PropertyListView(showInfo: .constant(false), showInfoButton:
.constant(true), showGuide: .constant(false), showGuideButton:
.constant(false))
.environment(\.realmConfiguration, Realm.Configuration(inMemoryIdentifier: "previewRealm", schemaVersion: 1))
PropertyListView(showInfo: .constant(false), showInfoButton:
.constant(true), showGuide: .constant(false), showGuideButton:
.constant(false))
.environment(\.realmConfiguration, Realm.Configuration(inMemoryIdentifier: "previewRealm", schemaVersion: 1))
.preferredColorScheme(.dark)
}
}
}
With Realm changing almost every other day it is like walking on quicksand. It is a wonderful DB for mobile development because they are constantly making it better, but boy you really have to stay on top of the changes.

Related

SwiftUI publishing an environment change from within view update

The app has a model that stores the user's current preference for light/dark mode, which the user can change by clicking on a button:
class DataModel: ObservableObject {
#Published var mode: ColorScheme = .light
The ContentView's body tracks the model, and adjusts the colorScheme when the model changes:
struct ContentView: View {
#StateObject private var dataModel = DataModel()
var body: some View {
NavigationStack(path: $path) { ...
}
.environmentObject(dataModel)
.environment(\.colorScheme, dataModel.mode)
As of Xcode Version 14.0 beta 5, this is producing a purple warning: Publishing changes from within view updates is not allowed, this will cause undefined behavior. Is there another way to do this? Or is it a hiccup in the beta release? Thanks!
Update: 2022-09-28
Xcode 14.1 Beta 3 (finally) fixed the "Publishing changes from within view updates is not allowed, this will cause undefined behavior"
See: https://www.donnywals.com/xcode-14-publishing-changes-from-within-view-updates-is-not-allowed-this-will-cause-undefined-behavior/
Full disclosure - I'm not entirely sure why this is happening but these have been the two solutions I have found that seem to work.
Example Code
// -- main view
#main
struct MyApp: App {
#StateObject private var vm = ViewModel()
var body: some Scene {
WindowGroup {
ViewOne()
.environmentObject(vm)
}
}
}
// -- initial view
struct ViewOne: View {
#EnvironmentObject private var vm: ViewModel
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $vm.isPresented) {
SheetView()
}
}
}
// -- sheet view
struct SheetView: View {
#EnvironmentObject private var vm: ViewModel
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Close sheet")
}
}
}
// -- view model
class ViewModel: ObservableObject {
#Published var isPresented: Bool = false
}
Solution 1
Note: from my testing and the example below I still get the error to appear. But if I have a more complex/nested app then the error disappears..
Adding a .buttonStyle() to the button that does the initial toggling.
So within the ContentView on the Button() {} add in a .buttonStyle(.plain) and it will remove the purple error:
struct ViewOne: View {
#EnvironmentObject private var vm: ViewModel
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.buttonStyle(.plain) // <-- here
.sheet(isPresented: $vm.isPresented) {
SheetView()
}
}
}
^ This is probably more of a hack than solution since it'll output a new view from the modifier and that is probably what is causing it to not output the error on larger views.
Solution 2
This one is credit to Alex Nagy (aka. Rebeloper)
As Alex explains:
.. with SwiftUI 3 and SwiftUI 4 the data handling kind of changed. How SwiftUI handles, more specifically the #Published variable ..
So the solution is to have the boolean trigger to be a #State variable within the view and not as a #Published one inside the ViewModel. But as Alex points out it can make your views messy and if you have a lot of states in it, or not be able to deep link, etc.
However, since this is the way that SwiftUI 4 wants these to operate, we run the code as such:
// -- main view
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ViewOne()
}
}
}
// -- initial view
struct ViewOne: View {
#State private var isPresented = false
var body: some View {
Button {
isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $isPresented) {
SheetView(isPresented: $isPresented)
// SheetView() <-- if using dismiss() in >= iOS 15
}
}
}
// -- sheet view
struct SheetView: View {
// I'm showing a #Binding here for < iOS 15
// but you can use the dismiss() option if you
// target higher
// #Environment(\.dismiss) private var dismiss
#Binding var isPresented: Bool
var body: some View {
Button {
isPresented.toggle()
// dismiss()
} label: {
Text("Close sheet")
}
}
}
Using the #Published and the #State
Continuing from the video, if you need to still use the #Published variable as it might tie into other areas of your app you can do so with a .onChange and a .onReceive to link the two variables:
struct ViewOne: View {
#EnvironmentObject private var vm: ViewModel
#State private var isPresented = false
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $isPresented) {
SheetView(isPresented: $isPresented)
}
.onReceive(vm.$isPresented) { newValue in
isPresented = newValue
}
.onChange(of: isPresented) { newValue in
vm.isPresented = newValue
}
}
}
However, this can become really messy in your code if you have to trigger it for every sheet or fullScreenCover.
Creating a ViewModifier
So to make it easier for you to implement it you can create a ViewModifier which Alex has shown works too:
extension View {
func sync(_ published: Binding<Bool>, with binding: Binding<Bool>) -> some View {
self
.onChange(of: published.wrappedValue) { newValue in
binding.wrappedValue = newValue
}
.onChange(of: binding.wrappedValue) { newValue in
published.wrappedValue = newValue
}
}
}
And in use on the View:
struct ViewOne: View {
#EnvironmentObject private var vm: ViewModel
#State private var isPresented = false
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $isPresented) {
SheetView(isPresented: $isPresented)
}
.sync($vm.isPresented, with: $isPresented)
// .onReceive(vm.$isPresented) { newValue in
// isPresented = newValue
// }
// .onChange(of: isPresented) { newValue in
// vm.isPresented = newValue
// }
}
}
^ Anything denoted with this is my assumptions and not real technical understanding - I am not a technical knowledgeable :/
Try running the code that's throwing the purple error asynchronously, for example, by using DispatchQueue.main.async or Task.
DispatchQueue.main.async {
// environment changing code comes here
}
Task {
// environment changing code comes here
}
Improved Solution of Rebel Developer
as a generic function.
Rebeloper solution
It helped me a lot.
1- Create extension for it:
extension View{
func sync<T:Equatable>(_ published:Binding<T>, with binding:Binding<T>)-> some View{
self
.onChange(of: published.wrappedValue) { published in
binding.wrappedValue = published
}
.onChange(of: binding.wrappedValue) { binding in
published.wrappedValue = binding
}
}
}
2- sync() ViewModel #Published var to local #State var
struct ContentView: View {
#EnvironmentObject var viewModel:ViewModel
#State var fullScreenType:FullScreenType?
var body: some View {
//..
}
.sync($viewModel.fullScreenType, with: $fullScreenType)

SwiftUi - Changing Views Via a function

I'm struggling in a big way to get some basic code working that allows me to change a view in a Swiftui project.
I have 3 views: my default ContentView, a login screen and a main menu.
The project loads to ContentView which is just a logo. I have a boolean value which defaults to false and an extension function which either loads the login screen, or main menu dependent on the value of that boolean.
This part is working fine, project loads and i see the login page. The login button calls a function which does a URLsession, and depending on the returned value from that, sets the boolean flag to true or leaves it as false in the case of a failed login.
The bit im struggling with is getting the function to change the view. I can toggle the boolean flag in the function fine, but if I include a statement such as MainMenu() to load my main menu view, nothing happens.
I have experimented with observable objects and "subscribers" to try to get this working but i'm not sure if this is actually needed and I had no joy getting it working.
any help is greatly appreciated
Full code:
import SwiftUI
var isLoggedin = false
var authenticationFailure = false
func DoLogin(username: inout String, password: inout String){
print(isLoggedin)
let url = URL(string: "https://www.example.com/mobile/ios/test.php")!
var request = URLRequest(url: url)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
let parameters: [String: Any] = [
"username": username,
"password": password]
request.httpBody = parameters.percentEncoded()
let task = URLSession.shared.dataTask(with: request) { data,response,error in
guard let data = data,
let response = response as? HTTPURLResponse,
error == nil else{
print("error", error ?? "Unknown error")
return
}
guard (200 ... 299) ~= response.statusCode else {
print("statuscode should be 2xx, got \(response.statusCode)")
print("response = \(response)")
return
}
let responseString = String(data: data, encoding: .utf8)
if responseString == "1"{
print("Logged in")
isLoggedin = true
print(isLoggedin)
MainMenu()
}
else{
print("NO LOGIN")
isLoggedin = false
}
}
task.resume()
}
extension View {
#ViewBuilder func changeView(_ isLoggedin: Bool) -> some View {
switch isLoggedin {
case false: LoginView()
case true: MainMenu()
}
}
}
struct ContentView: View {
#State var isLoggedin = false
var body: some View {
Color.clear
.changeView(isLoggedin)
VStack{
Image("logo")
.padding(.bottom, 40)
}
}
}
struct LoginView: View {
#State var username: String = ""
#State var password: String = ""
#State var isLoggedin = false
var body: some View {
VStack{
Form{
TextField("Username: ", text:$username)
.frame(maxWidth: .infinity, alignment: .center)
.autocapitalization(.none)
SecureField("Password: ",text:$password)
Button("Login"){
DoLogin(username: &username, password: &password)
}
}
.padding(.top, 100)
}
}
}
struct MainMenu: View{
#State var isLoggedin = true
var body: some View{
VStack{
Text("Main Menu")
}
}
}
/*struct ContentView_Previews: PreviewProvider {
static var previews: some View {
/*ContentView() */
}
}*/
You have some problems with your code.
In your Content view
#State var isLoggedin = false
isn't being changed by anything inside the body of the struct, so it is always going to be false.
Your LoginView calls doLogin but it doesn't change any variables that the views use to render themselves. In the body of your doLogin method it is returning views, but it isn't returning them to anything.
Here is an example that does sort of what you want. shows different screens depending on state. SwiftUI shows views depending on states, so you need to change states to show different views. I've done this in one file so it's easier to show here:
import SwiftUI
class ContentViewModel: ObservableObject {
enum ViewState {
case initial
case loading
case login
case menu
}
#Published var username = ""
#Published var password = ""
#Published var viewState = ViewState.initial
var loginButtonDisabled: Bool {
username.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ||
password.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
func goToLogin() {
viewState = .login
}
func login() {
viewState = .loading
// I'm not actually logging in, just randomly simulating either a successful or unsuccessful login after a short delay
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if Bool.random() {
self.viewState = .menu
} else {
self.viewState = .login
}
}
}
}
struct ContentView: View {
#StateObject var viewModel = ContentViewModel()
var body: some View {
ZStack {
initialView
loginView
loadingView
menuView
}
}
private var initialView: InitialView? {
guard .initial == viewModel.viewState else { return nil }
return InitialView(viewModel: viewModel)
}
private var loginView: LoginView? {
guard .login == viewModel.viewState else { return nil }
return LoginView(viewModel: viewModel)
}
private var loadingView: LoadingView? {
guard .loading == viewModel.viewState else { return nil }
return LoadingView()
}
private var menuView: MenuView? {
guard .menu == viewModel.viewState else { return nil }
return MenuView()
}
}
struct InitialView: View {
#ObservedObject var viewModel: ContentViewModel
var body: some View {
VStack {
Text("Initial View")
.font(.largeTitle)
.padding()
Button("Login") { viewModel.goToLogin() }
}
}
}
struct LoginView: View {
#ObservedObject var viewModel: ContentViewModel
var body: some View {
VStack {
Text("Login View")
.font(.largeTitle)
.padding()
TextField("Username", text: $viewModel.username)
.padding()
TextField("Password", text: $viewModel.password)
.padding()
Button("Login") {viewModel.login() }
.padding()
.disabled(viewModel.loginButtonDisabled)
}
}
}
struct LoadingView: View {
var body: some View {
Text("Loading View")
.font(.largeTitle)
}
}
struct MenuView: View {
var body: some View {
Text("Menu View")
.font(.largeTitle)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This is all driven off one view model which publishes an enum state that is used by ContentView to show the different views. This is possible because in groups (such as the ZStack), a nil view is not rendered.
You can clone a project with this from https://github.com/Abizern/SO-68407322

SwiftUI: #State property not updated without weird workaround

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
}))
}
}
}
}

How to add an observable property when other properties change

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.

Dismiss SwiftUI modal "sheets" from within a List

I'm trying to present a modal within a List. Modals have changed many times in the betas, but I believe I'm using the correct/latest way (as of Xcode Beta 7). However, when I'm triggering the modal from within a list, for each list item it opens once but then never again.
import SwiftUI
//This works perfectly fine
struct BasicTest : View {
struct SampleObject: Identifiable {
var id = UUID()
var name: String
}
let sampleObjects = [
SampleObject(name: "Buffalo"),
SampleObject(name: "Italy"),
SampleObject(name: "Portland"),
]
#State private var showModal:Bool = false
var body: some View {
Button(action: {
print("Modal to open")
self.showModal = true
}) {
Text("Show Modal")
}.sheet(isPresented: self.$showModal)
{
TestDetailView(name: "Example")
}
}
}
#if DEBUG
struct BasicTest_Previews: PreviewProvider {
static var previews: some View {
BasicTest()
}
}
#endif
//this one only opens once
struct ListTest : View {
struct SampleObject: Identifiable {
var id = UUID()
var name: String
}
let sampleObjects = [
SampleObject(name: "Buffalo"),
SampleObject(name: "Italy"),
SampleObject(name: "Portland"),
]
#State private var showModal:Bool = false
var body: some View {
List {
ForEach(sampleObjects) {
item in
Button(action: {
print("Modal to open")
self.showModal = true
}) {
Text("Show Modal for \(item.name)")
}.sheet(isPresented: self.$showModal)
{
TestDetailView(name: item.name)
}
}
}
}
}
#if DEBUG
struct ListTest_Previews: PreviewProvider {
static var previews: some View {
ListTest()
}
}
#endif
struct TestDetailView: View {
#Environment(\.presentationMode) var presentationMode
var name: String
var body: some View {
VStack {
Button(action: {
print("Button clicked")
self.presentationMode.wrappedValue.dismiss()
}) {
Image(systemName: "chevron.compact.down").font(Font.system(.largeTitle).bold())
}
Text(name)
}
}
}
No error messages exist that I can see, just a flash when trying to open a second time.
It’ll work if you create a view for list row and move .sheet() there