In SwiftUI how can I animate a Published View change? - swift

I would like to animate a View change.
In particular, I have done a View where one of its children is a changing View.
struct MasterView: View {
#State var playerLeft: Bool = false
#ObservedObject var viewModel: MasterViewModel
var body: some View {
ZStack {
WaitPlayerBackView(isShowing: self.$playerLeft) {
self.viewModel.currentView
}
}
}
}
The view model is an ObservableObject class. currentView is changed by a method called from an async thread, like this:
class MasterViewModel: ObservableObject {
#Published var currentView: AnyView = AnyView(EmptyView())
func changeView() {
self.currentView = AnyView(NightView())
}
}
It's like a copy of Android fragments (pardon the comparison).
How can I animate this View change?
I have tried different options like:
self.viewModel.currentView.animate(.default)
or
struct MasterView: View {
#State var playerLeft: Bool = false
#ObservedObject var viewModel: MasterViewModel
var body: some View {
ZStack {
WaitPlayerBackView(isShowing: self.$playerLeft) {
self.viewModel.currentView
}
}.animate(self.viewModel.animate ? .easeIn(duration: 1) : .none)
}
}
class MasterViewModel: ObservableObject {
#Published var currentView: AnyView = AnyView(EmptyView())
#Published var animate = false
func changeView() {
self.animate = false
self.currentView = AnyView(NightView())
self.animate = true
}
}
However, none of them worked.
This is how I see a transition when changing the instance of currentView.

Moving your view initialization to MasterView makes everything easier:
struct MasterView: View {
#State var playerLeft: Bool = false
#ObservedObject var viewModel: MasterViewModel
var body: some View {
ZStack {
VStack { //this represents WaitPlayerBackView
if viewModel.day { //here are the posibles views that self.viewModel.currentView could have
Text("DayView").transition(AnyTransition.opacity.animation(.easeInOut(duration: 1.0)))
} else {
Text("NightView").transition(AnyTransition.opacity.animation(.(duration: 1.0)))
}
}
}
}
}
class MasterViewModel: ObservableObject {
#Published var day: Bool = false
init () {
_ = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(changeView), userInfo: nil, repeats: true) //Only for testing
}
#objc func changeView() { //#objc is only for testing
day.toggle()
}
}

Related

SwiftUI: How to update textfield with live changes

In my content view i have function that detects whenever a user copies a website address
ContentView
#State private var detectedurl = ""
.................
.onAppear {
urlclipboardwatcher()
}
func urlclipboardwatcher() {
let pasteboard = NSPasteboard.general
var changeCount = NSPasteboard.general.changeCount
Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { _ in
if let copiedString = pasteboard.string(forType: .string) {
...............
if copiedString.starts(with: "https://") {
detectedurl = copiedString
}
}
}
}
I want to pass this value to the textfield in my NewBookmark View. How do i update the textfield with any changes that happen with the pasteboard?
struct NewBookmark: View {
#Binding var detectedurl: String
#ObservedObject private var vm: AddNewBookmarkViewModel
init(vm: AddNewBookmarkViewModel, detectedurl: Binding<String>) {
self.vm = vm
self._detectedurl = detectedurl
}
TextField("Enter a URL", text: $vm.url)
// i want the detected url to automatically populate this textfield
Button("Save") {
vm.save()
}.disabled(vm.url.isEmpty)
AddBookMarkViewModel
class AddNewBookmarkViewModel: ObservableObject {
#Published var url: String = ""
.............
func save() {
do {
let myBM = MyBookmark(context: context)
myBM.url = url
try myBM.save()
} catch {
print(error)
}
}
}
Tbh, I am not really sure how the code which you posted works. But I did something similar in the past. Maybe it helps.
What I basically did is, one viewModel with two views. Both views hold on to the viewModel PasteboardViewModel. PasteboardViewModel is a StateObject which is passed on two the second view via. environmentObject. And url variable in the viewModel is bound to the PasteboardView. So every time this Publisher changes the TextField does it too.
struct ContentView: View {
#StateObject var viewModel: PasteboardViewModel = .init()
var body: some View {
VStack {
.....
PasteboardView()
.environmentObject(viewModel)
}
.onAppear {
viewModel.watchPasteboard()
}
.padding()
}
}
struct PasteboardView: View {
#EnvironmentObject var viewModel: PasteboardViewModel
var body: some View {
TextField(text: $viewModel.url) {
Text("Test")
}
}
}
class PasteboardViewModel: ObservableObject {
#Published var url: String = ""
func watchPasteboard() {
let pasteboard = UIPasteboard.general
var changeCount = UIPasteboard.general.changeCount
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
if let copiedString = pasteboard.string {
if pasteboard.changeCount != changeCount {
self.url = copiedString
changeCount = pasteboard.changeCount
}
}
}
}
}

SwitUI parent child binding: #Published in #StateObject doesn't work while #State does

I have a custom modal structure coming from this question (code below). Some property is modified in the modal view and is reflected in the source with a Binding. The catch is that when the property is coming from a #StateObject + #Published the changes are not reflected back in the modal view. It's working when using a simple #State.
Minimal example (full code):
class Model: ObservableObject {
#Published var selection: String? = nil
}
struct ParentChildBindingTestView: View {
#State private var isPresented = false
// not working with #StateObject
#StateObject private var model = Model()
// working with #State
// #State private var selection: String? = nil
var body: some View {
VStack(spacing: 20) {
Button("Show child", action: { isPresented = true })
Text("selection: \(model.selection ?? "nil")") // replace: selection
}
.modalBottom(isPresented: $isPresented, view: {
ChildView(selection: $model.selection) // replace: $selection
})
}
}
struct ChildView: View {
#Environment(\.dismissModal) var dismissModal
#Binding var selection: String?
var body: some View {
VStack {
Button("Dismiss", action: { dismissModal() })
VStack(spacing: 0) {
ForEach(["Option 1", "Option 2", "Option 3", "Option 4"], id: \.self) { choice in
Button(action: { selection = choice }) {
HStack(spacing: 12) {
Circle().fill(choice == selection ? Color.purple : Color.black)
.frame(width: 26, height: 26, alignment: .center)
Text(choice)
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
}
.padding(50)
.background(Color.gray)
}
}
extension View {
func modalBottom<Content: View>(isPresented: Binding<Bool>, #ViewBuilder view: #escaping () -> Content) -> some View {
onChange(of: isPresented.wrappedValue) { isPresentedValue in
if isPresentedValue == true {
present(view: view(), dismissCallback: { isPresented.wrappedValue = false })
}
else {
topMostController().dismiss(animated: false)
}
}
.onAppear {
if isPresented.wrappedValue {
present(view: view(), dismissCallback: { isPresented.wrappedValue = false })
}
}
}
fileprivate func present<Content: View>(view: Content, dismissCallback: #escaping () -> ()) {
DispatchQueue.main.async {
let topMostController = self.topMostController()
let someView = VStack {
Spacer()
view
.environment(\.dismissModal, dismissCallback)
}
let viewController = UIHostingController(rootView: someView)
viewController.view?.backgroundColor = .clear
viewController.modalPresentationStyle = .overFullScreen
topMostController.present(viewController, animated: false, completion: nil)
}
}
}
extension View {
func topMostController() -> UIViewController {
var topController: UIViewController = UIApplication.shared.windows.first!.rootViewController!
while (topController.presentedViewController != nil) {
topController = topController.presentedViewController!
}
return topController
}
}
private struct ModalDismissKey: EnvironmentKey {
static let defaultValue: () -> Void = {}
}
extension EnvironmentValues {
var dismissModal: () -> Void {
get { self[ModalDismissKey.self] }
set { self[ModalDismissKey.self] = newValue }
}
}
struct ParentChildBindingTestView_Previews: PreviewProvider {
static var previews: some View {
ZStack {
ParentChildBindingTestView()
}
}
}
The changes are reflected properly when replacing my custom structure with a fullScreenCover, so the problem comes from there. But I find it surprising that it works with a #State and not with a #StateObject + #Published. I thought those were identical.
If having #StateObject is a must for your code, and your ChildView has to update the data back to its ParentView, then you can still make this works around #StateObject.
Something like this:
struct Parent: View {
#StateObject var h = Helper()
var body: some View {
TextField("edit child view", text: $h.helper)
Child(helper: $h.helper)
}
}
struct Child: View {
#Binding var helper: String
var body: some View {
Text(helper)
}
}
class Helper: ObservableObject {
#Published var helper = ""
}
I think your can get anwser here
with #State we use onChange because it uses for only current View
with #Published we use onReceive because it uses for many Views
#State should be used with #Binding
#StateObject with #ObservedObject
In your case, you would pass the model to the child view and update it's properties there.

SwiftUI: #Environment(\.presentationMode)'s dismiss not working in iOS14

I have a view that shows a sheet for filtering the items in a list. The view has this var:
struct JobsTab: View {
#State private var jobFilter: JobFilter = JobFilter()
var filter: some View {
Button {
self.showFilter = true
} label: {
Image(systemName: "line.horizontal.3.decrease.circle")
.renderingMode(.original)
}
.sheet(isPresented: $showFilter) {
FilterView($jobFilter, categoriesViewModel, jobsViewModel)
}
}
However, in the sheet, I'm trying the following and I can't make the view dismissed when clicking on the DONE button, only on the CANCEL button:
struct FilterView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var categoriesViewModel: CategoriesViewModel
#ObservedObject var jobsViewModel: JobsViewModel
let filterViewModel: FilterViewModel
#Binding var jobFilter: JobFilter
#State private var draft: JobFilter
#State var searchText = ""
init(_ jobFilter: Binding<JobFilter>, _ categoriesViewModel: CategoriesViewModel, _ jobsViewModel: JobsViewModel) {
_jobFilter = jobFilter
_draft = State(wrappedValue: jobFilter.wrappedValue)
self.categoriesViewModel = categoriesViewModel
self.jobsViewModel = jobsViewModel
self.filterViewModel = FilterViewModel()
}
...
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("FilterView.Button.Cancel.Text".capitalizedLocalization) {
presentationMode.wrappedValue.dismiss()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("FilterView.Button.Done.Text".capitalizedLocalization) {
let request = Job.defaultRequest()
request.predicate = filterViewModel.buildPredicate(withJobFilterDraft: self.draft)
request.sortDescriptors = [NSSortDescriptor(key: #keyPath(Job.publicationDate), ascending: false)]
jobsViewModel.filteredJobsFetchRequest = request
self.jobFilter = self.draft
presentationMode.wrappedValue.dismiss()
}
}
}
I have also tried with a #Binding like Paul says here but there's no luck.
Is there any workaround, or am I doing something wrong?
Thanks in advance!
EDIT: I've posted the properties of both views, because I think the problem comes from the the line in FilterView self.jobFilter = self.draft.
What I'm trying to do here is create a filter view, and the aforementioned line will be executed when the user presses the DONE button: I want to assign my binding jobFilter in the JobsTab the value of the FilterView source of truth (which is a #State) and probably, since I'm updating the binding jobFilter the FilterView is being shown again even though the $showFilter is false? I don't know to be honest.
EDIT2: I have also tried
``
if #available(iOS 15.0, *) {
let _ = Self._printChanges()
} else {
// Fallback on earlier versions
}
in both `FilterView` and its called `JobTabs` and in both, I get the same result: unchanged
According to your code, I assumed your FilterView() is not a sub view, but an independent view by its own.
Therefore, to make sure "presentationMode.wrappedValue.dismiss()" works, you don't need to create #Binding or #State variables outside the FilerView() for passing the data back and forth between different views. Just create one variable inside your FilterView() to make it works.
I don't have your full code, but I created a similar situation to your problem as below code:
import SwiftUI
struct Main: View {
#State private var showFilter = false
var body: some View {
Button {
self.showFilter = true
} label: {
Image(systemName: "line.horizontal.3.decrease.circle")
.renderingMode(.original)
}
.sheet(isPresented: $showFilter) {
FilterView()
}
}
}
struct FilterView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
NavigationView {
VStack {
Text("Filter View")
}.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("cancel")
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("okay")
}
}
}
}
}
}
the parameter onDismiss is missing:
.sheet(isPresented: $showFilter, onDismiss: { isPresented = false }) { ... }
View model objects are usually a source of bugs because we don't use those in SwiftUI instead we use structs for speed and consistency. I recommend making an #State struct containing a bool property isSheetPresented and also the data the sheet needs. Add a mutating func and call it on the struct from the button event. Pass a binding to the struct into the sheet's view where you can call another mutating function to set the bool to false. Something like this:
struct SheetConfig {
var isPresented = false
var data: [String] = []
mutating func show(initialData: [String] ) {
isPresented = true
data = initialData
}
mutating func hide() {
isPresented = false
}
}
struct ContentView: View {
#State var config = SheetConfig()
var body: some View {
Button {
config.show(intialData: ...)
} label: {
}
.sheet(isPresented: $config.isPresented) {
FilterView($config)
}
Here is what I did to handle this issue.
Create a new protocol to avoid repeatations.
public protocol UIViewControllerHostedView where Self: View {
/// A method which should be triggered whenever dismiss is needed.
/// - Note: Since `presentationMode & isPresented` are not working for presented UIHostingControllers on lower iOS versions than 15. You must call, this method whenever you want to dismiss the presented SwiftUI.
func dismissHostedView(presentationMode: Binding<PresentationMode>)
}
public extension UIViewControllerHostedView {
func dismissHostedView(presentationMode: Binding<PresentationMode>) {
// Note: presentationMode & isPresented are not working for presented UIHostingControllers on lower iOS versions than 15.
if #available(iOS 15, *) {
presentationMode.wrappedValue.dismiss()
} else {
self.topViewController?.dismisOrPopBack()
}
}
}
Extend UIWindow and UIApplication
import UIKit
public extension UIWindow {
// Credits: - https://gist.github.com/matteodanelli/b8dcdfef39e3417ec7116a2830ff67cf
func visibleViewController() -> UIViewController? {
if let rootViewController: UIViewController = self.rootViewController {
return UIWindow.getVisibleViewControllerFrom(vc: rootViewController)
}
return nil
}
class func getVisibleViewControllerFrom(vc:UIViewController) -> UIViewController {
switch(vc){
case is UINavigationController:
let navigationController = vc as! UINavigationController
return UIWindow.getVisibleViewControllerFrom( vc: navigationController.visibleViewController!)
case is UITabBarController:
let tabBarController = vc as! UITabBarController
return UIWindow.getVisibleViewControllerFrom(vc: tabBarController.selectedViewController!)
default:
if let presentedViewController = vc.presentedViewController {
if let presentedViewController2 = presentedViewController.presentedViewController {
return UIWindow.getVisibleViewControllerFrom(vc: presentedViewController2)
}
else{
return vc;
}
}
else{
return vc;
}
}
}
}
#objc public extension UIApplication {
/// LCUIComponents: Returns the current visible top most window of the app.
var topWindow: UIWindow? {
return windows.first(where: { $0.isKeyWindow })
}
var topViewController: UIViewController? {
return topWindow?.visibleViewController()
}
}
Extend View to handle UIHostingController presented View`
public extension View {
weak var topViewController: UIViewController? {
UIApplication.shared.topViewController
}
}
Finally, use the helper method in your SwiftUI view as follows: -
struct YourView: View, UIViewControllerHostedView {
// MARK: - Properties
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body some View {
Button {
// Note: this part trigger a method in a created protocol above.
dismissHostedView(presentationMode: presentationMode)
} label: {
Text("Tap to dismiss")
}
}
In the sheet try adding this instead.
#Environment(\.dismiss) var dismiss // EnvironmentValue
Button("Some text") {
// Code
dismiss()
}

Two way binding to multiple instances

I have a view with a ForEach with multiple instances of another view. I want to be able to:
Click a button in the main view and trigger validation on the nested views, and
On the way back, populate an array with the results of the validations
I've simplified the project so it can be reproduced. Here's what I have:
import SwiftUI
final class AddEditItemViewModel: ObservableObject {
#Published var item : String
#Published var isValid : Bool
#Published var doValidate: Bool {
didSet{
print(doValidate) // This is never called
validate()
}
}
init(item : String, isValid : Bool, validate: Bool) {
self.item = item
self.isValid = isValid
self.doValidate = validate
}
func validate() { // This is never called
isValid = Int(item) != nil
}
}
struct AddEditItemView: View {
#ObservedObject var viewModel : AddEditItemViewModel
var body: some View {
Text(viewModel.item)
}
}
final class AddEditProjectViewModel: ObservableObject {
let array = ["1", "2", "3", "nope"]
#Published var countersValidationResults = [Bool]()
#Published var performValidation = false
init() {
for _ in array {
countersValidationResults.append(false)
}
}
}
struct ContentView: View {
#ObservedObject var viewModel : AddEditProjectViewModel
#State var result : Bool = false
var body: some View {
VStack {
ForEach(
viewModel.countersValidationResults.indices, id: \.self) { i in
AddEditItemView(viewModel: AddEditItemViewModel(
item: viewModel.array[i],
isValid: viewModel.countersValidationResults[i],
validate: viewModel.performValidation
)
)
}
Button(action: {
viewModel.performValidation = true
result = viewModel.countersValidationResults.filter{ $0 == false }.count == 0
}) {
Text("Validate")
}
Text("All is valid: \(result.description)")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(viewModel: AddEditProjectViewModel())
}
}
When I change the property in the main view, the property doesn't change in the nested views, even though it's a #Published property.
Since this first step is not working, I haven't even been able to test the second part (updating the array of books with the validation results)
I need the setup to be like this because if an item is not valid, that view will show an error message, so the embedded views need to know whether they are valid or not.
UPDATE:
My issue was that you can't seem to be able to store Binding objects in view models, only in views, so I moved my properties to the view, and it works:
import SwiftUI
final class AddEditItemViewModel: ObservableObject {
#Published var item : String
init(item : String) {
self.item = item
print("item",item)
}
func validate() -> Bool{
return Int(item) != nil
}
}
struct AddEditItemView: View {
#ObservedObject var viewModel : AddEditItemViewModel
#Binding var doValidate: Bool
#Binding var isValid : Bool
init(viewModel: AddEditItemViewModel, doValidate:Binding<Bool>, isValid : Binding<Bool>) {
self.viewModel = viewModel
self._doValidate = doValidate
self._isValid = isValid
}
var body: some View {
Text("\(viewModel.item): \(isValid.description)").onChange(of: doValidate) { _ in isValid = viewModel.validate() }
}
}
struct ContentView: View {
#State var performValidation = false
#State var countersValidationResults = [false,false,false,false] // had to hard code this here
#State var result : Bool = false
let array = ["1", "2", "3", "nope"]
// init() {
// for _ in array {
// countersValidationResults.append(false) // For some weird reason this appending doesn't happen!
// }
// }
var body: some View {
VStack {
ForEach(array.indices, id: \.self) { i in
AddEditItemView(viewModel: AddEditItemViewModel(item: array[i]), doValidate: $performValidation, isValid: $countersValidationResults[i])
}
Button(action: {
performValidation.toggle()
result = countersValidationResults.filter{ $0 == false }.count == 0
}) {
Text("Validate")
}
Text("All is valid: \(result.description)")
Text(countersValidationResults.description)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I'm having trouble reconciling the question with the example code and figuring out what's supposed to be happening. Think that there are a few issues going on.
didSet will not get called on #Published properties. You can (SwiftUI - is it possible to get didSet to fire when changing a #Published struct?) but the gist is that it's not a normal property, because of the #propertyWrapper around it
You say in your question that you want a "binding", but you never in fact us a Binding. If you did want to bind the properties together, you should look into using either #Binding or creating a binding without the property wrapper. Here's some additional reading on that: https://swiftwithmajid.com/2020/04/08/binding-in-swiftui/
You have some circular logic in your example code. Like I said, it's a little hard to figure out what's a symptom of the code and what you're really trying to achieve. Here's an example that strips away a lot of the extraneous stuff going on and functions:
struct AddEditItemView: View {
var item : String
var isValid : Bool
var body: some View {
Text(item)
}
}
final class AddEditProjectViewModel: ObservableObject {
let array = ["1", "2", "3"]// "nope"]
#Published var countersValidationResults = [Bool]()
init() {
for _ in array {
countersValidationResults.append(false)
}
}
func validate(index: Int) { // This is never called
countersValidationResults[index] = Int(array[index]) != nil
}
}
struct ContentView: View {
#ObservedObject var viewModel : AddEditProjectViewModel
#State var result : Bool = false
var body: some View {
VStack {
ForEach(
viewModel.countersValidationResults.indices, id: \.self) { i in
AddEditItemView(item: viewModel.array[i], isValid: viewModel.countersValidationResults[i])
}
Button(action: {
viewModel.array.enumerated().forEach { (index,_) in
viewModel.validate(index: index)
}
result = viewModel.countersValidationResults.filter{ $0 == false }.count == 0
}) {
Text("Validate")
}
Text("All is valid: \(result.description)")
}
}
}
Note that it your array, if you include the "nope" item, not everything validates, since there's a non-number, and if you omit it, everything validates.
In your case, there really wasn't the need for that second view model on the detail view. And, if you did have it, at least the way you had things written, it would have gotten you into a recursive loop, as it would've validated, then refreshed the #Published property on the parent view, which would've triggered the list to be refreshed, etc.
If you did get in a situation where you needed to communicate between two view models, you can do that by passing a Binding to the parent's #Published property by using the $ operator:
class ViewModel : ObservableObject {
#Published var isValid = false
}
struct ContentView : View {
#ObservedObject var viewModel : ViewModel
var body: some View {
VStack {
ChildView(viewModel: ChildViewModel(isValid: $viewModel.isValid))
}
}
}
class ChildViewModel : ObservableObject {
var isValid : Binding<Bool>
init(isValid: Binding<Bool>) {
self.isValid = isValid
}
func toggle() {
isValid.wrappedValue.toggle()
}
}
struct ChildView : View {
#ObservedObject var viewModel : ChildViewModel
var body: some View {
VStack {
Text("Valid: \(viewModel.isValid.wrappedValue ? "true" : "false")")
Button(action: {
viewModel.toggle()
}) {
Text("Toggle")
}
}
}
}

How to implement derived property using Combine?

I have an observable object with two properties
class Demo: ObservableObject {
#Published var propertyA: Bool
#Published var propertyB: Bool
}
Now I want to add a derived property "propertyC" that is "true" if both propertyA and propertyB are true.
I found similar questions with answers that didn't satisfy me. I'm looking for a solution that uses the Combine Framework and not a "didSet" method as my real world project computes the derived property from more than two other properties.
When I'm using the derived propertyC in a SwiftUI view it should trigger a refresh whenever propertyA or propertyB changes even if I don't use those in the view.
Here is possible approach (tested with Xcode 11.2 / iOS 13.2)
class Demo: ObservableObject {
#Published var propertyA: Bool = false
#Published var propertyB: Bool = false
#Published var propertyC: Bool = false
private var subscribers = Set<AnyCancellable>()
init() {
Publishers.CombineLatest(_propertyA.projectedValue, _propertyB.projectedValue)
.receive(on: RunLoop.main)
.map { $0 && $1 }
.assign(to: \.propertyC, on: self)
.store(in: &subscribers)
}
}
// view for testing, works in Preview as well
struct FastDemoTest: View {
#ObservedObject var demo = Demo()
var body: some View {
VStack {
Button("Toggle A") { self.demo.propertyA.toggle() }
Button("Toggle B") { self.demo.propertyB.toggle() }
Divider()
Text("Result of C: \( demo.propertyC ? "true" : "false" )")
}
}
}
All credits should go Rob (see his notes to this question)
class Demo: ObservableObject {
#Published var propertyA: Bool = false
#Published var propertyB: Bool = false
var propertyC: Bool {
propertyA && propertyB
}
}
struct ContentView: View {
#ObservedObject var demo = Demo()
var body: some View {
VStack {
Button("Toggle A = \(demo.propertyA.description)") { self.demo.propertyA.toggle() }
Button("Toggle B = \(demo.propertyB.description)") { self.demo.propertyB.toggle() }
Divider()
Text("Result of C: \(demo.propertyC.description)")
}
}
}